@webjsdev/server 0.7.3 → 0.8.0
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/README.md +7 -2
- package/index.js +21 -3
- package/package.json +1 -3
- package/src/actions.js +6 -6
- package/src/cache.js +19 -2
- package/src/check.js +226 -95
- package/src/component-elision.js +797 -0
- package/src/component-scanner.js +8 -2
- package/src/context.js +36 -0
- package/src/crypto-utils.js +65 -0
- package/src/dev.js +478 -93
- package/src/importmap.js +282 -20
- package/src/js-scan.js +288 -0
- package/src/module-graph.js +150 -13
- package/src/rate-limit.js +100 -12
- package/src/script-tag-json.js +63 -0
- package/src/session.js +60 -14
- package/src/ssr.js +133 -17
- package/src/vendor.js +1231 -103
- package/src/websocket.js +3 -1
package/src/component-scanner.js
CHANGED
|
@@ -94,11 +94,17 @@ export async function scanComponents(appDir) {
|
|
|
94
94
|
* again (e.g. on dev-server rebuild after a file add), new discoveries
|
|
95
95
|
* are added and existing tags are updated.
|
|
96
96
|
*
|
|
97
|
+
* Pass `components` if you already have the scanned list (e.g. the
|
|
98
|
+
* dev server scans once and reuses for both the registry and the
|
|
99
|
+
* source-serving authorisation gate). Omitting it triggers a fresh
|
|
100
|
+
* scan, matching the original single-arg signature.
|
|
101
|
+
*
|
|
97
102
|
* @param {string} appDir
|
|
103
|
+
* @param {Awaited<ReturnType<typeof scanComponents>>} [components]
|
|
98
104
|
* @returns {Promise<{ count: number }>}
|
|
99
105
|
*/
|
|
100
|
-
export async function primeComponentRegistry(appDir) {
|
|
101
|
-
|
|
106
|
+
export async function primeComponentRegistry(appDir, components) {
|
|
107
|
+
components = components ?? await scanComponents(appDir);
|
|
102
108
|
for (const { tag, moduleUrl } of components) {
|
|
103
109
|
primeModuleUrl(tag, moduleUrl);
|
|
104
110
|
}
|
package/src/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { parseCookies } from './csrf.js';
|
|
3
|
+
import { setCspNonceProvider, cspNonce } from '@webjsdev/core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Per-request context backed by AsyncLocalStorage. Lets server-side code
|
|
@@ -31,6 +32,41 @@ export function getRequest() {
|
|
|
31
32
|
return als.getStore()?.req ?? null;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Server-only implementation of the CSP nonce reader: pulls the
|
|
37
|
+
* current request from AsyncLocalStorage, parses the
|
|
38
|
+
* `script-src 'nonce-...'` value from its CSP header, returns ''
|
|
39
|
+
* when none in scope.
|
|
40
|
+
*
|
|
41
|
+
* The public `cspNonce()` function lives in `@webjsdev/core` so user
|
|
42
|
+
* layouts / pages can import it without dragging server-only deps
|
|
43
|
+
* (node:async_hooks etc.) into browser-loaded modules. The actual
|
|
44
|
+
* implementation is wired here, server-side only, via
|
|
45
|
+
* `setCspNonceProvider`. On the browser there is no provider, so
|
|
46
|
+
* `cspNonce()` returns '' (empty `nonce=""` attribute, browser
|
|
47
|
+
* ignores it).
|
|
48
|
+
*/
|
|
49
|
+
// The regex captures the first `nonce-...` token anywhere in the CSP
|
|
50
|
+
// header. Webjs uses a single per-request nonce shared across all
|
|
51
|
+
// directives that emit it (the standard CSP3 single-nonce model),
|
|
52
|
+
// so reading the first match is correct. If a future caller emits
|
|
53
|
+
// styled inline content under a separate style nonce, this reader
|
|
54
|
+
// would need to become directive-scoped. Kept identical to the
|
|
55
|
+
// matching helper in ssr.js so both paths interpret the header the
|
|
56
|
+
// same way.
|
|
57
|
+
setCspNonceProvider(() => {
|
|
58
|
+
const req = als.getStore()?.req;
|
|
59
|
+
if (!req) return '';
|
|
60
|
+
const csp = req.headers.get('content-security-policy') || '';
|
|
61
|
+
const match = /\bnonce-([A-Za-z0-9+/=]+)/.exec(csp);
|
|
62
|
+
return match ? match[1] : '';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Re-export for backwards-compat: callers that imported cspNonce from
|
|
66
|
+
// @webjsdev/server still work. New code should import from
|
|
67
|
+
// @webjsdev/core for browser-isomorphism.
|
|
68
|
+
export { cspNonce };
|
|
69
|
+
|
|
34
70
|
/**
|
|
35
71
|
* Read-only headers for the in-flight request. Throws outside a request
|
|
36
72
|
* (e.g. at module top-level).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-Crypto-based hash helpers. Shared by the SSR / vendor / actions
|
|
3
|
+
* paths that previously used `node:crypto.createHash`. The Web Crypto
|
|
4
|
+
* API replaces the synchronous Node-only API with a Promise-returning
|
|
5
|
+
* one; the trade-off is portability across Node, Deno, Bun, and edge
|
|
6
|
+
* runtimes.
|
|
7
|
+
*
|
|
8
|
+
* @module crypto-utils
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const enc = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ArrayBuffer | Uint8Array} buf
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function bufToHex(buf) {
|
|
18
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
19
|
+
let hex = '';
|
|
20
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, '0');
|
|
21
|
+
return hex;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {ArrayBuffer | Uint8Array} buf
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function bufToBase64(buf) {
|
|
29
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
30
|
+
let s = '';
|
|
31
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
32
|
+
return btoa(s);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string | ArrayBufferView | ArrayBuffer} data
|
|
37
|
+
* @returns {Uint8Array}
|
|
38
|
+
*/
|
|
39
|
+
function toBytes(data) {
|
|
40
|
+
if (typeof data === 'string') return enc.encode(data);
|
|
41
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
42
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute a hex-encoded digest of `data` under `algo`.
|
|
47
|
+
*
|
|
48
|
+
* @param {'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512'} algo
|
|
49
|
+
* @param {string | ArrayBufferView | ArrayBuffer} data
|
|
50
|
+
* @returns {Promise<string>} full hex string (no truncation)
|
|
51
|
+
*/
|
|
52
|
+
export async function digestHex(algo, data) {
|
|
53
|
+
return bufToHex(await crypto.subtle.digest(algo, toBytes(data)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute a base64-encoded digest of `data` under `algo`.
|
|
58
|
+
*
|
|
59
|
+
* @param {'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512'} algo
|
|
60
|
+
* @param {string | ArrayBufferView | ArrayBuffer} data
|
|
61
|
+
* @returns {Promise<string>}
|
|
62
|
+
*/
|
|
63
|
+
export async function digestBase64(algo, data) {
|
|
64
|
+
return bufToBase64(await crypto.subtle.digest(algo, toBytes(data)));
|
|
65
|
+
}
|