@crowi/plugin-api 1.0.0-alpha.3 → 1.0.0-alpha.5

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/index.d.mts CHANGED
@@ -206,6 +206,66 @@ interface PluginLogger {
206
206
  error(message: string, ...args: unknown[]): void;
207
207
  }
208
208
 
209
+ /**
210
+ * Domain events emitted by core. The full event payload shapes live in
211
+ * `@crowi/server`; this contract publishes only the event names so the
212
+ * type signature of `EventBus.on` stays type-safe at the plugin layer.
213
+ *
214
+ * `pluginHooks` are the v2.0 internal-use-only events. Community
215
+ * plugins should NOT subscribe — the surface is reserved while we
216
+ * stabilise it.
217
+ */
218
+ interface PluginEvents {
219
+ 'page:created': {
220
+ pageId: string;
221
+ path: string;
222
+ };
223
+ 'page:updated': {
224
+ pageId: string;
225
+ path: string;
226
+ };
227
+ 'page:deleted': {
228
+ pageId: string;
229
+ path: string;
230
+ };
231
+ 'page:renamed': {
232
+ pageId: string;
233
+ oldPath: string;
234
+ newPath: string;
235
+ };
236
+ 'comment:added': {
237
+ pageId: string;
238
+ commentId: string;
239
+ };
240
+ 'comment:removed': {
241
+ pageId: string;
242
+ commentId: string;
243
+ };
244
+ 'user:registered': {
245
+ userId: string;
246
+ };
247
+ 'user:activated': {
248
+ userId: string;
249
+ };
250
+ }
251
+ interface EventBus {
252
+ on<K extends keyof PluginEvents>(event: K, listener: (payload: PluginEvents[K]) => void | Promise<void>): void;
253
+ }
254
+
255
+ /**
256
+ * HTML-emitting helper for renderer plugins.
257
+ *
258
+ * A renderer plugin that builds HTML from author-controlled or external
259
+ * strings (an OGP title, a math error message, …) must escape them, and
260
+ * that escape is a security primitive — a hardening change has to reach
261
+ * every plugin at once, not whichever local copies someone remembers.
262
+ * This is the SDK's single copy (`@crowi/plugin-renderer-katex` and
263
+ * `@crowi/plugin-renderer-link-card` each carried an identical local one
264
+ * before it was hoisted here).
265
+ */
266
+ /** Escape `&` `<` `>` `"` `'` for interpolation into HTML text or double/single-quoted attribute values. */
267
+ declare function escapeHtml(s: string): string;
268
+
209
269
  /**
210
270
  * Metadata accompanying a `put`. The runtime always provides
211
271
  * `contentType`; drivers are free to store additional fields under
@@ -417,8 +477,8 @@ type AuthVerifyResult = {
417
477
  * Auth provider driver. The login screen asks core for the list of
418
478
  * registered drivers and renders one button per driver
419
479
  * (`Sign in with Google`). Clicking redirects through the plugin's
420
- * registered routes (`/api/v2/plugins/<name>/oauth/start`); the
421
- * provider redirects back to `/api/v2/plugins/<name>/oauth/callback`,
480
+ * registered routes (`/api/plugins/<name>/oauth/start`); the
481
+ * provider redirects back to `/api/plugins/<name>/oauth/callback`,
422
482
  * which the plugin's contract handles.
423
483
  *
424
484
  * `verify` is the bridge: given whatever the plugin pulled out of the
@@ -564,52 +624,6 @@ interface MailSenderRegistry {
564
624
  register(driverName: string, driver: MailSender): void;
565
625
  }
566
626
 
567
- /**
568
- * Domain events emitted by core. The full event payload shapes live in
569
- * `@crowi/server`; this contract publishes only the event names so the
570
- * type signature of `EventBus.on` stays type-safe at the plugin layer.
571
- *
572
- * `pluginHooks` are the v2.0 internal-use-only events. Community
573
- * plugins should NOT subscribe — the surface is reserved while we
574
- * stabilise it.
575
- */
576
- interface PluginEvents {
577
- 'page:created': {
578
- pageId: string;
579
- path: string;
580
- };
581
- 'page:updated': {
582
- pageId: string;
583
- path: string;
584
- };
585
- 'page:deleted': {
586
- pageId: string;
587
- path: string;
588
- };
589
- 'page:renamed': {
590
- pageId: string;
591
- oldPath: string;
592
- newPath: string;
593
- };
594
- 'comment:added': {
595
- pageId: string;
596
- commentId: string;
597
- };
598
- 'comment:removed': {
599
- pageId: string;
600
- commentId: string;
601
- };
602
- 'user:registered': {
603
- userId: string;
604
- };
605
- 'user:activated': {
606
- userId: string;
607
- };
608
- }
609
- interface EventBus {
610
- on<K extends keyof PluginEvents>(event: K, listener: (payload: PluginEvents[K]) => void | Promise<void>): void;
611
- }
612
-
613
627
  /**
614
628
  * Renderer extension contract — type-only. Plugins contribute parse /
615
629
  * transform behaviour to the server-side markdown pipeline through
@@ -691,9 +705,64 @@ interface CodeBlockRenderer {
691
705
  * strip comments, etc.
692
706
  */
693
707
  computeEmbedKey?(info: CodeBlockInfo): string;
708
+ /**
709
+ * Opt into a CPU-bounded admission control pool (`render-admission.ts`,
710
+ * `packages/api/src/renderer/core/render-admission.ts`). When present,
711
+ * `cachedRenderOrPending` (`packages/api/src/renderer/cache/index.ts`)
712
+ * and `renderCodeBlockForPreview` acquire a ticket from the named pool
713
+ * (keyed per `pluginName`) immediately around the `render()` call and
714
+ * release it on completion; when absent (the default — PlantUML /
715
+ * KaTeX / emoji and existing embed/URL-expansion plugins), `render()`
716
+ * is called directly with no admission gate, matching today's
717
+ * behaviour. See spec §6 for the full design (global / per-user
718
+ * concurrency caps + priority queue).
719
+ */
720
+ admissionControl?: AdmissionControlConfig;
721
+ /**
722
+ * Opt into server-rendering during editor live preview
723
+ * (`POST /pages/preview`, which runs with no `pageId`). Default
724
+ * `'source'` (omitted) leaves the fenced block untouched in preview —
725
+ * today's behaviour for every existing `CodeBlockRenderer`. Plugins
726
+ * that declare `'server-render'` MUST be no-I/O and deterministic:
727
+ * `makePreviewCodeBlockDispatch` calls them outside the persisted-
728
+ * cache path (`packages/api/src/renderer/core/code-block-dispatch.ts`).
729
+ */
730
+ previewPolicy?: 'source' | 'server-render';
694
731
  /** Render a single code block. */
695
732
  render(info: CodeBlockInfo, ctx: RenderContext): EmbedFragment | RenderResult | Promise<EmbedFragment | RenderResult>;
696
733
  }
734
+ /**
735
+ * Per-`pluginName` admission-control pool declaration (§6). Shared by
736
+ * `CodeBlockRenderer` and `EmbedRenderer` — `EmbedRenderer` carries it so
737
+ * `code-block-dispatch.ts`'s `codeBlockAsEmbedRenderer` adaptor can copy
738
+ * `CodeBlockRenderer.admissionControl` straight through to the
739
+ * `EmbedRenderer` shape `cachedRenderOrPending` actually consumes.
740
+ */
741
+ interface AdmissionControlConfig {
742
+ /** Process-wide concurrent `render()` calls in flight for this plugin. */
743
+ maxConcurrentGlobal: number;
744
+ /** Concurrent `render()` calls in flight for a single `actor` (kind:'user' only). */
745
+ maxConcurrentPerUser: number;
746
+ /** Max jobs allowed to wait for a slot before new requests are rejected outright. */
747
+ queueDepth: number;
748
+ }
749
+ /**
750
+ * Who is driving this render call. Threaded through `RenderContext` so
751
+ * admission control (§6) can apply a per-user concurrency cap. Today
752
+ * every real call site is authenticated (`createJwtAuth` has no
753
+ * anonymous fallback), so `'user'` is the only variant actually
754
+ * produced — `'anonymous'` / `'system'` are reserved for future
755
+ * unauthenticated-read and offline-tooling call sites and must not be
756
+ * synthesised speculatively.
757
+ */
758
+ type RenderActor = {
759
+ kind: 'user';
760
+ userId: string;
761
+ } | {
762
+ kind: 'anonymous';
763
+ } | {
764
+ kind: 'system';
765
+ };
697
766
  interface CodeBlockInfo {
698
767
  /** The language tag from the fence (the `ts` in ```` ```ts ````). */
699
768
  lang: string;
@@ -737,6 +806,41 @@ interface EmbedRenderer {
737
806
  * volatility (`?utm_*`) or (b) include external state (Accept-Language).
738
807
  */
739
808
  computeEmbedKey?(input: EmbedInput): string;
809
+ /**
810
+ * Opt into admission control (§6). See `CodeBlockRenderer.admissionControl`
811
+ * for the full rationale — declared here too because `cachedRenderOrPending`
812
+ * (`packages/api/src/renderer/cache/index.ts`) is written against the
813
+ * `EmbedRenderer` shape (`code-block-dispatch.ts`'s
814
+ * `codeBlockAsEmbedRenderer` adaptor copies a `CodeBlockRenderer`'s
815
+ * declaration through unchanged). Native `EmbedRenderer` plugins
816
+ * (embed-tags / URL-inline-expansion) can also opt in if a future one
817
+ * turns out to be CPU-bound.
818
+ */
819
+ admissionControl?: AdmissionControlConfig;
820
+ /**
821
+ * Optional per-dispatch cache-bypass predicate
822
+ * (feature-renderer-plugin-boundary Phase 3). Checked by the generic
823
+ * embed-tag dispatcher (`packages/api/src/renderer/core/embed-tags.ts`)
824
+ * BEFORE it touches `CacheStorage` at all for this dispatch — no
825
+ * `get`, no `set`. When it returns `true`, the dispatcher calls
826
+ * `render()` directly (via the same `normalizeRenderResult` error
827
+ * normalisation the preview path uses) and never persists the
828
+ * result.
829
+ *
830
+ * This exists because a renderer whose behaviour is gated by a
831
+ * runtime policy toggle (e.g. link-card's admin
832
+ * `security:linkCardEnabled` switch) cannot enforce a literal
833
+ * zero-cache-access guarantee by checking the toggle only inside
834
+ * `render()` — a cache HIT from before the toggle flipped would
835
+ * short-circuit `render()` entirely and keep serving pre-toggle
836
+ * output (and, symmetrically, writing a toggled-off result to the
837
+ * cache would keep serving it for up to that entry's TTL after the
838
+ * toggle flips back). Declaring the check here instead makes the
839
+ * dispatcher skip the cache outright for that one call. Absent (the
840
+ * default) or returning `false` goes through the normal cached path
841
+ * unchanged.
842
+ */
843
+ shouldBypassCache?(input: EmbedInput): boolean;
740
844
  /** Render a single embed. */
741
845
  render(input: EmbedInput, ctx: RenderContext): RenderResult | Promise<RenderResult>;
742
846
  /**
@@ -768,9 +872,36 @@ interface EmbedInput {
768
872
  * background-refresh window (see
769
873
  * `packages/api/src/renderer/cache/index.ts:cachedRender`).
770
874
  */
875
+ /**
876
+ * RFC-0023 (design doc §12) — the structured (typed) counterpart of a
877
+ * producer's `html` output. Additive and optional everywhere: a plugin
878
+ * that never sets it keeps today's behaviour byte-for-byte.
879
+ *
880
+ * `node` is the producer-shaped typed node (`type` selects the sidecar
881
+ * kind — `'crowiDiagram'` / `'crowiLinkCard'` / `'crowiPlaceholder'`).
882
+ * Deliberately loose (`Record<string, unknown>`) at this SDK layer:
883
+ * `@crowi/plugin-api` does not depend on `@crowi/api-contract`, so the
884
+ * authoritative shape lives in the api-contract sidecar schemas and the
885
+ * api-side dispatch mapper validates against them before stamping a
886
+ * sidecar onto the persisted AST (invalid payloads degrade to a plain
887
+ * `html` node, never poisoning what the web reads).
888
+ */
889
+ interface StructuredRenderPayload {
890
+ node: Record<string, unknown>;
891
+ }
771
892
  interface RenderResult {
772
- /** Already-sanitised HTML the core will inline. */
893
+ /** Already-sanitised HTML the core will inline. Unchanged — the one and only web/legacy representation. */
773
894
  html: string;
895
+ /**
896
+ * RFC-0023 — optional structured payload paired with `html`. Both
897
+ * must describe the SAME render outcome: the dispatch layer stamps
898
+ * this (schema-validated) as a sidecar on the `html` node it splices,
899
+ * and the `X-Crowi-Ast-Version: 1` projection turns it into a typed
900
+ * node. On an `error` result, pair it with `errorHtml` when the
901
+ * error display carries real content (e.g. link-card's fallback
902
+ * card); leave it unset to get the generic structured placeholder.
903
+ */
904
+ structured?: StructuredRenderPayload;
774
905
  /**
775
906
  * Optional `<head>`-bound assets — Phase 4 records them on the
776
907
  * cache entry but the SSR layer does not yet inject them. Phase 7
@@ -790,19 +921,43 @@ interface RenderResult {
790
921
  ttlSec?: number;
791
922
  /**
792
923
  * When the render failed (network / auth / not_found / rate_limit /
793
- * timeout / unknown), plugins should set `error` instead of building
794
- * an html error frame. The core caches the error using `RENDER_ERROR_TTL`
795
- * and substitutes a fixed placeholder when re-rendering the page.
924
+ * timeout / unknown / blocked), plugins should set `error` instead of
925
+ * building an html error frame. The core caches the error using
926
+ * `RENDER_ERROR_TTL` and, absent `errorHtml`, substitutes a fixed
927
+ * placeholder when re-rendering the page.
796
928
  */
797
929
  error?: RenderError;
930
+ /**
931
+ * Optional failure-display HTML, paired with `error`. When `error` is
932
+ * set and `errorHtml` is present, the core shows `errorHtml` instead of
933
+ * the generic `errorPlaceholder()` — e.g. a link-card plugin can keep
934
+ * its URL clickable even when the OGP fetch failed. Same trust
935
+ * contract as `html`: **pre-sanitised, the core does not re-escape it**.
936
+ *
937
+ * Deliberately a separate field rather than "non-empty `html` + `error`
938
+ * means show `html`" — that shape makes a plugin's stray/forgotten
939
+ * `html` leak into the error display by accident. An explicit opt-in
940
+ * field means a plugin that hasn't been updated for `errorHtml` keeps
941
+ * the current safe-by-default behaviour (placeholder).
942
+ *
943
+ * Ignored when `error` is unset.
944
+ */
945
+ errorHtml?: string;
798
946
  }
799
947
  /**
800
948
  * Error categories cached with their own per-code TTLs. See
801
949
  * `packages/api/src/renderer/cache/index.ts:RENDER_ERROR_TTL` for the
802
- * concrete numbers.
950
+ * concrete numbers. `blocked` is a policy-level permanent rejection
951
+ * (SSRF block, disallowed scheme, disallowed content-type) — distinct
952
+ * from `not_found` semantically but sharing its 1h persistent-failure
953
+ * TTL. `busy` is a transient renderer-admission rejection (e.g. a
954
+ * shared fetch/render concurrency semaphore's wait queue was full, or a
955
+ * queued request's wait deadline elapsed) — never a property of the
956
+ * embed's target, so it shares a short transient TTL with
957
+ * `network`/`timeout` rather than `blocked`'s persistent one.
803
958
  */
804
959
  interface RenderError {
805
- code: 'auth' | 'rate_limit' | 'not_found' | 'network' | 'timeout' | 'unknown';
960
+ code: 'auth' | 'rate_limit' | 'not_found' | 'network' | 'timeout' | 'unknown' | 'blocked' | 'busy';
806
961
  /** Free-form text for log/debug — NOT inlined into the user-facing placeholder. */
807
962
  message?: string;
808
963
  /**
@@ -865,6 +1020,8 @@ type InlineExpansion = {
865
1020
  interface EmbedFragment {
866
1021
  /** Pre-sanitised HTML fragment to inline at the source position. */
867
1022
  html: string;
1023
+ /** RFC-0023 — optional structured payload paired with `html` (see `RenderResult.structured`). */
1024
+ structured?: StructuredRenderPayload;
868
1025
  /** Optional `<head>`-bound assets (CSS / JS) keyed by URL. */
869
1026
  assets?: {
870
1027
  css?: string[];
@@ -895,6 +1052,17 @@ interface CacheEntry {
895
1052
  result: RenderResult;
896
1053
  fetchedAt: Date;
897
1054
  expiresAt: Date;
1055
+ /**
1056
+ * Present ⇔ this is a stale-if-error entry keeping a prior success on
1057
+ * screen (see `packages/api/src/renderer/cache/index.ts:
1058
+ * STALE_IF_ERROR_MAX_AGE_SEC`): the value is that ORIGINAL success's
1059
+ * timestamp, carried forward unchanged across consecutive failed
1060
+ * retries — never the failed attempt's `fetchedAt`. Success entries do
1061
+ * not carry it (their `fetchedAt` IS the last-good time; readers use
1062
+ * that directly, which also covers, value-identically, entries written
1063
+ * while this field was still being set on success).
1064
+ */
1065
+ lastGoodFetchedAt?: Date;
898
1066
  }
899
1067
  /**
900
1068
  * MongoDB-backed cache surface. Phase 4 ships exactly one
@@ -987,6 +1155,25 @@ interface RenderContext {
987
1155
  * encrypted-config-backed implementation.
988
1156
  */
989
1157
  auth?: AuthContext;
1158
+ /**
1159
+ * Who is driving this render. Required so admission control (§6) can
1160
+ * apply its per-user concurrency cap end-to-end — every entry point
1161
+ * (`Renderer.run`/`runMetadata`/`runRender`, `packages/api/src/renderer/
1162
+ * index.ts`) requires callers to supply this. See `RenderActor`'s doc
1163
+ * comment for which variant real call sites actually produce today.
1164
+ */
1165
+ actor: RenderActor;
1166
+ /**
1167
+ * Optional cancellation signal, propagated from the originating HTTP
1168
+ * request (`c.req.raw.signal` on `POST /pages/preview`). A waiting
1169
+ * (not-yet-running) admission-control job is removed from its queue
1170
+ * the instant this fires; an already-running child-process render is
1171
+ * NOT force-killed (§6 — the cost of killing/respawning a worker
1172
+ * outweighs letting an already-cheap render finish and discarding the
1173
+ * result). Absent on the save / read call sites, which have no
1174
+ * request to cancel against.
1175
+ */
1176
+ signal?: AbortSignal;
990
1177
  }
991
1178
  /**
992
1179
  * The registry handed to every plugin's `registerRenderer(scope, ctx)`.
@@ -1002,6 +1189,10 @@ interface RenderContext {
1002
1189
  *
1003
1190
  * Phase 4 stubs (warn-noop):
1004
1191
  * - `addCodeBlockRenderer` (Phase 6 lights this up)
1192
+ *
1193
+ * feature-renderer-plugin-boundary Phase 1 adds `addStylesheet(path)` —
1194
+ * the boot-time CSS-manifest extension point (see that method's own doc
1195
+ * comment).
1005
1196
  */
1006
1197
  interface RendererRegistry {
1007
1198
  /**
@@ -1040,6 +1231,35 @@ interface RendererRegistry {
1040
1231
  * preserved; the first match that returns `'replaced'` wins.
1041
1232
  */
1042
1233
  addUrlInlineExpander(rule: UrlInlineExpansionRule): void;
1234
+ /**
1235
+ * Declare a static CSS asset the plugin needs the browser to load
1236
+ * (e.g. KaTeX's ~30KB math stylesheet). `path` MUST be an
1237
+ * API-relative absolute path confined to the plugin's own
1238
+ * `registerRoutes` namespace — `/api/plugins/<this plugin's
1239
+ * name>/<…>` — the same prefix `PluginRouterScope.route(...)` mounts
1240
+ * that plugin's HTTP routes under. A URL scheme, protocol-relative
1241
+ * `//host`, backslash, `..` traversal segment, or a path outside the
1242
+ * plugin's own namespace all throw synchronously (boot-time reject —
1243
+ * this is not an operator-configurable external URL; see spec
1244
+ * §2.1's "不採用案"). During the `feature-api-v2-path-removal`
1245
+ * migration period the legacy `/api/v2/plugins/<name>/<…>` prefix is
1246
+ * also accepted and silently normalised to the canonical `/api/plugins/`
1247
+ * form before publication — a plugin package that hasn't bumped its own
1248
+ * `addStylesheet(...)` call site yet still gets a working manifest
1249
+ * entry; this dual-accept is transitional, not a permanent alias.
1250
+ *
1251
+ * The call only stages the path in a per-plugin pending set: it is
1252
+ * published to the public `GET /api/app/info` `rendererStylesheets`
1253
+ * manifest ONLY after this plugin's OWN `registerRoutes(scope, ctx)`
1254
+ * completes without throwing (so the manifest never advertises a path
1255
+ * whose route failed to mount). A plugin with no `registerRoutes` at
1256
+ * all, or whose `registerRoutes` throws, never gets its pending
1257
+ * stylesheets committed — dropped wholesale, not partially. Query /
1258
+ * fragment are allowed; duplicate calls with the same path are a
1259
+ * no-op. Call this from `registerRenderer`, not `registerRoutes` —
1260
+ * commit timing depends on this method having already run.
1261
+ */
1262
+ addStylesheet(path: string): void;
1043
1263
  }
1044
1264
 
1045
1265
  /**
@@ -1080,7 +1300,7 @@ interface PluginRouteOptions {
1080
1300
  /**
1081
1301
  * Scope passed to `registerRoutes(scope, ctx)`. Lets a plugin contribute
1082
1302
  * HTTP routes that the runtime mounts at
1083
- * `/api/v2/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1303
+ * `/api/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1084
1304
  * segment guarantees that core endpoints and other plugins cannot
1085
1305
  * collide (RFC-0013 §4).
1086
1306
  *
@@ -1091,9 +1311,9 @@ interface PluginRouteOptions {
1091
1311
  interface PluginRouterScope {
1092
1312
  /**
1093
1313
  * Mount `handler` for `method` at `<path>` under this plugin's
1094
- * namespace. `path` is relative to `/api/v2/plugins/<plugin-name>` and
1314
+ * namespace. `path` is relative to `/api/plugins/<plugin-name>` and
1095
1315
  * should start with `/` (e.g. `route('POST', '/events', handler, {
1096
- * auth: 'public' })` → `POST /api/v2/plugins/<name>/events`).
1316
+ * auth: 'public' })` → `POST /api/plugins/<name>/events`).
1097
1317
  *
1098
1318
  * Pass `{ auth: 'public' }` to bypass Crowi auth entirely for self-
1099
1319
  * authenticating inbound webhooks, `{ auth: 'admin' }` to require
@@ -1267,7 +1487,7 @@ interface CrowiPlugin {
1267
1487
  registerHooks?: (events: EventBus, ctx: PluginContext) => void;
1268
1488
  /**
1269
1489
  * HTTP routes the plugin contributes, mounted at
1270
- * `/api/v2/plugins/<name>/<path>` (the `<name>` path segment guarantees
1490
+ * `/api/plugins/<name>/<path>` (the `<name>` path segment guarantees
1271
1491
  * that core endpoints and other plugins cannot collide). Used for
1272
1492
  * inbound webhooks (Slack events / slash / interactivity), "Test
1273
1493
  * connection" buttons, `@action` targets, OAuth callbacks, etc.
@@ -1347,7 +1567,7 @@ declare const SENSITIVE_FIELD_MARKER = "@sensitive";
1347
1567
  *
1348
1568
  * The admin form renders a button with the given label that calls the
1349
1569
  * plugin's contributed endpoint at the given verb / path (relative to
1350
- * `/api/v2/plugins/<name>/`). Useful for "Test connection",
1570
+ * `/api/plugins/<name>/`). Useful for "Test connection",
1351
1571
  * "Authorise with Google", etc. without forcing every plugin to ship
1352
1572
  * its own React component.
1353
1573
  */
@@ -1367,7 +1587,7 @@ interface ActionAnnotation {
1367
1587
  label: string;
1368
1588
  /** HTTP verb of the plugin endpoint to call. */
1369
1589
  method: PluginRouteMethod;
1370
- /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */
1590
+ /** Path relative to `/api/plugins/<name>/`, with leading slash. */
1371
1591
  path: string;
1372
1592
  }
1373
1593
  /**
@@ -1387,4 +1607,126 @@ interface ActionAnnotation {
1387
1607
  */
1388
1608
  declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
1389
1609
 
1390
- export { ACTION_FIELD_MARKER, type AppInfo, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginEvents, type PluginLogger, type PluginRouteHandler, type PluginRouteMethod, type PluginRouteOptions, type PluginRouterScope, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StateCell, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type UrlInlineExpansionRule, getActionAnnotation, isSensitiveField };
1610
+ /**
1611
+ * Parses a root `<svg>` element's `viewBox` (`minX minY width height`) to
1612
+ * derive intrinsic pixel dimensions. Shared by
1613
+ * `@crowi/plugin-renderer-mermaid` (its original home — the `<img>`
1614
+ * `width`/`height` intrinsic-size fix) and, since RFC-0023,
1615
+ * `@crowi/plugin-renderer-plantuml`'s SVG sidecar path — both need the
1616
+ * same derivation and both already bundle this package, so it lives
1617
+ * here rather than being copied per plugin.
1618
+ *
1619
+ * Reads attributes off the sanitized SVG source string only — never
1620
+ * decodes any `data:` payload.
1621
+ */
1622
+ declare function extractSvgDimensions(svg: string): {
1623
+ width: number;
1624
+ height: number;
1625
+ } | null;
1626
+
1627
+ /**
1628
+ * Renderer-specific knobs for `sanitizeSvg`. The sanitizer itself is a
1629
+ * single shared implementation (`sanitize.ts`) — per-renderer differences
1630
+ * are expressed as parameters here, never as a second copy of the DOM
1631
+ * walk (spec §9: "実装自体をrenderer間で複製しない").
1632
+ */
1633
+ interface SanitizeSvgPolicy {
1634
+ /**
1635
+ * When `true`, `href` / `xlink:href` values pointing at an `https:`
1636
+ * URL are preserved (PlantUML's existing "preserves href to a safe
1637
+ * URL" behaviour, consumed starting Phase 3). When `false`, every
1638
+ * `href` / `xlink:href` is stripped unless it is a local fragment
1639
+ * reference (`#id`) — Mermaid's strict policy (spec §1 layer 1 already
1640
+ * disables Mermaid's own click callbacks, so no link should survive
1641
+ * either).
1642
+ *
1643
+ * Regardless of this flag, `javascript:`, `data:`, and
1644
+ * protocol-relative (`//host/...`) URLs are ALWAYS stripped — no
1645
+ * policy may re-allow those.
1646
+ */
1647
+ allowSafeHref: boolean;
1648
+ }
1649
+
1650
+ type SanitizeSvgResult = {
1651
+ ok: true;
1652
+ svg: string;
1653
+ } | {
1654
+ ok: false;
1655
+ reason: string;
1656
+ };
1657
+ /**
1658
+ * DOM-based SVG sanitizer shared by `@crowi/plugin-renderer-mermaid` and
1659
+ * (from Phase 3) `@crowi/plugin-renderer-plantuml`. Spec §2 layer 2 / §9.
1660
+ *
1661
+ * Design: allowlist-first for elements (unknown/unexpected element names
1662
+ * are dropped with their whole subtree — safer than trying to enumerate
1663
+ * every dangerous tag), then a small set of attribute-level rules that
1664
+ * apply uniformly to every surviving element. This is a from-scratch DOM
1665
+ * walk, not a regex pass (`packages/plugin-renderer-plantuml/src/
1666
+ * sanitize.ts`'s existing implementation is explicitly documented there
1667
+ * as "not a substitute for DOMPurify" — this package is the replacement
1668
+ * both renderers converge on, PlantUML starting Phase 3).
1669
+ *
1670
+ * What gets removed:
1671
+ * - Any element not in `ALLOWED_ELEMENTS` (`script`, `foreignObject`,
1672
+ * `iframe`, `object`, `embed`, SMIL `animate*`/`set`/`discard`, ...) —
1673
+ * dropped together with its entire subtree.
1674
+ * - `on*` event-handler attributes (any casing).
1675
+ * - The `style` attribute (inline styles). Mermaid/PlantUML's real
1676
+ * styling lives in the `<style>` *element* (class-based), which is
1677
+ * sanitized separately below rather than dropped — dropping inline
1678
+ * `style=""` is a deliberate hardening tradeoff (removes a CSS-value
1679
+ * injection vector) the regression tests confirm does not break
1680
+ * either renderer's *structural* output.
1681
+ * - `@import` at-rules and non-local-fragment `url(...)` function
1682
+ * values inside `<style>` element text content (external stylesheet
1683
+ * / font / image loads) — see `sanitizeStyleText` below for why the
1684
+ * element itself is not dropped wholesale.
1685
+ * - `xmlns` / `xmlns:*` declarations on any non-root element (namespace
1686
+ * declarations only ever legitimately live on the root `<svg>`).
1687
+ * - Any root-level `xmlns:*` declaration other than a correctly-bound
1688
+ * `xmlns:xlink` (see `isEssentialRootNamespaceDeclaration`). These are
1689
+ * already functionally inert under the strict unprefixed-SVG-element
1690
+ * invariant enforced elsewhere in this file, but are dropped anyway
1691
+ * as defence-in-depth against relying on that invariant alone.
1692
+ * - `xml:base` on any element (root or descendant). Left in place, it
1693
+ * would silently change the base URI every *local-fragment* `href` /
1694
+ * `xlink:href` / `url(#id)` reference in its subtree resolves
1695
+ * against — turning an in-document `#id` reference into an external
1696
+ * `https://evil.example/#id` fetch some SVG consumers follow,
1697
+ * defeating the local-fragment-only guarantees above even though
1698
+ * every individual `href`/`url()` value still looks safe in
1699
+ * isolation.
1700
+ * - `ProcessingInstruction` nodes anywhere in the tree
1701
+ * (`<?xml-stylesheet ...?>` etc).
1702
+ * - `href` / `xlink:href` values that are not a local fragment
1703
+ * reference (`#id`) and not allowed by `policy.allowSafeHref`.
1704
+ * `javascript:`, `data:`, and protocol-relative (`//...`) values are
1705
+ * ALWAYS stripped regardless of policy.
1706
+ * - `url(...)` references inside SVG *presentation attributes* that
1707
+ * accept a `<FuncIRI>` (`fill`, `stroke`, `filter`, `clip-path`,
1708
+ * `mask`, `cursor`, `marker-start`, `marker-mid`, `marker-end`) when
1709
+ * the reference target is not a local fragment (`#id`) — e.g.
1710
+ * `fill="url(https://evil.example/paint.svg)"` or
1711
+ * `filter="url(data:image/svg+xml;base64,...)"`. These are the same
1712
+ * class of external-resource load as `href`/`style` but reachable via
1713
+ * a different attribute name, so they get the same href-style
1714
+ * drop-the-attribute treatment. `url(#localId)` references (the
1715
+ * normal way Mermaid/PlantUML wire arrowhead markers and gradients)
1716
+ * are always preserved.
1717
+ *
1718
+ * What is explicitly preserved:
1719
+ * - `href` / `xlink:href` local fragment references (`#id`) — legitimate
1720
+ * internal `<use>` / gradient / clip-path wiring.
1721
+ * - `https:` `href` values when `policy.allowSafeHref` is `true`.
1722
+ * - `url(#id)` local fragment references in presentation attributes
1723
+ * (`fill="url(#gradient)"`, `marker-end="url(#arrowhead)"`, ...).
1724
+ *
1725
+ * A parse failure (malformed XML) or a sanitized result whose root is not
1726
+ * a single `<svg>` element both return `{ ok: false }` — callers must
1727
+ * treat that as "invalid output" (spec §2 layer 2), never fall back to
1728
+ * the unsanitized input.
1729
+ */
1730
+ declare function sanitizeSvg(input: string, policy: SanitizeSvgPolicy): SanitizeSvgResult;
1731
+
1732
+ export { ACTION_FIELD_MARKER, type AdmissionControlConfig, type AppInfo, type AuthContext, type AuthDriver, type AuthProfile, type AuthRegistry, type AuthVerifyResult, type CacheEntry, type CacheKey, type CacheStorage, type CodeBlockInfo, type CodeBlockRenderer, type CrowiPlugin, type EmailMessage, type EmbedFragment, type EmbedInput, type EmbedRenderer, type EventBus, type InlineExpansion, type MailSender, type MailSenderRegistry, type NodeRenderer, type NotificationPayload, type NotifierDriver, type NotifierRegistry, type PageMetadataAccessor, type PluginContext, type PluginEvents, type PluginLogger, type PluginRouteHandler, type PluginRouteMethod, type PluginRouteOptions, type PluginRouterScope, type RenderActor, type RenderContext, type RenderError, type RenderPhase, type RenderResult, type RendererRegistry, type Reservation, SENSITIVE_FIELD_MARKER, type SanitizeSvgPolicy, type SanitizeSvgResult, type ScopedCacheStorage, type SearchDriver, type SearchHit, type SearchHits, type SearchPageType, type SearchQuery, type SearchQueryGrants, type SearchQueryViewer, type SearchRegistry, type SearchableDoc, type StateCell, type StorageDriver, type StoragePutMeta, type StoragePutResult, type StorageRegistry, type StructuredRenderPayload, type UrlInlineExpansionRule, escapeHtml, extractSvgDimensions, getActionAnnotation, isSensitiveField, sanitizeSvg };