dalila 1.9.21 → 1.9.24

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 (90) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/index.js +27 -5
  3. package/dist/cli/security-smoke.d.ts +8 -0
  4. package/dist/cli/security-smoke.js +502 -0
  5. package/dist/components/ui/runtime.d.ts +3 -1
  6. package/dist/components/ui/runtime.js +2 -0
  7. package/dist/components/ui/ui-types.d.ts +8 -8
  8. package/dist/core/html.d.ts +10 -3
  9. package/dist/core/html.js +10 -3
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +3 -0
  12. package/dist/core/observability.d.ts +11 -0
  13. package/dist/core/observability.js +53 -0
  14. package/dist/core/persist.js +9 -1
  15. package/dist/core/signal.d.ts +34 -2
  16. package/dist/core/signal.js +102 -96
  17. package/dist/form/field-array.d.ts +12 -0
  18. package/dist/form/field-array.js +306 -0
  19. package/dist/form/form-dom.d.ts +6 -0
  20. package/dist/form/form-dom.js +52 -0
  21. package/dist/form/form-types.d.ts +24 -15
  22. package/dist/form/form.d.ts +7 -53
  23. package/dist/form/form.js +49 -932
  24. package/dist/form/index.d.ts +2 -2
  25. package/dist/form/index.js +1 -2
  26. package/dist/form/parse-form-data.d.ts +13 -0
  27. package/dist/form/parse-form-data.js +88 -0
  28. package/dist/form/path-utils.d.ts +25 -0
  29. package/dist/form/path-utils.js +127 -0
  30. package/dist/form/path-watchers.d.ts +23 -0
  31. package/dist/form/path-watchers.js +82 -0
  32. package/dist/form/validation-pipeline.d.ts +29 -0
  33. package/dist/form/validation-pipeline.js +223 -0
  34. package/dist/http/client.js +63 -16
  35. package/dist/router/index.d.ts +1 -1
  36. package/dist/router/index.js +1 -1
  37. package/dist/router/location-utils.d.ts +19 -0
  38. package/dist/router/location-utils.js +60 -0
  39. package/dist/router/lru-cache.d.ts +17 -0
  40. package/dist/router/lru-cache.js +63 -0
  41. package/dist/router/preload-metadata.d.ts +26 -0
  42. package/dist/router/preload-metadata.js +65 -0
  43. package/dist/router/router-lifecycle.d.ts +12 -0
  44. package/dist/router/router-lifecycle.js +31 -0
  45. package/dist/router/router-mount-lifecycle.d.ts +11 -0
  46. package/dist/router/router-mount-lifecycle.js +50 -0
  47. package/dist/router/router-prefetch.d.ts +22 -0
  48. package/dist/router/router-prefetch.js +86 -0
  49. package/dist/router/router-preload-cache.d.ts +25 -0
  50. package/dist/router/router-preload-cache.js +68 -0
  51. package/dist/router/router-render-utils.d.ts +14 -0
  52. package/dist/router/router-render-utils.js +11 -0
  53. package/dist/router/router-validation.d.ts +4 -0
  54. package/dist/router/router-validation.js +100 -0
  55. package/dist/router/router-view-composer.d.ts +16 -0
  56. package/dist/router/router-view-composer.js +41 -0
  57. package/dist/router/router.d.ts +12 -3
  58. package/dist/router/router.js +241 -621
  59. package/dist/runtime/array-directive-dom.d.ts +4 -0
  60. package/dist/runtime/array-directive-dom.js +30 -0
  61. package/dist/runtime/bind.d.ts +82 -1
  62. package/dist/runtime/bind.js +940 -935
  63. package/dist/runtime/boundary.d.ts +2 -2
  64. package/dist/runtime/boundary.js +19 -3
  65. package/dist/runtime/fromHtml.d.ts +10 -0
  66. package/dist/runtime/fromHtml.js +6 -4
  67. package/dist/runtime/html-sinks.d.ts +20 -0
  68. package/dist/runtime/html-sinks.js +165 -0
  69. package/dist/runtime/index.d.ts +2 -2
  70. package/dist/runtime/index.js +1 -1
  71. package/dist/runtime/internal/components/component-props.d.ts +15 -0
  72. package/dist/runtime/internal/components/component-props.js +69 -0
  73. package/dist/runtime/internal/components/component-slots.d.ts +10 -0
  74. package/dist/runtime/internal/components/component-slots.js +68 -0
  75. package/dist/runtime/internal/list/list-clone-factory.d.ts +17 -0
  76. package/dist/runtime/internal/list/list-clone-factory.js +35 -0
  77. package/dist/runtime/internal/list/list-clone-registry.d.ts +12 -0
  78. package/dist/runtime/internal/list/list-clone-registry.js +43 -0
  79. package/dist/runtime/internal/list/list-keying.d.ts +13 -0
  80. package/dist/runtime/internal/list/list-keying.js +65 -0
  81. package/dist/runtime/internal/list/list-metadata.d.ts +11 -0
  82. package/dist/runtime/internal/list/list-metadata.js +21 -0
  83. package/dist/runtime/internal/list/list-reconcile.d.ts +3 -0
  84. package/dist/runtime/internal/list/list-reconcile.js +25 -0
  85. package/dist/runtime/internal/list/list-scheduler.d.ts +16 -0
  86. package/dist/runtime/internal/list/list-scheduler.js +45 -0
  87. package/dist/runtime/internal/virtual/virtual-list-helpers.d.ts +48 -0
  88. package/dist/runtime/internal/virtual/virtual-list-helpers.js +291 -0
  89. package/package.json +13 -6
  90. package/scripts/dev-server.cjs +445 -123
package/README.md CHANGED
@@ -58,7 +58,9 @@ bind(document.getElementById('app')!, ctx);
58
58
  ### Core
59
59
 
60
60
  - [Signals](./docs/core/signals.md)
61
+ - [Effects Guide](./docs/core/effects-guide.md)
61
62
  - [Scopes](./docs/core/scope.md)
63
+ - [Scopes Guide](./docs/core/scopes-guide.md)
62
64
  - [Persist](./docs/core/persist.md)
63
65
  - [Context](./docs/context.md)
64
66
  - [Scheduler](./docs/core/scheduler.md)
@@ -87,11 +89,13 @@ bind(document.getElementById('app')!, ctx);
87
89
 
88
90
  - [Template Check CLI](./docs/cli/check.md)
89
91
  - [Devtools Extension](./devtools-extension/README.md)
90
-
91
- Firefox extension workflows:
92
-
93
- - `npm run devtools:firefox:run` — launch Firefox with extension loaded for dev
94
- - `npm run devtools:firefox:build` — package extension artifact for submission/signing
92
+ - [Production Guide](./docs/production.md)
93
+ - [Security Hardening](./docs/security-hardening.md)
94
+ - [Threat Model](./docs/threat-model.md)
95
+ - [Recipes](./docs/recipes.md)
96
+ - [Upgrade Guide](./docs/upgrade.md)
97
+ - [Release Checklist](./docs/release-checklist.md)
98
+ - [Security Release Notes](./docs/security-release-notes.md)
95
99
 
96
100
  ## Packages
97
101
 
@@ -110,6 +114,7 @@ npm install
110
114
  npm run build
111
115
  npm run serve # Dev server with HMR
112
116
  npm test
117
+ npm run test:watch
113
118
  ```
114
119
 
115
120
  ## License
package/dist/cli/index.js CHANGED
@@ -16,7 +16,7 @@ Usage:
16
16
  dalila routes init Initialize app and generate routes outputs
17
17
  dalila routes watch [options] Watch routes and regenerate outputs on changes
18
18
  dalila routes --help Show routes command help
19
- dalila check [path] [--strict] Static analysis of HTML templates against loaders
19
+ dalila check [path] [--strict] Static analysis plus project security smoke
20
20
  dalila help Show this help message
21
21
 
22
22
  Options:
@@ -55,10 +55,11 @@ function showCheckHelp() {
55
55
  🐰✂️ Dalila CLI - Check
56
56
 
57
57
  Usage:
58
- dalila check [path] [options] Static analysis of HTML templates
58
+ dalila check [path] [options] Static analysis of HTML templates + project security smoke
59
59
 
60
60
  Validates that identifiers used in HTML templates ({expr}, d-* directives)
61
61
  match the return type of the corresponding loader() in TypeScript.
62
+ Also runs project-wide security smoke checks for obvious raw HTML / XSS patterns.
62
63
 
63
64
  Arguments:
64
65
  [path] App directory to check (default: src/app)
@@ -160,6 +161,18 @@ function resolveDefaultAppDir(cwd) {
160
161
  }
161
162
  return path.join(appRoot, relToRoot);
162
163
  }
164
+ function resolveDefaultSecuritySmokePath(cwd) {
165
+ const resolvedCwd = path.resolve(cwd);
166
+ const projectRoot = findProjectRoot(resolvedCwd);
167
+ if (!projectRoot) {
168
+ return resolveDefaultAppDir(cwd);
169
+ }
170
+ const srcRoot = path.join(projectRoot, 'src');
171
+ if (fs.existsSync(srcRoot) && fs.statSync(srcRoot).isDirectory()) {
172
+ return srcRoot;
173
+ }
174
+ return resolveDefaultAppDir(cwd);
175
+ }
163
176
  function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
164
177
  const dirIndex = cliArgs.indexOf('--dir');
165
178
  const outputIndex = cliArgs.indexOf('--output');
@@ -368,9 +381,18 @@ async function main() {
368
381
  const appDir = positional[0]
369
382
  ? path.resolve(positional[0])
370
383
  : resolveDefaultAppDir(process.cwd());
371
- const { runCheck } = await import('./check.js');
372
- const exitCode = await runCheck(appDir, { strict });
373
- process.exit(exitCode);
384
+ const smokePath = positional[0]
385
+ ? appDir
386
+ : resolveDefaultSecuritySmokePath(process.cwd());
387
+ const [{ runCheck }, { runSecuritySmokeChecks }] = await Promise.all([
388
+ import('./check.js'),
389
+ import('./security-smoke.js'),
390
+ ]);
391
+ const [checkExitCode, smokeExitCode] = await Promise.all([
392
+ runCheck(appDir, { strict }),
393
+ runSecuritySmokeChecks(smokePath),
394
+ ]);
395
+ process.exit(checkExitCode !== 0 || smokeExitCode !== 0 ? 1 : 0);
374
396
  }
375
397
  }
376
398
  else if (command === '--help' || command === '-h') {
@@ -0,0 +1,8 @@
1
+ export interface SecuritySmokeFinding {
2
+ filePath: string;
3
+ line: number;
4
+ col: number;
5
+ severity: 'error' | 'warning';
6
+ message: string;
7
+ }
8
+ export declare function runSecuritySmokeChecks(scanPath: string): Promise<number>;
@@ -0,0 +1,502 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ const INLINE_EVENT_HANDLER_MESSAGE = 'Inline event handler found in template. Use d-on-* instead.';
4
+ const JAVASCRIPT_URL_MESSAGE = 'Executable javascript: URL found in template sink.';
5
+ const EXECUTABLE_DATA_URL_MESSAGE = 'Executable data: URL found in template sink.';
6
+ const DANGEROUS_URL_PROTOCOL_MESSAGE = 'Dangerous URL protocol found in template sink.';
7
+ const SRCDOC_WARNING_MESSAGE = 'srcdoc embeds raw HTML. Review this iframe content as trusted-only.';
8
+ const D_HTML_WARNING_MESSAGE = 'd-html renders raw HTML. Keep the source trusted or sanitized.';
9
+ const D_ATTR_SRCDOC_WARNING_MESSAGE = 'd-attr-srcdoc writes raw iframe markup. Review the source as trusted-only.';
10
+ const HTML_INLINE_HANDLER_ATTR_NAMES = new Set([
11
+ 'onabort',
12
+ 'onafterprint',
13
+ 'onanimationcancel',
14
+ 'onanimationend',
15
+ 'onanimationiteration',
16
+ 'onanimationstart',
17
+ 'onauxclick',
18
+ 'onbeforeinput',
19
+ 'onbeforematch',
20
+ 'onbeforeprint',
21
+ 'onbeforetoggle',
22
+ 'onbeforeunload',
23
+ 'onbegin',
24
+ 'onblur',
25
+ 'oncancel',
26
+ 'oncanplay',
27
+ 'oncanplaythrough',
28
+ 'onchange',
29
+ 'onclick',
30
+ 'onclose',
31
+ 'oncontextlost',
32
+ 'oncontextmenu',
33
+ 'oncontextrestored',
34
+ 'oncopy',
35
+ 'oncuechange',
36
+ 'oncut',
37
+ 'ondblclick',
38
+ 'ondrag',
39
+ 'ondragend',
40
+ 'ondragenter',
41
+ 'ondragleave',
42
+ 'ondragover',
43
+ 'ondragstart',
44
+ 'ondrop',
45
+ 'ondurationchange',
46
+ 'onemptied',
47
+ 'onend',
48
+ 'onended',
49
+ 'onerror',
50
+ 'onfocus',
51
+ 'onformdata',
52
+ 'onfullscreenchange',
53
+ 'onfullscreenerror',
54
+ 'ongotpointercapture',
55
+ 'onhashchange',
56
+ 'oninput',
57
+ 'oninvalid',
58
+ 'onkeydown',
59
+ 'onkeypress',
60
+ 'onkeyup',
61
+ 'onlanguagechange',
62
+ 'onload',
63
+ 'onloadeddata',
64
+ 'onloadedmetadata',
65
+ 'onloadstart',
66
+ 'onlostpointercapture',
67
+ 'onmessage',
68
+ 'onmessageerror',
69
+ 'onmousedown',
70
+ 'onmouseenter',
71
+ 'onmouseleave',
72
+ 'onmousemove',
73
+ 'onmouseout',
74
+ 'onmouseover',
75
+ 'onmouseup',
76
+ 'onoffline',
77
+ 'ononline',
78
+ 'onpagehide',
79
+ 'onpageshow',
80
+ 'onpaste',
81
+ 'onpause',
82
+ 'onplay',
83
+ 'onplaying',
84
+ 'onpointercancel',
85
+ 'onpointerdown',
86
+ 'onpointerenter',
87
+ 'onpointerleave',
88
+ 'onpointermove',
89
+ 'onpointerout',
90
+ 'onpointerover',
91
+ 'onpointerrawupdate',
92
+ 'onpointerup',
93
+ 'onpopstate',
94
+ 'onprogress',
95
+ 'onratechange',
96
+ 'onrepeat',
97
+ 'onreset',
98
+ 'onresize',
99
+ 'onscroll',
100
+ 'onscrollend',
101
+ 'onsecuritypolicyviolation',
102
+ 'onseeked',
103
+ 'onseeking',
104
+ 'onselect',
105
+ 'onselectionchange',
106
+ 'onselectstart',
107
+ 'onslotchange',
108
+ 'onstalled',
109
+ 'onstorage',
110
+ 'onsubmit',
111
+ 'onsuspend',
112
+ 'ontimeupdate',
113
+ 'ontoggle',
114
+ 'ontransitioncancel',
115
+ 'ontransitionend',
116
+ 'ontransitionrun',
117
+ 'ontransitionstart',
118
+ 'onunhandledrejection',
119
+ 'onunload',
120
+ 'onvolumechange',
121
+ 'onwaiting',
122
+ 'onwebkitanimationend',
123
+ 'onwebkitanimationiteration',
124
+ 'onwebkitanimationstart',
125
+ 'onwebkittransitionend',
126
+ 'onwheel',
127
+ ]);
128
+ const HTML_URL_ATTR_NAMES = new Set([
129
+ 'href',
130
+ 'src',
131
+ 'xlink:href',
132
+ 'formaction',
133
+ 'action',
134
+ 'poster',
135
+ 'data',
136
+ ]);
137
+ const SAFE_URL_PROTOCOLS = new Set([
138
+ 'http:',
139
+ 'https:',
140
+ 'mailto:',
141
+ 'tel:',
142
+ 'sms:',
143
+ 'blob:',
144
+ ]);
145
+ const EXECUTABLE_DATA_URL_PATTERN = /^data:(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)\b/i;
146
+ const SOURCE_SECURITY_PATTERNS = [
147
+ {
148
+ severity: 'warning',
149
+ regex: /\b(?:innerHTML|outerHTML)\s*=/g,
150
+ message: 'Raw HTML sink assignment found in source. Review the input as trusted-only.',
151
+ },
152
+ {
153
+ severity: 'warning',
154
+ regex: /\binsertAdjacentHTML\s*\(/g,
155
+ message: 'insertAdjacentHTML() found in source. Review the inserted markup as trusted-only.',
156
+ },
157
+ {
158
+ severity: 'warning',
159
+ regex: /\bdocument\.write\s*\(/g,
160
+ message: 'document.write() found in source. Review the written markup as trusted-only.',
161
+ },
162
+ {
163
+ severity: 'warning',
164
+ regex: /\bfromHtml\s*\(/g,
165
+ message: 'fromHtml() found in source. Pass only trusted template markup.',
166
+ },
167
+ ];
168
+ function findProjectRoot(startDir) {
169
+ let current = path.resolve(startDir);
170
+ while (true) {
171
+ if (fs.existsSync(path.join(current, 'package.json'))
172
+ || fs.existsSync(path.join(current, 'tsconfig.json'))) {
173
+ return current;
174
+ }
175
+ const parent = path.dirname(current);
176
+ if (parent === current) {
177
+ return null;
178
+ }
179
+ current = parent;
180
+ }
181
+ }
182
+ function findExistingAncestor(startPath) {
183
+ let current = path.resolve(startPath);
184
+ while (!fs.existsSync(current)) {
185
+ const parent = path.dirname(current);
186
+ if (parent === current) {
187
+ return current;
188
+ }
189
+ current = parent;
190
+ }
191
+ return current;
192
+ }
193
+ function resolveSecuritySmokeScope(scanPath) {
194
+ const resolvedPath = path.resolve(scanPath);
195
+ const existingPath = findExistingAncestor(resolvedPath);
196
+ const startDir = fs.statSync(existingPath).isDirectory()
197
+ ? existingPath
198
+ : path.dirname(existingPath);
199
+ const projectRoot = findProjectRoot(startDir) ?? startDir;
200
+ return {
201
+ projectRoot,
202
+ scanRoot: resolvedPath,
203
+ };
204
+ }
205
+ function collectCandidateFiles(dir, files = []) {
206
+ if (!fs.existsSync(dir))
207
+ return files;
208
+ const stat = fs.statSync(dir);
209
+ if (stat.isFile()) {
210
+ const ext = path.extname(dir).toLowerCase();
211
+ if (ext === '.html' || ext === '.ts' || ext === '.js') {
212
+ files.push(dir);
213
+ }
214
+ return files;
215
+ }
216
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
217
+ if (entry.name.startsWith('.'))
218
+ continue;
219
+ if (entry.name === 'dist' || entry.name === 'node_modules')
220
+ continue;
221
+ const fullPath = path.join(dir, entry.name);
222
+ if (entry.isDirectory()) {
223
+ collectCandidateFiles(fullPath, files);
224
+ continue;
225
+ }
226
+ if (!entry.isFile())
227
+ continue;
228
+ const ext = path.extname(entry.name).toLowerCase();
229
+ if (ext === '.html' || ext === '.ts' || ext === '.js') {
230
+ files.push(fullPath);
231
+ }
232
+ }
233
+ return files;
234
+ }
235
+ function getLineCol(source, offset) {
236
+ const before = source.slice(0, offset);
237
+ const lines = before.split('\n');
238
+ return {
239
+ line: lines.length,
240
+ col: lines[lines.length - 1].length + 1,
241
+ };
242
+ }
243
+ function isHtmlTagStartChar(char) {
244
+ return !!char && /[a-zA-Z]/.test(char);
245
+ }
246
+ function collectHtmlStartTags(source) {
247
+ const tags = [];
248
+ let i = 0;
249
+ while (i < source.length) {
250
+ if (source[i] !== '<') {
251
+ i += 1;
252
+ continue;
253
+ }
254
+ const next = source[i + 1];
255
+ if (next === '/' || next === '!' || next === '?' || !isHtmlTagStartChar(next)) {
256
+ i += 1;
257
+ continue;
258
+ }
259
+ i += 1;
260
+ const tagNameStart = i;
261
+ while (i < source.length && /[\w:-]/.test(source[i])) {
262
+ i += 1;
263
+ }
264
+ const tagName = source.slice(tagNameStart, i);
265
+ if (!tagName) {
266
+ i += 1;
267
+ continue;
268
+ }
269
+ const attrs = [];
270
+ let closed = false;
271
+ while (i < source.length) {
272
+ while (i < source.length && /\s/.test(source[i])) {
273
+ i += 1;
274
+ }
275
+ if (i >= source.length)
276
+ break;
277
+ if (source[i] === '>') {
278
+ i += 1;
279
+ closed = true;
280
+ break;
281
+ }
282
+ if (source[i] === '/' && source[i + 1] === '>') {
283
+ i += 2;
284
+ closed = true;
285
+ break;
286
+ }
287
+ if (source[i] === '/') {
288
+ i += 1;
289
+ continue;
290
+ }
291
+ const attrStart = i;
292
+ while (i < source.length && !/[\s=/>]/.test(source[i])) {
293
+ i += 1;
294
+ }
295
+ if (i === attrStart) {
296
+ i += 1;
297
+ continue;
298
+ }
299
+ const name = source.slice(attrStart, i);
300
+ while (i < source.length && /\s/.test(source[i])) {
301
+ i += 1;
302
+ }
303
+ let value = null;
304
+ if (source[i] === '=') {
305
+ i += 1;
306
+ while (i < source.length && /\s/.test(source[i])) {
307
+ i += 1;
308
+ }
309
+ if (i >= source.length)
310
+ break;
311
+ const quote = source[i];
312
+ if (quote === '"' || quote === "'") {
313
+ i += 1;
314
+ const valueStart = i;
315
+ while (i < source.length && source[i] !== quote) {
316
+ i += 1;
317
+ }
318
+ value = source.slice(valueStart, i);
319
+ if (i < source.length) {
320
+ i += 1;
321
+ }
322
+ }
323
+ else {
324
+ const valueStart = i;
325
+ while (i < source.length && !/[\s>]/.test(source[i])) {
326
+ i += 1;
327
+ }
328
+ value = source.slice(valueStart, i);
329
+ }
330
+ }
331
+ attrs.push({ name, value, index: attrStart });
332
+ }
333
+ if (closed) {
334
+ tags.push({ tagName, attrs });
335
+ }
336
+ }
337
+ return tags;
338
+ }
339
+ function normalizeProtocolCheckValue(value) {
340
+ return value.replace(/[\u0000-\u0020\u007f]+/g, '').toLowerCase();
341
+ }
342
+ function extractUrlProtocol(value) {
343
+ const match = value.match(/^([a-z][a-z0-9+\-.]*):/i);
344
+ return match ? `${match[1].toLowerCase()}:` : null;
345
+ }
346
+ function classifyDangerousHtmlUrl(tagName, attrName, value) {
347
+ const normalizedAttrName = attrName.toLowerCase();
348
+ if (!HTML_URL_ATTR_NAMES.has(normalizedAttrName))
349
+ return null;
350
+ if (normalizedAttrName === 'data' && tagName.toLowerCase() !== 'object') {
351
+ return null;
352
+ }
353
+ if (value == null)
354
+ return null;
355
+ const normalized = normalizeProtocolCheckValue(value);
356
+ if (!normalized
357
+ || normalized.startsWith('/')
358
+ || normalized.startsWith('./')
359
+ || normalized.startsWith('../')
360
+ || normalized.startsWith('?')
361
+ || normalized.startsWith('#')) {
362
+ return null;
363
+ }
364
+ const protocol = extractUrlProtocol(normalized);
365
+ if (!protocol)
366
+ return null;
367
+ if (protocol === 'javascript:')
368
+ return JAVASCRIPT_URL_MESSAGE;
369
+ if (EXECUTABLE_DATA_URL_PATTERN.test(normalized))
370
+ return EXECUTABLE_DATA_URL_MESSAGE;
371
+ if (!SAFE_URL_PROTOCOLS.has(protocol))
372
+ return DANGEROUS_URL_PROTOCOL_MESSAGE;
373
+ return null;
374
+ }
375
+ function collectHtmlTemplateFindings(source, filePath, findings) {
376
+ for (const tag of collectHtmlStartTags(source)) {
377
+ for (const attr of tag.attrs) {
378
+ const attrName = attr.name.toLowerCase();
379
+ const { line, col } = getLineCol(source, attr.index);
380
+ if (HTML_INLINE_HANDLER_ATTR_NAMES.has(attrName)) {
381
+ findings.push({
382
+ filePath,
383
+ line,
384
+ col,
385
+ severity: 'error',
386
+ message: INLINE_EVENT_HANDLER_MESSAGE,
387
+ });
388
+ }
389
+ const dangerousUrlMessage = classifyDangerousHtmlUrl(tag.tagName, attrName, attr.value);
390
+ if (dangerousUrlMessage) {
391
+ findings.push({
392
+ filePath,
393
+ line,
394
+ col,
395
+ severity: 'error',
396
+ message: dangerousUrlMessage,
397
+ });
398
+ }
399
+ if (attrName === 'srcdoc') {
400
+ findings.push({
401
+ filePath,
402
+ line,
403
+ col,
404
+ severity: 'warning',
405
+ message: SRCDOC_WARNING_MESSAGE,
406
+ });
407
+ }
408
+ if (attrName === 'd-html') {
409
+ findings.push({
410
+ filePath,
411
+ line,
412
+ col,
413
+ severity: 'warning',
414
+ message: D_HTML_WARNING_MESSAGE,
415
+ });
416
+ }
417
+ if (attrName === 'd-attr-srcdoc') {
418
+ findings.push({
419
+ filePath,
420
+ line,
421
+ col,
422
+ severity: 'warning',
423
+ message: D_ATTR_SRCDOC_WARNING_MESSAGE,
424
+ });
425
+ }
426
+ }
427
+ }
428
+ }
429
+ function collectPatternFindings(source, filePath, patterns, findings) {
430
+ for (const pattern of patterns) {
431
+ pattern.regex.lastIndex = 0;
432
+ let match = null;
433
+ while ((match = pattern.regex.exec(source)) !== null) {
434
+ const { line, col } = getLineCol(source, match.index);
435
+ findings.push({
436
+ filePath,
437
+ line,
438
+ col,
439
+ severity: pattern.severity,
440
+ message: pattern.message,
441
+ });
442
+ if (match[0].length === 0) {
443
+ pattern.regex.lastIndex += 1;
444
+ }
445
+ }
446
+ }
447
+ }
448
+ export async function runSecuritySmokeChecks(scanPath) {
449
+ const { projectRoot, scanRoot } = resolveSecuritySmokeScope(scanPath);
450
+ if (!fs.existsSync(scanRoot)) {
451
+ return 1;
452
+ }
453
+ const findings = [];
454
+ const files = collectCandidateFiles(scanRoot);
455
+ for (const filePath of files) {
456
+ const source = fs.readFileSync(filePath, 'utf8');
457
+ const ext = path.extname(filePath).toLowerCase();
458
+ if (ext === '.html') {
459
+ collectHtmlTemplateFindings(source, filePath, findings);
460
+ continue;
461
+ }
462
+ collectPatternFindings(source, filePath, SOURCE_SECURITY_PATTERNS, findings);
463
+ }
464
+ const errors = findings.filter((finding) => finding.severity === 'error');
465
+ const warnings = findings.filter((finding) => finding.severity === 'warning');
466
+ console.log('🛡 Dalila Security Smoke');
467
+ if (findings.length === 0) {
468
+ console.log('✅ No dangerous template patterns found');
469
+ console.log('');
470
+ return 0;
471
+ }
472
+ const grouped = new Map();
473
+ for (const finding of findings) {
474
+ const bucket = grouped.get(finding.filePath) ?? [];
475
+ bucket.push(finding);
476
+ grouped.set(finding.filePath, bucket);
477
+ }
478
+ for (const [filePath, bucket] of grouped) {
479
+ console.log(` ${path.relative(projectRoot, filePath)}`);
480
+ for (const finding of bucket) {
481
+ const prefix = finding.severity === 'error' ? '❌' : '⚠️';
482
+ console.log(` ${`${finding.line}:${finding.col}`.padEnd(8)} ${prefix} ${finding.message}`);
483
+ }
484
+ console.log('');
485
+ }
486
+ const summaryParts = [];
487
+ if (errors.length > 0) {
488
+ summaryParts.push(`${errors.length} error${errors.length === 1 ? '' : 's'}`);
489
+ }
490
+ if (warnings.length > 0) {
491
+ summaryParts.push(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`);
492
+ }
493
+ const summary = summaryParts.join(' and ');
494
+ if (errors.length > 0) {
495
+ console.log(`❌ Security smoke found ${summary}`);
496
+ console.log('');
497
+ return 1;
498
+ }
499
+ console.log(`⚠️ Security smoke found ${summary}`);
500
+ console.log('');
501
+ return 0;
502
+ }
@@ -1,11 +1,13 @@
1
1
  import { type Signal } from "../../core/signal.js";
2
- import { type BindContext } from "../../runtime/bind.js";
2
+ import { type BindContext, type BindOptions } from "../../runtime/bind.js";
3
3
  import type { Calendar, Combobox, Dialog, Drawer, Dropdown, Dropzone, PopoverMount, TabsMount, Toast } from "./ui-types.js";
4
4
  export interface MountUIOptions {
5
5
  context?: BindContext;
6
6
  events?: string[];
7
7
  theme?: boolean;
8
8
  sliderValue?: Signal<string>;
9
+ sanitizeHtml?: BindOptions["sanitizeHtml"];
10
+ security?: BindOptions["security"];
9
11
  dialogs?: Record<string, Dialog>;
10
12
  drawers?: Record<string, Drawer>;
11
13
  dropdowns?: Record<string, Dropdown>;
@@ -354,6 +354,8 @@ export function mountUI(root, options) {
354
354
  withScope(scope, () => {
355
355
  cleanups.push(bind(mountedRoot, ctx, {
356
356
  events: options.events ?? DEFAULT_EVENTS,
357
+ sanitizeHtml: options.sanitizeHtml,
358
+ security: options.security,
357
359
  }));
358
360
  // Attach dialogs
359
361
  for (const [key, dialog] of Object.entries(options.dialogs ?? {})) {
@@ -1,4 +1,4 @@
1
- import type { Signal } from "../../core/signal.js";
1
+ import type { ComputedSignal, ReadonlySignal, Signal } from "../../core/signal.js";
2
2
  export interface DialogOptions {
3
3
  closeOnBackdrop?: boolean;
4
4
  closeOnEscape?: boolean;
@@ -56,9 +56,9 @@ export interface Tabs {
56
56
  _attachTo(el: HTMLElement): void;
57
57
  }
58
58
  export interface TabBindings {
59
- tabClass: Signal<string>;
60
- selected: Signal<string>;
61
- visible: Signal<boolean>;
59
+ tabClass: ComputedSignal<string>;
60
+ selected: ComputedSignal<string>;
61
+ visible: ComputedSignal<boolean>;
62
62
  }
63
63
  export interface DropdownOptions {
64
64
  closeOnSelect?: boolean;
@@ -84,7 +84,7 @@ export interface Combobox {
84
84
  query: Signal<string>;
85
85
  value: Signal<string>;
86
86
  label: Signal<string>;
87
- filtered: Signal<ComboboxOption[]>;
87
+ filtered: ReadonlySignal<ComboboxOption[]>;
88
88
  highlightedIndex: Signal<number>;
89
89
  show(): void;
90
90
  close(): void;
@@ -103,7 +103,7 @@ export interface Accordion {
103
103
  toggle(itemId: string): void;
104
104
  open(itemId: string): void;
105
105
  close(itemId: string): void;
106
- isOpen(itemId: string): Signal<boolean>;
106
+ isOpen(itemId: string): ReadonlySignal<boolean>;
107
107
  _attachTo(el: HTMLElement): void;
108
108
  }
109
109
  export interface CalendarDay {
@@ -125,8 +125,8 @@ export interface Calendar {
125
125
  year: Signal<number>;
126
126
  month: Signal<number>;
127
127
  selected: Signal<Date | null>;
128
- title: Signal<string>;
129
- days: Signal<CalendarDay[]>;
128
+ title: ReadonlySignal<string>;
129
+ days: ReadonlySignal<CalendarDay[]>;
130
130
  dayLabels: string[];
131
131
  prev(): void;
132
132
  next(): void;