@invisibleloop/pulse 0.20.44 → 0.20.46
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/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/workflow.md +2 -0
- package/src/cli/feedback.js +3 -0
- package/src/cli/report-server.js +10 -7
- package/src/cli/scaffold.js +2 -2
- package/src/mcp/server.js +3 -1
- package/src/md/index.js +29 -6
- package/src/runtime/index.js +2 -1
- package/src/server/index.js +9 -3
- package/src/testing/html.js +2 -1
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.20.
|
|
1
|
+
0.20.46
|
package/src/agent/workflow.md
CHANGED
|
@@ -343,6 +343,8 @@ If it still fails, it is a framework bug:
|
|
|
343
343
|
3. If they agree, call `pulse_report_issue` without `confirmed`. Show the user the returned preview verbatim and ask for approval.
|
|
344
344
|
4. Only after they approve, call it again with `confirmed: true` and the `previewId`. Never confirm on your own.
|
|
345
345
|
|
|
346
|
+
Never report a security vulnerability this way — reports become public issues. If you suspect one (XSS, injection, auth or CSRF bypass, anything exploitable), tell the user to report it privately at https://github.com/invisibleloop/pulse-framework/security/advisories/new.
|
|
347
|
+
|
|
346
348
|
Do not report: your own spec or API-usage errors, missing features or feature requests, design preferences, or anything you have not isolated to Pulse. Keep secrets, credentials and private user data out of every field.
|
|
347
349
|
|
|
348
350
|
---
|
package/src/cli/feedback.js
CHANGED
|
@@ -108,6 +108,8 @@ export function previewId(payload) {
|
|
|
108
108
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const SECURITY_URL = 'https://github.com/invisibleloop/pulse-framework/security/advisories/new'
|
|
112
|
+
|
|
111
113
|
export function formatPreview(payload, { transport = 'proxy', endpoint = DEFAULT_ENDPOINT } = {}) {
|
|
112
114
|
const dest = transport === 'gh'
|
|
113
115
|
? `GitHub issue on ${REPO}, filed with your own \`gh\` login`
|
|
@@ -116,6 +118,7 @@ export function formatPreview(payload, { transport = 'proxy', endpoint = DEFAULT
|
|
|
116
118
|
return [
|
|
117
119
|
`This report has NOT been sent. It will be published as: ${dest}.`,
|
|
118
120
|
`Only the fields below are sent — no project files, paths or environment. Local paths and secret-like strings were redacted.`,
|
|
121
|
+
`This becomes a PUBLIC issue. If it is a security vulnerability, do NOT send it — tell the user to report it privately at ${SECURITY_URL}.`,
|
|
119
122
|
``,
|
|
120
123
|
`Title: ${payload.title}`,
|
|
121
124
|
`Pulse version: ${payload.version}`,
|
package/src/cli/report-server.js
CHANGED
|
@@ -22,6 +22,9 @@ const REPORTS_DIR = path.join(ROOT, '.pulse', 'reports')
|
|
|
22
22
|
const LOAD_REPORTS_DIR = path.join(ROOT, '.pulse', 'load-reports')
|
|
23
23
|
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000
|
|
24
24
|
|
|
25
|
+
// Report data (notably the audited URL) is untrusted when it reaches HTML.
|
|
26
|
+
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
27
|
+
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// Data — Lighthouse
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
@@ -349,7 +352,7 @@ function renderPerfTab(slug, reports) {
|
|
|
349
352
|
${staleBanner}
|
|
350
353
|
<div class="page-header">
|
|
351
354
|
<div>
|
|
352
|
-
<h2 class="page-heading">${latest.url || '/' + slug}</h2>
|
|
355
|
+
<h2 class="page-heading">${esc(latest.url || '/' + slug)}</h2>
|
|
353
356
|
<p class="page-meta">${count} audit${count !== 1 ? 's' : ''} · last run ${when}</p>
|
|
354
357
|
</div>
|
|
355
358
|
<div class="health-badge" style="color:${health[1]};border-color:${health[1]}20;background:${health[1]}10">${health[0]}</div>
|
|
@@ -480,7 +483,7 @@ function renderLoadTab(slug, reports) {
|
|
|
480
483
|
${localhostBanner}
|
|
481
484
|
<div class="page-header">
|
|
482
485
|
<div>
|
|
483
|
-
<h2 class="page-heading">${latest.url || '/' + slug}</h2>
|
|
486
|
+
<h2 class="page-heading">${esc(latest.url || '/' + slug)}</h2>
|
|
484
487
|
<p class="page-meta">${count} test${count !== 1 ? 's' : ''} · last run ${when}</p>
|
|
485
488
|
</div>
|
|
486
489
|
</div>
|
|
@@ -517,8 +520,8 @@ function renderIndex(slugs) {
|
|
|
517
520
|
: ''
|
|
518
521
|
|
|
519
522
|
return `
|
|
520
|
-
<a href="/${slug}" class="index-card">
|
|
521
|
-
<span class="index-url">${latest?.url || latestLd?.url || '/' + slug}</span>
|
|
523
|
+
<a href="/${esc(slug)}" class="index-card">
|
|
524
|
+
<span class="index-url">${esc(latest?.url || latestLd?.url || '/' + slug)}</span>
|
|
522
525
|
<span class="index-gauges">${gauges}</span>
|
|
523
526
|
${loadBadge}
|
|
524
527
|
</a>`
|
|
@@ -636,7 +639,7 @@ function renderHTML({ title, content, slugs, activeSlug, activeTab, hasBoth }) {
|
|
|
636
639
|
const loadRpts = loadLoadReports(slug)
|
|
637
640
|
const ldUrl = loadRpts[loadRpts.length - 1]?.url
|
|
638
641
|
const display = url || ldUrl || '/' + slug
|
|
639
|
-
return `<a href="/${slug}" class="sidebar-link${slug === activeSlug ? ' active' : ''}">${display}</a>`
|
|
642
|
+
return `<a href="/${esc(slug)}" class="sidebar-link${slug === activeSlug ? ' active' : ''}">${esc(display)}</a>`
|
|
640
643
|
}).join('')
|
|
641
644
|
|
|
642
645
|
const tabs = activeSlug && hasBoth ? `
|
|
@@ -656,7 +659,7 @@ function renderHTML({ title, content, slugs, activeSlug, activeTab, hasBoth }) {
|
|
|
656
659
|
<head>
|
|
657
660
|
<meta charset="UTF-8">
|
|
658
661
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
659
|
-
<title>${title} — Pulse Reports</title>
|
|
662
|
+
<title>${esc(title)} — Pulse Reports</title>
|
|
660
663
|
<style>${CSS}</style>
|
|
661
664
|
</head>
|
|
662
665
|
<body>
|
|
@@ -718,6 +721,6 @@ http.createServer((req, res) => {
|
|
|
718
721
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
719
722
|
res.end(renderHTML({ title, content, slugs, activeSlug: slug, activeTab, hasBoth }))
|
|
720
723
|
|
|
721
|
-
}).listen(PORT, () => {
|
|
724
|
+
}).listen(PORT, '127.0.0.1', () => {
|
|
722
725
|
console.log(`\n⚡ Pulse report server running at http://localhost:${PORT}\n`)
|
|
723
726
|
})
|
package/src/cli/scaffold.js
CHANGED
|
@@ -488,7 +488,7 @@ Run these steps in order — do not declare work done without them:
|
|
|
488
488
|
|
|
489
489
|
## If Pulse itself is broken
|
|
490
490
|
|
|
491
|
-
If a documented Pulse feature or tool misbehaves and you have reduced it to a minimal repro that involves only Pulse, tell the user, give them the workaround, and offer to report it with \`pulse_report_issue\`. Preview first, show the user the redacted report verbatim, and send only after they approve. Do not report your own mistakes, spec errors or feature requests.
|
|
491
|
+
If a documented Pulse feature or tool misbehaves and you have reduced it to a minimal repro that involves only Pulse, tell the user, give them the workaround, and offer to report it with \`pulse_report_issue\`. Preview first, show the user the redacted report verbatim, and send only after they approve. Do not report your own mistakes, spec errors or feature requests. Never report a security vulnerability this way (reports are public): tell the user to report it privately at https://github.com/invisibleloop/pulse-framework/security/advisories/new.
|
|
492
492
|
|
|
493
493
|
## Writing tests
|
|
494
494
|
|
|
@@ -582,7 +582,7 @@ Run these steps in order — do not declare work done without them:
|
|
|
582
582
|
|
|
583
583
|
## If Pulse itself is broken
|
|
584
584
|
|
|
585
|
-
If a documented Pulse feature or tool misbehaves and you have reduced it to a minimal repro that involves only Pulse, tell the user, give them the workaround, and offer to report it with \`pulse_report_issue\`. Preview first, show the user the redacted report verbatim, and send only after they approve. Do not report your own mistakes, spec errors or feature requests.
|
|
585
|
+
If a documented Pulse feature or tool misbehaves and you have reduced it to a minimal repro that involves only Pulse, tell the user, give them the workaround, and offer to report it with \`pulse_report_issue\`. Preview first, show the user the redacted report verbatim, and send only after they approve. Do not report your own mistakes, spec errors or feature requests. Never report a security vulnerability this way (reports are public): tell the user to report it privately at https://github.com/invisibleloop/pulse-framework/security/advisories/new.
|
|
586
586
|
|
|
587
587
|
## Writing tests
|
|
588
588
|
|
package/src/mcp/server.js
CHANGED
|
@@ -1081,7 +1081,7 @@ The generated file uses @invisibleloop/pulse/testing (renderSync / render). Do N
|
|
|
1081
1081
|
: []
|
|
1082
1082
|
|
|
1083
1083
|
const specName = path.basename(file, '.js')
|
|
1084
|
-
const importPath = path.relative(path.dirname(testFile), file)
|
|
1084
|
+
const importPath = path.relative(path.dirname(testFile), file)
|
|
1085
1085
|
|
|
1086
1086
|
const lines = []
|
|
1087
1087
|
lines.push(`import { describe, it } from 'node:test'`)
|
|
@@ -1632,6 +1632,8 @@ server.registerTool(
|
|
|
1632
1632
|
{
|
|
1633
1633
|
description: `Report a bug in the Pulse framework itself (not in the user's own code). Use only after you have confirmed the problem is Pulse's — e.g. a documented feature misbehaves, a Pulse tool returns a wrong result, or a component renders incorrectly from valid input — and have a minimal repro. Do not report your own mistakes or missing features.
|
|
1634
1634
|
|
|
1635
|
+
Never use this for a security vulnerability — reports become public issues; tell the user to report it privately at https://github.com/invisibleloop/pulse-framework/security/advisories/new.
|
|
1636
|
+
|
|
1635
1637
|
Two steps. First call (no confirmed): returns the redacted report for you to show the user verbatim — nothing is sent. If the user agrees, call again with the same fields plus confirmed: true and the previewId from step one. Never set confirmed without the user's explicit approval. Do not put secrets, credentials or private user data in any field.`,
|
|
1636
1638
|
inputSchema: {
|
|
1637
1639
|
title: z.string().min(8).max(120).describe('One-line summary of the bug'),
|
package/src/md/index.js
CHANGED
|
@@ -17,6 +17,20 @@ import { highlight } from './highlight.js'
|
|
|
17
17
|
// HTML escaping
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Remove an ATX heading's optional closing sequence ("## Title ##" → "Title").
|
|
22
|
+
* Done by hand rather than with /\s+#+\s*$/, which backtracks quadratically on
|
|
23
|
+
* long runs of whitespace.
|
|
24
|
+
*/
|
|
25
|
+
function stripClosingHashes(text) {
|
|
26
|
+
let end = text.length
|
|
27
|
+
while (end > 0 && /\s/.test(text[end - 1])) end--
|
|
28
|
+
let start = end
|
|
29
|
+
while (start > 0 && text[start - 1] === '#') start--
|
|
30
|
+
if (start === end || start === 0 || !/\s/.test(text[start - 1])) return text
|
|
31
|
+
return text.slice(0, start).trimEnd()
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
function esc(s) {
|
|
21
35
|
return String(s)
|
|
22
36
|
.replace(/&/g, '&')
|
|
@@ -147,10 +161,14 @@ function parseBlocks(text) {
|
|
|
147
161
|
continue
|
|
148
162
|
}
|
|
149
163
|
|
|
150
|
-
// ATX heading (# through ######)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
// ATX heading (# through ######). This must accept every line the paragraph
|
|
165
|
+
// loop below treats as "a heading starts here" (/^#{1,6}\s/): if the two
|
|
166
|
+
// disagree, neither claims the line and the parser never advances.
|
|
167
|
+
// The marker is matched on its own, with the text taken from what follows, so
|
|
168
|
+
// there is no overlapping whitespace/content regex to backtrack.
|
|
169
|
+
const headMarker = line.match(/^#{1,6}(?=\s)/)
|
|
170
|
+
if (headMarker) {
|
|
171
|
+
blocks.push({ type: 'heading', level: headMarker[0].length, content: stripClosingHashes(line.slice(headMarker[0].length).trimStart()) })
|
|
154
172
|
i++; continue
|
|
155
173
|
}
|
|
156
174
|
|
|
@@ -219,9 +237,12 @@ function parseBlocks(text) {
|
|
|
219
237
|
if (/^\d+[.)]\s/.test(l)) break
|
|
220
238
|
paraLines.push(l); i++
|
|
221
239
|
}
|
|
222
|
-
if (paraLines.length
|
|
223
|
-
|
|
240
|
+
if (paraLines.length === 0) {
|
|
241
|
+
// Nothing above claimed this line and the paragraph loop refused it. Never
|
|
242
|
+
// loop without consuming input — treat it as text and move on.
|
|
243
|
+
paraLines.push(lines[i]); i++
|
|
224
244
|
}
|
|
245
|
+
blocks.push({ type: 'paragraph', content: paraLines.join(' ') })
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
return blocks
|
|
@@ -311,6 +332,8 @@ function markdownToHtml(text) {
|
|
|
311
332
|
* @returns {{ html: string, frontmatter: Record<string, string> }}
|
|
312
333
|
*/
|
|
313
334
|
export function parseMd(source) {
|
|
335
|
+
// Normalise line endings first: CRLF files otherwise leave a stray "\r" on every line.
|
|
336
|
+
source = String(source).replace(/\r\n?/g, '\n')
|
|
314
337
|
const { frontmatter, body } = extractFrontmatter(source)
|
|
315
338
|
return { html: markdownToHtml(body), frontmatter }
|
|
316
339
|
}
|
package/src/runtime/index.js
CHANGED
|
@@ -869,7 +869,8 @@ function viewErrorFallback(err) {
|
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
function isValidEmail(value) {
|
|
872
|
-
|
|
872
|
+
// 254 is the RFC 5321 maximum; the cap also bounds the regex's worst case.
|
|
873
|
+
return value.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
873
874
|
}
|
|
874
875
|
|
|
875
876
|
// ---------------------------------------------------------------------------
|
package/src/server/index.js
CHANGED
|
@@ -1659,9 +1659,15 @@ function matchRoute(router, pathname) {
|
|
|
1659
1659
|
*/
|
|
1660
1660
|
function routeToRegex(route) {
|
|
1661
1661
|
const params = []
|
|
1662
|
+
// Split on :params so every literal segment can be regex-escaped ("." in
|
|
1663
|
+
// /feed.xml must not match any character).
|
|
1662
1664
|
const pattern = route
|
|
1663
|
-
.
|
|
1664
|
-
.
|
|
1665
|
+
.split(/(:[^/]+)/)
|
|
1666
|
+
.map(part => {
|
|
1667
|
+
if (part.startsWith(':')) { params.push(part.slice(1)); return '([^/]+)' }
|
|
1668
|
+
return part.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
|
|
1669
|
+
})
|
|
1670
|
+
.join('')
|
|
1665
1671
|
|
|
1666
1672
|
return {
|
|
1667
1673
|
pattern: new RegExp(`^${pattern}\\/?$`),
|
|
@@ -2221,7 +2227,7 @@ function streamErrorScript(err, dev, nonce = '') {
|
|
|
2221
2227
|
`</div>`
|
|
2222
2228
|
: `<div style="padding:2rem;text-align:center;font-family:system-ui,sans-serif;color:#666">Something went wrong.</div>`
|
|
2223
2229
|
|
|
2224
|
-
return `<script nonce="${nonce}">(function(){var r=document.getElementById('pulse-root');if(r)r.innerHTML=${JSON.stringify(inner)};})()</script>`
|
|
2230
|
+
return `<script nonce="${nonce}">(function(){var r=document.getElementById('pulse-root');if(r)r.innerHTML=${JSON.stringify(inner).replace(/</g, '\\u003c')};})()</script>`
|
|
2225
2231
|
}
|
|
2226
2232
|
|
|
2227
2233
|
function errorPage500() {
|
package/src/testing/html.js
CHANGED
|
@@ -64,14 +64,15 @@ function parseAttrs(str) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function decodeEntities(str) {
|
|
67
|
+
// & must be decoded last: decoding it first turns "&lt;" into "<".
|
|
67
68
|
return str
|
|
68
|
-
.replace(/&/g, '&')
|
|
69
69
|
.replace(/</g, '<')
|
|
70
70
|
.replace(/>/g, '>')
|
|
71
71
|
.replace(/"/g, '"')
|
|
72
72
|
.replace(/�?39;/g, "'")
|
|
73
73
|
.replace(/'/g, "'")
|
|
74
74
|
.replace(/ /g, ' ')
|
|
75
|
+
.replace(/&/g, '&')
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|