@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.
@@ -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
- const components = await scanComponents(appDir);
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
+ }