wiki-formant 0.48.0 → 0.50.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 +85 -27
- 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 +0 -13
- package/dist/chart.js +4 -0
- 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 +45 -0
- package/dist/upload.js +106 -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/rate-limit.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export interface HeaderReader {
|
|
|
60
60
|
export declare function clientIp(headers: HeaderReader): string;
|
|
61
61
|
/** `prefix:ip` — the key `rateLimit` expects for a per-IP gate. */
|
|
62
62
|
export declare function clientKey(prefix: string, headers: HeaderReader): string;
|
|
63
|
+
/**
|
|
64
|
+
* `rateLimit` on the caller's address under `prefix`: the per-IP gate a dozen
|
|
65
|
+
* call sites across four apps wrote out as `rateLimit(clientKey(…), opts)`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function limitClient(prefix: string, headers: HeaderReader, opts: RateLimitOptions): RateLimitResult;
|
|
63
68
|
/** The message a 429 body carries, so every surface refuses in the same words. */
|
|
64
69
|
export declare function retryMessage(retryAfterSec: number): string;
|
|
65
70
|
/**
|
|
@@ -87,22 +92,6 @@ export declare function rateLimitedResponse(retryAfterSec: number): Response;
|
|
|
87
92
|
export declare function rateLimitHeaders(result: RateLimitResult, opts: RateLimitOptions): Record<string, string>;
|
|
88
93
|
/** `rateLimitHeaders` applied to a response you already have. */
|
|
89
94
|
export declare function withRateLimit<T extends Response>(res: T, result: RateLimitResult, opts: RateLimitOptions): T;
|
|
90
|
-
/** Test seam: the bucket map is module state and outlives a single test. */
|
|
91
|
-
export declare function resetRateLimits(): void;
|
|
92
|
-
/**
|
|
93
|
-
* The one MCP rate-limit number, for every surface that states one.
|
|
94
|
-
*
|
|
95
|
-
* `.well-known/mcp.json`, the OpenAPI spec, agents.md, llms.txt and the
|
|
96
|
-
* `initialize` instructions all quote this, so the endpoint enforces exactly
|
|
97
|
-
* what the documents claim. It is a cross-surface contract, which is why it
|
|
98
|
-
* lives here rather than three times over. A second copy is how a number
|
|
99
|
-
* becomes impossible to change safely: nobody can tell a considered difference
|
|
100
|
-
* from a stale one.
|
|
101
|
-
*
|
|
102
|
-
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
103
|
-
* What it must not do is restate this one.
|
|
104
|
-
*/
|
|
105
|
-
export declare const MCP_RATE_LIMIT_PER_MIN = 60;
|
|
106
95
|
export declare const MCP_RATE_LIMIT_TEXT = "60 requests per minute per IP";
|
|
107
96
|
/** The budget as `mcpResponse` takes it, so a route states it once. */
|
|
108
97
|
export declare const MCP_RATE_LIMIT: RateLimitOptions;
|
package/dist/rate-limit.js
CHANGED
|
@@ -108,6 +108,13 @@ export function clientIp(headers) {
|
|
|
108
108
|
export function clientKey(prefix, headers) {
|
|
109
109
|
return `${prefix}:${clientIp(headers)}`;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* `rateLimit` on the caller's address under `prefix`: the per-IP gate a dozen
|
|
113
|
+
* call sites across four apps wrote out as `rateLimit(clientKey(…), opts)`.
|
|
114
|
+
*/
|
|
115
|
+
export function limitClient(prefix, headers, opts) {
|
|
116
|
+
return rateLimit(clientKey(prefix, headers), opts);
|
|
117
|
+
}
|
|
111
118
|
/** The message a 429 body carries, so every surface refuses in the same words. */
|
|
112
119
|
export function retryMessage(retryAfterSec) {
|
|
113
120
|
return `Too many requests. Try again in ${retryAfterSec}s.`;
|
|
@@ -157,7 +164,7 @@ export function withRateLimit(res, result, opts) {
|
|
|
157
164
|
res.headers.set(k, v);
|
|
158
165
|
return res;
|
|
159
166
|
}
|
|
160
|
-
/** Test seam: the bucket map is module state and outlives a single test. */
|
|
167
|
+
/** Test seam: the bucket map is module state and outlives a single test. @internal */
|
|
161
168
|
export function resetRateLimits() {
|
|
162
169
|
buckets.clear();
|
|
163
170
|
blocks.clear();
|
|
@@ -175,6 +182,8 @@ export function resetRateLimits() {
|
|
|
175
182
|
*
|
|
176
183
|
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
177
184
|
* What it must not do is restate this one.
|
|
185
|
+
*
|
|
186
|
+
* @internal
|
|
178
187
|
*/
|
|
179
188
|
export const MCP_RATE_LIMIT_PER_MIN = 60;
|
|
180
189
|
export const MCP_RATE_LIMIT_TEXT = `${MCP_RATE_LIMIT_PER_MIN} requests per minute per IP`;
|
package/dist/react-server.d.ts
CHANGED
|
@@ -103,18 +103,6 @@ export interface RelatedPagesProps {
|
|
|
103
103
|
* the landmark and the list, the other neither.
|
|
104
104
|
*/
|
|
105
105
|
export declare function RelatedPages({ pages, heading, href, link: Link, id }: RelatedPagesProps): import("react").JSX.Element | null;
|
|
106
|
-
/**
|
|
107
|
-
* A JSON-LD payload, safe to place inside `<script type="application/ld+json">`.
|
|
108
|
-
*
|
|
109
|
-
* The body has to go in through `dangerouslySetInnerHTML` — React escapes a
|
|
110
|
-
* text child's `<`, which breaks the parser Google reads — and these payloads
|
|
111
|
-
* carry authored strings: page titles, display names, excerpts. An authored
|
|
112
|
-
* `</script>` closes the tag and the rest parses as markup. Re-encoding every
|
|
113
|
-
* `<` as its JSON escape closes that; every JSON parser decodes it back, so the
|
|
114
|
-
* data a crawler reads is unchanged. One of the three wikis did this; the
|
|
115
|
-
* other two, and this package's own breadcrumb trail, did not.
|
|
116
|
-
*/
|
|
117
|
-
export declare function jsonLdScript(data: unknown): string;
|
|
118
106
|
/** A `<script type="application/ld+json">` for `data`, escaped by `jsonLdScript`. */
|
|
119
107
|
export declare function JsonLd({ data }: {
|
|
120
108
|
data: unknown;
|
package/dist/react-server.js
CHANGED
|
@@ -92,6 +92,8 @@ export function RelatedPages({ pages, heading, href, link: Link = Anchor, id = '
|
|
|
92
92
|
* `<` as its JSON escape closes that; every JSON parser decodes it back, so the
|
|
93
93
|
* data a crawler reads is unchanged. One of the three wikis did this; the
|
|
94
94
|
* other two, and this package's own breadcrumb trail, did not.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
95
97
|
*/
|
|
96
98
|
export function jsonLdScript(data) {
|
|
97
99
|
return JSON.stringify(data).replace(/</g, '\\u003c');
|
package/dist/revisions.d.ts
CHANGED
|
@@ -69,13 +69,6 @@ export declare function extractBlocks<B>(blocks: readonly B[], containers: (bloc
|
|
|
69
69
|
* as one removal and one addition.
|
|
70
70
|
*/
|
|
71
71
|
export declare function diffBlocks<B extends DiffBlock, L = unknown>(oldBlocks: readonly B[], newBlocks: readonly B[], opts: DiffOptions<B, L>): BlockChange<L>[];
|
|
72
|
-
/**
|
|
73
|
-
* `major` for structure, `minor` for prose, `patch` for a page-level flag.
|
|
74
|
-
*
|
|
75
|
-
* A `modified` change is only ever recorded when `diffAttributes` returned
|
|
76
|
-
* something, so a non-empty change set is always at least a minor bump.
|
|
77
|
-
*/
|
|
78
|
-
export declare function classifyChanges(changes: readonly BlockChange<unknown>[], titleChanged: boolean, metaChanged?: boolean): ChangeType;
|
|
79
72
|
export declare function changeSummary(diff: {
|
|
80
73
|
changes: readonly BlockChange<unknown>[];
|
|
81
74
|
titleChanged: boolean;
|
package/dist/revisions.js
CHANGED
|
@@ -132,6 +132,8 @@ export function diffBlocks(oldBlocks, newBlocks, opts) {
|
|
|
132
132
|
*
|
|
133
133
|
* A `modified` change is only ever recorded when `diffAttributes` returned
|
|
134
134
|
* something, so a non-empty change set is always at least a minor bump.
|
|
135
|
+
*
|
|
136
|
+
* @internal
|
|
135
137
|
*/
|
|
136
138
|
export function classifyChanges(changes, titleChanged, metaChanged = false) {
|
|
137
139
|
if (changes.length === 0 && !titleChanged && !metaChanged)
|
package/dist/rola.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type JWTPayload } from 'jose';
|
|
2
|
+
export interface SignedChallenge {
|
|
3
|
+
challenge: string;
|
|
4
|
+
address: string;
|
|
5
|
+
proof: {
|
|
6
|
+
publicKey: string;
|
|
7
|
+
signature: string;
|
|
8
|
+
curve: 'curve25519' | 'secp256k1';
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** What `prisma.challenge` already is, narrowed to the two calls this makes. */
|
|
12
|
+
export interface ChallengeStore {
|
|
13
|
+
create(args: {
|
|
14
|
+
data: {
|
|
15
|
+
challenge: string;
|
|
16
|
+
expiresAt: Date;
|
|
17
|
+
};
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
deleteMany(args: {
|
|
20
|
+
where: {
|
|
21
|
+
challenge?: string;
|
|
22
|
+
expiresAt: {
|
|
23
|
+
lt: Date;
|
|
24
|
+
} | {
|
|
25
|
+
gt: Date;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}): Promise<{
|
|
29
|
+
count: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface RolaOptions {
|
|
33
|
+
expectedOrigin: string;
|
|
34
|
+
dAppDefinitionAddress: string;
|
|
35
|
+
networkId: number;
|
|
36
|
+
applicationName: string;
|
|
37
|
+
challenges: ChallengeStore;
|
|
38
|
+
/** Default 300: the wallet round-trip, with room for a slow signer. */
|
|
39
|
+
challengeTtlSec?: number;
|
|
40
|
+
}
|
|
41
|
+
export type Verification = {
|
|
42
|
+
isValid: true;
|
|
43
|
+
} | {
|
|
44
|
+
isValid: false;
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
export interface RolaAuth {
|
|
48
|
+
generateChallenge(): Promise<{
|
|
49
|
+
challenge: string;
|
|
50
|
+
expiresAt: Date;
|
|
51
|
+
}>;
|
|
52
|
+
/** Consumes the challenge whatever the outcome, so a signature cannot be retried. */
|
|
53
|
+
verifySignedChallenge(signed: SignedChallenge): Promise<Verification>;
|
|
54
|
+
}
|
|
55
|
+
export declare function createRola({ challenges, challengeTtlSec, ...identity }: RolaOptions): RolaAuth;
|
|
56
|
+
/**
|
|
57
|
+
* The signing key for an environment variable's value. Throws on a missing or
|
|
58
|
+
* short one: a low-entropy HS256 secret is crackable offline from one captured
|
|
59
|
+
* token, which would let anyone mint a session for any address. Generate one
|
|
60
|
+
* with `openssl rand -base64 48`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function sessionSecret(value: string | undefined): Uint8Array;
|
|
63
|
+
/** An HS256 token carrying `claims`, issued now, expiring in `ttlSec`, with a unique `jti`. */
|
|
64
|
+
export declare function signSession(claims: JWTPayload, secret: Uint8Array, ttlSec: number): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* The claims of a token that verifies, or null for a bad signature, an expired
|
|
67
|
+
* `exp` or any other algorithm. HS256 is pinned, so a token cannot choose how
|
|
68
|
+
* it is checked.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifySession<T extends object = JWTPayload>(token: string, secret: Uint8Array): Promise<(T & JWTPayload) | null>;
|
|
71
|
+
/** An `Authorization: Bearer` token, the way an agent presents the cookie's. */
|
|
72
|
+
export declare function bearerToken(headers: {
|
|
73
|
+
get(name: string): string | null;
|
|
74
|
+
}): string | null;
|
package/dist/rola.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// rola.ts — Radix On-Ledger Authentication: a single-use challenge, the wallet's
|
|
2
|
+
// proof of it, and the HS256 token a verified address is handed.
|
|
3
|
+
//
|
|
4
|
+
// radix-wiki and caper sign in the same way and keep a session differently:
|
|
5
|
+
// radix-wiki a `Session` row behind a seven-day cookie, caper a stateless token
|
|
6
|
+
// it slides forward on every visit, with a twelve-hour one for agents. What
|
|
7
|
+
// they share is everything up to the token, plus the token's own arithmetic,
|
|
8
|
+
// and that is all this holds. Where the two copies differed it takes the
|
|
9
|
+
// stricter: a challenge is consumed by one conditional delete, so two requests
|
|
10
|
+
// racing with one signature cannot both pass; expired challenges are purged as
|
|
11
|
+
// new ones are made; only an account proof signs in, which both apps already
|
|
12
|
+
// enforced; a secret under 32 characters refuses to start, since HS256 is only
|
|
13
|
+
// as strong as its secret; and verification pins HS256.
|
|
14
|
+
//
|
|
15
|
+
// The challenge table is a port, not an import: a Prisma client is generated
|
|
16
|
+
// per app, and both apps' `prisma.challenge` already satisfies it. `jose` and
|
|
17
|
+
// `@radixdlt/rola` are optional peers, needed only by an app importing this.
|
|
18
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
19
|
+
import { Rola } from '@radixdlt/rola';
|
|
20
|
+
const hex = (bytes) => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
21
|
+
export function createRola({ challenges, challengeTtlSec = 300, ...identity }) {
|
|
22
|
+
const rola = Rola(identity);
|
|
23
|
+
return {
|
|
24
|
+
async generateChallenge() {
|
|
25
|
+
const challenge = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
26
|
+
const expiresAt = new Date(Date.now() + challengeTtlSec * 1000);
|
|
27
|
+
await challenges.deleteMany({ where: { expiresAt: { lt: new Date() } } });
|
|
28
|
+
await challenges.create({ data: { challenge, expiresAt } });
|
|
29
|
+
return { challenge, expiresAt };
|
|
30
|
+
},
|
|
31
|
+
async verifySignedChallenge(signed) {
|
|
32
|
+
try {
|
|
33
|
+
const { count } = await challenges.deleteMany({
|
|
34
|
+
where: { challenge: signed.challenge, expiresAt: { gt: new Date() } },
|
|
35
|
+
});
|
|
36
|
+
if (count !== 1) {
|
|
37
|
+
console.error('ROLA: challenge invalid or expired', `${signed.challenge.slice(0, 16)}...`);
|
|
38
|
+
return { isValid: false, error: 'Invalid or expired challenge' };
|
|
39
|
+
}
|
|
40
|
+
// A persona proof names no account, and the session belongs to the
|
|
41
|
+
// address the proof verified, never to one a request body names.
|
|
42
|
+
if (!signed.address.startsWith('account_')) {
|
|
43
|
+
return { isValid: false, error: 'Sign in with an account proof' };
|
|
44
|
+
}
|
|
45
|
+
const result = await rola.verifySignedChallenge({ ...signed, type: 'account' });
|
|
46
|
+
if (result.isErr()) {
|
|
47
|
+
console.error('ROLA: verification failed', result.error.reason, result.error);
|
|
48
|
+
return { isValid: false, error: result.error.reason };
|
|
49
|
+
}
|
|
50
|
+
return { isValid: true };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('ROLA verification error:', error);
|
|
54
|
+
return { isValid: false, error: 'Verification failed' };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The signing key for an environment variable's value. Throws on a missing or
|
|
61
|
+
* short one: a low-entropy HS256 secret is crackable offline from one captured
|
|
62
|
+
* token, which would let anyone mint a session for any address. Generate one
|
|
63
|
+
* with `openssl rand -base64 48`.
|
|
64
|
+
*/
|
|
65
|
+
export function sessionSecret(value) {
|
|
66
|
+
if (!value || value.length < 32) {
|
|
67
|
+
throw new Error('The session secret must be at least 32 characters of high-entropy randomness');
|
|
68
|
+
}
|
|
69
|
+
return new TextEncoder().encode(value);
|
|
70
|
+
}
|
|
71
|
+
/** An HS256 token carrying `claims`, issued now, expiring in `ttlSec`, with a unique `jti`. */
|
|
72
|
+
export function signSession(claims, secret, ttlSec) {
|
|
73
|
+
return new SignJWT(claims)
|
|
74
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
75
|
+
.setIssuedAt()
|
|
76
|
+
.setExpirationTime(`${ttlSec}s`)
|
|
77
|
+
.setJti(crypto.randomUUID())
|
|
78
|
+
.sign(secret);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The claims of a token that verifies, or null for a bad signature, an expired
|
|
82
|
+
* `exp` or any other algorithm. HS256 is pinned, so a token cannot choose how
|
|
83
|
+
* it is checked.
|
|
84
|
+
*/
|
|
85
|
+
export async function verifySession(token, secret) {
|
|
86
|
+
try {
|
|
87
|
+
const { payload } = await jwtVerify(token, secret, { algorithms: ['HS256'] });
|
|
88
|
+
return payload;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** An `Authorization: Bearer` token, the way an agent presents the cookie's. */
|
|
95
|
+
export function bearerToken(headers) {
|
|
96
|
+
return headers.get('authorization')?.match(/^Bearer\s+(.+)$/i)?.[1] ?? null;
|
|
97
|
+
}
|
package/dist/search.d.ts
CHANGED
|
@@ -20,18 +20,6 @@
|
|
|
20
20
|
* when the query aliases its table.
|
|
21
21
|
*/
|
|
22
22
|
export declare function proseSql(content?: string): string;
|
|
23
|
-
/**
|
|
24
|
-
* The `regexp_replace` pattern inside {@link proseSql}, as it must appear in
|
|
25
|
-
* SQL. Written for a single-quoted SQL string literal, so a backslash that
|
|
26
|
-
* Postgres' regex engine should see as an escape is doubled here.
|
|
27
|
-
*/
|
|
28
|
-
export declare const PROSE_PATTERN = "<[^>]*>| |\\\\[nrt\"\\\\/]|\", \"|\\[\"|\"\\]";
|
|
29
|
-
/**
|
|
30
|
-
* The generated column behind the full-text tier: title at weight A, prose at
|
|
31
|
-
* weight B, so a title hit outranks a body hit inside tier 3 as well as across
|
|
32
|
-
* tiers.
|
|
33
|
-
*/
|
|
34
|
-
export declare function searchTsvSql(content?: string, title?: string): string;
|
|
35
23
|
/**
|
|
36
24
|
* The statements that (re)build a table's `search_tsv` column and its GIN index.
|
|
37
25
|
* Run `column` inside one transaction: DROP then ADD, so no reader ever sees
|
package/dist/search.js
CHANGED
|
@@ -49,12 +49,16 @@ export function proseSql(content = 'content') {
|
|
|
49
49
|
* The `regexp_replace` pattern inside {@link proseSql}, as it must appear in
|
|
50
50
|
* SQL. Written for a single-quoted SQL string literal, so a backslash that
|
|
51
51
|
* Postgres' regex engine should see as an escape is doubled here.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
52
54
|
*/
|
|
53
55
|
export const PROSE_PATTERN = '<[^>]*>| |\\\\[nrt"\\\\/]|", "|\\["|"\\]';
|
|
54
56
|
/**
|
|
55
57
|
* The generated column behind the full-text tier: title at weight A, prose at
|
|
56
58
|
* weight B, so a title hit outranks a body hit inside tier 3 as well as across
|
|
57
59
|
* tiers.
|
|
60
|
+
*
|
|
61
|
+
* @internal
|
|
58
62
|
*/
|
|
59
63
|
export function searchTsvSql(content = 'content', title = 'title') {
|
|
60
64
|
return `setweight(to_tsvector('english', coalesce(${title},'')), 'A') || ` +
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -20,22 +20,6 @@ export declare function resolveSidebarOpen({ chosen, current, stored, isMobile,
|
|
|
20
20
|
}): boolean;
|
|
21
21
|
/** The attribute the boot script and the hook both write to `<html>`. */
|
|
22
22
|
export declare const SIDEBAR_ATTRIBUTE = "data-sidebar";
|
|
23
|
-
/**
|
|
24
|
-
* The custom property a consumer's stylesheet sets inside its own rail media
|
|
25
|
-
* query — `1` where the rail stops being a column beside the article, `0`
|
|
26
|
-
* elsewhere.
|
|
27
|
-
*
|
|
28
|
-
* The rail's breakpoint is necessarily known twice: this hook needs it in JS
|
|
29
|
-
* (to close the rail on navigate, and to default a first-ever visit), and the
|
|
30
|
-
* stylesheet needs it in CSS (to lay the rail out, before any script runs and
|
|
31
|
-
* whether or not one ever does). A media query cannot read a JS constant and a
|
|
32
|
-
* JS constant cannot read a media query, so the two numbers cannot be merged —
|
|
33
|
-
* but they can be made unable to disagree QUIETLY, which is the actual hazard.
|
|
34
|
-
*
|
|
35
|
-
* All three wikis matched by hand and all three happened to be right; nothing
|
|
36
|
-
* said so. Declaring this property is what opts a stylesheet into the check.
|
|
37
|
-
*/
|
|
38
|
-
export declare const RAIL_FLOATING_PROPERTY = "--rail-floating";
|
|
39
23
|
/**
|
|
40
24
|
* What the stylesheet currently says about the rail: `true` inside the repo's
|
|
41
25
|
* rail media query, `false` outside it, `null` when the property is not
|
package/dist/sidebar.js
CHANGED
|
@@ -35,6 +35,8 @@ export const SIDEBAR_ATTRIBUTE = 'data-sidebar';
|
|
|
35
35
|
*
|
|
36
36
|
* All three wikis matched by hand and all three happened to be right; nothing
|
|
37
37
|
* said so. Declaring this property is what opts a stylesheet into the check.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
38
40
|
*/
|
|
39
41
|
export const RAIL_FLOATING_PROPERTY = '--rail-floating';
|
|
40
42
|
/**
|
package/dist/stats.d.ts
CHANGED
|
@@ -9,10 +9,6 @@ export interface StatsProps {
|
|
|
9
9
|
/** The windows offered as `?days=` links on the same page. */
|
|
10
10
|
ranges?: readonly number[];
|
|
11
11
|
}
|
|
12
|
-
/** The windows the stats page offers, in days; `Infinity` is all time. */
|
|
13
|
-
export declare const STATS_RANGES: readonly [1, 7, 30, 90, 365, number];
|
|
14
|
-
/** A `?days=` value as one of `ranges`, else `fallback`. */
|
|
15
|
-
export declare const statsDays: (param: unknown, ranges?: readonly number[], fallback?: number) => number;
|
|
16
12
|
/**
|
|
17
13
|
* A stats page's query as the window and the filter, both ready for `digest`
|
|
18
14
|
* and `Stats`: `const q = statsQuery(await searchParams)`. A repeated or empty
|
package/dist/stats.js
CHANGED
|
@@ -2,12 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { STATS_FILTERS } from './analytics.js';
|
|
3
3
|
import { StatsFigures } from './stats-figures.js';
|
|
4
4
|
/** The windows the stats page offers, in days; `Infinity` is all time. */
|
|
5
|
-
|
|
5
|
+
const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
|
|
6
6
|
/** A window as its `?days=` value. */
|
|
7
7
|
const daysParam = (n) => (Number.isFinite(n) ? String(n) : 'all');
|
|
8
8
|
/** A window as `TimeChart` names its ranges, since its buttons are where the windows sit. */
|
|
9
9
|
const windowLabel = (n) => (n === 1 ? '24H' : n === 365 ? '1Y' : Number.isFinite(n) ? `${n}D` : 'All');
|
|
10
|
-
/** A `?days=` value as one of `ranges`, else `fallback`. */
|
|
10
|
+
/** A `?days=` value as one of `ranges`, else `fallback`. @internal */
|
|
11
11
|
export const statsDays = (param, ranges = STATS_RANGES, fallback = 30) => ranges.find(n => daysParam(n) === param) ?? fallback;
|
|
12
12
|
/**
|
|
13
13
|
* A stats page's query as the window and the filter, both ready for `digest`
|
package/dist/taxonomy.d.ts
CHANGED
|
@@ -118,10 +118,6 @@ export interface TaxonomyConfig {
|
|
|
118
118
|
export declare function hrefBuilder(prefix?: string): (tagPath: string, state: CategoryState) => string;
|
|
119
119
|
/** Categories mounted at the root. */
|
|
120
120
|
export declare const defaultHref: (tagPath: string, state: CategoryState) => string;
|
|
121
|
-
/** Bucket for titles that don't start with a letter (numerals, `$CAPER`). */
|
|
122
|
-
export declare function firstLetter(title: string): string;
|
|
123
|
-
/** Adding a filter already active removes it, so every chip is its own off-switch. */
|
|
124
|
-
export declare function toggleFilter(filters: FacetFilters, key: string, value: string): FacetFilters;
|
|
125
121
|
export interface Taxonomy {
|
|
126
122
|
/** The select-typed keys a tag path declares — its facetable axes. */
|
|
127
123
|
facetKeys: (tagPath: string) => MetadataKeyDefinition[];
|
package/dist/taxonomy.js
CHANGED
|
@@ -38,12 +38,12 @@ export function hrefBuilder(prefix = '') {
|
|
|
38
38
|
export const defaultHref = hrefBuilder();
|
|
39
39
|
const DEFAULT_ALPHA_INDEX_MIN_PAGES = 40;
|
|
40
40
|
const metaValue = (page, key) => (page.metadata?.[key] ?? '').trim();
|
|
41
|
-
/** Bucket for titles that don't start with a letter (numerals, `$CAPER`). */
|
|
41
|
+
/** Bucket for titles that don't start with a letter (numerals, `$CAPER`). @internal */
|
|
42
42
|
export function firstLetter(title) {
|
|
43
43
|
const ch = title.trim()[0]?.toUpperCase() ?? '';
|
|
44
44
|
return ch >= 'A' && ch <= 'Z' ? ch : '#';
|
|
45
45
|
}
|
|
46
|
-
/** Adding a filter already active removes it, so every chip is its own off-switch. */
|
|
46
|
+
/** Adding a filter already active removes it, so every chip is its own off-switch. @internal */
|
|
47
47
|
export function toggleFilter(filters, key, value) {
|
|
48
48
|
const next = { ...filters };
|
|
49
49
|
if (next[key] === value)
|
package/dist/text.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CodeTab, LinkGridGroup, ReferenceItem, StatItem } from './blocks.js';
|
|
2
1
|
/**
|
|
3
2
|
* HTML to readable text. Links keep their href in parentheses so a model can
|
|
4
3
|
* still follow a citation; block-level tags become newlines; list items get a
|
|
@@ -6,6 +5,22 @@ import type { CodeTab, LinkGridGroup, ReferenceItem, StatItem } from './blocks.j
|
|
|
6
5
|
* prose cannot become a tag the strip has already run past.
|
|
7
6
|
*/
|
|
8
7
|
export declare function stripHtml(html: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* HTML as one plain line: tags become spaces, entities are decoded, and the
|
|
10
|
+
* space a tag left before punctuation is closed up again, or prose that wrapped
|
|
11
|
+
* a link around the word before a comma reads "the Radix network ." Unlike
|
|
12
|
+
* `stripHtml`, links keep no href: this is for a description, not evidence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function plainText(html: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* How a page opens: the first top-level `content` block that has prose once
|
|
17
|
+
* its headings are out, as plain text. Wiki pages open with an infobox far more
|
|
18
|
+
* often than not, so the first characters of the whole page are a flattened
|
|
19
|
+
* metadata table, which is what two wikis' descriptions said until both
|
|
20
|
+
* switched to this rule. `''` when no block has prose; a wiki with other
|
|
21
|
+
* prose-bearing types falls back to its own extractor.
|
|
22
|
+
*/
|
|
23
|
+
export declare function openingText(blocks: unknown): string;
|
|
9
24
|
/** The six maintenance-banner variants every wiki here renders. */
|
|
10
25
|
export type BannerVariant = 'stub' | 'unsourced' | 'outdated' | 'promotional' | 'cleanup' | 'coi';
|
|
11
26
|
/**
|
|
@@ -29,27 +44,6 @@ export declare const BANNER_VARIANTS: ReadonlyArray<{
|
|
|
29
44
|
}>;
|
|
30
45
|
/** A stored variant this package knows, or `cleanup` for one it does not. */
|
|
31
46
|
export declare function bannerVariant(variant: string): BannerVariant;
|
|
32
|
-
/** A maintenance notice, inline: `[Notice: Needs citations] …`. */
|
|
33
|
-
export declare function bannerToText(label: string, text?: string | null): string;
|
|
34
|
-
/** Each tab under its label, tags stripped — highlighted markup is noise here. */
|
|
35
|
-
export declare function codeTabsToText(tabs: readonly CodeTab[]): string;
|
|
36
|
-
/** Metric cards, one per line: `99% Uptime`. */
|
|
37
|
-
export declare function statsToText(items: readonly StatItem[]): string;
|
|
38
|
-
/**
|
|
39
|
-
* Each group's heading over its links, hrefs in parentheses the way
|
|
40
|
-
* `stripHtml` keeps them.
|
|
41
|
-
*
|
|
42
|
-
* Two of the three wikis extracted nothing from a link grid, or from stats or
|
|
43
|
-
* page lists, so a hub page read to an agent as far emptier than it is. The
|
|
44
|
-
* third had its own three bodies. These are those bodies.
|
|
45
|
-
*/
|
|
46
|
-
export declare function linkGridToText(groups: readonly LinkGridGroup[], intro?: string | null): string;
|
|
47
|
-
/** A resolved page list: one title per line. */
|
|
48
|
-
export declare function pageListToText(pages: readonly {
|
|
49
|
-
title: string;
|
|
50
|
-
}[]): string;
|
|
51
|
-
/** A numbered reference list, or `''` when there are none. */
|
|
52
|
-
export declare function referencesToText(items: readonly ReferenceItem[]): string;
|
|
53
47
|
/**
|
|
54
48
|
* The passage that matched `query`, not the opening of the page.
|
|
55
49
|
*
|
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,45 @@
|
|
|
1
|
+
import { type Sharp } from 'sharp';
|
|
2
|
+
import { type RateLimitOptions } from './rate-limit.js';
|
|
3
|
+
export type ImageFormat = 'jpg' | 'png' | 'gif' | 'webp' | 'avif';
|
|
4
|
+
type Encode = (image: Sharp, source: {
|
|
5
|
+
format: ImageFormat;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}) => {
|
|
9
|
+
image: Sharp;
|
|
10
|
+
format: ImageFormat;
|
|
11
|
+
};
|
|
12
|
+
export interface NormalizeOptions {
|
|
13
|
+
/** Decoded pixels, default 40 million: a phone camera's full resolution. */
|
|
14
|
+
maxPixels?: number;
|
|
15
|
+
/**
|
|
16
|
+
* How the upright image is written. Default the format it arrived in. A wiki
|
|
17
|
+
* with an image standard resizes and converts here.
|
|
18
|
+
*/
|
|
19
|
+
encode?: Encode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The image re-encoded upright with no metadata, in the format `encode` chose.
|
|
23
|
+
* Throws, with a message fit to show the uploader, for bytes that are not an
|
|
24
|
+
* accepted image or decode to more than `maxPixels`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeImage(bytes: Buffer, { maxPixels, encode, }?: NormalizeOptions): Promise<{
|
|
27
|
+
buffer: Buffer;
|
|
28
|
+
format: ImageFormat;
|
|
29
|
+
contentType: string;
|
|
30
|
+
}>;
|
|
31
|
+
export interface UploadOptions extends NormalizeOptions {
|
|
32
|
+
/** The uploader's id, which keys the rate limit, or the response that turns them away. */
|
|
33
|
+
authorize: (request: Request) => Promise<string | Response>;
|
|
34
|
+
/** Default 4 MB. */
|
|
35
|
+
maxBytes?: number;
|
|
36
|
+
/** Default a burst of 20, then one every five seconds. */
|
|
37
|
+
rateLimit?: RateLimitOptions;
|
|
38
|
+
/** Prepended to the stored name, e.g. `wiki/`. */
|
|
39
|
+
prefix?: string;
|
|
40
|
+
/** Stores the bytes and resolves to their public URL. Default Vercel Blob, public. */
|
|
41
|
+
store?: (path: string, body: Buffer, contentType: string) => Promise<string>;
|
|
42
|
+
}
|
|
43
|
+
/** The POST handler: a multipart `file` in, `{ url }` out. */
|
|
44
|
+
export declare function uploadHandler({ authorize, maxBytes, maxPixels, rateLimit, encode, prefix, store, }: UploadOptions): (request: Request) => Promise<Response>;
|
|
45
|
+
export {};
|