@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.
Files changed (141) hide show
  1. package/LICENSE +69 -0
  2. package/README.md +9 -515
  3. package/dist/client/entry.d.mts +2 -0
  4. package/dist/client/entry.mjs +1 -0
  5. package/dist/client/index.d.ts +2 -0
  6. package/dist/client/index.js +8 -0
  7. package/dist/config/base-path.d.ts +44 -0
  8. package/dist/config/base-path.js +100 -0
  9. package/dist/config/codegen.d.ts +29 -0
  10. package/dist/config/codegen.js +131 -0
  11. package/dist/config/define.d.ts +8 -0
  12. package/dist/config/define.js +12 -0
  13. package/dist/config/env.d.ts +19 -0
  14. package/dist/config/env.js +40 -0
  15. package/dist/config/index.d.ts +18 -0
  16. package/dist/config/index.js +46 -0
  17. package/dist/config/load.d.ts +28 -0
  18. package/dist/config/load.js +82 -0
  19. package/dist/config/middleware.d.ts +18 -0
  20. package/dist/config/middleware.js +79 -0
  21. package/dist/config/rules.d.ts +59 -0
  22. package/dist/config/rules.js +162 -0
  23. package/dist/config/types.d.ts +189 -0
  24. package/dist/config/types.js +2 -0
  25. package/dist/config/validate.d.ts +19 -0
  26. package/dist/config/validate.js +143 -0
  27. package/dist/config/watch.d.ts +53 -0
  28. package/dist/config/watch.js +163 -0
  29. package/dist/fs/module-extensions.d.ts +105 -0
  30. package/dist/fs/module-extensions.js +213 -0
  31. package/dist/head/resolve.d.ts +5 -0
  32. package/dist/head/resolve.js +166 -0
  33. package/dist/head/types.d.ts +44 -0
  34. package/dist/head/types.js +2 -0
  35. package/dist/helpers/html-generator.d.ts +26 -6
  36. package/dist/helpers/html-generator.js +96 -138
  37. package/dist/helpers/middlewares.d.ts +32 -21
  38. package/dist/helpers/middlewares.js +181 -159
  39. package/dist/helpers/page-config-merge.d.ts +11 -0
  40. package/dist/helpers/page-config-merge.js +84 -0
  41. package/dist/helpers/request-handler.d.ts +15 -2
  42. package/dist/helpers/request-handler.js +164 -27
  43. package/dist/index.d.ts +35 -3
  44. package/dist/index.js +77 -8
  45. package/dist/isr/capture.d.ts +79 -0
  46. package/dist/isr/capture.js +222 -0
  47. package/dist/isr/handler.d.ts +49 -0
  48. package/dist/isr/handler.js +207 -0
  49. package/dist/isr/key.d.ts +26 -0
  50. package/dist/isr/key.js +59 -0
  51. package/dist/isr/page-config.d.ts +17 -0
  52. package/dist/isr/page-config.js +71 -0
  53. package/dist/isr/preview.d.ts +5 -0
  54. package/dist/isr/preview.js +38 -0
  55. package/dist/isr/public-request.d.ts +50 -0
  56. package/dist/isr/public-request.js +108 -0
  57. package/dist/isr/response.d.ts +41 -0
  58. package/dist/isr/response.js +128 -0
  59. package/dist/isr/store.d.ts +42 -0
  60. package/dist/isr/store.js +108 -0
  61. package/dist/isr/types.d.ts +62 -0
  62. package/dist/isr/types.js +2 -0
  63. package/dist/pages.d.ts +10 -9
  64. package/dist/pages.js +42 -261
  65. package/dist/platform.d.ts +11 -14
  66. package/dist/platform.js +18 -102
  67. package/dist/plugin/environments.d.ts +68 -0
  68. package/dist/plugin/environments.js +74 -0
  69. package/dist/plugin/index.d.ts +59 -0
  70. package/dist/plugin/index.js +195 -0
  71. package/dist/plugin/route-manifest.d.ts +19 -0
  72. package/dist/plugin/route-manifest.js +55 -0
  73. package/dist/plugin/virtual-entries.d.ts +26 -0
  74. package/dist/plugin/virtual-entries.js +80 -0
  75. package/dist/render.d.ts +39 -2
  76. package/dist/render.js +75 -77
  77. package/dist/router.d.ts +32 -13
  78. package/dist/router.js +180 -146
  79. package/dist/routes/dispatcher.d.ts +58 -0
  80. package/dist/routes/dispatcher.js +70 -0
  81. package/dist/routes/manifest.d.ts +17 -0
  82. package/dist/routes/manifest.js +62 -0
  83. package/dist/routes/match.d.ts +20 -0
  84. package/dist/routes/match.js +75 -0
  85. package/dist/routes/module.d.ts +14 -0
  86. package/dist/routes/module.js +30 -0
  87. package/dist/routes/parse.d.ts +31 -0
  88. package/dist/routes/parse.js +114 -0
  89. package/dist/routes/rank.d.ts +12 -0
  90. package/dist/routes/rank.js +49 -0
  91. package/dist/routes/router.d.ts +54 -0
  92. package/dist/routes/router.js +153 -0
  93. package/dist/routes/scan.d.ts +26 -0
  94. package/dist/routes/scan.js +98 -0
  95. package/dist/routes/types.d.ts +114 -0
  96. package/dist/routes/types.js +17 -0
  97. package/dist/runtime/dev-server.d.ts +64 -0
  98. package/dist/runtime/dev-server.js +94 -0
  99. package/dist/runtime/dev-styles.d.ts +13 -0
  100. package/dist/runtime/dev-styles.js +50 -0
  101. package/dist/runtime/module-loader.d.ts +55 -0
  102. package/dist/runtime/module-loader.js +122 -0
  103. package/dist/runtime/prod-server.d.ts +55 -0
  104. package/dist/runtime/prod-server.js +188 -0
  105. package/dist/runtime/template.d.ts +10 -0
  106. package/dist/runtime/template.js +33 -0
  107. package/dist/security/client-props.d.ts +25 -0
  108. package/dist/security/client-props.js +51 -0
  109. package/dist/security/config.d.ts +121 -0
  110. package/dist/security/config.js +77 -0
  111. package/dist/security/csp.d.ts +53 -0
  112. package/dist/security/csp.js +118 -0
  113. package/dist/security/dev-hardening.d.ts +46 -0
  114. package/dist/security/dev-hardening.js +65 -0
  115. package/dist/security/errors.d.ts +37 -0
  116. package/dist/security/errors.js +84 -0
  117. package/dist/security/escape.d.ts +40 -0
  118. package/dist/security/escape.js +90 -0
  119. package/dist/security/head-tags.d.ts +32 -0
  120. package/dist/security/head-tags.js +156 -0
  121. package/dist/security/headers.d.ts +76 -0
  122. package/dist/security/headers.js +278 -0
  123. package/dist/security/index.d.ts +24 -0
  124. package/dist/security/index.js +49 -0
  125. package/dist/security/serialize.d.ts +48 -0
  126. package/dist/security/serialize.js +146 -0
  127. package/dist/variants/assets.d.ts +33 -0
  128. package/dist/variants/assets.js +144 -0
  129. package/dist/variants/catalog.d.ts +33 -0
  130. package/dist/variants/catalog.js +79 -0
  131. package/dist/variants/platform-tokens.d.ts +9 -0
  132. package/dist/variants/platform-tokens.js +15 -0
  133. package/dist/variants/resolve.d.ts +40 -0
  134. package/dist/variants/resolve.js +90 -0
  135. package/dist/variants/suffix.d.ts +8 -0
  136. package/dist/variants/suffix.js +12 -0
  137. package/dist/variants/types.d.ts +72 -0
  138. package/dist/variants/types.js +2 -0
  139. package/package.json +23 -11
  140. package/dist/helpers/vite-config.d.ts +0 -10
  141. 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;