@relvehq/agent-ready 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/README.md +338 -3
- package/SETUP.md +337 -0
- package/dist/bin/agent-ready.d.ts +23 -0
- package/dist/bin/agent-ready.js +350 -0
- package/dist/cli/init.d.ts +70 -0
- package/dist/cli/init.js +366 -0
- package/dist/core/accept.d.ts +8 -0
- package/dist/core/accept.js +76 -0
- package/dist/core/config.d.ts +145 -0
- package/dist/core/config.js +103 -0
- package/dist/core/convert.d.ts +22 -0
- package/dist/core/convert.js +334 -0
- package/dist/core/detect.d.ts +30 -0
- package/dist/core/detect.js +82 -0
- package/dist/core/easy.d.ts +19 -0
- package/dist/core/easy.js +117 -0
- package/dist/core/enrich.d.ts +30 -0
- package/dist/core/enrich.js +155 -0
- package/dist/core/llms.d.ts +11 -0
- package/dist/core/llms.js +71 -0
- package/dist/core/report.d.ts +15 -0
- package/dist/core/report.js +46 -0
- package/dist/core/resolve.d.ts +43 -0
- package/dist/core/resolve.js +204 -0
- package/dist/core/respond.d.ts +18 -0
- package/dist/core/respond.js +59 -0
- package/dist/core/sitemap.d.ts +31 -0
- package/dist/core/sitemap.js +140 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +76 -0
- package/dist/next/dashboard.d.ts +14 -0
- package/dist/next/dashboard.js +56 -0
- package/dist/next/index.d.ts +42 -0
- package/dist/next/index.js +100 -0
- package/dist/next/route.d.ts +26 -0
- package/dist/next/route.js +167 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +191 -0
- package/dist/proof/audit.d.ts +47 -0
- package/dist/proof/audit.js +253 -0
- package/dist/proof/bots.d.ts +24 -0
- package/dist/proof/bots.js +60 -0
- package/dist/proof/dashboard.d.ts +8 -0
- package/dist/proof/dashboard.js +77 -0
- package/dist/proof/hits.d.ts +18 -0
- package/dist/proof/hits.js +51 -0
- package/dist/proof/jsonl.d.ts +5 -0
- package/dist/proof/jsonl.js +80 -0
- package/dist/proof/stats.d.ts +39 -0
- package/dist/proof/stats.js +127 -0
- package/dist/refine/check.d.ts +17 -0
- package/dist/refine/check.js +134 -0
- package/dist/refine/extract.d.ts +7 -0
- package/dist/refine/extract.js +120 -0
- package/dist/refine/file-store.d.ts +6 -0
- package/dist/refine/file-store.js +87 -0
- package/dist/refine/model.d.ts +30 -0
- package/dist/refine/model.js +105 -0
- package/dist/refine/prompts.d.ts +49 -0
- package/dist/refine/prompts.js +178 -0
- package/dist/refine/refine.d.ts +38 -0
- package/dist/refine/refine.js +75 -0
- package/dist/refine/run.d.ts +52 -0
- package/dist/refine/run.js +66 -0
- package/dist/refine/types.d.ts +17 -0
- package/dist/refine/types.js +8 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +363 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +447 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +115 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +543 -0
- package/dist/wp/relve-agent-ready/includes/index.php +2 -0
- package/dist/wp/relve-agent-ready/index.php +2 -0
- package/dist/wp/relve-agent-ready/readme.txt +75 -0
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +70 -0
- package/dist/wp/relve-agent-ready/uninstall.php +24 -0
- package/package.json +64 -6
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Resolve: find the Markdown for a page.
|
|
3
|
+
//
|
|
4
|
+
// native config.templates, first matching route pattern ("/tool/:slug")
|
|
5
|
+
// stored config.store.get(path)
|
|
6
|
+
// convert fetch the page's own HTML from the origin and convert it
|
|
7
|
+
// null nothing usable; the caller serves HTML (or a 404 for `.md` URLs)
|
|
8
|
+
//
|
|
9
|
+
// Each step that throws or returns nothing falls through to the next one.
|
|
10
|
+
// The origin fetch sends `Accept: text/html` and the internal header, so the
|
|
11
|
+
// middleware adapter never intercepts it, and sends no cookies, so the
|
|
12
|
+
// Markdown is always the public page.
|
|
13
|
+
//
|
|
14
|
+
// NODE ONLY when the convert step runs (it loads ./convert lazily).
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.resolveMarkdown = resolveMarkdown;
|
|
50
|
+
exports.defaultFetchOrigin = defaultFetchOrigin;
|
|
51
|
+
exports.matchPattern = matchPattern;
|
|
52
|
+
const config_1 = require("./config");
|
|
53
|
+
const MAX_HTML_BYTES = 5 * 1024 * 1024;
|
|
54
|
+
async function resolveMarkdown(cfg, input, deps = {}) {
|
|
55
|
+
const { path, search, request } = input;
|
|
56
|
+
const pageUrl = cfg.site + (path === "/" ? "/" : path) + search;
|
|
57
|
+
const miss = { source: null, doc: null, sourceUrl: pageUrl, html: null, originStatus: null };
|
|
58
|
+
const found = (source, doc) => ({
|
|
59
|
+
source,
|
|
60
|
+
doc,
|
|
61
|
+
sourceUrl: pageUrl,
|
|
62
|
+
html: null,
|
|
63
|
+
originStatus: null,
|
|
64
|
+
});
|
|
65
|
+
if ((0, config_1.isExcluded)(cfg, path))
|
|
66
|
+
return miss;
|
|
67
|
+
// 1. native
|
|
68
|
+
for (const [pattern, fn] of Object.entries(cfg.templates)) {
|
|
69
|
+
const params = matchPattern(pattern, path);
|
|
70
|
+
if (!params)
|
|
71
|
+
continue;
|
|
72
|
+
const doc = await safely(`template ${pattern}`, () => fn({ path, params, search, request }));
|
|
73
|
+
if (doc)
|
|
74
|
+
return found("native", doc);
|
|
75
|
+
break; // only the first matching pattern is tried
|
|
76
|
+
}
|
|
77
|
+
// 2. stored
|
|
78
|
+
if (cfg.store) {
|
|
79
|
+
const store = cfg.store;
|
|
80
|
+
const doc = await safely("store", () => store.get(path, { request }));
|
|
81
|
+
if (doc)
|
|
82
|
+
return found("stored", doc);
|
|
83
|
+
}
|
|
84
|
+
// 3. convert
|
|
85
|
+
if (cfg.fallback !== "convert")
|
|
86
|
+
return miss;
|
|
87
|
+
const origin = (cfg.fetchOrigin ? cfg.fetchOrigin(request) : defaultFetchOrigin(request, cfg.site)).replace(/\/+$/, "");
|
|
88
|
+
const fetchUrl = origin + (path === "/" ? "/" : path) + search;
|
|
89
|
+
const doFetch = deps.fetch ?? fetch;
|
|
90
|
+
let res;
|
|
91
|
+
try {
|
|
92
|
+
res = await doFetch(fetchUrl, {
|
|
93
|
+
method: "GET",
|
|
94
|
+
redirect: "follow",
|
|
95
|
+
cache: "no-store",
|
|
96
|
+
headers: {
|
|
97
|
+
accept: "text/html",
|
|
98
|
+
[cfg.internalHeader]: "1",
|
|
99
|
+
"user-agent": "agent-ready/0.1 (+internal origin fetch)",
|
|
100
|
+
},
|
|
101
|
+
signal: timeoutSignal(cfg.fetchTimeoutMs),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.warn(`[agent-ready] origin fetch failed for ${path}: ${err?.message ?? err}`);
|
|
106
|
+
return miss;
|
|
107
|
+
}
|
|
108
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
109
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
110
|
+
if (declared > MAX_HTML_BYTES)
|
|
111
|
+
return { ...miss, originStatus: res.status };
|
|
112
|
+
const body = await res.text().catch(() => "");
|
|
113
|
+
const html = { status: res.status, contentType: contentType || "text/html; charset=utf-8", body };
|
|
114
|
+
if (!res.ok || !/text\/html|application\/xhtml/i.test(contentType) || body.length > MAX_HTML_BYTES) {
|
|
115
|
+
return { ...miss, html, originStatus: res.status };
|
|
116
|
+
}
|
|
117
|
+
const convert = deps.convert ?? (await Promise.resolve().then(() => __importStar(require("./convert")))).htmlToMarkdown;
|
|
118
|
+
let converted = null;
|
|
119
|
+
try {
|
|
120
|
+
converted = convert(body, { url: pageUrl, minChars: cfg.minChars });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.warn(`[agent-ready] convert failed for ${path}: ${err?.message ?? err}`);
|
|
124
|
+
}
|
|
125
|
+
if (!converted)
|
|
126
|
+
return { ...miss, html, originStatus: res.status };
|
|
127
|
+
return found("convert", converted);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Where the converter fetches HTML from: the host the request came in on, so
|
|
131
|
+
* staging converts staging and a test host converts itself. `*.localhost`
|
|
132
|
+
* names become `localhost` (Node's resolver doesn't map them everywhere).
|
|
133
|
+
*/
|
|
134
|
+
function defaultFetchOrigin(request, site) {
|
|
135
|
+
const h = request.headers;
|
|
136
|
+
const host = (h.get("x-forwarded-host") ?? h.get("host") ?? "").split(",")[0].trim();
|
|
137
|
+
let reqUrl = null;
|
|
138
|
+
try {
|
|
139
|
+
reqUrl = new URL(request.url);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// fall through to site
|
|
143
|
+
}
|
|
144
|
+
if (!host)
|
|
145
|
+
return reqUrl ? reqUrl.origin : site;
|
|
146
|
+
const proto = (h.get("x-forwarded-proto") ?? "").split(",")[0].trim() ||
|
|
147
|
+
(reqUrl ? reqUrl.protocol.replace(":", "") : "https");
|
|
148
|
+
const [name, port] = host.split(":");
|
|
149
|
+
const hostname = name.toLowerCase().endsWith(".localhost") ? "localhost" : name;
|
|
150
|
+
return `${proto}://${hostname}${port ? `:${port}` : ""}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* "/tool/:slug" -> { slug }, "/docs/*" -> { "*": rest }. Exact otherwise.
|
|
154
|
+
* Returns null when the path doesn't match.
|
|
155
|
+
*/
|
|
156
|
+
function matchPattern(pattern, path) {
|
|
157
|
+
const p = pattern.replace(/\/+$/, "") || "/";
|
|
158
|
+
const pp = p.split("/").filter(Boolean);
|
|
159
|
+
const xs = path.split("/").filter(Boolean);
|
|
160
|
+
const params = {};
|
|
161
|
+
for (let i = 0; i < pp.length; i++) {
|
|
162
|
+
const seg = pp[i];
|
|
163
|
+
if (seg === "*" && i === pp.length - 1) {
|
|
164
|
+
if (i >= xs.length)
|
|
165
|
+
return null;
|
|
166
|
+
params["*"] = xs.slice(i).join("/");
|
|
167
|
+
return params;
|
|
168
|
+
}
|
|
169
|
+
if (i >= xs.length)
|
|
170
|
+
return null;
|
|
171
|
+
if (seg.startsWith(":")) {
|
|
172
|
+
try {
|
|
173
|
+
params[seg.slice(1)] = decodeURIComponent(xs[i]);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (seg !== xs[i]) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return pp.length === xs.length ? params : null;
|
|
184
|
+
}
|
|
185
|
+
function normalizeDoc(r) {
|
|
186
|
+
if (!r)
|
|
187
|
+
return null;
|
|
188
|
+
if (typeof r === "string")
|
|
189
|
+
return r.trim() ? { markdown: r } : null;
|
|
190
|
+
return r.markdown && r.markdown.trim() ? r : null;
|
|
191
|
+
}
|
|
192
|
+
async function safely(label, fn) {
|
|
193
|
+
try {
|
|
194
|
+
return normalizeDoc((await fn()));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
console.warn(`[agent-ready] ${label} failed: ${err?.message ?? err}`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function timeoutSignal(ms) {
|
|
202
|
+
const AS = AbortSignal;
|
|
203
|
+
return typeof AS.timeout === "function" ? AS.timeout(ms) : undefined;
|
|
204
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RespondOptions {
|
|
2
|
+
request: Request;
|
|
3
|
+
/** Absolute canonical URL of the HTML page. */
|
|
4
|
+
canonical: string;
|
|
5
|
+
tokens: number;
|
|
6
|
+
cacheControl: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
contentType?: string;
|
|
9
|
+
/** Extra headers, e.g. { "x-agent-ready-source": "convert" }. */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
|
|
13
|
+
export declare function markdownResponse(body: string, opts: RespondOptions): Promise<Response>;
|
|
14
|
+
/** A strong ETag: the first 32 hex chars of SHA-256(body). */
|
|
15
|
+
export declare function etagFor(body: string): Promise<string>;
|
|
16
|
+
/** RFC 9110 §13.1.2: weak comparison; `*` matches any current representation. */
|
|
17
|
+
export declare function ifNoneMatchHits(header: string | null, etag: string): boolean;
|
|
18
|
+
export declare function byteLength(s: string): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Respond: turn a Markdown body into a standard Web Response.
|
|
3
|
+
//
|
|
4
|
+
// Headers: Content-Type text/markdown, Vary: Accept, Link rel=canonical,
|
|
5
|
+
// X-Robots-Tag: noindex (the HTML page is the indexable one), x-markdown-tokens,
|
|
6
|
+
// Cache-Control and a strong ETag. If-None-Match is honoured with a 304.
|
|
7
|
+
//
|
|
8
|
+
// EDGE-SAFE. Uses Web Crypto only.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MARKDOWN_CONTENT_TYPE = void 0;
|
|
11
|
+
exports.markdownResponse = markdownResponse;
|
|
12
|
+
exports.etagFor = etagFor;
|
|
13
|
+
exports.ifNoneMatchHits = ifNoneMatchHits;
|
|
14
|
+
exports.byteLength = byteLength;
|
|
15
|
+
exports.MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
|
|
16
|
+
async function markdownResponse(body, opts) {
|
|
17
|
+
// The front matter's generated_at changes on every conversion, so hashing it
|
|
18
|
+
// gave every request a new ETag and 304s never happened (seen in the
|
|
19
|
+
// fresh-app test, 2026-10-21). Hash the content without that line; the
|
|
20
|
+
// bytes can still differ by the timestamp, so the ETag is weak (W/).
|
|
21
|
+
const stable = body.replace(/^generated_at:.*\n?/m, "");
|
|
22
|
+
const etag = stable === body ? await etagFor(body) : `W/${await etagFor(stable)}`;
|
|
23
|
+
const headers = new Headers({
|
|
24
|
+
"content-type": opts.contentType ?? exports.MARKDOWN_CONTENT_TYPE,
|
|
25
|
+
vary: "Accept",
|
|
26
|
+
link: `<${opts.canonical}>; rel="canonical"`,
|
|
27
|
+
"x-robots-tag": "noindex",
|
|
28
|
+
"x-markdown-tokens": String(opts.tokens),
|
|
29
|
+
"cache-control": opts.cacheControl,
|
|
30
|
+
etag,
|
|
31
|
+
...(opts.headers ?? {}),
|
|
32
|
+
});
|
|
33
|
+
if (ifNoneMatchHits(opts.request.headers.get("if-none-match"), etag)) {
|
|
34
|
+
headers.delete("content-type");
|
|
35
|
+
return new Response(null, { status: 304, headers });
|
|
36
|
+
}
|
|
37
|
+
const isHead = opts.request.method === "HEAD";
|
|
38
|
+
headers.set("content-length", String(byteLength(body)));
|
|
39
|
+
return new Response(isHead ? null : body, { status: opts.status ?? 200, headers });
|
|
40
|
+
}
|
|
41
|
+
/** A strong ETag: the first 32 hex chars of SHA-256(body). */
|
|
42
|
+
async function etagFor(body) {
|
|
43
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
44
|
+
const hex = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
45
|
+
return `"${hex.slice(0, 32)}"`;
|
|
46
|
+
}
|
|
47
|
+
/** RFC 9110 §13.1.2: weak comparison; `*` matches any current representation. */
|
|
48
|
+
function ifNoneMatchHits(header, etag) {
|
|
49
|
+
if (!header)
|
|
50
|
+
return false;
|
|
51
|
+
const bare = etag.replace(/^W\//, "");
|
|
52
|
+
return header.split(",").some((raw) => {
|
|
53
|
+
const t = raw.trim();
|
|
54
|
+
return t === "*" || t.replace(/^W\//, "") === bare;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function byteLength(s) {
|
|
58
|
+
return new TextEncoder().encode(s).length;
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { LlmsEntry } from "./config";
|
|
2
|
+
export interface ParsedSitemap {
|
|
3
|
+
/** Page URLs from a <urlset>. */
|
|
4
|
+
urls: string[];
|
|
5
|
+
/** Child sitemap URLs from a <sitemapindex>. */
|
|
6
|
+
sitemaps: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Parse a sitemap or sitemap index. Tolerant: unknown markup is ignored. */
|
|
9
|
+
export declare function parseSitemap(xml: string): ParsedSitemap;
|
|
10
|
+
export interface SitemapEntriesOptions {
|
|
11
|
+
/** Sitemap location. Default "/sitemap.xml" on the request's origin. */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Most pages listed. Default 200. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** Child sitemaps followed from an index. Default 5. */
|
|
16
|
+
maxSitemaps?: number;
|
|
17
|
+
/** Paths (segment-aware prefixes) or RegExps left out. */
|
|
18
|
+
exclude?: Array<string | RegExp>;
|
|
19
|
+
/** Fetch timeout per sitemap. Default 5000 ms. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Injected for tests. */
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
/** An `llms.entries` function that reads the site's sitemap. */
|
|
25
|
+
export declare function sitemapEntries(opts?: SitemapEntriesOptions): (ctx: {
|
|
26
|
+
request: Request;
|
|
27
|
+
}) => Promise<LlmsEntry[]>;
|
|
28
|
+
/** Turn page URLs into llms entries (exported for tests and custom sources). */
|
|
29
|
+
export declare function entriesFromUrls(urls: string[], origin: string, opts?: Pick<SitemapEntriesOptions, "limit" | "exclude">): LlmsEntry[];
|
|
30
|
+
/** "ai-tools_list" -> "Ai tools list". */
|
|
31
|
+
export declare function humanize(seg: string): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Discover, the easy way: llms.txt entries from the site's own sitemap.xml,
|
|
3
|
+
// so a venture lists nothing by hand (changes/agent-ready-engine, "easy setup").
|
|
4
|
+
//
|
|
5
|
+
// llms: { title: "Acme", entries: sitemapEntries() }
|
|
6
|
+
//
|
|
7
|
+
// Reads /sitemap.xml (following a sitemap index one level down), keeps
|
|
8
|
+
// same-site page URLs, and turns each into an entry: a title from the last
|
|
9
|
+
// path segment ("/pricing/teams" -> "Teams"), a section from the first one
|
|
10
|
+
// ("Pricing"; the home page goes under "Pages"). Capped, de-duplicated,
|
|
11
|
+
// sorted by section then path. Any failure -> no entries, never a throw.
|
|
12
|
+
//
|
|
13
|
+
// EDGE-SAFE. No Node APIs (regex parsing, global fetch).
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseSitemap = parseSitemap;
|
|
16
|
+
exports.sitemapEntries = sitemapEntries;
|
|
17
|
+
exports.entriesFromUrls = entriesFromUrls;
|
|
18
|
+
exports.humanize = humanize;
|
|
19
|
+
/** Parse a sitemap or sitemap index. Tolerant: unknown markup is ignored. */
|
|
20
|
+
function parseSitemap(xml) {
|
|
21
|
+
const locs = (block) => Array.from(block.matchAll(/<loc>\s*(?:<!\[CDATA\[)?\s*([^<\]]+?)\s*(?:\]\]>)?\s*<\/loc>/gi), (m) => decodeXml(m[1]));
|
|
22
|
+
if (/<sitemapindex[\s>]/i.test(xml)) {
|
|
23
|
+
const sitemaps = Array.from(xml.matchAll(/<sitemap[\s>][\s\S]*?<\/sitemap>/gi)).flatMap((m) => locs(m[0]));
|
|
24
|
+
return { urls: [], sitemaps };
|
|
25
|
+
}
|
|
26
|
+
const urls = Array.from(xml.matchAll(/<url[\s>][\s\S]*?<\/url>/gi)).flatMap((m) => locs(m[0]));
|
|
27
|
+
return { urls, sitemaps: [] };
|
|
28
|
+
}
|
|
29
|
+
/** An `llms.entries` function that reads the site's sitemap. */
|
|
30
|
+
function sitemapEntries(opts = {}) {
|
|
31
|
+
return async ({ request }) => {
|
|
32
|
+
try {
|
|
33
|
+
const origin = new URL(request.url).origin;
|
|
34
|
+
const urls = await collectUrls(new URL(opts.path ?? "/sitemap.xml", origin).href, origin, opts);
|
|
35
|
+
return entriesFromUrls(urls, origin, opts);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Turn page URLs into llms entries (exported for tests and custom sources). */
|
|
43
|
+
function entriesFromUrls(urls, origin, opts = {}) {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const raw of urls) {
|
|
47
|
+
let u;
|
|
48
|
+
try {
|
|
49
|
+
u = new URL(raw, origin);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Only the path is kept: a sitemap names the production host, and the
|
|
55
|
+
// entries are linked on whatever site serves this llms.txt.
|
|
56
|
+
const path = (u.pathname.replace(/\/+$/, "") || "/").replace(/\.html?$/i, "");
|
|
57
|
+
if (/\.(xml|txt|json|pdf|jpe?g|png|gif|svg|webp|zip)$/i.test(path))
|
|
58
|
+
continue;
|
|
59
|
+
if (isOut(path, opts.exclude))
|
|
60
|
+
continue;
|
|
61
|
+
if (seen.has(path))
|
|
62
|
+
continue;
|
|
63
|
+
seen.add(path);
|
|
64
|
+
const segs = path.split("/").filter(Boolean).map(safeDecode);
|
|
65
|
+
out.push({
|
|
66
|
+
path,
|
|
67
|
+
url: path,
|
|
68
|
+
title: segs.length ? humanize(segs[segs.length - 1]) : "Home",
|
|
69
|
+
section: segs.length > 1 ? humanize(segs[0]) : "Pages",
|
|
70
|
+
});
|
|
71
|
+
if (out.length >= (opts.limit ?? 200))
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
out.sort((a, b) => (a.section === b.section ? a.path.localeCompare(b.path) : a.section === "Pages" ? -1 : b.section === "Pages" ? 1 : a.section.localeCompare(b.section)));
|
|
75
|
+
return out.map(({ path: _path, ...e }) => e);
|
|
76
|
+
}
|
|
77
|
+
async function collectUrls(start, origin, opts) {
|
|
78
|
+
const first = await fetchSitemap(start, opts);
|
|
79
|
+
if (!first)
|
|
80
|
+
return [];
|
|
81
|
+
if (!first.sitemaps.length)
|
|
82
|
+
return first.urls;
|
|
83
|
+
const children = first.sitemaps.slice(0, opts.maxSitemaps ?? 5).map((s) => {
|
|
84
|
+
// Child sitemaps on the production host are read from this origin instead.
|
|
85
|
+
try {
|
|
86
|
+
const u = new URL(s, origin);
|
|
87
|
+
return new URL(u.pathname + u.search, origin).href;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const lists = await Promise.all(children.filter((c) => !!c).map((c) => fetchSitemap(c, opts)));
|
|
94
|
+
return lists.flatMap((l) => (l ? l.urls : []));
|
|
95
|
+
}
|
|
96
|
+
async function fetchSitemap(url, opts) {
|
|
97
|
+
const f = opts.fetch ?? fetch;
|
|
98
|
+
const ctl = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => ctl.abort(), opts.timeoutMs ?? 5000);
|
|
100
|
+
try {
|
|
101
|
+
const res = await f(url, { signal: ctl.signal, headers: { accept: "application/xml,text/xml;q=0.9,*/*;q=0.1" } });
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
return null;
|
|
104
|
+
return parseSitemap(await res.text());
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function isOut(path, exclude) {
|
|
114
|
+
for (const rule of exclude ?? []) {
|
|
115
|
+
if (typeof rule === "string") {
|
|
116
|
+
const p = rule.replace(/\/+$/, "");
|
|
117
|
+
if (p && (path === p || path.startsWith(p + "/")))
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
else if (rule.test(path))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/** "ai-tools_list" -> "Ai tools list". */
|
|
126
|
+
function humanize(seg) {
|
|
127
|
+
const s = seg.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
128
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : seg;
|
|
129
|
+
}
|
|
130
|
+
function safeDecode(s) {
|
|
131
|
+
try {
|
|
132
|
+
return decodeURIComponent(s);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return s;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function decodeXml(s) {
|
|
139
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
140
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { chooseFormat, parseAccept, type Format, type MediaRange } from "./core/accept";
|
|
2
|
+
export { ALWAYS_EXCLUDE, DEFAULT_CACHE_CONTROL, DEFAULT_INTERNAL_HEADER, DEFAULT_INTERNAL_PREFIX, DEFAULT_NEGOTIATED_CACHE_CONTROL, VIA_HEADER, defineAgentReady, emitHit, isEnabled, isExcluded, type AgentReadyConfig, type Awaitable, type DocResult, type HitEvent, type LlmsConfig, type LlmsEntry, type MarkdownDoc, type MarkdownStore, type ResolvedAgentReadyConfig, type Source, type TemplateContext, type TemplateFn, type Via, } from "./core/config";
|
|
3
|
+
export { mdUrlPath, normalizePath, stripMdSuffix, wantsMarkdown, type Detection, type RequestLike } from "./core/detect";
|
|
4
|
+
export { enrich, estimateTokens, frontMatter, hasFrontMatter, jsonLdFacts, type EnrichMeta, type Enriched } from "./core/enrich";
|
|
5
|
+
export { buildLlmsTxt, type BuildLlmsOptions } from "./core/llms";
|
|
6
|
+
export { byteLength, etagFor, ifNoneMatchHits, markdownResponse, MARKDOWN_CONTENT_TYPE, type RespondOptions } from "./core/respond";
|
|
7
|
+
export { COMMON_PRIVATE_PATHS, createAgentReady, enabledFromEnv, siteFromEnv, type EasyOptions, type Env } from "./core/easy";
|
|
8
|
+
export { entriesFromUrls, humanize, parseSitemap, sitemapEntries, type ParsedSitemap, type SitemapEntriesOptions } from "./core/sitemap";
|
|
9
|
+
export { reporter, reporterFromEnv, type ReporterOptions } from "./core/report";
|
|
10
|
+
export { ANSWER_BOT_TOKENS, classifyAgent, isAnswerBot, type Bot, type BotKind } from "./proof/bots";
|
|
11
|
+
export { filterHits, memoryHitStore, teeHits, type HitQuery, type HitStore } from "./proof/hits";
|
|
12
|
+
export { formatSummary, summarize, type Count, type HitSummary } from "./proof/stats";
|
|
13
|
+
export { renderDashboard, type DashboardOptions } from "./proof/dashboard";
|
|
14
|
+
export { auditSite, blockedBots, formatAudit, type AuditCheck, type AuditOptions, type AuditReport, type CheckStatus } from "./proof/audit";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @relvehq/agent-ready: serve a clean Markdown version of any page to AI agents.
|
|
3
|
+
//
|
|
4
|
+
// This barrel is EDGE-SAFE: it exports only modules with no Node APIs, so it
|
|
5
|
+
// can be imported from middleware. The Node-only parts are imported by path:
|
|
6
|
+
// @relvehq/agent-ready/core/convert HTML -> Markdown (linkedom, turndown)
|
|
7
|
+
// @relvehq/agent-ready/core/resolve native -> stored -> convert chain
|
|
8
|
+
// @relvehq/agent-ready/next/route Next.js route handler
|
|
9
|
+
// and the Next.js middleware adapter lives at @relvehq/agent-ready/next.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.formatAudit = exports.blockedBots = exports.auditSite = exports.renderDashboard = exports.summarize = exports.formatSummary = exports.teeHits = exports.memoryHitStore = exports.filterHits = exports.isAnswerBot = exports.classifyAgent = exports.ANSWER_BOT_TOKENS = exports.reporterFromEnv = exports.reporter = exports.sitemapEntries = exports.parseSitemap = exports.humanize = exports.entriesFromUrls = exports.siteFromEnv = exports.enabledFromEnv = exports.createAgentReady = exports.COMMON_PRIVATE_PATHS = exports.MARKDOWN_CONTENT_TYPE = exports.markdownResponse = exports.ifNoneMatchHits = exports.etagFor = exports.byteLength = exports.buildLlmsTxt = exports.jsonLdFacts = exports.hasFrontMatter = exports.frontMatter = exports.estimateTokens = exports.enrich = exports.wantsMarkdown = exports.stripMdSuffix = exports.normalizePath = exports.mdUrlPath = exports.isExcluded = exports.isEnabled = exports.emitHit = exports.defineAgentReady = exports.VIA_HEADER = exports.DEFAULT_NEGOTIATED_CACHE_CONTROL = exports.DEFAULT_INTERNAL_PREFIX = exports.DEFAULT_INTERNAL_HEADER = exports.DEFAULT_CACHE_CONTROL = exports.ALWAYS_EXCLUDE = exports.parseAccept = exports.chooseFormat = void 0;
|
|
12
|
+
var accept_1 = require("./core/accept");
|
|
13
|
+
Object.defineProperty(exports, "chooseFormat", { enumerable: true, get: function () { return accept_1.chooseFormat; } });
|
|
14
|
+
Object.defineProperty(exports, "parseAccept", { enumerable: true, get: function () { return accept_1.parseAccept; } });
|
|
15
|
+
var config_1 = require("./core/config");
|
|
16
|
+
Object.defineProperty(exports, "ALWAYS_EXCLUDE", { enumerable: true, get: function () { return config_1.ALWAYS_EXCLUDE; } });
|
|
17
|
+
Object.defineProperty(exports, "DEFAULT_CACHE_CONTROL", { enumerable: true, get: function () { return config_1.DEFAULT_CACHE_CONTROL; } });
|
|
18
|
+
Object.defineProperty(exports, "DEFAULT_INTERNAL_HEADER", { enumerable: true, get: function () { return config_1.DEFAULT_INTERNAL_HEADER; } });
|
|
19
|
+
Object.defineProperty(exports, "DEFAULT_INTERNAL_PREFIX", { enumerable: true, get: function () { return config_1.DEFAULT_INTERNAL_PREFIX; } });
|
|
20
|
+
Object.defineProperty(exports, "DEFAULT_NEGOTIATED_CACHE_CONTROL", { enumerable: true, get: function () { return config_1.DEFAULT_NEGOTIATED_CACHE_CONTROL; } });
|
|
21
|
+
Object.defineProperty(exports, "VIA_HEADER", { enumerable: true, get: function () { return config_1.VIA_HEADER; } });
|
|
22
|
+
Object.defineProperty(exports, "defineAgentReady", { enumerable: true, get: function () { return config_1.defineAgentReady; } });
|
|
23
|
+
Object.defineProperty(exports, "emitHit", { enumerable: true, get: function () { return config_1.emitHit; } });
|
|
24
|
+
Object.defineProperty(exports, "isEnabled", { enumerable: true, get: function () { return config_1.isEnabled; } });
|
|
25
|
+
Object.defineProperty(exports, "isExcluded", { enumerable: true, get: function () { return config_1.isExcluded; } });
|
|
26
|
+
var detect_1 = require("./core/detect");
|
|
27
|
+
Object.defineProperty(exports, "mdUrlPath", { enumerable: true, get: function () { return detect_1.mdUrlPath; } });
|
|
28
|
+
Object.defineProperty(exports, "normalizePath", { enumerable: true, get: function () { return detect_1.normalizePath; } });
|
|
29
|
+
Object.defineProperty(exports, "stripMdSuffix", { enumerable: true, get: function () { return detect_1.stripMdSuffix; } });
|
|
30
|
+
Object.defineProperty(exports, "wantsMarkdown", { enumerable: true, get: function () { return detect_1.wantsMarkdown; } });
|
|
31
|
+
var enrich_1 = require("./core/enrich");
|
|
32
|
+
Object.defineProperty(exports, "enrich", { enumerable: true, get: function () { return enrich_1.enrich; } });
|
|
33
|
+
Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return enrich_1.estimateTokens; } });
|
|
34
|
+
Object.defineProperty(exports, "frontMatter", { enumerable: true, get: function () { return enrich_1.frontMatter; } });
|
|
35
|
+
Object.defineProperty(exports, "hasFrontMatter", { enumerable: true, get: function () { return enrich_1.hasFrontMatter; } });
|
|
36
|
+
Object.defineProperty(exports, "jsonLdFacts", { enumerable: true, get: function () { return enrich_1.jsonLdFacts; } });
|
|
37
|
+
var llms_1 = require("./core/llms");
|
|
38
|
+
Object.defineProperty(exports, "buildLlmsTxt", { enumerable: true, get: function () { return llms_1.buildLlmsTxt; } });
|
|
39
|
+
var respond_1 = require("./core/respond");
|
|
40
|
+
Object.defineProperty(exports, "byteLength", { enumerable: true, get: function () { return respond_1.byteLength; } });
|
|
41
|
+
Object.defineProperty(exports, "etagFor", { enumerable: true, get: function () { return respond_1.etagFor; } });
|
|
42
|
+
Object.defineProperty(exports, "ifNoneMatchHits", { enumerable: true, get: function () { return respond_1.ifNoneMatchHits; } });
|
|
43
|
+
Object.defineProperty(exports, "markdownResponse", { enumerable: true, get: function () { return respond_1.markdownResponse; } });
|
|
44
|
+
Object.defineProperty(exports, "MARKDOWN_CONTENT_TYPE", { enumerable: true, get: function () { return respond_1.MARKDOWN_CONTENT_TYPE; } });
|
|
45
|
+
// Easy setup: smart defaults, llms.txt from the sitemap, dashboard reporting.
|
|
46
|
+
var easy_1 = require("./core/easy");
|
|
47
|
+
Object.defineProperty(exports, "COMMON_PRIVATE_PATHS", { enumerable: true, get: function () { return easy_1.COMMON_PRIVATE_PATHS; } });
|
|
48
|
+
Object.defineProperty(exports, "createAgentReady", { enumerable: true, get: function () { return easy_1.createAgentReady; } });
|
|
49
|
+
Object.defineProperty(exports, "enabledFromEnv", { enumerable: true, get: function () { return easy_1.enabledFromEnv; } });
|
|
50
|
+
Object.defineProperty(exports, "siteFromEnv", { enumerable: true, get: function () { return easy_1.siteFromEnv; } });
|
|
51
|
+
var sitemap_1 = require("./core/sitemap");
|
|
52
|
+
Object.defineProperty(exports, "entriesFromUrls", { enumerable: true, get: function () { return sitemap_1.entriesFromUrls; } });
|
|
53
|
+
Object.defineProperty(exports, "humanize", { enumerable: true, get: function () { return sitemap_1.humanize; } });
|
|
54
|
+
Object.defineProperty(exports, "parseSitemap", { enumerable: true, get: function () { return sitemap_1.parseSitemap; } });
|
|
55
|
+
Object.defineProperty(exports, "sitemapEntries", { enumerable: true, get: function () { return sitemap_1.sitemapEntries; } });
|
|
56
|
+
var report_1 = require("./core/report");
|
|
57
|
+
Object.defineProperty(exports, "reporter", { enumerable: true, get: function () { return report_1.reporter; } });
|
|
58
|
+
Object.defineProperty(exports, "reporterFromEnv", { enumerable: true, get: function () { return report_1.reporterFromEnv; } });
|
|
59
|
+
// Proof: who reads, how much, and how agent-ready a site is.
|
|
60
|
+
var bots_1 = require("./proof/bots");
|
|
61
|
+
Object.defineProperty(exports, "ANSWER_BOT_TOKENS", { enumerable: true, get: function () { return bots_1.ANSWER_BOT_TOKENS; } });
|
|
62
|
+
Object.defineProperty(exports, "classifyAgent", { enumerable: true, get: function () { return bots_1.classifyAgent; } });
|
|
63
|
+
Object.defineProperty(exports, "isAnswerBot", { enumerable: true, get: function () { return bots_1.isAnswerBot; } });
|
|
64
|
+
var hits_1 = require("./proof/hits");
|
|
65
|
+
Object.defineProperty(exports, "filterHits", { enumerable: true, get: function () { return hits_1.filterHits; } });
|
|
66
|
+
Object.defineProperty(exports, "memoryHitStore", { enumerable: true, get: function () { return hits_1.memoryHitStore; } });
|
|
67
|
+
Object.defineProperty(exports, "teeHits", { enumerable: true, get: function () { return hits_1.teeHits; } });
|
|
68
|
+
var stats_1 = require("./proof/stats");
|
|
69
|
+
Object.defineProperty(exports, "formatSummary", { enumerable: true, get: function () { return stats_1.formatSummary; } });
|
|
70
|
+
Object.defineProperty(exports, "summarize", { enumerable: true, get: function () { return stats_1.summarize; } });
|
|
71
|
+
var dashboard_1 = require("./proof/dashboard");
|
|
72
|
+
Object.defineProperty(exports, "renderDashboard", { enumerable: true, get: function () { return dashboard_1.renderDashboard; } });
|
|
73
|
+
var audit_1 = require("./proof/audit");
|
|
74
|
+
Object.defineProperty(exports, "auditSite", { enumerable: true, get: function () { return audit_1.auditSite; } });
|
|
75
|
+
Object.defineProperty(exports, "blockedBots", { enumerable: true, get: function () { return audit_1.blockedBots; } });
|
|
76
|
+
Object.defineProperty(exports, "formatAudit", { enumerable: true, get: function () { return audit_1.formatAudit; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HitStore } from "../proof/hits";
|
|
2
|
+
export interface DashboardRouteOptions {
|
|
3
|
+
store: HitStore;
|
|
4
|
+
/** Shown in the heading. */
|
|
5
|
+
site: string;
|
|
6
|
+
/** Access key. Default process.env.AGENT_READY_DASHBOARD_KEY. */
|
|
7
|
+
key?: string | null;
|
|
8
|
+
/** Default window in days. Default 30. */
|
|
9
|
+
days?: number;
|
|
10
|
+
now?: () => Date;
|
|
11
|
+
}
|
|
12
|
+
export declare function createDashboardRoute(opts: DashboardRouteOptions): {
|
|
13
|
+
GET: (req: Request) => Promise<Response>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The dashboard as a Next.js route (Proof). Behind a key; HTML or JSON.
|
|
3
|
+
//
|
|
4
|
+
// // app/agent-ready-dashboard/route.ts
|
|
5
|
+
// import { createDashboardRoute } from "@relvehq/agent-ready/next/dashboard";
|
|
6
|
+
// export const { GET } = createDashboardRoute({ store: hits, site: "acme.com" });
|
|
7
|
+
// export const dynamic = "force-dynamic";
|
|
8
|
+
//
|
|
9
|
+
// GET /agent-ready-dashboard?key=… the page
|
|
10
|
+
// GET /agent-ready-dashboard?key=…&format=json the summary as JSON
|
|
11
|
+
// GET /agent-ready-dashboard?days=7 last 7 days (default 30)
|
|
12
|
+
//
|
|
13
|
+
// The key is AGENT_READY_DASHBOARD_KEY (or `key`), sent as `?key=` or
|
|
14
|
+
// `Authorization: Bearer`. No key configured -> 404: a dashboard is never
|
|
15
|
+
// public by accident. EDGE-SAFE (works in either runtime).
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createDashboardRoute = createDashboardRoute;
|
|
18
|
+
const dashboard_1 = require("../proof/dashboard");
|
|
19
|
+
const stats_1 = require("../proof/stats");
|
|
20
|
+
function createDashboardRoute(opts) {
|
|
21
|
+
const GET = async (req) => {
|
|
22
|
+
const key = opts.key !== undefined ? opts.key : typeof process !== "undefined" ? process.env.AGENT_READY_DASHBOARD_KEY ?? null : null;
|
|
23
|
+
if (!key)
|
|
24
|
+
return new Response("Not found", { status: 404, headers: { "cache-control": "no-store" } });
|
|
25
|
+
const url = new URL(req.url);
|
|
26
|
+
const given = url.searchParams.get("key") ?? req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
|
|
27
|
+
if (!safeEqual(given, key)) {
|
|
28
|
+
return new Response("Unauthorized", { status: 401, headers: { "cache-control": "no-store", "www-authenticate": "Bearer" } });
|
|
29
|
+
}
|
|
30
|
+
const days = clampDays(url.searchParams.get("days"), opts.days ?? 30);
|
|
31
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
32
|
+
const since = new Date(now.getTime() - days * 86400000).toISOString();
|
|
33
|
+
const summary = (0, stats_1.summarize)(await opts.store.list({ since }));
|
|
34
|
+
const headers = { "cache-control": "no-store", "x-robots-tag": "noindex, nofollow" };
|
|
35
|
+
if (url.searchParams.get("format") === "json") {
|
|
36
|
+
return new Response(JSON.stringify({ site: opts.site, days, ...summary }, null, 2), {
|
|
37
|
+
headers: { ...headers, "content-type": "application/json; charset=utf-8" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return new Response((0, dashboard_1.renderDashboard)(summary, { site: opts.site, days }), {
|
|
41
|
+
headers: { ...headers, "content-type": "text/html; charset=utf-8" },
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
return { GET };
|
|
45
|
+
}
|
|
46
|
+
function clampDays(raw, fallback) {
|
|
47
|
+
const d = Number(raw);
|
|
48
|
+
return Number.isFinite(d) && d >= 1 ? Math.min(Math.floor(d), 365) : fallback;
|
|
49
|
+
}
|
|
50
|
+
/** Constant-time string compare, so the key can't be guessed by timing. */
|
|
51
|
+
function safeEqual(a, b) {
|
|
52
|
+
let diff = a.length ^ b.length;
|
|
53
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++)
|
|
54
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
55
|
+
return diff === 0;
|
|
56
|
+
}
|