wiki-formant 0.86.0 → 0.86.2

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/dist/knip.js CHANGED
@@ -1,31 +1,32 @@
1
- // knip.ts — the dead-code config every app here shares. An app's own knip.ts
2
- // spreads it and appends what only it has: `{ ...base, ignoreBinaries:
3
- // [...base.ignoreBinaries, 'ffmpeg'] }`. Each had kept a copy, and the copies
4
- // drifted on the one line that matters most: a glob that does not name
5
- // `.well-known` skips a dotted directory and every agent-surface route in it.
6
- const entry = [
7
- // What the App Router loads by convention and nothing imports.
8
- 'src/app/**/page.tsx',
9
- 'src/app/**/layout.tsx',
10
- 'src/app/**/route.{ts,tsx}',
11
- 'src/app/**/{error,not-found,loading,global-error}.tsx',
12
- 'src/app/.well-known/**/route.ts',
13
- 'src/app/**/{sitemap,robots,manifest,opengraph-image,icon,apple-icon}.{ts,tsx}',
14
- 'src/proxy.ts',
15
- // A module exporting for its test alone reads as dead without the test.
16
- 'src/**/*.test.ts',
17
- // The routines run these from the command line, and they reach into `src`.
18
- 'scripts/**/*.{ts,mts,mjs}',
19
- 'studio/**/*.mjs',
20
- ];
21
- export default {
22
- entry,
1
+ // knip.ts — the knip configuration every app here shares: what the App
2
+ // Router loads by convention and nothing imports, the scripts the routines
3
+ // run from the command line, and the two dependencies no TS import names.
4
+ // An app's `knip.ts` spreads it and adds what is its own:
5
+ //
6
+ // import base from 'wiki-formant/knip';
7
+ // export default { ...base, ignoreBinaries: [...base.ignoreBinaries, 'ffmpeg'] };
8
+ const config = {
9
+ $schema: 'https://unpkg.com/knip@6/schema.json',
10
+ entry: [
11
+ 'src/app/**/page.tsx',
12
+ 'src/app/**/layout.tsx',
13
+ 'src/app/**/route.{ts,tsx}',
14
+ 'src/app/**/{error,not-found,loading,global-error}.tsx',
15
+ // A dotted directory: a glob that does not name it skips the agent surface.
16
+ 'src/app/.well-known/**/route.ts',
17
+ 'src/app/**/{sitemap,robots,manifest,opengraph-image,icon,apple-icon}.{ts,tsx}',
18
+ 'src/**/*.test.ts',
19
+ 'scripts/**/*.{ts,mts,mjs}',
20
+ 'studio/**/*.mjs',
21
+ ],
23
22
  project: ['src/**/*.{ts,tsx}', 'scripts/**/*.{ts,mts,mjs}', 'studio/**/*.mjs'],
24
- ignore: ['src/generated/**'],
25
- // The generated Prisma client imports the first; the second is reached through
26
- // `@tailwindcss/postcss` and a stylesheet `@import`, neither an import edge.
27
- ignoreDependencies: ['@prisma/client', 'tailwindcss'],
28
- // The global CLI the scripts' cache invalidation shells out to, kept out of the
29
- // dependencies because it put ~330 packages into every build.
23
+ ignoreDependencies: [
24
+ // Reached through `@tailwindcss/postcss` and the `@import` in globals.css.
25
+ 'tailwindcss',
26
+ // Imported by the generated Prisma client.
27
+ '@prisma/client',
28
+ ],
29
+ // The Vercel CLI the scripts shell out to for a cache tag; a global, never a dependency.
30
30
  ignoreBinaries: ['vercel'],
31
31
  };
32
+ export default config;
@@ -76,51 +76,52 @@ export declare function collectBlockLinks<B extends {
76
76
  }>(blocks: readonly B[], containers?: (block: B) => B[][] | null): BlockLinks;
77
77
  /** `Promise.all` with a ceiling, preserving input order in the results. */
78
78
  export declare function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
79
- /** A page as `auditLinks` reads it: where it lives, its blocks, its metadata. */
79
+ /** A page as the audit reads it: its site path and its stored blocks. */
80
80
  export interface AuditPage {
81
81
  path: string;
82
82
  content: unknown;
83
83
  metadata?: unknown;
84
84
  }
85
- export interface AuditOptions {
86
- /** Every internal path that resolves: pages, category hubs, routes, assets. */
85
+ export interface AuditOptions<B extends {
86
+ type: string;
87
+ }> {
88
+ /** Site-relative paths that exist, with trailing slashes dropped. */
87
89
  livePaths: ReadonlySet<string>;
88
- /** Default `probeExternal`. A wiki with its own special cases wraps it. */
90
+ /** The probe for one external URL. Default `probeExternal`. */
89
91
  probe?: (url: string) => Promise<Probe>;
90
- /**
91
- * Whether an internal path is the wiki's to check. One it is not (an app
92
- * route, say) is kept in `skipped` and never reported broken. Default all.
93
- */
92
+ /** Whether a site-relative link is this checker's to verify. Default every one. */
94
93
  internal?: (path: string) => boolean;
95
- /** Default 12. */
96
94
  concurrency?: number;
95
+ containers?: (block: B) => B[][] | null;
97
96
  }
98
- export type Referenced<T> = T & {
99
- pages: string[];
100
- };
101
97
  export interface LinkAudit {
102
98
  externalChecked: number;
103
- brokenExternal: Referenced<Probe>[];
104
- brokenInternal: Referenced<{
99
+ brokenExternal: Array<Probe & {
100
+ pages: string[];
101
+ }>;
102
+ brokenInternal: Array<{
105
103
  url: string;
106
- }>[];
107
- /** Internal paths `internal` turned away, each with the pages citing it. */
108
- skipped: Map<string, string[]>;
109
- /** Every embed, unprobed: a status check on one proves little on its own. */
104
+ pages: string[];
105
+ }>;
106
+ /** Every embed, each with the pages that carry it, for a checker that probes them. */
110
107
  embeds: Map<string, {
111
108
  kind: string;
112
109
  pages: string[];
113
110
  }>;
111
+ /** Internal links `internal` declined, each with its pages. */
112
+ skipped: Map<string, string[]>;
114
113
  }
115
114
  /**
116
- * Every link on every page, probed once per URL: externals by `probe`,
117
- * internals against `livePaths`. A page's own `/history` and `/edit` resolve
118
- * with it, and a query is a view of the path it rides on. The two checkers had
119
- * each written this walk, and each had one of those rules and not the other.
115
+ * Every link on every page, probed once each, with the pages that carry it.
116
+ * Metadata strings that name a site count as external links: a wiki's facts
117
+ * panel cites its sources there, sometimes as a bare domain. Two checkers had each written this walk; what
118
+ * they still do themselves is decide the live paths, the probe and the report.
120
119
  */
121
- export declare function auditLinks(pages: readonly AuditPage[], opts: AuditOptions): Promise<LinkAudit>;
122
- /** The human summary of an audit's broken links, under the scope it covered. */
123
- export declare function printLinkAudit(audit: Pick<LinkAudit, 'externalChecked' | 'brokenExternal' | 'brokenInternal'>, { scope, pagesScanned }: {
124
- scope: string;
120
+ export declare function auditLinks<B extends {
121
+ type: string;
122
+ }>(pages: readonly AuditPage[], opts: AuditOptions<B>): Promise<LinkAudit>;
123
+ /** The human report's first lines: what was checked, and what is broken. */
124
+ export declare function printLinkAudit(audit: LinkAudit, { scope, pagesScanned }: {
125
+ scope?: string;
125
126
  pagesScanned: number;
126
127
  }): void;
@@ -1,9 +1,9 @@
1
1
  // link-check.ts — the dead-link probe wiki sweep scripts share.
2
2
  //
3
- // Node-only (fetch, AbortSignal, URL). No framework, no database: which pages
4
- // to walk, which paths are live and what else to report stay with the caller,
5
- // because that is the half that is genuinely per-wiki. The walk over them and
6
- // the summary of what broke are `auditLinks` and `printLinkAudit`.
3
+ // Node-only (fetch, AbortSignal, URL). No framework, no database: what a
4
+ // checker does with the verdicts — which pages to walk, what counts as an
5
+ // internal path, how to report — stays with the caller, because that is the
6
+ // half that is genuinely per-wiki.
7
7
  //
8
8
  // EVERY GUARD BELOW WAS PAID FOR BY A FALSE POSITIVE. npmjs.com 403s scripted
9
9
  // requests, an expired certificate is not a dead host, a YouTube /embed/ URL
@@ -321,62 +321,68 @@ export async function mapLimit(items, limit, fn) {
321
321
  await Promise.all(workers);
322
322
  return results;
323
323
  }
324
- // A metadata fact (a website, an X handle's URL) renders as a live link and
325
- // rots like a citation, but lives outside the blocks; a bare domain counts.
324
+ /** A metadata value that names a site: an absolute URL as it is, a bare `www.` or `host.tld` as https. */
326
325
  const metadataUrl = (v) => {
327
326
  const s = typeof v === 'string' ? v.trim() : '';
328
327
  if (/^https?:\/\//i.test(s))
329
328
  return s;
330
329
  return /^www\./i.test(s) || /^[a-z0-9.-]+\.[a-z]{2,}(\/|$)/i.test(s) ? `https://${s}` : null;
331
330
  };
331
+ const push = (map, key, value) => {
332
+ const arr = map.get(key);
333
+ if (arr)
334
+ arr.push(value);
335
+ else
336
+ map.set(key, [value]);
337
+ };
332
338
  /**
333
- * Every link on every page, probed once per URL: externals by `probe`,
334
- * internals against `livePaths`. A page's own `/history` and `/edit` resolve
335
- * with it, and a query is a view of the path it rides on. The two checkers had
336
- * each written this walk, and each had one of those rules and not the other.
339
+ * Every link on every page, probed once each, with the pages that carry it.
340
+ * Metadata strings that name a site count as external links: a wiki's facts
341
+ * panel cites its sources there, sometimes as a bare domain. Two checkers had each written this walk; what
342
+ * they still do themselves is decide the live paths, the probe and the report.
337
343
  */
338
344
  export async function auditLinks(pages, opts) {
339
- const { livePaths, probe = probeExternal, internal = () => true, concurrency = 12 } = opts;
345
+ const { livePaths, probe = probeExternal, internal = () => true, concurrency = 12, containers = coreBlockShape().containers } = opts;
340
346
  const external = new Map();
341
- const internals = new Map();
347
+ const internalLinks = new Map();
342
348
  const skipped = new Map();
343
349
  const embeds = new Map();
344
- const cite = (map, key, page) => {
345
- const refs = map.get(key);
346
- if (!refs)
347
- map.set(key, [page]);
348
- else if (!refs.includes(page))
349
- refs.push(page);
350
- };
351
350
  for (const page of pages) {
352
- const links = collectBlockLinks(Array.isArray(page.content) ? page.content : []);
353
- const meta = page.metadata && typeof page.metadata === 'object' ? Object.values(page.metadata) : [];
354
- for (const url of [...links.external, ...meta.map(metadataUrl).filter((u) => !!u)])
355
- cite(external, url, page.path);
356
- for (const path of links.internal)
357
- cite(internal(path) ? internals : skipped, path, page.path);
358
- for (const { kind, url } of links.embeds) {
359
- const entry = embeds.get(url) ?? { kind, pages: [] };
351
+ const blocks = Array.isArray(page.content) ? page.content : [];
352
+ const links = collectBlockLinks(blocks, containers);
353
+ for (const v of Object.values((page.metadata ?? {}))) {
354
+ const url = metadataUrl(v);
355
+ if (url)
356
+ links.external.push(url);
357
+ }
358
+ for (const u of new Set(links.external))
359
+ push(external, u, page.path);
360
+ for (const u of new Set(links.internal))
361
+ push(internal(u) ? internalLinks : skipped, u, page.path);
362
+ for (const e of links.embeds) {
363
+ const entry = embeds.get(e.url) ?? { kind: e.kind, pages: [] };
360
364
  if (!entry.pages.includes(page.path))
361
365
  entry.pages.push(page.path);
362
- embeds.set(url, entry);
366
+ embeds.set(e.url, entry);
363
367
  }
364
368
  }
365
- const results = await mapLimit([...external.keys()], concurrency, url => probe(url));
366
- const resolves = (url) => {
367
- const path = url.split('?')[0];
368
- return livePaths.has(path) || livePaths.has(path.replace(/\/(history|edit)$/, ''));
369
- };
369
+ const urls = [...external.keys()];
370
+ const results = await mapLimit(urls, concurrency, url => probe(url));
370
371
  return {
371
- externalChecked: external.size,
372
+ externalChecked: urls.length,
372
373
  brokenExternal: results.filter(r => !r.ok).map(r => ({ ...r, pages: external.get(r.url) })),
373
- brokenInternal: [...internals].filter(([url]) => !resolves(url)).map(([url, refs]) => ({ url, pages: refs })),
374
- skipped,
374
+ brokenInternal: [...internalLinks.entries()]
375
+ .filter(([u]) => {
376
+ const p = u.split('?')[0];
377
+ return !livePaths.has(p) && !livePaths.has(p.replace(/\/(history|edit)$/, ''));
378
+ })
379
+ .map(([url, pages]) => ({ url, pages })),
375
380
  embeds,
381
+ skipped,
376
382
  };
377
383
  }
378
- /** The human summary of an audit's broken links, under the scope it covered. */
379
- export function printLinkAudit(audit, { scope, pagesScanned }) {
384
+ /** The human report's first lines: what was checked, and what is broken. */
385
+ export function printLinkAudit(audit, { scope = '(entire wiki)', pagesScanned }) {
380
386
  console.log(`\nLink check — scope: ${scope}`);
381
387
  console.log(`Pages scanned: ${pagesScanned} | external links checked: ${audit.externalChecked}`);
382
388
  console.log(`\nBroken external (${audit.brokenExternal.length}):`);
package/dist/passkey.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface PasskeyGate {
14
14
  cookie: string;
15
15
  /** Whether a session cookie's value is a live session whose passkey is still saved. */
16
16
  signedIn(session: string | undefined): Promise<boolean>;
17
- /** Whether a request carries a live session: `signedIn` over its cookie, for a write route. */
17
+ /** `signedIn` for a request's own cookie: the check a write route makes. */
18
18
  allows(request: Request): Promise<boolean>;
19
19
  /** A single-use invite token, good for `days`. The bin turns it into a link. */
20
20
  invite(days?: number): Promise<string>;
package/dist/passkey.js CHANGED
@@ -54,17 +54,20 @@ export function createPasskeyGate({ sql, rpName, cookie = 'passkey_session', ses
54
54
  const value = await issue(sql, 'session', sessionDays * DAY, undefined, passkeyId);
55
55
  return setCookie(value, sessionDays * 86_400, secure);
56
56
  }
57
- const sessionOf = (request) => request.headers.get('cookie')?.split(/;\s*/).find(c => c.startsWith(`${cookie}=`))?.slice(cookie.length + 1);
58
- const signedIn = async (value) => !!value &&
59
- (await sql(`SELECT 1 FROM "PasskeyToken" t JOIN "Passkey" p ON p.id = t."passkeyId"
60
- WHERE t.hash = $1 AND t.kind = 'session' AND t."expiresAt" > now()`, await hash(value))).length > 0;
61
- return {
57
+ const cookieOf = (request) => request.headers
58
+ .get('cookie')
59
+ ?.split(/;\s*/)
60
+ .find(c => c.startsWith(`${cookie}=`))
61
+ ?.slice(cookie.length + 1);
62
+ const gate = {
62
63
  cookie,
63
- signedIn,
64
- allows: request => signedIn(sessionOf(request)),
64
+ allows: request => gate.signedIn(cookieOf(request)),
65
+ signedIn: async (value) => !!value &&
66
+ (await sql(`SELECT 1 FROM "PasskeyToken" t JOIN "Passkey" p ON p.id = t."passkeyId"
67
+ WHERE t.hash = $1 AND t.kind = 'session' AND t."expiresAt" > now()`, await hash(value))).length > 0,
65
68
  invite: (days = 1) => issue(sql, 'invite', days * DAY),
66
69
  async signOut(request) {
67
- const value = sessionOf(request);
70
+ const value = cookieOf(request);
68
71
  if (value)
69
72
  await spend(sql, 'session', value);
70
73
  return setCookie('', 0, new URL(request.url).protocol === 'https:');
@@ -131,4 +134,5 @@ export function createPasskeyGate({ sql, rpName, cookie = 'passkey_session', ses
131
134
  return session(passkey.id, protocol === 'https:');
132
135
  },
133
136
  };
137
+ return gate;
134
138
  }
package/dist/scripts.d.ts CHANGED
@@ -1,33 +1,20 @@
1
- import type { BannerVariant } from './text.js';
1
+ import { type SaveInput, type WikiPageRow, type WikiStore } from './store.js';
2
2
  import { type ChangeType } from './versioning.js';
3
- import { type PageMetadata, type WikiPageRow } from './store.js';
4
- /** The slice of a `pg` client every helper here takes. Prisma fits it with `$queryRawUnsafe`. */
5
- export interface ScriptClient {
6
- query(text: string, values?: unknown[]): Promise<{
7
- rows: unknown[];
8
- }>;
9
- }
10
- type Block = {
11
- id: string;
12
- type: string;
13
- };
3
+ import type { BannerVariant } from './text.js';
14
4
  export declare const uid: () => string;
15
- export declare const content: (html: string) => {
5
+ export declare const content: (text: string) => {
16
6
  id: string;
17
7
  type: "content";
18
8
  text: string;
19
9
  };
20
- /**
21
- * A notice. Always pass `text`: the variant's standard message lives in the
22
- * renderer, not the row, so a banner without it is invisible to search.
23
- */
10
+ /** Always pass `text`: the renderer's default message lives in React, and search indexes only what the row holds. */
24
11
  export declare const banner: (variant: BannerVariant, text: string) => {
25
12
  id: string;
26
13
  type: "banner";
27
14
  variant: BannerVariant;
28
15
  text: string;
29
16
  };
30
- /** A facts panel leading an article: `table` is the inner `<table>` HTML. */
17
+ /** A facts panel that leads an article: `table` is the inner `<table>` HTML. */
31
18
  export declare const infobox: (table: string) => {
32
19
  id: string;
33
20
  type: "infobox";
@@ -38,152 +25,72 @@ export declare const infobox: (table: string) => {
38
25
  }[];
39
26
  };
40
27
  /**
41
- * Throw on a link-grid link or reference list whose URL fields are mis-shaped.
42
- *
43
- * The app's validator rejects these, and scripts never reach it. The views drop
44
- * an entry whose URL fails rather than rendering it inert, so a `{ label, url }`
45
- * link (the shape is `{ label, href }`) shows as a heading with no pills and
46
- * nothing reports it. A throw inside `withClient` is a rollback and a failed run.
47
- */
48
- export declare function assertLinkShapes(blocks: unknown, where?: string): void;
49
- /**
50
- * Mark everything cached under `tag` stale on the project `cwd` is linked to.
51
- * A script's write reaches no route that would bust it, and pages hold a day.
28
+ * Refuse block content whose URL-bearing leaves are mis-shaped, before it
29
+ * reaches the database. `LinkGridView` and `ReferencesView` drop an entry whose
30
+ * URL fails `safeLinkHref` rather than rendering it inert, so a one-field slip
31
+ * (`url` for a link, whose shape is `{ label, href }`) shows as a heading with
32
+ * no pills and nothing reports it. Throws, so `withClient` rolls back.
52
33
  */
34
+ export declare function assertLinkShapes(blocks: readonly unknown[], where?: string): void;
35
+ /** `vercel cache invalidate --tag`, reported either way. */
53
36
  export declare function invalidate(tag: string, cwd?: string): void;
54
- /**
55
- * Run `fn` on a connected client, then release it and its pool. A throw rolls
56
- * back the open transaction and fails the process: every script writes to the
57
- * live database, and a half-applied edit must never pass for a success. A run
58
- * that wrote anything ends by invalidating `tag`.
59
- */
60
- export declare function withClient<T>(fn: (client: ScriptClient) => Promise<T>, { tag, cwd }?: {
37
+ /** The slice of a `pg` client a script uses. */
38
+ export interface ScriptClient {
39
+ query(text: string, values?: unknown[]): Promise<{
40
+ rows: unknown[];
41
+ }>;
42
+ }
43
+ export interface WithClientOptions {
44
+ /** The cache tag a write expires. Default `wiki`. */
61
45
  tag?: string;
46
+ /** Where the Vercel project is linked. Default the working directory. */
62
47
  cwd?: string;
63
- }): Promise<T | undefined>;
64
- /** The app's own store over a script's client, so a script writes what the editor writes. */
65
- export declare const storeFor: (client: ScriptClient, siteId?: string) => {
66
- siteId: string;
67
- hidden: string[];
68
- isHidden: (tagPath: string) => boolean;
69
- get: (tagPath: string, slug: string) => Promise<NonNullable<WikiPageRow<Block> & {
70
- revisionCount: number;
71
- }> | null>;
72
- getById: (id: string) => Promise<NonNullable<WikiPageRow<Block> & {
73
- revisionCount: number;
74
- }> | null>;
75
- list: (tagPath: string, { sort, limit, offset }?: {
76
- sort?: import("./store.js").ListSort;
77
- limit?: number;
78
- offset?: number;
79
- }) => Promise<import("./store.js").WikiPageListing[]>;
80
- articles: (tagPath: string, { sort, limit, offset }?: {
81
- sort?: import("./store.js").ListSort;
82
- limit?: number;
83
- offset?: number;
84
- }) => Promise<WikiPageRow<Block>[]>;
85
- subtree: (tagPath: string) => Promise<import("./store.js").WikiPageRef[]>;
86
- recent: ({ tagPath, limit }?: {
87
- tagPath?: string;
88
- limit?: number;
89
- }) => Promise<import("./store.js").WikiPageListing[]>;
90
- byIds: (ids: readonly string[]) => Promise<import("./store.js").WikiPageListing[]>;
91
- refs: () => Promise<Omit<WikiPageRow<unknown>, "content">[]>;
92
- corpus: () => Promise<WikiPageRow<Block>[]>;
93
- branch: ({ tagPath, sort, limit, offset, since }?: {
94
- tagPath?: string;
95
- sort?: import("./store.js").ListSort;
96
- limit?: number;
97
- offset?: number;
98
- since?: Date;
99
- }) => Promise<{
100
- items: {
101
- content: Block[];
102
- title: string;
103
- id: string;
104
- metadata: PageMetadata;
105
- tagPath: string;
106
- slug: string;
107
- siteId: string;
108
- version: string;
109
- authorId: string | null;
110
- lastVerifiedAt: Date | null;
111
- createdAt: Date;
112
- updatedAt: Date;
113
- snippet: string;
114
- }[];
115
- total: number;
116
- }>;
117
- counts: () => Promise<{
118
- tagPath: string;
119
- pages: number;
120
- }[]>;
121
- stats: () => Promise<{
122
- pages: number;
123
- updated: Date | null;
124
- }>;
125
- search: (query: string, { tagPath, skip, take, content: withContent }?: {
126
- tagPath?: string | null;
127
- skip?: number;
128
- take?: number;
129
- content?: boolean;
130
- }) => Promise<{
131
- items: (Omit<WikiPageRow<unknown>, "content"> & {
132
- snippet: string;
133
- } & {
134
- score: number;
135
- content?: Block[] | undefined;
136
- })[];
137
- total: number;
138
- }>;
139
- history: (pageId: string, { limit, offset }?: {
140
- limit?: number;
141
- offset?: number;
142
- }) => Promise<import("./store.js").WikiRevisionEntry[]>;
143
- revision: (pageId: string, revisionId: string) => Promise<import("./store.js").WikiRevisionRow<Block> | null>;
144
- save: (input: import("./store.js").SaveInput<Block>) => Promise<import("./store.js").SaveResult<Block>>;
145
- restore: (tagPath: string, slug: string, revisionId: string, by?: {
146
- authorId?: string | null;
147
- }) => Promise<import("./store.js").SaveResult<Block>>;
148
- remove: (tagPath: string, slug: string) => Promise<boolean>;
149
- verify: (tagPath: string, slug: string) => Promise<boolean>;
48
+ /** Default `DATABASE_URL`, read from the environment or `./.env`. */
49
+ url?: string;
50
+ }
51
+ /**
52
+ * Run `fn` against a connected client, then release the client and the pool.
53
+ * A throw rolls back whatever transaction was open and fails the process:
54
+ * every one of these scripts writes to the live database, so a half-applied
55
+ * edit must never be left behind and never look like a success. A run that
56
+ * wrote anything ends by expiring the wiki's cache tag, because a script
57
+ * bypasses the routes that purge the pages they change.
58
+ */
59
+ export declare function withClient<T>(fn: (client: ScriptClient) => Promise<T>, { tag, cwd, url }?: WithClientOptions): Promise<T | undefined>;
60
+ type AnyBlock = {
61
+ id: string;
62
+ type: string;
150
63
  };
64
+ /** The app's own store over a script's client, so a script writes exactly what the editor writes. */
65
+ export declare const storeFor: (client: ScriptClient, siteId?: string) => WikiStore<AnyBlock>;
151
66
  /** A page row by path, every column: what `writeRevision` takes. Null when there is none. */
152
- export declare const readPage: (client: ScriptClient, tagPath: string, slug: string, siteId?: string) => Promise<NonNullable<WikiPageRow<Block> & {
153
- revisionCount: number;
154
- }> | null>;
67
+ export declare const readPage: (client: ScriptClient, tagPath: string, slug: string, siteId?: string) => Promise<WikiPageRow<AnyBlock> | null>;
155
68
  export interface RevisionOptions {
156
- /** The bump, and the revision's change type. Default minor. */
157
69
  change?: Exclude<ChangeType, 'none'>;
158
- message?: string;
159
- /** Stamp `lastVerifiedAt`: the edit re-checked the page's facts. */
70
+ message?: string | null;
71
+ /** The writer's word that the page's facts were re-checked. */
160
72
  verified?: boolean;
161
- /** Write nothing; return the version it would have been. */
73
+ /** Write nothing; return the version the bump would give. */
162
74
  dry?: boolean;
163
- metadata?: PageMetadata;
75
+ metadata?: Record<string, unknown>;
164
76
  authorId?: string | null;
77
+ siteId?: string;
165
78
  }
166
79
  /**
167
- * Write edited blocks back to a page row from `readPage` as one revision. The
168
- * save names the version it read, so an edit landing in between is refused
169
- * (409) rather than overwritten. Returns the new version.
80
+ * Write edited blocks back to a page from `readPage` as one revision, through
81
+ * the store: the page and its revision in one statement, the version bumped
82
+ * from the stored one by `change`. The save names the version it read, so an
83
+ * edit landing in between is refused (409) instead of overwritten. Returns the
84
+ * new version.
170
85
  */
171
- export declare function writeRevision(client: ScriptClient, page: Pick<WikiPageRow, 'tagPath' | 'slug' | 'title' | 'version' | 'siteId'>, blocks: Block[], { change, message, verified, dry, metadata, authorId }?: RevisionOptions): Promise<string>;
86
+ export declare function writeRevision(client: ScriptClient, page: WikiPageRow<AnyBlock>, blocks: AnyBlock[], { change, message, verified, dry, metadata, authorId, siteId }?: RevisionOptions): Promise<string>;
172
87
  /**
173
- * Create a page or update it in place, keyed by site, tag path and slug. A
174
- * change to its title or blocks writes a revision; a re-run that changes
175
- * nothing writes none. `change` records that bump instead of the diff's, and
176
- * always writes one.
88
+ * Create a page, or replace its title and blocks: a change writes a revision
89
+ * bumped from the page's own version, a re-run that changes nothing writes
90
+ * none, and a stated `change` always records one.
177
91
  */
178
- export declare function upsertWikiPage(client: ScriptClient, input: {
179
- tagPath: string;
180
- slug: string;
181
- title: string;
182
- content: Block[];
183
- metadata?: PageMetadata;
184
- message?: string;
185
- change?: Exclude<ChangeType, 'none'>;
186
- authorId?: string | null;
92
+ export declare function upsertWikiPage(client: ScriptClient, input: SaveInput<AnyBlock> & {
93
+ content: AnyBlock[];
187
94
  siteId?: string;
188
95
  }): Promise<'created' | 'updated'>;
189
96
  export {};