@crowi/plugin-api 1.0.0-alpha.4 → 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
@@ -477,8 +477,8 @@ type AuthVerifyResult = {
477
477
  * Auth provider driver. The login screen asks core for the list of
478
478
  * registered drivers and renders one button per driver
479
479
  * (`Sign in with Google`). Clicking redirects through the plugin's
480
- * registered routes (`/api/v2/plugins/<name>/oauth/start`); the
481
- * 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`,
482
482
  * which the plugin's contract handles.
483
483
  *
484
484
  * `verify` is the bridge: given whatever the plugin pulled out of the
@@ -872,9 +872,36 @@ interface EmbedInput {
872
872
  * background-refresh window (see
873
873
  * `packages/api/src/renderer/cache/index.ts:cachedRender`).
874
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
+ }
875
892
  interface RenderResult {
876
- /** Already-sanitised HTML the core will inline. */
893
+ /** Already-sanitised HTML the core will inline. Unchanged — the one and only web/legacy representation. */
877
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;
878
905
  /**
879
906
  * Optional `<head>`-bound assets — Phase 4 records them on the
880
907
  * cache entry but the SSR layer does not yet inject them. Phase 7
@@ -993,6 +1020,8 @@ type InlineExpansion = {
993
1020
  interface EmbedFragment {
994
1021
  /** Pre-sanitised HTML fragment to inline at the source position. */
995
1022
  html: string;
1023
+ /** RFC-0023 — optional structured payload paired with `html` (see `RenderResult.structured`). */
1024
+ structured?: StructuredRenderPayload;
996
1025
  /** Optional `<head>`-bound assets (CSS / JS) keyed by URL. */
997
1026
  assets?: {
998
1027
  css?: string[];
@@ -1206,16 +1235,21 @@ interface RendererRegistry {
1206
1235
  * Declare a static CSS asset the plugin needs the browser to load
1207
1236
  * (e.g. KaTeX's ~30KB math stylesheet). `path` MUST be an
1208
1237
  * API-relative absolute path confined to the plugin's own
1209
- * `registerRoutes` namespace — `/api/v2/plugins/<this plugin's
1238
+ * `registerRoutes` namespace — `/api/plugins/<this plugin's
1210
1239
  * name>/<…>` — the same prefix `PluginRouterScope.route(...)` mounts
1211
1240
  * that plugin's HTTP routes under. A URL scheme, protocol-relative
1212
1241
  * `//host`, backslash, `..` traversal segment, or a path outside the
1213
1242
  * plugin's own namespace all throw synchronously (boot-time reject —
1214
1243
  * this is not an operator-configurable external URL; see spec
1215
- * §2.1's "不採用案").
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.
1216
1250
  *
1217
1251
  * The call only stages the path in a per-plugin pending set: it is
1218
- * published to the public `GET /api/v2/app/info` `rendererStylesheets`
1252
+ * published to the public `GET /api/app/info` `rendererStylesheets`
1219
1253
  * manifest ONLY after this plugin's OWN `registerRoutes(scope, ctx)`
1220
1254
  * completes without throwing (so the manifest never advertises a path
1221
1255
  * whose route failed to mount). A plugin with no `registerRoutes` at
@@ -1266,7 +1300,7 @@ interface PluginRouteOptions {
1266
1300
  /**
1267
1301
  * Scope passed to `registerRoutes(scope, ctx)`. Lets a plugin contribute
1268
1302
  * HTTP routes that the runtime mounts at
1269
- * `/api/v2/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1303
+ * `/api/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1270
1304
  * segment guarantees that core endpoints and other plugins cannot
1271
1305
  * collide (RFC-0013 §4).
1272
1306
  *
@@ -1277,9 +1311,9 @@ interface PluginRouteOptions {
1277
1311
  interface PluginRouterScope {
1278
1312
  /**
1279
1313
  * Mount `handler` for `method` at `<path>` under this plugin's
1280
- * namespace. `path` is relative to `/api/v2/plugins/<plugin-name>` and
1314
+ * namespace. `path` is relative to `/api/plugins/<plugin-name>` and
1281
1315
  * should start with `/` (e.g. `route('POST', '/events', handler, {
1282
- * auth: 'public' })` → `POST /api/v2/plugins/<name>/events`).
1316
+ * auth: 'public' })` → `POST /api/plugins/<name>/events`).
1283
1317
  *
1284
1318
  * Pass `{ auth: 'public' }` to bypass Crowi auth entirely for self-
1285
1319
  * authenticating inbound webhooks, `{ auth: 'admin' }` to require
@@ -1453,7 +1487,7 @@ interface CrowiPlugin {
1453
1487
  registerHooks?: (events: EventBus, ctx: PluginContext) => void;
1454
1488
  /**
1455
1489
  * HTTP routes the plugin contributes, mounted at
1456
- * `/api/v2/plugins/<name>/<path>` (the `<name>` path segment guarantees
1490
+ * `/api/plugins/<name>/<path>` (the `<name>` path segment guarantees
1457
1491
  * that core endpoints and other plugins cannot collide). Used for
1458
1492
  * inbound webhooks (Slack events / slash / interactivity), "Test
1459
1493
  * connection" buttons, `@action` targets, OAuth callbacks, etc.
@@ -1533,7 +1567,7 @@ declare const SENSITIVE_FIELD_MARKER = "@sensitive";
1533
1567
  *
1534
1568
  * The admin form renders a button with the given label that calls the
1535
1569
  * plugin's contributed endpoint at the given verb / path (relative to
1536
- * `/api/v2/plugins/<name>/`). Useful for "Test connection",
1570
+ * `/api/plugins/<name>/`). Useful for "Test connection",
1537
1571
  * "Authorise with Google", etc. without forcing every plugin to ship
1538
1572
  * its own React component.
1539
1573
  */
@@ -1553,7 +1587,7 @@ interface ActionAnnotation {
1553
1587
  label: string;
1554
1588
  /** HTTP verb of the plugin endpoint to call. */
1555
1589
  method: PluginRouteMethod;
1556
- /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */
1590
+ /** Path relative to `/api/plugins/<name>/`, with leading slash. */
1557
1591
  path: string;
1558
1592
  }
1559
1593
  /**
@@ -1573,4 +1607,126 @@ interface ActionAnnotation {
1573
1607
  */
1574
1608
  declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
1575
1609
 
1576
- 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 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, escapeHtml, 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 };
package/dist/index.d.ts CHANGED
@@ -477,8 +477,8 @@ type AuthVerifyResult = {
477
477
  * Auth provider driver. The login screen asks core for the list of
478
478
  * registered drivers and renders one button per driver
479
479
  * (`Sign in with Google`). Clicking redirects through the plugin's
480
- * registered routes (`/api/v2/plugins/<name>/oauth/start`); the
481
- * 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`,
482
482
  * which the plugin's contract handles.
483
483
  *
484
484
  * `verify` is the bridge: given whatever the plugin pulled out of the
@@ -872,9 +872,36 @@ interface EmbedInput {
872
872
  * background-refresh window (see
873
873
  * `packages/api/src/renderer/cache/index.ts:cachedRender`).
874
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
+ }
875
892
  interface RenderResult {
876
- /** Already-sanitised HTML the core will inline. */
893
+ /** Already-sanitised HTML the core will inline. Unchanged — the one and only web/legacy representation. */
877
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;
878
905
  /**
879
906
  * Optional `<head>`-bound assets — Phase 4 records them on the
880
907
  * cache entry but the SSR layer does not yet inject them. Phase 7
@@ -993,6 +1020,8 @@ type InlineExpansion = {
993
1020
  interface EmbedFragment {
994
1021
  /** Pre-sanitised HTML fragment to inline at the source position. */
995
1022
  html: string;
1023
+ /** RFC-0023 — optional structured payload paired with `html` (see `RenderResult.structured`). */
1024
+ structured?: StructuredRenderPayload;
996
1025
  /** Optional `<head>`-bound assets (CSS / JS) keyed by URL. */
997
1026
  assets?: {
998
1027
  css?: string[];
@@ -1206,16 +1235,21 @@ interface RendererRegistry {
1206
1235
  * Declare a static CSS asset the plugin needs the browser to load
1207
1236
  * (e.g. KaTeX's ~30KB math stylesheet). `path` MUST be an
1208
1237
  * API-relative absolute path confined to the plugin's own
1209
- * `registerRoutes` namespace — `/api/v2/plugins/<this plugin's
1238
+ * `registerRoutes` namespace — `/api/plugins/<this plugin's
1210
1239
  * name>/<…>` — the same prefix `PluginRouterScope.route(...)` mounts
1211
1240
  * that plugin's HTTP routes under. A URL scheme, protocol-relative
1212
1241
  * `//host`, backslash, `..` traversal segment, or a path outside the
1213
1242
  * plugin's own namespace all throw synchronously (boot-time reject —
1214
1243
  * this is not an operator-configurable external URL; see spec
1215
- * §2.1's "不採用案").
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.
1216
1250
  *
1217
1251
  * The call only stages the path in a per-plugin pending set: it is
1218
- * published to the public `GET /api/v2/app/info` `rendererStylesheets`
1252
+ * published to the public `GET /api/app/info` `rendererStylesheets`
1219
1253
  * manifest ONLY after this plugin's OWN `registerRoutes(scope, ctx)`
1220
1254
  * completes without throwing (so the manifest never advertises a path
1221
1255
  * whose route failed to mount). A plugin with no `registerRoutes` at
@@ -1266,7 +1300,7 @@ interface PluginRouteOptions {
1266
1300
  /**
1267
1301
  * Scope passed to `registerRoutes(scope, ctx)`. Lets a plugin contribute
1268
1302
  * HTTP routes that the runtime mounts at
1269
- * `/api/v2/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1303
+ * `/api/plugins/<plugin-name>/<path>` — the `<plugin-name>` path
1270
1304
  * segment guarantees that core endpoints and other plugins cannot
1271
1305
  * collide (RFC-0013 §4).
1272
1306
  *
@@ -1277,9 +1311,9 @@ interface PluginRouteOptions {
1277
1311
  interface PluginRouterScope {
1278
1312
  /**
1279
1313
  * Mount `handler` for `method` at `<path>` under this plugin's
1280
- * namespace. `path` is relative to `/api/v2/plugins/<plugin-name>` and
1314
+ * namespace. `path` is relative to `/api/plugins/<plugin-name>` and
1281
1315
  * should start with `/` (e.g. `route('POST', '/events', handler, {
1282
- * auth: 'public' })` → `POST /api/v2/plugins/<name>/events`).
1316
+ * auth: 'public' })` → `POST /api/plugins/<name>/events`).
1283
1317
  *
1284
1318
  * Pass `{ auth: 'public' }` to bypass Crowi auth entirely for self-
1285
1319
  * authenticating inbound webhooks, `{ auth: 'admin' }` to require
@@ -1453,7 +1487,7 @@ interface CrowiPlugin {
1453
1487
  registerHooks?: (events: EventBus, ctx: PluginContext) => void;
1454
1488
  /**
1455
1489
  * HTTP routes the plugin contributes, mounted at
1456
- * `/api/v2/plugins/<name>/<path>` (the `<name>` path segment guarantees
1490
+ * `/api/plugins/<name>/<path>` (the `<name>` path segment guarantees
1457
1491
  * that core endpoints and other plugins cannot collide). Used for
1458
1492
  * inbound webhooks (Slack events / slash / interactivity), "Test
1459
1493
  * connection" buttons, `@action` targets, OAuth callbacks, etc.
@@ -1533,7 +1567,7 @@ declare const SENSITIVE_FIELD_MARKER = "@sensitive";
1533
1567
  *
1534
1568
  * The admin form renders a button with the given label that calls the
1535
1569
  * plugin's contributed endpoint at the given verb / path (relative to
1536
- * `/api/v2/plugins/<name>/`). Useful for "Test connection",
1570
+ * `/api/plugins/<name>/`). Useful for "Test connection",
1537
1571
  * "Authorise with Google", etc. without forcing every plugin to ship
1538
1572
  * its own React component.
1539
1573
  */
@@ -1553,7 +1587,7 @@ interface ActionAnnotation {
1553
1587
  label: string;
1554
1588
  /** HTTP verb of the plugin endpoint to call. */
1555
1589
  method: PluginRouteMethod;
1556
- /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */
1590
+ /** Path relative to `/api/plugins/<name>/`, with leading slash. */
1557
1591
  path: string;
1558
1592
  }
1559
1593
  /**
@@ -1573,4 +1607,126 @@ interface ActionAnnotation {
1573
1607
  */
1574
1608
  declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
1575
1609
 
1576
- 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 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, escapeHtml, 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 };
package/dist/index.js CHANGED
@@ -23,8 +23,10 @@ __export(index_exports, {
23
23
  ACTION_FIELD_MARKER: () => ACTION_FIELD_MARKER,
24
24
  SENSITIVE_FIELD_MARKER: () => SENSITIVE_FIELD_MARKER,
25
25
  escapeHtml: () => escapeHtml,
26
+ extractSvgDimensions: () => extractSvgDimensions,
26
27
  getActionAnnotation: () => getActionAnnotation,
27
- isSensitiveField: () => isSensitiveField
28
+ isSensitiveField: () => isSensitiveField,
29
+ sanitizeSvg: () => sanitizeSvg
28
30
  });
29
31
  module.exports = __toCommonJS(index_exports);
30
32
 
@@ -66,12 +68,215 @@ function getActionAnnotation(field) {
66
68
  const [, label, method, path] = match;
67
69
  return { label, method, path };
68
70
  }
71
+
72
+ // ../svg-sanitize/dist/index.mjs
73
+ var import_xmldom = require("@xmldom/xmldom");
74
+ var MAX_DIMENSION_PX = 1e6;
75
+ function extractSvgDimensions(svg) {
76
+ const match = /\bviewBox\s*=\s*["']\s*([-\d.]+)[\s,]+([-\d.]+)[\s,]+([-\d.]+)[\s,]+([-\d.]+)\s*["']/.exec(svg);
77
+ if (!match) return null;
78
+ const width = Math.round(Number(match[3]));
79
+ const height = Math.round(Number(match[4]));
80
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
81
+ if (width <= 0 || height <= 0 || width > MAX_DIMENSION_PX || height > MAX_DIMENSION_PX) return null;
82
+ return { width, height };
83
+ }
84
+ function sanitizeSvg(input, policy) {
85
+ const doc = parseXml(input);
86
+ if (!doc) return { ok: false, reason: "malformed_xml" };
87
+ if (doc.doctype) {
88
+ return { ok: false, reason: "doctype_not_allowed" };
89
+ }
90
+ const root = doc.documentElement;
91
+ if (!isUnprefixedSvgElement(root)) {
92
+ return { ok: false, reason: "root_is_not_svg" };
93
+ }
94
+ sanitizeElementTree(root, policy, true);
95
+ const serializer = new import_xmldom.XMLSerializer();
96
+ const serialized = serializer.serializeToString(root);
97
+ const verifyDoc = parseXml(serialized);
98
+ if (!verifyDoc || !isUnprefixedSvgElement(verifyDoc.documentElement)) {
99
+ return { ok: false, reason: "sanitized_output_not_single_root_svg" };
100
+ }
101
+ return { ok: true, svg: serialized };
102
+ }
103
+ var SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
104
+ var XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
105
+ var XLINK_NAMESPACE_URI = "http://www.w3.org/1999/xlink";
106
+ function isSvgNamespaceElement(el) {
107
+ return el.namespaceURI === SVG_NAMESPACE_URI && el.prefix == null;
108
+ }
109
+ function isUnprefixedSvgElement(el) {
110
+ return el != null && el.localName === "svg" && isSvgNamespaceElement(el);
111
+ }
112
+ function parseXml(source) {
113
+ try {
114
+ return new import_xmldom.DOMParser({ onError: () => void 0 }).parseFromString(source, "image/svg+xml");
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
120
+ "svg",
121
+ "g",
122
+ "defs",
123
+ "symbol",
124
+ "use",
125
+ "title",
126
+ "desc",
127
+ "metadata",
128
+ "path",
129
+ "rect",
130
+ "circle",
131
+ "ellipse",
132
+ "line",
133
+ "polyline",
134
+ "polygon",
135
+ "text",
136
+ "tspan",
137
+ "textPath",
138
+ "tref",
139
+ "marker",
140
+ "clipPath",
141
+ "mask",
142
+ "pattern",
143
+ "linearGradient",
144
+ "radialGradient",
145
+ "stop",
146
+ "image",
147
+ "style",
148
+ "a",
149
+ "switch",
150
+ "filter",
151
+ "feGaussianBlur",
152
+ "feOffset",
153
+ "feMerge",
154
+ "feMergeNode",
155
+ "feColorMatrix",
156
+ "feComposite",
157
+ "feFlood",
158
+ "feBlend",
159
+ "feDropShadow",
160
+ "feMorphology",
161
+ "feTurbulence",
162
+ "feDisplacementMap"
163
+ ]);
164
+ var PROCESSING_INSTRUCTION_NODE = 7;
165
+ var ELEMENT_NODE = 1;
166
+ function sanitizeElementTree(el, policy, isRoot) {
167
+ sanitizeAttributes(el, policy, isRoot);
168
+ if (el.localName === "style") {
169
+ el.textContent = sanitizeStyleText(el.textContent ?? "");
170
+ return;
171
+ }
172
+ for (const child of Array.from(el.childNodes)) {
173
+ if (child.nodeType === PROCESSING_INSTRUCTION_NODE) {
174
+ el.removeChild(child);
175
+ continue;
176
+ }
177
+ if (child.nodeType !== ELEMENT_NODE) continue;
178
+ const childEl = child;
179
+ if (!isSvgNamespaceElement(childEl) || !ALLOWED_ELEMENTS.has(childEl.localName ?? childEl.nodeName)) {
180
+ el.removeChild(childEl);
181
+ continue;
182
+ }
183
+ sanitizeElementTree(childEl, policy, false);
184
+ }
185
+ }
186
+ function sanitizeAttributes(el, policy, isRoot) {
187
+ for (const attr of Array.from(el.attributes)) {
188
+ const localName = attr.localName ?? attr.name;
189
+ if (/^on/i.test(localName)) {
190
+ el.removeAttributeNode(attr);
191
+ continue;
192
+ }
193
+ if (localName === "style") {
194
+ el.removeAttributeNode(attr);
195
+ continue;
196
+ }
197
+ if (localName === "base" && attr.namespaceURI === XML_NAMESPACE_URI) {
198
+ el.removeAttributeNode(attr);
199
+ continue;
200
+ }
201
+ if (localName === "href") {
202
+ const sanitizedValue = sanitizeHrefValue(attr.value, policy);
203
+ if (sanitizedValue === null) {
204
+ el.removeAttributeNode(attr);
205
+ } else {
206
+ attr.value = sanitizedValue;
207
+ }
208
+ continue;
209
+ }
210
+ if (URL_VALUED_PRESENTATION_ATTRS.has(localName)) {
211
+ if (!isUrlFuncIriSafe(attr.value)) {
212
+ el.removeAttributeNode(attr);
213
+ }
214
+ continue;
215
+ }
216
+ if (attr.name === "xmlns" || attr.name.startsWith("xmlns:")) {
217
+ if (!isRoot || !isEssentialRootNamespaceDeclaration(attr)) {
218
+ el.removeAttributeNode(attr);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ function isEssentialRootNamespaceDeclaration(attr) {
224
+ if (attr.name === "xmlns") return true;
225
+ return attr.name === "xmlns:xlink" && attr.value === XLINK_NAMESPACE_URI;
226
+ }
227
+ var URL_VALUED_PRESENTATION_ATTRS = /* @__PURE__ */ new Set([
228
+ "fill",
229
+ "stroke",
230
+ "filter",
231
+ "clip-path",
232
+ "mask",
233
+ "cursor",
234
+ "marker",
235
+ "marker-start",
236
+ "marker-mid",
237
+ "marker-end"
238
+ ]);
239
+ var URL_FUNC_PATTERN = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
240
+ function isUrlFuncIriSafe(rawValue) {
241
+ const matches = Array.from(cssUnescape(rawValue).matchAll(URL_FUNC_PATTERN));
242
+ if (matches.length === 0) return true;
243
+ return matches.every(([, , target]) => target.trim().startsWith("#"));
244
+ }
245
+ function sanitizeHrefValue(rawValue, policy) {
246
+ const value = rawValue.trim();
247
+ if (value.startsWith("#")) return value;
248
+ if (/^javascript:/i.test(value)) return null;
249
+ if (/^data:/i.test(value)) return null;
250
+ if (value.startsWith("//")) return null;
251
+ if (policy.allowSafeHref && /^https:\/\//i.test(value)) return value;
252
+ return null;
253
+ }
254
+ function sanitizeStyleText(css) {
255
+ const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, " ");
256
+ let out = cssUnescape(withoutComments);
257
+ out = out.replace(/@import\b[^;]*;?/gi, "");
258
+ out = out.replace(URL_FUNC_PATTERN, (match, _quote, target) => target.trim().startsWith("#") ? match : "none");
259
+ return out;
260
+ }
261
+ function cssUnescape(css) {
262
+ return css.replace(/\\([0-9a-fA-F]{1,6})[ \t\n\f\r]?|\\([^\r\n\f])|\\$/g, (_match, hex, literal) => {
263
+ if (hex !== void 0) {
264
+ const codePoint = Number.parseInt(hex, 16);
265
+ if (codePoint === 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) return "\uFFFD";
266
+ return String.fromCodePoint(codePoint);
267
+ }
268
+ if (literal !== void 0) return literal;
269
+ return "\uFFFD";
270
+ });
271
+ }
69
272
  // Annotate the CommonJS export names for ESM import in node:
70
273
  0 && (module.exports = {
71
274
  ACTION_FIELD_MARKER,
72
275
  SENSITIVE_FIELD_MARKER,
73
276
  escapeHtml,
277
+ extractSvgDimensions,
74
278
  getActionAnnotation,
75
- isSensitiveField
279
+ isSensitiveField,
280
+ sanitizeSvg
76
281
  });
77
282
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/html.ts","../src/schema-markers.ts"],"sourcesContent":["/**\n * @crowi/plugin-api — type-only contract for Crowi 2.0 plugins.\n *\n * Plugins author against this package. The runtime (@crowi/server) loads\n * plugins listed in `crowi.config.json`, calls each plugin's\n * `register*` callbacks, and routes all the side effects (storage,\n * search, auth, notifications) through the typed registries declared\n * here.\n *\n * For the design rationale see `docs/rfcs/0001-plugin-architecture.md`\n * in the Crowi monorepo.\n */\n\nexport type { AppInfo, PageMetadataAccessor, PluginContext, PluginLogger, StateCell } from './context';\nexport type { EventBus, PluginEvents } from './events';\nexport { escapeHtml } from './html';\nexport type { CrowiPlugin } from './plugin';\n\nexport type { AuthDriver, AuthProfile, AuthRegistry, AuthVerifyResult } from './registries/auth';\nexport type { EmailMessage, MailSender, MailSenderRegistry } from './registries/mail';\nexport type { NotificationPayload, NotifierDriver, NotifierRegistry } from './registries/notifier';\nexport type {\n SearchableDoc,\n SearchDriver,\n SearchHit,\n SearchHits,\n SearchPageType,\n SearchQuery,\n SearchQueryGrants,\n SearchQueryViewer,\n SearchRegistry,\n} from './registries/search';\nexport type { StorageDriver, StoragePutMeta, StoragePutResult, StorageRegistry } from './registries/storage';\nexport type {\n AdmissionControlConfig,\n AuthContext,\n CacheEntry,\n CacheKey,\n CacheStorage,\n CodeBlockInfo,\n CodeBlockRenderer,\n EmbedFragment,\n EmbedInput,\n EmbedRenderer,\n InlineExpansion,\n NodeRenderer,\n RenderActor,\n RenderContext,\n RenderError,\n RendererRegistry,\n RenderPhase,\n RenderResult,\n Reservation,\n ScopedCacheStorage,\n UrlInlineExpansionRule,\n} from './renderer';\nexport type { PluginRouteHandler, PluginRouteMethod, PluginRouteOptions, PluginRouterScope } from './routes';\nexport { ACTION_FIELD_MARKER, getActionAnnotation, isSensitiveField, SENSITIVE_FIELD_MARKER } from './schema-markers';\n","/**\n * HTML-emitting helper for renderer plugins.\n *\n * A renderer plugin that builds HTML from author-controlled or external\n * strings (an OGP title, a math error message, …) must escape them, and\n * that escape is a security primitive — a hardening change has to reach\n * every plugin at once, not whichever local copies someone remembers.\n * This is the SDK's single copy (`@crowi/plugin-renderer-katex` and\n * `@crowi/plugin-renderer-link-card` each carried an identical local one\n * before it was hoisted here).\n */\n\n/** Escape `&` `<` `>` `\"` `'` for interpolation into HTML text or double/single-quoted attribute values. */\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => {\n switch (c) {\n case '&':\n return '&amp;';\n case '<':\n return '&lt;';\n case '>':\n return '&gt;';\n case '\"':\n return '&quot;';\n case \"'\":\n return '&#39;';\n default:\n return c;\n }\n });\n}\n","import type { z } from 'zod/v3';\n\nimport type { PluginRouteMethod } from './routes';\n\n/**\n * `configSchema` description-string markers.\n *\n * The admin UI walks the schema and looks at each field's\n * `description` (set via `z.string().describe('@sensitive ...')`). A\n * description starting with one of these marker tokens unlocks special\n * UI behaviour without forcing every field to declare a custom Zod\n * type.\n */\n\n/**\n * Marker that flags a config field as sensitive (encrypted at rest).\n * Usage:\n *\n * z.string().describe('@sensitive AWS secret access key')\n *\n * The runtime auto-encrypts on write and decrypts on read, using the\n * same KeyProvider as core sensitive Config. The admin UI renders the\n * field via `<SecretField>` (saved badge / clear pending / undo).\n */\nexport const SENSITIVE_FIELD_MARKER = '@sensitive';\n\n/**\n * Marker that adds an action button next to a config field. Usage:\n *\n * z.string().describe('@action \"Test connection\" POST /test')\n *\n * The admin form renders a button with the given label that calls the\n * plugin's contributed endpoint at the given verb / path (relative to\n * `/api/v2/plugins/<name>/`). Useful for \"Test connection\",\n * \"Authorise with Google\", etc. without forcing every plugin to ship\n * its own React component.\n */\nexport const ACTION_FIELD_MARKER = '@action';\n\n/**\n * True if the schema field is marked `@sensitive`.\n *\n * `field` is `z.ZodTypeAny` (intentionally loose); call sites pass the\n * value type from `configSchema.shape[key]`.\n */\nexport function isSensitiveField(field: z.ZodTypeAny): boolean {\n const description = field.description;\n return typeof description === 'string' && description.trimStart().startsWith(SENSITIVE_FIELD_MARKER);\n}\n\n/**\n * Parsed `@action` annotation extracted from a field's `description`.\n */\nexport interface ActionAnnotation {\n /** Visible button label, e.g. \"Test connection\". */\n label: string;\n /** HTTP verb of the plugin endpoint to call. */\n method: PluginRouteMethod;\n /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */\n path: string;\n}\n\n/**\n * Parse an `@action` annotation off a field, or return null if absent.\n *\n * Format: `@action \"<label>\" <METHOD> <path>`\n * e.g. `@action \"Test connection\" POST /test`\n *\n * The label may include spaces when wrapped in double quotes; the method\n * must be one of `PluginRouteMethod` (`GET` / `POST` — the only verbs a\n * plugin route can actually be mounted on, see `routes.ts`); the path\n * begins with `/`. A description that starts with the `@action` marker\n * but declares an unsupported verb (e.g. `PUT` / `DELETE`) fails to match\n * and returns `null` here — callers that walk a plugin's `configSchema`\n * (e.g. `PluginManager.activate()`) are expected to warn on that case at\n * boot, since it would otherwise be a silent dead button.\n */\nexport function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null {\n const description = field.description;\n if (typeof description !== 'string') return null;\n const trimmed = description.trimStart();\n if (!trimmed.startsWith(ACTION_FIELD_MARKER)) return null;\n\n const rest = trimmed.slice(ACTION_FIELD_MARKER.length).trimStart();\n // `\"<label>\" <METHOD> <path>`\n const match = rest.match(/^\"([^\"]+)\"\\s+(GET|POST)\\s+(\\/\\S*)/);\n if (!match) return null;\n\n const [, label, method, path] = match;\n return { label, method: method as ActionAnnotation['method'], path };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACaO,SAAS,WAAW,GAAmB;AAC5C,SAAO,EAAE,QAAQ,YAAY,CAAC,MAAM;AAClC,YAAQ,GAAG;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC;AACH;;;ACNO,IAAM,yBAAyB;AAa/B,IAAM,sBAAsB;AAQ5B,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,cAAc,MAAM;AAC1B,SAAO,OAAO,gBAAgB,YAAY,YAAY,UAAU,EAAE,WAAW,sBAAsB;AACrG;AA6BO,SAAS,oBAAoB,OAA8C;AAChF,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,SAAU,QAAO;AAC5C,QAAM,UAAU,YAAY,UAAU;AACtC,MAAI,CAAC,QAAQ,WAAW,mBAAmB,EAAG,QAAO;AAErD,QAAM,OAAO,QAAQ,MAAM,oBAAoB,MAAM,EAAE,UAAU;AAEjE,QAAM,QAAQ,KAAK,MAAM,mCAAmC;AAC5D,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,OAAO,QAAQ,IAAI,IAAI;AAChC,SAAO,EAAE,OAAO,QAA8C,KAAK;AACrE;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/html.ts","../src/schema-markers.ts","../../svg-sanitize/src/dimensions.ts","../../svg-sanitize/src/sanitize.ts"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -36,11 +36,214 @@ function getActionAnnotation(field) {
36
36
  const [, label, method, path] = match;
37
37
  return { label, method, path };
38
38
  }
39
+
40
+ // ../svg-sanitize/dist/index.mjs
41
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
42
+ var MAX_DIMENSION_PX = 1e6;
43
+ function extractSvgDimensions(svg) {
44
+ const match = /\bviewBox\s*=\s*["']\s*([-\d.]+)[\s,]+([-\d.]+)[\s,]+([-\d.]+)[\s,]+([-\d.]+)\s*["']/.exec(svg);
45
+ if (!match) return null;
46
+ const width = Math.round(Number(match[3]));
47
+ const height = Math.round(Number(match[4]));
48
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
49
+ if (width <= 0 || height <= 0 || width > MAX_DIMENSION_PX || height > MAX_DIMENSION_PX) return null;
50
+ return { width, height };
51
+ }
52
+ function sanitizeSvg(input, policy) {
53
+ const doc = parseXml(input);
54
+ if (!doc) return { ok: false, reason: "malformed_xml" };
55
+ if (doc.doctype) {
56
+ return { ok: false, reason: "doctype_not_allowed" };
57
+ }
58
+ const root = doc.documentElement;
59
+ if (!isUnprefixedSvgElement(root)) {
60
+ return { ok: false, reason: "root_is_not_svg" };
61
+ }
62
+ sanitizeElementTree(root, policy, true);
63
+ const serializer = new XMLSerializer();
64
+ const serialized = serializer.serializeToString(root);
65
+ const verifyDoc = parseXml(serialized);
66
+ if (!verifyDoc || !isUnprefixedSvgElement(verifyDoc.documentElement)) {
67
+ return { ok: false, reason: "sanitized_output_not_single_root_svg" };
68
+ }
69
+ return { ok: true, svg: serialized };
70
+ }
71
+ var SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
72
+ var XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
73
+ var XLINK_NAMESPACE_URI = "http://www.w3.org/1999/xlink";
74
+ function isSvgNamespaceElement(el) {
75
+ return el.namespaceURI === SVG_NAMESPACE_URI && el.prefix == null;
76
+ }
77
+ function isUnprefixedSvgElement(el) {
78
+ return el != null && el.localName === "svg" && isSvgNamespaceElement(el);
79
+ }
80
+ function parseXml(source) {
81
+ try {
82
+ return new DOMParser({ onError: () => void 0 }).parseFromString(source, "image/svg+xml");
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
88
+ "svg",
89
+ "g",
90
+ "defs",
91
+ "symbol",
92
+ "use",
93
+ "title",
94
+ "desc",
95
+ "metadata",
96
+ "path",
97
+ "rect",
98
+ "circle",
99
+ "ellipse",
100
+ "line",
101
+ "polyline",
102
+ "polygon",
103
+ "text",
104
+ "tspan",
105
+ "textPath",
106
+ "tref",
107
+ "marker",
108
+ "clipPath",
109
+ "mask",
110
+ "pattern",
111
+ "linearGradient",
112
+ "radialGradient",
113
+ "stop",
114
+ "image",
115
+ "style",
116
+ "a",
117
+ "switch",
118
+ "filter",
119
+ "feGaussianBlur",
120
+ "feOffset",
121
+ "feMerge",
122
+ "feMergeNode",
123
+ "feColorMatrix",
124
+ "feComposite",
125
+ "feFlood",
126
+ "feBlend",
127
+ "feDropShadow",
128
+ "feMorphology",
129
+ "feTurbulence",
130
+ "feDisplacementMap"
131
+ ]);
132
+ var PROCESSING_INSTRUCTION_NODE = 7;
133
+ var ELEMENT_NODE = 1;
134
+ function sanitizeElementTree(el, policy, isRoot) {
135
+ sanitizeAttributes(el, policy, isRoot);
136
+ if (el.localName === "style") {
137
+ el.textContent = sanitizeStyleText(el.textContent ?? "");
138
+ return;
139
+ }
140
+ for (const child of Array.from(el.childNodes)) {
141
+ if (child.nodeType === PROCESSING_INSTRUCTION_NODE) {
142
+ el.removeChild(child);
143
+ continue;
144
+ }
145
+ if (child.nodeType !== ELEMENT_NODE) continue;
146
+ const childEl = child;
147
+ if (!isSvgNamespaceElement(childEl) || !ALLOWED_ELEMENTS.has(childEl.localName ?? childEl.nodeName)) {
148
+ el.removeChild(childEl);
149
+ continue;
150
+ }
151
+ sanitizeElementTree(childEl, policy, false);
152
+ }
153
+ }
154
+ function sanitizeAttributes(el, policy, isRoot) {
155
+ for (const attr of Array.from(el.attributes)) {
156
+ const localName = attr.localName ?? attr.name;
157
+ if (/^on/i.test(localName)) {
158
+ el.removeAttributeNode(attr);
159
+ continue;
160
+ }
161
+ if (localName === "style") {
162
+ el.removeAttributeNode(attr);
163
+ continue;
164
+ }
165
+ if (localName === "base" && attr.namespaceURI === XML_NAMESPACE_URI) {
166
+ el.removeAttributeNode(attr);
167
+ continue;
168
+ }
169
+ if (localName === "href") {
170
+ const sanitizedValue = sanitizeHrefValue(attr.value, policy);
171
+ if (sanitizedValue === null) {
172
+ el.removeAttributeNode(attr);
173
+ } else {
174
+ attr.value = sanitizedValue;
175
+ }
176
+ continue;
177
+ }
178
+ if (URL_VALUED_PRESENTATION_ATTRS.has(localName)) {
179
+ if (!isUrlFuncIriSafe(attr.value)) {
180
+ el.removeAttributeNode(attr);
181
+ }
182
+ continue;
183
+ }
184
+ if (attr.name === "xmlns" || attr.name.startsWith("xmlns:")) {
185
+ if (!isRoot || !isEssentialRootNamespaceDeclaration(attr)) {
186
+ el.removeAttributeNode(attr);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ function isEssentialRootNamespaceDeclaration(attr) {
192
+ if (attr.name === "xmlns") return true;
193
+ return attr.name === "xmlns:xlink" && attr.value === XLINK_NAMESPACE_URI;
194
+ }
195
+ var URL_VALUED_PRESENTATION_ATTRS = /* @__PURE__ */ new Set([
196
+ "fill",
197
+ "stroke",
198
+ "filter",
199
+ "clip-path",
200
+ "mask",
201
+ "cursor",
202
+ "marker",
203
+ "marker-start",
204
+ "marker-mid",
205
+ "marker-end"
206
+ ]);
207
+ var URL_FUNC_PATTERN = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
208
+ function isUrlFuncIriSafe(rawValue) {
209
+ const matches = Array.from(cssUnescape(rawValue).matchAll(URL_FUNC_PATTERN));
210
+ if (matches.length === 0) return true;
211
+ return matches.every(([, , target]) => target.trim().startsWith("#"));
212
+ }
213
+ function sanitizeHrefValue(rawValue, policy) {
214
+ const value = rawValue.trim();
215
+ if (value.startsWith("#")) return value;
216
+ if (/^javascript:/i.test(value)) return null;
217
+ if (/^data:/i.test(value)) return null;
218
+ if (value.startsWith("//")) return null;
219
+ if (policy.allowSafeHref && /^https:\/\//i.test(value)) return value;
220
+ return null;
221
+ }
222
+ function sanitizeStyleText(css) {
223
+ const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, " ");
224
+ let out = cssUnescape(withoutComments);
225
+ out = out.replace(/@import\b[^;]*;?/gi, "");
226
+ out = out.replace(URL_FUNC_PATTERN, (match, _quote, target) => target.trim().startsWith("#") ? match : "none");
227
+ return out;
228
+ }
229
+ function cssUnescape(css) {
230
+ return css.replace(/\\([0-9a-fA-F]{1,6})[ \t\n\f\r]?|\\([^\r\n\f])|\\$/g, (_match, hex, literal) => {
231
+ if (hex !== void 0) {
232
+ const codePoint = Number.parseInt(hex, 16);
233
+ if (codePoint === 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) return "\uFFFD";
234
+ return String.fromCodePoint(codePoint);
235
+ }
236
+ if (literal !== void 0) return literal;
237
+ return "\uFFFD";
238
+ });
239
+ }
39
240
  export {
40
241
  ACTION_FIELD_MARKER,
41
242
  SENSITIVE_FIELD_MARKER,
42
243
  escapeHtml,
244
+ extractSvgDimensions,
43
245
  getActionAnnotation,
44
- isSensitiveField
246
+ isSensitiveField,
247
+ sanitizeSvg
45
248
  };
46
249
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/html.ts","../src/schema-markers.ts"],"sourcesContent":["/**\n * HTML-emitting helper for renderer plugins.\n *\n * A renderer plugin that builds HTML from author-controlled or external\n * strings (an OGP title, a math error message, …) must escape them, and\n * that escape is a security primitive — a hardening change has to reach\n * every plugin at once, not whichever local copies someone remembers.\n * This is the SDK's single copy (`@crowi/plugin-renderer-katex` and\n * `@crowi/plugin-renderer-link-card` each carried an identical local one\n * before it was hoisted here).\n */\n\n/** Escape `&` `<` `>` `\"` `'` for interpolation into HTML text or double/single-quoted attribute values. */\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => {\n switch (c) {\n case '&':\n return '&amp;';\n case '<':\n return '&lt;';\n case '>':\n return '&gt;';\n case '\"':\n return '&quot;';\n case \"'\":\n return '&#39;';\n default:\n return c;\n }\n });\n}\n","import type { z } from 'zod/v3';\n\nimport type { PluginRouteMethod } from './routes';\n\n/**\n * `configSchema` description-string markers.\n *\n * The admin UI walks the schema and looks at each field's\n * `description` (set via `z.string().describe('@sensitive ...')`). A\n * description starting with one of these marker tokens unlocks special\n * UI behaviour without forcing every field to declare a custom Zod\n * type.\n */\n\n/**\n * Marker that flags a config field as sensitive (encrypted at rest).\n * Usage:\n *\n * z.string().describe('@sensitive AWS secret access key')\n *\n * The runtime auto-encrypts on write and decrypts on read, using the\n * same KeyProvider as core sensitive Config. The admin UI renders the\n * field via `<SecretField>` (saved badge / clear pending / undo).\n */\nexport const SENSITIVE_FIELD_MARKER = '@sensitive';\n\n/**\n * Marker that adds an action button next to a config field. Usage:\n *\n * z.string().describe('@action \"Test connection\" POST /test')\n *\n * The admin form renders a button with the given label that calls the\n * plugin's contributed endpoint at the given verb / path (relative to\n * `/api/v2/plugins/<name>/`). Useful for \"Test connection\",\n * \"Authorise with Google\", etc. without forcing every plugin to ship\n * its own React component.\n */\nexport const ACTION_FIELD_MARKER = '@action';\n\n/**\n * True if the schema field is marked `@sensitive`.\n *\n * `field` is `z.ZodTypeAny` (intentionally loose); call sites pass the\n * value type from `configSchema.shape[key]`.\n */\nexport function isSensitiveField(field: z.ZodTypeAny): boolean {\n const description = field.description;\n return typeof description === 'string' && description.trimStart().startsWith(SENSITIVE_FIELD_MARKER);\n}\n\n/**\n * Parsed `@action` annotation extracted from a field's `description`.\n */\nexport interface ActionAnnotation {\n /** Visible button label, e.g. \"Test connection\". */\n label: string;\n /** HTTP verb of the plugin endpoint to call. */\n method: PluginRouteMethod;\n /** Path relative to `/api/v2/plugins/<name>/`, with leading slash. */\n path: string;\n}\n\n/**\n * Parse an `@action` annotation off a field, or return null if absent.\n *\n * Format: `@action \"<label>\" <METHOD> <path>`\n * e.g. `@action \"Test connection\" POST /test`\n *\n * The label may include spaces when wrapped in double quotes; the method\n * must be one of `PluginRouteMethod` (`GET` / `POST` — the only verbs a\n * plugin route can actually be mounted on, see `routes.ts`); the path\n * begins with `/`. A description that starts with the `@action` marker\n * but declares an unsupported verb (e.g. `PUT` / `DELETE`) fails to match\n * and returns `null` here — callers that walk a plugin's `configSchema`\n * (e.g. `PluginManager.activate()`) are expected to warn on that case at\n * boot, since it would otherwise be a silent dead button.\n */\nexport function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null {\n const description = field.description;\n if (typeof description !== 'string') return null;\n const trimmed = description.trimStart();\n if (!trimmed.startsWith(ACTION_FIELD_MARKER)) return null;\n\n const rest = trimmed.slice(ACTION_FIELD_MARKER.length).trimStart();\n // `\"<label>\" <METHOD> <path>`\n const match = rest.match(/^\"([^\"]+)\"\\s+(GET|POST)\\s+(\\/\\S*)/);\n if (!match) return null;\n\n const [, label, method, path] = match;\n return { label, method: method as ActionAnnotation['method'], path };\n}\n"],"mappings":";AAaO,SAAS,WAAW,GAAmB;AAC5C,SAAO,EAAE,QAAQ,YAAY,CAAC,MAAM;AAClC,YAAQ,GAAG;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC;AACH;;;ACNO,IAAM,yBAAyB;AAa/B,IAAM,sBAAsB;AAQ5B,SAAS,iBAAiB,OAA8B;AAC7D,QAAM,cAAc,MAAM;AAC1B,SAAO,OAAO,gBAAgB,YAAY,YAAY,UAAU,EAAE,WAAW,sBAAsB;AACrG;AA6BO,SAAS,oBAAoB,OAA8C;AAChF,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,SAAU,QAAO;AAC5C,QAAM,UAAU,YAAY,UAAU;AACtC,MAAI,CAAC,QAAQ,WAAW,mBAAmB,EAAG,QAAO;AAErD,QAAM,OAAO,QAAQ,MAAM,oBAAoB,MAAM,EAAE,UAAU;AAEjE,QAAM,QAAQ,KAAK,MAAM,mCAAmC;AAC5D,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,OAAO,QAAQ,IAAI,IAAI;AAChC,SAAO,EAAE,OAAO,QAA8C,KAAK;AACrE;","names":[]}
1
+ {"version":3,"sources":["../src/html.ts","../src/schema-markers.ts","../../svg-sanitize/src/dimensions.ts","../../svg-sanitize/src/sanitize.ts"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowi/plugin-api",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Type-only contract for Crowi 2.0 plugins. See docs/rfcs/0001-plugin-architecture.md.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,12 @@
37
37
  "tsup": "^8.3.5",
38
38
  "typescript": "^5.8.3",
39
39
  "zod": "^4.4.3",
40
+ "@crowi/svg-sanitize": "0.1.0-alpha.1",
40
41
  "@crowi/tsconfig": "0.1.0-alpha.0"
41
42
  },
43
+ "dependencies": {
44
+ "@xmldom/xmldom": "^0.9.10"
45
+ },
42
46
  "scripts": {
43
47
  "build": "tsup",
44
48
  "dev": "tsup --watch --no-clean",