wiki-formant 0.13.0 → 0.16.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/README.md +12 -25
- package/bin/check-classes.mjs +2 -3
- package/dist/analytics.d.ts +0 -1
- package/dist/analytics.js +0 -1
- package/dist/block-views.d.ts +97 -0
- package/dist/block-views.js +119 -0
- package/dist/blocks.d.ts +0 -1
- package/dist/blocks.js +1 -2
- package/dist/combobox.d.ts +0 -1
- package/dist/combobox.js +0 -1
- package/dist/conformance.d.ts +0 -3
- package/dist/conformance.js +40 -38
- package/dist/crawlers.d.ts +0 -1
- package/dist/crawlers.js +0 -1
- package/dist/dom.d.ts +0 -1
- package/dist/dom.js +0 -1
- package/dist/editor.d.ts +101 -0
- package/dist/editor.js +218 -0
- package/dist/feed.d.ts +0 -1
- package/dist/feed.js +0 -1
- package/dist/freshness.d.ts +0 -1
- package/dist/freshness.js +2 -2
- package/dist/headings.d.ts +0 -1
- package/dist/headings.js +1 -3
- package/dist/html.d.ts +16 -0
- package/dist/html.js +23 -0
- package/dist/http.d.ts +20 -1
- package/dist/http.js +23 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/license.d.ts +0 -1
- package/dist/license.js +0 -1
- package/dist/link-check.d.ts +92 -0
- package/dist/link-check.js +249 -0
- package/dist/links.d.ts +0 -1
- package/dist/links.js +1 -6
- package/dist/maps.d.ts +0 -1
- package/dist/maps.js +2 -2
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +34 -3
- package/dist/mcp.d.ts +17 -7
- package/dist/mcp.js +8 -8
- package/dist/pagination.d.ts +19 -1
- package/dist/pagination.js +21 -1
- package/dist/rate-limit.d.ts +18 -1
- package/dist/rate-limit.js +22 -1
- package/dist/react-server.d.ts +91 -9
- package/dist/react-server.js +68 -16
- package/dist/react.d.ts +37 -48
- package/dist/react.js +44 -23
- package/dist/revisions.d.ts +0 -1
- package/dist/revisions.js +0 -1
- package/dist/rola.d.ts +0 -1
- package/dist/rola.js +0 -1
- package/dist/seeded.d.ts +11 -0
- package/dist/seeded.js +32 -0
- package/dist/sidebar.d.ts +0 -1
- package/dist/sidebar.js +0 -1
- package/dist/taxonomy.d.ts +0 -1
- package/dist/taxonomy.js +0 -1
- package/dist/text.d.ts +14 -1
- package/dist/text.js +15 -2
- package/dist/tiptap.d.ts +1 -1
- package/dist/tiptap.js +1 -2
- package/dist/validation.d.ts +2 -3
- package/dist/validation.js +4 -5
- package/dist/versioning.d.ts +0 -1
- package/dist/versioning.js +0 -1
- package/dist/well-known.d.ts +19 -2
- package/dist/well-known.js +28 -2
- package/dist/x402.d.ts +2 -11
- package/dist/x402.js +4 -16
- package/package.json +52 -27
- package/dist/analytics.d.ts.map +0 -1
- package/dist/analytics.js.map +0 -1
- package/dist/blocks.d.ts.map +0 -1
- package/dist/blocks.js.map +0 -1
- package/dist/combobox.d.ts.map +0 -1
- package/dist/combobox.js.map +0 -1
- package/dist/conformance.d.ts.map +0 -1
- package/dist/conformance.js.map +0 -1
- package/dist/crawlers.d.ts.map +0 -1
- package/dist/crawlers.js.map +0 -1
- package/dist/dom.d.ts.map +0 -1
- package/dist/dom.js.map +0 -1
- package/dist/entities.d.ts +0 -3
- package/dist/entities.d.ts.map +0 -1
- package/dist/entities.js +0 -30
- package/dist/entities.js.map +0 -1
- package/dist/feed.d.ts.map +0 -1
- package/dist/feed.js.map +0 -1
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js.map +0 -1
- package/dist/headings.d.ts.map +0 -1
- package/dist/headings.js.map +0 -1
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/license.d.ts.map +0 -1
- package/dist/license.js.map +0 -1
- package/dist/links.d.ts.map +0 -1
- package/dist/links.js.map +0 -1
- package/dist/maps.d.ts.map +0 -1
- package/dist/maps.js.map +0 -1
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js.map +0 -1
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js.map +0 -1
- package/dist/pagination.d.ts.map +0 -1
- package/dist/pagination.js.map +0 -1
- package/dist/rate-limit.d.ts.map +0 -1
- package/dist/rate-limit.js.map +0 -1
- package/dist/react-server.d.ts.map +0 -1
- package/dist/react-server.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/revisions.d.ts.map +0 -1
- package/dist/revisions.js.map +0 -1
- package/dist/rola.d.ts.map +0 -1
- package/dist/rola.js.map +0 -1
- package/dist/sidebar.d.ts.map +0 -1
- package/dist/sidebar.js.map +0 -1
- package/dist/taxonomy.d.ts.map +0 -1
- package/dist/taxonomy.js.map +0 -1
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js.map +0 -1
- package/dist/tiptap.d.ts.map +0 -1
- package/dist/tiptap.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/versioning.d.ts.map +0 -1
- package/dist/versioning.js.map +0 -1
- package/dist/well-known.d.ts.map +0 -1
- package/dist/well-known.js.map +0 -1
- package/dist/x402.d.ts.map +0 -1
- package/dist/x402.js.map +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// link-check.ts — the dead-link probe both wikis' sweep scripts had written.
|
|
2
|
+
//
|
|
3
|
+
// Node-only (fetch, AbortSignal, URL). No framework, no database: what a
|
|
4
|
+
// checker does with the verdicts — which pages to walk, what counts as an
|
|
5
|
+
// internal path, how to report — stays with the caller, because that is the
|
|
6
|
+
// half that is genuinely per-wiki.
|
|
7
|
+
//
|
|
8
|
+
// EVERY GUARD BELOW WAS PAID FOR BY A FALSE POSITIVE, and the two copies had
|
|
9
|
+
// each learned a different half of the lesson. One knew that npmjs.com 403s
|
|
10
|
+
// scripted requests, that an expired certificate is not a dead host, and that a
|
|
11
|
+
// YouTube /embed/ URL answers 200 for a deleted video. The other knew that a
|
|
12
|
+
// connect refusal is usually concurrency rather than death, and that
|
|
13
|
+
// serialising per hostname is what fixes it. Neither copy was behind. A sweep
|
|
14
|
+
// running either one alone strips good citations for reasons the other repo had
|
|
15
|
+
// already written down — which is the entire argument for this file.
|
|
16
|
+
import { stripTags } from './html.js';
|
|
17
|
+
const DEFAULTS = { timeoutMs: 12_000, slowTimeoutMs: 40_000 };
|
|
18
|
+
/**
|
|
19
|
+
* TLS-verification failures are NOT death.
|
|
20
|
+
*
|
|
21
|
+
* A Let's Encrypt certificate on a DAO's own consultation platform — cited 20
|
|
22
|
+
* times across 7 pages — expired at 13:47 UTC, and every one of those citations
|
|
23
|
+
* started reading as `status: 0, "fetch failed"`, indistinguishable from a
|
|
24
|
+
* vanished host. Behind the interstitial the site answered 200. undici buries
|
|
25
|
+
* the reason in `err.cause`, so it is surfaced and labelled: a run that cannot
|
|
26
|
+
* tell an expired cert from a dead domain will eventually strip good citations
|
|
27
|
+
* over a lapsed renewal.
|
|
28
|
+
*/
|
|
29
|
+
const TLS_CODES = new Set([
|
|
30
|
+
'CERT_HAS_EXPIRED',
|
|
31
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
32
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
33
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
34
|
+
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Codes that flap. A sweep found 16 status-0 rows and 15 were one host, every
|
|
38
|
+
* one answering 200 when probed alone. Carry the code through so a caller can
|
|
39
|
+
* treat these as retryable rather than banking a death.
|
|
40
|
+
*/
|
|
41
|
+
export const TRANSIENT_CODES = new Set([
|
|
42
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
43
|
+
'UND_ERR_SOCKET',
|
|
44
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
45
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
46
|
+
'ECONNRESET',
|
|
47
|
+
'ECONNREFUSED',
|
|
48
|
+
'ETIMEDOUT',
|
|
49
|
+
'EAI_AGAIN',
|
|
50
|
+
'EPIPE',
|
|
51
|
+
]);
|
|
52
|
+
/** A failure as a report row: the message, the code, and the TLS caveat. */
|
|
53
|
+
export function describeFailure(err) {
|
|
54
|
+
const e = err;
|
|
55
|
+
if (e?.name === 'AbortError')
|
|
56
|
+
return { error: 'timeout' };
|
|
57
|
+
const code = e?.cause?.code ?? e?.code;
|
|
58
|
+
const error = code ? `${e.message} (${code})` : (e?.message ?? 'fetch failed');
|
|
59
|
+
if (code && TLS_CODES.has(code)) {
|
|
60
|
+
return {
|
|
61
|
+
error,
|
|
62
|
+
code,
|
|
63
|
+
tls: code,
|
|
64
|
+
note: 'TLS verification failed — the host may well be serving fine behind the interstitial; confirm before touching the citation',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return code ? { error, code } : { error };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The URL to probe INSTEAD, where the public page lies about its own health.
|
|
71
|
+
*
|
|
72
|
+
* npmjs.com serves 403 to scripted requests whether or not the package exists,
|
|
73
|
+
* which made every package citation a permanent false positive. The registry
|
|
74
|
+
* answers honestly.
|
|
75
|
+
*/
|
|
76
|
+
export function probeUrlFor(url) {
|
|
77
|
+
const npm = url.match(/^https:\/\/(?:www\.)?npmjs\.com\/package\/(.+?)\/?$/);
|
|
78
|
+
return npm ? `https://registry.npmjs.org/${npm[1]}` : url;
|
|
79
|
+
}
|
|
80
|
+
// ---- per-host serialisation -------------------------------------------------
|
|
81
|
+
const hostQueues = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Run `fn` with at most one request in flight against this hostname.
|
|
84
|
+
*
|
|
85
|
+
* ECONNREFUSED looks deterministic and is not. A delayed retry was tried first
|
|
86
|
+
* and made things worse — 269 to 277 broken, the refusals unchanged and 8 fresh
|
|
87
|
+
* timeouts on hosts that had been fine — because the problem is not timing, it
|
|
88
|
+
* is how many sockets one host is asked for at once. The global pool still runs
|
|
89
|
+
* wide across DIFFERENT hosts; any single host is probed one request at a time,
|
|
90
|
+
* exactly as a hand re-probe does it.
|
|
91
|
+
*/
|
|
92
|
+
export function perHost(url, fn) {
|
|
93
|
+
let host;
|
|
94
|
+
try {
|
|
95
|
+
host = new URL(url).hostname;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return fn();
|
|
99
|
+
}
|
|
100
|
+
const prev = hostQueues.get(host) ?? Promise.resolve();
|
|
101
|
+
// `.then(fn, fn)` so one rejection does not poison the rest of the queue.
|
|
102
|
+
const next = prev.then(fn, fn);
|
|
103
|
+
hostQueues.set(host, next.catch(() => undefined));
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
async function attempt(url, budgetMs) {
|
|
107
|
+
// One signal for both requests, so the budget covers the HEAD *and* the GET
|
|
108
|
+
// rather than resetting between them. `engines.node` is >= 20, so this needs
|
|
109
|
+
// no controller and no `clearTimeout` in a `finally` that could be forgotten.
|
|
110
|
+
const signal = AbortSignal.timeout(budgetMs);
|
|
111
|
+
try {
|
|
112
|
+
let res = await fetch(url, { method: 'HEAD', redirect: 'follow', signal });
|
|
113
|
+
// Some hosts reject HEAD outright; retry with GET before believing a 4xx.
|
|
114
|
+
if (res.status >= 400) {
|
|
115
|
+
res = await fetch(url, { method: 'GET', redirect: 'follow', signal });
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
url,
|
|
119
|
+
status: res.status,
|
|
120
|
+
ok: res.status < 400,
|
|
121
|
+
contentType: (res.headers.get('content-type') || '').split(';')[0]?.trim(),
|
|
122
|
+
bytes: Number(res.headers.get('content-length') || 0),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return { url, status: 0, ok: false, ...describeFailure(err) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Probe one URL, honestly.
|
|
131
|
+
*
|
|
132
|
+
* A timeout is not a death and neither is a dropped connection: slow replays —
|
|
133
|
+
* web.archive.org above all, which a sweep leans on to rescue dying citations —
|
|
134
|
+
* exceed the ordinary budget and land as status 0. So a status-0 result with a
|
|
135
|
+
* transient code earns one more attempt on the longer budget before it is
|
|
136
|
+
* reported. A 404 does not: it is an answer.
|
|
137
|
+
*/
|
|
138
|
+
export async function probeUrl(url, opts = {}) {
|
|
139
|
+
const { timeoutMs, slowTimeoutMs } = { ...DEFAULTS, ...opts };
|
|
140
|
+
const target = probeUrlFor(url);
|
|
141
|
+
const first = await perHost(target, () => attempt(target, timeoutMs));
|
|
142
|
+
const retryable = !first.ok && first.status === 0 && (!first.code || TRANSIENT_CODES.has(first.code));
|
|
143
|
+
if (!retryable)
|
|
144
|
+
return { ...first, url };
|
|
145
|
+
const second = await perHost(target, () => attempt(target, slowTimeoutMs));
|
|
146
|
+
return { ...second, url };
|
|
147
|
+
}
|
|
148
|
+
// ---- YouTube ----------------------------------------------------------------
|
|
149
|
+
export const YOUTUBE_EMBED = /^https?:\/\/(?:www\.)?(?:youtube-nocookie\.com|youtube\.com)\/embed\/([\w-]+)/;
|
|
150
|
+
/**
|
|
151
|
+
* The same video cited as a LINK rather than an iframe. `youtu.be/<id>` 303s and
|
|
152
|
+
* `youtube.com/watch?v=<id>` 200s for deleted and private videos alike, so an
|
|
153
|
+
* anchor carrying a dead video is invisible to a status check — 55 of them went
|
|
154
|
+
* unprobed corpus-wide until one sweep resolved them and found 7 unwatchable.
|
|
155
|
+
*/
|
|
156
|
+
const YOUTUBE_WATCH = /^https?:\/\/(?:(?:www\.)?youtube\.com\/(?:watch\?(?:[^#]*&)?v=|shorts\/|live\/)|youtu\.be\/)([\w-]{6,})/;
|
|
157
|
+
/**
|
|
158
|
+
* A YouTube `/embed/<id>` URL answers 200 for private, deleted and
|
|
159
|
+
* playback-restricted videos alike, so HEADing it can never spot a dead hero
|
|
160
|
+
* video. oEmbed can:
|
|
161
|
+
*
|
|
162
|
+
* 200 → public and embeddable | 404 → deleted | 401/403 → private or embedding off
|
|
163
|
+
*/
|
|
164
|
+
export async function probeYouTube(videoId, opts = {}) {
|
|
165
|
+
const { timeoutMs } = { ...DEFAULTS, ...opts };
|
|
166
|
+
const oembed = `https://www.youtube.com/oembed?url=https%3A//www.youtube.com/watch%3Fv%3D${videoId}&format=json`;
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(oembed, {
|
|
169
|
+
method: 'GET',
|
|
170
|
+
redirect: 'follow',
|
|
171
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
172
|
+
});
|
|
173
|
+
if (res.status === 404)
|
|
174
|
+
return { status: 404, ok: false, reason: 'deleted' };
|
|
175
|
+
if (res.status === 401 || res.status === 403) {
|
|
176
|
+
return { status: res.status, ok: false, reason: 'restricted (private or embedding disabled)' };
|
|
177
|
+
}
|
|
178
|
+
return { status: res.status, ok: res.status < 400 };
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return { status: 0, ok: false, ...describeFailure(err) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** An external anchor, with a video treated as a video. */
|
|
185
|
+
export async function probeExternal(url, opts = {}) {
|
|
186
|
+
const yt = url.match(YOUTUBE_WATCH);
|
|
187
|
+
if (yt?.[1])
|
|
188
|
+
return { url, videoId: yt[1], ...(await probeYouTube(yt[1], opts)) };
|
|
189
|
+
return probeUrl(url, opts);
|
|
190
|
+
}
|
|
191
|
+
// ---- unverifiable hosts -----------------------------------------------------
|
|
192
|
+
/**
|
|
193
|
+
* Hosts that answer 200 with a JavaScript loader shell regardless of whether
|
|
194
|
+
* the deck / store / dataset behind the query string still exists. A status
|
|
195
|
+
* check on these is meaningless, so a caller should report them as unverifiable
|
|
196
|
+
* rather than healthy — a green row is worse than an unknown one, because
|
|
197
|
+
* nobody looks at it again.
|
|
198
|
+
*/
|
|
199
|
+
export function unverifiableReason(url, hosts) {
|
|
200
|
+
let host;
|
|
201
|
+
try {
|
|
202
|
+
host = new URL(url).hostname.replace(/^www\./, '');
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
for (const [h, reason] of hosts) {
|
|
208
|
+
if (host === h || host.endsWith(`.${h}`))
|
|
209
|
+
return reason;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
// ---- extraction -------------------------------------------------------------
|
|
214
|
+
const LINK_RE = /<a\s+[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
215
|
+
const EMBED_RE = /<(iframe|img)\b[^>]*?\ssrc="([^"]*)"/gi;
|
|
216
|
+
/** Every anchor in a fragment, as `{ href, text }`. */
|
|
217
|
+
export function extractLinks(html) {
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const m of html.matchAll(LINK_RE)) {
|
|
220
|
+
if (m[1])
|
|
221
|
+
out.push({ href: m[1], text: stripTags(m[2] ?? '') });
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** Every `<iframe>` and `<img>` source in a fragment. */
|
|
226
|
+
export function extractEmbeds(html) {
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const m of html.matchAll(EMBED_RE)) {
|
|
229
|
+
if (m[1] && m[2])
|
|
230
|
+
out.push({ kind: m[1].toLowerCase(), url: m[2] });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
// ---- concurrency ------------------------------------------------------------
|
|
235
|
+
/** `Promise.all` with a ceiling, preserving input order in the results. */
|
|
236
|
+
export async function mapLimit(items, limit, fn) {
|
|
237
|
+
const results = new Array(items.length);
|
|
238
|
+
let cursor = 0;
|
|
239
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
240
|
+
for (;;) {
|
|
241
|
+
const i = cursor++;
|
|
242
|
+
if (i >= items.length)
|
|
243
|
+
return;
|
|
244
|
+
results[i] = await fn(items[i], i);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
await Promise.all(workers);
|
|
248
|
+
return results;
|
|
249
|
+
}
|
package/dist/links.d.ts
CHANGED
package/dist/links.js
CHANGED
|
@@ -14,11 +14,7 @@
|
|
|
14
14
|
// What they drifted on: only radix-wiki synthesised a label for an anchor with
|
|
15
15
|
// no visible text. That is an accessibility fix, not a preference, so it is the
|
|
16
16
|
// default here and caper gains it by adopting this module.
|
|
17
|
-
|
|
18
|
-
const getAttr = (attrs, name) => attrs.match(new RegExp(`\\s${name}\\s*=\\s*"([^"]*)"`, 'i'))?.[1] ?? null;
|
|
19
|
-
/** Drop every occurrence of one attribute from a tag's attribute string. */
|
|
20
|
-
const removeAttr = (attrs, name) => attrs.replace(new RegExp(`\\s${name}\\s*=\\s*"[^"]*"`, 'gi'), '');
|
|
21
|
-
const stripTags = (s) => s.replace(/<[^>]*>/g, '').trim();
|
|
17
|
+
import { getAttr, removeAttr, stripTags } from './html.js';
|
|
22
18
|
/**
|
|
23
19
|
* A readable label for an anchor whose text is empty, derived from its href.
|
|
24
20
|
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
@@ -78,4 +74,3 @@ export function normaliseLinks(html, options = {}) {
|
|
|
78
74
|
const self = options.selfHost ? selfHostPattern(options.selfHost) : null;
|
|
79
75
|
return html.replace(ANCHOR, (_m, attrs, inner) => normaliseAnchor(attrs, inner, options, self));
|
|
80
76
|
}
|
|
81
|
-
//# sourceMappingURL=links.js.map
|
package/dist/maps.d.ts
CHANGED
|
@@ -18,4 +18,3 @@ export declare function extractCoordsFromUrl(url: string): MapCoords | null;
|
|
|
18
18
|
export declare function toMapEmbedUrl(url: string): string | null;
|
|
19
19
|
/** True for the shortener forms that only a redirect can resolve. */
|
|
20
20
|
export declare const isShortMapUrl: (url: string) => boolean;
|
|
21
|
-
//# sourceMappingURL=maps.d.ts.map
|
package/dist/maps.js
CHANGED
|
@@ -23,7 +23,8 @@ export function extractCoordsFromUrl(url) {
|
|
|
23
23
|
if (data)
|
|
24
24
|
return { lat: +data[1], lon: +data[2] };
|
|
25
25
|
try {
|
|
26
|
-
const
|
|
26
|
+
const params = new URL(url).searchParams;
|
|
27
|
+
const ll = params.get('ll') ?? params.get('sll');
|
|
27
28
|
if (ll) {
|
|
28
29
|
const [lat, lon] = ll.split(',').map(Number);
|
|
29
30
|
if (lat !== undefined && lon !== undefined && !isNaN(lat) && !isNaN(lon))
|
|
@@ -74,4 +75,3 @@ export function toMapEmbedUrl(url) {
|
|
|
74
75
|
}
|
|
75
76
|
/** True for the shortener forms that only a redirect can resolve. */
|
|
76
77
|
export const isShortMapUrl = (url) => /maps\.app\.goo\.gl|goo\.gl\/maps/.test(url);
|
|
77
|
-
//# sourceMappingURL=maps.js.map
|
package/dist/markdown.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function decodeEntities(s: string): string;
|
|
1
2
|
/** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
|
|
2
3
|
export declare function inlineToMarkdown(html: string): string;
|
|
3
4
|
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
|
|
@@ -22,4 +23,3 @@ export interface FrontmatterFields {
|
|
|
22
23
|
export declare function frontmatter(fields: FrontmatterFields): string;
|
|
23
24
|
/** A complete markdown document: frontmatter, an H1, and a body you supply. */
|
|
24
25
|
export declare function markdownDocument(fields: FrontmatterFields, body: string): string;
|
|
25
|
-
//# sourceMappingURL=markdown.d.ts.map
|
package/dist/markdown.js
CHANGED
|
@@ -9,7 +9,40 @@
|
|
|
9
9
|
// so `blocksToMarkdown` belongs in the app. Give this module HTML and it gives
|
|
10
10
|
// you markdown; give `frontmatter` the fields and it gives you the document
|
|
11
11
|
// head. That is the whole portable half.
|
|
12
|
-
import {
|
|
12
|
+
import { isoDate } from './html.js';
|
|
13
|
+
// ---- entity decoding --------------------------------------------------------
|
|
14
|
+
//
|
|
15
|
+
// Rich-text editors store typographic punctuation as named entities, so pages
|
|
16
|
+
// carry raw `—` / `“` / `’` into every text export — ampersand
|
|
17
|
+
// soup an agent has to read through. Numeric forms decode generically; the named
|
|
18
|
+
// table covers what wiki corpora actually use (punctuation, arrows, maths,
|
|
19
|
+
// accented Latin, Greek) rather than attempting the full HTML5 list.
|
|
20
|
+
//
|
|
21
|
+
// Lived in its own `entities.ts` with no subpath of its own, so the only way to
|
|
22
|
+
// reach `decodeEntities` was the root barrel — and the README claimed it came
|
|
23
|
+
// from here, which is where it now actually is.
|
|
24
|
+
const NAMED_ENTITIES = {
|
|
25
|
+
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
|
|
26
|
+
mdash: '—', ndash: '–', hellip: '…', middot: '·', bull: '•', sect: '§', para: '¶', dagger: '†',
|
|
27
|
+
ldquo: '“', rdquo: '”', lsquo: '‘', rsquo: '’', laquo: '«', raquo: '»',
|
|
28
|
+
larr: '←', rarr: '→', uarr: '↑', darr: '↓', harr: '↔',
|
|
29
|
+
times: '×', divide: '÷', minus: '−', plusmn: '±', ne: '≠', le: '≤', ge: '≥',
|
|
30
|
+
asymp: '≈', radic: '√', infin: '∞', sum: '∑', prod: '∏', int: '∫', deg: '°', permil: '‰',
|
|
31
|
+
sup2: '²', sup3: '³', frac12: '½', frac14: '¼', frac34: '¾',
|
|
32
|
+
euro: '€', pound: '£', yen: '¥', cent: '¢', copy: '©', reg: '®', trade: '™',
|
|
33
|
+
eacute: 'é', egrave: 'è', ecirc: 'ê', aacute: 'á', agrave: 'à', acirc: 'â', aring: 'å',
|
|
34
|
+
auml: 'ä', ouml: 'ö', uuml: 'ü', iacute: 'í', oacute: 'ó', uacute: 'ú', ntilde: 'ñ',
|
|
35
|
+
ccedil: 'ç', oslash: 'ø', szlig: 'ß', aelig: 'æ',
|
|
36
|
+
alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', epsilon: 'ε', theta: 'θ', lambda: 'λ',
|
|
37
|
+
mu: 'μ', nu: 'ν', pi: 'π', rho: 'ρ', sigma: 'σ', tau: 'τ', phi: 'φ', omega: 'ω',
|
|
38
|
+
Delta: 'Δ', Sigma: 'Σ', Omega: 'Ω', Lambda: 'Λ', Phi: 'Φ', Pi: 'Π',
|
|
39
|
+
};
|
|
40
|
+
export function decodeEntities(s) {
|
|
41
|
+
return s
|
|
42
|
+
.replace(/&#x([0-9a-fA-F]{1,6});/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
43
|
+
.replace(/&#(\d{1,7});/g, (_m, dec) => String.fromCodePoint(Number(dec)))
|
|
44
|
+
.replace(/&([a-zA-Z][a-zA-Z0-9]{1,9});/g, (m, name) => NAMED_ENTITIES[name] ?? m);
|
|
45
|
+
}
|
|
13
46
|
/** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
|
|
14
47
|
export function inlineToMarkdown(html) {
|
|
15
48
|
return decodeEntities(html
|
|
@@ -81,7 +114,6 @@ export function htmlToMarkdown(html) {
|
|
|
81
114
|
.replace(/\n{3,}/g, '\n\n')
|
|
82
115
|
.trim();
|
|
83
116
|
}
|
|
84
|
-
const isoDate = (d) => (typeof d === 'string' ? d : d.toISOString()).split('T')[0];
|
|
85
117
|
/** YAML frontmatter block, `---` fences included. */
|
|
86
118
|
export function frontmatter(fields) {
|
|
87
119
|
const esc = (s) => s.replace(/"/g, '\\"');
|
|
@@ -104,4 +136,3 @@ export function frontmatter(fields) {
|
|
|
104
136
|
export function markdownDocument(fields, body) {
|
|
105
137
|
return `${frontmatter(fields)}\n\n# ${fields.title}\n\n${body.trim()}\n`;
|
|
106
138
|
}
|
|
107
|
-
//# sourceMappingURL=markdown.js.map
|
package/dist/mcp.d.ts
CHANGED
|
@@ -42,12 +42,6 @@ export type ToolSchema = {
|
|
|
42
42
|
* calls has no way to tell a lookup from one that reaches a payment processor.
|
|
43
43
|
*/
|
|
44
44
|
export type ToolAnnotations = {
|
|
45
|
-
/**
|
|
46
|
-
* Human-facing label — the pre-2025-06-18 slot. `tools/list` hoists it to
|
|
47
|
-
* the top-level `title` when that is absent, so a manifest written against
|
|
48
|
-
* either revision renders the same in a client. Prefer `McpTool.title`.
|
|
49
|
-
*/
|
|
50
|
-
title?: string;
|
|
51
45
|
/** Does not modify anything. */
|
|
52
46
|
readOnlyHint?: boolean;
|
|
53
47
|
/** May destroy or overwrite state (only meaningful when not read-only). */
|
|
@@ -192,6 +186,23 @@ export type RpcRequest = {
|
|
|
192
186
|
method: string;
|
|
193
187
|
params?: unknown;
|
|
194
188
|
};
|
|
189
|
+
/** How many entries one JSON-RPC batch may carry. Quoted by the conformance suite. */
|
|
190
|
+
export declare const DEFAULT_MAX_BATCH = 20;
|
|
191
|
+
/**
|
|
192
|
+
* A tool result envelope. Exported because `x402.ts` builds the same shape when
|
|
193
|
+
* it withholds a paid call, and had grown two more copies of it doing so.
|
|
194
|
+
*/
|
|
195
|
+
export declare const toolText: (id: RpcRequest["id"], text: string, isError?: boolean, extra?: Record<string, unknown>) => {
|
|
196
|
+
jsonrpc: "2.0";
|
|
197
|
+
id: string | number | null;
|
|
198
|
+
result: {
|
|
199
|
+
isError?: boolean | undefined;
|
|
200
|
+
content: {
|
|
201
|
+
type: string;
|
|
202
|
+
text: string;
|
|
203
|
+
}[];
|
|
204
|
+
};
|
|
205
|
+
};
|
|
195
206
|
/** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
|
|
196
207
|
interface Dispatch {
|
|
197
208
|
protocolVersion: McpProtocolVersion;
|
|
@@ -222,4 +233,3 @@ export declare function mcpRateLimited(retryAfterSec: number, message?: string):
|
|
|
222
233
|
/** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
|
|
223
234
|
export declare function mcpResponse(request: Request, config: McpServerConfig): Promise<Response>;
|
|
224
235
|
export {};
|
|
225
|
-
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/mcp.js
CHANGED
|
@@ -52,7 +52,8 @@ export class McpToolError extends Error {
|
|
|
52
52
|
this.name = 'McpToolError';
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/** How many entries one JSON-RPC batch may carry. Quoted by the conformance suite. */
|
|
56
|
+
export const DEFAULT_MAX_BATCH = 20;
|
|
56
57
|
const quote = (list) => list.map(s => `"${s}"`).join(', ');
|
|
57
58
|
function typeOf(v) {
|
|
58
59
|
if (Array.isArray(v))
|
|
@@ -66,7 +67,11 @@ const rpcError = (id, code, message, data) => ({
|
|
|
66
67
|
id,
|
|
67
68
|
error: { code, message, ...(data ? { data } : {}) },
|
|
68
69
|
});
|
|
69
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A tool result envelope. Exported because `x402.ts` builds the same shape when
|
|
72
|
+
* it withholds a paid call, and had grown two more copies of it doing so.
|
|
73
|
+
*/
|
|
74
|
+
export const toolText = (id, text, isError = false, extra = {}) => ({
|
|
70
75
|
jsonrpc: '2.0',
|
|
71
76
|
id,
|
|
72
77
|
result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}), ...extra },
|
|
@@ -188,11 +193,7 @@ async function handleRpc(req, config, dispatch) {
|
|
|
188
193
|
result: {
|
|
189
194
|
tools: config.tools.map(({ name, title, description, inputSchema, outputSchema, annotations }) => ({
|
|
190
195
|
name,
|
|
191
|
-
|
|
192
|
-
// rather than requiring every manifest to be rewritten: a
|
|
193
|
-
// client reading only the new slot showed a raw tool name for
|
|
194
|
-
// every surface that had not moved its label yet.
|
|
195
|
-
...(title ?? annotations?.title ? { title: title ?? annotations.title } : {}),
|
|
196
|
+
...(title ? { title } : {}),
|
|
196
197
|
description,
|
|
197
198
|
inputSchema,
|
|
198
199
|
...(outputSchema ? { outputSchema } : {}),
|
|
@@ -433,4 +434,3 @@ export async function mcpResponse(request, config) {
|
|
|
433
434
|
}
|
|
434
435
|
return Response.json(result, { headers });
|
|
435
436
|
}
|
|
436
|
-
//# sourceMappingURL=mcp.js.map
|
package/dist/pagination.d.ts
CHANGED
|
@@ -49,4 +49,22 @@ export declare function toOffset({ page, pageSize }: Pagination): {
|
|
|
49
49
|
skip: number;
|
|
50
50
|
take: number;
|
|
51
51
|
};
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The entries either side of the current one in an already-ordered list.
|
|
54
|
+
*
|
|
55
|
+
* The ordering is the caller's, deliberately — it is the one thing here that is
|
|
56
|
+
* never portable. A wiki's sequence is its section's configured sort, a
|
|
57
|
+
* knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
|
|
58
|
+
* What both wikis had written twice is this scan, not the sort.
|
|
59
|
+
*
|
|
60
|
+
* `null` on both sides when the page is not in the list, so a page reached by a
|
|
61
|
+
* URL its own section does not list renders no nav rather than a wrong one.
|
|
62
|
+
*
|
|
63
|
+
* No query. Both callers already hold the ordered siblings for something else —
|
|
64
|
+
* a related-pages panel, a section listing — and the two indexed lookups the
|
|
65
|
+
* neighbours used to cost were the reason one wiki dropped the control.
|
|
66
|
+
*/
|
|
67
|
+
export declare function adjacentPages<T>(ordered: readonly T[], isCurrent: (page: T) => boolean): {
|
|
68
|
+
prev: T | null;
|
|
69
|
+
next: T | null;
|
|
70
|
+
};
|
package/dist/pagination.js
CHANGED
|
@@ -56,4 +56,24 @@ export function listEnvelope(pages, total, page, pageSize, emptyNote) {
|
|
|
56
56
|
export function toOffset({ page, pageSize }) {
|
|
57
57
|
return { skip: (page - 1) * pageSize, take: pageSize };
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The entries either side of the current one in an already-ordered list.
|
|
61
|
+
*
|
|
62
|
+
* The ordering is the caller's, deliberately — it is the one thing here that is
|
|
63
|
+
* never portable. A wiki's sequence is its section's configured sort, a
|
|
64
|
+
* knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
|
|
65
|
+
* What both wikis had written twice is this scan, not the sort.
|
|
66
|
+
*
|
|
67
|
+
* `null` on both sides when the page is not in the list, so a page reached by a
|
|
68
|
+
* URL its own section does not list renders no nav rather than a wrong one.
|
|
69
|
+
*
|
|
70
|
+
* No query. Both callers already hold the ordered siblings for something else —
|
|
71
|
+
* a related-pages panel, a section listing — and the two indexed lookups the
|
|
72
|
+
* neighbours used to cost were the reason one wiki dropped the control.
|
|
73
|
+
*/
|
|
74
|
+
export function adjacentPages(ordered, isCurrent) {
|
|
75
|
+
const i = ordered.findIndex(isCurrent);
|
|
76
|
+
if (i < 0)
|
|
77
|
+
return { prev: null, next: null };
|
|
78
|
+
return { prev: ordered[i - 1] ?? null, next: ordered[i + 1] ?? null };
|
|
79
|
+
}
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -63,4 +63,21 @@ export declare function rateLimitHeaders(result: RateLimitResult, opts: RateLimi
|
|
|
63
63
|
export declare function withRateLimit<T extends Response>(res: T, result: RateLimitResult, opts: RateLimitOptions): T;
|
|
64
64
|
/** Test seam: the bucket map is module state and outlives a single test. */
|
|
65
65
|
export declare function resetRateLimits(): void;
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The one MCP rate-limit number, for every surface that states one.
|
|
68
|
+
*
|
|
69
|
+
* `.well-known/mcp.json`, the OpenAPI spec, agents.md, llms.txt and the
|
|
70
|
+
* `initialize` instructions all quote this, so the endpoint enforces exactly
|
|
71
|
+
* what the documents claim. It is a cross-surface contract, which is why it
|
|
72
|
+
* lives here rather than three times over: it had already drifted to 200 on one
|
|
73
|
+
* surface with nothing recorded about why, and that is the state that makes a
|
|
74
|
+
* number impossible to change safely later — nobody can tell a considered
|
|
75
|
+
* difference from a stale one.
|
|
76
|
+
*
|
|
77
|
+
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
78
|
+
* What it must not do is restate this one.
|
|
79
|
+
*/
|
|
80
|
+
export declare const MCP_RATE_LIMIT_PER_MIN = 60;
|
|
81
|
+
export declare const MCP_RATE_LIMIT_TEXT = "60 requests per minute per IP";
|
|
82
|
+
/** The budget as `mcpResponse` takes it, so a route states it once. */
|
|
83
|
+
export declare const MCP_RATE_LIMIT: RateLimitOptions;
|
package/dist/rate-limit.js
CHANGED
|
@@ -110,4 +110,25 @@ export function withRateLimit(res, result, opts) {
|
|
|
110
110
|
export function resetRateLimits() {
|
|
111
111
|
buckets.clear();
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
// ---- the MCP budget ---------------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* The one MCP rate-limit number, for every surface that states one.
|
|
116
|
+
*
|
|
117
|
+
* `.well-known/mcp.json`, the OpenAPI spec, agents.md, llms.txt and the
|
|
118
|
+
* `initialize` instructions all quote this, so the endpoint enforces exactly
|
|
119
|
+
* what the documents claim. It is a cross-surface contract, which is why it
|
|
120
|
+
* lives here rather than three times over: it had already drifted to 200 on one
|
|
121
|
+
* surface with nothing recorded about why, and that is the state that makes a
|
|
122
|
+
* number impossible to change safely later — nobody can tell a considered
|
|
123
|
+
* difference from a stale one.
|
|
124
|
+
*
|
|
125
|
+
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
126
|
+
* What it must not do is restate this one.
|
|
127
|
+
*/
|
|
128
|
+
export const MCP_RATE_LIMIT_PER_MIN = 60;
|
|
129
|
+
export const MCP_RATE_LIMIT_TEXT = `${MCP_RATE_LIMIT_PER_MIN} requests per minute per IP`;
|
|
130
|
+
/** The budget as `mcpResponse` takes it, so a route states it once. */
|
|
131
|
+
export const MCP_RATE_LIMIT = {
|
|
132
|
+
capacity: MCP_RATE_LIMIT_PER_MIN,
|
|
133
|
+
refillPerSec: MCP_RATE_LIMIT_PER_MIN / 60,
|
|
134
|
+
};
|