nuxt-ai-ready 3.4.0 → 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.
@@ -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.O6_9Egm8.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 matchingStaticContentSignals, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.O6_9Egm8.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';
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.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.4.0",
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 { a as default } from './shared/nuxt-ai-ready.O6_9Egm8.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';
@@ -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)
@@ -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
  /**
@@ -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';
@@ -1143,7 +1144,8 @@ function resolveContentSignalPolicy(option) {
1143
1144
  _tag: "Configured",
1144
1145
  // One comma-separated line: validators reject one line per signal.
1145
1146
  contentSignal: `ai-train=${value(option.aiTrain)}, search=${value(option.search)}, ai-input=${value(option.aiInput)}`,
1146
- ...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"}` }
1147
1149
  };
1148
1150
  }
1149
1151
  function hasContentSignal(value) {
@@ -1184,6 +1186,8 @@ function applyContentSignalPolicy(groups, policy) {
1184
1186
  }
1185
1187
  }
1186
1188
 
1189
+ const AI_READY_CRON_SCHEDULE = "*/5 * * * *";
1190
+
1187
1191
  const DATABASE_TYPE_NONE = "none";
1188
1192
  const DATABASE_DEPENDENT_OPTIONS = ["runtimeSync", "cron"];
1189
1193
  const DATABASE_CONSUMER_LABELS = {
@@ -2239,18 +2243,17 @@ ${details}`);
2239
2243
  const sitemapConfig = nuxt.options.sitemap;
2240
2244
  const sitemapRouteRule = nuxt.options.nitro?.routeRules?.["/sitemap.xml"];
2241
2245
  const sitemapPrerendered = !!(sitemapConfig?.zeroRuntime || sitemapRouteRule?.prerender || process.argv.includes("generate") || process.env.NUXT_GENERATE === "true");
2242
- let runtimeSyncSecret = config.runtimeSyncSecret || process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET;
2243
- if (!runtimeSyncSecret && runtimeSyncEnabled) {
2244
- runtimeSyncSecret = randomBytes(32).toString("hex");
2245
- if (!nuxt.options.dev && !nuxt.options._prepare)
2246
- 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.");
2247
- }
2248
- if (runtimeSyncSecret) {
2249
- const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt/ai-ready");
2250
- logger.debug(`Creating cache directory for secret: ${cacheDir}`);
2251
- await mkdir(cacheDir, { recursive: true });
2252
- logger.debug(`Writing runtimeSyncSecret to cache`);
2253
- 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 });
2254
2257
  }
2255
2258
  nuxt.hooks.hook("nitro:config", (nitroConfig) => {
2256
2259
  if (getNitroVersion(nuxt) === 3) {
@@ -2289,9 +2292,11 @@ ${details}`);
2289
2292
  const externals = nitroConfig.externals ||= {};
2290
2293
  externals.external ||= [];
2291
2294
  externals.external.push("mdream");
2295
+ const rollupExternal = (nitroConfig.rollupConfig ||= {}).external ||= [];
2296
+ if (Array.isArray(rollupExternal))
2297
+ rollupExternal.push("node:sqlite");
2292
2298
  }
2293
2299
  if (config.cron && !nuxt.options.dev) {
2294
- const cronSchedule = "*/5 * * * *";
2295
2300
  const preset2 = String(nitroConfig.preset || "");
2296
2301
  const isVercel = preset2 === "vercel" || preset2 === "vercel-edge";
2297
2302
  const isCloudflarePages = preset2 === "cloudflare-pages" || preset2 === "cloudflare_pages";
@@ -2302,7 +2307,7 @@ ${details}`);
2302
2307
  nitroConfig.vercel.config = nitroConfig.vercel.config || {};
2303
2308
  nitroConfig.vercel.config.crons = nitroConfig.vercel.config.crons || [];
2304
2309
  nitroConfig.vercel.config.crons.push({
2305
- schedule: cronSchedule,
2310
+ schedule: AI_READY_CRON_SCHEDULE,
2306
2311
  path: "/__ai-ready/cron"
2307
2312
  });
2308
2313
  } else {
@@ -2312,8 +2317,8 @@ ${details}`);
2312
2317
  handler: resolve$1("./runtime/server/tasks/ai-ready-cron")
2313
2318
  };
2314
2319
  nitroConfig.scheduledTasks = nitroConfig.scheduledTasks || {};
2315
- nitroConfig.scheduledTasks[cronSchedule] = nitroConfig.scheduledTasks[cronSchedule] || [];
2316
- 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");
2317
2322
  const isCloudflareWorkers = preset2.startsWith("cloudflare");
2318
2323
  if (isCloudflareWorkers) {
2319
2324
  nitroConfig.cloudflare = nitroConfig.cloudflare || {};
@@ -2321,8 +2326,8 @@ ${details}`);
2321
2326
  nitroConfig.cloudflare.wrangler = nitroConfig.cloudflare.wrangler || {};
2322
2327
  nitroConfig.cloudflare.wrangler.triggers = nitroConfig.cloudflare.wrangler.triggers || {};
2323
2328
  nitroConfig.cloudflare.wrangler.triggers.crons = nitroConfig.cloudflare.wrangler.triggers.crons || [];
2324
- if (!nitroConfig.cloudflare.wrangler.triggers.crons.includes(cronSchedule)) {
2325
- 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);
2326
2331
  }
2327
2332
  }
2328
2333
  }
@@ -2440,7 +2445,7 @@ export function trackDrizzleWork(event, work) { return work }
2440
2445
  batchSize: runtimeSyncConfig.batchSize ?? 50,
2441
2446
  pruneTtl: runtimeSyncConfig.pruneTtl ?? 0
2442
2447
  },
2443
- runtimeSyncSecret,
2448
+ runtimeSyncSecret: legacySecret || "",
2444
2449
  sitemapPrerendered,
2445
2450
  i18n: i18nConfig,
2446
2451
  ftsTokenizer,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.4.0",
4
+ "version": "3.5.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -29,13 +29,19 @@ For D1, set `database: { type: 'd1', bindingName: 'AI_READY_DB' }` and create th
29
29
  export default defineNuxtConfig({
30
30
  aiReady: {
31
31
  runtimeSync: { ttl: 3600, batchSize: 50, pruneTtl: 0 },
32
- runtimeSyncSecret: process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET,
33
32
  cron: true, // every 5 minutes, also enables runtimeSync
34
33
  },
35
34
  })
36
35
  ```
37
36
 
38
- Set `runtimeSyncSecret`. Without it, each build generates a new random secret, and external schedulers lose access after the next deployment.
37
+ Set `NUXT_APP_SECRET` in production to a string with at least 32 characters.
38
+ AI Ready and OG Image derive separate tokens from this shared Nuxt root secret.
39
+ Run `pnpm exec nuxt-ai-ready token` with the root secret in your environment to obtain the runtime sync bearer token.
40
+ Cloudflare can supply the root through its encrypted `NUXT_APP_SECRET` binding at runtime.
41
+ The legacy `runtimeSyncSecret` option and `NUXT_AI_READY_RUNTIME_SYNC_SECRET` remain supported with a deprecation warning.
42
+ They use the original bearer token unchanged when `appSecret` is unset or empty.
43
+ A non-empty application secret takes precedence. An invalid application secret fails without using the legacy token.
44
+ Without a root or legacy token, protected requests return `500`.
39
45
 
40
46
  Every control endpoint needs `Authorization: Bearer <secret>`, including `GET /__ai-ready/status`. Only `POST /__ai-ready/prune?dry=true` works without it.
41
47