@invisibleloop/pulse 0.8.1 → 0.8.2

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.
@@ -24,13 +24,13 @@ Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chr
24
24
 
25
25
  Run `mcp__chrome-devtools__lighthouse_audit` on the route with `{ "strategy": "desktop" }`.
26
26
 
27
- **Pass bar: Performance, Accessibility, Best Practices, and SEO must all be 100.** Report the actual scores. If any score is below 100, identify the failing audit(s), fix the issue, and restart from step 2.
27
+ **Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Performance is measured and reported but is not a hard requirement (it varies with machine load). Report the actual scores. If Accessibility, Best Practices, or SEO is below 100, identify the failing audit(s), fix the issue, and restart from step 2.
28
28
 
29
29
  ### 6. Lighthouse — mobile
30
30
 
31
31
  Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy": "mobile" }`.
32
32
 
33
- **Same pass bar: all four categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 2.
33
+ **Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 2.
34
34
 
35
35
  ### 7. Performance
36
36
 
@@ -42,7 +42,7 @@ Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexp
42
42
 
43
43
  ### 9. Code review
44
44
 
45
- Check the spec against the checklist in `.claude/pulse-checklist.md`. Work through every item. Fix anything that fails before proceeding to the next step.
45
+ Call `pulse_review` with the absolute path to the spec file. Read the source, rendered HTML, validator output, and checklist it returns. Fix every issue before proceeding. This is the structured review — do not skip it or replace it with a manual checklist read.
46
46
 
47
47
  ### 10. Close the browser
48
48
 
@@ -56,7 +56,7 @@ Run:
56
56
  date +%s > .pulse-verified
57
57
  ```
58
58
 
59
- This records that verify completed successfully. The stop hook checks this stamp — do not skip this step.
59
+ **This must be the last write operation before stopping.** The stop hook compares each edited spec's modification time against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after writing the stamp.
60
60
 
61
61
  ### 12. Report
62
62
 
@@ -69,4 +69,12 @@ Summarise:
69
69
  - Console: any errors
70
70
  - Review: pass or issues found and fixed
71
71
 
72
- Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
72
+ **Pass bar — all of these must be met before the page is confirmed good:**
73
+ - Validation: clean (no errors, no warnings)
74
+ - Lighthouse desktop: Accessibility, Best Practices, SEO all 100
75
+ - Lighthouse mobile: same
76
+ - CLS: 0.00 — any layout shift is a blocker
77
+ - LCP: report the value; flag and fix any insight that identifies a fixable cause
78
+ - Console: no errors
79
+
80
+ If any gate fails, fix the issue and run `/verify` again.
@@ -66,6 +66,11 @@ Before finishing any spec, verify every point below. Fix anything that fails.
66
66
  - **Validate FormData fields before use.** `formData.get('email')` returns `null` if the field is missing. Check for null/empty before passing to an API or database.
67
67
  - **Do not trust URL params.** `ctx.params.id` is a raw string from the URL. Validate it before use — check it exists, is the right type, and refers to a real resource. Return a 404 or redirect if it doesn't.
68
68
 
69
+ ### CSS
70
+
71
+ - **`app.css` must contain no hex values or raw colour values.** A lint hook enforces this and will block the build. Hex values belong in `public/theme.css` as token definitions; `app.css` references them via `var()` only.
72
+ - **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
73
+
69
74
  ### Security
70
75
 
71
76
  - Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
@@ -1 +1 @@
1
- 0.7.2
1
+ 0.8.1
@@ -40,6 +40,30 @@
40
40
 
41
41
  /* Touch target minimum per WCAG 2.5.5 */
42
42
  --ui-touch-min: 44px;
43
+
44
+ /* ── Spacing scale ── */
45
+ --ui-space-1: 0.25rem; /* 4px */
46
+ --ui-space-2: 0.5rem; /* 8px */
47
+ --ui-space-3: 0.75rem; /* 12px */
48
+ --ui-space-4: 1rem; /* 16px */
49
+ --ui-space-5: 1.25rem; /* 20px */
50
+ --ui-space-6: 1.5rem; /* 24px */
51
+ --ui-space-8: 2rem; /* 32px */
52
+ --ui-space-10: 2.5rem; /* 40px */
53
+ --ui-space-12: 3rem; /* 48px */
54
+ --ui-space-16: 4rem; /* 64px */
55
+ --ui-space-20: 5rem; /* 80px */
56
+ --ui-space-24: 6rem; /* 96px */
57
+
58
+ /* ── Type scale ── */
59
+ --ui-text-xs: 0.75rem; /* 12px */
60
+ --ui-text-sm: 0.875rem; /* 14px */
61
+ --ui-text-base: 1rem; /* 16px */
62
+ --ui-text-lg: 1.125rem; /* 18px */
63
+ --ui-text-xl: 1.25rem; /* 20px */
64
+ --ui-text-2xl: 1.5rem; /* 24px */
65
+ --ui-text-3xl: 1.875rem; /* 30px */
66
+ --ui-text-4xl: 2.25rem; /* 36px */
43
67
  }
44
68
 
45
69
  /* ─── Base reset ─────────────────────────────────────────────────────────────
@@ -39,6 +39,18 @@ export default {
39
39
  title: 'Pulse — The spec-first web framework',
40
40
  description: 'Pulse is a server-first Node.js framework with zero runtime dependencies. One spec object per page — server data, state, mutations, and view in plain JS. Streaming SSR, security headers, and production caching are enforced by the architecture.',
41
41
  styles: ['/pulse-ui.css', '/docs.css'],
42
+ schema: {
43
+ '@context': 'https://schema.org',
44
+ '@type': 'SoftwareApplication',
45
+ name: 'Pulse',
46
+ applicationCategory: 'DeveloperApplication',
47
+ operatingSystem: 'Node.js ≥ 22',
48
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
49
+ license: 'https://opensource.org/licenses/MIT',
50
+ description: 'Spec-first Node.js web framework. Zero runtime dependencies. Streaming SSR, built-in security headers, and production caching enforced by the architecture.',
51
+ url: 'https://pulse.invisibleloop.com',
52
+ codeRepository: 'https://github.com/invisibleloop/pulse-framework',
53
+ },
42
54
  },
43
55
  state: {},
44
56
  server: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.8.2
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { initClientStore, getStoreState, subscribe, updateStore, registerStoreMutations, dispatchStoreMutation } from './store.js'
11
+ import { trustedHTML } from './tt.js'
11
12
 
12
13
  // Toast is lazy-loaded on first use — pages that never use _toast pay zero bytes
13
14
  const showToast = (opts) => import('./toast.js').then(m => m.showToast(opts))
@@ -227,7 +228,7 @@ export function mount(spec, el, serverState = {}, options = {}) {
227
228
  refresh: render,
228
229
 
229
230
  /** Tear down — remove event listeners, unsubscribe from store, clear element */
230
- destroy: () => { _unsubStore?.(); _eventAbort.abort(); el.innerHTML = '' }
231
+ destroy: () => { _unsubStore?.(); _eventAbort.abort(); el.innerHTML = trustedHTML('') }
231
232
  }
232
233
  }
233
234
 
@@ -363,7 +364,7 @@ function morph(el, newHtml) {
363
364
  return
364
365
  }
365
366
  const temp = document.createElement('div')
366
- temp.innerHTML = newHtml
367
+ temp.innerHTML = trustedHTML(newHtml)
367
368
  morphNodes(el, temp)
368
369
  }
369
370
 
@@ -8,6 +8,8 @@
8
8
  * Falls back to full page load on any error.
9
9
  */
10
10
 
11
+ import { trustedHTML } from './tt.js'
12
+
11
13
  /**
12
14
  * Initialise navigation for a Pulse app.
13
15
  * Call once after the initial mount().
@@ -61,7 +63,7 @@ export function initNavigation(root, mountFn) {
61
63
  await applyScripts(msg.scripts)
62
64
 
63
65
  } else if (msg.type === 'html') {
64
- root.innerHTML = msg.html
66
+ root.innerHTML = trustedHTML(msg.html)
65
67
  // Index <pulse-deferred> placeholders so deferred chunks land in the right spot
66
68
  for (const id of (msg.deferred || [])) {
67
69
  const el = root.querySelector(`[id="pd-${id}"]`)
@@ -74,7 +76,7 @@ export function initNavigation(root, mountFn) {
74
76
  const slot = deferredSlots.get(msg.id)
75
77
  if (slot) {
76
78
  const tmp = document.createElement('div')
77
- tmp.innerHTML = msg.html
79
+ tmp.innerHTML = trustedHTML(msg.html)
78
80
  slot.replaceWith(...tmp.childNodes)
79
81
  deferredSlots.delete(msg.id)
80
82
  }
@@ -124,7 +126,7 @@ export function initNavigation(root, mountFn) {
124
126
  window.__updatePulseStore__(storeState)
125
127
  }
126
128
 
127
- root.innerHTML = html
129
+ root.innerHTML = trustedHTML(html)
128
130
  document.title = title || document.title
129
131
  applyStyles(styles)
130
132
  await applyScripts(scripts)
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Trusted Types helper — routes innerHTML assignments through a declared policy
3
+ * in browsers that support the API (Chromium). Falls back to a plain string
4
+ * everywhere else — behaviour is identical, the protection is simply absent.
5
+ *
6
+ * Policy name 'pulse' must match the trusted-types CSP directive on the server.
7
+ */
8
+
9
+ let _policy = null
10
+
11
+ /**
12
+ * Return a TrustedHTML object (Chromium) or the raw string (all other browsers).
13
+ * @param {string} html
14
+ * @returns {TrustedHTML|string}
15
+ */
16
+ export function trustedHTML(html) {
17
+ if (typeof window === 'undefined' || !window.trustedTypes) return html
18
+ if (!_policy) _policy = window.trustedTypes.createPolicy('pulse', { createHTML: s => s })
19
+ return _policy.createHTML(html)
20
+ }
@@ -63,10 +63,12 @@ const BASE_CSP = {
63
63
  'img-src': ["'self'", 'data:'],
64
64
  'font-src': ["'self'"],
65
65
  'connect-src': ["'self'"],
66
- 'object-src': ["'none'"],
67
- 'frame-ancestors':["'none'"],
68
- 'base-uri': ["'self'"],
69
- 'form-action': ["'self'"],
66
+ 'object-src': ["'none'"],
67
+ 'frame-ancestors': ["'none'"],
68
+ 'base-uri': ["'self'"],
69
+ 'form-action': ["'self'"],
70
+ 'require-trusted-types-for': ["'script'"],
71
+ 'trusted-types': ['pulse'],
70
72
  }
71
73
 
72
74
  function serializeCsp(directives) {
@@ -81,8 +83,8 @@ function serializeCsp(directives) {
81
83
  function buildCsp(nonce, ext = {}) {
82
84
  const d = {
83
85
  ...BASE_CSP,
84
- 'script-src': ["'self'", `'nonce-${nonce}'`],
85
- 'style-src': ["'self'", `'nonce-${nonce}'`], // allows toast's runtime-injected <style nonce>
86
+ 'script-src': ["'self'", `'nonce-${nonce}'`, "'unsafe-inline'"], // 'unsafe-inline' ignored by nonce-aware browsers; fallback for CSP1-only browsers
87
+ 'style-src': ["'self'", `'nonce-${nonce}'`, "'unsafe-inline'"], // same — nonce takes precedence; allows toast's runtime-injected <style> in older browsers
86
88
  }
87
89
  for (const [k, sources] of Object.entries(ext)) {
88
90
  d[k] = [...(d[k] || []), ...sources]