@c9up/aurora 0.1.29 → 0.1.31
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/dist/AuroraProvider.d.ts +1 -14
- package/dist/AuroraProvider.js +4 -4
- package/dist/Pages.d.ts +1 -1
- package/dist/Pages.js +3 -3
- package/dist/server/renderPage.d.ts +22 -0
- package/dist/server/renderPage.js +29 -3
- package/package.json +1 -1
- package/src/AuroraProvider.ts +4 -4
- package/src/Pages.ts +3 -3
- package/src/server/renderPage.ts +61 -10
package/dist/AuroraProvider.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface AuroraAppContext {
|
|
|
28
28
|
config: AuroraConfigStore;
|
|
29
29
|
}
|
|
30
30
|
export default class AuroraProvider {
|
|
31
|
+
#private;
|
|
31
32
|
protected app: AuroraAppContext;
|
|
32
33
|
constructor(app: AuroraAppContext);
|
|
33
34
|
register(): void;
|
|
@@ -35,19 +36,5 @@ export default class AuroraProvider {
|
|
|
35
36
|
start(): Promise<void>;
|
|
36
37
|
ready(): Promise<void>;
|
|
37
38
|
shutdown(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Resolve the user-supplied config:
|
|
40
|
-
* - relative `pages.root` (e.g. `./resources/pages`) is joined to
|
|
41
|
-
* the project's `appRoot` URL — same convention `modules.path`
|
|
42
|
-
* uses;
|
|
43
|
-
* - absolute paths are passed through;
|
|
44
|
-
* - missing config falls back to `<appRoot>/resources/pages`.
|
|
45
|
-
*
|
|
46
|
-
* `appRoot` is fetched from the container if the host registered
|
|
47
|
-
* one (Ream does, since v0.x — see Ignitor); other hosts get the
|
|
48
|
-
* `process.cwd()` fallback.
|
|
49
|
-
*/
|
|
50
|
-
private resolveConfig;
|
|
51
|
-
private readAppRoot;
|
|
52
39
|
}
|
|
53
40
|
export {};
|
package/dist/AuroraProvider.js
CHANGED
|
@@ -29,7 +29,7 @@ export default class AuroraProvider {
|
|
|
29
29
|
register() {
|
|
30
30
|
this.app.container.singleton(AuroraManager, async () => {
|
|
31
31
|
const raw = this.app.config.get("aurora");
|
|
32
|
-
const config = await this
|
|
32
|
+
const config = await this.#resolveConfig(raw);
|
|
33
33
|
const manager = new AuroraManager(config);
|
|
34
34
|
setAurora(manager);
|
|
35
35
|
return manager;
|
|
@@ -89,8 +89,8 @@ export default class AuroraProvider {
|
|
|
89
89
|
* one (Ream does, since v0.x — see Ignitor); other hosts get the
|
|
90
90
|
* `process.cwd()` fallback.
|
|
91
91
|
*/
|
|
92
|
-
async resolveConfig(raw) {
|
|
93
|
-
const appRoot = await this
|
|
92
|
+
async #resolveConfig(raw) {
|
|
93
|
+
const appRoot = await this.#readAppRoot();
|
|
94
94
|
const userRoot = raw?.pages?.root;
|
|
95
95
|
const root = typeof userRoot === "string" && userRoot.length > 0
|
|
96
96
|
? isAbsolute(userRoot)
|
|
@@ -102,7 +102,7 @@ export default class AuroraProvider {
|
|
|
102
102
|
pages: { ...(raw?.pages ?? {}), root },
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
-
async readAppRoot() {
|
|
105
|
+
async #readAppRoot() {
|
|
106
106
|
try {
|
|
107
107
|
const raw = await this.app.container.resolve("appRoot");
|
|
108
108
|
if (raw instanceof URL)
|
package/dist/Pages.d.ts
CHANGED
|
@@ -45,10 +45,10 @@ export interface PagesConfig {
|
|
|
45
45
|
* call `register()` to short-circuit the disk lookup.
|
|
46
46
|
*/
|
|
47
47
|
export declare class Pages {
|
|
48
|
+
#private;
|
|
48
49
|
readonly root: string;
|
|
49
50
|
readonly urlPrefix: string;
|
|
50
51
|
readonly extension: string;
|
|
51
|
-
private readonly registry;
|
|
52
52
|
constructor(config: PagesConfig);
|
|
53
53
|
/**
|
|
54
54
|
* Pre-register a page factory under `name`, bypassing the disk
|
package/dist/Pages.js
CHANGED
|
@@ -26,7 +26,7 @@ export class Pages {
|
|
|
26
26
|
root;
|
|
27
27
|
urlPrefix;
|
|
28
28
|
extension;
|
|
29
|
-
registry = new Map();
|
|
29
|
+
#registry = new Map();
|
|
30
30
|
constructor(config) {
|
|
31
31
|
// Normalize the root ONCE so the `startsWith(root + sep)` containment
|
|
32
32
|
// check below compares like-for-like against the resolved page path.
|
|
@@ -47,7 +47,7 @@ export class Pages {
|
|
|
47
47
|
* back as `unknown` — the renderer JSON.stringifies them either way.
|
|
48
48
|
*/
|
|
49
49
|
register(name, factory) {
|
|
50
|
-
this
|
|
50
|
+
this.#registry.set(name, factory);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Resolve a page name to its factory function. Throws when the
|
|
@@ -58,7 +58,7 @@ export class Pages {
|
|
|
58
58
|
* under `root` — defense in depth against URL-decoding tricks.
|
|
59
59
|
*/
|
|
60
60
|
async resolve(name) {
|
|
61
|
-
const preset = this
|
|
61
|
+
const preset = this.#registry.get(name);
|
|
62
62
|
if (preset)
|
|
63
63
|
return preset;
|
|
64
64
|
assertSafeName(name);
|
|
@@ -31,12 +31,34 @@ export interface RenderResponse {
|
|
|
31
31
|
status(code: number): RenderResponse;
|
|
32
32
|
header(name: string, value: string): RenderResponse;
|
|
33
33
|
send(body: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Per-request CSP nonce, when a security layer set one.
|
|
36
|
+
*
|
|
37
|
+
* `@c9up/blackhole` seeds it on the response (the AdonisJS idiom,
|
|
38
|
+
* `response.nonce`) whenever the policy uses `@nonce`. Optional, and read
|
|
39
|
+
* structurally: aurora stays free of any dependency on it, and a host that
|
|
40
|
+
* sets no policy renders exactly as before.
|
|
41
|
+
*/
|
|
42
|
+
nonce?: string;
|
|
34
43
|
}
|
|
35
44
|
export interface RenderHttpContext {
|
|
36
45
|
request: unknown;
|
|
37
46
|
response: RenderResponse;
|
|
47
|
+
/** Per-request bag; blackhole also seeds `cspNonce` here. */
|
|
48
|
+
store?: {
|
|
49
|
+
get(key: string): unknown;
|
|
50
|
+
};
|
|
38
51
|
}
|
|
39
52
|
export interface RenderPageOptions {
|
|
53
|
+
/**
|
|
54
|
+
* CSP nonce for the inline scripts this page emits.
|
|
55
|
+
*
|
|
56
|
+
* Normally left unset: it is read from `response.nonce` (what blackhole
|
|
57
|
+
* seeds) or from the request store. Pass it only when the security layer
|
|
58
|
+
* puts it somewhere else. It must be the SAME nonce the policy header
|
|
59
|
+
* names, otherwise the browser blocks the scripts anyway.
|
|
60
|
+
*/
|
|
61
|
+
nonce?: string;
|
|
40
62
|
/**
|
|
41
63
|
* Importmap entries injected into `<head>`. Defaults to mapping
|
|
42
64
|
* `@c9up/aurora` to `/__assets/aurora/index.js`. Override to point
|
|
@@ -73,17 +73,23 @@ async function renderPageInScope(ctx, pages, name, props, options) {
|
|
|
73
73
|
const rootClass = options.rootClass;
|
|
74
74
|
const lang = options.lang ?? "en";
|
|
75
75
|
const pageUrl = pages.urlFor(name);
|
|
76
|
+
// A CSP that names a nonce blocks every inline script that lacks it, and the
|
|
77
|
+
// page then renders but never hydrates — the HTML is byte-identical, so only
|
|
78
|
+
// a real browser shows the failure. Reading it here is what lets a default
|
|
79
|
+
// policy stay strict instead of being turned off.
|
|
80
|
+
const nonce = resolveNonce(ctx, options.nonce);
|
|
81
|
+
const nonceAttr = nonce === undefined ? "" : ` nonce="${escapeAttr(nonce)}"`;
|
|
76
82
|
const doc = `<!doctype html>
|
|
77
83
|
<html lang="${escapeAttr(lang)}">
|
|
78
84
|
<head>
|
|
79
85
|
<meta charset="utf-8" />
|
|
80
86
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
81
|
-
<script type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
87
|
+
<script${nonceAttr} type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
82
88
|
${options.headExtra ?? ""}
|
|
83
89
|
</head>
|
|
84
90
|
<body>
|
|
85
91
|
<${rootTag}${rootAttrs(rootId, rootClass)}>${body}</${rootTag}>
|
|
86
|
-
<script id="aurora-page-data" type="application/json">${escapeJsonForScript({
|
|
92
|
+
<script${nonceAttr} id="aurora-page-data" type="application/json">${escapeJsonForScript({
|
|
87
93
|
name,
|
|
88
94
|
props: pageProps,
|
|
89
95
|
url: pageUrl,
|
|
@@ -91,7 +97,7 @@ ${options.headExtra ?? ""}
|
|
|
91
97
|
routes: options.routes ?? {},
|
|
92
98
|
version: options.assetsVersion ?? null,
|
|
93
99
|
})}</script>
|
|
94
|
-
<script type="module">
|
|
100
|
+
<script${nonceAttr} type="module">
|
|
95
101
|
import { hydrate, setRouteManifest } from '@c9up/aurora'
|
|
96
102
|
import Page from ${JSON.stringify(pageUrl)}
|
|
97
103
|
const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
|
|
@@ -103,6 +109,26 @@ hydrate(document.getElementById(data.rootId), () => Page(data.props))
|
|
|
103
109
|
ctx.response.header("content-type", "text/html; charset=utf-8");
|
|
104
110
|
ctx.response.send(doc);
|
|
105
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The nonce to stamp on inline scripts, or `undefined` when there is none.
|
|
114
|
+
*
|
|
115
|
+
* Explicit option first, then `response.nonce` (what AdonisJS exposes and what
|
|
116
|
+
* blackhole seeds), then the `cspNonce` a middleware may have left in the
|
|
117
|
+
* per-request store. Never generated here: a nonce aurora invented would not
|
|
118
|
+
* appear in the policy header, so it would block the page rather than unblock
|
|
119
|
+
* it.
|
|
120
|
+
*/
|
|
121
|
+
function resolveNonce(ctx, explicit) {
|
|
122
|
+
if (typeof explicit === "string" && explicit.length > 0)
|
|
123
|
+
return explicit;
|
|
124
|
+
const fromResponse = ctx.response.nonce;
|
|
125
|
+
if (typeof fromResponse === "string" && fromResponse.length > 0)
|
|
126
|
+
return fromResponse;
|
|
127
|
+
const fromStore = ctx.store?.get("cspNonce");
|
|
128
|
+
if (typeof fromStore === "string" && fromStore.length > 0)
|
|
129
|
+
return fromStore;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
106
132
|
async function resolveSharedProps(ctx, shared) {
|
|
107
133
|
if (!shared)
|
|
108
134
|
return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/AuroraProvider.ts
CHANGED
|
@@ -54,7 +54,7 @@ export default class AuroraProvider {
|
|
|
54
54
|
register(): void {
|
|
55
55
|
this.app.container.singleton(AuroraManager, async () => {
|
|
56
56
|
const raw = this.app.config.get<AuroraManagerConfig>("aurora");
|
|
57
|
-
const config = await this
|
|
57
|
+
const config = await this.#resolveConfig(raw);
|
|
58
58
|
const manager = new AuroraManager(config);
|
|
59
59
|
setAurora(manager);
|
|
60
60
|
return manager;
|
|
@@ -127,10 +127,10 @@ export default class AuroraProvider {
|
|
|
127
127
|
* one (Ream does, since v0.x — see Ignitor); other hosts get the
|
|
128
128
|
* `process.cwd()` fallback.
|
|
129
129
|
*/
|
|
130
|
-
|
|
130
|
+
async #resolveConfig(
|
|
131
131
|
raw: AuroraManagerConfig | undefined,
|
|
132
132
|
): Promise<AuroraManagerConfig> {
|
|
133
|
-
const appRoot = await this
|
|
133
|
+
const appRoot = await this.#readAppRoot();
|
|
134
134
|
const userRoot = raw?.pages?.root;
|
|
135
135
|
const root =
|
|
136
136
|
typeof userRoot === "string" && userRoot.length > 0
|
|
@@ -144,7 +144,7 @@ export default class AuroraProvider {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
async #readAppRoot(): Promise<string> {
|
|
148
148
|
try {
|
|
149
149
|
const raw = await this.app.container.resolve<unknown>("appRoot");
|
|
150
150
|
if (raw instanceof URL) return fileURLToPath(raw);
|
package/src/Pages.ts
CHANGED
|
@@ -59,7 +59,7 @@ export class Pages {
|
|
|
59
59
|
readonly urlPrefix: string;
|
|
60
60
|
readonly extension: string;
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
readonly #registry = new Map<string, PageFactory>();
|
|
63
63
|
|
|
64
64
|
constructor(config: PagesConfig) {
|
|
65
65
|
// Normalize the root ONCE so the `startsWith(root + sep)` containment
|
|
@@ -82,7 +82,7 @@ export class Pages {
|
|
|
82
82
|
* back as `unknown` — the renderer JSON.stringifies them either way.
|
|
83
83
|
*/
|
|
84
84
|
register<P>(name: string, factory: PageFactory<P>): void {
|
|
85
|
-
this
|
|
85
|
+
this.#registry.set(name, factory as PageFactory);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
@@ -94,7 +94,7 @@ export class Pages {
|
|
|
94
94
|
* under `root` — defense in depth against URL-decoding tricks.
|
|
95
95
|
*/
|
|
96
96
|
async resolve(name: string): Promise<PageFactory> {
|
|
97
|
-
const preset = this
|
|
97
|
+
const preset = this.#registry.get(name);
|
|
98
98
|
if (preset) return preset;
|
|
99
99
|
|
|
100
100
|
assertSafeName(name);
|
package/src/server/renderPage.ts
CHANGED
|
@@ -37,13 +37,33 @@ export interface RenderResponse {
|
|
|
37
37
|
status(code: number): RenderResponse;
|
|
38
38
|
header(name: string, value: string): RenderResponse;
|
|
39
39
|
send(body: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Per-request CSP nonce, when a security layer set one.
|
|
42
|
+
*
|
|
43
|
+
* `@c9up/blackhole` seeds it on the response (the AdonisJS idiom,
|
|
44
|
+
* `response.nonce`) whenever the policy uses `@nonce`. Optional, and read
|
|
45
|
+
* structurally: aurora stays free of any dependency on it, and a host that
|
|
46
|
+
* sets no policy renders exactly as before.
|
|
47
|
+
*/
|
|
48
|
+
nonce?: string;
|
|
40
49
|
}
|
|
41
50
|
export interface RenderHttpContext {
|
|
42
51
|
request: unknown;
|
|
43
52
|
response: RenderResponse;
|
|
53
|
+
/** Per-request bag; blackhole also seeds `cspNonce` here. */
|
|
54
|
+
store?: { get(key: string): unknown };
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export interface RenderPageOptions {
|
|
58
|
+
/**
|
|
59
|
+
* CSP nonce for the inline scripts this page emits.
|
|
60
|
+
*
|
|
61
|
+
* Normally left unset: it is read from `response.nonce` (what blackhole
|
|
62
|
+
* seeds) or from the request store. Pass it only when the security layer
|
|
63
|
+
* puts it somewhere else. It must be the SAME nonce the policy header
|
|
64
|
+
* names, otherwise the browser blocks the scripts anyway.
|
|
65
|
+
*/
|
|
66
|
+
nonce?: string;
|
|
47
67
|
/**
|
|
48
68
|
* Importmap entries injected into `<head>`. Defaults to mapping
|
|
49
69
|
* `@c9up/aurora` to `/__assets/aurora/index.js`. Override to point
|
|
@@ -203,25 +223,34 @@ async function renderPageInScope<P>(
|
|
|
203
223
|
const lang = options.lang ?? "en";
|
|
204
224
|
const pageUrl = pages.urlFor(name);
|
|
205
225
|
|
|
226
|
+
// A CSP that names a nonce blocks every inline script that lacks it, and the
|
|
227
|
+
// page then renders but never hydrates — the HTML is byte-identical, so only
|
|
228
|
+
// a real browser shows the failure. Reading it here is what lets a default
|
|
229
|
+
// policy stay strict instead of being turned off.
|
|
230
|
+
const nonce = resolveNonce(ctx, options.nonce);
|
|
231
|
+
const nonceAttr = nonce === undefined ? "" : ` nonce="${escapeAttr(nonce)}"`;
|
|
232
|
+
|
|
206
233
|
const doc = `<!doctype html>
|
|
207
234
|
<html lang="${escapeAttr(lang)}">
|
|
208
235
|
<head>
|
|
209
236
|
<meta charset="utf-8" />
|
|
210
237
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
211
|
-
<script type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
238
|
+
<script${nonceAttr} type="importmap">${escapeJsonForScript({ imports: importmap })}</script>
|
|
212
239
|
${options.headExtra ?? ""}
|
|
213
240
|
</head>
|
|
214
241
|
<body>
|
|
215
242
|
<${rootTag}${rootAttrs(rootId, rootClass)}>${body}</${rootTag}>
|
|
216
|
-
<script id="aurora-page-data" type="application/json">${escapeJsonForScript(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
243
|
+
<script${nonceAttr} id="aurora-page-data" type="application/json">${escapeJsonForScript(
|
|
244
|
+
{
|
|
245
|
+
name,
|
|
246
|
+
props: pageProps,
|
|
247
|
+
url: pageUrl,
|
|
248
|
+
rootId,
|
|
249
|
+
routes: options.routes ?? {},
|
|
250
|
+
version: options.assetsVersion ?? null,
|
|
251
|
+
},
|
|
252
|
+
)}</script>
|
|
253
|
+
<script${nonceAttr} type="module">
|
|
225
254
|
import { hydrate, setRouteManifest } from '@c9up/aurora'
|
|
226
255
|
import Page from ${JSON.stringify(pageUrl)}
|
|
227
256
|
const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
|
|
@@ -235,6 +264,28 @@ hydrate(document.getElementById(data.rootId), () => Page(data.props))
|
|
|
235
264
|
ctx.response.send(doc);
|
|
236
265
|
}
|
|
237
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The nonce to stamp on inline scripts, or `undefined` when there is none.
|
|
269
|
+
*
|
|
270
|
+
* Explicit option first, then `response.nonce` (what AdonisJS exposes and what
|
|
271
|
+
* blackhole seeds), then the `cspNonce` a middleware may have left in the
|
|
272
|
+
* per-request store. Never generated here: a nonce aurora invented would not
|
|
273
|
+
* appear in the policy header, so it would block the page rather than unblock
|
|
274
|
+
* it.
|
|
275
|
+
*/
|
|
276
|
+
function resolveNonce(
|
|
277
|
+
ctx: RenderHttpContext,
|
|
278
|
+
explicit?: string,
|
|
279
|
+
): string | undefined {
|
|
280
|
+
if (typeof explicit === "string" && explicit.length > 0) return explicit;
|
|
281
|
+
const fromResponse = ctx.response.nonce;
|
|
282
|
+
if (typeof fromResponse === "string" && fromResponse.length > 0)
|
|
283
|
+
return fromResponse;
|
|
284
|
+
const fromStore = ctx.store?.get("cspNonce");
|
|
285
|
+
if (typeof fromStore === "string" && fromStore.length > 0) return fromStore;
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
238
289
|
async function resolveSharedProps(
|
|
239
290
|
ctx: RenderHttpContext,
|
|
240
291
|
shared: RenderPageOptions["shared"],
|