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.
Files changed (79) hide show
  1. package/README.md +85 -27
  2. package/bin/maintenance-log.mjs +198 -0
  3. package/bin/mark-verified.mjs +75 -0
  4. package/bin/prisma-models.mjs +53 -0
  5. package/dist/analytics.d.ts +0 -25
  6. package/dist/analytics.js +10 -49
  7. package/dist/base.css +23 -2
  8. package/dist/blocks.d.ts +7 -6
  9. package/dist/blocks.js +11 -3
  10. package/dist/chart.d.ts +0 -13
  11. package/dist/chart.js +4 -0
  12. package/dist/combobox.d.ts +0 -4
  13. package/dist/combobox.js +2 -0
  14. package/dist/crawlers.d.ts +0 -24
  15. package/dist/crawlers.js +9 -3
  16. package/dist/dom.d.ts +0 -5
  17. package/dist/dom.js +2 -0
  18. package/dist/editor.d.ts +0 -43
  19. package/dist/editor.js +7 -1
  20. package/dist/feed.d.ts +0 -14
  21. package/dist/feed.js +4 -0
  22. package/dist/freshness.d.ts +0 -7
  23. package/dist/freshness.js +3 -0
  24. package/dist/headers.d.ts +0 -8
  25. package/dist/headers.js +3 -1
  26. package/dist/headings.d.ts +0 -8
  27. package/dist/headings.js +2 -0
  28. package/dist/history.d.ts +0 -6
  29. package/dist/history.js +2 -0
  30. package/dist/http.d.ts +0 -11
  31. package/dist/http.js +5 -0
  32. package/dist/license.d.ts +0 -8
  33. package/dist/license.js +2 -0
  34. package/dist/link-check.d.ts +0 -21
  35. package/dist/link-check.js +6 -2
  36. package/dist/links.d.ts +0 -5
  37. package/dist/links.js +2 -0
  38. package/dist/maintenance.d.ts +61 -0
  39. package/dist/maintenance.js +137 -0
  40. package/dist/maps.d.ts +0 -6
  41. package/dist/maps.js +2 -0
  42. package/dist/markdown.d.ts +0 -2
  43. package/dist/markdown.js +1 -1
  44. package/dist/mcp.d.ts +0 -14
  45. package/dist/mcp.js +1 -1
  46. package/dist/metadata.d.ts +14 -11
  47. package/dist/metadata.js +33 -0
  48. package/dist/pagination.d.ts +0 -5
  49. package/dist/pagination.js +0 -4
  50. package/dist/passkey.js +2 -15
  51. package/dist/rate-limit.d.ts +5 -16
  52. package/dist/rate-limit.js +10 -1
  53. package/dist/react-server.d.ts +0 -12
  54. package/dist/react-server.js +2 -0
  55. package/dist/revisions.d.ts +0 -7
  56. package/dist/revisions.js +2 -0
  57. package/dist/rola.d.ts +74 -0
  58. package/dist/rola.js +97 -0
  59. package/dist/search.d.ts +0 -12
  60. package/dist/search.js +4 -0
  61. package/dist/sidebar.d.ts +0 -16
  62. package/dist/sidebar.js +2 -0
  63. package/dist/stats.d.ts +0 -4
  64. package/dist/stats.js +2 -2
  65. package/dist/taxonomy.d.ts +0 -4
  66. package/dist/taxonomy.js +2 -2
  67. package/dist/text.d.ts +16 -22
  68. package/dist/text.js +39 -5
  69. package/dist/tiptap.d.ts +5 -4
  70. package/dist/tiptap.js +2 -1
  71. package/dist/upload.d.ts +45 -0
  72. package/dist/upload.js +106 -0
  73. package/dist/validation.d.ts +0 -6
  74. package/dist/validation.js +3 -3
  75. package/dist/well-known.d.ts +0 -20
  76. package/dist/well-known.js +3 -0
  77. package/package.json +37 -8
  78. package/prisma/analytics.prisma +57 -0
  79. package/prisma/passkey.prisma +21 -0
@@ -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;
@@ -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`;
@@ -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;
@@ -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');
@@ -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 = "<[^>]*>|&nbsp;|\\\\[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 = '<[^>]*>|&nbsp;|\\\\[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
- export const STATS_RANGES = [1, 7, 30, 90, 365, Infinity];
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`
@@ -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. Each wiki's own list, from its block-utils. */
38
- langs: readonly string[];
39
- defaultLang: string;
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, }: CodeBlockOptions): TiptapNode<import("@tiptap/extension-code-block").CodeBlockOptions, any>;
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), []);
@@ -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 {};