@ressjs/vite-router 0.5.2 → 0.6.0-rc.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/LICENSE +69 -0
- package/README.md +9 -515
- package/dist/client/entry.d.mts +2 -0
- package/dist/client/entry.mjs +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -0
- package/dist/config/base-path.d.ts +44 -0
- package/dist/config/base-path.js +100 -0
- package/dist/config/codegen.d.ts +29 -0
- package/dist/config/codegen.js +131 -0
- package/dist/config/define.d.ts +8 -0
- package/dist/config/define.js +12 -0
- package/dist/config/env.d.ts +19 -0
- package/dist/config/env.js +40 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/config/index.js +46 -0
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +82 -0
- package/dist/config/middleware.d.ts +18 -0
- package/dist/config/middleware.js +79 -0
- package/dist/config/rules.d.ts +59 -0
- package/dist/config/rules.js +162 -0
- package/dist/config/types.d.ts +189 -0
- package/dist/config/types.js +2 -0
- package/dist/config/validate.d.ts +19 -0
- package/dist/config/validate.js +143 -0
- package/dist/config/watch.d.ts +53 -0
- package/dist/config/watch.js +163 -0
- package/dist/fs/module-extensions.d.ts +105 -0
- package/dist/fs/module-extensions.js +213 -0
- package/dist/head/resolve.d.ts +5 -0
- package/dist/head/resolve.js +166 -0
- package/dist/head/types.d.ts +44 -0
- package/dist/head/types.js +2 -0
- package/dist/helpers/html-generator.d.ts +26 -6
- package/dist/helpers/html-generator.js +96 -138
- package/dist/helpers/middlewares.d.ts +32 -21
- package/dist/helpers/middlewares.js +181 -159
- package/dist/helpers/page-config-merge.d.ts +11 -0
- package/dist/helpers/page-config-merge.js +84 -0
- package/dist/helpers/request-handler.d.ts +15 -2
- package/dist/helpers/request-handler.js +164 -27
- package/dist/index.d.ts +35 -3
- package/dist/index.js +77 -8
- package/dist/isr/capture.d.ts +79 -0
- package/dist/isr/capture.js +222 -0
- package/dist/isr/handler.d.ts +49 -0
- package/dist/isr/handler.js +207 -0
- package/dist/isr/key.d.ts +26 -0
- package/dist/isr/key.js +59 -0
- package/dist/isr/page-config.d.ts +17 -0
- package/dist/isr/page-config.js +71 -0
- package/dist/isr/preview.d.ts +5 -0
- package/dist/isr/preview.js +38 -0
- package/dist/isr/public-request.d.ts +50 -0
- package/dist/isr/public-request.js +108 -0
- package/dist/isr/response.d.ts +41 -0
- package/dist/isr/response.js +128 -0
- package/dist/isr/store.d.ts +42 -0
- package/dist/isr/store.js +108 -0
- package/dist/isr/types.d.ts +62 -0
- package/dist/isr/types.js +2 -0
- package/dist/pages.d.ts +10 -9
- package/dist/pages.js +42 -261
- package/dist/platform.d.ts +11 -14
- package/dist/platform.js +18 -102
- package/dist/plugin/environments.d.ts +68 -0
- package/dist/plugin/environments.js +74 -0
- package/dist/plugin/index.d.ts +59 -0
- package/dist/plugin/index.js +195 -0
- package/dist/plugin/route-manifest.d.ts +19 -0
- package/dist/plugin/route-manifest.js +55 -0
- package/dist/plugin/virtual-entries.d.ts +26 -0
- package/dist/plugin/virtual-entries.js +80 -0
- package/dist/render.d.ts +39 -2
- package/dist/render.js +75 -77
- package/dist/router.d.ts +32 -13
- package/dist/router.js +180 -146
- package/dist/routes/dispatcher.d.ts +58 -0
- package/dist/routes/dispatcher.js +70 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +62 -0
- package/dist/routes/match.d.ts +20 -0
- package/dist/routes/match.js +75 -0
- package/dist/routes/module.d.ts +14 -0
- package/dist/routes/module.js +30 -0
- package/dist/routes/parse.d.ts +31 -0
- package/dist/routes/parse.js +114 -0
- package/dist/routes/rank.d.ts +12 -0
- package/dist/routes/rank.js +49 -0
- package/dist/routes/router.d.ts +54 -0
- package/dist/routes/router.js +153 -0
- package/dist/routes/scan.d.ts +26 -0
- package/dist/routes/scan.js +98 -0
- package/dist/routes/types.d.ts +114 -0
- package/dist/routes/types.js +17 -0
- package/dist/runtime/dev-server.d.ts +64 -0
- package/dist/runtime/dev-server.js +94 -0
- package/dist/runtime/dev-styles.d.ts +13 -0
- package/dist/runtime/dev-styles.js +50 -0
- package/dist/runtime/module-loader.d.ts +55 -0
- package/dist/runtime/module-loader.js +122 -0
- package/dist/runtime/prod-server.d.ts +55 -0
- package/dist/runtime/prod-server.js +188 -0
- package/dist/runtime/template.d.ts +10 -0
- package/dist/runtime/template.js +33 -0
- package/dist/security/client-props.d.ts +25 -0
- package/dist/security/client-props.js +51 -0
- package/dist/security/config.d.ts +121 -0
- package/dist/security/config.js +77 -0
- package/dist/security/csp.d.ts +53 -0
- package/dist/security/csp.js +118 -0
- package/dist/security/dev-hardening.d.ts +46 -0
- package/dist/security/dev-hardening.js +65 -0
- package/dist/security/errors.d.ts +37 -0
- package/dist/security/errors.js +84 -0
- package/dist/security/escape.d.ts +40 -0
- package/dist/security/escape.js +90 -0
- package/dist/security/head-tags.d.ts +32 -0
- package/dist/security/head-tags.js +156 -0
- package/dist/security/headers.d.ts +76 -0
- package/dist/security/headers.js +278 -0
- package/dist/security/index.d.ts +24 -0
- package/dist/security/index.js +49 -0
- package/dist/security/serialize.d.ts +48 -0
- package/dist/security/serialize.js +146 -0
- package/dist/variants/assets.d.ts +33 -0
- package/dist/variants/assets.js +144 -0
- package/dist/variants/catalog.d.ts +33 -0
- package/dist/variants/catalog.js +79 -0
- package/dist/variants/platform-tokens.d.ts +9 -0
- package/dist/variants/platform-tokens.js +15 -0
- package/dist/variants/resolve.d.ts +40 -0
- package/dist/variants/resolve.js +90 -0
- package/dist/variants/suffix.d.ts +8 -0
- package/dist/variants/suffix.js +12 -0
- package/dist/variants/types.d.ts +72 -0
- package/dist/variants/types.js +2 -0
- package/package.json +23 -11
- package/dist/helpers/vite-config.d.ts +0 -10
- package/dist/helpers/vite-config.js +0 -90
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Qué puede poner una página en el `<head>`.
|
|
4
|
+
*
|
|
5
|
+
* El `<head>` es el lugar más rentable para inyectar algo: un `<script>` ahí
|
|
6
|
+
* corre antes que la página, un `<meta http-equiv="refresh">` la redirige, y un
|
|
7
|
+
* `<base>` cambia a dónde apunta cada URL relativa del documento. Por eso lo que
|
|
8
|
+
* se admite es una lista cerrada y no una lista de lo prohibido.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.HEAD_TAG_ALLOWLIST = void 0;
|
|
12
|
+
exports.renderHeadTags = renderHeadTags;
|
|
13
|
+
const escape_1 = require("./escape");
|
|
14
|
+
exports.HEAD_TAG_ALLOWLIST = ['meta', 'link', 'title', 'base', 'style', 'noscript'];
|
|
15
|
+
/** Etiquetas sin contenido, que se emiten autocerradas. */
|
|
16
|
+
const VOID_TAGS = new Set(['meta', 'link', 'base']);
|
|
17
|
+
/**
|
|
18
|
+
* Valores de `rel` admitidos en un `<link>`.
|
|
19
|
+
*
|
|
20
|
+
* Fuera de esta lista quedan cosas como `import`, que carga y ejecuta un
|
|
21
|
+
* documento entero.
|
|
22
|
+
*/
|
|
23
|
+
const LINK_REL_ALLOWLIST = new Set([
|
|
24
|
+
'stylesheet',
|
|
25
|
+
'preload',
|
|
26
|
+
'prefetch',
|
|
27
|
+
'preconnect',
|
|
28
|
+
'dns-prefetch',
|
|
29
|
+
'icon',
|
|
30
|
+
'apple-touch-icon',
|
|
31
|
+
'manifest',
|
|
32
|
+
'canonical',
|
|
33
|
+
'alternate',
|
|
34
|
+
'modulepreload',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* `http-equiv` que se descartan: uno redirige la página y el otro declara una
|
|
38
|
+
* política de contenido que debilitaría la que emite el framework por cabecera.
|
|
39
|
+
*/
|
|
40
|
+
const BLOCKED_HTTP_EQUIV = new Set(['refresh', 'content-security-policy']);
|
|
41
|
+
/**
|
|
42
|
+
* Filtra y emite las etiquetas adicionales del `<head>`.
|
|
43
|
+
*
|
|
44
|
+
* Nunca se concatena un string de HTML crudo que aporte la aplicación: la
|
|
45
|
+
* entrada es siempre estructurada, y cada atributo pasa por el escapado.
|
|
46
|
+
*/
|
|
47
|
+
function renderHeadTags(tags, ctx, options = {}) {
|
|
48
|
+
const allowed = new Set([
|
|
49
|
+
...exports.HEAD_TAG_ALLOWLIST,
|
|
50
|
+
...(options.extraAllowlist ?? []),
|
|
51
|
+
'script',
|
|
52
|
+
]);
|
|
53
|
+
const out = [];
|
|
54
|
+
let baseEmitted = false;
|
|
55
|
+
for (const rawTag of tags ?? []) {
|
|
56
|
+
// Los nombres de atributo HTML no distinguen mayúsculas: para el navegador
|
|
57
|
+
// `HTTP-EQUIV` y `http-equiv` son el mismo atributo. Si las reglas de abajo
|
|
58
|
+
// sólo miraran la forma en minúsculas tal cual la escribió la página,
|
|
59
|
+
// `{ 'HTTP-EQUIV': 'refresh' }` pasaba sin que ninguna regla lo reconociera.
|
|
60
|
+
// Por eso se normaliza acá, antes de evaluar CUALQUIER regla, y se emite ya
|
|
61
|
+
// normalizado.
|
|
62
|
+
const tag = { ...rawTag, attrs: normalizeAttrNames(rawTag?.attrs) };
|
|
63
|
+
const name = String(tag?.tag ?? '').toLowerCase();
|
|
64
|
+
if (!allowed.has(name)) {
|
|
65
|
+
discard(ctx, name, 'no está entre las etiquetas admitidas en <head>');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (name === 'script' && !options.allowScriptTags && !isJsonLdScript(tag)) {
|
|
69
|
+
discard(ctx, name, 'los scripts ejecutables no están habilitados en la configuración');
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (name === 'base') {
|
|
73
|
+
if (baseEmitted) {
|
|
74
|
+
discard(ctx, name, 'ya hay un <base>; sólo se admite uno por documento');
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
baseEmitted = true;
|
|
78
|
+
}
|
|
79
|
+
if (name === 'link' && !isAllowedLink(tag)) {
|
|
80
|
+
discard(ctx, name, `su rel no está admitido`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (name === 'meta' && isBlockedMeta(tag)) {
|
|
84
|
+
discard(ctx, name, 'su http-equiv no está admitido');
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// El contenido de un <style> se emite sin escapar —es CSS, no HTML—, así
|
|
88
|
+
// que lo único que puede sacarlo de su elemento es un cierre literal.
|
|
89
|
+
if (name === 'style' && /<\/style/i.test(tag.children ?? '')) {
|
|
90
|
+
discard(ctx, name, 'su contenido cierra el elemento');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out.push(emit(name, tag, options.nonce));
|
|
94
|
+
}
|
|
95
|
+
return out.join('\n');
|
|
96
|
+
}
|
|
97
|
+
function emit(name, tag, nonce) {
|
|
98
|
+
const attrs = { ...(tag.attrs ?? {}) };
|
|
99
|
+
if (nonce && (name === 'style' || name === 'script'))
|
|
100
|
+
attrs.nonce = nonce;
|
|
101
|
+
const rendered = (0, escape_1.renderAttrs)(attrs, { where: `<${name}>`, isProduction: true });
|
|
102
|
+
const open = rendered ? `<${name} ${rendered}` : `<${name}`;
|
|
103
|
+
if (VOID_TAGS.has(name))
|
|
104
|
+
return `${open}>`;
|
|
105
|
+
// CSS y JavaScript son elementos de texto crudo. Se neutraliza el único
|
|
106
|
+
// fragmento que puede cerrar el elemento desde su contenido.
|
|
107
|
+
const body = name === 'style' || name === 'script'
|
|
108
|
+
? String(tag.children ?? '').replace(new RegExp(`</${name}`, 'gi'), `<\\/${name}`)
|
|
109
|
+
: (0, escape_1.escapeHtmlText)(tag.children ?? '');
|
|
110
|
+
return `${open}>${body}</${name}>`;
|
|
111
|
+
}
|
|
112
|
+
function isAllowedLink(tag) {
|
|
113
|
+
const rel = String(tag.attrs?.rel ?? '').toLowerCase().trim();
|
|
114
|
+
return rel !== '' && rel.split(/\s+/).every((r) => LINK_REL_ALLOWLIST.has(r));
|
|
115
|
+
}
|
|
116
|
+
function isBlockedMeta(tag) {
|
|
117
|
+
const httpEquiv = String(tag.attrs?.['http-equiv'] ?? '').toLowerCase().trim();
|
|
118
|
+
return BLOCKED_HTTP_EQUIV.has(httpEquiv);
|
|
119
|
+
}
|
|
120
|
+
function isJsonLdScript(tag) {
|
|
121
|
+
return String(tag.attrs?.type ?? '').toLowerCase().trim() === 'application/ld+json';
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Los nombres de React que no son el atributo HTML en minúsculas. Son los
|
|
125
|
+
* mismos que traduce `htmlAttributeName` en `@ressjs/assets`: un
|
|
126
|
+
* `pageConfig.head` y un `<Head>` con la misma prop tienen que emitir y
|
|
127
|
+
* filtrarse igual.
|
|
128
|
+
*/
|
|
129
|
+
const REACT_ATTRIBUTE_ALIASES = {
|
|
130
|
+
httpEquiv: 'http-equiv',
|
|
131
|
+
className: 'class',
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Baja cada nombre de atributo a minúsculas, que es como el navegador los ve.
|
|
135
|
+
*
|
|
136
|
+
* Los alias de React van aparte: `httpEquiv` en minúsculas queda `httpequiv`,
|
|
137
|
+
* que el navegador NO reconoce como `http-equiv` (no redirige), y `className`
|
|
138
|
+
* quedaría `classname` en vez de `class`.
|
|
139
|
+
*/
|
|
140
|
+
function normalizeAttrNames(attrs) {
|
|
141
|
+
if (!attrs)
|
|
142
|
+
return {};
|
|
143
|
+
const camelCaseAliases = REACT_ATTRIBUTE_ALIASES;
|
|
144
|
+
const normalized = {};
|
|
145
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
146
|
+
const key = (camelCaseAliases[name] ?? name).toLowerCase();
|
|
147
|
+
normalized[key] = value;
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
151
|
+
function discard(ctx, name, why) {
|
|
152
|
+
if (ctx.isProduction)
|
|
153
|
+
return;
|
|
154
|
+
const where = ctx.route ? ` de ${ctx.route}` : '';
|
|
155
|
+
console.warn(`[ress] etiqueta <${name}> descartada en <head>${where}: ${why}.`);
|
|
156
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cabeceras de la respuesta.
|
|
3
|
+
*
|
|
4
|
+
* Todas se emiten **antes** de escribir el cuerpo: una vez que la respuesta
|
|
5
|
+
* empezó a escribirse ya no se pueden cambiar, y el intento tira una excepción
|
|
6
|
+
* que enmascara el error original.
|
|
7
|
+
*/
|
|
8
|
+
import type { ServerResponse } from 'node:http';
|
|
9
|
+
import { type Axis } from '@ressjs/platform';
|
|
10
|
+
import type { ResolvedSecurityOptions } from './config';
|
|
11
|
+
export interface HeaderContext {
|
|
12
|
+
isProduction: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Ejes por los que **esta** respuesta varía.
|
|
15
|
+
*
|
|
16
|
+
* Sale de las variantes que la página declara: una página sin variantes
|
|
17
|
+
* devuelve el mismo documento a cualquier cliente, y declarar que varía por
|
|
18
|
+
* dieciocho cabeceras obliga a una caché intermedia a guardar una entrada por
|
|
19
|
+
* combinación de algo que no cambia nunca.
|
|
20
|
+
*
|
|
21
|
+
* Vacío o `null` significa que no se emite `Vary` de plataforma.
|
|
22
|
+
*/
|
|
23
|
+
varyBy?: readonly Axis[] | null;
|
|
24
|
+
/** Cabeceras de la red de distribución que el proyecto declaró. */
|
|
25
|
+
edge?: readonly {
|
|
26
|
+
header: string;
|
|
27
|
+
axis: string;
|
|
28
|
+
}[];
|
|
29
|
+
nonce?: string;
|
|
30
|
+
kind: 'html' | 'error';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Agrega campos a `Vary` sin duplicar y sin reemplazar lo que ya haya.
|
|
34
|
+
*
|
|
35
|
+
* Reemplazar sería destruir el trabajo de un middleware anterior: si alguien
|
|
36
|
+
* declaró `Vary: Accept-Language` porque su respuesta depende del idioma, y el
|
|
37
|
+
* framework lo pisa, el CDN sirve la traducción equivocada.
|
|
38
|
+
*
|
|
39
|
+
* Un `Vary: *` existente no se toca: ya es más restrictivo que cualquier lista.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendVary(res: ServerResponse, fields: readonly string[]): void;
|
|
42
|
+
/**
|
|
43
|
+
* Aplica las cabeceras de seguridad de una respuesta.
|
|
44
|
+
*
|
|
45
|
+
* Es idempotente y no pisa lo que la aplicación ya haya definido, con dos
|
|
46
|
+
* excepciones: `Vary`, que amplía, y el `Content-Type` del HTML, que fija —el
|
|
47
|
+
* `charset` explícito es parte del endurecimiento, porque sin él un navegador
|
|
48
|
+
* puede inferir una codificación en la que los mismos bytes significan otra cosa.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applySecurityHeaders(res: ServerResponse, opts: ResolvedSecurityOptions, ctx: HeaderContext): void;
|
|
51
|
+
/**
|
|
52
|
+
* ¿Una caché compartida podría guardar una respuesta con este `Cache-Control`?
|
|
53
|
+
*
|
|
54
|
+
* Se decide al revés que adivinando: la respuesta queda fuera de una caché
|
|
55
|
+
* compartida sólo si lo prohíbe explícitamente, con `no-store` o con `private`
|
|
56
|
+
* sin lista de campos. `private="set-cookie"` no alcanza (la caché guarda el
|
|
57
|
+
* resto), tampoco un `private` que aparece como nombre de campo dentro de las
|
|
58
|
+
* comillas, y cualquier otra forma —`max-age=060`, directivas que no conocemos,
|
|
59
|
+
* o ninguna— cuenta como compartible. Reconocer las directivas de frescura en
|
|
60
|
+
* vez de las de privacidad dejaba pasar variantes como esa.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isShareableCacheControl(value: unknown): boolean;
|
|
63
|
+
/** El `Cache-Control` dado, o uno privado si el dado dejaría compartir la respuesta. */
|
|
64
|
+
export declare function privateCacheControl(value: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Cuánto puede cachearse un archivo estático.
|
|
67
|
+
*
|
|
68
|
+
* Un nombre con hash de contenido cambia cuando cambia el contenido, así que se
|
|
69
|
+
* puede guardar para siempre. Cualquier otro nombre puede volver a servirse con
|
|
70
|
+
* contenido distinto, y un archivo guardado como inmutable por error es
|
|
71
|
+
* irrecuperable: no hay forma de invalidarlo en el navegador de quien ya lo
|
|
72
|
+
* bajó. Por eso el default es revalidar.
|
|
73
|
+
*/
|
|
74
|
+
export declare function classifyAsset(pathname: string, buildOutputs: ReadonlySet<string>): 'immutable' | 'revalidate';
|
|
75
|
+
/** El valor de `Cache-Control` de un archivo estático según su clase. */
|
|
76
|
+
export declare function assetCacheControl(kind: 'immutable' | 'revalidate', opts: ResolvedSecurityOptions): string;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cabeceras de la respuesta.
|
|
4
|
+
*
|
|
5
|
+
* Todas se emiten **antes** de escribir el cuerpo: una vez que la respuesta
|
|
6
|
+
* empezó a escribirse ya no se pueden cambiar, y el intento tira una excepción
|
|
7
|
+
* que enmascara el error original.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.appendVary = appendVary;
|
|
11
|
+
exports.applySecurityHeaders = applySecurityHeaders;
|
|
12
|
+
exports.isShareableCacheControl = isShareableCacheControl;
|
|
13
|
+
exports.privateCacheControl = privateCacheControl;
|
|
14
|
+
exports.classifyAsset = classifyAsset;
|
|
15
|
+
exports.assetCacheControl = assetCacheControl;
|
|
16
|
+
const platform_1 = require("@ressjs/platform");
|
|
17
|
+
/**
|
|
18
|
+
* Agrega campos a `Vary` sin duplicar y sin reemplazar lo que ya haya.
|
|
19
|
+
*
|
|
20
|
+
* Reemplazar sería destruir el trabajo de un middleware anterior: si alguien
|
|
21
|
+
* declaró `Vary: Accept-Language` porque su respuesta depende del idioma, y el
|
|
22
|
+
* framework lo pisa, el CDN sirve la traducción equivocada.
|
|
23
|
+
*
|
|
24
|
+
* Un `Vary: *` existente no se toca: ya es más restrictivo que cualquier lista.
|
|
25
|
+
*/
|
|
26
|
+
function appendVary(res, fields) {
|
|
27
|
+
const current = res.getHeader('Vary');
|
|
28
|
+
const existing = Array.isArray(current) ? current.join(', ') : String(current ?? '');
|
|
29
|
+
if (existing.trim() === '*')
|
|
30
|
+
return;
|
|
31
|
+
const present = new Set(existing
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((f) => f.trim().toLowerCase())
|
|
34
|
+
.filter(Boolean));
|
|
35
|
+
const merged = existing ? [existing.trim()] : [];
|
|
36
|
+
for (const field of fields) {
|
|
37
|
+
if (present.has(field.toLowerCase()))
|
|
38
|
+
continue;
|
|
39
|
+
present.add(field.toLowerCase());
|
|
40
|
+
merged.push(field);
|
|
41
|
+
}
|
|
42
|
+
if (merged.length)
|
|
43
|
+
res.setHeader('Vary', merged.join(', '));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Aplica las cabeceras de seguridad de una respuesta.
|
|
47
|
+
*
|
|
48
|
+
* Es idempotente y no pisa lo que la aplicación ya haya definido, con dos
|
|
49
|
+
* excepciones: `Vary`, que amplía, y el `Content-Type` del HTML, que fija —el
|
|
50
|
+
* `charset` explícito es parte del endurecimiento, porque sin él un navegador
|
|
51
|
+
* puede inferir una codificación en la que los mismos bytes significan otra cosa.
|
|
52
|
+
*/
|
|
53
|
+
function applySecurityHeaders(res, opts, ctx) {
|
|
54
|
+
if (res.headersSent) {
|
|
55
|
+
if (!ctx.isProduction) {
|
|
56
|
+
console.warn('[ress] no se pudieron aplicar las cabeceras de seguridad: la respuesta ya empezó a enviarse.');
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Nunca incluye User-Agent: tiene tantos valores que una caché que separe
|
|
61
|
+
// por él casi nunca acierta. Es una decisión de política, no un límite del
|
|
62
|
+
// protocolo: la detección lo usa igual (R-10), y por eso lo que depende de
|
|
63
|
+
// la plataforma no se declara compartible (ver más abajo).
|
|
64
|
+
const fields = (0, platform_1.varyHeadersForAxes)(ctx.varyBy ?? [], {
|
|
65
|
+
// Sólo las que el proyecto declaró para su red: declarar cabeceras que nadie
|
|
66
|
+
// manda vuelve a inflar la clave de caché, que es lo que se quería evitar.
|
|
67
|
+
edge: ctx.edge,
|
|
68
|
+
});
|
|
69
|
+
if (fields.length) {
|
|
70
|
+
appendVary(res, fields);
|
|
71
|
+
requestClientHints(res);
|
|
72
|
+
}
|
|
73
|
+
if (opts.headers.contentTypeOptions)
|
|
74
|
+
setIfAbsent(res, 'X-Content-Type-Options', 'nosniff');
|
|
75
|
+
if (opts.headers.referrerPolicy) {
|
|
76
|
+
setIfAbsent(res, 'Referrer-Policy', opts.headers.referrerPolicy);
|
|
77
|
+
}
|
|
78
|
+
// No se emite por defecto: ress.js sirve páginas embebidas en apps por diseño,
|
|
79
|
+
// y una política de encuadre rompería el caso de uso principal.
|
|
80
|
+
if (opts.headers.frameOptions) {
|
|
81
|
+
setIfAbsent(res, 'X-Frame-Options', opts.headers.frameOptions);
|
|
82
|
+
}
|
|
83
|
+
// Apagada por defecto: activarla sobre un dominio que todavía sirve HTTP por
|
|
84
|
+
// alguna ruta lo deja inaccesible, y esa decisión es del proyecto.
|
|
85
|
+
if (opts.headers.hsts && ctx.isProduction) {
|
|
86
|
+
const { maxAge, includeSubDomains, preload } = opts.headers.hsts;
|
|
87
|
+
setIfAbsent(res, 'Strict-Transport-Security', [
|
|
88
|
+
`max-age=${maxAge}`,
|
|
89
|
+
includeSubDomains ? 'includeSubDomains' : '',
|
|
90
|
+
preload ? 'preload' : '',
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join('; '));
|
|
94
|
+
}
|
|
95
|
+
setIfAbsent(res, 'Content-Type', 'text/html; charset=utf-8');
|
|
96
|
+
// Un archivo servido como documento por un visor de otra tecnología puede
|
|
97
|
+
// leer políticas de dominio cruzado que nadie revisó. Cuesta una cabecera.
|
|
98
|
+
setIfAbsent(res, 'X-Permitted-Cross-Domain-Policies', 'none');
|
|
99
|
+
// `X-XSS-Protection` no se emite a propósito: el filtro que activaba está
|
|
100
|
+
// retirado de los navegadores y, donde queda, introduce vulnerabilidades
|
|
101
|
+
// propias. La política de contenido es lo que lo reemplaza.
|
|
102
|
+
// Una página de error guardada por un intermediario es un incidente: el
|
|
103
|
+
// siguiente que pida esa URL recibe el error de otro.
|
|
104
|
+
if (ctx.kind === 'error') {
|
|
105
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// `private` porque el HTML renderizado en el servidor puede llevar datos de
|
|
109
|
+
// la sesión. F-010 lo sobrescribe en las rutas que genere estáticamente.
|
|
110
|
+
setIfAbsent(res, 'Cache-Control', opts.cache.html);
|
|
111
|
+
forbidSharedCacheIfSettingCookies(res, ctx);
|
|
112
|
+
forbidSharedCacheIfPlatformDependent(res, opts, ctx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* ¿Una caché compartida podría guardar una respuesta con este `Cache-Control`?
|
|
117
|
+
*
|
|
118
|
+
* Se decide al revés que adivinando: la respuesta queda fuera de una caché
|
|
119
|
+
* compartida sólo si lo prohíbe explícitamente, con `no-store` o con `private`
|
|
120
|
+
* sin lista de campos. `private="set-cookie"` no alcanza (la caché guarda el
|
|
121
|
+
* resto), tampoco un `private` que aparece como nombre de campo dentro de las
|
|
122
|
+
* comillas, y cualquier otra forma —`max-age=060`, directivas que no conocemos,
|
|
123
|
+
* o ninguna— cuenta como compartible. Reconocer las directivas de frescura en
|
|
124
|
+
* vez de las de privacidad dejaba pasar variantes como esa.
|
|
125
|
+
*/
|
|
126
|
+
function isShareableCacheControl(value) {
|
|
127
|
+
return !cacheControlDirectives(value).some((d) => d === 'private' || d === 'no-store' || d.startsWith('no-store='));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Las directivas de un `Cache-Control`, en minúsculas.
|
|
131
|
+
*
|
|
132
|
+
* Se separa por comas respetando las comillas: en `private="set-cookie, x"`
|
|
133
|
+
* la coma es parte del argumento, no el fin de la directiva. Separar a ciegas
|
|
134
|
+
* convertía el `private` de adentro en una directiva propia.
|
|
135
|
+
*/
|
|
136
|
+
function cacheControlDirectives(value) {
|
|
137
|
+
const directives = [];
|
|
138
|
+
let current = '';
|
|
139
|
+
let quoted = false;
|
|
140
|
+
let escaped = false;
|
|
141
|
+
for (const char of String(value ?? '')) {
|
|
142
|
+
if (escaped) {
|
|
143
|
+
current += char;
|
|
144
|
+
escaped = false;
|
|
145
|
+
}
|
|
146
|
+
else if (quoted && char === '\\') {
|
|
147
|
+
current += char;
|
|
148
|
+
escaped = true;
|
|
149
|
+
}
|
|
150
|
+
else if (char === '"') {
|
|
151
|
+
quoted = !quoted;
|
|
152
|
+
current += char;
|
|
153
|
+
}
|
|
154
|
+
else if (char === ',' && !quoted) {
|
|
155
|
+
directives.push(current);
|
|
156
|
+
current = '';
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
current += char;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
directives.push(current);
|
|
163
|
+
return directives.map((d) => d.trim().toLowerCase()).filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
/** El `Cache-Control` dado, o uno privado si el dado dejaría compartir la respuesta. */
|
|
166
|
+
function privateCacheControl(value) {
|
|
167
|
+
return isShareableCacheControl(value) ? 'private, no-cache' : value;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Un HTML que depende de la plataforma nunca se declara compartible.
|
|
171
|
+
*
|
|
172
|
+
* La detección usa siempre el User-Agent, y el User-Agent no se declara en
|
|
173
|
+
* `Vary`: tiene tantos valores que una caché que separe por él casi nunca
|
|
174
|
+
* acierta. Es una decisión de política, no un límite del protocolo: sin ese
|
|
175
|
+
* campo en `Vary`, dos clientes que sólo difieren en User-Agent comparten la
|
|
176
|
+
* clave de caché aunque reciban documentos distintos. Entonces ese documento no
|
|
177
|
+
* puede ser compartible, venga el `Cache-Control` de `security.cache.html` o de
|
|
178
|
+
* un middleware.
|
|
179
|
+
*
|
|
180
|
+
* Depende de la plataforma si la página tiene variantes o si el documento lleva
|
|
181
|
+
* la plataforma serializada. Si la aplicación misma lee el User-Agent en su
|
|
182
|
+
* código y declara la respuesta pública, le toca declarar su propio `Vary`.
|
|
183
|
+
*/
|
|
184
|
+
function forbidSharedCacheIfPlatformDependent(res, opts, ctx) {
|
|
185
|
+
if (ctx.varyBy == null)
|
|
186
|
+
return;
|
|
187
|
+
const dependsOnPlatform = ctx.varyBy.length > 0 || opts.serializePlatform === 'always';
|
|
188
|
+
if (!dependsOnPlatform)
|
|
189
|
+
return;
|
|
190
|
+
const current = res.getHeader('Cache-Control');
|
|
191
|
+
if (!isShareableCacheControl(current))
|
|
192
|
+
return;
|
|
193
|
+
res.setHeader('Cache-Control', 'private, no-cache');
|
|
194
|
+
if (!ctx.isProduction) {
|
|
195
|
+
console.warn(`[ress] la respuesta depende de la plataforma y su Cache-Control (${String(current)}) ` +
|
|
196
|
+
'dejaba que una caché compartida la guardara. Se cambió a `private, no-cache`: ' +
|
|
197
|
+
'le habría entregado a un cliente la variante de otro.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Pide al navegador las pistas de plataforma que necesita la resolución.
|
|
202
|
+
*
|
|
203
|
+
* `Sec-CH-UA-Form-Factors` es la señal que dice `Desktop`, `Mobile`, `Tablet` o
|
|
204
|
+
* `TV`: exactamente lo que hoy hay que adivinar del User-Agent, pero con cuatro
|
|
205
|
+
* valores posibles en vez de cientos de miles. Es de alta entropía, así que el
|
|
206
|
+
* navegador no la manda salvo que el servidor la pida.
|
|
207
|
+
*
|
|
208
|
+
* `Critical-CH` hace que la **primera** petición se reintente ya con la pista,
|
|
209
|
+
* en lugar de servir una variante adivinada y acertar recién en la segunda.
|
|
210
|
+
* Cuesta un viaje extra la primera vez y sólo se pide en las páginas que varían.
|
|
211
|
+
*
|
|
212
|
+
* Es una mejora para navegadores basados en Chromium. Safari y Firefox no
|
|
213
|
+
* mandan estas pistas, y para ellos el User-Agent sigue siendo la única señal
|
|
214
|
+
* disponible en una navegación directa.
|
|
215
|
+
*/
|
|
216
|
+
function requestClientHints(res) {
|
|
217
|
+
const hints = 'Sec-CH-UA-Platform, Sec-CH-UA-Mobile, Sec-CH-UA-Form-Factors';
|
|
218
|
+
setIfAbsent(res, 'Accept-CH', hints);
|
|
219
|
+
setIfAbsent(res, 'Critical-CH', 'Sec-CH-UA-Form-Factors');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Una respuesta que entrega una cookie no puede guardarse en una caché compartida.
|
|
223
|
+
*
|
|
224
|
+
* Si un intermediario la guarda, el siguiente que pida esa URL recibe **la
|
|
225
|
+
* cookie de otro**: la sesión de una persona pasa a la siguiente. Es de los
|
|
226
|
+
* incidentes más caros que produce una configuración de caché, y ocurre en
|
|
227
|
+
* cuanto alguien pone la caché del HTML en `public` sin acordarse de que un
|
|
228
|
+
* middleware setea una cookie.
|
|
229
|
+
*
|
|
230
|
+
* El default `private` ya lo evita; esto protege al proyecto que lo cambió.
|
|
231
|
+
*/
|
|
232
|
+
function forbidSharedCacheIfSettingCookies(res, ctx) {
|
|
233
|
+
if (!res.getHeader('set-cookie'))
|
|
234
|
+
return;
|
|
235
|
+
const current = String(res.getHeader('Cache-Control') ?? '');
|
|
236
|
+
if (!/\bpublic\b/.test(current))
|
|
237
|
+
return;
|
|
238
|
+
res.setHeader('Cache-Control', current.replace(/\bpublic\b/, 'private'));
|
|
239
|
+
if (!ctx.isProduction) {
|
|
240
|
+
console.warn('[ress] la respuesta entrega una cookie y su Cache-Control decía `public`. ' +
|
|
241
|
+
'Se cambió a `private`: una caché compartida le habría entregado esa cookie ' +
|
|
242
|
+
'a quien pidiera la misma URL después.');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function setIfAbsent(res, name, value) {
|
|
246
|
+
if (res.getHeader(name) === undefined)
|
|
247
|
+
res.setHeader(name, value);
|
|
248
|
+
}
|
|
249
|
+
/** Nombre de archivo con un hash de contenido: `algo-A1b2C3d4.js`. */
|
|
250
|
+
const HASHED_NAME_RE = /-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/;
|
|
251
|
+
/**
|
|
252
|
+
* Cuánto puede cachearse un archivo estático.
|
|
253
|
+
*
|
|
254
|
+
* Un nombre con hash de contenido cambia cuando cambia el contenido, así que se
|
|
255
|
+
* puede guardar para siempre. Cualquier otro nombre puede volver a servirse con
|
|
256
|
+
* contenido distinto, y un archivo guardado como inmutable por error es
|
|
257
|
+
* irrecuperable: no hay forma de invalidarlo en el navegador de quien ya lo
|
|
258
|
+
* bajó. Por eso el default es revalidar.
|
|
259
|
+
*/
|
|
260
|
+
function classifyAsset(pathname, buildOutputs) {
|
|
261
|
+
const normalized = pathname.replace(/^\//, '');
|
|
262
|
+
const fileName = normalized.split('/').pop() ?? '';
|
|
263
|
+
// Un documento cacheado de forma inmutable deja la aplicación congelada.
|
|
264
|
+
if (fileName.endsWith('.html'))
|
|
265
|
+
return 'revalidate';
|
|
266
|
+
// El manifest es la fuente autoritativa: son nombres que generó el
|
|
267
|
+
// empaquetador y por construcción llevan el hash del contenido.
|
|
268
|
+
if (buildOutputs.has(normalized))
|
|
269
|
+
return 'immutable';
|
|
270
|
+
// Respaldo para los archivos que emite un plugin sin pasar por el manifest.
|
|
271
|
+
return HASHED_NAME_RE.test(fileName) ? 'immutable' : 'revalidate';
|
|
272
|
+
}
|
|
273
|
+
/** El valor de `Cache-Control` de un archivo estático según su clase. */
|
|
274
|
+
function assetCacheControl(kind, opts) {
|
|
275
|
+
return kind === 'immutable'
|
|
276
|
+
? `public, max-age=${opts.cache.immutableMaxAge}, immutable`
|
|
277
|
+
: `public, max-age=${opts.cache.staticMaxAge}, must-revalidate`;
|
|
278
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endurecimiento del renderizado en el servidor.
|
|
3
|
+
*
|
|
4
|
+
* Reúne lo que protege una respuesta: qué se serializa hacia el cliente, cómo se
|
|
5
|
+
* escapa lo que llega al documento, qué cabeceras lleva y qué se cuenta cuando
|
|
6
|
+
* algo falla.
|
|
7
|
+
*/
|
|
8
|
+
export { resolveSecurityOptions } from './config';
|
|
9
|
+
export type { ClientPropsPolicy, ResolvedSecurityOptions, SecurityContext, SecurityOptions, } from './config';
|
|
10
|
+
export { escapeAttrValue, escapeHtmlText, renderAttrs, safeAttrName } from './escape';
|
|
11
|
+
export type { EscapeContext } from './escape';
|
|
12
|
+
export { HEAD_TAG_ALLOWLIST, renderHeadTags } from './head-tags';
|
|
13
|
+
export type { HeadTag, HeadTagOptions } from './head-tags';
|
|
14
|
+
export { checkStateSize, renderStateScript, serializeState, SerializationError, } from './serialize';
|
|
15
|
+
export type { SizeCheck, StateBlock } from './serialize';
|
|
16
|
+
export { appendVary, applySecurityHeaders, isShareableCacheControl, privateCacheControl, assetCacheControl, classifyAsset, } from './headers';
|
|
17
|
+
export type { HeaderContext } from './headers';
|
|
18
|
+
export { buildCsp, cspHeaderName, createNonce, injectNonce, reportingEndpointsHeader, } from './csp';
|
|
19
|
+
export type { CspOptions } from './csp';
|
|
20
|
+
export { logServerError, PublicFacingError, sanitizeError } from './errors';
|
|
21
|
+
export type { PublicError } from './errors';
|
|
22
|
+
export { pickClientProps, withheldKeys } from './client-props';
|
|
23
|
+
export { assertDevServerHardening, DENIED_FILES, devServerDefaults } from './dev-hardening';
|
|
24
|
+
export type { DevServerConfig } from './dev-hardening';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Endurecimiento del renderizado en el servidor.
|
|
4
|
+
*
|
|
5
|
+
* Reúne lo que protege una respuesta: qué se serializa hacia el cliente, cómo se
|
|
6
|
+
* escapa lo que llega al documento, qué cabeceras lleva y qué se cuenta cuando
|
|
7
|
+
* algo falla.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.devServerDefaults = exports.DENIED_FILES = exports.assertDevServerHardening = exports.withheldKeys = exports.pickClientProps = exports.sanitizeError = exports.PublicFacingError = exports.logServerError = exports.reportingEndpointsHeader = exports.injectNonce = exports.createNonce = exports.cspHeaderName = exports.buildCsp = exports.classifyAsset = exports.assetCacheControl = exports.privateCacheControl = exports.isShareableCacheControl = exports.applySecurityHeaders = exports.appendVary = exports.SerializationError = exports.serializeState = exports.renderStateScript = exports.checkStateSize = exports.renderHeadTags = exports.HEAD_TAG_ALLOWLIST = exports.safeAttrName = exports.renderAttrs = exports.escapeHtmlText = exports.escapeAttrValue = exports.resolveSecurityOptions = void 0;
|
|
11
|
+
var config_1 = require("./config");
|
|
12
|
+
Object.defineProperty(exports, "resolveSecurityOptions", { enumerable: true, get: function () { return config_1.resolveSecurityOptions; } });
|
|
13
|
+
var escape_1 = require("./escape");
|
|
14
|
+
Object.defineProperty(exports, "escapeAttrValue", { enumerable: true, get: function () { return escape_1.escapeAttrValue; } });
|
|
15
|
+
Object.defineProperty(exports, "escapeHtmlText", { enumerable: true, get: function () { return escape_1.escapeHtmlText; } });
|
|
16
|
+
Object.defineProperty(exports, "renderAttrs", { enumerable: true, get: function () { return escape_1.renderAttrs; } });
|
|
17
|
+
Object.defineProperty(exports, "safeAttrName", { enumerable: true, get: function () { return escape_1.safeAttrName; } });
|
|
18
|
+
var head_tags_1 = require("./head-tags");
|
|
19
|
+
Object.defineProperty(exports, "HEAD_TAG_ALLOWLIST", { enumerable: true, get: function () { return head_tags_1.HEAD_TAG_ALLOWLIST; } });
|
|
20
|
+
Object.defineProperty(exports, "renderHeadTags", { enumerable: true, get: function () { return head_tags_1.renderHeadTags; } });
|
|
21
|
+
var serialize_1 = require("./serialize");
|
|
22
|
+
Object.defineProperty(exports, "checkStateSize", { enumerable: true, get: function () { return serialize_1.checkStateSize; } });
|
|
23
|
+
Object.defineProperty(exports, "renderStateScript", { enumerable: true, get: function () { return serialize_1.renderStateScript; } });
|
|
24
|
+
Object.defineProperty(exports, "serializeState", { enumerable: true, get: function () { return serialize_1.serializeState; } });
|
|
25
|
+
Object.defineProperty(exports, "SerializationError", { enumerable: true, get: function () { return serialize_1.SerializationError; } });
|
|
26
|
+
var headers_1 = require("./headers");
|
|
27
|
+
Object.defineProperty(exports, "appendVary", { enumerable: true, get: function () { return headers_1.appendVary; } });
|
|
28
|
+
Object.defineProperty(exports, "applySecurityHeaders", { enumerable: true, get: function () { return headers_1.applySecurityHeaders; } });
|
|
29
|
+
Object.defineProperty(exports, "isShareableCacheControl", { enumerable: true, get: function () { return headers_1.isShareableCacheControl; } });
|
|
30
|
+
Object.defineProperty(exports, "privateCacheControl", { enumerable: true, get: function () { return headers_1.privateCacheControl; } });
|
|
31
|
+
Object.defineProperty(exports, "assetCacheControl", { enumerable: true, get: function () { return headers_1.assetCacheControl; } });
|
|
32
|
+
Object.defineProperty(exports, "classifyAsset", { enumerable: true, get: function () { return headers_1.classifyAsset; } });
|
|
33
|
+
var csp_1 = require("./csp");
|
|
34
|
+
Object.defineProperty(exports, "buildCsp", { enumerable: true, get: function () { return csp_1.buildCsp; } });
|
|
35
|
+
Object.defineProperty(exports, "cspHeaderName", { enumerable: true, get: function () { return csp_1.cspHeaderName; } });
|
|
36
|
+
Object.defineProperty(exports, "createNonce", { enumerable: true, get: function () { return csp_1.createNonce; } });
|
|
37
|
+
Object.defineProperty(exports, "injectNonce", { enumerable: true, get: function () { return csp_1.injectNonce; } });
|
|
38
|
+
Object.defineProperty(exports, "reportingEndpointsHeader", { enumerable: true, get: function () { return csp_1.reportingEndpointsHeader; } });
|
|
39
|
+
var errors_1 = require("./errors");
|
|
40
|
+
Object.defineProperty(exports, "logServerError", { enumerable: true, get: function () { return errors_1.logServerError; } });
|
|
41
|
+
Object.defineProperty(exports, "PublicFacingError", { enumerable: true, get: function () { return errors_1.PublicFacingError; } });
|
|
42
|
+
Object.defineProperty(exports, "sanitizeError", { enumerable: true, get: function () { return errors_1.sanitizeError; } });
|
|
43
|
+
var client_props_1 = require("./client-props");
|
|
44
|
+
Object.defineProperty(exports, "pickClientProps", { enumerable: true, get: function () { return client_props_1.pickClientProps; } });
|
|
45
|
+
Object.defineProperty(exports, "withheldKeys", { enumerable: true, get: function () { return client_props_1.withheldKeys; } });
|
|
46
|
+
var dev_hardening_1 = require("./dev-hardening");
|
|
47
|
+
Object.defineProperty(exports, "assertDevServerHardening", { enumerable: true, get: function () { return dev_hardening_1.assertDevServerHardening; } });
|
|
48
|
+
Object.defineProperty(exports, "DENIED_FILES", { enumerable: true, get: function () { return dev_hardening_1.DENIED_FILES; } });
|
|
49
|
+
Object.defineProperty(exports, "devServerDefaults", { enumerable: true, get: function () { return dev_hardening_1.devServerDefaults; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialización del estado que viaja al cliente.
|
|
3
|
+
*
|
|
4
|
+
* El JSON se embebe en un `<script>` inline, así que además de ser JSON válido
|
|
5
|
+
* tiene que sobrevivir a dos gramáticas más: la del HTML, que busca `</script`,
|
|
6
|
+
* `<!--` y `<!` dentro del elemento, y la de JavaScript, donde U+2028 y U+2029
|
|
7
|
+
* son terminadores de línea que parten un literal de cadena.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SerializationError extends Error {
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
constructor(reason: string, path: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Serializa un valor para embeberlo en un `<script>` inline.
|
|
16
|
+
*
|
|
17
|
+
* El resultado es JSON válido y `JSON.parse` lo devuelve idéntico al original.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeState(value: unknown, path?: string): string;
|
|
20
|
+
export interface StateBlock {
|
|
21
|
+
/** Nombre de la variable global. Ej: `__RESS_PROPS__`. */
|
|
22
|
+
varName: string;
|
|
23
|
+
value: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* El bloque `<script>` con el estado.
|
|
27
|
+
*
|
|
28
|
+
* Recibe el nonce cuando hay política de contenido: sin él, el bloque no se
|
|
29
|
+
* ejecuta y la hidratación no encuentra sus props.
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderStateScript(blocks: StateBlock[], nonce?: string): string;
|
|
32
|
+
export interface SizeCheck {
|
|
33
|
+
bytes: number;
|
|
34
|
+
overLimit: boolean;
|
|
35
|
+
/** Las claves de primer nivel más pesadas: es lo que hace accionable el aviso. */
|
|
36
|
+
heaviest: Array<{
|
|
37
|
+
key: string;
|
|
38
|
+
bytes: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Cuánto pesa el estado que viaja.
|
|
43
|
+
*
|
|
44
|
+
* Se mide sobre el string **ya serializado y escapado**, que es lo que
|
|
45
|
+
* efectivamente se transfiere, y en bytes y no en caracteres, porque un
|
|
46
|
+
* carácter fuera de ASCII ocupa más de uno.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkStateSize(serialized: string, limitBytes: number, value?: unknown): SizeCheck;
|