wiki-formant 0.47.1 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -26
- package/bin/maintenance-log.mjs +198 -0
- package/bin/mark-verified.mjs +75 -0
- package/bin/prisma-models.mjs +53 -0
- package/dist/analytics.d.ts +0 -25
- package/dist/analytics.js +10 -49
- package/dist/base.css +23 -2
- package/dist/blocks.d.ts +7 -6
- package/dist/blocks.js +11 -3
- package/dist/chart.d.ts +17 -8
- package/dist/chart.js +53 -6
- package/dist/combobox.d.ts +0 -4
- package/dist/combobox.js +2 -0
- package/dist/crawlers.d.ts +0 -24
- package/dist/crawlers.js +9 -3
- package/dist/dom.d.ts +0 -5
- package/dist/dom.js +2 -0
- package/dist/editor.d.ts +0 -43
- package/dist/editor.js +7 -1
- package/dist/feed.d.ts +0 -14
- package/dist/feed.js +4 -0
- package/dist/freshness.d.ts +0 -7
- package/dist/freshness.js +3 -0
- package/dist/headers.d.ts +0 -8
- package/dist/headers.js +3 -1
- package/dist/headings.d.ts +0 -8
- package/dist/headings.js +2 -0
- package/dist/history.d.ts +0 -6
- package/dist/history.js +2 -0
- package/dist/http.d.ts +0 -11
- package/dist/http.js +5 -0
- package/dist/license.d.ts +0 -8
- package/dist/license.js +2 -0
- package/dist/link-check.d.ts +0 -21
- package/dist/link-check.js +6 -2
- package/dist/links.d.ts +0 -5
- package/dist/links.js +2 -0
- package/dist/maintenance.d.ts +61 -0
- package/dist/maintenance.js +137 -0
- package/dist/maps.d.ts +0 -6
- package/dist/maps.js +2 -0
- package/dist/markdown.d.ts +0 -2
- package/dist/markdown.js +1 -1
- package/dist/mcp.d.ts +0 -14
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +14 -11
- package/dist/metadata.js +33 -0
- package/dist/pagination.d.ts +0 -5
- package/dist/pagination.js +0 -4
- package/dist/passkey.js +2 -15
- package/dist/rate-limit.d.ts +5 -16
- package/dist/rate-limit.js +10 -1
- package/dist/react-server.d.ts +0 -12
- package/dist/react-server.js +2 -0
- package/dist/revisions.d.ts +0 -7
- package/dist/revisions.js +2 -0
- package/dist/rola.d.ts +74 -0
- package/dist/rola.js +97 -0
- package/dist/search.d.ts +0 -12
- package/dist/search.js +4 -0
- package/dist/sidebar.d.ts +0 -16
- package/dist/sidebar.js +2 -0
- package/dist/stats.d.ts +0 -4
- package/dist/stats.js +2 -2
- package/dist/taxonomy.d.ts +0 -4
- package/dist/taxonomy.js +2 -2
- package/dist/text.d.ts +16 -22
- package/dist/text.js +39 -5
- package/dist/tiptap.d.ts +5 -4
- package/dist/tiptap.js +2 -1
- package/dist/upload.d.ts +31 -0
- package/dist/upload.js +91 -0
- package/dist/validation.d.ts +0 -6
- package/dist/validation.js +3 -3
- package/dist/well-known.d.ts +0 -20
- package/dist/well-known.js +3 -0
- package/package.json +37 -8
- package/prisma/analytics.prisma +57 -0
- package/prisma/passkey.prisma +21 -0
package/dist/metadata.d.ts
CHANGED
|
@@ -152,17 +152,6 @@ export declare function faqPageLd(pairs: readonly QaPair[], url: string): LdNode
|
|
|
152
152
|
* which is the number that matters to whoever writes the title.
|
|
153
153
|
*/
|
|
154
154
|
export declare const TITLE_BUDGET = 60;
|
|
155
|
-
/**
|
|
156
|
-
* Past this, a title is not merely trimmed but cut into.
|
|
157
|
-
*
|
|
158
|
-
* Between the budget and this limit a title loses its tail, which is often the
|
|
159
|
-
* site name and no loss at all. Past it, the words carrying what the page is
|
|
160
|
-
* about are going too: the radix.wiki ideas board spent its first sixteen
|
|
161
|
-
* characters on a working group and reached 93, and the symptom pages on
|
|
162
|
-
* acuiq.com reached 88 before naming the symptom. That is the line worth
|
|
163
|
-
* failing a build over; the band below it is worth reporting and no more.
|
|
164
|
-
*/
|
|
165
|
-
export declare const TITLE_LIMIT = 70;
|
|
166
155
|
/**
|
|
167
156
|
* The document `<title>` for a page: its short form where the page carries one,
|
|
168
157
|
* else its own title.
|
|
@@ -196,6 +185,20 @@ export declare function fittingTitle(variants: readonly string[], opts?: {
|
|
|
196
185
|
suffix?: string;
|
|
197
186
|
limit?: number;
|
|
198
187
|
}): string;
|
|
188
|
+
/** Characters a search result shows of a meta description before it cuts. */
|
|
189
|
+
export declare const DESCRIPTION_LIMIT = 160;
|
|
190
|
+
/**
|
|
191
|
+
* A meta description from any prose: HTML, markdown or plain. Tags, entities,
|
|
192
|
+
* link targets and bare URLs go; what is left is fitted to `limit` as whole
|
|
193
|
+
* sentences where they fill at least half of it, else cut at a word and marked
|
|
194
|
+
* with an ellipsis. Never longer than `limit`.
|
|
195
|
+
*
|
|
196
|
+
* Each of the four sites had its own: one clamped at 150 mid-word, one sliced
|
|
197
|
+
* at 160 with no mark, one fitted sentences, and one kept whatever sentences
|
|
198
|
+
* fitted however few. Pass a page's own excerpt where it has one and its
|
|
199
|
+
* opening (`openingText`) where it does not: `describe(excerpt || openingText(blocks))`.
|
|
200
|
+
*/
|
|
201
|
+
export declare function describe(text: string | null | undefined, limit?: number): string;
|
|
199
202
|
/**
|
|
200
203
|
* `pageMetadata` bound to a site's name, locale and handle, the three fields
|
|
201
204
|
* Next drops from every page that sets its own card. Two wikis wrapped it to
|
package/dist/metadata.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// with no image while their og:image named a card, one listed citations, one
|
|
20
20
|
// collection page had no ItemList at all.
|
|
21
21
|
import { decodeEntities } from './markdown.js';
|
|
22
|
+
import { plainText } from './text.js';
|
|
22
23
|
export function pageMetadata(o) {
|
|
23
24
|
const { title, description, url, type = 'website', image, imageSize = { width: 1200, height: 630 }, siteName, locale, handle, markdownTwin, article } = o;
|
|
24
25
|
const images = image ? [{ url: image, ...imageSize, alt: o.imageAlt ?? title }] : undefined;
|
|
@@ -175,6 +176,8 @@ export const TITLE_BUDGET = 60;
|
|
|
175
176
|
* characters on a working group and reached 93, and the symptom pages on
|
|
176
177
|
* acuiq.com reached 88 before naming the symptom. That is the line worth
|
|
177
178
|
* failing a build over; the band below it is worth reporting and no more.
|
|
179
|
+
*
|
|
180
|
+
* @internal
|
|
178
181
|
*/
|
|
179
182
|
export const TITLE_LIMIT = 70;
|
|
180
183
|
/**
|
|
@@ -216,6 +219,36 @@ export function fittingTitle(variants, opts = {}) {
|
|
|
216
219
|
return '';
|
|
217
220
|
return usable.find(v => v.length + suffix.length <= limit) ?? usable[usable.length - 1];
|
|
218
221
|
}
|
|
222
|
+
/** Characters a search result shows of a meta description before it cuts. */
|
|
223
|
+
export const DESCRIPTION_LIMIT = 160;
|
|
224
|
+
/**
|
|
225
|
+
* A meta description from any prose: HTML, markdown or plain. Tags, entities,
|
|
226
|
+
* link targets and bare URLs go; what is left is fitted to `limit` as whole
|
|
227
|
+
* sentences where they fill at least half of it, else cut at a word and marked
|
|
228
|
+
* with an ellipsis. Never longer than `limit`.
|
|
229
|
+
*
|
|
230
|
+
* Each of the four sites had its own: one clamped at 150 mid-word, one sliced
|
|
231
|
+
* at 160 with no mark, one fitted sentences, and one kept whatever sentences
|
|
232
|
+
* fitted however few. Pass a page's own excerpt where it has one and its
|
|
233
|
+
* opening (`openingText`) where it does not: `describe(excerpt || openingText(blocks))`.
|
|
234
|
+
*/
|
|
235
|
+
export function describe(text, limit = DESCRIPTION_LIMIT) {
|
|
236
|
+
const t = plainText(text ?? '')
|
|
237
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
238
|
+
.replace(/\((?:https?:\/\/|mailto:|\/|#)[^)\s]*\)/g, '')
|
|
239
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
240
|
+
.replace(/\(\s*\)/g, '')
|
|
241
|
+
.replace(/\s+([,.;:!?])/g, '$1')
|
|
242
|
+
.replace(/\s{2,}/g, ' ')
|
|
243
|
+
.trim();
|
|
244
|
+
if (t.length <= limit)
|
|
245
|
+
return t;
|
|
246
|
+
const sentences = t.slice(0, limit + 1).match(/^.*[.!?](?=\s)/)?.[0];
|
|
247
|
+
if (sentences && sentences.length >= limit / 2)
|
|
248
|
+
return sentences;
|
|
249
|
+
const cut = t.lastIndexOf(' ', limit - 1);
|
|
250
|
+
return `${t.slice(0, cut > 0 ? cut : limit - 1).replace(/[\s,;:–—-]+$/, '')}…`;
|
|
251
|
+
}
|
|
219
252
|
// ---- the site's own identity ----------------------------------------------------
|
|
220
253
|
/**
|
|
221
254
|
* `pageMetadata` bound to a site's name, locale and handle, the three fields
|
package/dist/pagination.d.ts
CHANGED
|
@@ -42,11 +42,6 @@ export declare function listEnvelope<T>(pages: T[], total: number, page: number,
|
|
|
42
42
|
note?: string;
|
|
43
43
|
pages: T[];
|
|
44
44
|
};
|
|
45
|
-
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
46
|
-
export declare function toOffset({ page, pageSize }: Pagination): {
|
|
47
|
-
skip: number;
|
|
48
|
-
take: number;
|
|
49
|
-
};
|
|
50
45
|
/**
|
|
51
46
|
* The entries either side of the current one in an already-ordered list.
|
|
52
47
|
*
|
package/dist/pagination.js
CHANGED
|
@@ -52,10 +52,6 @@ export function listEnvelope(pages, total, page, pageSize, emptyNote) {
|
|
|
52
52
|
pages,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
56
|
-
export function toOffset({ page, pageSize }) {
|
|
57
|
-
return { skip: (page - 1) * pageSize, take: pageSize };
|
|
58
|
-
}
|
|
59
55
|
/**
|
|
60
56
|
* The entries either side of the current one in an already-ordered list.
|
|
61
57
|
*
|
package/dist/passkey.js
CHANGED
|
@@ -10,21 +10,8 @@
|
|
|
10
10
|
// not open the deployed site, and one saved on the site does not open localhost.
|
|
11
11
|
//
|
|
12
12
|
// Like analytics, the package owns the SQL and the app owns the connection. The
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// model Passkey {
|
|
16
|
-
// id String @id // the credential id, base64url
|
|
17
|
-
// publicKey String // base64url
|
|
18
|
-
// counter Int
|
|
19
|
-
// transports String[]
|
|
20
|
-
// createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
21
|
-
// }
|
|
22
|
-
//
|
|
23
|
-
// model PasskeyToken {
|
|
24
|
-
// hash String @id // sha-256 of the token, base64url
|
|
25
|
-
// kind String // session | invite | challenge
|
|
26
|
-
// expiresAt DateTime @db.Timestamptz(3)
|
|
27
|
-
// }
|
|
13
|
+
// two tables are Prisma models shipped in `prisma/passkey.prisma`, which the
|
|
14
|
+
// `prisma-models` bin copies into the app's schema folder.
|
|
28
15
|
//
|
|
29
16
|
// Every token is stored as its hash, so a read of the table opens nothing. A
|
|
30
17
|
// challenge is spent the moment a response quotes it, and an invite the moment
|
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
|
*
|