@jini-ai/cms 0.5.2 → 0.5.3

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.
Files changed (205) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/media/index.d.ts +0 -3
  46. package/dist/media/index.js +1 -4
  47. package/dist/media/node.d.ts +5 -0
  48. package/dist/media/node.js +6 -0
  49. package/dist/redirects/capture.d.ts +80 -0
  50. package/dist/redirects/capture.js +66 -0
  51. package/dist/redirects/express/index.d.ts +27 -0
  52. package/dist/redirects/express/index.js +40 -0
  53. package/dist/redirects/hit-sink.d.ts +56 -0
  54. package/dist/redirects/hit-sink.js +44 -0
  55. package/dist/redirects/host-config/index.d.ts +40 -0
  56. package/dist/redirects/host-config/index.js +145 -0
  57. package/dist/redirects/index.d.ts +14 -0
  58. package/dist/redirects/index.js +14 -0
  59. package/dist/redirects/matcher.d.ts +58 -0
  60. package/dist/redirects/matcher.js +144 -0
  61. package/dist/redirects/order.d.ts +30 -0
  62. package/dist/redirects/order.js +26 -0
  63. package/dist/redirects/ports.d.ts +242 -0
  64. package/dist/redirects/ports.internal.d.ts +57 -0
  65. package/dist/redirects/ports.internal.js +14 -0
  66. package/dist/redirects/ports.js +2 -0
  67. package/dist/redirects/redirects.d.ts +226 -0
  68. package/dist/redirects/redirects.js +590 -0
  69. package/dist/redirects/referrer-alias.d.ts +31 -0
  70. package/dist/redirects/referrer-alias.js +33 -0
  71. package/dist/redirects/repo.memory.d.ts +51 -0
  72. package/dist/redirects/repo.memory.js +98 -0
  73. package/dist/redirects/reserved-destination.d.ts +58 -0
  74. package/dist/redirects/reserved-destination.js +59 -0
  75. package/dist/redirects/resolver.d.ts +100 -0
  76. package/dist/redirects/resolver.js +87 -0
  77. package/dist/redirects/spa/index.d.ts +30 -0
  78. package/dist/redirects/spa/index.js +27 -0
  79. package/dist/redirects/sql/index.d.ts +4 -0
  80. package/dist/redirects/sql/index.js +4 -0
  81. package/dist/redirects/sql/repo.d.ts +82 -0
  82. package/dist/redirects/sql/repo.js +143 -0
  83. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  84. package/dist/redirects/sql/repo.rows.js +79 -0
  85. package/dist/redirects/sql/row-types.d.ts +34 -0
  86. package/dist/redirects/sql/row-types.js +2 -0
  87. package/dist/redirects/target-gate.d.ts +30 -0
  88. package/dist/redirects/target-gate.js +85 -0
  89. package/dist/redirects/types.d.ts +213 -0
  90. package/dist/redirects/types.js +42 -0
  91. package/dist/seo/absolute-url.d.ts +58 -0
  92. package/dist/seo/absolute-url.js +117 -0
  93. package/dist/seo/dom/index.d.ts +14 -0
  94. package/dist/seo/dom/index.js +52 -0
  95. package/dist/seo/errors.d.ts +35 -0
  96. package/dist/seo/errors.js +35 -0
  97. package/dist/seo/feed.d.ts +49 -0
  98. package/dist/seo/feed.js +61 -0
  99. package/dist/seo/index.d.ts +13 -0
  100. package/dist/seo/index.js +13 -0
  101. package/dist/seo/media.d.ts +57 -0
  102. package/dist/seo/media.js +134 -0
  103. package/dist/seo/page-head.d.ts +18 -0
  104. package/dist/seo/page-head.js +114 -0
  105. package/dist/seo/ports.d.ts +221 -0
  106. package/dist/seo/ports.js +27 -0
  107. package/dist/seo/seo.d.ts +73 -0
  108. package/dist/seo/seo.js +230 -0
  109. package/dist/seo/sitemap.d.ts +139 -0
  110. package/dist/seo/sitemap.js +217 -0
  111. package/dist/seo/types.d.ts +278 -0
  112. package/dist/seo/types.js +2 -0
  113. package/dist/seo/write-service.d.ts +79 -0
  114. package/dist/seo/write-service.js +273 -0
  115. package/dist/seo/xml-escape.d.ts +16 -0
  116. package/dist/seo/xml-escape.js +19 -0
  117. package/dist/settings/express/cms-adapter.d.ts +20 -0
  118. package/dist/settings/express/cms-adapter.js +124 -0
  119. package/dist/settings/express/contracts.d.ts +135 -0
  120. package/dist/settings/express/contracts.js +2 -0
  121. package/dist/settings/express/http.d.ts +10 -0
  122. package/dist/settings/express/http.js +211 -0
  123. package/dist/settings/express/index.d.ts +5 -0
  124. package/dist/settings/express/index.js +5 -0
  125. package/dist/settings/express/shared.d.ts +68 -0
  126. package/dist/settings/express/shared.js +54 -0
  127. package/dist/settings/express/stream.d.ts +24 -0
  128. package/dist/settings/express/stream.js +147 -0
  129. package/dist/settings/index.d.ts +3 -38
  130. package/dist/settings/index.js +3 -35
  131. package/dist/taxonomy/write-service.d.ts +2 -30
  132. package/dist/widgets/authorize-helper.d.ts +42 -0
  133. package/dist/widgets/authorize-helper.js +16 -0
  134. package/dist/widgets/concurrency.d.ts +12 -0
  135. package/dist/widgets/concurrency.js +44 -0
  136. package/dist/widgets/config-validation.d.ts +34 -0
  137. package/dist/widgets/config-validation.js +125 -0
  138. package/dist/widgets/embed-service.d.ts +133 -0
  139. package/dist/widgets/embed-service.js +460 -0
  140. package/dist/widgets/embed-validation.d.ts +23 -0
  141. package/dist/widgets/embed-validation.js +38 -0
  142. package/dist/widgets/entry-payload.d.ts +103 -0
  143. package/dist/widgets/entry-payload.js +160 -0
  144. package/dist/widgets/errors.d.ts +136 -0
  145. package/dist/widgets/errors.js +136 -0
  146. package/dist/widgets/host-ports.d.ts +149 -0
  147. package/dist/widgets/host-ports.js +2 -0
  148. package/dist/widgets/html/html-embeds.d.ts +247 -0
  149. package/dist/widgets/html/html-embeds.js +408 -0
  150. package/dist/widgets/html/index.d.ts +5 -0
  151. package/dist/widgets/html/index.js +4 -0
  152. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  153. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  154. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  155. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  156. package/dist/widgets/index.d.ts +20 -0
  157. package/dist/widgets/index.js +19 -0
  158. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  159. package/dist/widgets/markers/html-attributes.js +216 -0
  160. package/dist/widgets/markers/index.d.ts +3 -0
  161. package/dist/widgets/markers/index.js +3 -0
  162. package/dist/widgets/markers/marker.d.ts +496 -0
  163. package/dist/widgets/markers/marker.js +780 -0
  164. package/dist/widgets/page-embed-types.d.ts +42 -0
  165. package/dist/widgets/page-embed-types.js +42 -0
  166. package/dist/widgets/ports.d.ts +142 -0
  167. package/dist/widgets/ports.js +2 -0
  168. package/dist/widgets/read-service.d.ts +81 -0
  169. package/dist/widgets/read-service.js +79 -0
  170. package/dist/widgets/region-area-service.d.ts +110 -0
  171. package/dist/widgets/region-area-service.js +180 -0
  172. package/dist/widgets/registry.d.ts +217 -0
  173. package/dist/widgets/registry.js +172 -0
  174. package/dist/widgets/repo.memory.d.ts +41 -0
  175. package/dist/widgets/repo.memory.js +48 -0
  176. package/dist/widgets/resolver-service.d.ts +209 -0
  177. package/dist/widgets/resolver-service.js +909 -0
  178. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  179. package/dist/widgets/resolvers/contact-form.js +54 -0
  180. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  181. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  182. package/dist/widgets/resolvers/index.d.ts +65 -0
  183. package/dist/widgets/resolvers/index.js +155 -0
  184. package/dist/widgets/resolvers/menu.d.ts +42 -0
  185. package/dist/widgets/resolvers/menu.js +43 -0
  186. package/dist/widgets/resolvers/ports.d.ts +82 -0
  187. package/dist/widgets/resolvers/ports.js +2 -0
  188. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  189. package/dist/widgets/resolvers/recent-entries.js +165 -0
  190. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  191. package/dist/widgets/resolvers/taxonomy.js +34 -0
  192. package/dist/widgets/sql/index.d.ts +3 -0
  193. package/dist/widgets/sql/index.js +3 -0
  194. package/dist/widgets/sql/repo.d.ts +41 -0
  195. package/dist/widgets/sql/repo.js +82 -0
  196. package/dist/widgets/sql/tables.d.ts +13 -0
  197. package/dist/widgets/sql/tables.js +2 -0
  198. package/dist/widgets/types.d.ts +213 -0
  199. package/dist/widgets/types.js +17 -0
  200. package/dist/widgets/where-used.d.ts +41 -0
  201. package/dist/widgets/where-used.js +11 -0
  202. package/dist/widgets/write-service.d.ts +204 -0
  203. package/dist/widgets/write-service.js +332 -0
  204. package/dist/workspace/delete.d.ts +1 -1
  205. package/package.json +220 -11
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @file The ONE render-time HTML-attribute allowlist for every free-text `htmlAttributes` field this
3
+ * codebase re-parses at render time: media assets, media post-nodes, and (from this task on) widget
4
+ * post-nodes (2026-09-23 embed-attributes-everywhere plan, decision §2). It replaces two prior,
5
+ * narrower boundaries in the same render path:
6
+ *
7
+ * - `@jini-ai/cms/media`'s `parseMediaHtmlAttributes` (the Jini-side twin of
8
+ * `apps/admin/src/features/media/rules.ts`'s `parseMediaHtmlAttributes`, which this file's
9
+ * tokenizer and name pattern are ported from) — rejected `style`/`id`/`class` outright and failed
10
+ * the WHOLE string closed on the first bad token.
11
+ * - `render.ts`'s own `restrictToDataAriaAttributes` — an extra widget-only restriction down to
12
+ * `data-*`/`aria-*`, now folded into this one list instead of being a second filter layered on top.
13
+ *
14
+ * Two behavior changes from that prior boundary, both owner-directed:
15
+ * 1. **The allowlist widens** to include `class`, `id`, `style`, `title`, `role`, `tabindex`, `lang`,
16
+ * `dir`, `hidden`, `translate` and `draggable` — global HTML attributes an author styling or
17
+ * animating a widget/media embed needs — on top of the open-ended `data-*`/`aria-*` families and
18
+ * today's `<img>`/`<video>`-only extras (`loading`, `decoding`, `playsinline`, `muted`, `loop`,
19
+ * `autoplay`, `poster`). One shared list for every element shape, not a list per tag: the owner
20
+ * rejected per-element name filtering (an extra `loading` on a `<div>` wrapper is harmless).
21
+ * 2. **One bad token drops only that token**, not the whole string. Once a name/value pair has been
22
+ * tokenized, it is self-contained — dropping it cannot change how its neighbors are read — so
23
+ * `data-x="1" onclick="…" style="color:red"` keeps `data-x` and `style`, drops only `onclick`. The
24
+ * exception is `malformed`: the tokenizer itself lost sync (e.g. a stray quote), so token
25
+ * boundaries downstream of that point cannot be trusted (value text could re-parse as a name) —
26
+ * that failure mode still discards everything and fails the WHOLE string closed, same as before.
27
+ *
28
+ * The admin (`apps/admin/src/features/media/rules.ts`) keeps its OWN hand-copied allowlist, unchanged
29
+ * by this task — it is a UX hint only (never gates `save()`, incident `a7cce060`), and the
30
+ * browser/Node split between admin and this server-side file is the existing precedent (see that
31
+ * file's own header). Widening ITS list, and Jini's write-path copy, is tracked separately (plan
32
+ * slice J) — until then, an admin Media-library save with `style` still gets Jini's own 400.
33
+ */
34
+ /** Why one parsed attribute token was rejected. `unsafe-url` replaces the old `javascript-url` name
35
+ * (it now also covers `vbscript:`/`data:` schemes, not just `javascript:`) — the admin's own i18n
36
+ * message key for it is unchanged. `unsafe-style` is new: a `style` value carrying a known CSS
37
+ * injection/escape vector. `malformed` is the one reason that still discards the WHOLE parsed
38
+ * result, not just its own token — see this file's header for why. */
39
+ export type EmbedHtmlAttributeRejectionReason = "disallowed-name" | "event-handler" | "unsafe-url" | "unsafe-style" | "malformed";
40
+ export interface EmbedHtmlAttributeError {
41
+ reason: EmbedHtmlAttributeRejectionReason;
42
+ /** The exact attribute name (or, for `malformed`, the unparsable fragment) a caller's error
43
+ * message must name — never a generic "invalid input". */
44
+ attribute: string;
45
+ }
46
+ export interface ParsedEmbedHtmlAttributes {
47
+ /** Lowercased attribute name -> value, for every token that WAS accepted. Never all-or-nothing
48
+ * except on `malformed` (see this file's header): a rejected token is simply absent here while
49
+ * every other accepted token in the same string still lands. A boolean attribute (`muted`,
50
+ * written with no `="..."`) maps to `""`. */
51
+ attributes: Record<string, string>;
52
+ /** Every rejection found, in the order encountered — zero or more. Unlike `attributes`, this is
53
+ * never reset to empty except that a `malformed` entry is always the last one recorded (parsing
54
+ * stops there). */
55
+ errors: EmbedHtmlAttributeError[];
56
+ /** The FIRST rejection in `errors`, or `null` when every token was accepted — kept as its own field
57
+ * for the existing admin-hint callers that only ever showed one reason at a time. */
58
+ error: EmbedHtmlAttributeError | null;
59
+ }
60
+ /**
61
+ * Exact-match attribute names accepted beyond the open-ended `data-`/`aria-` prefix families (see
62
+ * {@link isAllowedEmbedHtmlAttributeName}). Two groups, kept in one flat list because the owner
63
+ * rejected per-element filtering (§2): global attributes any element can carry (styling/animation/
64
+ * a11y), and the pre-existing `<img>`/`<video>`-only extras that are harmless, if meaningless, on
65
+ * any other element.
66
+ */
67
+ export declare const EMBED_HTML_ATTRIBUTE_ALLOWED_NAMES: readonly ["class", "id", "style", "title", "role", "tabindex", "lang", "dir", "hidden", "translate", "draggable", "loading", "decoding", "playsinline", "muted", "loop", "autoplay", "poster"];
68
+ /** Whether `name` is on the allowlist — a well-formed name ({@link EMBED_HTML_ATTRIBUTE_NAME_PATTERN})
69
+ * that is either an exact match against {@link EMBED_HTML_ATTRIBUTE_ALLOWED_NAMES} or carries a
70
+ * `data-`/`aria-` prefix. Case-insensitive, since HTML attribute names are themselves
71
+ * case-insensitive.
72
+ *
73
+ * @complexity O(n) in the name's length (one anchored regex test), then O(1).
74
+ */
75
+ export declare function isAllowedEmbedHtmlAttributeName({ name }: {
76
+ name: string;
77
+ }, _optional?: Record<string, never>): boolean;
78
+ /**
79
+ * Parses free-text HTML attributes (`name="value" name2="value2"`, or a bare boolean `name`) into a
80
+ * validated attribute map, per-token: an accepted token lands in `attributes`, a rejected one lands
81
+ * in `errors` and is simply omitted — this string never fails closed as a whole EXCEPT when the
82
+ * tokenizer itself loses sync (a stray quote or other unparseable fragment between/after matches),
83
+ * in which case `attributes` is `{}` and the sole entry in `errors` has `reason: "malformed"` (see
84
+ * this file's header for why that one case is different).
85
+ *
86
+ * This is a SECURITY boundary, not a syntax convenience: every string this parses was authored in an
87
+ * admin/owner context but rendered on the public site, so free-text HTML-attribute pass-through is a
88
+ * stored-XSS vector the moment it reaches a public page. {@link isAllowedEmbedHtmlAttributeName} is
89
+ * therefore the ONLY path to acceptance for a name; nothing here tries to sanitize an otherwise-
90
+ * disallowed name or value into something safe.
91
+ *
92
+ * @complexity Time O(n) in `text`'s length (one regex pass over it), space O(k) for k accepted
93
+ * attributes plus O(e) for e rejected tokens.
94
+ */
95
+ export declare function parseEmbedHtmlAttributes({ text }: {
96
+ text: string;
97
+ }, _optional?: Record<string, never>): ParsedEmbedHtmlAttributes;
98
+ //# sourceMappingURL=html-attributes.d.ts.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @file The ONE render-time HTML-attribute allowlist for every free-text `htmlAttributes` field this
3
+ * codebase re-parses at render time: media assets, media post-nodes, and (from this task on) widget
4
+ * post-nodes (2026-09-23 embed-attributes-everywhere plan, decision §2). It replaces two prior,
5
+ * narrower boundaries in the same render path:
6
+ *
7
+ * - `@jini-ai/cms/media`'s `parseMediaHtmlAttributes` (the Jini-side twin of
8
+ * `apps/admin/src/features/media/rules.ts`'s `parseMediaHtmlAttributes`, which this file's
9
+ * tokenizer and name pattern are ported from) — rejected `style`/`id`/`class` outright and failed
10
+ * the WHOLE string closed on the first bad token.
11
+ * - `render.ts`'s own `restrictToDataAriaAttributes` — an extra widget-only restriction down to
12
+ * `data-*`/`aria-*`, now folded into this one list instead of being a second filter layered on top.
13
+ *
14
+ * Two behavior changes from that prior boundary, both owner-directed:
15
+ * 1. **The allowlist widens** to include `class`, `id`, `style`, `title`, `role`, `tabindex`, `lang`,
16
+ * `dir`, `hidden`, `translate` and `draggable` — global HTML attributes an author styling or
17
+ * animating a widget/media embed needs — on top of the open-ended `data-*`/`aria-*` families and
18
+ * today's `<img>`/`<video>`-only extras (`loading`, `decoding`, `playsinline`, `muted`, `loop`,
19
+ * `autoplay`, `poster`). One shared list for every element shape, not a list per tag: the owner
20
+ * rejected per-element name filtering (an extra `loading` on a `<div>` wrapper is harmless).
21
+ * 2. **One bad token drops only that token**, not the whole string. Once a name/value pair has been
22
+ * tokenized, it is self-contained — dropping it cannot change how its neighbors are read — so
23
+ * `data-x="1" onclick="…" style="color:red"` keeps `data-x` and `style`, drops only `onclick`. The
24
+ * exception is `malformed`: the tokenizer itself lost sync (e.g. a stray quote), so token
25
+ * boundaries downstream of that point cannot be trusted (value text could re-parse as a name) —
26
+ * that failure mode still discards everything and fails the WHOLE string closed, same as before.
27
+ *
28
+ * The admin (`apps/admin/src/features/media/rules.ts`) keeps its OWN hand-copied allowlist, unchanged
29
+ * by this task — it is a UX hint only (never gates `save()`, incident `a7cce060`), and the
30
+ * browser/Node split between admin and this server-side file is the existing precedent (see that
31
+ * file's own header). Widening ITS list, and Jini's write-path copy, is tracked separately (plan
32
+ * slice J) — until then, an admin Media-library save with `style` still gets Jini's own 400.
33
+ */
34
+ /**
35
+ * Exact-match attribute names accepted beyond the open-ended `data-`/`aria-` prefix families (see
36
+ * {@link isAllowedEmbedHtmlAttributeName}). Two groups, kept in one flat list because the owner
37
+ * rejected per-element filtering (§2): global attributes any element can carry (styling/animation/
38
+ * a11y), and the pre-existing `<img>`/`<video>`-only extras that are harmless, if meaningless, on
39
+ * any other element.
40
+ */
41
+ export const EMBED_HTML_ATTRIBUTE_ALLOWED_NAMES = [
42
+ // Global attributes.
43
+ "class",
44
+ "id",
45
+ "style",
46
+ "title",
47
+ "role",
48
+ "tabindex",
49
+ "lang",
50
+ "dir",
51
+ "hidden",
52
+ "translate",
53
+ "draggable",
54
+ // Pre-existing <img>/<video> extras, kept everywhere (one list, not one per element).
55
+ "loading",
56
+ "decoding",
57
+ "playsinline",
58
+ "muted",
59
+ "loop",
60
+ "autoplay",
61
+ "poster",
62
+ ];
63
+ /**
64
+ * The shape EVERY accepted attribute name must have, checked before allowlist membership (ported
65
+ * unchanged from the 2026-09-07 stored-XSS fix in `apps/admin/src/features/media/rules.ts`). The
66
+ * `data-`/`aria-` families are open-ended by design — no fixed suffix list — so the prefix check
67
+ * alone would accept whatever characters {@link HTML_ATTRIBUTE_TOKEN}'s name class (`[^\s="']+`,
68
+ * which excludes only whitespace, `=` and quotes) lets through; `<`, `>` and `/` are all legal in
69
+ * that class, and the renderer templates a name into ` name="value"` escaping only the VALUE — so a
70
+ * stored `data-x><svg/onload=alert(1)` would close the tag and open a live `<svg onload>`.
71
+ * Constraining the NAME to characters a real HTML attribute name can contain is what makes escaping
72
+ * only the value sufficient.
73
+ */
74
+ const EMBED_HTML_ATTRIBUTE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
75
+ /** Whether `name` is on the allowlist — a well-formed name ({@link EMBED_HTML_ATTRIBUTE_NAME_PATTERN})
76
+ * that is either an exact match against {@link EMBED_HTML_ATTRIBUTE_ALLOWED_NAMES} or carries a
77
+ * `data-`/`aria-` prefix. Case-insensitive, since HTML attribute names are themselves
78
+ * case-insensitive.
79
+ *
80
+ * @complexity O(n) in the name's length (one anchored regex test), then O(1).
81
+ */
82
+ export function isAllowedEmbedHtmlAttributeName({ name }, _optional = {}) {
83
+ const lower = name.toLowerCase();
84
+ if (!EMBED_HTML_ATTRIBUTE_NAME_PATTERN.test(lower))
85
+ return false;
86
+ if (lower.startsWith("data-") || lower.startsWith("aria-"))
87
+ return true;
88
+ return EMBED_HTML_ATTRIBUTE_ALLOWED_NAMES.includes(lower);
89
+ }
90
+ /** The highest ASCII code point {@link isUnsafeUrlValue} strips: every C0 control character plus
91
+ * the ordinary space, i.e. code points `0`-`32` inclusive. Named rather than written as an inline
92
+ * regex control-character range so the linter never has to reason about a literal control
93
+ * character inside a pattern. */
94
+ const ASCII_WHITESPACE_OR_CONTROL_MAX_CODE_POINT = 0x20;
95
+ /**
96
+ * Whether `value`'s scheme, once every ASCII whitespace/control character (code points `0`-`32`) is
97
+ * stripped and the result lowercased, starts with `javascript:`, `vbscript:` or `data:` — the three
98
+ * schemes a browser will still execute or navigate to even when the URL carries interior whitespace
99
+ * a naive end-trim (the old `.trim()`-only check) would miss, e.g. `java\tscript:x`: browsers strip
100
+ * tabs and newlines from inside a URL before parsing its scheme, so the check must too. Filters by
101
+ * code point rather than a regex character class so no literal control character ever has to appear
102
+ * in a pattern.
103
+ *
104
+ * @complexity O(n) in the value's length (one filter pass, one prefix test).
105
+ */
106
+ function isUnsafeUrlValue(value) {
107
+ let stripped = "";
108
+ for (const char of value) {
109
+ if ((char.codePointAt(0) ?? 0) > ASCII_WHITESPACE_OR_CONTROL_MAX_CODE_POINT)
110
+ stripped += char;
111
+ }
112
+ stripped = stripped.toLowerCase();
113
+ return stripped.startsWith("javascript:") || stripped.startsWith("vbscript:") || stripped.startsWith("data:");
114
+ }
115
+ /**
116
+ * Whether a `style` value carries a known CSS injection or CSS-escape-bypass vector:
117
+ * `expression(...)` (legacy IE script-in-CSS), a `javascript:`/`vbscript:` value inside a CSS
118
+ * `url(...)`, `-moz-binding`/`behavior:` (XBL/HTC script bindings), `@import` (pulls in a second,
119
+ * unvetted stylesheet), or ANY backslash — a CSS escape sequence (`\65` is `e`) can respell any of
120
+ * the above past a plain substring check, so a backslash anywhere in the value is rejected outright
121
+ * rather than trying to first decode every possible CSS escape. Plain `url(...)` values (background
122
+ * images) are otherwise allowed — authors here are the owner/admins, not anonymous input.
123
+ *
124
+ * @complexity O(n) in the value's length (one regex test).
125
+ */
126
+ const UNSAFE_STYLE_PATTERN = /expression\(|javascript:|vbscript:|-moz-binding|behavior:|@import|\\/i;
127
+ function isUnsafeStyleValue(value) {
128
+ return UNSAFE_STYLE_PATTERN.test(value);
129
+ }
130
+ /**
131
+ * Classifies one already-tokenized `name`/`value` pair into a rejection reason, or `null` when it is
132
+ * accepted. Split out of {@link parseEmbedHtmlAttributes} into its own function, and into three
133
+ * single-purpose checks ({@link isAllowedEmbedHtmlAttributeName}, {@link isUnsafeUrlValue},
134
+ * {@link isUnsafeStyleValue}), so each rejection reason is its own directly testable branch and this
135
+ * function's own complexity stays low.
136
+ *
137
+ * Order matters: `on*` is checked FIRST (so an `onerror` always reports as `event-handler`, the more
138
+ * actionable reason, never the generic `disallowed-name`), then the URL-scheme check runs against
139
+ * EVERY value regardless of name (matching the pre-existing `javascript:` check this is ported from
140
+ * — the only allowlisted attribute with a URL-shaped value is `poster`, but checking unconditionally
141
+ * costs nothing and adds no gap), then allowlist membership, then — only for an already-allowed
142
+ * `style` — the CSS-specific unsafe-value check.
143
+ *
144
+ * @complexity O(1) — four fixed checks against one already-extracted token; each check's own O(n) in
145
+ * the token's length is linear, not nested.
146
+ */
147
+ function classifyEmbedHtmlAttributeToken(name, value) {
148
+ if (name.startsWith("on"))
149
+ return { reason: "event-handler", attribute: name };
150
+ if (isUnsafeUrlValue(value))
151
+ return { reason: "unsafe-url", attribute: name };
152
+ if (!isAllowedEmbedHtmlAttributeName({ name: name }))
153
+ return { reason: "disallowed-name", attribute: name };
154
+ if (name === "style" && isUnsafeStyleValue(value))
155
+ return { reason: "unsafe-style", attribute: name };
156
+ return null;
157
+ }
158
+ /** Matches one `name`, or one `name="value"`/`name='value'`/`name=value` pair — the same loose shape
159
+ * real HTML attribute syntax allows, ported unchanged from the admin's tokenizer. */
160
+ const HTML_ATTRIBUTE_TOKEN = /([^\s="']+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s"']+)))?/g;
161
+ /**
162
+ * Parses free-text HTML attributes (`name="value" name2="value2"`, or a bare boolean `name`) into a
163
+ * validated attribute map, per-token: an accepted token lands in `attributes`, a rejected one lands
164
+ * in `errors` and is simply omitted — this string never fails closed as a whole EXCEPT when the
165
+ * tokenizer itself loses sync (a stray quote or other unparseable fragment between/after matches),
166
+ * in which case `attributes` is `{}` and the sole entry in `errors` has `reason: "malformed"` (see
167
+ * this file's header for why that one case is different).
168
+ *
169
+ * This is a SECURITY boundary, not a syntax convenience: every string this parses was authored in an
170
+ * admin/owner context but rendered on the public site, so free-text HTML-attribute pass-through is a
171
+ * stored-XSS vector the moment it reaches a public page. {@link isAllowedEmbedHtmlAttributeName} is
172
+ * therefore the ONLY path to acceptance for a name; nothing here tries to sanitize an otherwise-
173
+ * disallowed name or value into something safe.
174
+ *
175
+ * @complexity Time O(n) in `text`'s length (one regex pass over it), space O(k) for k accepted
176
+ * attributes plus O(e) for e rejected tokens.
177
+ */
178
+ export function parseEmbedHtmlAttributes({ text }, _optional = {}) {
179
+ const trimmed = text.trim();
180
+ if (trimmed === "")
181
+ return { attributes: {}, errors: [], error: null };
182
+ const attributes = {};
183
+ const errors = [];
184
+ HTML_ATTRIBUTE_TOKEN.lastIndex = 0;
185
+ let consumed = 0;
186
+ // A `for` loop (rather than `while ((match = …exec(trimmed)) !== null)`) keeps every assignment
187
+ // to `match` in statement position, never inside the loop's own test expression.
188
+ for (let match = HTML_ATTRIBUTE_TOKEN.exec(trimmed); match !== null; match = HTML_ATTRIBUTE_TOKEN.exec(trimmed)) {
189
+ // Non-whitespace text between the previous match and this one is a fragment the token pattern
190
+ // could not parse as a name (e.g. a stray quote) — the tokenizer has lost sync, so everything
191
+ // parsed so far is discarded rather than trusted (see this file's header).
192
+ const skipped = trimmed.slice(consumed, match.index);
193
+ if (skipped.trim() !== "") {
194
+ const malformed = { reason: "malformed", attribute: skipped.trim() };
195
+ errors.push(malformed);
196
+ return { attributes: {}, errors, error: malformed };
197
+ }
198
+ consumed = match.index + match[0].length;
199
+ const name = (match[1] ?? "").toLowerCase();
200
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
201
+ const rejection = classifyEmbedHtmlAttributeToken(name, value);
202
+ if (rejection) {
203
+ errors.push(rejection);
204
+ continue;
205
+ }
206
+ attributes[name] = value;
207
+ }
208
+ const trailing = trimmed.slice(consumed);
209
+ if (trailing.trim() !== "") {
210
+ const malformed = { reason: "malformed", attribute: trailing.trim() };
211
+ errors.push(malformed);
212
+ return { attributes: {}, errors, error: malformed };
213
+ }
214
+ return { attributes, errors, error: errors[0] ?? null };
215
+ }
216
+ //# sourceMappingURL=html-attributes.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./marker.js";
2
+ export * from "./html-attributes.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./marker.js";
2
+ export * from "./html-attributes.js";
3
+ //# sourceMappingURL=index.js.map