@stacksjs/sanitizer 0.1.16 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Stacks.js
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,360 @@
1
+ ![Social Card of stx](https://github.com/stacksjs/stx/blob/main/.github/art/cover.jpg)
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![GitHub Actions][github-actions-src]][github-actions-href]
5
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
6
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
7
+
8
+ # stx
9
+
10
+ A modern templating engine with Vue-like Single File Components, Laravel Blade directives, and Bun-powered performance.
11
+
12
+ ## Features
13
+
14
+ - **Vue-like SFC** - `<script>`, `<template>`, `<style>` structure
15
+ - **Auto-imported Components** - Use `<Card />` directly, no imports needed
16
+ - **Two-way Binding** - `x-model` and `x-text` for reactive forms
17
+ - **Blade Directives** - `@if`, `@foreach`, `@layout`, `@section`
18
+ - **Props & Slots** - Pass data and content to components
19
+ - **200K+ Icons** - Built-in Iconify integration
20
+ - **Custom Directives** - Extend with your own directives
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ bun add bun-plugin-stx
26
+ ```
27
+
28
+ ```toml
29
+ # bunfig.toml
30
+ preload = ["bun-plugin-stx"]
31
+ ```
32
+
33
+ ## Single File Components
34
+
35
+ STX components use a Vue-like structure:
36
+
37
+ ```html
38
+ <!-- components/Greeting.stx -->
39
+ <script server>
40
+ // Server-side only - used for SSR, stripped from output
41
+ const name = props.name || 'World'
42
+ const time = new Date().toLocaleTimeString()
43
+ </script>
44
+
45
+ <template>
46
+ <div class="greeting">
47
+ <h1>Hello, {{ name }}!</h1>
48
+ <p>Current time: {{ time }}</p>
49
+ <slot />
50
+ </div>
51
+ </template>
52
+
53
+ <style>
54
+ .greeting {
55
+ padding: 2rem;
56
+ background: #f5f5f5;
57
+ }
58
+ </style>
59
+ ```
60
+
61
+ ### Script Types
62
+
63
+ | Type | Behavior |
64
+ |------|----------|
65
+ | `<script server>` | SSR only - extracted for variables, stripped from output |
66
+ | `<script client>` | Client only - preserved for browser, skips server evaluation |
67
+ | `<script>` | Both - runs on server AND preserved for client |
68
+
69
+ ## Components
70
+
71
+ Components in `components/` are auto-imported using PascalCase:
72
+
73
+ ```html
74
+ <!-- pages/home.stx -->
75
+ <Header />
76
+
77
+ <main>
78
+ <UserCard name="John" role="Admin" />
79
+ <Card title="Welcome">
80
+ <p>This goes into the slot!</p>
81
+ </Card>
82
+ </main>
83
+
84
+ <Footer />
85
+ ```
86
+
87
+ ### Props
88
+
89
+ Pass data to components via attributes:
90
+
91
+ ```html
92
+ <!-- String prop -->
93
+ <Card title="Hello" />
94
+
95
+ <!-- Expression binding with : -->
96
+ <Card :count="items.length" :active="isActive" />
97
+
98
+ <!-- Mustache interpolation -->
99
+ <Card title="{{ userName }}" />
100
+ ```
101
+
102
+ Access props in components:
103
+
104
+ ```html
105
+ <script server>
106
+ const title = props.title || 'Default'
107
+ const count = props.count || 0
108
+ </script>
109
+
110
+ <template>
111
+ <h1>{{ title }}</h1>
112
+ <p>Count: {{ count }}</p>
113
+ </template>
114
+ ```
115
+
116
+ ### Slots
117
+
118
+ Use `<slot />` to inject content:
119
+
120
+ ```html
121
+ <!-- components/Card.stx -->
122
+ <template>
123
+ <div class="card">
124
+ <h2>{{ props.title }}</h2>
125
+ <slot />
126
+ </div>
127
+ </template>
128
+ ```
129
+
130
+ ```html
131
+ <!-- Usage -->
132
+ <Card title="News">
133
+ <p>This content appears in the slot!</p>
134
+ </Card>
135
+ ```
136
+
137
+ ### Explicit Imports
138
+
139
+ For components outside `components/`, use `@import`:
140
+
141
+ ```html
142
+ @import('layouts/Sidebar')
143
+ @import('shared/Button', 'shared/Modal')
144
+
145
+ <Sidebar />
146
+ <Button label="Click me" />
147
+ ```
148
+
149
+ ## Layouts
150
+
151
+ Wrap pages with common structure using `@layout`:
152
+
153
+ ```html
154
+ <!-- layouts/default.stx -->
155
+ <!DOCTYPE html>
156
+ <html>
157
+ <head>
158
+ <title>{{ title || 'My App' }}</title>
159
+ </head>
160
+ <body>
161
+ <Header />
162
+ <main>
163
+ @yield('content')
164
+ </main>
165
+ <Footer />
166
+ </body>
167
+ </html>
168
+ ```
169
+
170
+ ```html
171
+ <!-- pages/about.stx -->
172
+ @layout('default')
173
+
174
+ @section('content')
175
+ <h1>About Us</h1>
176
+ <p>Welcome to our site.</p>
177
+ @endsection
178
+ ```
179
+
180
+ ## Two-Way Binding (x-element)
181
+
182
+ For reactive forms, use x-element directives:
183
+
184
+ ```html
185
+ <div x-data="{ message: '', count: 0 }">
186
+ <!-- Two-way binding -->
187
+ <input x-model="message" placeholder="Type here..." />
188
+
189
+ <!-- Reactive display -->
190
+ <p>You typed: <span x-text="message"></span></p>
191
+
192
+ <!-- Event handling -->
193
+ <button @click="count++">Increment</button>
194
+ <button @click="count--">Decrement</button>
195
+ <span x-text="count"></span>
196
+ </div>
197
+ ```
198
+
199
+ | Directive | Purpose |
200
+ |-----------|---------|
201
+ | `x-data` | Define reactive scope |
202
+ | `x-model` | Two-way binding for inputs |
203
+ | `x-text` | Reactive text content |
204
+ | `@click` | Event handling |
205
+
206
+ ## Template Directives
207
+
208
+ ### Conditionals
209
+
210
+ ```html
211
+ @if (user.isAdmin)
212
+ <AdminPanel />
213
+ @elseif (user.isEditor)
214
+ <EditorTools />
215
+ @else
216
+ <UserView />
217
+ @endif
218
+ ```
219
+
220
+ ### Loops
221
+
222
+ ```html
223
+ @foreach (items as item)
224
+ <li>{{ item.name }}</li>
225
+ @endforeach
226
+
227
+ @for (let i = 0; i < 5; i++)
228
+ <li>Item {{ i }}</li>
229
+ @endfor
230
+ ```
231
+
232
+ ### Auth Guards
233
+
234
+ ```html
235
+ @auth
236
+ <p>Welcome back, {{ user.name }}!</p>
237
+ @endauth
238
+
239
+ @guest
240
+ <a href="/login">Please log in</a>
241
+ @endguest
242
+ ```
243
+
244
+ ### Output
245
+
246
+ ```html
247
+ <!-- Escaped (safe) -->
248
+ {{ userInput }}
249
+
250
+ <!-- Raw HTML (trusted content only) -->
251
+ {!! trustedHtml !!}
252
+ ```
253
+
254
+ ## Custom Directives
255
+
256
+ Register custom directives in your build:
257
+
258
+ ```typescript
259
+ import { stxPlugin, type CustomDirective } from 'bun-plugin-stx'
260
+
261
+ const uppercase: CustomDirective = {
262
+ name: 'uppercase',
263
+ handler: (content, params) => params[0]?.toUpperCase() || content.toUpperCase()
264
+ }
265
+
266
+ const wrap: CustomDirective = {
267
+ name: 'wrap',
268
+ hasEndTag: true,
269
+ handler: (content, params) => `<div class="${params[0] || 'wrapper'}">${content}</div>`
270
+ }
271
+
272
+ Bun.build({
273
+ entrypoints: ['./src/index.stx'],
274
+ plugins: [stxPlugin({
275
+ customDirectives: [uppercase, wrap]
276
+ })]
277
+ })
278
+ ```
279
+
280
+ ```html
281
+ <!-- Usage -->
282
+ <p>@uppercase('hello world')</p>
283
+
284
+ @wrap('container')
285
+ <p>Wrapped content</p>
286
+ @endwrap
287
+ ```
288
+
289
+ ## Icons
290
+
291
+ 200K+ icons via Iconify:
292
+
293
+ ```html
294
+ <HomeIcon size="24" />
295
+ <SearchIcon size="20" color="#333" />
296
+ ```
297
+
298
+ ```bash
299
+ bun stx iconify list
300
+ bun stx iconify generate material-symbols
301
+ ```
302
+
303
+ ## Complete Example
304
+
305
+ ```html
306
+ <!-- components/TodoApp.stx -->
307
+ <script server>
308
+ const title = props.title || 'My Todos'
309
+ </script>
310
+
311
+ <template>
312
+ <div class="todo-app" x-data="{ todos: [], newTodo: '' }">
313
+ <h1>{{ title }}</h1>
314
+
315
+ <form @submit.prevent="todos.push({ text: newTodo, done: false }); newTodo = ''">
316
+ <input x-model="newTodo" placeholder="Add todo..." />
317
+ <button type="submit">Add</button>
318
+ </form>
319
+
320
+ @if (initialTodos)
321
+ <ul>
322
+ @foreach (initialTodos as todo)
323
+ <li>{{ todo.text }}</li>
324
+ @endforeach
325
+ </ul>
326
+ @endif
327
+ </div>
328
+ </template>
329
+
330
+ <style>
331
+ .todo-app {
332
+ max-width: 400px;
333
+ margin: 0 auto;
334
+ }
335
+ </style>
336
+ ```
337
+
338
+ ## Documentation
339
+
340
+ - [Full Documentation](https://stx.sh)
341
+ - [Syntax Highlighting Guide](./docs/STX_SYNTAX_HIGHLIGHTING.md)
342
+ - [Examples](./examples/)
343
+
344
+ ## Testing
345
+
346
+ ```bash
347
+ bun test
348
+ ```
349
+
350
+ ## License
351
+
352
+ MIT
353
+
354
+ <!-- Badges -->
355
+ [npm-version-src]: https://img.shields.io/npm/v/@stacksjs/stx?style=flat-square
356
+ [npm-version-href]: https://npmjs.com/package/@stacksjs/stx
357
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@stacksjs/stx?style=flat-square
358
+ [npm-downloads-href]: https://npmjs.com/package/@stacksjs/stx
359
+ [github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/stx/ci.yml?style=flat-square&branch=main
360
+ [github-actions-href]: https://github.com/stacksjs/stx/actions?query=workflow%3Aci
@@ -0,0 +1,7 @@
1
+ import { sanitize } from './sanitizer';
2
+ export * from './presets';
3
+ export { basic, getPreset, markdown, relaxed, strict } from './presets';
4
+ export * from './sanitizer';
5
+ export { escape, isSafe, sanitize, sanitizeWithInfo, stripTags } from './sanitizer';
6
+ export * from './types';
7
+ export default sanitize;
package/dist/index.js ADDED
@@ -0,0 +1,468 @@
1
+ // @bun
2
+ // src/presets.ts
3
+ var strict = {
4
+ allowedTags: [
5
+ "p",
6
+ "br",
7
+ "strong",
8
+ "em",
9
+ "u",
10
+ "span",
11
+ "h1",
12
+ "h2",
13
+ "h3",
14
+ "h4",
15
+ "h5",
16
+ "h6",
17
+ "ul",
18
+ "ol",
19
+ "li",
20
+ "code",
21
+ "pre"
22
+ ],
23
+ allowedAttributes: {
24
+ "*": ["class", "id"]
25
+ },
26
+ allowedSchemes: ["http", "https", "mailto"],
27
+ allowDataAttributes: false,
28
+ allowAriaAttributes: true,
29
+ stripTags: true,
30
+ allowComments: false
31
+ };
32
+ var basic = {
33
+ allowedTags: [
34
+ "p",
35
+ "br",
36
+ "strong",
37
+ "em",
38
+ "u",
39
+ "span",
40
+ "div",
41
+ "h1",
42
+ "h2",
43
+ "h3",
44
+ "h4",
45
+ "h5",
46
+ "h6",
47
+ "ul",
48
+ "ol",
49
+ "li",
50
+ "a",
51
+ "img",
52
+ "code",
53
+ "pre",
54
+ "blockquote",
55
+ "table",
56
+ "thead",
57
+ "tbody",
58
+ "tr",
59
+ "th",
60
+ "td"
61
+ ],
62
+ allowedAttributes: {
63
+ "*": ["class", "id"],
64
+ a: ["class", "id", "href", "title", "target", "rel"],
65
+ img: ["class", "id", "src", "alt", "title", "width", "height"],
66
+ td: ["class", "id", "colspan", "rowspan", "align"],
67
+ th: ["class", "id", "colspan", "rowspan", "align", "scope"]
68
+ },
69
+ allowedSchemes: ["http", "https", "mailto", "tel"],
70
+ allowDataAttributes: false,
71
+ allowAriaAttributes: true,
72
+ stripTags: true,
73
+ allowComments: false
74
+ };
75
+ var relaxed = {
76
+ allowedTags: [
77
+ "p",
78
+ "br",
79
+ "strong",
80
+ "em",
81
+ "u",
82
+ "span",
83
+ "div",
84
+ "section",
85
+ "article",
86
+ "aside",
87
+ "h1",
88
+ "h2",
89
+ "h3",
90
+ "h4",
91
+ "h5",
92
+ "h6",
93
+ "ul",
94
+ "ol",
95
+ "li",
96
+ "dl",
97
+ "dt",
98
+ "dd",
99
+ "a",
100
+ "img",
101
+ "figure",
102
+ "figcaption",
103
+ "code",
104
+ "pre",
105
+ "kbd",
106
+ "samp",
107
+ "var",
108
+ "blockquote",
109
+ "q",
110
+ "cite",
111
+ "table",
112
+ "thead",
113
+ "tbody",
114
+ "tfoot",
115
+ "tr",
116
+ "th",
117
+ "td",
118
+ "caption",
119
+ "del",
120
+ "ins",
121
+ "sub",
122
+ "sup",
123
+ "abbr",
124
+ "address",
125
+ "time",
126
+ "hr",
127
+ "video",
128
+ "audio",
129
+ "source",
130
+ "track"
131
+ ],
132
+ allowedAttributes: {
133
+ "*": ["class", "id", "title"],
134
+ a: ["class", "id", "href", "title", "target", "rel"],
135
+ img: ["class", "id", "src", "srcset", "alt", "title", "width", "height", "loading"],
136
+ video: ["class", "id", "src", "width", "height", "controls", "preload", "poster"],
137
+ audio: ["class", "id", "src", "controls", "preload"],
138
+ source: ["src", "type"],
139
+ track: ["src", "kind", "srclang", "label"],
140
+ td: ["class", "id", "colspan", "rowspan", "align"],
141
+ th: ["class", "id", "colspan", "rowspan", "align", "scope"],
142
+ time: ["class", "id", "datetime"],
143
+ abbr: ["class", "id", "title"]
144
+ },
145
+ allowedSchemes: ["http", "https", "mailto", "tel", "data"],
146
+ allowDataAttributes: true,
147
+ allowAriaAttributes: true,
148
+ stripTags: true,
149
+ allowComments: false
150
+ };
151
+ var markdown = {
152
+ allowedTags: [
153
+ "p",
154
+ "br",
155
+ "strong",
156
+ "em",
157
+ "span",
158
+ "h1",
159
+ "h2",
160
+ "h3",
161
+ "h4",
162
+ "h5",
163
+ "h6",
164
+ "ul",
165
+ "ol",
166
+ "li",
167
+ "a",
168
+ "img",
169
+ "code",
170
+ "pre",
171
+ "blockquote",
172
+ "table",
173
+ "thead",
174
+ "tbody",
175
+ "tr",
176
+ "th",
177
+ "td",
178
+ "del",
179
+ "ins",
180
+ "hr",
181
+ "input"
182
+ ],
183
+ allowedAttributes: {
184
+ "*": ["class", "id"],
185
+ a: ["class", "id", "href", "title"],
186
+ img: ["class", "id", "src", "alt", "title"],
187
+ code: ["class"],
188
+ td: ["align"],
189
+ th: ["align", "scope"],
190
+ input: ["type", "checked", "disabled"]
191
+ },
192
+ allowedSchemes: ["http", "https", "mailto"],
193
+ allowDataAttributes: false,
194
+ allowAriaAttributes: false,
195
+ stripTags: true,
196
+ allowComments: false
197
+ };
198
+ function getPreset(name) {
199
+ switch (name) {
200
+ case "strict":
201
+ return strict;
202
+ case "basic":
203
+ return basic;
204
+ case "relaxed":
205
+ return relaxed;
206
+ case "markdown":
207
+ return markdown;
208
+ default:
209
+ return basic;
210
+ }
211
+ }
212
+
213
+ // src/sanitizer.ts
214
+ var DEFAULT_OPTIONS = {
215
+ allowedTags: [],
216
+ allowedAttributes: {},
217
+ allowedSchemes: ["http", "https", "mailto"],
218
+ allowDataAttributes: false,
219
+ allowAriaAttributes: true,
220
+ stripTags: true,
221
+ allowComments: false
222
+ };
223
+ var DANGEROUS_TAGS = [
224
+ "script",
225
+ "iframe",
226
+ "object",
227
+ "embed",
228
+ "applet",
229
+ "base",
230
+ "link",
231
+ "meta",
232
+ "style"
233
+ ];
234
+ var URL_ATTRIBUTES = [
235
+ "href",
236
+ "src",
237
+ "action",
238
+ "formaction",
239
+ "data",
240
+ "poster",
241
+ "cite",
242
+ "background",
243
+ "longdesc"
244
+ ];
245
+ var EVENT_ATTRIBUTES_REGEX = /^on\w+/i;
246
+ function sanitize(html, options) {
247
+ const result = sanitizeWithInfo(html, options);
248
+ return result.html;
249
+ }
250
+ function sanitizeWithInfo(html, options) {
251
+ const opts = typeof options === "string" ? getPreset(options) : { ...DEFAULT_OPTIONS, ...options };
252
+ const removedTags = [];
253
+ const removedAttributes = [];
254
+ let result = html;
255
+ let modified = false;
256
+ if (!opts.allowComments) {
257
+ const commentRegex = /<!--[\s\S]*?-->/g;
258
+ if (commentRegex.test(result)) {
259
+ result = result.replace(commentRegex, "");
260
+ modified = true;
261
+ }
262
+ }
263
+ const tagRegex = /<(\/?)([a-zA-Z][\w-]*)([^>]*)>/g;
264
+ result = result.replace(tagRegex, (match, closing, tagName, attributesStr) => {
265
+ const lowerTag = tagName.toLowerCase();
266
+ if (DANGEROUS_TAGS.includes(lowerTag)) {
267
+ if (!removedTags.includes(lowerTag)) {
268
+ removedTags.push(lowerTag);
269
+ }
270
+ modified = true;
271
+ return opts.stripTags ? "" : escapeHtml(match);
272
+ }
273
+ const allowedTags = opts.allowedTags || [];
274
+ if (allowedTags.length > 0 && !allowedTags.includes(lowerTag)) {
275
+ if (!removedTags.includes(lowerTag)) {
276
+ removedTags.push(lowerTag);
277
+ }
278
+ modified = true;
279
+ return opts.stripTags ? "" : escapeHtml(match);
280
+ }
281
+ if (closing) {
282
+ return match;
283
+ }
284
+ const sanitizedAttrs = sanitizeAttributes(attributesStr, lowerTag, opts, removedAttributes);
285
+ if (sanitizedAttrs !== attributesStr) {
286
+ modified = true;
287
+ }
288
+ if (opts.transformTag) {
289
+ const attributes = parseAttributes(sanitizedAttrs);
290
+ const transformed = opts.transformTag(lowerTag, attributes);
291
+ if (transformed === null) {
292
+ if (!removedTags.includes(lowerTag)) {
293
+ removedTags.push(lowerTag);
294
+ }
295
+ modified = true;
296
+ return "";
297
+ }
298
+ if (transformed.tagName !== lowerTag || JSON.stringify(transformed.attributes) !== JSON.stringify(attributes)) {
299
+ modified = true;
300
+ return buildTag(transformed.tagName, transformed.attributes);
301
+ }
302
+ }
303
+ return `<${tagName}${sanitizedAttrs}>`;
304
+ });
305
+ return {
306
+ html: result,
307
+ modified,
308
+ removedTags: removedTags.length > 0 ? removedTags : undefined,
309
+ removedAttributes: removedAttributes.length > 0 ? removedAttributes : undefined
310
+ };
311
+ }
312
+ function sanitizeAttributes(attributesStr, tagName, options, removedAttributes) {
313
+ if (!attributesStr.trim()) {
314
+ return attributesStr;
315
+ }
316
+ const attributes = parseAttributes(attributesStr);
317
+ const sanitized = {};
318
+ for (const [name, value] of Object.entries(attributes)) {
319
+ const lowerName = name.toLowerCase();
320
+ if (EVENT_ATTRIBUTES_REGEX.test(lowerName)) {
321
+ if (!removedAttributes.includes(lowerName)) {
322
+ removedAttributes.push(lowerName);
323
+ }
324
+ continue;
325
+ }
326
+ if (!isAttributeAllowed(lowerName, tagName, options)) {
327
+ if (!removedAttributes.includes(lowerName)) {
328
+ removedAttributes.push(lowerName);
329
+ }
330
+ continue;
331
+ }
332
+ if (URL_ATTRIBUTES.includes(lowerName)) {
333
+ if (!isUrlSafe(value, options)) {
334
+ if (!removedAttributes.includes(lowerName)) {
335
+ removedAttributes.push(lowerName);
336
+ }
337
+ continue;
338
+ }
339
+ }
340
+ if (lowerName === "style" && options.allowedStyles) {
341
+ sanitized[name] = sanitizeStyle(value, options.allowedStyles);
342
+ } else {
343
+ sanitized[name] = value;
344
+ }
345
+ }
346
+ return buildAttributes(sanitized);
347
+ }
348
+ function isAttributeAllowed(attrName, tagName, options) {
349
+ const allowedAttrs = options.allowedAttributes;
350
+ if (!allowedAttrs) {
351
+ return false;
352
+ }
353
+ if (attrName.startsWith("data-")) {
354
+ return options.allowDataAttributes || false;
355
+ }
356
+ if (attrName.startsWith("aria-")) {
357
+ return options.allowAriaAttributes || false;
358
+ }
359
+ if (Array.isArray(allowedAttrs)) {
360
+ return allowedAttrs.includes(attrName);
361
+ }
362
+ if (allowedAttrs[tagName]?.includes(attrName)) {
363
+ return true;
364
+ }
365
+ if (allowedAttrs["*"]?.includes(attrName)) {
366
+ return true;
367
+ }
368
+ return false;
369
+ }
370
+ function isUrlSafe(url, options) {
371
+ if (options.urlValidator) {
372
+ return options.urlValidator(url);
373
+ }
374
+ const trimmed = url.trim().toLowerCase();
375
+ const cleaned = trimmed.replace(/[\s\x00-\x1F]/g, "");
376
+ const dangerousProtocols = ["javascript:", "data:text/html", "vbscript:", "file:"];
377
+ for (const protocol of dangerousProtocols) {
378
+ if (cleaned.startsWith(protocol)) {
379
+ return false;
380
+ }
381
+ }
382
+ if (options.allowedSchemes && options.allowedSchemes.length > 0) {
383
+ if (!cleaned.includes(":")) {
384
+ return true;
385
+ }
386
+ const hasAllowedScheme = options.allowedSchemes.some((scheme) => {
387
+ return cleaned.startsWith(`${scheme}:`);
388
+ });
389
+ if (!hasAllowedScheme) {
390
+ return false;
391
+ }
392
+ }
393
+ return true;
394
+ }
395
+ function sanitizeStyle(style, allowedStyles) {
396
+ const properties = style.split(";").filter((p) => p.trim());
397
+ const sanitized = [];
398
+ for (const prop of properties) {
399
+ const colonIndex = prop.indexOf(":");
400
+ if (colonIndex === -1)
401
+ continue;
402
+ const name = prop.substring(0, colonIndex).trim().toLowerCase();
403
+ const value = prop.substring(colonIndex + 1).trim();
404
+ if (allowedStyles.includes(name)) {
405
+ if (!value.toLowerCase().includes("javascript:") && !value.toLowerCase().includes("expression(")) {
406
+ sanitized.push(`${name}: ${value}`);
407
+ }
408
+ }
409
+ }
410
+ return sanitized.join("; ");
411
+ }
412
+ function parseAttributes(attributesStr) {
413
+ const attributes = {};
414
+ const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
415
+ let match;
416
+ while (match = attrRegex.exec(attributesStr)) {
417
+ const name = match[1];
418
+ const value = match[2] || match[3] || match[4] || "";
419
+ attributes[name] = value;
420
+ }
421
+ return attributes;
422
+ }
423
+ function buildAttributes(attributes) {
424
+ const parts = [];
425
+ for (const [name, value] of Object.entries(attributes)) {
426
+ if (value === "") {
427
+ parts.push(name);
428
+ } else {
429
+ const escaped = value.replace(/"/g, "&quot;");
430
+ parts.push(`${name}="${escaped}"`);
431
+ }
432
+ }
433
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
434
+ }
435
+ function buildTag(tagName, attributes) {
436
+ return `<${tagName}${buildAttributes(attributes)}>`;
437
+ }
438
+ function escapeHtml(text) {
439
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
440
+ }
441
+ function isSafe(html, options) {
442
+ const result = sanitizeWithInfo(html, options);
443
+ return !result.modified;
444
+ }
445
+ function stripTags(html) {
446
+ return html.replace(/<[^>]*>/g, "");
447
+ }
448
+ function escape(text) {
449
+ return escapeHtml(text);
450
+ }
451
+
452
+ // src/index.ts
453
+ var src_default = sanitize;
454
+ export {
455
+ stripTags,
456
+ strict,
457
+ sanitizeWithInfo,
458
+ sanitize,
459
+ relaxed,
460
+ markdown,
461
+ isSafe,
462
+ getPreset,
463
+ escape,
464
+ src_default as default,
465
+ basic
466
+ };
467
+
468
+ //# debugId=11C850CF49E4930C64756E2164756E21
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/presets.ts", "../src/sanitizer.ts", "../src/index.ts"],
4
+ "sourcesContent": [
5
+ "import type { SanitizerOptions } from './types'\n\n/**\n * Strict preset - Only safe, basic formatting\n */\nexport const strict: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'code',\n 'pre',\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n },\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Basic preset - Common safe HTML elements\n */\nexport const basic: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'div',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'a',\n 'img',\n 'code',\n 'pre',\n 'blockquote',\n 'table',\n 'thead',\n 'tbody',\n 'tr',\n 'th',\n 'td',\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n 'a': ['class', 'id', 'href', 'title', 'target', 'rel'],\n 'img': ['class', 'id', 'src', 'alt', 'title', 'width', 'height'],\n 'td': ['class', 'id', 'colspan', 'rowspan', 'align'],\n 'th': ['class', 'id', 'colspan', 'rowspan', 'align', 'scope'],\n },\n allowedSchemes: ['http', 'https', 'mailto', 'tel'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Relaxed preset - More permissive, includes media and interactive elements\n */\nexport const relaxed: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'div',\n 'section',\n 'article',\n 'aside',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'dl',\n 'dt',\n 'dd',\n 'a',\n 'img',\n 'figure',\n 'figcaption',\n 'code',\n 'pre',\n 'kbd',\n 'samp',\n 'var',\n 'blockquote',\n 'q',\n 'cite',\n 'table',\n 'thead',\n 'tbody',\n 'tfoot',\n 'tr',\n 'th',\n 'td',\n 'caption',\n 'del',\n 'ins',\n 'sub',\n 'sup',\n 'abbr',\n 'address',\n 'time',\n 'hr',\n 'video',\n 'audio',\n 'source',\n 'track',\n ],\n allowedAttributes: {\n '*': ['class', 'id', 'title'],\n 'a': ['class', 'id', 'href', 'title', 'target', 'rel'],\n 'img': ['class', 'id', 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],\n 'video': ['class', 'id', 'src', 'width', 'height', 'controls', 'preload', 'poster'],\n 'audio': ['class', 'id', 'src', 'controls', 'preload'],\n 'source': ['src', 'type'],\n 'track': ['src', 'kind', 'srclang', 'label'],\n 'td': ['class', 'id', 'colspan', 'rowspan', 'align'],\n 'th': ['class', 'id', 'colspan', 'rowspan', 'align', 'scope'],\n 'time': ['class', 'id', 'datetime'],\n 'abbr': ['class', 'id', 'title'],\n },\n allowedSchemes: ['http', 'https', 'mailto', 'tel', 'data'],\n allowDataAttributes: true,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Markdown preset - Optimized for markdown-generated HTML\n */\nexport const markdown: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'span',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'a',\n 'img',\n 'code',\n 'pre',\n 'blockquote',\n 'table',\n 'thead',\n 'tbody',\n 'tr',\n 'th',\n 'td',\n 'del',\n 'ins',\n 'hr',\n 'input', // For task lists\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n 'a': ['class', 'id', 'href', 'title'],\n 'img': ['class', 'id', 'src', 'alt', 'title'],\n 'code': ['class'], // For syntax highlighting\n 'td': ['align'],\n 'th': ['align', 'scope'],\n 'input': ['type', 'checked', 'disabled'], // For task lists\n },\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: false,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Get preset by name\n */\nexport function getPreset(name: string): SanitizerOptions {\n switch (name) {\n case 'strict':\n return strict\n case 'basic':\n return basic\n case 'relaxed':\n return relaxed\n case 'markdown':\n return markdown\n default:\n return basic\n }\n}\n",
6
+ "import type { SanitizeResult, SanitizerOptions, SanitizerPreset } from './types'\nimport { getPreset } from './presets'\n\n/**\n * Fast, native HTML sanitizer optimized for Bun\n * Provides DOMPurify-like features with performance focus\n */\n\nconst DEFAULT_OPTIONS: SanitizerOptions = {\n allowedTags: [],\n allowedAttributes: {},\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n// Dangerous tags that should never be allowed\nconst DANGEROUS_TAGS = [\n 'script',\n 'iframe',\n 'object',\n 'embed',\n 'applet',\n 'base',\n 'link',\n 'meta',\n 'style',\n]\n\n// URL attributes that need validation\nconst URL_ATTRIBUTES = [\n 'href',\n 'src',\n 'action',\n 'formaction',\n 'data',\n 'poster',\n 'cite',\n 'background',\n 'longdesc',\n]\n\n// Event handler attributes (always removed)\nconst EVENT_ATTRIBUTES_REGEX = /^on\\w+/i\n\n/**\n * Sanitize HTML content\n */\nexport function sanitize(\n html: string,\n options?: SanitizerOptions | SanitizerPreset,\n): string {\n const result = sanitizeWithInfo(html, options)\n return result.html\n}\n\n/**\n * Sanitize HTML content with detailed information\n */\nexport function sanitizeWithInfo(\n html: string,\n options?: SanitizerOptions | SanitizerPreset,\n): SanitizeResult {\n // Load preset if string provided\n const opts: SanitizerOptions = typeof options === 'string'\n ? getPreset(options)\n : { ...DEFAULT_OPTIONS, ...options }\n\n const removedTags: string[] = []\n const removedAttributes: string[] = []\n\n // Use a simple regex-based parser for performance\n let result = html\n let modified = false\n\n // Remove comments if not allowed\n if (!opts.allowComments) {\n const commentRegex = /<!--[\\s\\S]*?-->/g\n if (commentRegex.test(result)) {\n result = result.replace(commentRegex, '')\n modified = true\n }\n }\n\n // Parse and sanitize tags\n // eslint-disable-next-line regexp/use-ignore-case\n const tagRegex = /<(\\/?)([a-zA-Z][\\w-]*)([^>]*)>/g\n result = result.replace(tagRegex, (match, closing, tagName, attributesStr) => {\n const lowerTag = tagName.toLowerCase()\n\n // Always remove dangerous tags\n if (DANGEROUS_TAGS.includes(lowerTag)) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return opts.stripTags ? '' : escapeHtml(match)\n }\n\n // Check if tag is allowed\n const allowedTags = opts.allowedTags || []\n if (allowedTags.length > 0 && !allowedTags.includes(lowerTag)) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return opts.stripTags ? '' : escapeHtml(match)\n }\n\n // For closing tags, just return as-is (already validated by opening tag)\n if (closing) {\n return match\n }\n\n // Sanitize attributes\n const sanitizedAttrs = sanitizeAttributes(\n attributesStr,\n lowerTag,\n opts,\n removedAttributes,\n )\n\n if (sanitizedAttrs !== attributesStr) {\n modified = true\n }\n\n // Transform tag if transformer provided\n if (opts.transformTag) {\n const attributes = parseAttributes(sanitizedAttrs)\n const transformed = opts.transformTag(lowerTag, attributes)\n\n if (transformed === null) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return ''\n }\n\n if (transformed.tagName !== lowerTag || JSON.stringify(transformed.attributes) !== JSON.stringify(attributes)) {\n modified = true\n return buildTag(transformed.tagName, transformed.attributes)\n }\n }\n\n return `<${tagName}${sanitizedAttrs}>`\n })\n\n return {\n html: result,\n modified,\n removedTags: removedTags.length > 0 ? removedTags : undefined,\n removedAttributes: removedAttributes.length > 0 ? removedAttributes : undefined,\n }\n}\n\n/**\n * Sanitize attributes\n */\nfunction sanitizeAttributes(\n attributesStr: string,\n tagName: string,\n options: SanitizerOptions,\n removedAttributes: string[],\n): string {\n if (!attributesStr.trim()) {\n return attributesStr\n }\n\n const attributes = parseAttributes(attributesStr)\n const sanitized: Record<string, string> = {}\n\n for (const [name, value] of Object.entries(attributes)) {\n const lowerName = name.toLowerCase()\n\n // Remove event handlers\n if (EVENT_ATTRIBUTES_REGEX.test(lowerName)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n\n // Check if attribute is allowed\n if (!isAttributeAllowed(lowerName, tagName, options)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n\n // Validate URL attributes\n if (URL_ATTRIBUTES.includes(lowerName)) {\n if (!isUrlSafe(value, options)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n }\n\n // Sanitize style attribute\n if (lowerName === 'style' && options.allowedStyles) {\n sanitized[name] = sanitizeStyle(value, options.allowedStyles)\n }\n else {\n sanitized[name] = value\n }\n }\n\n return buildAttributes(sanitized)\n}\n\n/**\n * Check if attribute is allowed\n */\nfunction isAttributeAllowed(\n attrName: string,\n tagName: string,\n options: SanitizerOptions,\n): boolean {\n const allowedAttrs = options.allowedAttributes\n\n if (!allowedAttrs) {\n return false\n }\n\n // Check data attributes\n if (attrName.startsWith('data-')) {\n return options.allowDataAttributes || false\n }\n\n // Check aria attributes\n if (attrName.startsWith('aria-')) {\n return options.allowAriaAttributes || false\n }\n\n // Array format (global allowed attributes)\n if (Array.isArray(allowedAttrs)) {\n return allowedAttrs.includes(attrName)\n }\n\n // Object format (per-tag or global)\n if (allowedAttrs[tagName]?.includes(attrName)) {\n return true\n }\n\n // Check global attributes (*)\n if (allowedAttrs['*']?.includes(attrName)) {\n return true\n }\n\n return false\n}\n\n/**\n * Validate URL safety\n */\nfunction isUrlSafe(url: string, options: SanitizerOptions): boolean {\n // Use custom validator if provided\n if (options.urlValidator) {\n return options.urlValidator(url)\n }\n\n // Check for javascript: protocol and other dangerous schemes\n const trimmed = url.trim().toLowerCase()\n\n // Remove common whitespace/encoding tricks\n // eslint-disable-next-line no-control-regex\n const cleaned = trimmed.replace(/[\\s\\x00-\\x1F]/g, '')\n\n // Dangerous protocols\n const dangerousProtocols = ['javascript:', 'data:text/html', 'vbscript:', 'file:']\n for (const protocol of dangerousProtocols) {\n if (cleaned.startsWith(protocol)) {\n return false\n }\n }\n\n // Check allowed schemes\n if (options.allowedSchemes && options.allowedSchemes.length > 0) {\n // Relative URLs are allowed\n if (!cleaned.includes(':')) {\n return true\n }\n\n const hasAllowedScheme = options.allowedSchemes.some((scheme) => {\n return cleaned.startsWith(`${scheme}:`)\n })\n\n if (!hasAllowedScheme) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Sanitize inline styles\n */\nfunction sanitizeStyle(style: string, allowedStyles: string[]): string {\n const properties = style.split(';').filter(p => p.trim())\n const sanitized: string[] = []\n\n for (const prop of properties) {\n const colonIndex = prop.indexOf(':')\n if (colonIndex === -1)\n continue\n\n const name = prop.substring(0, colonIndex).trim().toLowerCase()\n const value = prop.substring(colonIndex + 1).trim()\n\n // Check if property is allowed\n if (allowedStyles.includes(name)) {\n // Additional validation for dangerous values\n if (!value.toLowerCase().includes('javascript:') && !value.toLowerCase().includes('expression(')) {\n sanitized.push(`${name}: ${value}`)\n }\n }\n }\n\n return sanitized.join('; ')\n}\n\n/**\n * Parse HTML attributes from string\n */\nfunction parseAttributes(attributesStr: string): Record<string, string> {\n const attributes: Record<string, string> = {}\n const attrRegex = /([\\w-]+)(?:=(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+)))?/g\n\n let match\n // biome-ignore lint/suspicious/noAssignInExpressions: needed for regex matching\n // eslint-disable-next-line no-cond-assign\n while ((match = attrRegex.exec(attributesStr))) {\n const name = match[1]\n const value = match[2] || match[3] || match[4] || ''\n attributes[name] = value\n }\n\n return attributes\n}\n\n/**\n * Build attributes string from object\n */\nfunction buildAttributes(attributes: Record<string, string>): string {\n const parts: string[] = []\n\n for (const [name, value] of Object.entries(attributes)) {\n if (value === '') {\n parts.push(name)\n }\n else {\n // Escape quotes in value\n const escaped = value.replace(/\"/g, '&quot;')\n parts.push(`${name}=\"${escaped}\"`)\n }\n }\n\n return parts.length > 0 ? ` ${parts.join(' ')}` : ''\n}\n\n/**\n * Build tag string from name and attributes\n */\nfunction buildTag(tagName: string, attributes: Record<string, string>): string {\n return `<${tagName}${buildAttributes(attributes)}>`\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/**\n * Check if HTML content is safe (quick check)\n */\nexport function isSafe(html: string, options?: SanitizerOptions | SanitizerPreset): boolean {\n const result = sanitizeWithInfo(html, options)\n return !result.modified\n}\n\n/**\n * Strip all HTML tags\n */\nexport function stripTags(html: string): string {\n return html.replace(/<[^>]*>/g, '')\n}\n\n/**\n * Escape HTML for safe display\n */\nexport function escape(text: string): string {\n return escapeHtml(text)\n}\n",
7
+ "/**\n * @stacksjs/sanitizer\n *\n * A fast, native Bun-powered HTML sanitizer with DOMPurify-like features.\n * Protection against XSS and malicious content.\n */\n\n// Default export\nimport { sanitize } from './sanitizer'\n\nexport * from './presets'\nexport { basic, getPreset, markdown, relaxed, strict } from './presets'\nexport * from './sanitizer'\n\n// Re-export for convenience\nexport { escape, isSafe, sanitize, sanitizeWithInfo, stripTags } from './sanitizer'\nexport * from './types'\n\nexport default sanitize\n"
8
+ ],
9
+ "mappings": "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",
10
+ "debugId": "11C850CF49E4930C64756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,21 @@
1
+ import type { SanitizerOptions } from './types';
2
+ /**
3
+ * Get preset by name
4
+ */
5
+ export declare function getPreset(name: string): SanitizerOptions;
6
+ /**
7
+ * Strict preset - Only safe, basic formatting
8
+ */
9
+ export declare const strict: SanitizerOptions;
10
+ /**
11
+ * Basic preset - Common safe HTML elements
12
+ */
13
+ export declare const basic: SanitizerOptions;
14
+ /**
15
+ * Relaxed preset - More permissive, includes media and interactive elements
16
+ */
17
+ export declare const relaxed: SanitizerOptions;
18
+ /**
19
+ * Markdown preset - Optimized for markdown-generated HTML
20
+ */
21
+ export declare const markdown: SanitizerOptions;
@@ -0,0 +1,21 @@
1
+ import type { SanitizeResult, SanitizerOptions, SanitizerPreset } from './types';
2
+ /**
3
+ * Sanitize HTML content
4
+ */
5
+ export declare function sanitize(html: string, options?: SanitizerOptions | SanitizerPreset): string;
6
+ /**
7
+ * Sanitize HTML content with detailed information
8
+ */
9
+ export declare function sanitizeWithInfo(html: string, options?: SanitizerOptions | SanitizerPreset): SanitizeResult;
10
+ /**
11
+ * Check if HTML content is safe (quick check)
12
+ */
13
+ export declare function isSafe(html: string, options?: SanitizerOptions | SanitizerPreset): boolean;
14
+ /**
15
+ * Strip all HTML tags
16
+ */
17
+ export declare function stripTags(html: string): string;
18
+ /**
19
+ * Escape HTML for safe display
20
+ */
21
+ export declare function escape(text: string): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sanitizer configuration options
3
+ */
4
+ export declare interface SanitizerOptions {
5
+ allowedTags?: string[]
6
+ allowedAttributes?: Record<string, string[]> | string[]
7
+ allowedSchemes?: string[]
8
+ allowedStyles?: string[]
9
+ allowDataAttributes?: boolean
10
+ allowAriaAttributes?: boolean
11
+ urlValidator?: (url: string) => boolean
12
+ stripTags?: boolean
13
+ allowComments?: boolean
14
+ transformTag?: (tagName: string, attributes: Record<string, string>) => { tagName: string, attributes: Record<string, string> } | null
15
+ }
16
+ /**
17
+ * HTML sanitization result
18
+ */
19
+ export declare interface SanitizeResult {
20
+ html: string
21
+ modified: boolean
22
+ removedTags?: string[]
23
+ removedAttributes?: string[]
24
+ }
25
+ /**
26
+ * Preset configurations
27
+ */
28
+ export type SanitizerPreset = 'strict' | 'basic' | 'relaxed' | 'markdown'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/sanitizer",
3
3
  "type": "module",
4
- "version": "0.1.16",
4
+ "version": "0.2.5",
5
5
  "description": "A fast, native Bun-powered HTML sanitizer with DOMPurify-like features. Protection against XSS and malicious content.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -40,6 +40,7 @@
40
40
  ],
41
41
  "scripts": {
42
42
  "build": "bun --bun build.ts",
43
+ "prepublishOnly": "bun run build",
43
44
  "test": "bun test",
44
45
  "test:watch": "bun test --watch",
45
46
  "test:coverage": "bun test --coverage",