wiki-formant 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/maps.js ADDED
@@ -0,0 +1,77 @@
1
+ // maps.ts — turn a map URL a human pasted into one an <iframe> will accept.
2
+ //
3
+ // Both wikis carried this byte-for-byte apart from one `export` keyword. It is
4
+ // pure string work over Google and Apple Maps URL shapes, with no framework or
5
+ // database in it, which is why neither copy had a reason to diverge.
6
+ /** A plain embed URL for a coordinate pair. */
7
+ export function mapsEmbedUrl(lat, lon, zoom = 15) {
8
+ return `https://maps.google.com/maps?q=${lat},${lon}&z=${zoom}&output=embed`;
9
+ }
10
+ /**
11
+ * Dig a coordinate pair out of a maps URL. Four shapes in descending
12
+ * specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
13
+ * `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
14
+ */
15
+ export function extractCoordsFromUrl(url) {
16
+ const coords = url.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*),?(\d+\.?\d*)?z?/);
17
+ if (coords)
18
+ return { lat: +coords[1], lon: +coords[2], zoom: coords[3] ? +coords[3] : undefined };
19
+ const search = url.match(/\/search\/(-?\d+\.?\d*),[\s+]*(-?\d+\.?\d*)/);
20
+ if (search)
21
+ return { lat: +search[1], lon: +search[2] };
22
+ const data = url.match(/!3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)/);
23
+ if (data)
24
+ return { lat: +data[1], lon: +data[2] };
25
+ try {
26
+ const ll = new URL(url).searchParams.get('ll') ?? new URL(url).searchParams.get('sll');
27
+ if (ll) {
28
+ const [lat, lon] = ll.split(',').map(Number);
29
+ if (lat !== undefined && lon !== undefined && !isNaN(lat) && !isNaN(lon))
30
+ return { lat, lon };
31
+ }
32
+ }
33
+ catch {
34
+ /* not a valid URL */
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * An embeddable URL for `url`, or null when it is not a map link this
40
+ * understands. Already-embeddable URLs pass through untouched.
41
+ */
42
+ export function toMapEmbedUrl(url) {
43
+ if (/google\.[a-z.]+\/maps\/embed/.test(url))
44
+ return url;
45
+ if (/embed\.apple\.com\/maps/.test(url))
46
+ return url;
47
+ const c = extractCoordsFromUrl(url);
48
+ if (c)
49
+ return mapsEmbedUrl(c.lat, c.lon, c.zoom);
50
+ if (/google\.[a-z.]+\/maps/.test(url)) {
51
+ const place = url.match(/\/place\/([^/@]+)/);
52
+ if (place) {
53
+ const q = encodeURIComponent(decodeURIComponent(place[1]).replace(/\+/g, ' '));
54
+ return `https://maps.google.com/maps?q=${q}&output=embed`;
55
+ }
56
+ }
57
+ if (/maps\.apple\.com/.test(url)) {
58
+ try {
59
+ const u = new URL(url);
60
+ const ll = u.searchParams.get('ll') ?? u.searchParams.get('sll');
61
+ const q = u.searchParams.get('q') ?? u.searchParams.get('address');
62
+ const params = new URLSearchParams();
63
+ if (ll)
64
+ params.set('ll', ll);
65
+ if (q)
66
+ params.set('q', q);
67
+ return `https://embed.apple.com/maps?${params.toString()}`;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /** True for the shortener forms that only a redirect can resolve. */
76
+ export const isShortMapUrl = (url) => /maps\.app\.goo\.gl|goo\.gl\/maps/.test(url);
77
+ //# sourceMappingURL=maps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maps.js","sourceRoot":"","sources":["../src/maps.ts"],"names":[],"mappings":"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"}
package/dist/text.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { CodeTab, ReferenceItem } from './blocks.js';
2
+ /**
3
+ * HTML to readable text. Links keep their href in parentheses so a model can
4
+ * still follow a citation; block-level tags become newlines; list items get a
5
+ * bullet. Entities are decoded last, after the tags are gone, so a `&lt;` in
6
+ * prose cannot become a tag the strip has already run past.
7
+ */
8
+ export declare function stripHtml(html: string): string;
9
+ /** The six maintenance-banner variants every wiki here renders. */
10
+ export type BannerVariant = 'stub' | 'unsourced' | 'outdated' | 'promotional' | 'cleanup' | 'coi';
11
+ /**
12
+ * Display labels for the maintenance banners. Canonical: the prose extractor,
13
+ * the markdown twin and the MDX export all render the same six strings.
14
+ */
15
+ export declare const BANNER_LABELS: Record<BannerVariant, string>;
16
+ /** A maintenance notice, inline: `[Notice: Needs citations] …`. */
17
+ export declare function bannerToText(label: string, text?: string | null): string;
18
+ /** Each tab under its label, tags stripped — highlighted markup is noise here. */
19
+ export declare function codeTabsToText(tabs: readonly CodeTab[]): string;
20
+ /** A numbered reference list, or `''` when there are none. */
21
+ export declare function referencesToText(items: readonly ReferenceItem[]): string;
22
+ /**
23
+ * Every `text` value at any depth of a block tree, in document order.
24
+ *
25
+ * Deliberately NOT the typed extractor above: that walks a switch and formats
26
+ * for reading (labels, bullets, reference numbering), so it can surface text a
27
+ * search index never matched and miss text it did. A snippet claiming to show
28
+ * why a row matched has to read the same bytes the match was made against.
29
+ */
30
+ export declare function collectText(node: unknown, out?: string[]): string[];
31
+ /**
32
+ * The passage that matched `query`, not the opening of the page.
33
+ *
34
+ * Wiki pages open with an infobox far more often than not — 243 of 269 on caper
35
+ * when this was measured — so an opening-line snippet hands nine search rows in
36
+ * ten a flattened metadata table, whatever the query was. Falls back to the
37
+ * opening when the term appears only in the title, which is a real case rather
38
+ * than a failure: title-only hits are how the top tiers match.
39
+ */
40
+ export declare function matchSnippet(blocks: unknown, query: string, opening: () => string, maxLen?: number): string;
41
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9C;AAED,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,GAAG,KAAK,CAAC;AAElG;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAOvD,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAExE;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAIxE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAYvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAwBxG"}
package/dist/text.js ADDED
@@ -0,0 +1,112 @@
1
+ // text.ts — a block tree as plain prose, for LLM and MCP exports.
2
+ //
3
+ // The markdown twin in `blocks.ts` is for a reader; this is for a model reading
4
+ // the page as evidence. Both wikis had the same `stripHtml`, the same six
5
+ // banner labels and the same four leaf bodies, character for character.
6
+ //
7
+ // As in `blocks.ts`, the dispatch stays with the caller — a `switch` over its
8
+ // own block union, where a new type is a compile error until it is handled —
9
+ // and only the bodies live here.
10
+ import { decodeEntities } from './entities.js';
11
+ /**
12
+ * HTML to readable text. Links keep their href in parentheses so a model can
13
+ * still follow a citation; block-level tags become newlines; list items get a
14
+ * bullet. Entities are decoded last, after the tags are gone, so a `&lt;` in
15
+ * prose cannot become a tag the strip has already run past.
16
+ */
17
+ export function stripHtml(html) {
18
+ return decodeEntities(html
19
+ .replace(/<a[^>]+href="([^"]*)"[^>]*>(.*?)<\/a>/gi, ' $2 ($1) ')
20
+ .replace(/<br\s*\/?>/gi, '\n')
21
+ .replace(/<\/(?:p|h[1-6]|li|tr|th|td|div)>/gi, '\n')
22
+ .replace(/<(?:li)>/gi, '- ')
23
+ .replace(/<[^>]+>/g, ''))
24
+ .replace(/\n{3,}/g, '\n\n')
25
+ .trim();
26
+ }
27
+ /**
28
+ * Display labels for the maintenance banners. Canonical: the prose extractor,
29
+ * the markdown twin and the MDX export all render the same six strings.
30
+ */
31
+ export const BANNER_LABELS = {
32
+ stub: 'Stub',
33
+ unsourced: 'Needs citations',
34
+ outdated: 'May be outdated',
35
+ promotional: 'Written like an advertisement',
36
+ cleanup: 'Needs cleanup',
37
+ coi: 'Conflict of interest',
38
+ };
39
+ /** A maintenance notice, inline: `[Notice: Needs citations] …`. */
40
+ export function bannerToText(label, text) {
41
+ return `[Notice: ${label}]${text ? ' ' + stripHtml(text) : ''}`;
42
+ }
43
+ /** Each tab under its label, tags stripped — highlighted markup is noise here. */
44
+ export function codeTabsToText(tabs) {
45
+ return tabs.map(t => `[${t.label}]\n${t.code}`).join('\n');
46
+ }
47
+ /** A numbered reference list, or `''` when there are none. */
48
+ export function referencesToText(items) {
49
+ if (!items.length)
50
+ return '';
51
+ const lines = items.map((it, i) => `${i + 1}. ${stripHtml(it.text)}${it.url ? ` (${it.url})` : ''}`);
52
+ return `References:\n${lines.join('\n')}`;
53
+ }
54
+ /**
55
+ * Every `text` value at any depth of a block tree, in document order.
56
+ *
57
+ * Deliberately NOT the typed extractor above: that walks a switch and formats
58
+ * for reading (labels, bullets, reference numbering), so it can surface text a
59
+ * search index never matched and miss text it did. A snippet claiming to show
60
+ * why a row matched has to read the same bytes the match was made against.
61
+ */
62
+ export function collectText(node, out = []) {
63
+ if (Array.isArray(node)) {
64
+ for (const item of node)
65
+ collectText(item, out);
66
+ return out;
67
+ }
68
+ if (node && typeof node === 'object') {
69
+ for (const [key, value] of Object.entries(node)) {
70
+ if (key === 'text' && typeof value === 'string')
71
+ out.push(value);
72
+ else
73
+ collectText(value, out);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+ /**
79
+ * The passage that matched `query`, not the opening of the page.
80
+ *
81
+ * Wiki pages open with an infobox far more often than not — 243 of 269 on caper
82
+ * when this was measured — so an opening-line snippet hands nine search rows in
83
+ * ten a flattened metadata table, whatever the query was. Falls back to the
84
+ * opening when the term appears only in the title, which is a real case rather
85
+ * than a failure: title-only hits are how the top tiers match.
86
+ */
87
+ export function matchSnippet(blocks, query, opening, maxLen = 200) {
88
+ const term = query.trim();
89
+ if (!term)
90
+ return opening();
91
+ // Collapse tags and non-breaking spaces the way SQL does, so a phrase broken
92
+ // by markup is still one searchable string, then decode what is left: editors
93
+ // store typographic punctuation named, and without this a snippet reads back
94
+ // "docs &middot; Related".
95
+ const text = decodeEntities(collectText(blocks).join(' ').replace(/<[^>]*>|&nbsp;/g, ' '))
96
+ .replace(/\s+/g, ' ') // JS \s covers U+00A0, which SQL has to translate by hand
97
+ .trim();
98
+ const at = text.toLowerCase().indexOf(term.toLowerCase());
99
+ if (at === -1)
100
+ return opening();
101
+ // Keep about a line of lead-in, cut to a word boundary so it does not open
102
+ // mid-word.
103
+ let start = Math.max(0, at - 60);
104
+ if (start > 0) {
105
+ const boundary = text.indexOf(' ', start);
106
+ if (boundary > -1 && boundary < at)
107
+ start = boundary + 1;
108
+ }
109
+ const end = Math.min(text.length, start + maxLen);
110
+ return `${start > 0 ? '…' : ''}${text.slice(start, end).trimEnd()}${end < text.length ? '…' : ''}`;
111
+ }
112
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * An href acceptable to persist: http, https, mailto, or a relative path or
3
+ * fragment. Everything else — `javascript:`, `data:`, `vbscript:`, a
4
+ * protocol-relative `//host` — comes back null.
5
+ *
6
+ * React 19 already neutralises `javascript:` in an href at render time, so this
7
+ * is defence in depth rather than the only thing between an editor and a
8
+ * reader. Its value is at the write path: a URL that can never render safely is
9
+ * better rejected than stored.
10
+ */
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
+ /** `[{ id, text, url? }]` — the shape a references block stores. */
15
+ export declare function validateReferenceItems(items: unknown, urlCheck?: (u: unknown) => boolean): boolean;
16
+ /** `[{ id, heading, links: [{ label, href }] }]` — a link-grid block's groups. */
17
+ export declare function validateLinkGroups(groups: unknown, urlCheck?: (u: unknown) => boolean): boolean;
18
+ export interface BlockValidatorOptions {
19
+ /** True for a type this wiki knows at all. */
20
+ isKnownType: (type: string) => boolean;
21
+ /** True for a type that may nest inside a container. */
22
+ isAtomicType: (type: string) => boolean;
23
+ /** This wiki's switch over its leaf types. Runs only after id/type pass. */
24
+ validateAtomic: (block: Record<string, unknown>) => boolean;
25
+ }
26
+ /**
27
+ * The block-tree walk, with one wiki's leaf switch plugged into it. The caller
28
+ * keeps its own type parameter, so this stays free of any repo's types while
29
+ * the call site still gets a real type guard.
30
+ */
31
+ export declare function createBlockValidator(opts: BlockValidatorOptions): {
32
+ /** One leaf block; container types are rejected. */
33
+ validateAtomicBlock: (block: unknown) => boolean;
34
+ /** One block of any kind, containers included. */
35
+ validateBlock: (block: unknown) => boolean;
36
+ /** A whole page's content array. */
37
+ validateBlocks: (content: unknown) => boolean;
38
+ };
39
+ /**
40
+ * A copy of a block with a fresh id at every level, so a duplicated container
41
+ * does not share child ids with its original.
42
+ */
43
+ export declare function duplicateBlockIds<B extends {
44
+ type: string;
45
+ id: string;
46
+ }>(block: B, newId: () => string): B;
47
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAWA;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAa1E;AAED,iFAAiF;AACjF,eAAO,MAAM,KAAK,GAAI,GAAG,OAAO,KAAG,OAC8B,CAAC;AAKlE,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,GAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAe,GAAG,OAAO,CAWzG;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAe,GAAG,OAAO,CAYtG;AAED,MAAM,WAAW,qBAAqB;IACpC,8CAA8C;IAC9C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,wDAAwD;IACxD,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,4EAA4E;IAC5E,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;CAC7D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB;IAkC5D,oDAAoD;iCA/B/B,OAAO,KAAG,OAAO;IAiCtC,kDAAkD;2BA1BhC,OAAO,KAAG,OAAO;IA4BnC,oCAAoC;8BACV,OAAO,KAAG,OAAO;EAE9C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,MAAM,GAAG,CAAC,CAgB1G"}
@@ -0,0 +1,124 @@
1
+ // validation.ts — the block-tree checks that are the same wiki to wiki.
2
+ //
3
+ // Both wikis validate a block tree before it reaches the database, and both had
4
+ // written the same walk: an id-and-type gate, a container branch for
5
+ // columns/infobox, and a switch over leaf types. Only the switch is a project's
6
+ // own — its type set is. So the walk and the two fiddly nested validators live
7
+ // here, and the switch is passed in.
8
+ //
9
+ // As elsewhere in this package, the dispatch stays with the caller so a new
10
+ // block type is a compile error there until it is handled.
11
+ /**
12
+ * An href acceptable to persist: http, https, mailto, or a relative path or
13
+ * fragment. Everything else — `javascript:`, `data:`, `vbscript:`, a
14
+ * protocol-relative `//host` — comes back null.
15
+ *
16
+ * React 19 already neutralises `javascript:` in an href at render time, so this
17
+ * is defence in depth rather than the only thing between an editor and a
18
+ * reader. Its value is at the write path: a URL that can never render safely is
19
+ * better rejected than stored.
20
+ */
21
+ export function safeLinkHref(raw) {
22
+ if (!raw)
23
+ return null;
24
+ // Strip C0 controls (including tab, newline and CR) and DEL before reading
25
+ // the scheme, so a newline cannot smuggle one past the match below.
26
+ const href = raw.replace(/[\u0000-\u0020\u007f]/g, '').trim();
27
+ if (!href)
28
+ return null;
29
+ if (href.startsWith('//'))
30
+ return null; // protocol-relative -> external host
31
+ const scheme = href.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
32
+ if (scheme) {
33
+ const s = scheme[1].toLowerCase();
34
+ if (s !== 'http' && s !== 'https' && s !== 'mailto')
35
+ return null;
36
+ }
37
+ return href;
38
+ }
39
+ /** An author-supplied URL is fine iff it is empty (unset) or resolves safely. */
40
+ export const okUrl = (u) => typeof u === 'string' && (u === '' || safeLinkHref(u) !== null);
41
+ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
42
+ /** `[{ id, text, url? }]` — the shape a references block stores. */
43
+ export function validateReferenceItems(items, urlCheck = okUrl) {
44
+ return (Array.isArray(items) &&
45
+ items.every(it => isRecord(it) &&
46
+ typeof it.id === 'string' &&
47
+ typeof it.text === 'string' &&
48
+ (it.url === undefined || urlCheck(it.url))));
49
+ }
50
+ /** `[{ id, heading, links: [{ label, href }] }]` — a link-grid block's groups. */
51
+ export function validateLinkGroups(groups, urlCheck = okUrl) {
52
+ return (Array.isArray(groups) &&
53
+ groups.every(g => isRecord(g) &&
54
+ typeof g.id === 'string' &&
55
+ typeof g.heading === 'string' &&
56
+ Array.isArray(g.links) &&
57
+ g.links.every(l => isRecord(l) && typeof l.label === 'string' && urlCheck(l.href))));
58
+ }
59
+ /**
60
+ * The block-tree walk, with one wiki's leaf switch plugged into it. The caller
61
+ * keeps its own type parameter, so this stays free of any repo's types while
62
+ * the call site still gets a real type guard.
63
+ */
64
+ export function createBlockValidator(opts) {
65
+ const { isKnownType, isAtomicType, validateAtomic } = opts;
66
+ const atomic = (block) => {
67
+ if (!isRecord(block))
68
+ return false;
69
+ if (typeof block.id !== 'string')
70
+ return false;
71
+ if (typeof block.type !== 'string' || !isKnownType(block.type) || !isAtomicType(block.type))
72
+ return false;
73
+ return validateAtomic(block);
74
+ };
75
+ const one = (block) => {
76
+ if (!isRecord(block))
77
+ return false;
78
+ if (typeof block.id !== 'string')
79
+ return false;
80
+ if (typeof block.type !== 'string' || !isKnownType(block.type))
81
+ return false;
82
+ if (block.type === 'columns') {
83
+ return (Array.isArray(block.columns) &&
84
+ block.columns.every(col => isRecord(col) &&
85
+ typeof col.id === 'string' &&
86
+ Array.isArray(col.blocks) &&
87
+ col.blocks.every(atomic)));
88
+ }
89
+ if (block.type === 'infobox') {
90
+ return Array.isArray(block.blocks) && block.blocks.every(atomic);
91
+ }
92
+ return atomic(block);
93
+ };
94
+ return {
95
+ /** One leaf block; container types are rejected. */
96
+ validateAtomicBlock: atomic,
97
+ /** One block of any kind, containers included. */
98
+ validateBlock: one,
99
+ /** A whole page's content array. */
100
+ validateBlocks: (content) => Array.isArray(content) && content.every(one),
101
+ };
102
+ }
103
+ /**
104
+ * A copy of a block with a fresh id at every level, so a duplicated container
105
+ * does not share child ids with its original.
106
+ */
107
+ export function duplicateBlockIds(block, newId) {
108
+ const b = block;
109
+ if (block.type === 'columns' && Array.isArray(b.columns)) {
110
+ return {
111
+ ...block,
112
+ id: newId(),
113
+ columns: b.columns.map(col => {
114
+ const c = col;
115
+ return { ...c, id: newId(), blocks: (c.blocks ?? []).map(x => ({ ...x, id: newId() })) };
116
+ }),
117
+ };
118
+ }
119
+ if (block.type === 'infobox' && Array.isArray(b.blocks)) {
120
+ return { ...block, id: newId(), blocks: b.blocks.map(x => ({ ...x, id: newId() })) };
121
+ }
122
+ return { ...block, id: newId() };
123
+ }
124
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ export interface RegistryAuthRecord {
2
+ body: string;
3
+ contentType: string;
4
+ cacheControl: string;
5
+ }
6
+ /**
7
+ * The `v=MCPv1` record the official MCP registry fetches from
8
+ * `/.well-known/mcp-registry-auth` to prove domain ownership. `mcp-publisher
9
+ * login http --domain=<host>` checks the key here against the private key
10
+ * signing the login, which is what grants publish rights over the reversed-
11
+ * domain namespace the server's name sits in.
12
+ *
13
+ * Returns null when no key is configured, so the caller can 404 rather than
14
+ * serve a malformed record — a missing key should read as "not configured",
15
+ * not as a verification failure nobody can explain.
16
+ *
17
+ * The key material belongs in an env var rather than the repo: the public half
18
+ * is harmless to serve but pointless to commit, and keeping it out gives the
19
+ * private half an obvious home too (a keychain, never git).
20
+ *
21
+ * openssl genpkey -algorithm Ed25519 -out key.pem
22
+ * openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
23
+ */
24
+ export declare function registryAuthRecord(publicKey?: string, keyType?: string): RegistryAuthRecord | null;
25
+ /** A day at the edge, a week stale-while-revalidate. A card changes rarely. */
26
+ export declare const AGENT_CARD_CACHE_CONTROL = "public, s-maxage=86400, stale-while-revalidate=604800";
27
+ export interface AgentSkill {
28
+ id: string;
29
+ name: string;
30
+ description: string;
31
+ tags: string[];
32
+ examples?: string[];
33
+ authentication?: unknown;
34
+ }
35
+ /** The shape a tool needs to carry to become a skill. */
36
+ export interface SkillSource {
37
+ name: string;
38
+ description: string;
39
+ skill?: {
40
+ id: string;
41
+ tags: string[];
42
+ examples?: string[];
43
+ } | undefined;
44
+ auth?: unknown;
45
+ }
46
+ /**
47
+ * Skills derived from the MCP tool manifest, so a card can never advertise a
48
+ * capability the server does not have. `search_pages` becomes "Search Pages".
49
+ */
50
+ export declare function skillsFromTools(tools: readonly SkillSource[]): AgentSkill[];
51
+ export interface AgentCardLicense {
52
+ name: string;
53
+ url: string;
54
+ spdx?: string;
55
+ /** What the licence covers, e.g. `'content'`. */
56
+ scope?: string;
57
+ }
58
+ export interface AgentCardConfig {
59
+ name: string;
60
+ description: string;
61
+ /** Origin, no trailing slash. Every derived URL below hangs off it. */
62
+ url: string;
63
+ version: string;
64
+ skills: AgentSkill[];
65
+ license?: AgentCardLicense;
66
+ /** Defaults to `name`. */
67
+ organization?: string;
68
+ /** Defaults to `${url}/llms.txt`. */
69
+ documentationUrl?: string;
70
+ /** Defaults to `${url}/api/mcp`. Pass null for an origin with no MCP server. */
71
+ mcpEndpoint?: string | null;
72
+ /** Anything this origin advertises that the others do not. */
73
+ extra?: Record<string, unknown>;
74
+ }
75
+ /**
76
+ * An A2A Agent Card. Serve the same object at both `/.well-known/agent.json`
77
+ * and `/.well-known/agent-card.json`: v0.3 renamed the path and defined no
78
+ * fallback in either direction, so a spec-current client probes only the new
79
+ * one and a client on an older SDK probes only the old one. Serving one path
80
+ * means half the callers conclude the origin has no agent at all.
81
+ */
82
+ export declare function agentCard(config: AgentCardConfig): Record<string, unknown>;
83
+ //# sourceMappingURL=well-known.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known.d.ts","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAQlG;AAID,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,0DAA0D,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,UAAU,EAAE,CAW3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiB1E"}
@@ -0,0 +1,84 @@
1
+ // well-known.ts — the descriptors an agent finds before it knows anything else
2
+ // about an origin: the A2A Agent Card and the MCP registry's domain proof.
3
+ //
4
+ // All three apps here serve both, and all three had written them out. The
5
+ // registry-auth handler was identical bar a domain named in a comment; the
6
+ // agent cards shared an envelope, a skills mapping and a cache header, and
7
+ // differed only in the fields a card is *supposed* to differ in.
8
+ //
9
+ // Nothing here touches a framework: these return plain values, and the caller
10
+ // wraps them in whatever its router wants. That keeps this package free of a
11
+ // Next dependency and keeps each app's route a two-liner.
12
+ /**
13
+ * The `v=MCPv1` record the official MCP registry fetches from
14
+ * `/.well-known/mcp-registry-auth` to prove domain ownership. `mcp-publisher
15
+ * login http --domain=<host>` checks the key here against the private key
16
+ * signing the login, which is what grants publish rights over the reversed-
17
+ * domain namespace the server's name sits in.
18
+ *
19
+ * Returns null when no key is configured, so the caller can 404 rather than
20
+ * serve a malformed record — a missing key should read as "not configured",
21
+ * not as a verification failure nobody can explain.
22
+ *
23
+ * The key material belongs in an env var rather than the repo: the public half
24
+ * is harmless to serve but pointless to commit, and keeping it out gives the
25
+ * private half an obvious home too (a keychain, never git).
26
+ *
27
+ * openssl genpkey -algorithm Ed25519 -out key.pem
28
+ * openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
29
+ */
30
+ export function registryAuthRecord(publicKey, keyType) {
31
+ if (!publicKey)
32
+ return null;
33
+ return {
34
+ // ed25519 unless a P-384 key was used instead (the LibreSSL-friendly path).
35
+ body: `v=MCPv1; k=${keyType || 'ed25519'}; p=${publicKey}\n`,
36
+ contentType: 'text/plain; charset=utf-8',
37
+ cacheControl: 'no-store',
38
+ };
39
+ }
40
+ // ---- A2A Agent Card ---------------------------------------------------------
41
+ /** A day at the edge, a week stale-while-revalidate. A card changes rarely. */
42
+ export const AGENT_CARD_CACHE_CONTROL = 'public, s-maxage=86400, stale-while-revalidate=604800';
43
+ /**
44
+ * Skills derived from the MCP tool manifest, so a card can never advertise a
45
+ * capability the server does not have. `search_pages` becomes "Search Pages".
46
+ */
47
+ export function skillsFromTools(tools) {
48
+ return tools
49
+ .filter(t => t.skill)
50
+ .map(t => ({
51
+ id: t.skill.id,
52
+ name: t.name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
53
+ description: t.description,
54
+ tags: t.skill.tags,
55
+ ...(t.skill.examples ? { examples: t.skill.examples } : {}),
56
+ ...(t.auth ? { authentication: t.auth } : {}),
57
+ }));
58
+ }
59
+ /**
60
+ * An A2A Agent Card. Serve the same object at both `/.well-known/agent.json`
61
+ * and `/.well-known/agent-card.json`: v0.3 renamed the path and defined no
62
+ * fallback in either direction, so a spec-current client probes only the new
63
+ * one and a client on an older SDK probes only the old one. Serving one path
64
+ * means half the callers conclude the origin has no agent at all.
65
+ */
66
+ export function agentCard(config) {
67
+ const { name, description, url, version, skills, license, organization, documentationUrl, mcpEndpoint, extra } = config;
68
+ return {
69
+ name,
70
+ description,
71
+ url,
72
+ version,
73
+ capabilities: { streaming: false, pushNotifications: false },
74
+ skills,
75
+ provider: { organization: organization ?? name, url },
76
+ documentationUrl: documentationUrl ?? `${url}/llms.txt`,
77
+ ...(mcpEndpoint === null ? {} : { mcpEndpoint: mcpEndpoint ?? `${url}/api/mcp` }),
78
+ ...(license ? { license } : {}),
79
+ ...extra,
80
+ defaultInputModes: ['text/plain', 'application/json'],
81
+ defaultOutputModes: ['text/plain', 'application/json', 'text/markdown'],
82
+ };
83
+ }
84
+ //# sourceMappingURL=well-known.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known.js","sourceRoot":"","sources":["../src/well-known.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,0DAA0D;AAU1D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAkB,EAAE,OAAgB;IACrE,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO;QACL,4EAA4E;QAC5E,IAAI,EAAE,cAAc,OAAO,IAAI,SAAS,OAAO,SAAS,IAAI;QAC5D,WAAW,EAAE,2BAA2B;QACxC,YAAY,EAAE,UAAU;KACzB,CAAC;AACJ,CAAC;AAED,gFAAgF;AAEhF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,uDAAuD,CAAC;AAmBhG;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAA6B;IAC3D,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;SACpB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACT,EAAE,EAAE,CAAC,CAAC,KAAM,CAAC,EAAE;QACf,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACtE,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,IAAI,EAAE,CAAC,CAAC,KAAM,CAAC,IAAI;QACnB,GAAG,CAAC,CAAC,CAAC,KAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC,CAAC,CAAC;AACR,CAAC;AA4BD;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IACxH,OAAO;QACL,IAAI;QACJ,WAAW;QACX,GAAG;QACH,OAAO;QACP,YAAY,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE;QAC5D,MAAM;QACN,QAAQ,EAAE,EAAE,YAAY,EAAE,YAAY,IAAI,IAAI,EAAE,GAAG,EAAE;QACrD,gBAAgB,EAAE,gBAAgB,IAAI,GAAG,GAAG,WAAW;QACvD,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,IAAI,GAAG,GAAG,UAAU,EAAE,CAAC;QACjF,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,KAAK;QACR,iBAAiB,EAAE,CAAC,YAAY,EAAE,kBAAkB,CAAC;QACrD,kBAAkB,EAAE,CAAC,YAAY,EAAE,kBAAkB,EAAE,eAAe,CAAC;KACxE,CAAC;AACJ,CAAC"}