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/upload.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// upload.ts — the route an editor's image upload posts to.
|
|
2
|
+
//
|
|
3
|
+
// radix-wiki and caper had each written it, and their limits had drifted apart:
|
|
4
|
+
// one took 25 MB with no cap on decoded pixels and trusted the client's
|
|
5
|
+
// Content-Type, the other 4 MB, 40 megapixels and the file's own leading bytes.
|
|
6
|
+
// This is the second, with the first one's re-encode as an option.
|
|
7
|
+
//
|
|
8
|
+
// - **The bytes say what the file is.** The Content-Type and the name are the
|
|
9
|
+
// caller's to choose, and trusting them stores arbitrary bytes under an image
|
|
10
|
+
// URL, served with a spoofed type. SVG is not accepted: it is markup.
|
|
11
|
+
// - **Pixels are capped as well as bytes.** A PNG well under the byte limit can
|
|
12
|
+
// decode to gigabytes.
|
|
13
|
+
// - **Every image is re-encoded, never stored as sent.** Re-encoding writes
|
|
14
|
+
// none of the source's metadata, and with it no GPS position from a phone's
|
|
15
|
+
// EXIF; the orientation that metadata carried is applied first.
|
|
16
|
+
// - **Each uploader has a budget**, keyed on who they are and where from.
|
|
17
|
+
//
|
|
18
|
+
// `normalizeImage` is the sniff, the pixel cap and the re-encode without the
|
|
19
|
+
// route, for an app whose images arrive another way: a server action, or a
|
|
20
|
+
// picture copied in from another site.
|
|
21
|
+
//
|
|
22
|
+
// `sharp` and `@vercel/blob` are optional peers. Run the route on Node, never
|
|
23
|
+
// the edge: sharp is native.
|
|
24
|
+
import sharp, {} from 'sharp';
|
|
25
|
+
import { put } from '@vercel/blob';
|
|
26
|
+
import { errors, handleRoute, json } from './http.js';
|
|
27
|
+
import { limitClient } from './rate-limit.js';
|
|
28
|
+
const CONTENT_TYPE = {
|
|
29
|
+
jpg: 'image/jpeg',
|
|
30
|
+
png: 'image/png',
|
|
31
|
+
gif: 'image/gif',
|
|
32
|
+
webp: 'image/webp',
|
|
33
|
+
avif: 'image/avif',
|
|
34
|
+
};
|
|
35
|
+
/** The format the leading bytes declare, or null for anything not an accepted image. */
|
|
36
|
+
function sniffImage(buf) {
|
|
37
|
+
const ascii = (from, to) => String.fromCharCode(...buf.subarray(from, to));
|
|
38
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
39
|
+
return 'jpg';
|
|
40
|
+
if (buf[0] === 0x89 && ascii(1, 4) === 'PNG')
|
|
41
|
+
return 'png';
|
|
42
|
+
if (/^GIF8[79]a$/.test(ascii(0, 6)))
|
|
43
|
+
return 'gif';
|
|
44
|
+
if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP')
|
|
45
|
+
return 'webp';
|
|
46
|
+
if (ascii(4, 8) === 'ftyp' && ['avif', 'avis'].includes(ascii(8, 12)))
|
|
47
|
+
return 'avif';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The image re-encoded upright with no metadata, in the format `encode` chose.
|
|
52
|
+
* Throws, with a message fit to show the uploader, for bytes that are not an
|
|
53
|
+
* accepted image or decode to more than `maxPixels`.
|
|
54
|
+
*/
|
|
55
|
+
export async function normalizeImage(bytes, { maxPixels = 40_000_000, encode = (image, { format }) => ({ image: image.toFormat(format === 'jpg' ? 'jpeg' : format), format }), } = {}) {
|
|
56
|
+
const megapixels = Math.round(maxPixels / 1_000_000);
|
|
57
|
+
const format = sniffImage(bytes);
|
|
58
|
+
if (!format)
|
|
59
|
+
throw new Error('Invalid file. Allowed: JPEG, PNG, GIF, WebP, AVIF');
|
|
60
|
+
try {
|
|
61
|
+
const meta = await sharp(bytes, { limitInputPixels: maxPixels }).metadata();
|
|
62
|
+
const width = meta.width ?? 0;
|
|
63
|
+
const height = meta.pageHeight ?? meta.height ?? 0;
|
|
64
|
+
if (!width || !height)
|
|
65
|
+
throw new Error('no dimensions');
|
|
66
|
+
if (width * height > maxPixels)
|
|
67
|
+
throw new Error('pixel limit');
|
|
68
|
+
const upright = sharp(bytes, { animated: (meta.pages ?? 1) > 1, limitInputPixels: maxPixels }).rotate();
|
|
69
|
+
const encoded = encode(upright, { format, width, height });
|
|
70
|
+
return { buffer: await encoded.image.toBuffer(), format: encoded.format, contentType: CONTENT_TYPE[encoded.format] };
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// Never fall back to the bytes as sent: storing them is what leaks the location.
|
|
74
|
+
throw new Error(e instanceof Error && e.message.includes('pixel limit')
|
|
75
|
+
? `Image too large (max ${megapixels} megapixels)`
|
|
76
|
+
: 'Could not read this image. Save it as PNG or JPEG and try again.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const vercelBlob = async (path, body, contentType) => (await put(path, body, { access: 'public', addRandomSuffix: false, contentType })).url;
|
|
80
|
+
/** The POST handler: a multipart `file` in, `{ url }` out. */
|
|
81
|
+
export function uploadHandler({ authorize, maxBytes = 4 * 1024 * 1024, maxPixels, rateLimit = { capacity: 20, refillPerSec: 0.2 }, encode, prefix = '', store = vercelBlob, }) {
|
|
82
|
+
const megabytes = Math.round((maxBytes / 1024 / 1024) * 10) / 10;
|
|
83
|
+
return request => handleRoute(async () => {
|
|
84
|
+
const who = await authorize(request);
|
|
85
|
+
if (who instanceof Response)
|
|
86
|
+
return who;
|
|
87
|
+
const limit = limitClient(`upload:${who}`, request.headers, rateLimit);
|
|
88
|
+
if (!limit.ok) {
|
|
89
|
+
return errors.tooManyRequests(limit.retryAfterSec, `Too many uploads. Try again in ${limit.retryAfterSec}s.`);
|
|
90
|
+
}
|
|
91
|
+
const file = (await request.formData()).get('file');
|
|
92
|
+
if (!(file instanceof File))
|
|
93
|
+
return errors.badRequest('No file provided');
|
|
94
|
+
if (file.size > maxBytes)
|
|
95
|
+
return errors.badRequest(`File too large (max ${megabytes}MB)`);
|
|
96
|
+
let out;
|
|
97
|
+
try {
|
|
98
|
+
out = await normalizeImage(Buffer.from(await file.arrayBuffer()), { maxPixels, encode });
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return errors.badRequest(e.message);
|
|
102
|
+
}
|
|
103
|
+
const url = await store(`${prefix}${crypto.randomUUID()}.${out.format}`, out.buffer, out.contentType);
|
|
104
|
+
return json({ url });
|
|
105
|
+
}, 'Upload failed');
|
|
106
|
+
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -9,16 +9,10 @@
|
|
|
9
9
|
* better rejected than stored.
|
|
10
10
|
*/
|
|
11
11
|
export declare function safeLinkHref(raw: string | null | undefined): string | null;
|
|
12
|
-
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. */
|
|
13
|
-
export declare const okUrl: (u: unknown) => boolean;
|
|
14
12
|
/** `[{ id, text, url? }]` — the shape a references block stores. */
|
|
15
13
|
export declare function validateReferenceItems(items: unknown): boolean;
|
|
16
14
|
/** `[{ id, heading, links: [{ label, href }] }]` — a link-grid block's groups. */
|
|
17
15
|
export declare function validateLinkGroups(groups: unknown): boolean;
|
|
18
|
-
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. */
|
|
19
|
-
export declare function validateCodeTabs(tabs: unknown): boolean;
|
|
20
|
-
/** `[{ value, label, suffix? }]` — a stats block's cards. */
|
|
21
|
-
export declare function validateStatItems(items: unknown): boolean;
|
|
22
16
|
/**
|
|
23
17
|
* The most rows an author-supplied count may ask for. Every `limit` and every
|
|
24
18
|
* `pageIds` list ends up in a database `take:` or `in:`, so the bound belongs
|
package/dist/validation.js
CHANGED
|
@@ -36,7 +36,7 @@ export function safeLinkHref(raw) {
|
|
|
36
36
|
}
|
|
37
37
|
return href;
|
|
38
38
|
}
|
|
39
|
-
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. */
|
|
39
|
+
/** An author-supplied URL is fine iff it is empty (unset) or resolves safely. @internal */
|
|
40
40
|
export const okUrl = (u) => typeof u === 'string' && (u === '' || safeLinkHref(u) !== null);
|
|
41
41
|
const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
42
42
|
/** `[{ id, text, url? }]` — the shape a references block stores. */
|
|
@@ -56,7 +56,7 @@ export function validateLinkGroups(groups) {
|
|
|
56
56
|
Array.isArray(g.links) &&
|
|
57
57
|
g.links.every(l => isRecord(l) && typeof l.label === 'string' && okUrl(l.href))));
|
|
58
58
|
}
|
|
59
|
-
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. */
|
|
59
|
+
/** `[{ label, language?, code }]` — a code-tabs block's tabs. `code` renders as HTML. @internal */
|
|
60
60
|
export function validateCodeTabs(tabs) {
|
|
61
61
|
return (Array.isArray(tabs) &&
|
|
62
62
|
tabs.every(t => isRecord(t) &&
|
|
@@ -64,7 +64,7 @@ export function validateCodeTabs(tabs) {
|
|
|
64
64
|
typeof t.code === 'string' &&
|
|
65
65
|
(t.language === undefined || typeof t.language === 'string')));
|
|
66
66
|
}
|
|
67
|
-
/** `[{ value, label, suffix? }]` — a stats block's cards. */
|
|
67
|
+
/** `[{ value, label, suffix? }]` — a stats block's cards. @internal */
|
|
68
68
|
export function validateStatItems(items) {
|
|
69
69
|
return (Array.isArray(items) &&
|
|
70
70
|
items.every(s => isRecord(s) &&
|
package/dist/well-known.d.ts
CHANGED
|
@@ -4,25 +4,6 @@ export interface RegistryAuthRecord {
|
|
|
4
4
|
contentType: string;
|
|
5
5
|
cacheControl: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
* The `v=MCPv1` record the official MCP registry fetches from
|
|
9
|
-
* `/.well-known/mcp-registry-auth` to prove domain ownership. `mcp-publisher
|
|
10
|
-
* login http --domain=<host>` checks the key here against the private key
|
|
11
|
-
* signing the login, which is what grants publish rights over the reversed-
|
|
12
|
-
* domain namespace the server's name sits in.
|
|
13
|
-
*
|
|
14
|
-
* Returns null when no key is configured, so the caller can 404 rather than
|
|
15
|
-
* serve a malformed record — a missing key should read as "not configured",
|
|
16
|
-
* not as a verification failure nobody can explain.
|
|
17
|
-
*
|
|
18
|
-
* The key material belongs in an env var rather than the repo: the public half
|
|
19
|
-
* is harmless to serve but pointless to commit, and keeping it out gives the
|
|
20
|
-
* private half an obvious home too (a keychain, never git).
|
|
21
|
-
*
|
|
22
|
-
* openssl genpkey -algorithm Ed25519 -out key.pem
|
|
23
|
-
* openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
|
|
24
|
-
*/
|
|
25
|
-
export declare function registryAuthRecord(publicKey?: string, keyType?: string): RegistryAuthRecord | null;
|
|
26
7
|
/** A day at the edge, a week stale-while-revalidate. A card changes rarely. */
|
|
27
8
|
export declare const AGENT_CARD_CACHE_CONTROL = "public, s-maxage=86400, stale-while-revalidate=604800";
|
|
28
9
|
export interface AgentSkill {
|
|
@@ -86,7 +67,6 @@ export interface ServerManifest {
|
|
|
86
67
|
websiteUrl?: string;
|
|
87
68
|
remotes?: ReadonlyArray<Record<string, unknown>>;
|
|
88
69
|
}
|
|
89
|
-
export declare const SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/draft/server-card.schema.json";
|
|
90
70
|
/**
|
|
91
71
|
* The card served at `<mcp-url>/server-card`.
|
|
92
72
|
*
|
package/dist/well-known.js
CHANGED
|
@@ -29,6 +29,8 @@ import { descriptorResponse } from './http.js';
|
|
|
29
29
|
*
|
|
30
30
|
* openssl genpkey -algorithm Ed25519 -out key.pem
|
|
31
31
|
* openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
32
34
|
*/
|
|
33
35
|
export function registryAuthRecord(publicKey, keyType) {
|
|
34
36
|
if (!publicKey)
|
|
@@ -105,6 +107,7 @@ export function agentCard(config) {
|
|
|
105
107
|
defaultOutputModes: ['text/plain', 'application/json', 'text/markdown'],
|
|
106
108
|
};
|
|
107
109
|
}
|
|
110
|
+
/** @internal */
|
|
108
111
|
export const SERVER_CARD_SCHEMA = 'https://static.modelcontextprotocol.io/schemas/draft/server-card.schema.json';
|
|
109
112
|
/**
|
|
110
113
|
* The card served at `<mcp-url>/server-card`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,6 +82,11 @@
|
|
|
82
82
|
"import": "./dist/passkey-button.js",
|
|
83
83
|
"default": "./dist/passkey-button.js"
|
|
84
84
|
},
|
|
85
|
+
"./rola": {
|
|
86
|
+
"types": "./dist/rola.d.ts",
|
|
87
|
+
"import": "./dist/rola.js",
|
|
88
|
+
"default": "./dist/rola.js"
|
|
89
|
+
},
|
|
85
90
|
"./blocks": {
|
|
86
91
|
"types": "./dist/blocks.d.ts",
|
|
87
92
|
"import": "./dist/blocks.js",
|
|
@@ -92,11 +97,6 @@
|
|
|
92
97
|
"import": "./dist/combobox.js",
|
|
93
98
|
"default": "./dist/combobox.js"
|
|
94
99
|
},
|
|
95
|
-
"./indexnow": {
|
|
96
|
-
"types": "./dist/indexnow.d.ts",
|
|
97
|
-
"import": "./dist/indexnow.js",
|
|
98
|
-
"default": "./dist/indexnow.js"
|
|
99
|
-
},
|
|
100
100
|
"./crawlers": {
|
|
101
101
|
"types": "./dist/crawlers.d.ts",
|
|
102
102
|
"import": "./dist/crawlers.js",
|
|
@@ -223,6 +223,11 @@
|
|
|
223
223
|
"import": "./dist/sanitize.js",
|
|
224
224
|
"default": "./dist/sanitize.js"
|
|
225
225
|
},
|
|
226
|
+
"./upload": {
|
|
227
|
+
"types": "./dist/upload.d.ts",
|
|
228
|
+
"import": "./dist/upload.js",
|
|
229
|
+
"default": "./dist/upload.js"
|
|
230
|
+
},
|
|
226
231
|
"./corpus": {
|
|
227
232
|
"types": "./dist/corpus.d.ts",
|
|
228
233
|
"import": "./dist/corpus.js",
|
|
@@ -246,11 +251,15 @@
|
|
|
246
251
|
"analytics-digest": "bin/analytics-digest.mjs",
|
|
247
252
|
"passkey-invite": "bin/passkey-invite.mjs",
|
|
248
253
|
"fts-ddl": "bin/fts-ddl.mjs",
|
|
249
|
-
"indexnow": "bin/indexnow.mjs"
|
|
254
|
+
"indexnow": "bin/indexnow.mjs",
|
|
255
|
+
"prisma-models": "bin/prisma-models.mjs",
|
|
256
|
+
"mark-verified": "bin/mark-verified.mjs",
|
|
257
|
+
"maintenance-log": "bin/maintenance-log.mjs"
|
|
250
258
|
},
|
|
251
259
|
"files": [
|
|
252
260
|
"dist",
|
|
253
261
|
"bin",
|
|
262
|
+
"prisma",
|
|
254
263
|
"tsconfig.base.json"
|
|
255
264
|
],
|
|
256
265
|
"engines": {
|
|
@@ -262,6 +271,7 @@
|
|
|
262
271
|
"prepare": "git config core.hooksPath .githooks || true"
|
|
263
272
|
},
|
|
264
273
|
"devDependencies": {
|
|
274
|
+
"@radixdlt/rola": "^2.1.0",
|
|
265
275
|
"@simplewebauthn/browser": "^14.0.0",
|
|
266
276
|
"@simplewebauthn/server": "^14.0.3",
|
|
267
277
|
"@tiptap/core": "^2.10.0",
|
|
@@ -280,11 +290,14 @@
|
|
|
280
290
|
"@types/node": "^22.0.0",
|
|
281
291
|
"@types/react": "^19.0.0",
|
|
282
292
|
"@types/sanitize-html": "^2.16.1",
|
|
293
|
+
"@vercel/blob": "^2.8.2",
|
|
283
294
|
"fast-diff": "^1.3.0",
|
|
295
|
+
"jose": "^6.2.12",
|
|
284
296
|
"jsdom": "^30.1.0",
|
|
285
297
|
"lightweight-charts": "^5.2.1",
|
|
286
298
|
"react": "^19.2.4",
|
|
287
299
|
"sanitize-html": "^2.17.7",
|
|
300
|
+
"sharp": "^0.35.5",
|
|
288
301
|
"typescript": "^5.9.0"
|
|
289
302
|
},
|
|
290
303
|
"peerDependencies": {
|
|
@@ -304,10 +317,14 @@
|
|
|
304
317
|
"@tiptap/react": ">=2.10",
|
|
305
318
|
"@tiptap/starter-kit": ">=2.10",
|
|
306
319
|
"fast-diff": ">=1.3",
|
|
320
|
+
"lightweight-charts": ">=5",
|
|
307
321
|
"pg": ">=8",
|
|
308
322
|
"react": ">=18",
|
|
309
323
|
"sanitize-html": ">=2",
|
|
310
|
-
"
|
|
324
|
+
"jose": ">=6",
|
|
325
|
+
"@radixdlt/rola": ">=2",
|
|
326
|
+
"sharp": ">=0.33",
|
|
327
|
+
"@vercel/blob": ">=2"
|
|
311
328
|
},
|
|
312
329
|
"peerDependenciesMeta": {
|
|
313
330
|
"react": {
|
|
@@ -369,6 +386,18 @@
|
|
|
369
386
|
},
|
|
370
387
|
"lightweight-charts": {
|
|
371
388
|
"optional": true
|
|
389
|
+
},
|
|
390
|
+
"jose": {
|
|
391
|
+
"optional": true
|
|
392
|
+
},
|
|
393
|
+
"@radixdlt/rola": {
|
|
394
|
+
"optional": true
|
|
395
|
+
},
|
|
396
|
+
"sharp": {
|
|
397
|
+
"optional": true
|
|
398
|
+
},
|
|
399
|
+
"@vercel/blob": {
|
|
400
|
+
"optional": true
|
|
372
401
|
}
|
|
373
402
|
}
|
|
374
403
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shipped by wiki-formant (`npx prisma-models`). Edit it there, not here:
|
|
2
|
+
// `prisma-models --check` fails on a copy that has drifted.
|
|
3
|
+
//
|
|
4
|
+
// Page views and events, counted by wiki-formant/analytics. Its SQL names these
|
|
5
|
+
// tables as written, so they carry no @@map. A visitor is a hash under the day's
|
|
6
|
+
// Salt, which is replaced daily; rows older than 400 days are deleted. `siteId`
|
|
7
|
+
// is "" in an app that serves one site.
|
|
8
|
+
|
|
9
|
+
model View {
|
|
10
|
+
id BigInt @id @default(autoincrement())
|
|
11
|
+
siteId String @default("")
|
|
12
|
+
path String
|
|
13
|
+
source String?
|
|
14
|
+
country String?
|
|
15
|
+
device String
|
|
16
|
+
visitor String
|
|
17
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
18
|
+
|
|
19
|
+
@@index([siteId, createdAt])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
model Event {
|
|
23
|
+
id BigInt @id @default(autoincrement())
|
|
24
|
+
siteId String @default("")
|
|
25
|
+
name String
|
|
26
|
+
path String?
|
|
27
|
+
props Json @default("{}")
|
|
28
|
+
visitor String?
|
|
29
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
30
|
+
|
|
31
|
+
@@index([siteId, name, createdAt])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
model Salt {
|
|
35
|
+
day String @id
|
|
36
|
+
value String
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Days a site counted somewhere else before it counted its own, one row a day
|
|
40
|
+
// with each breakdown as { value: visitors }. Nothing writes or deletes it:
|
|
41
|
+
// the app loads it once, and digest adds the days in a window to the views.
|
|
42
|
+
model ViewDay {
|
|
43
|
+
siteId String @default("")
|
|
44
|
+
day DateTime @db.Date
|
|
45
|
+
visitors Int
|
|
46
|
+
pageviews Int
|
|
47
|
+
visits Int
|
|
48
|
+
bounces Int
|
|
49
|
+
seconds Int
|
|
50
|
+
pages Json @default("{}")
|
|
51
|
+
entryPages Json @default("{}")
|
|
52
|
+
sources Json @default("{}")
|
|
53
|
+
countries Json @default("{}")
|
|
54
|
+
devices Json @default("{}")
|
|
55
|
+
|
|
56
|
+
@@id([siteId, day])
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shipped by wiki-formant (`npx prisma-models`). Edit it there, not here:
|
|
2
|
+
// `prisma-models --check` fails on a copy that has drifted.
|
|
3
|
+
//
|
|
4
|
+
// The passkey door to a site's admin pages. wiki-formant/passkey owns the SQL
|
|
5
|
+
// and names these tables as written, so they carry no @@map. Every token is
|
|
6
|
+
// stored as its hash; `npx passkey-invite <url>` prints the link that saves a
|
|
7
|
+
// passkey.
|
|
8
|
+
|
|
9
|
+
model Passkey {
|
|
10
|
+
id String @id // the credential id, base64url
|
|
11
|
+
publicKey String // base64url
|
|
12
|
+
counter Int
|
|
13
|
+
transports String[]
|
|
14
|
+
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model PasskeyToken {
|
|
18
|
+
hash String @id // sha-256 of the token, base64url
|
|
19
|
+
kind String // session | invite | challenge
|
|
20
|
+
expiresAt DateTime @db.Timestamptz(3)
|
|
21
|
+
}
|