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/text.js
CHANGED
|
@@ -24,6 +24,38 @@ export function stripHtml(html) {
|
|
|
24
24
|
.replace(/\n{3,}/g, '\n\n')
|
|
25
25
|
.trim();
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* HTML as one plain line: tags become spaces, entities are decoded, and the
|
|
29
|
+
* space a tag left before punctuation is closed up again, or prose that wrapped
|
|
30
|
+
* a link around the word before a comma reads "the Radix network ." Unlike
|
|
31
|
+
* `stripHtml`, links keep no href: this is for a description, not evidence.
|
|
32
|
+
*/
|
|
33
|
+
export function plainText(html) {
|
|
34
|
+
return decodeEntities(html.replace(/<[^>]+>/g, ' '))
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.replace(/\s+([,.;:!?%)\]])/g, '$1')
|
|
37
|
+
.replace(/([(\[])\s+/g, '$1')
|
|
38
|
+
.trim();
|
|
39
|
+
}
|
|
40
|
+
const HEADINGS = /<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi;
|
|
41
|
+
/**
|
|
42
|
+
* How a page opens: the first top-level `content` block that has prose once
|
|
43
|
+
* its headings are out, as plain text. Wiki pages open with an infobox far more
|
|
44
|
+
* often than not, so the first characters of the whole page are a flattened
|
|
45
|
+
* metadata table, which is what two wikis' descriptions said until both
|
|
46
|
+
* switched to this rule. `''` when no block has prose; a wiki with other
|
|
47
|
+
* prose-bearing types falls back to its own extractor.
|
|
48
|
+
*/
|
|
49
|
+
export function openingText(blocks) {
|
|
50
|
+
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
51
|
+
if (block?.type !== 'content' || typeof block.text !== 'string')
|
|
52
|
+
continue;
|
|
53
|
+
const text = plainText(block.text.replace(HEADINGS, ''));
|
|
54
|
+
if (text)
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
27
59
|
/**
|
|
28
60
|
* Display labels for the maintenance banners. Canonical: the prose extractor,
|
|
29
61
|
* the markdown twin and the MDX export all render the same six strings.
|
|
@@ -54,15 +86,15 @@ export const BANNER_VARIANTS = Object.keys(BANNER_LABELS).map(value => ({
|
|
|
54
86
|
export function bannerVariant(variant) {
|
|
55
87
|
return Object.hasOwn(BANNER_LABELS, variant) ? variant : 'cleanup';
|
|
56
88
|
}
|
|
57
|
-
/** A maintenance notice, inline: `[Notice: Needs citations] …`. */
|
|
89
|
+
/** A maintenance notice, inline: `[Notice: Needs citations] …`. @internal */
|
|
58
90
|
export function bannerToText(label, text) {
|
|
59
91
|
return `[Notice: ${label}]${text ? ' ' + stripHtml(text) : ''}`;
|
|
60
92
|
}
|
|
61
|
-
/** Each tab under its label, tags stripped — highlighted markup is noise here. */
|
|
93
|
+
/** Each tab under its label, tags stripped — highlighted markup is noise here. @internal */
|
|
62
94
|
export function codeTabsToText(tabs) {
|
|
63
95
|
return tabs.map(t => `[${t.label}]\n${t.code}`).join('\n');
|
|
64
96
|
}
|
|
65
|
-
/** Metric cards, one per line: `99% Uptime`. */
|
|
97
|
+
/** Metric cards, one per line: `99% Uptime`. @internal */
|
|
66
98
|
export function statsToText(items) {
|
|
67
99
|
return items.map(s => `${s.value}${s.suffix ?? ''} ${s.label}`).join('\n');
|
|
68
100
|
}
|
|
@@ -73,6 +105,8 @@ export function statsToText(items) {
|
|
|
73
105
|
* Two of the three wikis extracted nothing from a link grid, or from stats or
|
|
74
106
|
* page lists, so a hub page read to an agent as far emptier than it is. The
|
|
75
107
|
* third had its own three bodies. These are those bodies.
|
|
108
|
+
*
|
|
109
|
+
* @internal
|
|
76
110
|
*/
|
|
77
111
|
export function linkGridToText(groups, intro) {
|
|
78
112
|
return [
|
|
@@ -84,11 +118,11 @@ export function linkGridToText(groups, intro) {
|
|
|
84
118
|
].join('\n')),
|
|
85
119
|
].join('\n\n');
|
|
86
120
|
}
|
|
87
|
-
/** A resolved page list: one title per line. */
|
|
121
|
+
/** A resolved page list: one title per line. @internal */
|
|
88
122
|
export function pageListToText(pages) {
|
|
89
123
|
return pages.map(p => p.title).join('\n');
|
|
90
124
|
}
|
|
91
|
-
/** A numbered reference list, or `''` when there are none. */
|
|
125
|
+
/** A numbered reference list, or `''` when there are none. @internal */
|
|
92
126
|
export function referencesToText(items) {
|
|
93
127
|
if (!items.length)
|
|
94
128
|
return '';
|
package/dist/tiptap.d.ts
CHANGED
|
@@ -34,9 +34,10 @@ export interface CodeBlockClassNames {
|
|
|
34
34
|
optionActive?: string;
|
|
35
35
|
}
|
|
36
36
|
export interface CodeBlockOptions {
|
|
37
|
-
/** Selectable languages.
|
|
38
|
-
langs
|
|
39
|
-
|
|
37
|
+
/** Selectable languages. Default `CODE_LANGS`. */
|
|
38
|
+
langs?: readonly string[];
|
|
39
|
+
/** Default `DEFAULT_LANG`. */
|
|
40
|
+
defaultLang?: string;
|
|
40
41
|
classNames?: CodeBlockClassNames;
|
|
41
42
|
icons?: {
|
|
42
43
|
lang?: ReactNode;
|
|
@@ -46,7 +47,7 @@ export interface CodeBlockOptions {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
/** A code block whose language is pickable from a dropdown. */
|
|
49
|
-
export declare function createCodeBlock({ langs, defaultLang, classNames, icons, }
|
|
50
|
+
export declare function createCodeBlock({ langs, defaultLang, classNames, icons, }?: CodeBlockOptions): TiptapNode<import("@tiptap/extension-code-block").CodeBlockOptions, any>;
|
|
50
51
|
export interface TabsClassNames {
|
|
51
52
|
editor?: string;
|
|
52
53
|
list?: string;
|
package/dist/tiptap.js
CHANGED
|
@@ -23,6 +23,7 @@ import { toMapEmbedUrl } from './maps.js';
|
|
|
23
23
|
import { useClickOutside } from './react.js';
|
|
24
24
|
import { cx } from './html.js';
|
|
25
25
|
import { slugifyHeading, uniqueHeadingId } from './headings.js';
|
|
26
|
+
import { CODE_LANGS, DEFAULT_LANG } from './blocks.js';
|
|
26
27
|
/** Local `cn`. Both wikis import one; the package will not depend on one. */
|
|
27
28
|
// ---- iframe -----------------------------------------------------------------
|
|
28
29
|
export const Iframe = TiptapNode.create({
|
|
@@ -216,7 +217,7 @@ export function createMapEmbed({ resolveMapUrl }) {
|
|
|
216
217
|
});
|
|
217
218
|
}
|
|
218
219
|
/** A code block whose language is pickable from a dropdown. */
|
|
219
|
-
export function createCodeBlock({ langs, defaultLang, classNames = {}, icons = {}, }) {
|
|
220
|
+
export function createCodeBlock({ langs = CODE_LANGS, defaultLang = DEFAULT_LANG, classNames = {}, icons = {}, } = {}) {
|
|
220
221
|
function CodeBlockView({ node, updateAttributes, }) {
|
|
221
222
|
const [open, setOpen] = useState(false);
|
|
222
223
|
const close = useCallback(() => setOpen(false), []);
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Sharp } from 'sharp';
|
|
2
|
+
import { type RateLimitOptions } from './rate-limit.js';
|
|
3
|
+
export type ImageFormat = 'jpg' | 'png' | 'gif' | 'webp' | 'avif';
|
|
4
|
+
export interface UploadOptions {
|
|
5
|
+
/** The uploader's id, which keys the rate limit, or the response that turns them away. */
|
|
6
|
+
authorize: (request: Request) => Promise<string | Response>;
|
|
7
|
+
/** Default 4 MB. */
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
/** Decoded pixels, default 40 million: a phone camera's full resolution. */
|
|
10
|
+
maxPixels?: number;
|
|
11
|
+
/** Default a burst of 20, then one every five seconds. */
|
|
12
|
+
rateLimit?: RateLimitOptions;
|
|
13
|
+
/**
|
|
14
|
+
* How the upright image is written. Default the format it arrived in. A wiki
|
|
15
|
+
* with an image standard resizes and converts here.
|
|
16
|
+
*/
|
|
17
|
+
encode?: (image: Sharp, source: {
|
|
18
|
+
format: ImageFormat;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}) => {
|
|
22
|
+
image: Sharp;
|
|
23
|
+
format: ImageFormat;
|
|
24
|
+
};
|
|
25
|
+
/** Prepended to the stored name, e.g. `wiki/`. */
|
|
26
|
+
prefix?: string;
|
|
27
|
+
/** Stores the bytes and resolves to their public URL. Default Vercel Blob, public. */
|
|
28
|
+
store?: (path: string, body: Buffer, contentType: string) => Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/** The POST handler: a multipart `file` in, `{ url }` out. */
|
|
31
|
+
export declare function uploadHandler({ authorize, maxBytes, maxPixels, rateLimit, encode, prefix, store, }: UploadOptions): (request: Request) => Promise<Response>;
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// upload.ts — the route an editor's image upload posts to.
|
|
2
|
+
//
|
|
3
|
+
// radix-wiki and caper had each written it, and their limits had drifted apart:
|
|
4
|
+
// one took 25 MB with no cap on decoded pixels and trusted the client's
|
|
5
|
+
// Content-Type, the other 4 MB, 40 megapixels and the file's own leading bytes.
|
|
6
|
+
// This is the second, with the first one's re-encode as an option.
|
|
7
|
+
//
|
|
8
|
+
// - **The bytes say what the file is.** The Content-Type and the name are the
|
|
9
|
+
// caller's to choose, and trusting them stores arbitrary bytes under an image
|
|
10
|
+
// URL, served with a spoofed type. SVG is not accepted: it is markup.
|
|
11
|
+
// - **Pixels are capped as well as bytes.** A PNG well under the byte limit can
|
|
12
|
+
// decode to gigabytes.
|
|
13
|
+
// - **Every image is re-encoded, never stored as sent.** Re-encoding writes
|
|
14
|
+
// none of the source's metadata, and with it no GPS position from a phone's
|
|
15
|
+
// EXIF; the orientation that metadata carried is applied first.
|
|
16
|
+
// - **Each uploader has a budget**, keyed on who they are and where from.
|
|
17
|
+
//
|
|
18
|
+
// `sharp` and `@vercel/blob` are optional peers. Run the route on Node, never
|
|
19
|
+
// the edge: sharp is native.
|
|
20
|
+
import sharp, {} from 'sharp';
|
|
21
|
+
import { put } from '@vercel/blob';
|
|
22
|
+
import { errors, handleRoute, json } from './http.js';
|
|
23
|
+
import { limitClient } from './rate-limit.js';
|
|
24
|
+
const CONTENT_TYPE = {
|
|
25
|
+
jpg: 'image/jpeg',
|
|
26
|
+
png: 'image/png',
|
|
27
|
+
gif: 'image/gif',
|
|
28
|
+
webp: 'image/webp',
|
|
29
|
+
avif: 'image/avif',
|
|
30
|
+
};
|
|
31
|
+
/** The format the leading bytes declare, or null for anything not an accepted image. */
|
|
32
|
+
function sniffImage(buf) {
|
|
33
|
+
const ascii = (from, to) => String.fromCharCode(...buf.subarray(from, to));
|
|
34
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
35
|
+
return 'jpg';
|
|
36
|
+
if (buf[0] === 0x89 && ascii(1, 4) === 'PNG')
|
|
37
|
+
return 'png';
|
|
38
|
+
if (/^GIF8[79]a$/.test(ascii(0, 6)))
|
|
39
|
+
return 'gif';
|
|
40
|
+
if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP')
|
|
41
|
+
return 'webp';
|
|
42
|
+
if (ascii(4, 8) === 'ftyp' && ['avif', 'avis'].includes(ascii(8, 12)))
|
|
43
|
+
return 'avif';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const vercelBlob = async (path, body, contentType) => (await put(path, body, { access: 'public', addRandomSuffix: false, contentType })).url;
|
|
47
|
+
/** The POST handler: a multipart `file` in, `{ url }` out. */
|
|
48
|
+
export function uploadHandler({ authorize, maxBytes = 4 * 1024 * 1024, maxPixels = 40_000_000, rateLimit = { capacity: 20, refillPerSec: 0.2 }, encode = (image, { format }) => ({ image: image.toFormat(format === 'jpg' ? 'jpeg' : format), format }), prefix = '', store = vercelBlob, }) {
|
|
49
|
+
const megabytes = Math.round((maxBytes / 1024 / 1024) * 10) / 10;
|
|
50
|
+
const megapixels = Math.round(maxPixels / 1_000_000);
|
|
51
|
+
return request => handleRoute(async () => {
|
|
52
|
+
const who = await authorize(request);
|
|
53
|
+
if (who instanceof Response)
|
|
54
|
+
return who;
|
|
55
|
+
const limit = limitClient(`upload:${who}`, request.headers, rateLimit);
|
|
56
|
+
if (!limit.ok) {
|
|
57
|
+
return errors.tooManyRequests(limit.retryAfterSec, `Too many uploads. Try again in ${limit.retryAfterSec}s.`);
|
|
58
|
+
}
|
|
59
|
+
const file = (await request.formData()).get('file');
|
|
60
|
+
if (!(file instanceof File))
|
|
61
|
+
return errors.badRequest('No file provided');
|
|
62
|
+
if (file.size > maxBytes)
|
|
63
|
+
return errors.badRequest(`File too large (max ${megabytes}MB)`);
|
|
64
|
+
const bytes = Buffer.from(await file.arrayBuffer());
|
|
65
|
+
const format = sniffImage(bytes);
|
|
66
|
+
if (!format)
|
|
67
|
+
return errors.badRequest('Invalid file. Allowed: JPEG, PNG, GIF, WebP, AVIF');
|
|
68
|
+
let out;
|
|
69
|
+
try {
|
|
70
|
+
const meta = await sharp(bytes, { limitInputPixels: maxPixels }).metadata();
|
|
71
|
+
const width = meta.width ?? 0;
|
|
72
|
+
const height = meta.pageHeight ?? meta.height ?? 0;
|
|
73
|
+
if (!width || !height)
|
|
74
|
+
throw new Error('no dimensions');
|
|
75
|
+
if (width * height > maxPixels)
|
|
76
|
+
return errors.badRequest(`Image too large (max ${megapixels} megapixels)`);
|
|
77
|
+
const upright = sharp(bytes, { animated: (meta.pages ?? 1) > 1, limitInputPixels: maxPixels }).rotate();
|
|
78
|
+
const encoded = encode(upright, { format, width, height });
|
|
79
|
+
out = { buffer: await encoded.image.toBuffer(), format: encoded.format };
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
// Never fall back to the bytes as sent: storing them is what leaks the location.
|
|
83
|
+
const tooBig = e instanceof Error && e.message.includes('pixel limit');
|
|
84
|
+
return errors.badRequest(tooBig
|
|
85
|
+
? `Image too large (max ${megapixels} megapixels)`
|
|
86
|
+
: 'Could not read this image. Save it as PNG or JPEG and try again.');
|
|
87
|
+
}
|
|
88
|
+
const url = await store(`${prefix}${crypto.randomUUID()}.${out.format}`, out.buffer, CONTENT_TYPE[out.format]);
|
|
89
|
+
return json({ url });
|
|
90
|
+
}, 'Upload failed');
|
|
91
|
+
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -9,16 +9,10 @@
|
|
|
9
9
|
* better rejected than stored.
|
|
10
10
|
*/
|
|
11
11
|
export declare function safeLinkHref(raw: string | null | undefined): string | null;
|
|
12
|
-
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. */
|
|
13
|
-
export declare const okUrl: (u: unknown) => boolean;
|
|
14
12
|
/** `[{ id, text, url? }]` — the shape a references block stores. */
|
|
15
13
|
export declare function validateReferenceItems(items: unknown): boolean;
|
|
16
14
|
/** `[{ id, heading, links: [{ label, href }] }]` — a link-grid block's groups. */
|
|
17
15
|
export declare function validateLinkGroups(groups: unknown): boolean;
|
|
18
|
-
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. */
|
|
19
|
-
export declare function validateCodeTabs(tabs: unknown): boolean;
|
|
20
|
-
/** `[{ value, label, suffix? }]` — a stats block's cards. */
|
|
21
|
-
export declare function validateStatItems(items: unknown): boolean;
|
|
22
16
|
/**
|
|
23
17
|
* The most rows an author-supplied count may ask for. Every `limit` and every
|
|
24
18
|
* `pageIds` list ends up in a database `take:` or `in:`, so the bound belongs
|
package/dist/validation.js
CHANGED
|
@@ -36,7 +36,7 @@ export function safeLinkHref(raw) {
|
|
|
36
36
|
}
|
|
37
37
|
return href;
|
|
38
38
|
}
|
|
39
|
-
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. */
|
|
39
|
+
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. @internal */
|
|
40
40
|
export const okUrl = (u) => typeof u === 'string' && (u === '' || safeLinkHref(u) !== null);
|
|
41
41
|
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
42
42
|
/** `[{ id, text, url? }]` — the shape a references block stores. */
|
|
@@ -56,7 +56,7 @@ export function validateLinkGroups(groups) {
|
|
|
56
56
|
Array.isArray(g.links) &&
|
|
57
57
|
g.links.every(l => isRecord(l) && typeof l.label === 'string' && okUrl(l.href))));
|
|
58
58
|
}
|
|
59
|
-
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. */
|
|
59
|
+
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. @internal */
|
|
60
60
|
export function validateCodeTabs(tabs) {
|
|
61
61
|
return (Array.isArray(tabs) &&
|
|
62
62
|
tabs.every(t => isRecord(t) &&
|
|
@@ -64,7 +64,7 @@ export function validateCodeTabs(tabs) {
|
|
|
64
64
|
typeof t.code === 'string' &&
|
|
65
65
|
(t.language === undefined || typeof t.language === 'string')));
|
|
66
66
|
}
|
|
67
|
-
/** `[{ value, label, suffix? }]` — a stats block's cards. */
|
|
67
|
+
/** `[{ value, label, suffix? }]` — a stats block's cards. @internal */
|
|
68
68
|
export function validateStatItems(items) {
|
|
69
69
|
return (Array.isArray(items) &&
|
|
70
70
|
items.every(s => isRecord(s) &&
|
package/dist/well-known.d.ts
CHANGED
|
@@ -4,25 +4,6 @@ export interface RegistryAuthRecord {
|
|
|
4
4
|
contentType: string;
|
|
5
5
|
cacheControl: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
* The `v=MCPv1` record the official MCP registry fetches from
|
|
9
|
-
* `/.well-known/mcp-registry-auth` to prove domain ownership. `mcp-publisher
|
|
10
|
-
* login http --domain=<host>` checks the key here against the private key
|
|
11
|
-
* signing the login, which is what grants publish rights over the reversed-
|
|
12
|
-
* domain namespace the server's name sits in.
|
|
13
|
-
*
|
|
14
|
-
* Returns null when no key is configured, so the caller can 404 rather than
|
|
15
|
-
* serve a malformed record — a missing key should read as "not configured",
|
|
16
|
-
* not as a verification failure nobody can explain.
|
|
17
|
-
*
|
|
18
|
-
* The key material belongs in an env var rather than the repo: the public half
|
|
19
|
-
* is harmless to serve but pointless to commit, and keeping it out gives the
|
|
20
|
-
* private half an obvious home too (a keychain, never git).
|
|
21
|
-
*
|
|
22
|
-
* openssl genpkey -algorithm Ed25519 -out key.pem
|
|
23
|
-
* openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
|
|
24
|
-
*/
|
|
25
|
-
export declare function registryAuthRecord(publicKey?: string, keyType?: string): RegistryAuthRecord | null;
|
|
26
7
|
/** A day at the edge, a week stale-while-revalidate. A card changes rarely. */
|
|
27
8
|
export declare const AGENT_CARD_CACHE_CONTROL = "public, s-maxage=86400, stale-while-revalidate=604800";
|
|
28
9
|
export interface AgentSkill {
|
|
@@ -86,7 +67,6 @@ export interface ServerManifest {
|
|
|
86
67
|
websiteUrl?: string;
|
|
87
68
|
remotes?: ReadonlyArray<Record<string, unknown>>;
|
|
88
69
|
}
|
|
89
|
-
export declare const SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/draft/server-card.schema.json";
|
|
90
70
|
/**
|
|
91
71
|
* The card served at `<mcp-url>/server-card`.
|
|
92
72
|
*
|
package/dist/well-known.js
CHANGED
|
@@ -29,6 +29,8 @@ import { descriptorResponse } from './http.js';
|
|
|
29
29
|
*
|
|
30
30
|
* openssl genpkey -algorithm Ed25519 -out key.pem
|
|
31
31
|
* openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
32
34
|
*/
|
|
33
35
|
export function registryAuthRecord(publicKey, keyType) {
|
|
34
36
|
if (!publicKey)
|
|
@@ -105,6 +107,7 @@ export function agentCard(config) {
|
|
|
105
107
|
defaultOutputModes: ['text/plain', 'application/json', 'text/markdown'],
|
|
106
108
|
};
|
|
107
109
|
}
|
|
110
|
+
/** @internal */
|
|
108
111
|
export const SERVER_CARD_SCHEMA = 'https://static.modelcontextprotocol.io/schemas/draft/server-card.schema.json';
|
|
109
112
|
/**
|
|
110
113
|
* The card served at `<mcp-url>/server-card`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,6 +82,11 @@
|
|
|
82
82
|
"import": "./dist/passkey-button.js",
|
|
83
83
|
"default": "./dist/passkey-button.js"
|
|
84
84
|
},
|
|
85
|
+
"./rola": {
|
|
86
|
+
"types": "./dist/rola.d.ts",
|
|
87
|
+
"import": "./dist/rola.js",
|
|
88
|
+
"default": "./dist/rola.js"
|
|
89
|
+
},
|
|
85
90
|
"./blocks": {
|
|
86
91
|
"types": "./dist/blocks.d.ts",
|
|
87
92
|
"import": "./dist/blocks.js",
|
|
@@ -92,11 +97,6 @@
|
|
|
92
97
|
"import": "./dist/combobox.js",
|
|
93
98
|
"default": "./dist/combobox.js"
|
|
94
99
|
},
|
|
95
|
-
"./indexnow": {
|
|
96
|
-
"types": "./dist/indexnow.d.ts",
|
|
97
|
-
"import": "./dist/indexnow.js",
|
|
98
|
-
"default": "./dist/indexnow.js"
|
|
99
|
-
},
|
|
100
100
|
"./crawlers": {
|
|
101
101
|
"types": "./dist/crawlers.d.ts",
|
|
102
102
|
"import": "./dist/crawlers.js",
|
|
@@ -223,6 +223,11 @@
|
|
|
223
223
|
"import": "./dist/sanitize.js",
|
|
224
224
|
"default": "./dist/sanitize.js"
|
|
225
225
|
},
|
|
226
|
+
"./upload": {
|
|
227
|
+
"types": "./dist/upload.d.ts",
|
|
228
|
+
"import": "./dist/upload.js",
|
|
229
|
+
"default": "./dist/upload.js"
|
|
230
|
+
},
|
|
226
231
|
"./corpus": {
|
|
227
232
|
"types": "./dist/corpus.d.ts",
|
|
228
233
|
"import": "./dist/corpus.js",
|
|
@@ -246,11 +251,15 @@
|
|
|
246
251
|
"analytics-digest": "bin/analytics-digest.mjs",
|
|
247
252
|
"passkey-invite": "bin/passkey-invite.mjs",
|
|
248
253
|
"fts-ddl": "bin/fts-ddl.mjs",
|
|
249
|
-
"indexnow": "bin/indexnow.mjs"
|
|
254
|
+
"indexnow": "bin/indexnow.mjs",
|
|
255
|
+
"prisma-models": "bin/prisma-models.mjs",
|
|
256
|
+
"mark-verified": "bin/mark-verified.mjs",
|
|
257
|
+
"maintenance-log": "bin/maintenance-log.mjs"
|
|
250
258
|
},
|
|
251
259
|
"files": [
|
|
252
260
|
"dist",
|
|
253
261
|
"bin",
|
|
262
|
+
"prisma",
|
|
254
263
|
"tsconfig.base.json"
|
|
255
264
|
],
|
|
256
265
|
"engines": {
|
|
@@ -262,6 +271,7 @@
|
|
|
262
271
|
"prepare": "git config core.hooksPath .githooks || true"
|
|
263
272
|
},
|
|
264
273
|
"devDependencies": {
|
|
274
|
+
"@radixdlt/rola": "^2.1.0",
|
|
265
275
|
"@simplewebauthn/browser": "^14.0.0",
|
|
266
276
|
"@simplewebauthn/server": "^14.0.3",
|
|
267
277
|
"@tiptap/core": "^2.10.0",
|
|
@@ -280,11 +290,14 @@
|
|
|
280
290
|
"@types/node": "^22.0.0",
|
|
281
291
|
"@types/react": "^19.0.0",
|
|
282
292
|
"@types/sanitize-html": "^2.16.1",
|
|
293
|
+
"@vercel/blob": "^2.8.2",
|
|
283
294
|
"fast-diff": "^1.3.0",
|
|
295
|
+
"jose": "^6.2.12",
|
|
284
296
|
"jsdom": "^30.1.0",
|
|
285
297
|
"lightweight-charts": "^5.2.1",
|
|
286
298
|
"react": "^19.2.4",
|
|
287
299
|
"sanitize-html": "^2.17.7",
|
|
300
|
+
"sharp": "^0.35.5",
|
|
288
301
|
"typescript": "^5.9.0"
|
|
289
302
|
},
|
|
290
303
|
"peerDependencies": {
|
|
@@ -304,10 +317,14 @@
|
|
|
304
317
|
"@tiptap/react": ">=2.10",
|
|
305
318
|
"@tiptap/starter-kit": ">=2.10",
|
|
306
319
|
"fast-diff": ">=1.3",
|
|
320
|
+
"lightweight-charts": ">=5",
|
|
307
321
|
"pg": ">=8",
|
|
308
322
|
"react": ">=18",
|
|
309
323
|
"sanitize-html": ">=2",
|
|
310
|
-
"
|
|
324
|
+
"jose": ">=6",
|
|
325
|
+
"@radixdlt/rola": ">=2",
|
|
326
|
+
"sharp": ">=0.33",
|
|
327
|
+
"@vercel/blob": ">=2"
|
|
311
328
|
},
|
|
312
329
|
"peerDependenciesMeta": {
|
|
313
330
|
"react": {
|
|
@@ -369,6 +386,18 @@
|
|
|
369
386
|
},
|
|
370
387
|
"lightweight-charts": {
|
|
371
388
|
"optional": true
|
|
389
|
+
},
|
|
390
|
+
"jose": {
|
|
391
|
+
"optional": true
|
|
392
|
+
},
|
|
393
|
+
"@radixdlt/rola": {
|
|
394
|
+
"optional": true
|
|
395
|
+
},
|
|
396
|
+
"sharp": {
|
|
397
|
+
"optional": true
|
|
398
|
+
},
|
|
399
|
+
"@vercel/blob": {
|
|
400
|
+
"optional": true
|
|
372
401
|
}
|
|
373
402
|
}
|
|
374
403
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shipped by wiki-formant (`npx prisma-models`). Edit it there, not here:
|
|
2
|
+
// `prisma-models --check` fails on a copy that has drifted.
|
|
3
|
+
//
|
|
4
|
+
// Page views and events, counted by wiki-formant/analytics. Its SQL names these
|
|
5
|
+
// tables as written, so they carry no @@map. A visitor is a hash under the day's
|
|
6
|
+
// Salt, which is replaced daily; rows older than 400 days are deleted. `siteId`
|
|
7
|
+
// is "" in an app that serves one site.
|
|
8
|
+
|
|
9
|
+
model View {
|
|
10
|
+
id BigInt @id @default(autoincrement())
|
|
11
|
+
siteId String @default("")
|
|
12
|
+
path String
|
|
13
|
+
source String?
|
|
14
|
+
country String?
|
|
15
|
+
device String
|
|
16
|
+
visitor String
|
|
17
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
18
|
+
|
|
19
|
+
@@index([siteId, createdAt])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
model Event {
|
|
23
|
+
id BigInt @id @default(autoincrement())
|
|
24
|
+
siteId String @default("")
|
|
25
|
+
name String
|
|
26
|
+
path String?
|
|
27
|
+
props Json @default("{}")
|
|
28
|
+
visitor String?
|
|
29
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
30
|
+
|
|
31
|
+
@@index([siteId, name, createdAt])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
model Salt {
|
|
35
|
+
day String @id
|
|
36
|
+
value String
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Days a site counted somewhere else before it counted its own, one row a day
|
|
40
|
+
// with each breakdown as { value: visitors }. Nothing writes or deletes it:
|
|
41
|
+
// the app loads it once, and digest adds the days in a window to the views.
|
|
42
|
+
model ViewDay {
|
|
43
|
+
siteId String @default("")
|
|
44
|
+
day DateTime @db.Date
|
|
45
|
+
visitors Int
|
|
46
|
+
pageviews Int
|
|
47
|
+
visits Int
|
|
48
|
+
bounces Int
|
|
49
|
+
seconds Int
|
|
50
|
+
pages Json @default("{}")
|
|
51
|
+
entryPages Json @default("{}")
|
|
52
|
+
sources Json @default("{}")
|
|
53
|
+
countries Json @default("{}")
|
|
54
|
+
devices Json @default("{}")
|
|
55
|
+
|
|
56
|
+
@@id([siteId, day])
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shipped by wiki-formant (`npx prisma-models`). Edit it there, not here:
|
|
2
|
+
// `prisma-models --check` fails on a copy that has drifted.
|
|
3
|
+
//
|
|
4
|
+
// The passkey door to a site's admin pages. wiki-formant/passkey owns the SQL
|
|
5
|
+
// and names these tables as written, so they carry no @@map. Every token is
|
|
6
|
+
// stored as its hash; `npx passkey-invite <url>` prints the link that saves a
|
|
7
|
+
// passkey.
|
|
8
|
+
|
|
9
|
+
model Passkey {
|
|
10
|
+
id String @id // the credential id, base64url
|
|
11
|
+
publicKey String // base64url
|
|
12
|
+
counter Int
|
|
13
|
+
transports String[]
|
|
14
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model PasskeyToken {
|
|
18
|
+
hash String @id // sha-256 of the token, base64url
|
|
19
|
+
kind String // session | invite | challenge
|
|
20
|
+
expiresAt DateTime @db.Timestamptz(3)
|
|
21
|
+
}
|