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
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, readFile, readdir } from 'node:fs/promises';
3
3
  import { join, resolve, isAbsolute, relative, sep } from 'node:path';
4
4
  import { parseDocument } from 'yaml';
5
- import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.CKXM1jnE.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.BaNK8yfw.mjs';
6
6
  import 'node:module';
7
7
  import 'node:url';
8
8
  import '@nuxt/kit';
@@ -14,6 +14,7 @@ import 'node:fs';
14
14
  import '../../dist/runtime/llms-txt-files.js';
15
15
  import '../../dist/runtime/route-path.js';
16
16
  import '../../dist/runtime/server/utils/sitemap-md.js';
17
+ import '../../dist/runtime/sync-secret.js';
17
18
  import '../../dist/runtime/cache-control.js';
18
19
  import '../../dist/runtime/server/utils/discovery-response.js';
19
20
  import 'confbox';
@@ -6,7 +6,7 @@ import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
7
7
  import { collectSitemap } from 'sitemapd/parse';
8
8
  import { parseURL, withLeadingSlash, joinURL, withBase } from 'ufo';
9
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.CKXM1jnE.mjs';
9
+ import { l as logger, m as matchingStaticContentSignals, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.BaNK8yfw.mjs';
10
10
  import { toLogicalRoute, toDeployedRoute } from '../../dist/runtime/route-path.js';
11
11
  import { insertLlmsTxtDirective } from '../../dist/runtime/llms-txt-directive.js';
12
12
  import { absoluteSiteUrl } from '../../dist/runtime/markdown-links.js';
@@ -15,7 +15,6 @@ import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump
15
15
  import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../../dist/runtime/server/utils/llms-full.js';
16
16
  import { resolvePageUpdatedAt } from '../../dist/runtime/server/utils/page-updated-at.js';
17
17
  import { SITEMAP_MD_ROUTE, appendSitemapSection } from '../../dist/runtime/server/utils/sitemap-md.js';
18
- import 'node:crypto';
19
18
  import 'node:module';
20
19
  import 'node:url';
21
20
  import 'defu';
@@ -24,7 +23,9 @@ import 'nuxtseo-shared/kit';
24
23
  import '../shared/nuxt-ai-ready.B3hAuOLQ.mjs';
25
24
  import 'node:fs';
26
25
  import '../../dist/runtime/llms-txt-files.js';
26
+ import '../../dist/runtime/sync-secret.js';
27
27
  import '../../dist/runtime/cache-control.js';
28
+ import 'node:crypto';
28
29
  import '../../dist/runtime/server/utils/discovery-response.js';
29
30
  import 'confbox';
30
31
  import 'nuxtseo-shared/server';
@@ -497,6 +498,17 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras)
497
498
  initPromise = initCrawler(state);
498
499
  await initPromise;
499
500
  const parsed = JSON.parse(route.contents || "{}");
501
+ if (parsed.contentSignal !== void 0) {
502
+ route.contentSignal = parsed.contentSignal;
503
+ for (const sourceRoute of pageRoute === "/" ? ["/"] : [pageRoute, `${pageRoute}/`]) {
504
+ if (matchingStaticContentSignals(nitro.options.routeRules, sourceRoute).includes(parsed.contentSignal))
505
+ continue;
506
+ const source = nitro.options.routeRules[sourceRoute] ||= {};
507
+ source.headers ||= {};
508
+ if (!Object.keys(source.headers).some((name) => name.toLowerCase() === "content-signal"))
509
+ source.headers["Content-Signal"] = parsed.contentSignal;
510
+ }
511
+ }
500
512
  if (!state.prerenderedRoutes.has(pageRoute))
501
513
  await processMarkdownRoute(state, nuxt, pageRoute, parsed);
502
514
  const withSitemap = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, absoluteSiteUrl(toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL), siteInfo?.url));
package/dist/cli.mjs CHANGED
@@ -5,7 +5,9 @@ import { defineCommand, runMain } from 'citty';
5
5
  import { consola } from 'consola';
6
6
  import { colors } from 'consola/utils';
7
7
  import { resolve, join } from 'pathe';
8
+ import { joinURL } from 'ufo';
8
9
  import { r as readPackageMetadata } from './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
10
+ import { deriveRuntimeSyncSecret, LEGACY_RUNTIME_SYNC_SECRET_WARNING } from '../dist/runtime/sync-secret.js';
9
11
  import 'node:module';
10
12
  import 'node:path';
11
13
  import 'node:url';
@@ -245,6 +247,14 @@ async function auditSite(options, dependencies) {
245
247
  }
246
248
 
247
249
  async function getSecret(cwd) {
250
+ const root = process.env.NITRO_APP_SECRET ?? process.env.NUXT_APP_SECRET;
251
+ if (root !== void 0 && root !== "")
252
+ return deriveRuntimeSyncSecret(root);
253
+ const legacy = process.env.NUXT_NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET;
254
+ if (legacy) {
255
+ console.warn(`[nuxt-ai-ready] ${LEGACY_RUNTIME_SYNC_SECRET_WARNING}`);
256
+ return legacy;
257
+ }
248
258
  const secretPath = join(cwd, "node_modules/.cache/nuxt/ai-ready/secret");
249
259
  if (!existsSync(secretPath)) {
250
260
  return null;
@@ -256,10 +266,10 @@ async function getSecret(cwd) {
256
266
  function authHeaders(secret) {
257
267
  return { Authorization: `Bearer ${secret}` };
258
268
  }
259
- async function requireSecret(cwd, hint = "") {
269
+ async function requireSecret(cwd) {
260
270
  const secret = await getSecret(cwd);
261
271
  if (!secret) {
262
- consola.error(`No secret found. Run \`nuxi dev\` or \`nuxi build\` first${hint}.`);
272
+ throw new Error("No secret found. Set NUXT_APP_SECRET, or run `nuxt dev` to cache its development token.");
263
273
  }
264
274
  return secret;
265
275
  }
@@ -287,6 +297,15 @@ const main = defineCommand({
287
297
  version: await readPackageMetadata(import.meta.url).then((p) => p.version || "0.0.0")
288
298
  },
289
299
  subCommands: {
300
+ token: () => defineCommand({
301
+ meta: { name: "token", description: "Print the runtime sync bearer token" },
302
+ args: { cwd: { type: "string", description: "Working directory", default: "." } },
303
+ async run({ args }) {
304
+ const secret = await requireSecret(resolve(args.cwd));
305
+ process.stdout.write(`${secret}
306
+ `);
307
+ }
308
+ }),
290
309
  audit: () => defineCommand({
291
310
  meta: { name: "audit", description: "Check a public page and its agent discovery documents" },
292
311
  args: {
@@ -332,10 +351,10 @@ const main = defineCommand({
332
351
  },
333
352
  async run({ args }) {
334
353
  const cwd = resolve(args.cwd || ".");
335
- const secret = await requireSecret(cwd, " to generate one");
354
+ const secret = await requireSecret(cwd);
336
355
  if (!secret)
337
356
  return;
338
- const url = `${args.url}/__ai-ready/status`;
357
+ const url = joinURL(args.url, "/__ai-ready/status");
339
358
  consola.info(`Fetching status from ${args.url}...`);
340
359
  const res = await fetchJson(url, { headers: authHeaders(secret) }, "Failed to connect");
341
360
  consola.box("AI Ready Status");
@@ -419,7 +438,7 @@ const main = defineCommand({
419
438
  } else {
420
439
  params.set("limit", args.limit || "10");
421
440
  }
422
- const url = `${args.url}/__ai-ready/poll?${params}`;
441
+ const url = joinURL(args.url, `/__ai-ready/poll?${params}`);
423
442
  consola.info(`Triggering poll at ${args.url}...`);
424
443
  const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
425
444
  consola.success(`Indexed: ${colors.green(res.indexed?.toString() || "0")} pages`);
@@ -465,7 +484,7 @@ const main = defineCommand({
465
484
  if (!args.clear) {
466
485
  params.set("clear", "false");
467
486
  }
468
- const url = `${args.url}/__ai-ready/restore?${params}`;
487
+ const url = joinURL(args.url, `/__ai-ready/restore?${params}`);
469
488
  consola.info(`Restoring database at ${args.url}...`);
470
489
  const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
471
490
  consola.success(`Restored: ${colors.green(res.restored?.toString() || "0")} pages`);
@@ -505,15 +524,14 @@ const main = defineCommand({
505
524
  const cwd = resolve(args.cwd || ".");
506
525
  const secret = await getSecret(cwd);
507
526
  if (!secret && !args.dry) {
508
- consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
509
- return;
527
+ throw new Error("No secret found. Set NUXT_APP_SECRET, or run `nuxt dev` to cache its development token.");
510
528
  }
511
529
  const params = new URLSearchParams();
512
530
  if (args.dry)
513
531
  params.set("dry", "true");
514
532
  if (args.ttl)
515
533
  params.set("ttl", args.ttl);
516
- const url = `${args.url}/__ai-ready/prune?${params}`;
534
+ const url = joinURL(args.url, `/__ai-ready/prune?${params}`);
517
535
  consola.info(`${args.dry ? "Previewing" : "Pruning"} stale routes at ${args.url}...`);
518
536
  const res = await fetchJson(url, {
519
537
  method: "POST",
@@ -572,7 +590,7 @@ const main = defineCommand({
572
590
  params.set("route", args.route);
573
591
  if (!args.force)
574
592
  params.set("force", "false");
575
- const url = `${args.url}/__ai-ready/reindex?${params}`;
593
+ const url = joinURL(args.url, `/__ai-ready/reindex?${params}`);
576
594
  consola.info(`Reindexing ${args.route} at ${args.url}...`);
577
595
  const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
578
596
  if (res.indexed) {
package/dist/module.d.mts CHANGED
@@ -29,6 +29,7 @@ interface ParsedMarkdownResult {
29
29
  headings: Array<Record<string, string>>;
30
30
  keywords?: string[];
31
31
  updatedAt?: string;
32
+ contentSignal?: string;
32
33
  }
33
34
 
34
35
  interface ResolvedApiCatalogLinkTarget {
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.0 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "3.3.1",
7
+ "version": "3.5.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import 'node:crypto';
2
1
  import 'node:fs/promises';
3
2
  import 'node:module';
4
3
  import 'node:path';
@@ -7,13 +6,15 @@ import '@nuxt/kit';
7
6
  import 'defu';
8
7
  import 'nuxt-site-config/kit';
9
8
  import 'nuxtseo-shared/kit';
10
- export { m as default } from './shared/nuxt-ai-ready.CKXM1jnE.mjs';
9
+ export { a as default } from './shared/nuxt-ai-ready.BaNK8yfw.mjs';
11
10
  import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
12
11
  import '../dist/runtime/llms-txt-files.js';
13
12
  import '../dist/runtime/route-path.js';
14
13
  import '../dist/runtime/server/utils/sitemap-md.js';
14
+ import '../dist/runtime/sync-secret.js';
15
15
  import '../dist/runtime/server/utils/discovery-response.js';
16
16
  import '../dist/runtime/cache-control.js';
17
+ import 'node:crypto';
17
18
  import 'confbox';
18
19
  import '../dist/runtime/markdown-path.js';
19
20
  import 'nuxtseo-shared/server';
@@ -11,7 +11,7 @@ import { buildFrontmatter } from "../utils/frontmatter.js";
11
11
  import { extractKeywords } from "../utils/keywords.js";
12
12
  import { getMarkdownRenderInfo } from "../utils/markdown-request.js";
13
13
  import { agentSkillArtifactPaths } from "../utils/negotiation-response.js";
14
- import { consumePrerenderedHtml } from "../utils/prerender-html.js";
14
+ import { consumePrerenderedContentSignal, consumePrerenderedHtml } from "../utils/prerender-html.js";
15
15
  import { isSitemapMdRequest } from "../utils/sitemap-md.js";
16
16
  function extractHeadingsFromMarkdown(markdown) {
17
17
  const headings = [];
@@ -37,6 +37,7 @@ export default defineEventHandler(async (event) => {
37
37
  if (!renderInfo || "notAcceptable" in renderInfo)
38
38
  return;
39
39
  const { path } = renderInfo;
40
+ let contentSignal = consumePrerenderedContentSignal(path);
40
41
  const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
41
42
  const deployedPath = toDeployedRoute(path, fullRuntimeConfig.app.baseURL);
42
43
  const canonicalUrl = withSiteUrl(event, deployedPath, { withBase: true });
@@ -62,6 +63,7 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
62
63
  description: contentPage.description || "",
63
64
  headings,
64
65
  keywords: keywords2,
66
+ ...contentSignal !== void 0 && { contentSignal },
65
67
  // Only a real content date feeds the page index and sitemap lastmod.
66
68
  ...contentPage.updatedAt && { updatedAt: contentPage.updatedAt }
67
69
  });
@@ -95,8 +97,13 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
95
97
  });
96
98
  }
97
99
  html = await response.text();
100
+ const fetchedPolicy = response.headers.get("content-signal");
101
+ const sharedPolicy = fullRuntimeConfig["nuxt-robots"]?.contentSignalHeader;
102
+ if (fetchedPolicy !== null && fetchedPolicy !== sharedPolicy)
103
+ contentSignal = fetchedPolicy;
98
104
  logger.debug(`[markdown.prerender] Fetched HTML for ${path} (${html.length} bytes)`);
99
105
  consumePrerenderedHtml(path);
106
+ consumePrerenderedContentSignal(path);
100
107
  }
101
108
  if (html.includes("__NUXT_ERROR__") || html.includes("nuxt-error-page")) {
102
109
  return createError({
@@ -125,6 +132,7 @@ ${absoluteMarkdownLinks(contentPage.markdown, canonicalUrl)}`;
125
132
  description: result.description,
126
133
  headings: result.headings,
127
134
  keywords,
135
+ ...contentSignal !== void 0 && { contentSignal },
128
136
  ...result.updatedAt && { updatedAt: result.updatedAt }
129
137
  });
130
138
  });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
2
+ import { toLogicalRoute } from "../../route-path.js";
3
+ import { storePrerenderedContentSignal } from "../utils/prerender-html.js";
4
+ export default defineNitroPlugin((nitro) => {
5
+ const hooks = nitro.hooks;
6
+ hooks.hook("response", (response, event) => {
7
+ const policy = response.headers.get("content-signal");
8
+ const config = useRuntimeConfig();
9
+ const sharedPolicy = config["nuxt-robots"]?.contentSignalHeader;
10
+ if (policy !== null && policy !== sharedPolicy)
11
+ storePrerenderedContentSignal(toLogicalRoute(event.url.pathname, config.app.baseURL), policy);
12
+ });
13
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { getRequestURL, getResponseHeader } from "#nuxtseo/h3";
2
+ import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
3
+ import { toLogicalRoute } from "../../route-path.js";
4
+ import { storePrerenderedContentSignal } from "../utils/prerender-html.js";
5
+ export default defineNitroPlugin((nitro) => {
6
+ nitro.hooks.hook("beforeResponse", (event) => {
7
+ const policy = getResponseHeader(event, "content-signal");
8
+ const config = useRuntimeConfig(event);
9
+ const sharedPolicy = config["nuxt-robots"]?.contentSignalHeader;
10
+ if (typeof policy === "string" && policy !== sharedPolicy)
11
+ storePrerenderedContentSignal(toLogicalRoute(getRequestURL(event).pathname, config.app.baseURL), policy);
12
+ });
13
+ });
@@ -2,7 +2,7 @@ import { createError, eventHandler } from "#nuxtseo/h3";
2
2
  import { runCron } from "../../utils/runCron.js";
3
3
  export default eventHandler(async (event) => {
4
4
  const { requireAuth } = await import("../../utils/auth.js");
5
- requireAuth(event);
5
+ await requireAuth(event);
6
6
  const result = await runCron(event);
7
7
  if (result.failed) {
8
8
  throw createError({
@@ -5,7 +5,7 @@ import { logger } from "../../logger.js";
5
5
  import { batchIndexPages } from "../../utils/batchIndex.js";
6
6
  export default eventHandler(async (event) => {
7
7
  const { requireAuth } = await import("../../utils/auth.js");
8
- requireAuth(event);
8
+ await requireAuth(event);
9
9
  const lock = await tryAcquireCronLock(event);
10
10
  if (lock._tag === "held") {
11
11
  setResponseStatus(event, 409);
@@ -7,7 +7,7 @@ export default eventHandler(async (event) => {
7
7
  const dry = query.dry === "true" || query.dry === "1";
8
8
  if (!dry) {
9
9
  const { requireAuth } = await import("../../utils/auth.js");
10
- requireAuth(event);
10
+ await requireAuth(event);
11
11
  }
12
12
  const ttl = query.ttl ? Math.max(0, Math.trunc(Number(query.ttl)) || config.runtimeSync.pruneTtl) : config.runtimeSync.pruneTtl;
13
13
  const refreshWindowMs = resolveSeedRefreshWindowMs(config.runtimeSync.pruneTtl);
@@ -2,7 +2,7 @@ import { createError, eventHandler, getQuery, setResponseStatus } from "#nuxtseo
2
2
  import { indexPageByRoute } from "../../utils/indexPage.js";
3
3
  export default eventHandler(async (event) => {
4
4
  const { requireAuth } = await import("../../utils/auth.js");
5
- requireAuth(event);
5
+ await requireAuth(event);
6
6
  const query = getQuery(event);
7
7
  const route = typeof query.route === "string" ? query.route.trim() : "";
8
8
  if (!route.startsWith("/")) {
@@ -5,7 +5,7 @@ import { logger } from "../../logger.js";
5
5
  import { requireAuth } from "../../utils/auth.js";
6
6
  import { fetchPublicAsset } from "../../utils/cloudflare.js";
7
7
  export default eventHandler(async (event) => {
8
- requireAuth(event);
8
+ await requireAuth(event);
9
9
  const query = getQuery(event);
10
10
  logger.debug("[restore] Fetching dump...");
11
11
  const dumpData = await fetchPublicAsset(event, "/__ai-ready/pages.dump", { responseType: "text" });
@@ -4,7 +4,7 @@ import { countPages, countRecentlyIndexed, getCronLockStatus, getRecentCronRuns,
4
4
  export default eventHandler(async (event) => {
5
5
  const config = useRuntimeConfig(event)["nuxt-ai-ready"];
6
6
  const { requireAuth } = await import("../../utils/auth.js");
7
- requireAuth(event);
7
+ await requireAuth(event);
8
8
  const [total, pending, recentlyIndexed24h, recentlyIndexed1h, recentPages] = await Promise.all([
9
9
  countPages(event),
10
10
  countPages(event, { where: { pending: true } }),
@@ -2,8 +2,6 @@ import type { H3Event } from '#nuxtseo/h3';
2
2
  /**
3
3
  * Verify the request has a valid Authorization: Bearer token.
4
4
  * Throws 401 if the token is missing or invalid.
5
- * No-op if runtimeSyncSecret is not configured.
6
- *
7
- * Also accepts the deprecated `?secret=` query param with a warning.
5
+ * Legacy tokens remain valid when no application secret is configured.
8
6
  */
9
- export declare function requireAuth(event: H3Event): void;
7
+ export declare function requireAuth(event: H3Event): Promise<void>;
@@ -1,10 +1,31 @@
1
+ import { deriveSecret } from "nuxt/server";
1
2
  import { logger } from "#ai-ready-virtual/logger.mjs";
2
3
  import { createError, getHeader, getQuery } from "#nuxtseo/h3";
3
4
  import { useRuntimeConfig } from "#nuxtseo/nitro";
4
- export function requireAuth(event) {
5
- const config = useRuntimeConfig(event)["nuxt-ai-ready"];
6
- if (!config.runtimeSyncSecret)
7
- return;
5
+ import { deriveRuntimeSyncSecret, LEGACY_RUNTIME_SYNC_SECRET_WARNING, RUNTIME_SYNC_SECRET_PURPOSE } from "../../sync-secret.js";
6
+ import { getCfEnv } from "./cloudflare.js";
7
+ let warnedLegacy = false;
8
+ export async function requireAuth(event) {
9
+ const bindings = getCfEnv(event);
10
+ const config = useRuntimeConfig(event);
11
+ const root = bindings?.NUXT_APP_SECRET === void 0 ? config.appSecret : bindings.NUXT_APP_SECRET;
12
+ let secret;
13
+ if (root !== void 0 && root !== "") {
14
+ secret = await (bindings?.NUXT_APP_SECRET === void 0 ? deriveSecret(RUNTIME_SYNC_SECRET_PURPOSE) : deriveRuntimeSyncSecret(root)).catch((cause) => {
15
+ throw createError({ statusCode: 500, message: "Set NUXT_APP_SECRET to a string with at least 32 characters.", cause });
16
+ });
17
+ } else {
18
+ const moduleConfig = config["nuxt-ai-ready"];
19
+ const legacy = bindings?.NUXT_NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? bindings?.NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? globalThis.process?.env?.NUXT_NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? globalThis.process?.env?.NUXT_AI_READY_RUNTIME_SYNC_SECRET ?? moduleConfig.runtimeSyncSecret;
20
+ if (typeof legacy !== "string" || !legacy) {
21
+ throw createError({ statusCode: 500, message: "Set NUXT_APP_SECRET to a string with at least 32 characters." });
22
+ }
23
+ if (!warnedLegacy) {
24
+ warnedLegacy = true;
25
+ logger.warn(LEGACY_RUNTIME_SYNC_SECRET_WARNING);
26
+ }
27
+ secret = legacy;
28
+ }
8
29
  const authHeader = getHeader(event, "authorization");
9
30
  let token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0;
10
31
  if (!token) {
@@ -14,7 +35,7 @@ export function requireAuth(event) {
14
35
  logger.warn("Using `?secret=` query parameter is deprecated. Use `Authorization: Bearer <token>` header instead. See https://github.com/harlan-zw/nuxt-ai-ready/releases/tag/v1.0.0");
15
36
  }
16
37
  }
17
- if (!token || token !== config.runtimeSyncSecret) {
38
+ if (!token || token !== secret) {
18
39
  throw createError({ statusCode: 401, message: "Unauthorized" });
19
40
  }
20
41
  }
@@ -3,6 +3,9 @@ export interface CloudflareEnv {
3
3
  ASSETS?: {
4
4
  fetch: (req: Request | string) => Promise<Response>;
5
5
  };
6
+ NUXT_APP_SECRET?: unknown;
7
+ NUXT_AI_READY_RUNTIME_SYNC_SECRET?: unknown;
8
+ NUXT_NUXT_AI_READY_RUNTIME_SYNC_SECRET?: unknown;
6
9
  }
7
10
  /**
8
11
  * Get Cloudflare environment from event context or globalThis.__env__ (for scheduled tasks)
@@ -1,3 +1,6 @@
1
1
  import type { NuxtRenderHTMLContext } from 'nuxt/app';
2
2
  export declare function storePrerenderedHtml(path: string, html: string | NuxtRenderHTMLContext): void;
3
+ /** Only rendered pages can hand a policy to a Markdown twin. */
4
+ export declare function storePrerenderedContentSignal(path: string, policy: string): void;
5
+ export declare function consumePrerenderedContentSignal(path: string): string | undefined;
3
6
  export declare function consumePrerenderedHtml(path: string): string | undefined;
@@ -1,10 +1,22 @@
1
1
  const htmlByPath = /* @__PURE__ */ new Map();
2
+ const contentSignalByPath = /* @__PURE__ */ new Map();
2
3
  function normalizeHtmlCachePath(path) {
3
4
  return path.replace(/\/+$/, "") || "/";
4
5
  }
5
6
  export function storePrerenderedHtml(path, html) {
6
7
  htmlByPath.set(normalizeHtmlCachePath(path), html);
7
8
  }
9
+ export function storePrerenderedContentSignal(path, policy) {
10
+ const key = normalizeHtmlCachePath(path);
11
+ if (htmlByPath.has(key))
12
+ contentSignalByPath.set(key, policy);
13
+ }
14
+ export function consumePrerenderedContentSignal(path) {
15
+ const key = normalizeHtmlCachePath(path);
16
+ const policy = contentSignalByPath.get(key);
17
+ contentSignalByPath.delete(key);
18
+ return policy;
19
+ }
8
20
  export function consumePrerenderedHtml(path) {
9
21
  const key = normalizeHtmlCachePath(path);
10
22
  const html = htmlByPath.get(key);
@@ -0,0 +1,4 @@
1
+ export declare const RUNTIME_SYNC_SECRET_PURPOSE = "nuxt-ai-ready:runtime-sync";
2
+ export declare const LEGACY_RUNTIME_SYNC_SECRET_WARNING = "runtimeSyncSecret and NUXT_AI_READY_RUNTIME_SYNC_SECRET are deprecated. Set NUXT_APP_SECRET. Refresh scheduler tokens with `nuxt-ai-ready token`.";
3
+ /** Nuxt deriveSecret's HKDF, for the CLI and event-bound Worker secrets. */
4
+ export declare function deriveRuntimeSyncSecret(root: unknown): Promise<string>;
@@ -0,0 +1,15 @@
1
+ export const RUNTIME_SYNC_SECRET_PURPOSE = "nuxt-ai-ready:runtime-sync";
2
+ export const LEGACY_RUNTIME_SYNC_SECRET_WARNING = "runtimeSyncSecret and NUXT_AI_READY_RUNTIME_SYNC_SECRET are deprecated. Set NUXT_APP_SECRET. Refresh scheduler tokens with `nuxt-ai-ready token`.";
3
+ export async function deriveRuntimeSyncSecret(root) {
4
+ if (typeof root !== "string" || root.length < 32)
5
+ throw new Error("Set NUXT_APP_SECRET to a string with at least 32 characters.");
6
+ const encoder = new TextEncoder();
7
+ const key = await crypto.subtle.importKey("raw", encoder.encode(root), "HKDF", false, ["deriveBits"]);
8
+ const bits = await crypto.subtle.deriveBits({
9
+ name: "HKDF",
10
+ hash: "SHA-256",
11
+ salt: encoder.encode("nuxt"),
12
+ info: encoder.encode(RUNTIME_SYNC_SECRET_PURPOSE)
13
+ }, key, 256);
14
+ return Array.from(new Uint8Array(bits), (byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ }
@@ -215,9 +215,10 @@ export interface ModuleOptions {
215
215
  */
216
216
  aiTrain?: boolean;
217
217
  /**
218
- * Emit the draft Content-Usage robots.txt directive.
219
- * Set to false to avoid robots.txt validators that do not support it yet.
220
- * @default true
218
+ * Emit the draft IETF AI Preferences Content-Usage robots.txt directive.
219
+ * The Lighthouse robots-txt audit fails it as an unknown directive, so it is opt-in.
220
+ * Content-Signal already states the ai-train preference.
221
+ * @default false
221
222
  */
222
223
  contentUsage?: boolean;
223
224
  /**
@@ -336,8 +337,8 @@ export interface ModuleOptions {
336
337
  */
337
338
  cron?: boolean;
338
339
  /**
339
- * Secret token for authenticating runtime sync endpoints
340
- * When set, requires `Authorization: Bearer <token>` header for runtime sync endpoints
340
+ * Legacy bearer token for runtime sync endpoints.
341
+ * @deprecated Set NUXT_APP_SECRET instead. A configured application secret takes precedence.
341
342
  */
342
343
  runtimeSyncSecret?: string;
343
344
  /**