wiki-formant 0.86.1 → 0.86.3
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 +29 -39
- package/bin/_cli.mjs +1 -10
- package/dist/base.css +157 -144
- package/dist/blocks.d.ts +5 -2
- package/dist/headers.d.ts +5 -9
- package/dist/headers.js +12 -15
- package/dist/knip.d.ts +3 -3
- package/dist/knip.js +29 -28
- package/dist/link-check.d.ts +27 -26
- package/dist/link-check.js +44 -38
- package/dist/passkey.d.ts +1 -1
- package/dist/passkey.js +12 -8
- package/dist/scripts.d.ts +55 -148
- package/dist/scripts.js +81 -75
- package/dist/validation.d.ts +23 -30
- package/dist/validation.js +23 -17
- package/dist/well-known.d.ts +18 -15
- package/dist/well-known.js +20 -18
- package/package.json +1 -1
package/dist/scripts.js
CHANGED
|
@@ -1,138 +1,144 @@
|
|
|
1
|
-
// scripts.ts — what a wiki's
|
|
2
|
-
//
|
|
1
|
+
// scripts.ts — what a wiki's seed and sweep scripts share: a client on the
|
|
2
|
+
// app's database that rolls back on a throw and expires the wiki's cache tag
|
|
3
|
+
// after a write, the store over it, the block builders, and the two writes
|
|
4
|
+
// every script makes: a revision over a page it read, and an upsert.
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// this half, and each had a piece the other lacked: the shape check, the
|
|
8
|
-
// write-tracking pool and the rollback on one side, the upsert that records a
|
|
9
|
-
// revision only when something changed on the other.
|
|
10
|
-
import { execFileSync } from 'node:child_process';
|
|
6
|
+
// radix-wiki's `seed-utils.mjs` and caper's `wiki-utils.ts` had each written
|
|
7
|
+
// these, in different languages, with different halves of the same guards.
|
|
8
|
+
// Node-only, like the bins: `pg` is the app's, imported when a client is made.
|
|
11
9
|
import { randomUUID } from 'node:crypto';
|
|
12
|
-
import {
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
13
11
|
import { wikiStore } from './store.js';
|
|
12
|
+
import { bump } from './versioning.js';
|
|
14
13
|
import { validateLinkGroups, validateReferenceItems } from './validation.js';
|
|
15
|
-
// ---- block builders ---------------------------------------------------------
|
|
16
14
|
export const uid = () => randomUUID();
|
|
17
|
-
export const content = (
|
|
18
|
-
/**
|
|
19
|
-
* A notice. Always pass `text`: the variant's standard message lives in the
|
|
20
|
-
* renderer, not the row, so a banner without it is invisible to search.
|
|
21
|
-
*/
|
|
15
|
+
export const content = (text) => ({ id: uid(), type: 'content', text });
|
|
16
|
+
/** Always pass `text`: the renderer's default message lives in React, and search indexes only what the row holds. */
|
|
22
17
|
export const banner = (variant, text) => ({ id: uid(), type: 'banner', variant, text });
|
|
23
|
-
/** A facts panel
|
|
18
|
+
/** A facts panel that leads an article: `table` is the inner `<table>` HTML. */
|
|
24
19
|
export const infobox = (table) => ({ id: uid(), type: 'infobox', blocks: [content(table)] });
|
|
25
|
-
// ---- the shape check --------------------------------------------------------
|
|
26
20
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* nothing reports it. A throw inside `withClient` is a rollback and a failed run.
|
|
21
|
+
* Refuse block content whose URL-bearing leaves are mis-shaped, before it
|
|
22
|
+
* reaches the database. `LinkGridView` and `ReferencesView` drop an entry whose
|
|
23
|
+
* URL fails `safeLinkHref` rather than rendering it inert, so a one-field slip
|
|
24
|
+
* (`url` for a link, whose shape is `{ label, href }`) shows as a heading with
|
|
25
|
+
* no pills and nothing reports it. Throws, so `withClient` rolls back.
|
|
33
26
|
*/
|
|
34
27
|
export function assertLinkShapes(blocks, where = 'content') {
|
|
35
28
|
const bad = [];
|
|
36
29
|
const leaf = (b, path) => {
|
|
37
|
-
|
|
30
|
+
const block = b;
|
|
31
|
+
if (!block || typeof block !== 'object')
|
|
38
32
|
return;
|
|
39
|
-
if (
|
|
40
|
-
for (const [gi, g] of (
|
|
41
|
-
for (const [li, l] of (g?.links ?? []).entries())
|
|
42
|
-
if (!validateLinkGroups([{ id: '', heading: '', links: [l] }]))
|
|
33
|
+
if (block.type === 'linkGrid') {
|
|
34
|
+
for (const [gi, g] of (block.groups ?? []).entries()) {
|
|
35
|
+
for (const [li, l] of (g?.links ?? []).entries()) {
|
|
36
|
+
if (!validateLinkGroups([{ id: '', heading: '', links: [l] }])) {
|
|
43
37
|
bad.push(`${path}.groups[${gi}].links[${li}] "${l?.label ?? '(no label)'}" has fields {${Object.keys(l ?? {}).sort().join(', ')}} -- LinkGridLink is { label, href }`);
|
|
44
|
-
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (block.type === 'references' && !validateReferenceItems(block.items))
|
|
45
43
|
bad.push(`${path}.items -- ReferenceItem is { id, text, url? }`);
|
|
46
44
|
};
|
|
47
|
-
|
|
45
|
+
blocks.forEach((b, i) => {
|
|
48
46
|
leaf(b, `[${i}]`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const block = b;
|
|
48
|
+
if (block?.type === 'infobox')
|
|
49
|
+
(block.blocks ?? []).forEach((n, j) => leaf(n, `[${i}].blocks[${j}]`));
|
|
50
|
+
if (block?.type === 'columns') {
|
|
51
|
+
(block.columns ?? []).forEach((c, ci) => (c?.blocks ?? []).forEach((n, j) => leaf(n, `[${i}].columns[${ci}].blocks[${j}]`)));
|
|
52
|
+
}
|
|
53
53
|
});
|
|
54
54
|
if (bad.length)
|
|
55
55
|
throw new Error(`${where}: ${bad.length} mis-shaped link(s)\n ${bad.join('\n ')}`);
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* Mark everything cached under `tag` stale on the project `cwd` is linked to.
|
|
60
|
-
* A script's write reaches no route that would bust it, and pages hold a day.
|
|
61
|
-
*/
|
|
62
|
-
export function invalidate(tag, cwd = process.cwd()) {
|
|
57
|
+
/** `vercel cache invalidate --tag`, reported either way. */
|
|
58
|
+
export function invalidate(tag, cwd) {
|
|
63
59
|
try {
|
|
64
|
-
execFileSync('vercel', ['cache', 'invalidate', '--tag', tag, '--yes'], {
|
|
60
|
+
execFileSync('vercel', ['cache', 'invalidate', '--tag', tag, '--yes'], { stdio: 'ignore', ...(cwd ? { cwd } : {}) });
|
|
65
61
|
console.log(`cache: ${tag} tag invalidated`);
|
|
66
62
|
}
|
|
67
63
|
catch (e) {
|
|
68
|
-
console.warn(`cache: ${tag} tag NOT invalidated (${e.message}); run \`vercel cache invalidate --tag ${tag}
|
|
64
|
+
console.warn(`cache: ${tag} tag NOT invalidated (${e.message}); run \`vercel cache invalidate --tag ${tag}\``);
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
67
|
/**
|
|
72
|
-
* Run `fn`
|
|
73
|
-
* back
|
|
74
|
-
*
|
|
75
|
-
*
|
|
68
|
+
* Run `fn` against a connected client, then release the client and the pool.
|
|
69
|
+
* A throw rolls back whatever transaction was open and fails the process:
|
|
70
|
+
* every one of these scripts writes to the live database, so a half-applied
|
|
71
|
+
* edit must never be left behind and never look like a success. A run that
|
|
72
|
+
* wrote anything ends by expiring the wiki's cache tag, because a script
|
|
73
|
+
* bypasses the routes that purge the pages they change.
|
|
76
74
|
*/
|
|
77
|
-
export async function withClient(fn, { tag = 'wiki', cwd
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
export async function withClient(fn, { tag = 'wiki', cwd, url } = {}) {
|
|
76
|
+
if (!url && !process.env.DATABASE_URL) {
|
|
77
|
+
try {
|
|
78
|
+
process.loadEnvFile('.env');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// reported below
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const connectionString = url ?? process.env.DATABASE_URL;
|
|
85
|
+
if (!connectionString)
|
|
86
|
+
throw new Error('no DATABASE_URL in the environment or ./.env');
|
|
87
|
+
const { default: pg } = (await import('pg'));
|
|
88
|
+
const pool = new pg.Pool({ connectionString, max: 1, ssl: { rejectUnauthorized: false } });
|
|
89
|
+
const client = await pool.connect();
|
|
81
90
|
let wrote = false;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return conn.query(text, values);
|
|
88
|
-
},
|
|
91
|
+
const query = client.query.bind(client);
|
|
92
|
+
client.query = (text, values) => {
|
|
93
|
+
if (/\b(insert\s+into|update\s+\S+\s+set|delete\s+from)\b/i.test(text))
|
|
94
|
+
wrote = true;
|
|
95
|
+
return query(text, values);
|
|
89
96
|
};
|
|
90
97
|
try {
|
|
91
98
|
return await fn(client);
|
|
92
99
|
}
|
|
93
100
|
catch (e) {
|
|
94
|
-
await
|
|
101
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
95
102
|
console.error('ERROR:', e.message);
|
|
96
103
|
process.exitCode = 1;
|
|
97
104
|
return undefined;
|
|
98
105
|
}
|
|
99
106
|
finally {
|
|
100
|
-
|
|
107
|
+
client.release();
|
|
101
108
|
await pool.end();
|
|
102
|
-
// A run that throws part-way may have committed earlier writes.
|
|
103
109
|
if (wrote)
|
|
104
110
|
invalidate(tag, cwd);
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
/** The app's own store over a script's client, so a script writes what the editor writes. */
|
|
113
|
+
/** The app's own store over a script's client, so a script writes exactly what the editor writes. */
|
|
109
114
|
export const storeFor = (client, siteId = '') => wikiStore({ sql: async (q, ...v) => (await client.query(q, v)).rows, siteId });
|
|
110
115
|
/** A page row by path, every column: what `writeRevision` takes. Null when there is none. */
|
|
111
|
-
export const readPage = (client, tagPath, slug, siteId
|
|
116
|
+
export const readPage = (client, tagPath, slug, siteId) => storeFor(client, siteId).get(tagPath, slug);
|
|
112
117
|
/**
|
|
113
|
-
* Write edited blocks back to a page
|
|
114
|
-
*
|
|
115
|
-
*
|
|
118
|
+
* Write edited blocks back to a page from `readPage` as one revision, through
|
|
119
|
+
* the store: the page and its revision in one statement, the version bumped
|
|
120
|
+
* from the stored one by `change`. The save names the version it read, so an
|
|
121
|
+
* edit landing in between is refused (409) instead of overwritten. Returns the
|
|
122
|
+
* new version.
|
|
116
123
|
*/
|
|
117
|
-
export async function writeRevision(client, page, blocks, { change = 'minor', message, verified = false, dry = false, metadata, authorId = null } = {}) {
|
|
124
|
+
export async function writeRevision(client, page, blocks, { change = 'minor', message, verified = false, dry = false, metadata, authorId = null, siteId } = {}) {
|
|
118
125
|
if (dry)
|
|
119
126
|
return bump(page.version, change);
|
|
120
127
|
assertLinkShapes(blocks, page.title);
|
|
121
|
-
const { page: saved } = await storeFor(client,
|
|
128
|
+
const { page: saved } = await storeFor(client, siteId).save({
|
|
122
129
|
tagPath: page.tagPath, slug: page.slug, content: blocks, metadata,
|
|
123
130
|
change, message, verified, authorId, baseVersion: page.version,
|
|
124
131
|
});
|
|
125
132
|
return saved.version;
|
|
126
133
|
}
|
|
127
134
|
/**
|
|
128
|
-
* Create a page or
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* always writes one.
|
|
135
|
+
* Create a page, or replace its title and blocks: a change writes a revision
|
|
136
|
+
* bumped from the page's own version, a re-run that changes nothing writes
|
|
137
|
+
* none, and a stated `change` always records one.
|
|
132
138
|
*/
|
|
133
139
|
export async function upsertWikiPage(client, input) {
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
assertLinkShapes(input.content, `${input.tagPath}/${input.slug}`);
|
|
141
|
+
const { siteId, ...save } = input;
|
|
136
142
|
const { created } = await storeFor(client, siteId).save(save);
|
|
137
143
|
return created ? 'created' : 'updated';
|
|
138
144
|
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type BlockTreeShape } from './blocks.js';
|
|
2
1
|
/**
|
|
3
2
|
* An href acceptable to persist: http, https, mailto, tel, or a relative path or
|
|
4
3
|
* fragment. Everything else — `javascript:`, `data:`, `vbscript:`, a
|
|
@@ -87,45 +86,39 @@ export declare function duplicateBlockIds<B extends {
|
|
|
87
86
|
type: string;
|
|
88
87
|
id: string;
|
|
89
88
|
}>(block: B, newId?: () => string): B;
|
|
90
|
-
|
|
89
|
+
/** A leaf check over the stored record; `core` is the package's check for the core types. */
|
|
90
|
+
export type LeafCheck = (block: Record<string, unknown>, core: (block: Record<string, unknown>) => boolean) => boolean;
|
|
91
91
|
export interface BlockModelOptions<B extends {
|
|
92
92
|
id: string;
|
|
93
93
|
type: string;
|
|
94
94
|
}> {
|
|
95
|
-
/**
|
|
95
|
+
/** A new block of every type, without its id: `{ ...coreBlockDefaults(), daoTimeline: () => … }`. */
|
|
96
96
|
defaults: Record<B['type'], () => Omit<B, 'id'>>;
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* replacing it. A type with no entry gets the core check.
|
|
101
|
-
*/
|
|
102
|
-
checks?: Partial<Record<B['type'], (block: Checked, core: (block: Checked) => boolean) => boolean>>;
|
|
103
|
-
/** As `coreAtomicValidator`'s. */
|
|
97
|
+
/** The wiki's own leaf checks, and any core check it narrows (`list: (b, core) => core(b) && isSection(b.under)`). */
|
|
98
|
+
checks?: Partial<Record<B['type'], LeafCheck>>;
|
|
99
|
+
/** What `coreAtomicValidator` takes. */
|
|
104
100
|
pageId?: (id: string) => boolean;
|
|
105
101
|
maxRows?: number;
|
|
106
102
|
}
|
|
107
|
-
export interface BlockModel<B extends {
|
|
108
|
-
id: string;
|
|
109
|
-
type: string;
|
|
110
|
-
}> {
|
|
111
|
-
/** Every type, in the order `defaults` lists them. */
|
|
112
|
-
types: B['type'][];
|
|
113
|
-
/** Every type but the two containers: what may nest inside one. */
|
|
114
|
-
atomicTypes: B['type'][];
|
|
115
|
-
shape: BlockTreeShape<B>;
|
|
116
|
-
createBlock: (type: B['type']) => B;
|
|
117
|
-
duplicateBlock: (block: B) => B;
|
|
118
|
-
validateBlocks: (content: unknown) => content is B[];
|
|
119
|
-
blockIssues: (content: unknown) => BlockIssue[];
|
|
120
|
-
}
|
|
121
103
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
104
|
+
* Everything a wiki derives from its block set: the shape every tree walk
|
|
105
|
+
* takes, the types, a new block, a copy with fresh ids, and the validator
|
|
106
|
+
* with its issues. Three wikis had assembled these from the same seven
|
|
107
|
+
* primitives, each a hundred lines; a fourth kind of block now costs a default
|
|
108
|
+
* and a check.
|
|
126
109
|
*/
|
|
127
110
|
export declare function defineBlocks<B extends {
|
|
128
111
|
id: string;
|
|
129
112
|
type: string;
|
|
130
|
-
}>(opts: BlockModelOptions<B>):
|
|
131
|
-
|
|
113
|
+
}>(opts: BlockModelOptions<B>): {
|
|
114
|
+
shape: import("./blocks.js").BlockTreeShape<B>;
|
|
115
|
+
types: B["type"][];
|
|
116
|
+
atomicTypes: B["type"][];
|
|
117
|
+
isAtomic: (type: string) => boolean;
|
|
118
|
+
createBlock: (type: B["type"]) => B;
|
|
119
|
+
duplicateBlock: (block: B) => B;
|
|
120
|
+
validateBlocks: (content: unknown) => content is B[];
|
|
121
|
+
validateBlock: (block: unknown) => boolean;
|
|
122
|
+
validateAtomicBlock: (block: unknown) => boolean;
|
|
123
|
+
blockIssues: (content: unknown) => BlockIssue[];
|
|
124
|
+
};
|
package/dist/validation.js
CHANGED
|
@@ -242,29 +242,35 @@ export function duplicateBlockIds(block, newId = () => crypto.randomUUID()) {
|
|
|
242
242
|
return { ...block, id: newId() };
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
245
|
+
* Everything a wiki derives from its block set: the shape every tree walk
|
|
246
|
+
* takes, the types, a new block, a copy with fresh ids, and the validator
|
|
247
|
+
* with its issues. Three wikis had assembled these from the same seven
|
|
248
|
+
* primitives, each a hundred lines; a fourth kind of block now costs a default
|
|
249
|
+
* and a check.
|
|
249
250
|
*/
|
|
250
251
|
export function defineBlocks(opts) {
|
|
251
252
|
const { defaults, checks = {}, pageId, maxRows } = opts;
|
|
252
253
|
const types = Object.keys(defaults);
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
const core = coreAtomicValidator({ ...(pageId ? { pageId } : {}), ...(maxRows ? { maxRows } : {}) });
|
|
255
|
+
const isAtomic = (type) => type !== 'columns' && type !== 'infobox';
|
|
256
|
+
const validator = createBlockValidator({
|
|
257
|
+
isKnownType: t => t in defaults,
|
|
258
|
+
isAtomicType: isAtomic,
|
|
259
|
+
validateAtomic: b => {
|
|
260
|
+
const own = checks[b.type];
|
|
261
|
+
return own ? own(b, core) : core(b);
|
|
262
|
+
},
|
|
260
263
|
});
|
|
261
264
|
return {
|
|
262
|
-
types,
|
|
263
|
-
atomicTypes,
|
|
264
265
|
shape: coreBlockShape(),
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
types,
|
|
267
|
+
atomicTypes: types.filter(isAtomic),
|
|
268
|
+
isAtomic,
|
|
269
|
+
createBlock: (type) => ({ id: crypto.randomUUID(), ...defaults[type]() }),
|
|
270
|
+
duplicateBlock: (block) => duplicateBlockIds(block),
|
|
271
|
+
validateBlocks: (content) => validator.validateBlocks(content),
|
|
272
|
+
validateBlock: validator.validateBlock,
|
|
273
|
+
validateAtomicBlock: validator.validateAtomicBlock,
|
|
274
|
+
blockIssues: validator.blockIssues,
|
|
269
275
|
};
|
|
270
276
|
}
|
package/dist/well-known.d.ts
CHANGED
|
@@ -150,29 +150,32 @@ export declare function descriptorHandler(body: unknown, opts?: {
|
|
|
150
150
|
/** The whole `<mcp-url>/server-card` route, projected from server.json. */
|
|
151
151
|
export declare const serverCardHandler: (manifest: ServerManifest) => ((request: Request) => Response);
|
|
152
152
|
export interface WellKnownDocs {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
/** Served at both `agent.json` and `agent-card.json`. */
|
|
154
|
+
agentCard?: unknown;
|
|
155
|
+
/** `mcp.json`. */
|
|
156
|
+
mcpManifest?: unknown;
|
|
157
|
+
/** `openapi.json`. */
|
|
158
|
+
openapi?: unknown;
|
|
159
|
+
/** The registry proof, `mcp-registry-auth`: a key, or a handler; defaults to the environment's key. Pass null to serve none. */
|
|
160
160
|
registryAuth?: {
|
|
161
161
|
publicKey?: string;
|
|
162
162
|
keyType?: string;
|
|
163
|
-
} | null;
|
|
163
|
+
} | (() => Response) | null;
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
|
-
* The `/.well-known
|
|
167
|
-
* `
|
|
168
|
-
* MCP manifest at `mcp.json`, the OpenAPI spec at `openapi.json` and the
|
|
169
|
-
* registry's domain proof at `mcp-registry-auth`. Every other name is a 404.
|
|
166
|
+
* The whole `/.well-known/` directory as one dynamic route,
|
|
167
|
+
* `src/app/.well-known/[doc]/route.ts`:
|
|
170
168
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
169
|
+
* export const GET = wellKnownHandler({ agentCard: CARD, mcpManifest: MANIFEST, openapi: SPEC });
|
|
170
|
+
*
|
|
171
|
+
* Four repos carried five one-line route files each, twenty in all, and one
|
|
172
|
+
* of them answered a path the others 404'd. A document the app does not pass
|
|
173
|
+
* is a 404 here too, so an origin without an MCP server still serves its card.
|
|
173
174
|
*/
|
|
174
175
|
export declare function wellKnownHandler(docs: WellKnownDocs): (request: Request, context: {
|
|
175
176
|
params: Promise<{
|
|
176
177
|
doc: string;
|
|
177
|
-
}
|
|
178
|
+
}> | {
|
|
179
|
+
doc: string;
|
|
180
|
+
};
|
|
178
181
|
}) => Promise<Response>;
|
package/dist/well-known.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// well-known.ts — the descriptors an agent finds before it knows anything else
|
|
2
|
-
// about an origin: the A2A Agent Card
|
|
3
|
-
// domain proof, and the one route that serves them.
|
|
2
|
+
// about an origin: the A2A Agent Card and the MCP registry's domain proof.
|
|
4
3
|
//
|
|
5
4
|
// All three apps here serve both, and all three had written them out. The
|
|
6
5
|
// registry-auth handler was identical bar a domain named in a comment; the
|
|
@@ -220,27 +219,30 @@ export function descriptorHandler(body, opts = {}) {
|
|
|
220
219
|
/** The whole `<mcp-url>/server-card` route, projected from server.json. */
|
|
221
220
|
export const serverCardHandler = (manifest) => descriptorHandler(serverCard(manifest, MCP_PROTOCOL_VERSIONS));
|
|
222
221
|
/**
|
|
223
|
-
* The `/.well-known
|
|
224
|
-
* `
|
|
225
|
-
* MCP manifest at `mcp.json`, the OpenAPI spec at `openapi.json` and the
|
|
226
|
-
* registry's domain proof at `mcp-registry-auth`. Every other name is a 404.
|
|
222
|
+
* The whole `/.well-known/` directory as one dynamic route,
|
|
223
|
+
* `src/app/.well-known/[doc]/route.ts`:
|
|
227
224
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
225
|
+
* export const GET = wellKnownHandler({ agentCard: CARD, mcpManifest: MANIFEST, openapi: SPEC });
|
|
226
|
+
*
|
|
227
|
+
* Four repos carried five one-line route files each, twenty in all, and one
|
|
228
|
+
* of them answered a path the others 404'd. A document the app does not pass
|
|
229
|
+
* is a 404 here too, so an origin without an MCP server still serves its card.
|
|
230
230
|
*/
|
|
231
231
|
export function wellKnownHandler(docs) {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
'
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
'
|
|
232
|
+
const auth = docs.registryAuth;
|
|
233
|
+
const registry = auth === null ? null
|
|
234
|
+
: typeof auth === 'function' ? auth
|
|
235
|
+
: registryAuthHandler(auth?.publicKey ?? process.env.MCP_REGISTRY_PUBLIC_KEY, auth?.keyType ?? process.env.MCP_REGISTRY_KEY_TYPE);
|
|
236
|
+
const served = {
|
|
237
|
+
'agent.json': docs.agentCard === undefined ? undefined : descriptorHandler(docs.agentCard),
|
|
238
|
+
'agent-card.json': docs.agentCard === undefined ? undefined : descriptorHandler(docs.agentCard),
|
|
239
|
+
'mcp.json': docs.mcpManifest === undefined ? undefined : descriptorHandler(docs.mcpManifest),
|
|
240
|
+
'openapi.json': docs.openapi === undefined ? undefined : descriptorHandler(docs.openapi),
|
|
241
|
+
'mcp-registry-auth': registry ?? undefined,
|
|
238
242
|
};
|
|
239
243
|
return async (request, { params }) => {
|
|
240
244
|
const { doc } = await params;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
return Object.hasOwn(bodies, doc) ? descriptorResponse(request, bodies[doc]) : new Response('Not found', { status: 404 });
|
|
245
|
+
const handler = Object.hasOwn(served, doc) ? served[doc] : undefined;
|
|
246
|
+
return handler ? handler(request) : new Response('Not found', { status: 404 });
|
|
245
247
|
};
|
|
246
248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.86.
|
|
3
|
+
"version": "0.86.3",
|
|
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",
|