@bquery/bquery 1.2.0 → 1.3.0

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 (305) hide show
  1. package/README.md +501 -427
  2. package/dist/batch-4LAvfLE7.js +13 -0
  3. package/dist/batch-4LAvfLE7.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +36 -0
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +8 -0
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +70 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-COenAZjD.js +145 -0
  41. package/dist/core-COenAZjD.js.map +1 -0
  42. package/dist/core.es.mjs +411 -448
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +2 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +87 -64
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +2 -2
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +2 -2
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.es.mjs +138 -68
  53. package/dist/index.es.mjs.map +1 -1
  54. package/dist/motion/animate.d.ts +25 -0
  55. package/dist/motion/animate.d.ts.map +1 -0
  56. package/dist/motion/easing.d.ts +30 -0
  57. package/dist/motion/easing.d.ts.map +1 -0
  58. package/dist/motion/flip.d.ts +55 -0
  59. package/dist/motion/flip.d.ts.map +1 -0
  60. package/dist/motion/index.d.ts +11 -138
  61. package/dist/motion/index.d.ts.map +1 -1
  62. package/dist/motion/keyframes.d.ts +21 -0
  63. package/dist/motion/keyframes.d.ts.map +1 -0
  64. package/dist/motion/reduced-motion.d.ts +12 -0
  65. package/dist/motion/reduced-motion.d.ts.map +1 -0
  66. package/dist/motion/scroll.d.ts +15 -0
  67. package/dist/motion/scroll.d.ts.map +1 -0
  68. package/dist/motion/spring.d.ts +42 -0
  69. package/dist/motion/spring.d.ts.map +1 -0
  70. package/dist/motion/stagger.d.ts +22 -0
  71. package/dist/motion/stagger.d.ts.map +1 -0
  72. package/dist/motion/timeline.d.ts +21 -0
  73. package/dist/motion/timeline.d.ts.map +1 -0
  74. package/dist/motion/transition.d.ts +22 -0
  75. package/dist/motion/transition.d.ts.map +1 -0
  76. package/dist/motion/types.d.ts +182 -0
  77. package/dist/motion/types.d.ts.map +1 -0
  78. package/dist/motion.es.mjs +320 -61
  79. package/dist/motion.es.mjs.map +1 -1
  80. package/dist/persisted-Dz_ryNuC.js +278 -0
  81. package/dist/persisted-Dz_ryNuC.js.map +1 -0
  82. package/dist/reactive/batch.d.ts +13 -0
  83. package/dist/reactive/batch.d.ts.map +1 -0
  84. package/dist/reactive/computed.d.ts +50 -0
  85. package/dist/reactive/computed.d.ts.map +1 -0
  86. package/dist/reactive/core.d.ts +60 -0
  87. package/dist/reactive/core.d.ts.map +1 -0
  88. package/dist/reactive/effect.d.ts +15 -0
  89. package/dist/reactive/effect.d.ts.map +1 -0
  90. package/dist/reactive/index.d.ts +2 -2
  91. package/dist/reactive/index.d.ts.map +1 -1
  92. package/dist/reactive/internals.d.ts +36 -0
  93. package/dist/reactive/internals.d.ts.map +1 -0
  94. package/dist/reactive/linked.d.ts +36 -0
  95. package/dist/reactive/linked.d.ts.map +1 -0
  96. package/dist/reactive/persisted.d.ts +14 -0
  97. package/dist/reactive/persisted.d.ts.map +1 -0
  98. package/dist/reactive/readonly.d.ts +26 -0
  99. package/dist/reactive/readonly.d.ts.map +1 -0
  100. package/dist/reactive/signal.d.ts +13 -312
  101. package/dist/reactive/signal.d.ts.map +1 -1
  102. package/dist/reactive/type-guards.d.ts +20 -0
  103. package/dist/reactive/type-guards.d.ts.map +1 -0
  104. package/dist/reactive/untrack.d.ts +29 -0
  105. package/dist/reactive/untrack.d.ts.map +1 -0
  106. package/dist/reactive/watch.d.ts +42 -0
  107. package/dist/reactive/watch.d.ts.map +1 -0
  108. package/dist/reactive.es.mjs +30 -163
  109. package/dist/reactive.es.mjs.map +1 -1
  110. package/dist/router/index.d.ts +6 -252
  111. package/dist/router/index.d.ts.map +1 -1
  112. package/dist/router/links.d.ts +44 -0
  113. package/dist/router/links.d.ts.map +1 -0
  114. package/dist/router/match.d.ts +20 -0
  115. package/dist/router/match.d.ts.map +1 -0
  116. package/dist/router/navigation.d.ts +45 -0
  117. package/dist/router/navigation.d.ts.map +1 -0
  118. package/dist/router/query.d.ts +16 -0
  119. package/dist/router/query.d.ts.map +1 -0
  120. package/dist/router/router.d.ts +34 -0
  121. package/dist/router/router.d.ts.map +1 -0
  122. package/dist/router/state.d.ts +27 -0
  123. package/dist/router/state.d.ts.map +1 -0
  124. package/dist/router/types.d.ts +88 -0
  125. package/dist/router/types.d.ts.map +1 -0
  126. package/dist/router/utils.d.ts +65 -0
  127. package/dist/router/utils.d.ts.map +1 -0
  128. package/dist/router.es.mjs +168 -132
  129. package/dist/router.es.mjs.map +1 -1
  130. package/dist/sanitize-1FBEPAFH.js +272 -0
  131. package/dist/sanitize-1FBEPAFH.js.map +1 -0
  132. package/dist/security/constants.d.ts +42 -0
  133. package/dist/security/constants.d.ts.map +1 -0
  134. package/dist/security/csp.d.ts +24 -0
  135. package/dist/security/csp.d.ts.map +1 -0
  136. package/dist/security/index.d.ts +4 -2
  137. package/dist/security/index.d.ts.map +1 -1
  138. package/dist/security/sanitize-core.d.ts +13 -0
  139. package/dist/security/sanitize-core.d.ts.map +1 -0
  140. package/dist/security/sanitize.d.ts +5 -57
  141. package/dist/security/sanitize.d.ts.map +1 -1
  142. package/dist/security/trusted-types.d.ts +25 -0
  143. package/dist/security/trusted-types.d.ts.map +1 -0
  144. package/dist/security/types.d.ts +36 -0
  145. package/dist/security/types.d.ts.map +1 -0
  146. package/dist/security.es.mjs +50 -277
  147. package/dist/security.es.mjs.map +1 -1
  148. package/dist/store/create-store.d.ts +15 -0
  149. package/dist/store/create-store.d.ts.map +1 -0
  150. package/dist/store/define-store.d.ts +28 -0
  151. package/dist/store/define-store.d.ts.map +1 -0
  152. package/dist/store/devtools.d.ts +22 -0
  153. package/dist/store/devtools.d.ts.map +1 -0
  154. package/dist/store/index.d.ts +10 -286
  155. package/dist/store/index.d.ts.map +1 -1
  156. package/dist/store/mapping.d.ts +28 -0
  157. package/dist/store/mapping.d.ts.map +1 -0
  158. package/dist/store/persisted.d.ts +13 -0
  159. package/dist/store/persisted.d.ts.map +1 -0
  160. package/dist/store/plugins.d.ts +13 -0
  161. package/dist/store/plugins.d.ts.map +1 -0
  162. package/dist/store/registry.d.ts +28 -0
  163. package/dist/store/registry.d.ts.map +1 -0
  164. package/dist/store/types.d.ts +71 -0
  165. package/dist/store/types.d.ts.map +1 -0
  166. package/dist/store/utils.d.ts +28 -0
  167. package/dist/store/utils.d.ts.map +1 -0
  168. package/dist/store/watch.d.ts +23 -0
  169. package/dist/store/watch.d.ts.map +1 -0
  170. package/dist/store.es.mjs +22 -224
  171. package/dist/store.es.mjs.map +1 -1
  172. package/dist/type-guards-DRma3-Kc.js +16 -0
  173. package/dist/type-guards-DRma3-Kc.js.map +1 -0
  174. package/dist/untrack-BuEQKH7_.js +6 -0
  175. package/dist/untrack-BuEQKH7_.js.map +1 -0
  176. package/dist/view/directives/bind.d.ts +7 -0
  177. package/dist/view/directives/bind.d.ts.map +1 -0
  178. package/dist/view/directives/class.d.ts +8 -0
  179. package/dist/view/directives/class.d.ts.map +1 -0
  180. package/dist/view/directives/for.d.ts +23 -0
  181. package/dist/view/directives/for.d.ts.map +1 -0
  182. package/dist/view/directives/html.d.ts +7 -0
  183. package/dist/view/directives/html.d.ts.map +1 -0
  184. package/dist/view/directives/if.d.ts +7 -0
  185. package/dist/view/directives/if.d.ts.map +1 -0
  186. package/dist/view/directives/index.d.ts +12 -0
  187. package/dist/view/directives/index.d.ts.map +1 -0
  188. package/dist/view/directives/model.d.ts +7 -0
  189. package/dist/view/directives/model.d.ts.map +1 -0
  190. package/dist/view/directives/on.d.ts +7 -0
  191. package/dist/view/directives/on.d.ts.map +1 -0
  192. package/dist/view/directives/ref.d.ts +7 -0
  193. package/dist/view/directives/ref.d.ts.map +1 -0
  194. package/dist/view/directives/show.d.ts +7 -0
  195. package/dist/view/directives/show.d.ts.map +1 -0
  196. package/dist/view/directives/style.d.ts +7 -0
  197. package/dist/view/directives/style.d.ts.map +1 -0
  198. package/dist/view/directives/text.d.ts +7 -0
  199. package/dist/view/directives/text.d.ts.map +1 -0
  200. package/dist/view/evaluate.d.ts +43 -0
  201. package/dist/view/evaluate.d.ts.map +1 -0
  202. package/dist/view/index.d.ts +3 -93
  203. package/dist/view/index.d.ts.map +1 -1
  204. package/dist/view/mount.d.ts +69 -0
  205. package/dist/view/mount.d.ts.map +1 -0
  206. package/dist/view/process.d.ts +26 -0
  207. package/dist/view/process.d.ts.map +1 -0
  208. package/dist/view/types.d.ts +36 -0
  209. package/dist/view/types.d.ts.map +1 -0
  210. package/dist/view.es.mjs +368 -267
  211. package/dist/view.es.mjs.map +1 -1
  212. package/dist/watch-CXyaBC_9.js +58 -0
  213. package/dist/watch-CXyaBC_9.js.map +1 -0
  214. package/package.json +132 -132
  215. package/src/component/component.ts +289 -0
  216. package/src/component/html.ts +53 -0
  217. package/src/component/index.ts +40 -414
  218. package/src/component/props.ts +116 -0
  219. package/src/component/types.ts +85 -0
  220. package/src/core/collection.ts +588 -454
  221. package/src/core/dom.ts +38 -0
  222. package/src/core/element.ts +746 -740
  223. package/src/core/index.ts +43 -0
  224. package/src/core/utils/array.ts +102 -0
  225. package/src/core/utils/function.ts +110 -0
  226. package/src/core/utils/index.ts +83 -0
  227. package/src/core/utils/misc.ts +82 -0
  228. package/src/core/utils/number.ts +78 -0
  229. package/src/core/utils/object.ts +206 -0
  230. package/src/core/utils/string.ts +112 -0
  231. package/src/core/utils/type-guards.ts +112 -0
  232. package/src/full.ts +187 -150
  233. package/src/index.ts +36 -36
  234. package/src/motion/animate.ts +113 -0
  235. package/src/motion/easing.ts +40 -0
  236. package/src/motion/flip.ts +176 -0
  237. package/src/motion/index.ts +41 -358
  238. package/src/motion/keyframes.ts +46 -0
  239. package/src/motion/reduced-motion.ts +17 -0
  240. package/src/motion/scroll.ts +57 -0
  241. package/src/motion/spring.ts +150 -0
  242. package/src/motion/stagger.ts +43 -0
  243. package/src/motion/timeline.ts +246 -0
  244. package/src/motion/transition.ts +51 -0
  245. package/src/motion/types.ts +198 -0
  246. package/src/reactive/batch.ts +22 -0
  247. package/src/reactive/computed.ts +92 -0
  248. package/src/reactive/core.ts +93 -0
  249. package/src/reactive/effect.ts +43 -0
  250. package/src/reactive/index.ts +23 -22
  251. package/src/reactive/internals.ts +105 -0
  252. package/src/reactive/linked.ts +56 -0
  253. package/src/reactive/persisted.ts +74 -0
  254. package/src/reactive/readonly.ts +35 -0
  255. package/src/reactive/signal.ts +20 -520
  256. package/src/reactive/type-guards.ts +22 -0
  257. package/src/reactive/untrack.ts +31 -0
  258. package/src/reactive/watch.ts +73 -0
  259. package/src/router/index.ts +41 -718
  260. package/src/router/links.ts +130 -0
  261. package/src/router/match.ts +106 -0
  262. package/src/router/navigation.ts +71 -0
  263. package/src/router/query.ts +35 -0
  264. package/src/router/router.ts +211 -0
  265. package/src/router/state.ts +46 -0
  266. package/src/router/types.ts +93 -0
  267. package/src/router/utils.ts +116 -0
  268. package/src/security/constants.ts +209 -0
  269. package/src/security/csp.ts +77 -0
  270. package/src/security/index.ts +4 -12
  271. package/src/security/sanitize-core.ts +343 -0
  272. package/src/security/sanitize.ts +66 -625
  273. package/src/security/trusted-types.ts +69 -0
  274. package/src/security/types.ts +40 -0
  275. package/src/store/create-store.ts +329 -0
  276. package/src/store/define-store.ts +48 -0
  277. package/src/store/devtools.ts +45 -0
  278. package/src/store/index.ts +22 -848
  279. package/src/store/mapping.ts +73 -0
  280. package/src/store/persisted.ts +61 -0
  281. package/src/store/plugins.ts +32 -0
  282. package/src/store/registry.ts +51 -0
  283. package/src/store/types.ts +94 -0
  284. package/src/store/utils.ts +141 -0
  285. package/src/store/watch.ts +52 -0
  286. package/src/view/directives/bind.ts +23 -0
  287. package/src/view/directives/class.ts +70 -0
  288. package/src/view/directives/for.ts +275 -0
  289. package/src/view/directives/html.ts +19 -0
  290. package/src/view/directives/if.ts +30 -0
  291. package/src/view/directives/index.ts +11 -0
  292. package/src/view/directives/model.ts +56 -0
  293. package/src/view/directives/on.ts +41 -0
  294. package/src/view/directives/ref.ts +41 -0
  295. package/src/view/directives/show.ts +26 -0
  296. package/src/view/directives/style.ts +47 -0
  297. package/src/view/directives/text.ts +15 -0
  298. package/src/view/evaluate.ts +274 -0
  299. package/src/view/index.ts +112 -1041
  300. package/src/view/mount.ts +200 -0
  301. package/src/view/process.ts +92 -0
  302. package/src/view/types.ts +44 -0
  303. package/dist/core/utils.d.ts +0 -313
  304. package/dist/core/utils.d.ts.map +0 -1
  305. package/src/core/utils.ts +0 -444
@@ -1,625 +1,66 @@
1
- /**
2
- * Security utilities for HTML sanitization, CSP compatibility, and Trusted Types.
3
- * All DOM writes are sanitized by default to prevent XSS attacks.
4
- *
5
- * @module bquery/security
6
- */
7
-
8
- // ============================================================================
9
- // Types
10
- // ============================================================================
11
-
12
- /**
13
- * Sanitizer configuration options.
14
- */
15
- export interface SanitizeOptions {
16
- /** Allow these additional tags (default: none) */
17
- allowTags?: string[];
18
- /** Allow these additional attributes (default: none) */
19
- allowAttributes?: string[];
20
- /** Allow data-* attributes (default: true) */
21
- allowDataAttributes?: boolean;
22
- /** Strip all tags and return plain text (default: false) */
23
- stripAllTags?: boolean;
24
- }
25
-
26
- /**
27
- * Trusted Types policy name.
28
- */
29
- const POLICY_NAME = 'bquery-sanitizer';
30
-
31
- // ============================================================================
32
- // Trusted Types Support
33
- // ============================================================================
34
-
35
- /** Window interface extended with Trusted Types */
36
- interface TrustedTypesWindow extends Window {
37
- trustedTypes?: {
38
- createPolicy: (
39
- name: string,
40
- rules: { createHTML?: (input: string) => string }
41
- ) => TrustedTypePolicy;
42
- isHTML?: (value: unknown) => boolean;
43
- };
44
- }
45
-
46
- /** Trusted Types policy interface */
47
- interface TrustedTypePolicy {
48
- createHTML: (input: string) => TrustedHTML;
49
- }
50
-
51
- /** Trusted HTML type placeholder for environments without Trusted Types */
52
- interface TrustedHTML {
53
- toString(): string;
54
- }
55
-
56
- /** Cached Trusted Types policy */
57
- let cachedPolicy: TrustedTypePolicy | null = null;
58
-
59
- /**
60
- * Check if Trusted Types API is available.
61
- * @returns True if Trusted Types are supported
62
- */
63
- export const isTrustedTypesSupported = (): boolean => {
64
- return typeof (window as TrustedTypesWindow).trustedTypes !== 'undefined';
65
- };
66
-
67
- /**
68
- * Get or create the bQuery Trusted Types policy.
69
- * @returns The Trusted Types policy or null if unsupported
70
- */
71
- export const getTrustedTypesPolicy = (): TrustedTypePolicy | null => {
72
- if (cachedPolicy) return cachedPolicy;
73
-
74
- const win = window as TrustedTypesWindow;
75
- if (!win.trustedTypes) return null;
76
-
77
- try {
78
- cachedPolicy = win.trustedTypes.createPolicy(POLICY_NAME, {
79
- createHTML: (input: string) => sanitizeHtmlCore(input),
80
- });
81
- return cachedPolicy;
82
- } catch {
83
- // Policy may already exist or be blocked by CSP
84
- console.warn(`bQuery: Could not create Trusted Types policy "${POLICY_NAME}"`);
85
- return null;
86
- }
87
- };
88
-
89
- // ============================================================================
90
- // Default Safe Lists
91
- // ============================================================================
92
-
93
- /**
94
- * Default allowed HTML tags considered safe.
95
- */
96
- const DEFAULT_ALLOWED_TAGS = new Set([
97
- 'a',
98
- 'abbr',
99
- 'address',
100
- 'article',
101
- 'aside',
102
- 'b',
103
- 'bdi',
104
- 'bdo',
105
- 'blockquote',
106
- 'br',
107
- 'button',
108
- 'caption',
109
- 'cite',
110
- 'code',
111
- 'col',
112
- 'colgroup',
113
- 'data',
114
- 'dd',
115
- 'del',
116
- 'details',
117
- 'dfn',
118
- 'div',
119
- 'dl',
120
- 'dt',
121
- 'em',
122
- 'figcaption',
123
- 'figure',
124
- 'footer',
125
- 'form',
126
- 'h1',
127
- 'h2',
128
- 'h3',
129
- 'h4',
130
- 'h5',
131
- 'h6',
132
- 'header',
133
- 'hgroup',
134
- 'hr',
135
- 'i',
136
- 'img',
137
- 'input',
138
- 'ins',
139
- 'kbd',
140
- 'label',
141
- 'legend',
142
- 'li',
143
- 'main',
144
- 'mark',
145
- 'nav',
146
- 'ol',
147
- 'optgroup',
148
- 'option',
149
- 'p',
150
- 'picture',
151
- 'pre',
152
- 'progress',
153
- 'q',
154
- 'rp',
155
- 'rt',
156
- 'ruby',
157
- 's',
158
- 'samp',
159
- 'section',
160
- 'select',
161
- 'small',
162
- 'source',
163
- 'span',
164
- 'strong',
165
- 'sub',
166
- 'summary',
167
- 'sup',
168
- 'table',
169
- 'tbody',
170
- 'td',
171
- 'textarea',
172
- 'tfoot',
173
- 'th',
174
- 'thead',
175
- 'time',
176
- 'tr',
177
- 'u',
178
- 'ul',
179
- 'var',
180
- 'wbr',
181
- ]);
182
-
183
- /**
184
- * Explicitly dangerous tags that should never be allowed.
185
- * These are checked even if somehow added to allowTags.
186
- */
187
- const DANGEROUS_TAGS = new Set([
188
- 'script',
189
- 'iframe',
190
- 'frame',
191
- 'frameset',
192
- 'object',
193
- 'embed',
194
- 'applet',
195
- 'link',
196
- 'meta',
197
- 'style',
198
- 'base',
199
- 'template',
200
- 'slot',
201
- 'math',
202
- 'svg',
203
- 'foreignobject',
204
- 'noscript',
205
- ]);
206
-
207
- /**
208
- * Reserved IDs that could cause DOM clobbering attacks.
209
- * These are prevented to avoid overwriting global browser objects.
210
- */
211
- const RESERVED_IDS = new Set([
212
- // Global objects
213
- 'document',
214
- 'window',
215
- 'location',
216
- 'top',
217
- 'self',
218
- 'parent',
219
- 'frames',
220
- 'history',
221
- 'navigator',
222
- 'screen',
223
- // Dangerous functions
224
- 'alert',
225
- 'confirm',
226
- 'prompt',
227
- 'eval',
228
- 'Function',
229
- // Document properties
230
- 'cookie',
231
- 'domain',
232
- 'referrer',
233
- 'body',
234
- 'head',
235
- 'forms',
236
- 'images',
237
- 'links',
238
- 'scripts',
239
- // DOM traversal properties
240
- 'children',
241
- 'parentNode',
242
- 'firstChild',
243
- 'lastChild',
244
- // Content manipulation
245
- 'innerHTML',
246
- 'outerHTML',
247
- 'textContent',
248
- ]);
249
-
250
- /**
251
- * Default allowed attributes considered safe.
252
- */
253
- const DEFAULT_ALLOWED_ATTRIBUTES = new Set([
254
- 'alt',
255
- 'class',
256
- 'dir',
257
- 'height',
258
- 'hidden',
259
- 'href',
260
- 'id',
261
- 'lang',
262
- 'loading',
263
- 'name',
264
- 'rel',
265
- 'role',
266
- 'src',
267
- 'srcset',
268
- 'style',
269
- 'tabindex',
270
- 'target',
271
- 'title',
272
- 'type',
273
- 'width',
274
- 'aria-*',
275
- ]);
276
-
277
- /**
278
- * Dangerous attribute prefixes to always remove.
279
- */
280
- const DANGEROUS_ATTR_PREFIXES = ['on', 'formaction', 'xlink:', 'xmlns:'];
281
-
282
- /**
283
- * Dangerous URL protocols to block.
284
- */
285
- const DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:', 'file:'];
286
-
287
- // ============================================================================
288
- // Core Sanitization
289
- // ============================================================================
290
-
291
- /**
292
- * Check if an attribute name is allowed.
293
- * @internal
294
- */
295
- const isAllowedAttribute = (
296
- name: string,
297
- allowedSet: Set<string>,
298
- allowDataAttrs: boolean
299
- ): boolean => {
300
- const lowerName = name.toLowerCase();
301
-
302
- // Check dangerous prefixes
303
- for (const prefix of DANGEROUS_ATTR_PREFIXES) {
304
- if (lowerName.startsWith(prefix)) return false;
305
- }
306
-
307
- // Check data attributes
308
- if (allowDataAttrs && lowerName.startsWith('data-')) return true;
309
-
310
- // Check aria attributes (allowed by default)
311
- if (lowerName.startsWith('aria-')) return true;
312
-
313
- // Check explicit allow list
314
- return allowedSet.has(lowerName);
315
- };
316
-
317
- /**
318
- * Check if an ID/name value could cause DOM clobbering.
319
- * @internal
320
- */
321
- const isSafeIdOrName = (value: string): boolean => {
322
- const lowerValue = value.toLowerCase().trim();
323
- return !RESERVED_IDS.has(lowerValue);
324
- };
325
-
326
- /**
327
- * Normalize URL by removing control characters, whitespace, and Unicode tricks.
328
- * Enhanced to prevent various bypass techniques.
329
- * @internal
330
- */
331
- const normalizeUrl = (value: string): string =>
332
- value
333
- // Remove null bytes and control characters
334
- .replace(/[\u0000-\u001F\u007F]+/g, '')
335
- // Remove zero-width characters that could hide malicious content
336
- .replace(/[\u200B-\u200D\uFEFF\u2028\u2029]+/g, '')
337
- // Remove escaped Unicode sequences
338
- .replace(/\\u[\da-fA-F]{4}/g, '')
339
- // Remove whitespace
340
- .replace(/\s+/g, '')
341
- // Normalize case
342
- .toLowerCase();
343
-
344
- /**
345
- * Check if a URL value is safe.
346
- * @internal
347
- */
348
- const isSafeUrl = (value: string): boolean => {
349
- const normalized = normalizeUrl(value);
350
- for (const protocol of DANGEROUS_PROTOCOLS) {
351
- if (normalized.startsWith(protocol)) return false;
352
- }
353
- return true;
354
- };
355
-
356
- /**
357
- * Check if a URL is external (different origin).
358
- * @internal
359
- */
360
- const isExternalUrl = (url: string): boolean => {
361
- try {
362
- // Normalize URL by trimming whitespace
363
- const trimmedUrl = url.trim();
364
-
365
- // Protocol-relative URLs (//example.com) are always external.
366
- // CRITICAL: This check must run before the relative-URL check below;
367
- // otherwise, a protocol-relative URL like "//evil.com" would be treated
368
- // as a non-http(s) relative URL and incorrectly classified as same-origin.
369
- // Handling them up front guarantees correct security classification.
370
- if (trimmedUrl.startsWith('//')) {
371
- return true;
372
- }
373
-
374
- // Normalize URL for case-insensitive protocol checks
375
- const lowerUrl = trimmedUrl.toLowerCase();
376
-
377
- // Check for non-http(s) protocols which are considered external/special
378
- // (mailto:, tel:, ftp:, etc.)
379
- const hasProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmedUrl);
380
- if (hasProtocol && !lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
381
- // These are special protocols, not traditional "external" links
382
- // but we treat them as external for security consistency
383
- return true;
384
- }
385
-
386
- // Relative URLs are not external
387
- if (!lowerUrl.startsWith('http://') && !lowerUrl.startsWith('https://')) {
388
- return false;
389
- }
390
-
391
- // In non-browser environments (e.g., Node.js), treat all absolute URLs as external
392
- if (typeof window === 'undefined' || !window.location) {
393
- return true;
394
- }
395
-
396
- const urlObj = new URL(trimmedUrl, window.location.href);
397
- return urlObj.origin !== window.location.origin;
398
- } catch {
399
- // If URL parsing fails, treat as potentially external for safety
400
- return true;
401
- }
402
- };
403
-
404
- /**
405
- * Core sanitization logic (without Trusted Types wrapper).
406
- * @internal
407
- */
408
- const sanitizeHtmlCore = (html: string, options: SanitizeOptions = {}): string => {
409
- const {
410
- allowTags = [],
411
- allowAttributes = [],
412
- allowDataAttributes = true,
413
- stripAllTags = false,
414
- } = options;
415
-
416
- // Build combined allow sets (excluding dangerous tags even if specified)
417
- const allowedTags = new Set(
418
- [...DEFAULT_ALLOWED_TAGS, ...allowTags.map((t) => t.toLowerCase())].filter(
419
- (tag) => !DANGEROUS_TAGS.has(tag)
420
- )
421
- );
422
- const allowedAttrs = new Set([
423
- ...DEFAULT_ALLOWED_ATTRIBUTES,
424
- ...allowAttributes.map((a) => a.toLowerCase()),
425
- ]);
426
-
427
- // Use template for parsing
428
- const template = document.createElement('template');
429
- template.innerHTML = html;
430
-
431
- if (stripAllTags) {
432
- return template.content.textContent ?? '';
433
- }
434
-
435
- // Walk the DOM tree
436
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT);
437
-
438
- const toRemove: Element[] = [];
439
-
440
- while (walker.nextNode()) {
441
- const el = walker.currentNode as Element;
442
- const tagName = el.tagName.toLowerCase();
443
-
444
- // Remove explicitly dangerous tags even if in allow list
445
- if (DANGEROUS_TAGS.has(tagName)) {
446
- toRemove.push(el);
447
- continue;
448
- }
449
-
450
- // Remove disallowed tags entirely
451
- if (!allowedTags.has(tagName)) {
452
- toRemove.push(el);
453
- continue;
454
- }
455
-
456
- // Process attributes
457
- const attrsToRemove: string[] = [];
458
- for (const attr of Array.from(el.attributes)) {
459
- const attrName = attr.name.toLowerCase();
460
-
461
- // Check if attribute is allowed
462
- if (!isAllowedAttribute(attrName, allowedAttrs, allowDataAttributes)) {
463
- attrsToRemove.push(attr.name);
464
- continue;
465
- }
466
-
467
- // Check for DOM clobbering on id and name attributes
468
- if ((attrName === 'id' || attrName === 'name') && !isSafeIdOrName(attr.value)) {
469
- attrsToRemove.push(attr.name);
470
- continue;
471
- }
472
-
473
- // Validate URL attributes
474
- if (
475
- (attrName === 'href' || attrName === 'src' || attrName === 'srcset') &&
476
- !isSafeUrl(attr.value)
477
- ) {
478
- attrsToRemove.push(attr.name);
479
- }
480
- }
481
-
482
- // Remove disallowed attributes
483
- for (const attrName of attrsToRemove) {
484
- el.removeAttribute(attrName);
485
- }
486
-
487
- // Add rel="noopener noreferrer" to external links for security
488
- if (tagName === 'a') {
489
- const href = el.getAttribute('href');
490
- const target = el.getAttribute('target');
491
- const hasTargetBlank = target?.toLowerCase() === '_blank';
492
- const isExternal = href && isExternalUrl(href);
493
-
494
- // Add security attributes to links opening in new window or external links
495
- if (hasTargetBlank || isExternal) {
496
- const existingRel = el.getAttribute('rel');
497
- const relValues = new Set(
498
- existingRel ? existingRel.split(/\s+/).filter(Boolean) : []
499
- );
500
-
501
- // Add noopener and noreferrer
502
- relValues.add('noopener');
503
- relValues.add('noreferrer');
504
-
505
- el.setAttribute('rel', Array.from(relValues).join(' '));
506
- }
507
- }
508
- }
509
-
510
- // Remove disallowed elements
511
- for (const el of toRemove) {
512
- el.remove();
513
- }
514
-
515
- return template.innerHTML;
516
- };
517
-
518
- // ============================================================================
519
- // Public API
520
- // ============================================================================
521
-
522
- /**
523
- * Sanitize HTML string, removing dangerous elements and attributes.
524
- * Uses Trusted Types when available for CSP compliance.
525
- *
526
- * @param html - The HTML string to sanitize
527
- * @param options - Sanitization options
528
- * @returns Sanitized HTML string
529
- *
530
- * @example
531
- * ```ts
532
- * const safe = sanitizeHtml('<div onclick="alert(1)">Hello</div>');
533
- * // Returns: '<div>Hello</div>'
534
- * ```
535
- */
536
- export const sanitizeHtml = (html: string, options: SanitizeOptions = {}): string => {
537
- return sanitizeHtmlCore(html, options);
538
- };
539
-
540
- /**
541
- * Create a Trusted HTML value for use with Trusted Types-enabled sites.
542
- * Falls back to regular string when Trusted Types are unavailable.
543
- *
544
- * @param html - The HTML string to wrap
545
- * @returns Trusted HTML value or sanitized string
546
- */
547
- export const createTrustedHtml = (html: string): TrustedHTML | string => {
548
- const policy = getTrustedTypesPolicy();
549
- if (policy) {
550
- return policy.createHTML(html);
551
- }
552
- return sanitizeHtml(html);
553
- };
554
-
555
- /**
556
- * Escape HTML entities to prevent XSS.
557
- * Use this for displaying user content as text.
558
- *
559
- * @param text - The text to escape
560
- * @returns Escaped HTML string
561
- *
562
- * @example
563
- * ```ts
564
- * escapeHtml('<script>alert(1)</script>');
565
- * // Returns: '&lt;script&gt;alert(1)&lt;/script&gt;'
566
- * ```
567
- */
568
- export const escapeHtml = (text: string): string => {
569
- const escapeMap: Record<string, string> = {
570
- '&': '&amp;',
571
- '<': '&lt;',
572
- '>': '&gt;',
573
- '"': '&quot;',
574
- "'": '&#x27;',
575
- '`': '&#x60;',
576
- };
577
- return text.replace(/[&<>"'`]/g, (char) => escapeMap[char]);
578
- };
579
-
580
- /**
581
- * Strip all HTML tags and return plain text.
582
- *
583
- * @param html - The HTML string to strip
584
- * @returns Plain text content
585
- */
586
- export const stripTags = (html: string): string => {
587
- return sanitizeHtmlCore(html, { stripAllTags: true });
588
- };
589
-
590
- // ============================================================================
591
- // CSP Helpers
592
- // ============================================================================
593
-
594
- /**
595
- * Generate a nonce for inline scripts/styles.
596
- * Use with Content-Security-Policy nonce directives.
597
- *
598
- * @param length - Nonce length (default: 16)
599
- * @returns Cryptographically random nonce string
600
- */
601
- export const generateNonce = (length: number = 16): string => {
602
- const array = new Uint8Array(length);
603
- crypto.getRandomValues(array);
604
- return btoa(String.fromCharCode(...array))
605
- .replace(/\+/g, '-')
606
- .replace(/\//g, '_')
607
- .replace(/=/g, '');
608
- };
609
-
610
- /**
611
- * Check if a CSP header is present with specific directive.
612
- * Useful for feature detection and fallback strategies.
613
- *
614
- * @param directive - The CSP directive to check (e.g., 'script-src')
615
- * @returns True if the directive appears to be enforced
616
- */
617
- export const hasCSPDirective = (directive: string): boolean => {
618
- // Check meta tag
619
- const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
620
- if (meta) {
621
- const content = meta.getAttribute('content') ?? '';
622
- return content.includes(directive);
623
- }
624
- return false;
625
- };
1
+ /**
2
+ * Security utilities for HTML sanitization.
3
+ * All DOM writes are sanitized by default to prevent XSS attacks.
4
+ *
5
+ * @module bquery/security
6
+ */
7
+
8
+ import { sanitizeHtmlCore } from './sanitize-core';
9
+ import type { SanitizeOptions } from './types';
10
+ export { generateNonce } from './csp';
11
+ export { isTrustedTypesSupported } from './trusted-types';
12
+
13
+ /**
14
+ * Sanitize HTML string, removing dangerous elements and attributes.
15
+ * Uses Trusted Types when available for CSP compliance.
16
+ *
17
+ * @param html - The HTML string to sanitize
18
+ * @param options - Sanitization options
19
+ * @returns Sanitized HTML string
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const safe = sanitizeHtml('<div onclick="alert(1)">Hello</div>');
24
+ * // Returns: '<div>Hello</div>'
25
+ * ```
26
+ */
27
+ export const sanitizeHtml = (html: string, options: SanitizeOptions = {}): string => {
28
+ return sanitizeHtmlCore(html, options);
29
+ };
30
+
31
+ /**
32
+ * Escape HTML entities to prevent XSS.
33
+ * Use this for displaying user content as text.
34
+ *
35
+ * @param text - The text to escape
36
+ * @returns Escaped HTML string
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * escapeHtml('<script>alert(1)</script>');
41
+ * // Returns: '&lt;script&gt;alert(1)&lt;/script&gt;'
42
+ * ```
43
+ */
44
+ export const escapeHtml = (text: string): string => {
45
+ const escapeMap: Record<string, string> = {
46
+ '&': '&amp;',
47
+ '<': '&lt;',
48
+ '>': '&gt;',
49
+ '"': '&quot;',
50
+ "'": '&#x27;',
51
+ '`': '&#x60;',
52
+ };
53
+ return text.replace(/[&<>"'`]/g, (char) => escapeMap[char]);
54
+ };
55
+
56
+ /**
57
+ * Strip all HTML tags and return plain text.
58
+ *
59
+ * @param html - The HTML string to strip
60
+ * @returns Plain text content
61
+ */
62
+ export const stripTags = (html: string): string => {
63
+ return sanitizeHtmlCore(html, { stripAllTags: true });
64
+ };
65
+
66
+ export type { SanitizeOptions } from './types';