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.
- package/dist/chunks/agent-skills.mjs +2 -1
- package/dist/chunks/prerender.mjs +3 -2
- package/dist/cli.mjs +28 -10
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -2
- package/dist/runtime/server/routes/__ai-ready/cron.get.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/poll.post.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/prune.post.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/reindex.post.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/restore.post.js +1 -1
- package/dist/runtime/server/routes/__ai-ready/status.get.js +1 -1
- package/dist/runtime/server/utils/auth.d.ts +2 -4
- package/dist/runtime/server/utils/auth.js +26 -5
- package/dist/runtime/server/utils/cloudflare.d.ts +3 -0
- package/dist/runtime/sync-secret.d.ts +4 -0
- package/dist/runtime/sync-secret.js +15 -0
- package/dist/runtime/types.d.ts +6 -5
- package/dist/shared/{nuxt-ai-ready.O6_9Egm8.mjs → nuxt-ai-ready.BaNK8yfw.mjs} +27 -22
- package/package.json +1 -1
- 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.
|
|
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.
|
|
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
|
|
269
|
+
async function requireSecret(cwd) {
|
|
260
270
|
const secret = await getSecret(cwd);
|
|
261
271
|
if (!secret) {
|
|
262
|
-
|
|
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
|
|
354
|
+
const secret = await requireSecret(cwd);
|
|
336
355
|
if (!secret)
|
|
337
356
|
return;
|
|
338
|
-
const url =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 !==
|
|
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
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
220
|
-
*
|
|
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
|
-
*
|
|
340
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
if (
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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:
|
|
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[
|
|
2316
|
-
nitroConfig.scheduledTasks[
|
|
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(
|
|
2325
|
-
nitroConfig.cloudflare.wrangler.triggers.crons.push(
|
|
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
|
@@ -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 `
|
|
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
|
|