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.
- package/dist/chunks/agent-skills.mjs +2 -1
- package/dist/chunks/prerender.mjs +14 -2
- package/dist/cli.mjs +28 -10
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -2
- package/dist/runtime/server/middleware/markdown.prerender.js +9 -1
- package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.d.ts +2 -0
- package/dist/runtime/server/plugins/content-signal-capture-nitro3.prerender.js +13 -0
- package/dist/runtime/server/plugins/content-signal-capture.prerender.d.ts +2 -0
- package/dist/runtime/server/plugins/content-signal-capture.prerender.js +13 -0
- 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/server/utils/prerender-html.d.ts +3 -0
- package/dist/runtime/server/utils/prerender-html.js +12 -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.CKXM1jnE.mjs → nuxt-ai-ready.BaNK8yfw.mjs} +393 -266
- 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 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';
|
|
@@ -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
|
|
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.d.mts
CHANGED
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 {
|
|
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,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,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
|
-
*
|
|
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)
|
|
@@ -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
|
+
}
|
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
|
/**
|