@crowi/plugin-api 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +203 -13
- package/dist/index.d.ts +203 -13
- package/dist/index.js +207 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +204 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -3
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/
|
|
481
|
-
* provider redirects back to `/api/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
@@ -1289,6 +1323,32 @@ interface PluginRouterScope {
|
|
|
1289
1323
|
route(method: PluginRouteMethod, path: string, handler: PluginRouteHandler, opts?: PluginRouteOptions): void;
|
|
1290
1324
|
}
|
|
1291
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Declares that, when a specific driver from a specific registry is
|
|
1328
|
+
* selected (`crowi.config.json:<registry>.driver === driver`), this
|
|
1329
|
+
* plugin's own config becomes required to actually work at runtime —
|
|
1330
|
+
* even though the `configSchema` field itself is optional / defaults to
|
|
1331
|
+
* `''` so `configSchema.parse()` alone can't detect "present but
|
|
1332
|
+
* unusable" (see `@crowi/plugin-storage-aws-s3`'s `bucket` and
|
|
1333
|
+
* `@crowi/plugin-search-elasticsearch` / `@crowi/plugin-search-opensearch`'s
|
|
1334
|
+
* `url`, both `z.string().default('')`).
|
|
1335
|
+
*
|
|
1336
|
+
* This is metadata only — it never carries an actual config value.
|
|
1337
|
+
* `registry` / `driver` / every name in `requiredConfigFields` must be
|
|
1338
|
+
* non-empty. The runtime (`PluginManager.getReadinessIssues()`) reads
|
|
1339
|
+
* this once per admin readiness check, cross-references it against the
|
|
1340
|
+
* currently selected driver and the plugin's current config namespace,
|
|
1341
|
+
* and reports which declared fields are still empty — never the values
|
|
1342
|
+
* themselves. See RFC-none / feature-plugin-config-readiness.
|
|
1343
|
+
*/
|
|
1344
|
+
interface PluginReadinessDeclaration {
|
|
1345
|
+
/** Which driver registry this declaration is scoped to. */
|
|
1346
|
+
registry: 'storage' | 'search' | 'mail';
|
|
1347
|
+
/** The driver name (as registered via `registry.register(name, …)`) this declaration applies to. */
|
|
1348
|
+
driver: string;
|
|
1349
|
+
/** `configSchema` field names that must be non-empty for `driver` to actually work once selected. */
|
|
1350
|
+
requiredConfigFields: string[];
|
|
1351
|
+
}
|
|
1292
1352
|
/**
|
|
1293
1353
|
* The contract every Crowi plugin satisfies. Plugins export their
|
|
1294
1354
|
* `CrowiPlugin` object as the package's default export; the runtime
|
|
@@ -1423,6 +1483,14 @@ interface CrowiPlugin {
|
|
|
1423
1483
|
label?: string;
|
|
1424
1484
|
description?: string;
|
|
1425
1485
|
}>>;
|
|
1486
|
+
/**
|
|
1487
|
+
* Declares which of this plugin's own `configSchema` fields must be
|
|
1488
|
+
* non-empty for a specific driver selection to actually work at
|
|
1489
|
+
* runtime (see {@link PluginReadinessDeclaration}). Optional — a
|
|
1490
|
+
* plugin with no readiness declaration is never surfaced by the
|
|
1491
|
+
* admin readiness check, same as before this field existed.
|
|
1492
|
+
*/
|
|
1493
|
+
readiness?: PluginReadinessDeclaration;
|
|
1426
1494
|
/** Storage driver registration. Called once at boot. */
|
|
1427
1495
|
registerStorage?: (registry: StorageRegistry, ctx: PluginContext) => void;
|
|
1428
1496
|
/** Search backend registration. Called once at boot. */
|
|
@@ -1453,7 +1521,7 @@ interface CrowiPlugin {
|
|
|
1453
1521
|
registerHooks?: (events: EventBus, ctx: PluginContext) => void;
|
|
1454
1522
|
/**
|
|
1455
1523
|
* HTTP routes the plugin contributes, mounted at
|
|
1456
|
-
* `/api/
|
|
1524
|
+
* `/api/plugins/<name>/<path>` (the `<name>` path segment guarantees
|
|
1457
1525
|
* that core endpoints and other plugins cannot collide). Used for
|
|
1458
1526
|
* inbound webhooks (Slack events / slash / interactivity), "Test
|
|
1459
1527
|
* connection" buttons, `@action` targets, OAuth callbacks, etc.
|
|
@@ -1533,7 +1601,7 @@ declare const SENSITIVE_FIELD_MARKER = "@sensitive";
|
|
|
1533
1601
|
*
|
|
1534
1602
|
* The admin form renders a button with the given label that calls the
|
|
1535
1603
|
* plugin's contributed endpoint at the given verb / path (relative to
|
|
1536
|
-
* `/api/
|
|
1604
|
+
* `/api/plugins/<name>/`). Useful for "Test connection",
|
|
1537
1605
|
* "Authorise with Google", etc. without forcing every plugin to ship
|
|
1538
1606
|
* its own React component.
|
|
1539
1607
|
*/
|
|
@@ -1553,7 +1621,7 @@ interface ActionAnnotation {
|
|
|
1553
1621
|
label: string;
|
|
1554
1622
|
/** HTTP verb of the plugin endpoint to call. */
|
|
1555
1623
|
method: PluginRouteMethod;
|
|
1556
|
-
/** Path relative to `/api/
|
|
1624
|
+
/** Path relative to `/api/plugins/<name>/`, with leading slash. */
|
|
1557
1625
|
path: string;
|
|
1558
1626
|
}
|
|
1559
1627
|
/**
|
|
@@ -1573,4 +1641,126 @@ interface ActionAnnotation {
|
|
|
1573
1641
|
*/
|
|
1574
1642
|
declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
|
|
1575
1643
|
|
|
1576
|
-
|
|
1644
|
+
/**
|
|
1645
|
+
* Parses a root `<svg>` element's `viewBox` (`minX minY width height`) to
|
|
1646
|
+
* derive intrinsic pixel dimensions. Shared by
|
|
1647
|
+
* `@crowi/plugin-renderer-mermaid` (its original home — the `<img>`
|
|
1648
|
+
* `width`/`height` intrinsic-size fix) and, since RFC-0023,
|
|
1649
|
+
* `@crowi/plugin-renderer-plantuml`'s SVG sidecar path — both need the
|
|
1650
|
+
* same derivation and both already bundle this package, so it lives
|
|
1651
|
+
* here rather than being copied per plugin.
|
|
1652
|
+
*
|
|
1653
|
+
* Reads attributes off the sanitized SVG source string only — never
|
|
1654
|
+
* decodes any `data:` payload.
|
|
1655
|
+
*/
|
|
1656
|
+
declare function extractSvgDimensions(svg: string): {
|
|
1657
|
+
width: number;
|
|
1658
|
+
height: number;
|
|
1659
|
+
} | null;
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Renderer-specific knobs for `sanitizeSvg`. The sanitizer itself is a
|
|
1663
|
+
* single shared implementation (`sanitize.ts`) — per-renderer differences
|
|
1664
|
+
* are expressed as parameters here, never as a second copy of the DOM
|
|
1665
|
+
* walk (spec §9: "実装自体をrenderer間で複製しない").
|
|
1666
|
+
*/
|
|
1667
|
+
interface SanitizeSvgPolicy {
|
|
1668
|
+
/**
|
|
1669
|
+
* When `true`, `href` / `xlink:href` values pointing at an `https:`
|
|
1670
|
+
* URL are preserved (PlantUML's existing "preserves href to a safe
|
|
1671
|
+
* URL" behaviour, consumed starting Phase 3). When `false`, every
|
|
1672
|
+
* `href` / `xlink:href` is stripped unless it is a local fragment
|
|
1673
|
+
* reference (`#id`) — Mermaid's strict policy (spec §1 layer 1 already
|
|
1674
|
+
* disables Mermaid's own click callbacks, so no link should survive
|
|
1675
|
+
* either).
|
|
1676
|
+
*
|
|
1677
|
+
* Regardless of this flag, `javascript:`, `data:`, and
|
|
1678
|
+
* protocol-relative (`//host/...`) URLs are ALWAYS stripped — no
|
|
1679
|
+
* policy may re-allow those.
|
|
1680
|
+
*/
|
|
1681
|
+
allowSafeHref: boolean;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
type SanitizeSvgResult = {
|
|
1685
|
+
ok: true;
|
|
1686
|
+
svg: string;
|
|
1687
|
+
} | {
|
|
1688
|
+
ok: false;
|
|
1689
|
+
reason: string;
|
|
1690
|
+
};
|
|
1691
|
+
/**
|
|
1692
|
+
* DOM-based SVG sanitizer shared by `@crowi/plugin-renderer-mermaid` and
|
|
1693
|
+
* (from Phase 3) `@crowi/plugin-renderer-plantuml`. Spec §2 layer 2 / §9.
|
|
1694
|
+
*
|
|
1695
|
+
* Design: allowlist-first for elements (unknown/unexpected element names
|
|
1696
|
+
* are dropped with their whole subtree — safer than trying to enumerate
|
|
1697
|
+
* every dangerous tag), then a small set of attribute-level rules that
|
|
1698
|
+
* apply uniformly to every surviving element. This is a from-scratch DOM
|
|
1699
|
+
* walk, not a regex pass (`packages/plugin-renderer-plantuml/src/
|
|
1700
|
+
* sanitize.ts`'s existing implementation is explicitly documented there
|
|
1701
|
+
* as "not a substitute for DOMPurify" — this package is the replacement
|
|
1702
|
+
* both renderers converge on, PlantUML starting Phase 3).
|
|
1703
|
+
*
|
|
1704
|
+
* What gets removed:
|
|
1705
|
+
* - Any element not in `ALLOWED_ELEMENTS` (`script`, `foreignObject`,
|
|
1706
|
+
* `iframe`, `object`, `embed`, SMIL `animate*`/`set`/`discard`, ...) —
|
|
1707
|
+
* dropped together with its entire subtree.
|
|
1708
|
+
* - `on*` event-handler attributes (any casing).
|
|
1709
|
+
* - The `style` attribute (inline styles). Mermaid/PlantUML's real
|
|
1710
|
+
* styling lives in the `<style>` *element* (class-based), which is
|
|
1711
|
+
* sanitized separately below rather than dropped — dropping inline
|
|
1712
|
+
* `style=""` is a deliberate hardening tradeoff (removes a CSS-value
|
|
1713
|
+
* injection vector) the regression tests confirm does not break
|
|
1714
|
+
* either renderer's *structural* output.
|
|
1715
|
+
* - `@import` at-rules and non-local-fragment `url(...)` function
|
|
1716
|
+
* values inside `<style>` element text content (external stylesheet
|
|
1717
|
+
* / font / image loads) — see `sanitizeStyleText` below for why the
|
|
1718
|
+
* element itself is not dropped wholesale.
|
|
1719
|
+
* - `xmlns` / `xmlns:*` declarations on any non-root element (namespace
|
|
1720
|
+
* declarations only ever legitimately live on the root `<svg>`).
|
|
1721
|
+
* - Any root-level `xmlns:*` declaration other than a correctly-bound
|
|
1722
|
+
* `xmlns:xlink` (see `isEssentialRootNamespaceDeclaration`). These are
|
|
1723
|
+
* already functionally inert under the strict unprefixed-SVG-element
|
|
1724
|
+
* invariant enforced elsewhere in this file, but are dropped anyway
|
|
1725
|
+
* as defence-in-depth against relying on that invariant alone.
|
|
1726
|
+
* - `xml:base` on any element (root or descendant). Left in place, it
|
|
1727
|
+
* would silently change the base URI every *local-fragment* `href` /
|
|
1728
|
+
* `xlink:href` / `url(#id)` reference in its subtree resolves
|
|
1729
|
+
* against — turning an in-document `#id` reference into an external
|
|
1730
|
+
* `https://evil.example/#id` fetch some SVG consumers follow,
|
|
1731
|
+
* defeating the local-fragment-only guarantees above even though
|
|
1732
|
+
* every individual `href`/`url()` value still looks safe in
|
|
1733
|
+
* isolation.
|
|
1734
|
+
* - `ProcessingInstruction` nodes anywhere in the tree
|
|
1735
|
+
* (`<?xml-stylesheet ...?>` etc).
|
|
1736
|
+
* - `href` / `xlink:href` values that are not a local fragment
|
|
1737
|
+
* reference (`#id`) and not allowed by `policy.allowSafeHref`.
|
|
1738
|
+
* `javascript:`, `data:`, and protocol-relative (`//...`) values are
|
|
1739
|
+
* ALWAYS stripped regardless of policy.
|
|
1740
|
+
* - `url(...)` references inside SVG *presentation attributes* that
|
|
1741
|
+
* accept a `<FuncIRI>` (`fill`, `stroke`, `filter`, `clip-path`,
|
|
1742
|
+
* `mask`, `cursor`, `marker-start`, `marker-mid`, `marker-end`) when
|
|
1743
|
+
* the reference target is not a local fragment (`#id`) — e.g.
|
|
1744
|
+
* `fill="url(https://evil.example/paint.svg)"` or
|
|
1745
|
+
* `filter="url(data:image/svg+xml;base64,...)"`. These are the same
|
|
1746
|
+
* class of external-resource load as `href`/`style` but reachable via
|
|
1747
|
+
* a different attribute name, so they get the same href-style
|
|
1748
|
+
* drop-the-attribute treatment. `url(#localId)` references (the
|
|
1749
|
+
* normal way Mermaid/PlantUML wire arrowhead markers and gradients)
|
|
1750
|
+
* are always preserved.
|
|
1751
|
+
*
|
|
1752
|
+
* What is explicitly preserved:
|
|
1753
|
+
* - `href` / `xlink:href` local fragment references (`#id`) — legitimate
|
|
1754
|
+
* internal `<use>` / gradient / clip-path wiring.
|
|
1755
|
+
* - `https:` `href` values when `policy.allowSafeHref` is `true`.
|
|
1756
|
+
* - `url(#id)` local fragment references in presentation attributes
|
|
1757
|
+
* (`fill="url(#gradient)"`, `marker-end="url(#arrowhead)"`, ...).
|
|
1758
|
+
*
|
|
1759
|
+
* A parse failure (malformed XML) or a sanitized result whose root is not
|
|
1760
|
+
* a single `<svg>` element both return `{ ok: false }` — callers must
|
|
1761
|
+
* treat that as "invalid output" (spec §2 layer 2), never fall back to
|
|
1762
|
+
* the unsanitized input.
|
|
1763
|
+
*/
|
|
1764
|
+
declare function sanitizeSvg(input: string, policy: SanitizeSvgPolicy): SanitizeSvgResult;
|
|
1765
|
+
|
|
1766
|
+
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 PluginReadinessDeclaration, 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/
|
|
481
|
-
* provider redirects back to `/api/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
@@ -1289,6 +1323,32 @@ interface PluginRouterScope {
|
|
|
1289
1323
|
route(method: PluginRouteMethod, path: string, handler: PluginRouteHandler, opts?: PluginRouteOptions): void;
|
|
1290
1324
|
}
|
|
1291
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Declares that, when a specific driver from a specific registry is
|
|
1328
|
+
* selected (`crowi.config.json:<registry>.driver === driver`), this
|
|
1329
|
+
* plugin's own config becomes required to actually work at runtime —
|
|
1330
|
+
* even though the `configSchema` field itself is optional / defaults to
|
|
1331
|
+
* `''` so `configSchema.parse()` alone can't detect "present but
|
|
1332
|
+
* unusable" (see `@crowi/plugin-storage-aws-s3`'s `bucket` and
|
|
1333
|
+
* `@crowi/plugin-search-elasticsearch` / `@crowi/plugin-search-opensearch`'s
|
|
1334
|
+
* `url`, both `z.string().default('')`).
|
|
1335
|
+
*
|
|
1336
|
+
* This is metadata only — it never carries an actual config value.
|
|
1337
|
+
* `registry` / `driver` / every name in `requiredConfigFields` must be
|
|
1338
|
+
* non-empty. The runtime (`PluginManager.getReadinessIssues()`) reads
|
|
1339
|
+
* this once per admin readiness check, cross-references it against the
|
|
1340
|
+
* currently selected driver and the plugin's current config namespace,
|
|
1341
|
+
* and reports which declared fields are still empty — never the values
|
|
1342
|
+
* themselves. See RFC-none / feature-plugin-config-readiness.
|
|
1343
|
+
*/
|
|
1344
|
+
interface PluginReadinessDeclaration {
|
|
1345
|
+
/** Which driver registry this declaration is scoped to. */
|
|
1346
|
+
registry: 'storage' | 'search' | 'mail';
|
|
1347
|
+
/** The driver name (as registered via `registry.register(name, …)`) this declaration applies to. */
|
|
1348
|
+
driver: string;
|
|
1349
|
+
/** `configSchema` field names that must be non-empty for `driver` to actually work once selected. */
|
|
1350
|
+
requiredConfigFields: string[];
|
|
1351
|
+
}
|
|
1292
1352
|
/**
|
|
1293
1353
|
* The contract every Crowi plugin satisfies. Plugins export their
|
|
1294
1354
|
* `CrowiPlugin` object as the package's default export; the runtime
|
|
@@ -1423,6 +1483,14 @@ interface CrowiPlugin {
|
|
|
1423
1483
|
label?: string;
|
|
1424
1484
|
description?: string;
|
|
1425
1485
|
}>>;
|
|
1486
|
+
/**
|
|
1487
|
+
* Declares which of this plugin's own `configSchema` fields must be
|
|
1488
|
+
* non-empty for a specific driver selection to actually work at
|
|
1489
|
+
* runtime (see {@link PluginReadinessDeclaration}). Optional — a
|
|
1490
|
+
* plugin with no readiness declaration is never surfaced by the
|
|
1491
|
+
* admin readiness check, same as before this field existed.
|
|
1492
|
+
*/
|
|
1493
|
+
readiness?: PluginReadinessDeclaration;
|
|
1426
1494
|
/** Storage driver registration. Called once at boot. */
|
|
1427
1495
|
registerStorage?: (registry: StorageRegistry, ctx: PluginContext) => void;
|
|
1428
1496
|
/** Search backend registration. Called once at boot. */
|
|
@@ -1453,7 +1521,7 @@ interface CrowiPlugin {
|
|
|
1453
1521
|
registerHooks?: (events: EventBus, ctx: PluginContext) => void;
|
|
1454
1522
|
/**
|
|
1455
1523
|
* HTTP routes the plugin contributes, mounted at
|
|
1456
|
-
* `/api/
|
|
1524
|
+
* `/api/plugins/<name>/<path>` (the `<name>` path segment guarantees
|
|
1457
1525
|
* that core endpoints and other plugins cannot collide). Used for
|
|
1458
1526
|
* inbound webhooks (Slack events / slash / interactivity), "Test
|
|
1459
1527
|
* connection" buttons, `@action` targets, OAuth callbacks, etc.
|
|
@@ -1533,7 +1601,7 @@ declare const SENSITIVE_FIELD_MARKER = "@sensitive";
|
|
|
1533
1601
|
*
|
|
1534
1602
|
* The admin form renders a button with the given label that calls the
|
|
1535
1603
|
* plugin's contributed endpoint at the given verb / path (relative to
|
|
1536
|
-
* `/api/
|
|
1604
|
+
* `/api/plugins/<name>/`). Useful for "Test connection",
|
|
1537
1605
|
* "Authorise with Google", etc. without forcing every plugin to ship
|
|
1538
1606
|
* its own React component.
|
|
1539
1607
|
*/
|
|
@@ -1553,7 +1621,7 @@ interface ActionAnnotation {
|
|
|
1553
1621
|
label: string;
|
|
1554
1622
|
/** HTTP verb of the plugin endpoint to call. */
|
|
1555
1623
|
method: PluginRouteMethod;
|
|
1556
|
-
/** Path relative to `/api/
|
|
1624
|
+
/** Path relative to `/api/plugins/<name>/`, with leading slash. */
|
|
1557
1625
|
path: string;
|
|
1558
1626
|
}
|
|
1559
1627
|
/**
|
|
@@ -1573,4 +1641,126 @@ interface ActionAnnotation {
|
|
|
1573
1641
|
*/
|
|
1574
1642
|
declare function getActionAnnotation(field: z.ZodTypeAny): ActionAnnotation | null;
|
|
1575
1643
|
|
|
1576
|
-
|
|
1644
|
+
/**
|
|
1645
|
+
* Parses a root `<svg>` element's `viewBox` (`minX minY width height`) to
|
|
1646
|
+
* derive intrinsic pixel dimensions. Shared by
|
|
1647
|
+
* `@crowi/plugin-renderer-mermaid` (its original home — the `<img>`
|
|
1648
|
+
* `width`/`height` intrinsic-size fix) and, since RFC-0023,
|
|
1649
|
+
* `@crowi/plugin-renderer-plantuml`'s SVG sidecar path — both need the
|
|
1650
|
+
* same derivation and both already bundle this package, so it lives
|
|
1651
|
+
* here rather than being copied per plugin.
|
|
1652
|
+
*
|
|
1653
|
+
* Reads attributes off the sanitized SVG source string only — never
|
|
1654
|
+
* decodes any `data:` payload.
|
|
1655
|
+
*/
|
|
1656
|
+
declare function extractSvgDimensions(svg: string): {
|
|
1657
|
+
width: number;
|
|
1658
|
+
height: number;
|
|
1659
|
+
} | null;
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Renderer-specific knobs for `sanitizeSvg`. The sanitizer itself is a
|
|
1663
|
+
* single shared implementation (`sanitize.ts`) — per-renderer differences
|
|
1664
|
+
* are expressed as parameters here, never as a second copy of the DOM
|
|
1665
|
+
* walk (spec §9: "実装自体をrenderer間で複製しない").
|
|
1666
|
+
*/
|
|
1667
|
+
interface SanitizeSvgPolicy {
|
|
1668
|
+
/**
|
|
1669
|
+
* When `true`, `href` / `xlink:href` values pointing at an `https:`
|
|
1670
|
+
* URL are preserved (PlantUML's existing "preserves href to a safe
|
|
1671
|
+
* URL" behaviour, consumed starting Phase 3). When `false`, every
|
|
1672
|
+
* `href` / `xlink:href` is stripped unless it is a local fragment
|
|
1673
|
+
* reference (`#id`) — Mermaid's strict policy (spec §1 layer 1 already
|
|
1674
|
+
* disables Mermaid's own click callbacks, so no link should survive
|
|
1675
|
+
* either).
|
|
1676
|
+
*
|
|
1677
|
+
* Regardless of this flag, `javascript:`, `data:`, and
|
|
1678
|
+
* protocol-relative (`//host/...`) URLs are ALWAYS stripped — no
|
|
1679
|
+
* policy may re-allow those.
|
|
1680
|
+
*/
|
|
1681
|
+
allowSafeHref: boolean;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
type SanitizeSvgResult = {
|
|
1685
|
+
ok: true;
|
|
1686
|
+
svg: string;
|
|
1687
|
+
} | {
|
|
1688
|
+
ok: false;
|
|
1689
|
+
reason: string;
|
|
1690
|
+
};
|
|
1691
|
+
/**
|
|
1692
|
+
* DOM-based SVG sanitizer shared by `@crowi/plugin-renderer-mermaid` and
|
|
1693
|
+
* (from Phase 3) `@crowi/plugin-renderer-plantuml`. Spec §2 layer 2 / §9.
|
|
1694
|
+
*
|
|
1695
|
+
* Design: allowlist-first for elements (unknown/unexpected element names
|
|
1696
|
+
* are dropped with their whole subtree — safer than trying to enumerate
|
|
1697
|
+
* every dangerous tag), then a small set of attribute-level rules that
|
|
1698
|
+
* apply uniformly to every surviving element. This is a from-scratch DOM
|
|
1699
|
+
* walk, not a regex pass (`packages/plugin-renderer-plantuml/src/
|
|
1700
|
+
* sanitize.ts`'s existing implementation is explicitly documented there
|
|
1701
|
+
* as "not a substitute for DOMPurify" — this package is the replacement
|
|
1702
|
+
* both renderers converge on, PlantUML starting Phase 3).
|
|
1703
|
+
*
|
|
1704
|
+
* What gets removed:
|
|
1705
|
+
* - Any element not in `ALLOWED_ELEMENTS` (`script`, `foreignObject`,
|
|
1706
|
+
* `iframe`, `object`, `embed`, SMIL `animate*`/`set`/`discard`, ...) —
|
|
1707
|
+
* dropped together with its entire subtree.
|
|
1708
|
+
* - `on*` event-handler attributes (any casing).
|
|
1709
|
+
* - The `style` attribute (inline styles). Mermaid/PlantUML's real
|
|
1710
|
+
* styling lives in the `<style>` *element* (class-based), which is
|
|
1711
|
+
* sanitized separately below rather than dropped — dropping inline
|
|
1712
|
+
* `style=""` is a deliberate hardening tradeoff (removes a CSS-value
|
|
1713
|
+
* injection vector) the regression tests confirm does not break
|
|
1714
|
+
* either renderer's *structural* output.
|
|
1715
|
+
* - `@import` at-rules and non-local-fragment `url(...)` function
|
|
1716
|
+
* values inside `<style>` element text content (external stylesheet
|
|
1717
|
+
* / font / image loads) — see `sanitizeStyleText` below for why the
|
|
1718
|
+
* element itself is not dropped wholesale.
|
|
1719
|
+
* - `xmlns` / `xmlns:*` declarations on any non-root element (namespace
|
|
1720
|
+
* declarations only ever legitimately live on the root `<svg>`).
|
|
1721
|
+
* - Any root-level `xmlns:*` declaration other than a correctly-bound
|
|
1722
|
+
* `xmlns:xlink` (see `isEssentialRootNamespaceDeclaration`). These are
|
|
1723
|
+
* already functionally inert under the strict unprefixed-SVG-element
|
|
1724
|
+
* invariant enforced elsewhere in this file, but are dropped anyway
|
|
1725
|
+
* as defence-in-depth against relying on that invariant alone.
|
|
1726
|
+
* - `xml:base` on any element (root or descendant). Left in place, it
|
|
1727
|
+
* would silently change the base URI every *local-fragment* `href` /
|
|
1728
|
+
* `xlink:href` / `url(#id)` reference in its subtree resolves
|
|
1729
|
+
* against — turning an in-document `#id` reference into an external
|
|
1730
|
+
* `https://evil.example/#id` fetch some SVG consumers follow,
|
|
1731
|
+
* defeating the local-fragment-only guarantees above even though
|
|
1732
|
+
* every individual `href`/`url()` value still looks safe in
|
|
1733
|
+
* isolation.
|
|
1734
|
+
* - `ProcessingInstruction` nodes anywhere in the tree
|
|
1735
|
+
* (`<?xml-stylesheet ...?>` etc).
|
|
1736
|
+
* - `href` / `xlink:href` values that are not a local fragment
|
|
1737
|
+
* reference (`#id`) and not allowed by `policy.allowSafeHref`.
|
|
1738
|
+
* `javascript:`, `data:`, and protocol-relative (`//...`) values are
|
|
1739
|
+
* ALWAYS stripped regardless of policy.
|
|
1740
|
+
* - `url(...)` references inside SVG *presentation attributes* that
|
|
1741
|
+
* accept a `<FuncIRI>` (`fill`, `stroke`, `filter`, `clip-path`,
|
|
1742
|
+
* `mask`, `cursor`, `marker-start`, `marker-mid`, `marker-end`) when
|
|
1743
|
+
* the reference target is not a local fragment (`#id`) — e.g.
|
|
1744
|
+
* `fill="url(https://evil.example/paint.svg)"` or
|
|
1745
|
+
* `filter="url(data:image/svg+xml;base64,...)"`. These are the same
|
|
1746
|
+
* class of external-resource load as `href`/`style` but reachable via
|
|
1747
|
+
* a different attribute name, so they get the same href-style
|
|
1748
|
+
* drop-the-attribute treatment. `url(#localId)` references (the
|
|
1749
|
+
* normal way Mermaid/PlantUML wire arrowhead markers and gradients)
|
|
1750
|
+
* are always preserved.
|
|
1751
|
+
*
|
|
1752
|
+
* What is explicitly preserved:
|
|
1753
|
+
* - `href` / `xlink:href` local fragment references (`#id`) — legitimate
|
|
1754
|
+
* internal `<use>` / gradient / clip-path wiring.
|
|
1755
|
+
* - `https:` `href` values when `policy.allowSafeHref` is `true`.
|
|
1756
|
+
* - `url(#id)` local fragment references in presentation attributes
|
|
1757
|
+
* (`fill="url(#gradient)"`, `marker-end="url(#arrowhead)"`, ...).
|
|
1758
|
+
*
|
|
1759
|
+
* A parse failure (malformed XML) or a sanitized result whose root is not
|
|
1760
|
+
* a single `<svg>` element both return `{ ok: false }` — callers must
|
|
1761
|
+
* treat that as "invalid output" (spec §2 layer 2), never fall back to
|
|
1762
|
+
* the unsanitized input.
|
|
1763
|
+
*/
|
|
1764
|
+
declare function sanitizeSvg(input: string, policy: SanitizeSvgPolicy): SanitizeSvgResult;
|
|
1765
|
+
|
|
1766
|
+
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 PluginReadinessDeclaration, 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"
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/html.ts","../src/schema-markers.ts"
|
|
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.
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
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",
|
|
@@ -25,20 +25,24 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"hono": "^4.12.
|
|
28
|
+
"hono": "^4.12.34",
|
|
29
29
|
"zod": "^4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/jest": "^29.5.14",
|
|
33
33
|
"@types/node": "^24",
|
|
34
|
-
"hono": "^4.12.
|
|
34
|
+
"hono": "^4.12.34",
|
|
35
35
|
"jest": "^29.7.0",
|
|
36
36
|
"ts-jest": "^29.3.4",
|
|
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",
|