@invisibleloop/pulse 0.20.52 → 0.20.53
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 +2 -2
- package/public/.pulse-ui-version +1 -1
- package/src/agent/guide-server.md +13 -0
- package/src/cli/dev.js +6 -1
- package/src/cli/start.js +1 -1
- package/src/server/index.js +22 -4
- package/types/server.d.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invisibleloop/pulse",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI-first frontend framework. The spec is the source of truth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"dev": "node --watch src/cli/dev.js",
|
|
78
78
|
"build": "node scripts/build.js",
|
|
79
|
-
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
79
|
+
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/cli/dev-host.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
80
80
|
"typecheck": "tsc --noEmit",
|
|
81
81
|
"docs": "node --watch src/cli/dev.js --root ../../docs --port 4000",
|
|
82
82
|
"docs:start": "node ../../docs/server.js"
|
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.20.
|
|
1
|
+
0.20.53
|
|
@@ -371,6 +371,19 @@ export default {
|
|
|
371
371
|
}
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
+
## Production behind a proxy — pin the public origin
|
|
375
|
+
|
|
376
|
+
By default Pulse builds `<link rel="canonical">`, `sitemap.xml` and `robots.txt` from the request's `Host`, `X-Forwarded-Host` and `X-Forwarded-Proto` headers. Anyone can send those headers, and a CDN that ignores `Vary` will cache the result for everyone — one forged request can point every visitor's canonical URL at another domain. For a real deployment, pin the origin:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
// pulse.config.js
|
|
380
|
+
export default {
|
|
381
|
+
origin: 'https://example.com', // canonical + sitemap + robots use this; headers are ignored
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
It must be an absolute http(s) URL (a bad value fails at startup). `sitemap: { origin }` still wins for the sitemap if you set both. Also set `host: '127.0.0.1'` in `pulse.config.js` if a reverse proxy on the same machine is the only thing that should reach the server. `pulse dev` already listens on `127.0.0.1`; use `pulse dev --host 0.0.0.0` for phone testing or a container.
|
|
386
|
+
|
|
374
387
|
## Health check endpoint
|
|
375
388
|
|
|
376
389
|
Pulse exposes a built-in health check at `/healthz` by default. It responds **before** `onRequest`, static files, and route matching — so load balancers always get a response.
|
package/src/cli/dev.js
CHANGED
|
@@ -21,6 +21,7 @@ import * as log from './logger.js'
|
|
|
21
21
|
const args = process.argv.slice(2)
|
|
22
22
|
const rootArg = args.indexOf('--root')
|
|
23
23
|
const portArg = args.indexOf('--port')
|
|
24
|
+
const hostArg = args.indexOf('--host')
|
|
24
25
|
|
|
25
26
|
const ROOT = rootArg !== -1
|
|
26
27
|
? path.resolve(args[rootArg + 1])
|
|
@@ -295,6 +296,10 @@ const reloadScript = (nonce) => `<script nonce="${nonce}">
|
|
|
295
296
|
|
|
296
297
|
const { updateSpecs, updateStore } = await createServer(specs, {
|
|
297
298
|
port: PORT,
|
|
299
|
+
// Loopback by default: the dev server has dev-only endpoints and stack-trace error
|
|
300
|
+
// pages, and should not be reachable from the LAN. `--host 0.0.0.0` (or `host` in
|
|
301
|
+
// pulse.config.js) opts in — e.g. for testing on a phone, or inside a container.
|
|
302
|
+
host: (hostArg !== -1 && args[hostArg + 1]) || _config.host || '127.0.0.1',
|
|
298
303
|
stream: true,
|
|
299
304
|
staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
|
|
300
305
|
manifest: {}, // never use a build manifest in dev — always serve source files
|
|
@@ -305,7 +310,7 @@ const { updateSpecs, updateStore } = await createServer(specs, {
|
|
|
305
310
|
...(_config.csp ? { csp: _config.csp } : {}),
|
|
306
311
|
// Server options forwarded verbatim from pulse.config.js so dev behaves like
|
|
307
312
|
// production — keep in sync with start.js PASSTHROUGH_OPTIONS
|
|
308
|
-
...(['redirects', 'sitemap', 'robots', 'secret', 'live', 'trailingSlash', 'fetcherTimeout', 'maxBody'].reduce(
|
|
313
|
+
...(['redirects', 'sitemap', 'robots', 'secret', 'live', 'trailingSlash', 'fetcherTimeout', 'maxBody', 'origin'].reduce(
|
|
309
314
|
(acc, k) => (_config[k] !== undefined ? { ...acc, [k]: _config[k] } : acc), {})),
|
|
310
315
|
|
|
311
316
|
onRequest(req, res) {
|
package/src/cli/start.js
CHANGED
|
@@ -48,7 +48,7 @@ let csp = null
|
|
|
48
48
|
let passthrough = {}
|
|
49
49
|
|
|
50
50
|
// Server options forwarded verbatim from pulse.config.js — keep in sync with dev.js
|
|
51
|
-
const PASSTHROUGH_OPTIONS = ['redirects', 'sitemap', 'robots', 'secret', 'live', 'trailingSlash', 'fetcherTimeout', 'maxBody']
|
|
51
|
+
const PASSTHROUGH_OPTIONS = ['redirects', 'sitemap', 'robots', 'secret', 'live', 'trailingSlash', 'fetcherTimeout', 'maxBody', 'origin', 'host']
|
|
52
52
|
|
|
53
53
|
const configPath = path.join(ROOT, 'pulse.config.js')
|
|
54
54
|
if (fs.existsSync(configPath)) {
|
package/src/server/index.js
CHANGED
|
@@ -614,6 +614,11 @@ export async function createServer(entries, options = {}) {
|
|
|
614
614
|
staticDir = null,
|
|
615
615
|
manifest = null, // path to manifest.json or plain object
|
|
616
616
|
trailingSlash = 'remove', // 'remove' | 'add' | 'allow'
|
|
617
|
+
origin: pinOrigin = null, // pin the public origin, e.g. 'https://example.com': used for
|
|
618
|
+
// <link rel="canonical">, sitemap.xml and robots.txt INSTEAD of the
|
|
619
|
+
// Host / X-Forwarded-* headers, which a client (or a CDN that ignores
|
|
620
|
+
// Vary) can forge. Recommended in production.
|
|
621
|
+
host: listenHost = null, // interface to listen on (default: all). `pulse dev` uses 127.0.0.1.
|
|
617
622
|
extraBody = '', // extra HTML injected before </body> on every page
|
|
618
623
|
dev = false, // dev mode — show detailed error pages
|
|
619
624
|
store = null, // global store definition (pulse.store.js default export)
|
|
@@ -653,6 +658,17 @@ export async function createServer(entries, options = {}) {
|
|
|
653
658
|
quiet = false, // suppress request logging
|
|
654
659
|
} = options
|
|
655
660
|
|
|
661
|
+
// A pinned origin must be a bare http(s) origin; normalise it ("https://example.com/x" -> "https://example.com").
|
|
662
|
+
let pinnedOrigin = null
|
|
663
|
+
if (pinOrigin != null) {
|
|
664
|
+
let u = null
|
|
665
|
+
try { u = new URL(String(pinOrigin)) } catch { /* rejected below */ }
|
|
666
|
+
if (!u || (u.protocol !== 'http:' && u.protocol !== 'https:')) {
|
|
667
|
+
throw new Error(`createServer: origin must be an absolute http(s) URL such as "https://example.com" (got ${JSON.stringify(pinOrigin)})`)
|
|
668
|
+
}
|
|
669
|
+
pinnedOrigin = u.origin
|
|
670
|
+
}
|
|
671
|
+
|
|
656
672
|
// Resolve URL entries → spec objects, auto-setting hydrate where needed
|
|
657
673
|
const specs = await resolveSpecEntries(entries, root)
|
|
658
674
|
|
|
@@ -794,7 +810,7 @@ export async function createServer(entries, options = {}) {
|
|
|
794
810
|
// Sitemap — track the live spec list (updated on hot reload) and surface a
|
|
795
811
|
// startup hint for dynamic routes that can't be enumerated automatically.
|
|
796
812
|
const sitemapEnabled = !!sitemap
|
|
797
|
-
const sitemapOrigin = (typeof sitemap === 'object' && sitemap?.origin) ? String(sitemap.origin).replace(/\/$/, '') : null
|
|
813
|
+
const sitemapOrigin = ((typeof sitemap === 'object' && sitemap?.origin) ? String(sitemap.origin).replace(/\/$/, '') : null) || pinnedOrigin
|
|
798
814
|
let allSpecs = specs
|
|
799
815
|
if (sitemapEnabled && !quiet) {
|
|
800
816
|
const unenumerated = specs.filter(s =>
|
|
@@ -1157,7 +1173,7 @@ export async function createServer(entries, options = {}) {
|
|
|
1157
1173
|
const canonicalPath = trailingSlash === 'add' && pathname !== '/'
|
|
1158
1174
|
? (pathname.endsWith('/') ? pathname : pathname + '/')
|
|
1159
1175
|
: (pathname !== '/' && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname)
|
|
1160
|
-
const canonicalBase = `${proto}://${host}${canonicalPath}`
|
|
1176
|
+
const canonicalBase = pinnedOrigin ? `${pinnedOrigin}${canonicalPath}` : `${proto}://${host}${canonicalPath}`
|
|
1161
1177
|
|
|
1162
1178
|
// Raw content spec (RSS, sitemap, JSON API, webhooks) — bypass HTML pipeline
|
|
1163
1179
|
if (spec.contentType) {
|
|
@@ -1261,7 +1277,7 @@ export async function createServer(entries, options = {}) {
|
|
|
1261
1277
|
process.on('SIGTERM', shutdown)
|
|
1262
1278
|
process.on('SIGINT', shutdown)
|
|
1263
1279
|
|
|
1264
|
-
|
|
1280
|
+
const onListening = () => {
|
|
1265
1281
|
import('../cli/logger.js').then(({ banner }) => {
|
|
1266
1282
|
banner({
|
|
1267
1283
|
url: `http://localhost:${port}`,
|
|
@@ -1272,7 +1288,9 @@ export async function createServer(entries, options = {}) {
|
|
|
1272
1288
|
}).catch(() => {
|
|
1273
1289
|
console.log(`⚡ Pulse running at http://localhost:${port}`)
|
|
1274
1290
|
})
|
|
1275
|
-
}
|
|
1291
|
+
}
|
|
1292
|
+
if (listenHost) server.listen(port, listenHost, onListening)
|
|
1293
|
+
else server.listen(port, onListening)
|
|
1276
1294
|
|
|
1277
1295
|
return {
|
|
1278
1296
|
server,
|
package/types/server.d.ts
CHANGED
|
@@ -36,6 +36,21 @@ export interface ServerOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
trailingSlash?: 'remove' | 'add' | 'allow'
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Pin the site's public origin, e.g. `'https://example.com'`. Used for `<link rel="canonical">`,
|
|
41
|
+
* `sitemap.xml` and `robots.txt` INSTEAD of the `Host` / `X-Forwarded-Host` / `X-Forwarded-Proto`
|
|
42
|
+
* headers, which a client — or a CDN that ignores `Vary` — can forge to poison those URLs.
|
|
43
|
+
* Recommended in production. An explicit `sitemap.origin` still takes precedence for the sitemap.
|
|
44
|
+
* Must be an absolute http(s) URL; anything after the origin is dropped.
|
|
45
|
+
*/
|
|
46
|
+
origin?: string
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Network interface to listen on. Default: all interfaces (what a server behind a proxy or in
|
|
50
|
+
* a container needs). `pulse dev` passes `'127.0.0.1'` unless told otherwise.
|
|
51
|
+
*/
|
|
52
|
+
host?: string
|
|
53
|
+
|
|
39
54
|
/** Maximum request body size in bytes. Default 1 MB. Requests exceeding this receive a 413. */
|
|
40
55
|
maxBody?: number
|
|
41
56
|
|