wiki-formant 0.47.1 → 0.49.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 +95 -26
- package/bin/maintenance-log.mjs +198 -0
- package/bin/mark-verified.mjs +75 -0
- package/bin/prisma-models.mjs +53 -0
- package/dist/analytics.d.ts +0 -25
- package/dist/analytics.js +10 -49
- package/dist/base.css +23 -2
- package/dist/blocks.d.ts +7 -6
- package/dist/blocks.js +11 -3
- package/dist/chart.d.ts +17 -8
- package/dist/chart.js +53 -6
- package/dist/combobox.d.ts +0 -4
- package/dist/combobox.js +2 -0
- package/dist/crawlers.d.ts +0 -24
- package/dist/crawlers.js +9 -3
- package/dist/dom.d.ts +0 -5
- package/dist/dom.js +2 -0
- package/dist/editor.d.ts +0 -43
- package/dist/editor.js +7 -1
- package/dist/feed.d.ts +0 -14
- package/dist/feed.js +4 -0
- package/dist/freshness.d.ts +0 -7
- package/dist/freshness.js +3 -0
- package/dist/headers.d.ts +0 -8
- package/dist/headers.js +3 -1
- package/dist/headings.d.ts +0 -8
- package/dist/headings.js +2 -0
- package/dist/history.d.ts +0 -6
- package/dist/history.js +2 -0
- package/dist/http.d.ts +0 -11
- package/dist/http.js +5 -0
- package/dist/license.d.ts +0 -8
- package/dist/license.js +2 -0
- package/dist/link-check.d.ts +0 -21
- package/dist/link-check.js +6 -2
- package/dist/links.d.ts +0 -5
- package/dist/links.js +2 -0
- package/dist/maintenance.d.ts +61 -0
- package/dist/maintenance.js +137 -0
- package/dist/maps.d.ts +0 -6
- package/dist/maps.js +2 -0
- package/dist/markdown.d.ts +0 -2
- package/dist/markdown.js +1 -1
- package/dist/mcp.d.ts +0 -14
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +14 -11
- package/dist/metadata.js +33 -0
- package/dist/pagination.d.ts +0 -5
- package/dist/pagination.js +0 -4
- package/dist/passkey.js +2 -15
- package/dist/rate-limit.d.ts +5 -16
- package/dist/rate-limit.js +10 -1
- package/dist/react-server.d.ts +0 -12
- package/dist/react-server.js +2 -0
- package/dist/revisions.d.ts +0 -7
- package/dist/revisions.js +2 -0
- package/dist/rola.d.ts +74 -0
- package/dist/rola.js +97 -0
- package/dist/search.d.ts +0 -12
- package/dist/search.js +4 -0
- package/dist/sidebar.d.ts +0 -16
- package/dist/sidebar.js +2 -0
- package/dist/stats.d.ts +0 -4
- package/dist/stats.js +2 -2
- package/dist/taxonomy.d.ts +0 -4
- package/dist/taxonomy.js +2 -2
- package/dist/text.d.ts +16 -22
- package/dist/text.js +39 -5
- package/dist/tiptap.d.ts +5 -4
- package/dist/tiptap.js +2 -1
- package/dist/upload.d.ts +31 -0
- package/dist/upload.js +91 -0
- package/dist/validation.d.ts +0 -6
- package/dist/validation.js +3 -3
- package/dist/well-known.d.ts +0 -20
- package/dist/well-known.js +3 -0
- package/package.json +37 -8
- package/prisma/analytics.prisma +57 -0
- package/prisma/passkey.prisma +21 -0
package/dist/editor.js
CHANGED
|
@@ -71,6 +71,8 @@ function cleanPastedHtml(html) {
|
|
|
71
71
|
* which the editor cannot know, so the render path decides: `normaliseLinks`
|
|
72
72
|
* from `wiki-formant/links`. Dropping both attributes from the mark also cleans
|
|
73
73
|
* a legacy link the next time its page is saved.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
74
76
|
*/
|
|
75
77
|
export const wikiLink = TiptapLink.extend({
|
|
76
78
|
addAttributes() {
|
|
@@ -117,7 +119,7 @@ const SHORT_MAP = /maps\.app\.goo\.gl|goo\.gl\/maps/;
|
|
|
117
119
|
* swap re-finds the node by its `url` attribute rather than caching a position,
|
|
118
120
|
* because every keystroke between the paste and the resolve moves it.
|
|
119
121
|
*/
|
|
120
|
-
|
|
122
|
+
function insertEmbed(editor, url, { resolveMapUrl }) {
|
|
121
123
|
const yt = url.match(YOUTUBE);
|
|
122
124
|
if (yt) {
|
|
123
125
|
editor.chain().focus().setYoutubeVideo({ src: url }).run();
|
|
@@ -168,6 +170,8 @@ export function runTableAction(editor, command) {
|
|
|
168
170
|
* two titled their buttons with the internal key — a screen reader said
|
|
169
171
|
* "codeBlock", and inline code and code block shared one icon and one name.
|
|
170
172
|
* Upload and embed stay with the caller: they are the parts that differ.
|
|
173
|
+
*
|
|
174
|
+
* @internal
|
|
171
175
|
*/
|
|
172
176
|
export const TOOLBAR_ACTIONS = [
|
|
173
177
|
{ key: 'bold', label: 'Bold', active: 'bold', run: e => e.chain().focus().toggleBold().run() },
|
|
@@ -220,6 +224,8 @@ export function ToolbarButton({ label, pressed, onPress, className, children, })
|
|
|
220
224
|
* the link the cursor is in, across the whole link rather than the collapsed
|
|
221
225
|
* selection, and an emptied address takes it off while keeping the text. An
|
|
222
226
|
* embed with no address does nothing.
|
|
227
|
+
*
|
|
228
|
+
* @internal
|
|
223
229
|
*/
|
|
224
230
|
export function applyUrlPrompt(editor, kind, url, { resolveMapUrl }) {
|
|
225
231
|
const href = url.trim();
|
package/dist/feed.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export declare const escXml: (s: string) => string;
|
|
2
|
-
/**
|
|
3
|
-
* `content:encoded` carries raw HTML, so it travels inside CDATA. A literal
|
|
4
|
-
* `]]>` in the body would close the section early and corrupt every item after
|
|
5
|
-
* it — not just the one that contained it.
|
|
6
|
-
*/
|
|
7
|
-
export declare const cdata: (html: string) => string;
|
|
8
2
|
/**
|
|
9
3
|
* Aggregators hard-truncate a description around 150 characters. Trimming on a
|
|
10
4
|
* word boundary first means the cut lands between words rather than inside one.
|
|
@@ -45,14 +39,6 @@ export interface FeedChannel {
|
|
|
45
39
|
*/
|
|
46
40
|
lastBuild?: Date | null;
|
|
47
41
|
}
|
|
48
|
-
/**
|
|
49
|
-
* `lastBuildDate` comes from the newest item, not from the clock.
|
|
50
|
-
*
|
|
51
|
-
* A feed whose build date moves on every request tells every poller it changed
|
|
52
|
-
* when it did not, which is exactly the recrawl the conditional-GET helpers
|
|
53
|
-
* exist to prevent. An empty feed has no build date rather than a fictional one.
|
|
54
|
-
*/
|
|
55
|
-
export declare function renderFeed(channel: FeedChannel, items: readonly FeedItem[]): string;
|
|
56
42
|
/**
|
|
57
43
|
* The feed as a response, with the validators a poller revalidates against.
|
|
58
44
|
*
|
package/dist/feed.js
CHANGED
|
@@ -28,6 +28,8 @@ export const escXml = (s) => s.replace(/[&<>"']/g, c => XML_ESCAPES[c] ?? c);
|
|
|
28
28
|
* `content:encoded` carries raw HTML, so it travels inside CDATA. A literal
|
|
29
29
|
* `]]>` in the body would close the section early and corrupt every item after
|
|
30
30
|
* it — not just the one that contained it.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
31
33
|
*/
|
|
32
34
|
export const cdata = (html) => `<![CDATA[${html.replace(/]]>/g, ']]>')}]]>`;
|
|
33
35
|
/**
|
|
@@ -69,6 +71,8 @@ export function renderItem(item) {
|
|
|
69
71
|
* A feed whose build date moves on every request tells every poller it changed
|
|
70
72
|
* when it did not, which is exactly the recrawl the conditional-GET helpers
|
|
71
73
|
* exist to prevent. An empty feed has no build date rather than a fictional one.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
72
76
|
*/
|
|
73
77
|
export function renderFeed(channel, items) {
|
|
74
78
|
const newest = lastBuild(channel, items);
|
package/dist/freshness.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export interface FreshnessInput {
|
|
|
4
4
|
lastVerifiedAt?: Date | string | null;
|
|
5
5
|
updatedAt?: Date | string | null;
|
|
6
6
|
}
|
|
7
|
-
export declare const DEFAULT_MAX_AGE_DAYS = 180;
|
|
8
7
|
/** Whole days between `date` and `now`, or null when there is no usable date. */
|
|
9
8
|
export declare function daysSince(date: Date | string | null | undefined, now: number): number | null;
|
|
10
9
|
/**
|
|
@@ -13,12 +12,6 @@ export declare function daysSince(date: Date | string | null | undefined, now: n
|
|
|
13
12
|
* edit is a weaker signal than a verification, but it is a signal.
|
|
14
13
|
*/
|
|
15
14
|
export declare function isStale(page: FreshnessInput, now: number, maxAgeDays?: number): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* The sentence the notice carries. Both wikis shipped this string identically,
|
|
18
|
-
* down to the ISO date and the closing request, so it lives here rather than
|
|
19
|
-
* being re-typed either side of the extraction.
|
|
20
|
-
*/
|
|
21
|
-
export declare function freshnessNotice(page: FreshnessInput): string;
|
|
22
15
|
/**
|
|
23
16
|
* A synthetic `outdated` banner block for a stale page, or null when it is
|
|
24
17
|
* fresh — the shape every wiki's `banner` type already stores, so it drops
|
package/dist/freshness.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// disagree with the one the server just sent.
|
|
11
11
|
import { isoDate } from './html.js';
|
|
12
12
|
export { isoDate };
|
|
13
|
+
/** @internal */
|
|
13
14
|
export const DEFAULT_MAX_AGE_DAYS = 180;
|
|
14
15
|
const DAY_MS = 86_400_000;
|
|
15
16
|
/** Whole days between `date` and `now`, or null when there is no usable date. */
|
|
@@ -34,6 +35,8 @@ export function isStale(page, now, maxAgeDays = DEFAULT_MAX_AGE_DAYS) {
|
|
|
34
35
|
* The sentence the notice carries. Both wikis shipped this string identically,
|
|
35
36
|
* down to the ISO date and the closing request, so it lives here rather than
|
|
36
37
|
* being re-typed either side of the extraction.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
37
40
|
*/
|
|
38
41
|
export function freshnessNotice(page) {
|
|
39
42
|
const when = page.lastVerifiedAt
|
package/dist/headers.d.ts
CHANGED
|
@@ -9,16 +9,8 @@ export interface HeaderEntry {
|
|
|
9
9
|
* embed that clears one clears the other.
|
|
10
10
|
*/
|
|
11
11
|
export declare const DEFAULT_IFRAME_HOSTS: readonly string[];
|
|
12
|
-
/** `frame-src` sources for a host list — the one a sanitiser was given. */
|
|
13
|
-
export declare const frameSources: (hosts?: readonly string[]) => string[];
|
|
14
12
|
/** CSP directive names this builder writes, in the order it writes them. */
|
|
15
13
|
export type CspDirective = 'default-src' | 'base-uri' | 'object-src' | 'frame-ancestors' | 'form-action' | 'script-src' | 'style-src' | 'font-src' | 'img-src' | 'media-src' | 'connect-src' | 'frame-src' | 'worker-src' | 'manifest-src';
|
|
16
|
-
/**
|
|
17
|
-
* The policy all four origins shared line for line. Everything a site adds is
|
|
18
|
-
* a source list it hands `contentSecurityPolicy`, which replaces the default
|
|
19
|
-
* for that directive rather than appending, so a reader sees the whole list.
|
|
20
|
-
*/
|
|
21
|
-
export declare const BASE_CSP: Readonly<Partial<Record<CspDirective, readonly string[]>>>;
|
|
22
14
|
/**
|
|
23
15
|
* The policy string: `BASE_CSP` with each directive in `directives` replacing
|
|
24
16
|
* its default. An empty list drops the directive.
|
package/dist/headers.js
CHANGED
|
@@ -24,12 +24,14 @@ export const DEFAULT_IFRAME_HOSTS = [
|
|
|
24
24
|
'www.google.com', 'maps.google.com',
|
|
25
25
|
'embed.apple.com', 'maps.apple.com',
|
|
26
26
|
];
|
|
27
|
-
/** `frame-src` sources for a host list — the one a sanitiser was given. */
|
|
27
|
+
/** `frame-src` sources for a host list — the one a sanitiser was given. @internal */
|
|
28
28
|
export const frameSources = (hosts = DEFAULT_IFRAME_HOSTS) => hosts.length ? hosts.map(h => `https://${h}`) : ["'none'"];
|
|
29
29
|
/**
|
|
30
30
|
* The policy all four origins shared line for line. Everything a site adds is
|
|
31
31
|
* a source list it hands `contentSecurityPolicy`, which replaces the default
|
|
32
32
|
* for that directive rather than appending, so a reader sees the whole list.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
33
35
|
*/
|
|
34
36
|
export const BASE_CSP = {
|
|
35
37
|
'default-src': ["'self'"],
|
package/dist/headings.d.ts
CHANGED
|
@@ -53,14 +53,6 @@ export declare function injectHeadingIds(html: string, options?: HeadingIdOption
|
|
|
53
53
|
* consumers use only the injector above.
|
|
54
54
|
*/
|
|
55
55
|
export declare function headingsFrom(html: string): Heading[];
|
|
56
|
-
/**
|
|
57
|
-
* Every heading in `html`, in document order, whether or not it carries an id —
|
|
58
|
-
* the outline a crawler reads. `headingsFrom` above answers a different
|
|
59
|
-
* question: which headings a rail can link to, which is why it drops the ones
|
|
60
|
-
* with no id. An audit must not, because a chrome heading with no id still
|
|
61
|
-
* takes a level in the outline, and the levels are the thing under test.
|
|
62
|
-
*/
|
|
63
|
-
export declare function headingOutline(html: string): Heading[];
|
|
64
56
|
/**
|
|
65
57
|
* What is wrong with a page's heading outline.
|
|
66
58
|
*
|
package/dist/headings.js
CHANGED
|
@@ -92,6 +92,8 @@ export function headingsFrom(html) {
|
|
|
92
92
|
* question: which headings a rail can link to, which is why it drops the ones
|
|
93
93
|
* with no id. An audit must not, because a chrome heading with no id still
|
|
94
94
|
* takes a level in the outline, and the levels are the thing under test.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
95
97
|
*/
|
|
96
98
|
export function headingOutline(html) {
|
|
97
99
|
const out = [];
|
package/dist/history.d.ts
CHANGED
|
@@ -3,12 +3,6 @@ import { type BlockChange, type BlockGroup, type DiffBlock } from './revisions.j
|
|
|
3
3
|
export type DiffPart = [-1 | 0 | 1, string];
|
|
4
4
|
/** A change as a history view lists it: the diff it draws, not the two bodies behind it. */
|
|
5
5
|
export type HistoryChange = BlockChange<DiffPart[]>;
|
|
6
|
-
/**
|
|
7
|
-
* Two HTML bodies as the words struck and added between them, or `undefined`
|
|
8
|
-
* when their text is the same. Semantic cleanup is on: without it a rewritten
|
|
9
|
-
* sentence comes out as fragments of the letters the two versions share.
|
|
10
|
-
*/
|
|
11
|
-
export declare function textDiff(from: string, to: string): DiffPart[] | undefined;
|
|
12
6
|
export interface HistoryOptions<B> {
|
|
13
7
|
/** Defaults to `coreBlockGroups`, as in `diffBlocks`. */
|
|
14
8
|
containers?: (block: B) => BlockGroup<B>[] | null;
|
package/dist/history.js
CHANGED
|
@@ -20,6 +20,8 @@ const clip = (t) => (t.length > KEPT_MAX ? `${t.slice(0, 30)}…${t.slice(-30)}`
|
|
|
20
20
|
* Two HTML bodies as the words struck and added between them, or `undefined`
|
|
21
21
|
* when their text is the same. Semantic cleanup is on: without it a rewritten
|
|
22
22
|
* sentence comes out as fragments of the letters the two versions share.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
23
25
|
*/
|
|
24
26
|
export function textDiff(from, to) {
|
|
25
27
|
const a = stripHtml(from);
|
package/dist/http.d.ts
CHANGED
|
@@ -76,17 +76,6 @@ export declare function wantsMarkdown(request: {
|
|
|
76
76
|
export declare const VARY_ACCEPT: {
|
|
77
77
|
readonly Vary: "Accept";
|
|
78
78
|
};
|
|
79
|
-
/**
|
|
80
|
-
* Headers for a JSON descriptor — an agent card, an OpenAPI document, a
|
|
81
|
-
* registry manifest. These are the documents a client refetches most and the
|
|
82
|
-
* ones that had no validator at all: served as `Cache-Control: public` with no
|
|
83
|
-
* `max-age`, a caller falls back to heuristic freshness and can never
|
|
84
|
-
* revalidate, so a corrected card takes an unbounded time to reach anyone.
|
|
85
|
-
*/
|
|
86
|
-
export declare function descriptorHeaders(etag: string, opts?: {
|
|
87
|
-
maxAge?: number;
|
|
88
|
-
extra?: Record<string, string>;
|
|
89
|
-
}): Record<string, string>;
|
|
90
79
|
/**
|
|
91
80
|
* A JSON descriptor served with a validator, answering a conditional GET.
|
|
92
81
|
*
|
package/dist/http.js
CHANGED
|
@@ -141,6 +141,10 @@ export const VARY_ACCEPT = { Vary: 'Accept' };
|
|
|
141
141
|
* ones that had no validator at all: served as `Cache-Control: public` with no
|
|
142
142
|
* `max-age`, a caller falls back to heuristic freshness and can never
|
|
143
143
|
* revalidate, so a corrected card takes an unbounded time to reach anyone.
|
|
144
|
+
* Every one is public and read by browser agents too, so each allows any
|
|
145
|
+
* origin; six of the nine routes passed that by hand and three did not.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
144
148
|
*/
|
|
145
149
|
export function descriptorHeaders(etag, opts = {}) {
|
|
146
150
|
const maxAge = opts.maxAge ?? 86400;
|
|
@@ -148,6 +152,7 @@ export function descriptorHeaders(etag, opts = {}) {
|
|
|
148
152
|
'Content-Type': 'application/json; charset=utf-8',
|
|
149
153
|
'Cache-Control': `public, max-age=300, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 7}`,
|
|
150
154
|
ETag: etag,
|
|
155
|
+
'Access-Control-Allow-Origin': '*',
|
|
151
156
|
...opts.extra,
|
|
152
157
|
};
|
|
153
158
|
}
|
package/dist/license.d.ts
CHANGED
|
@@ -32,14 +32,6 @@ export interface LicenseBlockOptions {
|
|
|
32
32
|
/** `Licence` where the site's prose says so. Defaults to `License`. */
|
|
33
33
|
heading?: string;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* The block, as lines, ready to join into any text export.
|
|
37
|
-
*
|
|
38
|
-
* Returned as an array rather than a string because every caller splices it
|
|
39
|
-
* into a document it is already building line by line, and a caller that wants
|
|
40
|
-
* the string can join it.
|
|
41
|
-
*/
|
|
42
|
-
export declare function licenseLines({ license, scope, scopeVerb, excludes, heading, }: LicenseBlockOptions): string[];
|
|
43
35
|
export declare function licenseBlock(opts: LicenseBlockOptions): string;
|
|
44
36
|
/** The one-line form, for a frontmatter field or a feed's `<copyright>`. */
|
|
45
37
|
export declare function licenseNote(license: License): string;
|
package/dist/license.js
CHANGED
|
@@ -24,6 +24,8 @@ export function ccBy40(opts) {
|
|
|
24
24
|
* Returned as an array rather than a string because every caller splices it
|
|
25
25
|
* into a document it is already building line by line, and a caller that wants
|
|
26
26
|
* the string can join it.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
27
29
|
*/
|
|
28
30
|
export function licenseLines({ license, scope = 'This content', scopeVerb = 'is', excludes = [], heading = 'License & Attribution', }) {
|
|
29
31
|
return [
|
package/dist/link-check.d.ts
CHANGED
|
@@ -15,22 +15,6 @@ export interface ProbeOptions {
|
|
|
15
15
|
/** Second-attempt budget for a host that is merely slow. */
|
|
16
16
|
slowTimeoutMs?: number;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Codes that flap. A sweep found 16 status-0 rows and 15 were one host, every
|
|
20
|
-
* one answering 200 when probed alone. Carry the code through so a caller can
|
|
21
|
-
* treat these as retryable rather than banking a death.
|
|
22
|
-
*/
|
|
23
|
-
export declare const TRANSIENT_CODES: Set<string>;
|
|
24
|
-
/** A failure as a report row: the message, the code, and the TLS caveat. */
|
|
25
|
-
export declare function describeFailure(err: unknown): Pick<Probe, 'error' | 'code' | 'tls' | 'note'>;
|
|
26
|
-
/**
|
|
27
|
-
* The URL to probe INSTEAD, where the public page lies about its own health.
|
|
28
|
-
*
|
|
29
|
-
* npmjs.com serves 403 to scripted requests whether or not the package exists,
|
|
30
|
-
* which made every package citation a permanent false positive. The registry
|
|
31
|
-
* answers honestly.
|
|
32
|
-
*/
|
|
33
|
-
export declare function probeUrlFor(url: string): string;
|
|
34
18
|
/**
|
|
35
19
|
* Run `fn` with at most one request in flight against this hostname.
|
|
36
20
|
*
|
|
@@ -59,11 +43,6 @@ export declare function extractLinks(html: string): Array<{
|
|
|
59
43
|
href: string;
|
|
60
44
|
text: string;
|
|
61
45
|
}>;
|
|
62
|
-
/** Every `<iframe>` and `<img>` source in a fragment. */
|
|
63
|
-
export declare function extractEmbeds(html: string): Array<{
|
|
64
|
-
kind: string;
|
|
65
|
-
url: string;
|
|
66
|
-
}>;
|
|
67
46
|
/** What a page links to, split the way a checker probes it. */
|
|
68
47
|
export interface BlockLinks {
|
|
69
48
|
/** Absolute http(s) targets. */
|
package/dist/link-check.js
CHANGED
|
@@ -36,6 +36,8 @@ const TLS_CODES = new Set([
|
|
|
36
36
|
* Codes that flap. A sweep found 16 status-0 rows and 15 were one host, every
|
|
37
37
|
* one answering 200 when probed alone. Carry the code through so a caller can
|
|
38
38
|
* treat these as retryable rather than banking a death.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
39
41
|
*/
|
|
40
42
|
export const TRANSIENT_CODES = new Set([
|
|
41
43
|
'UND_ERR_CONNECT_TIMEOUT',
|
|
@@ -48,7 +50,7 @@ export const TRANSIENT_CODES = new Set([
|
|
|
48
50
|
'EAI_AGAIN',
|
|
49
51
|
'EPIPE',
|
|
50
52
|
]);
|
|
51
|
-
/** A failure as a report row: the message, the code, and the TLS caveat. */
|
|
53
|
+
/** A failure as a report row: the message, the code, and the TLS caveat. @internal */
|
|
52
54
|
export function describeFailure(err) {
|
|
53
55
|
const e = err;
|
|
54
56
|
if (e?.name === 'AbortError')
|
|
@@ -71,6 +73,8 @@ export function describeFailure(err) {
|
|
|
71
73
|
* npmjs.com serves 403 to scripted requests whether or not the package exists,
|
|
72
74
|
* which made every package citation a permanent false positive. The registry
|
|
73
75
|
* answers honestly.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
74
78
|
*/
|
|
75
79
|
export function probeUrlFor(url) {
|
|
76
80
|
const npm = url.match(/^https:\/\/(?:www\.)?npmjs\.com\/package\/(.+?)\/?$/);
|
|
@@ -223,7 +227,7 @@ export function extractLinks(html) {
|
|
|
223
227
|
}
|
|
224
228
|
return out;
|
|
225
229
|
}
|
|
226
|
-
/** Every `<iframe>` and `<img>` source in a fragment. */
|
|
230
|
+
/** Every `<iframe>` and `<img>` source in a fragment. @internal */
|
|
227
231
|
export function extractEmbeds(html) {
|
|
228
232
|
const out = [];
|
|
229
233
|
for (const m of html.matchAll(EMBED_RE)) {
|
package/dist/links.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A readable label for an anchor whose text is empty, derived from its href.
|
|
3
|
-
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
4
|
-
*/
|
|
5
|
-
export declare function fallbackAnchorText(href: string): string;
|
|
6
1
|
export interface NormaliseLinkOptions {
|
|
7
2
|
/**
|
|
8
3
|
* This wiki's own origin. An absolute link back to it is really internal, so
|
package/dist/links.js
CHANGED
|
@@ -18,6 +18,8 @@ import { getAttr, removeAttr, stripTags } from './html.js';
|
|
|
18
18
|
/**
|
|
19
19
|
* A readable label for an anchor whose text is empty, derived from its href.
|
|
20
20
|
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
21
23
|
*/
|
|
22
24
|
export function fallbackAnchorText(href) {
|
|
23
25
|
if (href.startsWith('#'))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SourceListItem = string | {
|
|
2
|
+
handle?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
export interface SourceEntry {
|
|
7
|
+
lastChecked?: string | null;
|
|
8
|
+
note?: string;
|
|
9
|
+
list?: SourceListItem[];
|
|
10
|
+
}
|
|
11
|
+
export type BacklogEntry = string | {
|
|
12
|
+
date?: string;
|
|
13
|
+
item: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
};
|
|
16
|
+
export type FeedbackEntry = string | {
|
|
17
|
+
date?: string;
|
|
18
|
+
note: string;
|
|
19
|
+
};
|
|
20
|
+
export interface RunEntry {
|
|
21
|
+
date?: string;
|
|
22
|
+
category?: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
tweet?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LogState {
|
|
27
|
+
mission?: string;
|
|
28
|
+
positioning?: string;
|
|
29
|
+
totalRuns?: number;
|
|
30
|
+
lastRun?: string | null;
|
|
31
|
+
rotation?: {
|
|
32
|
+
order?: string[];
|
|
33
|
+
next?: string;
|
|
34
|
+
};
|
|
35
|
+
sources?: Record<string, SourceEntry>;
|
|
36
|
+
backlog?: BacklogEntry[];
|
|
37
|
+
feedback?: FeedbackEntry[];
|
|
38
|
+
runHistory?: RunEntry[];
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** Drops what is closed or a diary entry, and caps what is append-only. Newest first. */
|
|
42
|
+
export declare function compact(state: LogState): LogState;
|
|
43
|
+
/** New backlog items, deduplicated against what is there, dated today unless they carry a date. */
|
|
44
|
+
export declare function addBacklog(state: LogState, items: readonly BacklogEntry[], today: string): BacklogEntry[];
|
|
45
|
+
/** The page's content blocks, re-rendered from state on every write. `routine` names who keeps it. */
|
|
46
|
+
export declare function render(state: LogState, routine: string, newId?: () => string): {
|
|
47
|
+
id: string;
|
|
48
|
+
type: "content";
|
|
49
|
+
text: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** A table or column name from the command line, quoted, or a thrown usage error. */
|
|
52
|
+
export declare function ident(name: string | undefined, what: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* `--scope column` limits a bin to rows whose column is NULL, `--scope
|
|
55
|
+
* column=value` to rows where it equals the value: caper's platform pages
|
|
56
|
+
* have no caper, and its company pages share their tag paths.
|
|
57
|
+
*/
|
|
58
|
+
export declare function scopeSql(scope: string | undefined, param: number): {
|
|
59
|
+
sql: string;
|
|
60
|
+
values: unknown[];
|
|
61
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// maintenance.ts — what the `maintenance-log` and `mark-verified` bins share,
|
|
2
|
+
// kept pure so it is tested without a database.
|
|
3
|
+
//
|
|
4
|
+
// A wiki-sweep routine keeps its memory between runs (the rotation cursor,
|
|
5
|
+
// source watermarks, backlog, feedback and run history) in one wiki page's
|
|
6
|
+
// `metadata.state`, and that page shows a capped rendering of it. radix-wiki
|
|
7
|
+
// and caper each had the script, with the same caps and rules and a different
|
|
8
|
+
// table. The rules are here; the table is the bin's arguments.
|
|
9
|
+
//
|
|
10
|
+
// Every write compacts first. `backlog` holds only open work items, since a
|
|
11
|
+
// run diary entry belongs in `runHistory`, which it duplicates, and the
|
|
12
|
+
// append-only lists are capped. Uncapped, radix-wiki's log page reached 384 KB
|
|
13
|
+
// of content and 710 KB of state, and a revision per run made it over half of
|
|
14
|
+
// its revisions table. Two kinds of entry are pinned against the caps because
|
|
15
|
+
// a run cannot recreate them: a backlog item flagged for a human, and a
|
|
16
|
+
// watermark that banks a verdict on a whole class of site (a bot wall, a 403),
|
|
17
|
+
// which is what lets an audit spend no probes on its flag list.
|
|
18
|
+
//
|
|
19
|
+
// No subpath exports this: the bins and the tests import it from dist.
|
|
20
|
+
/** Held in state. */
|
|
21
|
+
const MAX = { backlog: 60, runs: 30, feedback: 40, sources: 150 };
|
|
22
|
+
/** Shown on the page. What a reader pays is words, and the entries run long. */
|
|
23
|
+
const SHOWN = { backlog: 15, runs: 5, feedback: 8, sources: 15 };
|
|
24
|
+
/** "run 214 (ecosystem): link audit CLEAN…" is a diary entry, not a work item. */
|
|
25
|
+
const JOURNAL = /^run\s+\d+\b/i;
|
|
26
|
+
const PINNED = /FLAG FOR A HUMAN/i;
|
|
27
|
+
const BANKED = /\bbanked\b|bot[- ]?(wall|block)|\b(403|429|999)\b|cloudflare/i;
|
|
28
|
+
const itemOf = (b) => (typeof b === 'string' ? b : b.item);
|
|
29
|
+
const recency = (a, b) => String(b[1]?.lastChecked ?? '').localeCompare(String(a[1]?.lastChecked ?? ''));
|
|
30
|
+
/** Drops what is closed or a diary entry, and caps what is append-only. Newest first. */
|
|
31
|
+
export function compact(state) {
|
|
32
|
+
// Undated items fall back to append order: a later index is newer.
|
|
33
|
+
const open = (state.backlog ?? [])
|
|
34
|
+
.map((b, i) => ({ status: 'open', ...(typeof b === 'string' ? { date: '', item: b } : b), i }))
|
|
35
|
+
.filter(b => b.status === 'open' && b.item && (PINNED.test(b.item) || !JOURNAL.test(b.item)))
|
|
36
|
+
.sort((a, b) => String(b.date ?? '').localeCompare(String(a.date ?? '')) || b.i - a.i);
|
|
37
|
+
const backlog = [...open.filter(b => PINNED.test(b.item)), ...open.filter(b => !PINNED.test(b.item))]
|
|
38
|
+
.slice(0, MAX.backlog)
|
|
39
|
+
.map(({ i: _i, ...b }) => b);
|
|
40
|
+
// Most source keys are one-off verdicts on a single site. Sorting by
|
|
41
|
+
// lastChecked lets the cap age out only probes no run has touched since.
|
|
42
|
+
const entries = Object.entries(state.sources ?? {});
|
|
43
|
+
const banked = ([, v]) => BANKED.test(String(v?.note ?? ''));
|
|
44
|
+
const sources = Object.fromEntries([...entries.filter(banked), ...entries.filter(e => !banked(e)).sort(recency).slice(0, MAX.sources)].sort(recency));
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
backlog,
|
|
48
|
+
sources,
|
|
49
|
+
feedback: (state.feedback ?? []).slice(-MAX.feedback),
|
|
50
|
+
runHistory: (state.runHistory ?? []).slice(-MAX.runs),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** New backlog items, deduplicated against what is there, dated today unless they carry a date. */
|
|
54
|
+
export function addBacklog(state, items, today) {
|
|
55
|
+
const seen = new Set((state.backlog ?? []).map(itemOf));
|
|
56
|
+
const added = [];
|
|
57
|
+
for (const raw of items) {
|
|
58
|
+
const item = itemOf(raw);
|
|
59
|
+
if (!item || seen.has(item))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(item);
|
|
62
|
+
added.push({ date: (typeof raw === 'object' && raw.date) || today, item, status: 'open' });
|
|
63
|
+
}
|
|
64
|
+
return added;
|
|
65
|
+
}
|
|
66
|
+
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
67
|
+
const more = (shown, total, what) => total > shown ? `<p><em>${total - shown} older ${what} held in state.</em></p>` : '';
|
|
68
|
+
const table = (head, rows) => `<table><tbody><tr>${head.map(h => `<td><strong>${h}</strong></td>`).join('')}</tr>` +
|
|
69
|
+
(rows.join('') || `<tr>${head.map(() => '<td>–</td>').join('')}</tr>`) +
|
|
70
|
+
'</tbody></table>';
|
|
71
|
+
/** The page's content blocks, re-rendered from state on every write. `routine` names who keeps it. */
|
|
72
|
+
export function render(state, routine, newId = () => crypto.randomUUID()) {
|
|
73
|
+
const rot = state.rotation ?? {};
|
|
74
|
+
const backlog = state.backlog ?? [];
|
|
75
|
+
const feedback = state.feedback ?? [];
|
|
76
|
+
const sources = Object.entries(state.sources ?? {}).sort(recency);
|
|
77
|
+
const block = (text) => ({ id: newId(), type: 'content', text });
|
|
78
|
+
const label = (x) => (typeof x === 'string' ? x : (x.handle ?? x.name ?? x.url ?? ''));
|
|
79
|
+
const sourceRows = sources.slice(0, SHOWN.sources).map(([key, v]) => {
|
|
80
|
+
const watch = (v.list ?? []).map(label).filter(Boolean);
|
|
81
|
+
const watching = watch.length ? ` — watching: ${esc(watch.slice(0, 8).join(', '))}${watch.length > 8 ? '…' : ''}` : '';
|
|
82
|
+
return `<tr><td>${esc(key)}</td><td>${esc(v.lastChecked || '–')}</td><td>${esc(v.note)}${watching}</td></tr>`;
|
|
83
|
+
});
|
|
84
|
+
const runRows = (state.runHistory ?? []).slice(-SHOWN.runs).reverse().map(r => {
|
|
85
|
+
const tweet = r.tweet && /^https?:\/\//.test(r.tweet) ? `<a href="${esc(r.tweet)}">link</a>` : esc(r.tweet);
|
|
86
|
+
return `<tr><td>${esc(r.date)}</td><td>${esc(r.category)}</td><td>${esc(r.summary)}</td><td>${tweet}</td></tr>`;
|
|
87
|
+
});
|
|
88
|
+
return [
|
|
89
|
+
block(`<p><em>Auto-maintained by the <code>${esc(routine)}</code> routine. Each run records what was swept, ` +
|
|
90
|
+
'advances the category rotation, logs ecosystem signals, and tracks community reception. ' +
|
|
91
|
+
'Do not hand-edit the state block – the routine overwrites it.</em></p>' +
|
|
92
|
+
(state.mission ? `<p><strong>Mission:</strong> ${esc(state.mission)}</p>` : '') +
|
|
93
|
+
(state.positioning ? `<p><strong>Positioning:</strong> ${esc(state.positioning)}</p>` : '')),
|
|
94
|
+
block('<h2>Current status</h2><table><tbody>' +
|
|
95
|
+
`<tr><td><strong>Next category in rotation</strong></td><td>${esc(rot.next || '–')}</td></tr>` +
|
|
96
|
+
`<tr><td><strong>Rotation order</strong></td><td>${esc((rot.order ?? []).join(' → '))}</td></tr>` +
|
|
97
|
+
`<tr><td><strong>Last run</strong></td><td>${esc(state.lastRun || '–')}</td></tr>` +
|
|
98
|
+
`<tr><td><strong>Total runs</strong></td><td>${esc(state.totalRuns ?? 0)}</td></tr>` +
|
|
99
|
+
'</tbody></table>'),
|
|
100
|
+
block(`<h2>Signal watermarks</h2>${table(['Source', 'Last checked', 'Note'], sourceRows)}` +
|
|
101
|
+
more(SHOWN.sources, sources.length, 'watermarks')),
|
|
102
|
+
block('<h2>Open backlog</h2>' +
|
|
103
|
+
(backlog.length
|
|
104
|
+
? `<ul>${backlog.slice(0, SHOWN.backlog).map(b => `<li>${esc(itemOf(b))}</li>`).join('')}</ul>` +
|
|
105
|
+
more(SHOWN.backlog, backlog.length, 'open items')
|
|
106
|
+
: '<p><em>Empty.</em></p>')),
|
|
107
|
+
block('<h2>Community feedback & reception</h2>' +
|
|
108
|
+
(feedback.length
|
|
109
|
+
? `<ul>${feedback
|
|
110
|
+
.slice(-SHOWN.feedback)
|
|
111
|
+
.reverse()
|
|
112
|
+
.map(f => `<li>${esc(typeof f === 'string' ? f : `${f.date ?? ''} – ${f.note}`)}</li>`)
|
|
113
|
+
.join('')}</ul>` + more(SHOWN.feedback, feedback.length, 'entries')
|
|
114
|
+
: '<p><em>Nothing logged yet.</em></p>')),
|
|
115
|
+
block(`<h2>Recent runs</h2>${table(['Date', 'Category', 'Summary', 'Tweet'], runRows)}`),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
const IDENT = /^[a-z_][a-z0-9_]*$/;
|
|
119
|
+
/** A table or column name from the command line, quoted, or a thrown usage error. */
|
|
120
|
+
export function ident(name, what) {
|
|
121
|
+
if (!name || !IDENT.test(name))
|
|
122
|
+
throw new Error(`${what} must be a lower-case SQL identifier, got ${JSON.stringify(name)}`);
|
|
123
|
+
return `"${name}"`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `--scope column` limits a bin to rows whose column is NULL, `--scope
|
|
127
|
+
* column=value` to rows where it equals the value: caper's platform pages
|
|
128
|
+
* have no caper, and its company pages share their tag paths.
|
|
129
|
+
*/
|
|
130
|
+
export function scopeSql(scope, param) {
|
|
131
|
+
if (!scope)
|
|
132
|
+
return { sql: 'TRUE', values: [] };
|
|
133
|
+
const [column, value] = scope.split('=');
|
|
134
|
+
return value === undefined
|
|
135
|
+
? { sql: `${ident(column, '--scope')} IS NULL`, values: [] }
|
|
136
|
+
: { sql: `${ident(column, '--scope')} = $${param}`, values: [value] };
|
|
137
|
+
}
|
package/dist/maps.d.ts
CHANGED
|
@@ -3,12 +3,6 @@ export interface MapCoords {
|
|
|
3
3
|
lon: number;
|
|
4
4
|
zoom?: number | undefined;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
8
|
-
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
|
9
|
-
* `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
|
|
10
|
-
*/
|
|
11
|
-
export declare function extractCoordsFromUrl(url: string): MapCoords | null;
|
|
12
6
|
/**
|
|
13
7
|
* An embeddable URL for `url`, or null when it is not a map link this
|
|
14
8
|
* understands. Already-embeddable URLs pass through untouched.
|
package/dist/maps.js
CHANGED
|
@@ -12,6 +12,8 @@ function mapsEmbedUrl(lat, lon, zoom = 15) {
|
|
|
12
12
|
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
13
13
|
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
|
14
14
|
* `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
15
17
|
*/
|
|
16
18
|
export function extractCoordsFromUrl(url) {
|
|
17
19
|
const coords = url.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*),?(\d+\.?\d*)?z?/);
|
package/dist/markdown.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export declare function decodeEntities(s: string): string;
|
|
2
2
|
/** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
|
|
3
3
|
export declare function inlineToMarkdown(html: string): string;
|
|
4
|
-
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
|
|
5
|
-
export declare function tableToMarkdown(html: string): string;
|
|
6
4
|
/** Block-level HTML → markdown. */
|
|
7
5
|
export declare function htmlToMarkdown(html: string): string;
|
|
8
6
|
export interface FrontmatterFields {
|
package/dist/markdown.js
CHANGED
|
@@ -62,7 +62,7 @@ export function inlineToMarkdown(html) {
|
|
|
62
62
|
.replace(/\s+/g, ' ')
|
|
63
63
|
.trim();
|
|
64
64
|
}
|
|
65
|
-
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
|
|
65
|
+
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. @internal */
|
|
66
66
|
export function tableToMarkdown(html) {
|
|
67
67
|
const rows = [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map(m => [...m[1].matchAll(/<(t[hd])\b[^>]*>([\s\S]*?)<\/\1>/gi)].map(c => inlineToMarkdown(c[2]).replace(/\|/g, '\\|')));
|
|
68
68
|
if (!rows.length)
|
package/dist/mcp.d.ts
CHANGED
|
@@ -265,20 +265,6 @@ export declare const toolText: (id: RpcRequest["id"], text: string, isError?: bo
|
|
|
265
265
|
}[];
|
|
266
266
|
};
|
|
267
267
|
};
|
|
268
|
-
/** The methods one era has and the other does not. Everything else is shared. */
|
|
269
|
-
declare const ERA_METHODS: {
|
|
270
|
-
readonly legacy: readonly ["initialize", "ping"];
|
|
271
|
-
readonly modern: readonly ["server/discover"];
|
|
272
|
-
};
|
|
273
|
-
type Era = keyof typeof ERA_METHODS;
|
|
274
|
-
/** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
|
|
275
|
-
interface Dispatch {
|
|
276
|
-
protocolVersion: McpProtocolVersion;
|
|
277
|
-
era: Era;
|
|
278
|
-
request?: Request;
|
|
279
|
-
}
|
|
280
|
-
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
281
|
-
export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig, dispatch?: Dispatch): Promise<object | object[] | null>;
|
|
282
268
|
export declare const MCP_CORS: Record<string, string>;
|
|
283
269
|
export declare function withMcpCors<T extends Response>(res: T): T;
|
|
284
270
|
export declare function mcpOptions(): Response;
|
package/dist/mcp.js
CHANGED
|
@@ -433,7 +433,7 @@ async function handleRpc(req, config, dispatch) {
|
|
|
433
433
|
return rpcError(id, -32603, 'Internal error');
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
-
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
436
|
+
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. @internal */
|
|
437
437
|
export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION, era: 'legacy' }) {
|
|
438
438
|
const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
439
439
|
const isBatch = Array.isArray(body);
|