@ressjs/vite-router 0.5.0 → 0.6.0-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -31
- 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 +23 -0
- package/dist/config/codegen.js +118 -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 +44 -0
- package/dist/config/load.d.ts +26 -0
- package/dist/config/load.js +73 -0
- package/dist/config/middleware.d.ts +18 -0
- package/dist/config/middleware.js +79 -0
- package/dist/config/rules.d.ts +48 -0
- package/dist/config/rules.js +149 -0
- package/dist/config/types.d.ts +164 -0
- package/dist/config/types.js +2 -0
- package/dist/config/validate.d.ts +19 -0
- package/dist/config/validate.js +105 -0
- package/dist/config/watch.d.ts +41 -0
- package/dist/config/watch.js +132 -0
- package/dist/fs/module-extensions.d.ts +82 -0
- package/dist/fs/module-extensions.js +196 -0
- package/dist/helpers/html-generator.d.ts +22 -5
- package/dist/helpers/html-generator.js +89 -120
- package/dist/helpers/middlewares.d.ts +7 -7
- package/dist/helpers/middlewares.js +51 -103
- package/dist/helpers/request-handler.d.ts +2 -2
- package/dist/helpers/request-handler.js +118 -25
- package/dist/index.d.ts +30 -3
- package/dist/index.js +70 -8
- 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 +66 -0
- package/dist/plugin/environments.js +72 -0
- package/dist/plugin/index.d.ts +54 -0
- package/dist/plugin/index.js +178 -0
- package/dist/plugin/virtual-entries.d.ts +26 -0
- package/dist/plugin/virtual-entries.js +80 -0
- package/dist/render.d.ts +23 -2
- package/dist/render.js +40 -53
- package/dist/router.d.ts +32 -13
- package/dist/router.js +147 -146
- package/dist/routes/dispatcher.d.ts +37 -0
- package/dist/routes/dispatcher.js +69 -0
- package/dist/routes/manifest.d.ts +17 -0
- package/dist/routes/manifest.js +56 -0
- package/dist/routes/match.d.ts +20 -0
- package/dist/routes/match.js +75 -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 +44 -0
- package/dist/routes/router.js +81 -0
- package/dist/routes/scan.d.ts +16 -0
- package/dist/routes/scan.js +82 -0
- package/dist/routes/types.d.ts +85 -0
- package/dist/routes/types.js +13 -0
- package/dist/runtime/dev-server.d.ts +62 -0
- package/dist/runtime/dev-server.js +94 -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 +42 -0
- package/dist/runtime/prod-server.js +142 -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 +50 -0
- package/dist/security/config.d.ts +130 -0
- package/dist/security/config.js +73 -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 +111 -0
- package/dist/security/headers.d.ts +62 -0
- package/dist/security/headers.js +187 -0
- package/dist/security/index.d.ts +24 -0
- package/dist/security/index.js +47 -0
- package/dist/security/serialize.d.ts +48 -0
- package/dist/security/serialize.js +146 -0
- package/dist/variants/assets.d.ts +23 -0
- package/dist/variants/assets.js +78 -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 +69 -0
- package/dist/variants/types.js +2 -0
- package/package.json +22 -10
- package/dist/helpers/vite-config.d.ts +0 -10
- package/dist/helpers/vite-config.js +0 -90
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Opciones de seguridad del framework.
|
|
4
|
+
*
|
|
5
|
+
* Se resuelven **una vez al arrancar**, no por petición: el objeto resultante
|
|
6
|
+
* está completo y congelado, y viaja por el contexto de seguridad hasta quien lo
|
|
7
|
+
* necesite. Resolver defaults por petición sería trabajo repetido y, peor,
|
|
8
|
+
* abriría la puerta a que dos partes de la misma respuesta usen valores
|
|
9
|
+
* distintos.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveSecurityOptions = resolveSecurityOptions;
|
|
13
|
+
/**
|
|
14
|
+
* Defaults.
|
|
15
|
+
*
|
|
16
|
+
* Dos ausencias son deliberadas y están explicadas donde se aplican:
|
|
17
|
+
* `frameOptions` apagada, porque ress.js sirve páginas embebidas por diseño, y
|
|
18
|
+
* `hsts` apagada, porque activarla sobre un dominio que todavía sirve HTTP por
|
|
19
|
+
* alguna ruta lo deja inaccesible.
|
|
20
|
+
*/
|
|
21
|
+
function resolveSecurityOptions(user = {}) {
|
|
22
|
+
return deepFreeze({
|
|
23
|
+
clientProps: {
|
|
24
|
+
expose: user.clientProps?.expose ?? [],
|
|
25
|
+
exposeServerSideProps: user.clientProps?.exposeServerSideProps ?? true,
|
|
26
|
+
},
|
|
27
|
+
maxStateBytes: user.maxStateBytes ?? 131072,
|
|
28
|
+
onStateOverLimit: user.onStateOverLimit ?? 'warn',
|
|
29
|
+
varyOnUserAgent: user.varyOnUserAgent ?? false,
|
|
30
|
+
serializePlatform: user.serializePlatform ?? 'auto',
|
|
31
|
+
headers: {
|
|
32
|
+
contentTypeOptions: user.headers?.contentTypeOptions ?? true,
|
|
33
|
+
referrerPolicy: user.headers?.referrerPolicy ?? 'strict-origin-when-cross-origin',
|
|
34
|
+
frameOptions: user.headers?.frameOptions ?? false,
|
|
35
|
+
hsts: user.headers?.hsts ?? false,
|
|
36
|
+
},
|
|
37
|
+
cache: {
|
|
38
|
+
html: user.cache?.html ?? 'private, no-cache, must-revalidate',
|
|
39
|
+
immutableMaxAge: user.cache?.immutableMaxAge ?? 31536000,
|
|
40
|
+
staticMaxAge: user.cache?.staticMaxAge ?? 0,
|
|
41
|
+
},
|
|
42
|
+
csp: {
|
|
43
|
+
enabled: user.csp?.enabled ?? false,
|
|
44
|
+
reportOnly: user.csp?.reportOnly ?? false,
|
|
45
|
+
directives: user.csp?.directives ?? {},
|
|
46
|
+
...(user.csp?.reportUri ? { reportUri: user.csp.reportUri } : {}),
|
|
47
|
+
},
|
|
48
|
+
head: {
|
|
49
|
+
allowScriptTags: user.head?.allowScriptTags ?? false,
|
|
50
|
+
extraTagAllowlist: user.head?.extraTagAllowlist ?? [],
|
|
51
|
+
},
|
|
52
|
+
dev: {
|
|
53
|
+
...(user.dev?.allowedHosts ? { allowedHosts: user.dev.allowedHosts } : {}),
|
|
54
|
+
allowedOrigins: user.dev?.allowedOrigins ?? false,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Congela en profundidad.
|
|
60
|
+
*
|
|
61
|
+
* Que las opciones sean inmutables importa porque el objeto se comparte entre
|
|
62
|
+
* todas las peticiones: una mutación desde el código de una página cambiaría la
|
|
63
|
+
* política de seguridad para todas las siguientes.
|
|
64
|
+
*/
|
|
65
|
+
function deepFreeze(value) {
|
|
66
|
+
if (value && typeof value === 'object') {
|
|
67
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
68
|
+
deepFreeze(value[key]);
|
|
69
|
+
}
|
|
70
|
+
Object.freeze(value);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Política de seguridad de contenido.
|
|
3
|
+
*
|
|
4
|
+
* El obstáculo es el bloque de estado inline: cualquier política que valga la
|
|
5
|
+
* pena prohíbe `'unsafe-inline'`, y sin autorización explícita ese bloque no se
|
|
6
|
+
* ejecuta y la hidratación no encuentra sus props.
|
|
7
|
+
*
|
|
8
|
+
* Se resuelve con un **nonce por petición** y no con un hash: el contenido del
|
|
9
|
+
* bloque cambia en cada petición porque cambian las props, así que un hash
|
|
10
|
+
* habría que recalcularlo igual, y cuesta un SHA-256 sobre todo el estado contra
|
|
11
|
+
* 16 bytes de aleatoriedad.
|
|
12
|
+
*/
|
|
13
|
+
export interface CspOptions {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
reportOnly: boolean;
|
|
16
|
+
directives: Record<string, string[] | false>;
|
|
17
|
+
reportUri?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Uno por petición. */
|
|
20
|
+
export declare function createNonce(): string;
|
|
21
|
+
/**
|
|
22
|
+
* La política, con el nonce ya inyectado.
|
|
23
|
+
*
|
|
24
|
+
* La aplicación puede endurecer cualquier directiva reemplazando su lista, pero
|
|
25
|
+
* el nonce vuelve a `script-src` siempre: una política que la aplicación
|
|
26
|
+
* escribió sin él dejaría la página sin hidratar, y eso se ve como un error de
|
|
27
|
+
* la aplicación y no de su configuración.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildCsp(opts: CspOptions, nonce: string, isProduction: boolean): string;
|
|
30
|
+
/** Nombre del grupo que enlaza la directiva `report-to` con `Reporting-Endpoints`. */
|
|
31
|
+
export declare const REPORT_GROUP = "ress-csp";
|
|
32
|
+
/**
|
|
33
|
+
* La cabecera que declara a dónde van los informes.
|
|
34
|
+
*
|
|
35
|
+
* `report-to` nombra un grupo, no una dirección: sin esta cabecera que lo
|
|
36
|
+
* define, la directiva no apunta a ningún lado y los informes se pierden.
|
|
37
|
+
*/
|
|
38
|
+
export declare function reportingEndpointsHeader(reportUri: string): string;
|
|
39
|
+
/** Qué cabecera lleva la política, según se aplique o sólo se reporte. */
|
|
40
|
+
export declare function cspHeaderName(opts: CspOptions): string;
|
|
41
|
+
/**
|
|
42
|
+
* Respeta la política que ya escribió la aplicación, inyectándole el nonce.
|
|
43
|
+
*
|
|
44
|
+
* Una empresa va a querer su propia política —sus dominios, sus proveedores— y
|
|
45
|
+
* escribirla con un middleware de Express es la forma natural de hacerlo. Pisarla
|
|
46
|
+
* sería decidir por ella.
|
|
47
|
+
*
|
|
48
|
+
* Lo único que el framework agrega es el nonce en `script-src`: sin él, el bloque
|
|
49
|
+
* de estado no se ejecuta y la página no hidrata, y eso se ve como un error de la
|
|
50
|
+
* aplicación en vez de una consecuencia de su configuración. Es la misma regla
|
|
51
|
+
* que rige para las directivas declaradas en la configuración.
|
|
52
|
+
*/
|
|
53
|
+
export declare function injectNonce(existing: string, nonce: string): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Política de seguridad de contenido.
|
|
4
|
+
*
|
|
5
|
+
* El obstáculo es el bloque de estado inline: cualquier política que valga la
|
|
6
|
+
* pena prohíbe `'unsafe-inline'`, y sin autorización explícita ese bloque no se
|
|
7
|
+
* ejecuta y la hidratación no encuentra sus props.
|
|
8
|
+
*
|
|
9
|
+
* Se resuelve con un **nonce por petición** y no con un hash: el contenido del
|
|
10
|
+
* bloque cambia en cada petición porque cambian las props, así que un hash
|
|
11
|
+
* habría que recalcularlo igual, y cuesta un SHA-256 sobre todo el estado contra
|
|
12
|
+
* 16 bytes de aleatoriedad.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.REPORT_GROUP = void 0;
|
|
16
|
+
exports.createNonce = createNonce;
|
|
17
|
+
exports.buildCsp = buildCsp;
|
|
18
|
+
exports.reportingEndpointsHeader = reportingEndpointsHeader;
|
|
19
|
+
exports.cspHeaderName = cspHeaderName;
|
|
20
|
+
exports.injectNonce = injectNonce;
|
|
21
|
+
const node_crypto_1 = require("node:crypto");
|
|
22
|
+
/** Uno por petición. */
|
|
23
|
+
function createNonce() {
|
|
24
|
+
return (0, node_crypto_1.randomBytes)(16).toString('base64');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* La política, con el nonce ya inyectado.
|
|
28
|
+
*
|
|
29
|
+
* La aplicación puede endurecer cualquier directiva reemplazando su lista, pero
|
|
30
|
+
* el nonce vuelve a `script-src` siempre: una política que la aplicación
|
|
31
|
+
* escribió sin él dejaría la página sin hidratar, y eso se ve como un error de
|
|
32
|
+
* la aplicación y no de su configuración.
|
|
33
|
+
*/
|
|
34
|
+
function buildCsp(opts, nonce, isProduction) {
|
|
35
|
+
const defaults = {
|
|
36
|
+
'default-src': ["'self'"],
|
|
37
|
+
'script-src': ["'self'", `'nonce-${nonce}'`],
|
|
38
|
+
// `'unsafe-inline'` es el punto débil conocido de la política por defecto:
|
|
39
|
+
// se queda mientras el pipeline pueda emitir estilos inline.
|
|
40
|
+
'style-src': ["'self'", `'nonce-${nonce}'`, "'unsafe-inline'"],
|
|
41
|
+
'img-src': ["'self'", 'data:', 'blob:'],
|
|
42
|
+
'font-src': ["'self'", 'data:'],
|
|
43
|
+
'connect-src': ["'self'"],
|
|
44
|
+
'object-src': ["'none'"],
|
|
45
|
+
// Impide que un <base> inyectado redirija todas las URLs relativas.
|
|
46
|
+
'base-uri': ["'self'"],
|
|
47
|
+
'form-action': ["'self'"],
|
|
48
|
+
};
|
|
49
|
+
if (!isProduction) {
|
|
50
|
+
// El cliente de recarga en caliente de Vite necesita evaluar código y abrir
|
|
51
|
+
// un WebSocket.
|
|
52
|
+
defaults['script-src'].push("'unsafe-eval'");
|
|
53
|
+
defaults['connect-src'].push('ws:', 'wss:');
|
|
54
|
+
}
|
|
55
|
+
const merged = { ...defaults };
|
|
56
|
+
for (const [directive, value] of Object.entries(opts.directives ?? {})) {
|
|
57
|
+
merged[directive] = value;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(merged['script-src']) && !merged['script-src'].some(isNonce)) {
|
|
60
|
+
merged['script-src'] = [...merged['script-src'], `'nonce-${nonce}'`];
|
|
61
|
+
}
|
|
62
|
+
const parts = Object.entries(merged)
|
|
63
|
+
.filter((entry) => Array.isArray(entry[1]))
|
|
64
|
+
.map(([directive, values]) => `${directive} ${values.join(' ')}`);
|
|
65
|
+
// `report-to` es lo vigente; `report-uri` quedó deprecado pero se sigue
|
|
66
|
+
// emitiendo porque hay navegadores que sólo entienden ese.
|
|
67
|
+
if (opts.reportUri) {
|
|
68
|
+
parts.push(`report-uri ${opts.reportUri}`);
|
|
69
|
+
parts.push(`report-to ${exports.REPORT_GROUP}`);
|
|
70
|
+
}
|
|
71
|
+
return parts.join('; ');
|
|
72
|
+
}
|
|
73
|
+
/** Nombre del grupo que enlaza la directiva `report-to` con `Reporting-Endpoints`. */
|
|
74
|
+
exports.REPORT_GROUP = 'ress-csp';
|
|
75
|
+
/**
|
|
76
|
+
* La cabecera que declara a dónde van los informes.
|
|
77
|
+
*
|
|
78
|
+
* `report-to` nombra un grupo, no una dirección: sin esta cabecera que lo
|
|
79
|
+
* define, la directiva no apunta a ningún lado y los informes se pierden.
|
|
80
|
+
*/
|
|
81
|
+
function reportingEndpointsHeader(reportUri) {
|
|
82
|
+
return `${exports.REPORT_GROUP}="${reportUri}"`;
|
|
83
|
+
}
|
|
84
|
+
const isNonce = (v) => v.startsWith("'nonce-");
|
|
85
|
+
/** Qué cabecera lleva la política, según se aplique o sólo se reporte. */
|
|
86
|
+
function cspHeaderName(opts) {
|
|
87
|
+
return opts.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Respeta la política que ya escribió la aplicación, inyectándole el nonce.
|
|
91
|
+
*
|
|
92
|
+
* Una empresa va a querer su propia política —sus dominios, sus proveedores— y
|
|
93
|
+
* escribirla con un middleware de Express es la forma natural de hacerlo. Pisarla
|
|
94
|
+
* sería decidir por ella.
|
|
95
|
+
*
|
|
96
|
+
* Lo único que el framework agrega es el nonce en `script-src`: sin él, el bloque
|
|
97
|
+
* de estado no se ejecuta y la página no hidrata, y eso se ve como un error de la
|
|
98
|
+
* aplicación en vez de una consecuencia de su configuración. Es la misma regla
|
|
99
|
+
* que rige para las directivas declaradas en la configuración.
|
|
100
|
+
*/
|
|
101
|
+
function injectNonce(existing, nonce) {
|
|
102
|
+
const directives = existing
|
|
103
|
+
.split(';')
|
|
104
|
+
.map((d) => d.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const index = directives.findIndex((d) => /^script-src\b/i.test(d));
|
|
107
|
+
if (index === -1) {
|
|
108
|
+
// Sin `script-src`, manda `default-src`. Se agrega una que lo herede y sume
|
|
109
|
+
// el nonce, en vez de tocar la que la aplicación escribió.
|
|
110
|
+
const fallback = directives.find((d) => /^default-src\b/i.test(d));
|
|
111
|
+
const base = fallback ? fallback.replace(/^default-src\b/i, '').trim() : "'self'";
|
|
112
|
+
return [...directives, `script-src ${base} 'nonce-${nonce}'`].join('; ');
|
|
113
|
+
}
|
|
114
|
+
if (directives[index].includes("'nonce-"))
|
|
115
|
+
return existing;
|
|
116
|
+
directives[index] = `${directives[index]} 'nonce-${nonce}'`;
|
|
117
|
+
return directives.join('; ');
|
|
118
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verificaciones del servidor de desarrollo.
|
|
3
|
+
*
|
|
4
|
+
* El servidor de desarrollo corre con acceso al proyecto entero y sin
|
|
5
|
+
* autenticación. Las tres configuraciones que se rechazan acá lo vuelven
|
|
6
|
+
* alcanzable desde afuera de la máquina, y las tres se escriben por comodidad —
|
|
7
|
+
* para probar desde el teléfono, para que ande un iframe— sin que sea evidente
|
|
8
|
+
* qué se abre.
|
|
9
|
+
*/
|
|
10
|
+
export interface DevServerConfig {
|
|
11
|
+
server?: {
|
|
12
|
+
allowedHosts?: boolean | string[];
|
|
13
|
+
cors?: boolean | {
|
|
14
|
+
origin?: unknown;
|
|
15
|
+
};
|
|
16
|
+
fs?: {
|
|
17
|
+
strict?: boolean;
|
|
18
|
+
deny?: string[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
legacy?: {
|
|
22
|
+
skipWebSocketTokenCheck?: boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Archivos que el servidor de desarrollo nunca debe entregar, además de los que
|
|
27
|
+
* Vite ya niega.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DENIED_FILES: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Corre al arrancar en desarrollo. Falla con un mensaje que dice qué hacer.
|
|
32
|
+
*
|
|
33
|
+
* Es un error de arranque y no un aviso a propósito: un aviso en la consola de
|
|
34
|
+
* un servidor de desarrollo no lo lee nadie.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertDevServerHardening(config?: DevServerConfig): void;
|
|
37
|
+
/** Lo que el plugin aporta a la configuración del servidor de desarrollo. */
|
|
38
|
+
export declare function devServerDefaults(allowedOrigins: string[] | false): {
|
|
39
|
+
cors: {
|
|
40
|
+
origin: boolean | string[];
|
|
41
|
+
};
|
|
42
|
+
fs: {
|
|
43
|
+
strict: boolean;
|
|
44
|
+
deny: string[];
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verificaciones del servidor de desarrollo.
|
|
4
|
+
*
|
|
5
|
+
* El servidor de desarrollo corre con acceso al proyecto entero y sin
|
|
6
|
+
* autenticación. Las tres configuraciones que se rechazan acá lo vuelven
|
|
7
|
+
* alcanzable desde afuera de la máquina, y las tres se escriben por comodidad —
|
|
8
|
+
* para probar desde el teléfono, para que ande un iframe— sin que sea evidente
|
|
9
|
+
* qué se abre.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DENIED_FILES = void 0;
|
|
13
|
+
exports.assertDevServerHardening = assertDevServerHardening;
|
|
14
|
+
exports.devServerDefaults = devServerDefaults;
|
|
15
|
+
/**
|
|
16
|
+
* Archivos que el servidor de desarrollo nunca debe entregar, además de los que
|
|
17
|
+
* Vite ya niega.
|
|
18
|
+
*/
|
|
19
|
+
exports.DENIED_FILES = [
|
|
20
|
+
'.env',
|
|
21
|
+
'.env.*',
|
|
22
|
+
'*.pem',
|
|
23
|
+
'*.key',
|
|
24
|
+
'*.crt',
|
|
25
|
+
'ress.config.*',
|
|
26
|
+
'**/.git/**',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Corre al arrancar en desarrollo. Falla con un mensaje que dice qué hacer.
|
|
30
|
+
*
|
|
31
|
+
* Es un error de arranque y no un aviso a propósito: un aviso en la consola de
|
|
32
|
+
* un servidor de desarrollo no lo lee nadie.
|
|
33
|
+
*/
|
|
34
|
+
function assertDevServerHardening(config = {}) {
|
|
35
|
+
const server = config.server ?? {};
|
|
36
|
+
if (server.allowedHosts === true) {
|
|
37
|
+
throw new Error('[ress] `server.allowedHosts: true` desactiva la validación de la cabecera Host, ' +
|
|
38
|
+
'lo que permite que una página cualquiera resuelva su dominio a tu máquina y ' +
|
|
39
|
+
'lea lo que sirve tu servidor de desarrollo.\n' +
|
|
40
|
+
'Declará el host que necesitás: `server: { allowedHosts: ["mi-host.local"] }`.');
|
|
41
|
+
}
|
|
42
|
+
const origin = typeof server.cors === 'object' ? server.cors?.origin : server.cors;
|
|
43
|
+
if (origin === true || origin === '*') {
|
|
44
|
+
throw new Error('[ress] `server.cors.origin` abierto permite que cualquier sitio lea las respuestas ' +
|
|
45
|
+
'de tu servidor de desarrollo, incluido el código fuente de tu proyecto.\n' +
|
|
46
|
+
'Declará los orígenes que necesitás: `server: { cors: { origin: ["https://mi-sitio.local"] } }`.');
|
|
47
|
+
}
|
|
48
|
+
if (config.legacy?.skipWebSocketTokenCheck) {
|
|
49
|
+
console.warn('[ress] `legacy.skipWebSocketTokenCheck` desactiva la validación de origen del ' +
|
|
50
|
+
'WebSocket de recarga en caliente. Con eso, una página abierta en otra pestaña ' +
|
|
51
|
+
'puede conectarse a tu servidor de desarrollo y pedirle archivos del proyecto.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Lo que el plugin aporta a la configuración del servidor de desarrollo. */
|
|
55
|
+
function devServerDefaults(allowedOrigins) {
|
|
56
|
+
return {
|
|
57
|
+
// `origin: false` no emite cabeceras de CORS: otro origen puede disparar el
|
|
58
|
+
// pedido, pero no puede leer la respuesta.
|
|
59
|
+
cors: { origin: allowedOrigins === false ? false : allowedOrigins },
|
|
60
|
+
fs: {
|
|
61
|
+
strict: true,
|
|
62
|
+
deny: exports.DENIED_FILES,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qué ve quien pide una página cuando algo falla.
|
|
3
|
+
*
|
|
4
|
+
* Un stack trace en la respuesta entrega la estructura de directorios del
|
|
5
|
+
* servidor, las versiones de las dependencias y a veces el contenido de una
|
|
6
|
+
* consulta. En desarrollo eso es exactamente lo que se quiere ver; en producción
|
|
7
|
+
* es reconocimiento gratis.
|
|
8
|
+
*
|
|
9
|
+
* El error completo **siempre** se registra del lado del servidor. Lo que cambia
|
|
10
|
+
* es lo que viaja.
|
|
11
|
+
*/
|
|
12
|
+
export interface PublicError {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
message: string;
|
|
15
|
+
/** Sólo en desarrollo. */
|
|
16
|
+
stack?: string;
|
|
17
|
+
/** Sólo en desarrollo. */
|
|
18
|
+
original?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Un error cuyo mensaje está pensado para mostrarse.
|
|
22
|
+
*
|
|
23
|
+
* Es la única forma de que un mensaje sobreviva a producción: la aplicación
|
|
24
|
+
* puede decir «el producto no existe» sin que eso abra la puerta a que se filtre
|
|
25
|
+
* un error del framework.
|
|
26
|
+
*/
|
|
27
|
+
export declare class PublicFacingError extends Error {
|
|
28
|
+
readonly statusCode: number;
|
|
29
|
+
constructor(message: string, statusCode?: number);
|
|
30
|
+
}
|
|
31
|
+
export declare function sanitizeError(err: unknown, isProduction: boolean): PublicError;
|
|
32
|
+
/** Registra el error completo, con el contexto que hace falta para encontrarlo. */
|
|
33
|
+
export declare function logServerError(err: unknown, ctx: {
|
|
34
|
+
method?: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
statusCode: number;
|
|
37
|
+
}): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Qué ve quien pide una página cuando algo falla.
|
|
4
|
+
*
|
|
5
|
+
* Un stack trace en la respuesta entrega la estructura de directorios del
|
|
6
|
+
* servidor, las versiones de las dependencias y a veces el contenido de una
|
|
7
|
+
* consulta. En desarrollo eso es exactamente lo que se quiere ver; en producción
|
|
8
|
+
* es reconocimiento gratis.
|
|
9
|
+
*
|
|
10
|
+
* El error completo **siempre** se registra del lado del servidor. Lo que cambia
|
|
11
|
+
* es lo que viaja.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.PublicFacingError = void 0;
|
|
15
|
+
exports.sanitizeError = sanitizeError;
|
|
16
|
+
exports.logServerError = logServerError;
|
|
17
|
+
/**
|
|
18
|
+
* Un error cuyo mensaje está pensado para mostrarse.
|
|
19
|
+
*
|
|
20
|
+
* Es la única forma de que un mensaje sobreviva a producción: la aplicación
|
|
21
|
+
* puede decir «el producto no existe» sin que eso abra la puerta a que se filtre
|
|
22
|
+
* un error del framework.
|
|
23
|
+
*/
|
|
24
|
+
class PublicFacingError extends Error {
|
|
25
|
+
statusCode;
|
|
26
|
+
constructor(message, statusCode = 400) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.statusCode = statusCode;
|
|
29
|
+
this.name = 'PublicFacingError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.PublicFacingError = PublicFacingError;
|
|
33
|
+
const STATUS_TEXT = {
|
|
34
|
+
400: 'Bad Request',
|
|
35
|
+
401: 'Unauthorized',
|
|
36
|
+
403: 'Forbidden',
|
|
37
|
+
404: 'Not Found',
|
|
38
|
+
405: 'Method Not Allowed',
|
|
39
|
+
409: 'Conflict',
|
|
40
|
+
410: 'Gone',
|
|
41
|
+
422: 'Unprocessable Entity',
|
|
42
|
+
429: 'Too Many Requests',
|
|
43
|
+
500: 'Internal Server Error',
|
|
44
|
+
502: 'Bad Gateway',
|
|
45
|
+
503: 'Service Unavailable',
|
|
46
|
+
504: 'Gateway Timeout',
|
|
47
|
+
};
|
|
48
|
+
function sanitizeError(err, isProduction) {
|
|
49
|
+
const statusCode = readStatusCode(err);
|
|
50
|
+
const error = err instanceof Error ? err : undefined;
|
|
51
|
+
if (!isProduction) {
|
|
52
|
+
return {
|
|
53
|
+
statusCode,
|
|
54
|
+
message: error?.message ?? String(err),
|
|
55
|
+
...(error?.stack ? { stack: error.stack } : {}),
|
|
56
|
+
original: String(err),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// La excepción deliberada: un mensaje que la aplicación marcó como público.
|
|
60
|
+
if (err instanceof PublicFacingError) {
|
|
61
|
+
return { statusCode, message: err.message };
|
|
62
|
+
}
|
|
63
|
+
return { statusCode, message: STATUS_TEXT[statusCode] ?? 'Internal Server Error' };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Un código de estado que no sea un entero entre 400 y 599 es 500.
|
|
67
|
+
*
|
|
68
|
+
* Devolver el valor tal cual dejaría que un error con `statusCode: 200` se
|
|
69
|
+
* respondiera como éxito, o que uno con una cadena rompiera la respuesta entera.
|
|
70
|
+
*/
|
|
71
|
+
function readStatusCode(err) {
|
|
72
|
+
const raw = err;
|
|
73
|
+
const candidate = raw?.statusCode ?? raw?.status;
|
|
74
|
+
return typeof candidate === 'number' &&
|
|
75
|
+
Number.isInteger(candidate) &&
|
|
76
|
+
candidate >= 400 &&
|
|
77
|
+
candidate <= 599
|
|
78
|
+
? candidate
|
|
79
|
+
: 500;
|
|
80
|
+
}
|
|
81
|
+
/** Registra el error completo, con el contexto que hace falta para encontrarlo. */
|
|
82
|
+
function logServerError(err, ctx) {
|
|
83
|
+
console.error(`[ress] ${ctx.method ?? 'GET'} ${ctx.url ?? '?'} → ${ctx.statusCode}`, err instanceof Error ? (err.stack ?? err.message) : err);
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapado de HTML.
|
|
3
|
+
*
|
|
4
|
+
* Todo dato de la aplicación que llegue al documento pasa por acá. La regla es
|
|
5
|
+
* escapar **por carácter y no por secuencia**: buscar `</script>` es frágil
|
|
6
|
+
* porque `</ScRiPt >` también cierra el elemento y la lista de variantes es
|
|
7
|
+
* larga; escapar el `<` las cubre todas.
|
|
8
|
+
*/
|
|
9
|
+
/** Para texto entre etiquetas: el contenido de `<title>`, por ejemplo. */
|
|
10
|
+
export declare function escapeHtmlText(value: unknown): string;
|
|
11
|
+
/**
|
|
12
|
+
* Para el valor de un atributo, que siempre se emite entre comillas dobles.
|
|
13
|
+
*
|
|
14
|
+
* Se escapan las dos comillas aunque sólo una pueda cerrar el atributo: cuesta
|
|
15
|
+
* lo mismo y hace que la función siga siendo correcta si alguien la usa en otro
|
|
16
|
+
* contexto.
|
|
17
|
+
*/
|
|
18
|
+
export declare function escapeAttrValue(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Valida un nombre de atributo. Devuelve `null` si no se puede emitir.
|
|
21
|
+
*
|
|
22
|
+
* Se rechaza todo lo que empiece con `on`: son manejadores de eventos, y un
|
|
23
|
+
* valor controlado por quien envía la petición dentro de uno es ejecución de
|
|
24
|
+
* código. `srcdoc` va por lo mismo — su contenido es un documento entero.
|
|
25
|
+
*/
|
|
26
|
+
export declare function safeAttrName(name: string): string | null;
|
|
27
|
+
export interface EscapeContext {
|
|
28
|
+
/** Dónde se está emitiendo, para que el aviso diga qué elemento. */
|
|
29
|
+
where: string;
|
|
30
|
+
isProduction: boolean;
|
|
31
|
+
route?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Serializa un mapa de atributos ya escapado. Devuelve `''` si no queda ninguno.
|
|
35
|
+
*
|
|
36
|
+
* Un atributo rechazado se descarta. En desarrollo se avisa; en producción no,
|
|
37
|
+
* porque un registro por petición sobre un dato que controla quien ataca es en
|
|
38
|
+
* sí mismo una forma de inundar los logs.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderAttrs(attrs: Record<string, unknown>, ctx: EscapeContext | string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Escapado de HTML.
|
|
4
|
+
*
|
|
5
|
+
* Todo dato de la aplicación que llegue al documento pasa por acá. La regla es
|
|
6
|
+
* escapar **por carácter y no por secuencia**: buscar `</script>` es frágil
|
|
7
|
+
* porque `</ScRiPt >` también cierra el elemento y la lista de variantes es
|
|
8
|
+
* larga; escapar el `<` las cubre todas.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.escapeHtmlText = escapeHtmlText;
|
|
12
|
+
exports.escapeAttrValue = escapeAttrValue;
|
|
13
|
+
exports.safeAttrName = safeAttrName;
|
|
14
|
+
exports.renderAttrs = renderAttrs;
|
|
15
|
+
const HTML_ESCAPES = {
|
|
16
|
+
// El `&` va primero, siempre: si se reemplazara después, volvería a escapar
|
|
17
|
+
// los `&` que introducen los otros cuatro reemplazos.
|
|
18
|
+
'&': '&',
|
|
19
|
+
'<': '<',
|
|
20
|
+
'>': '>',
|
|
21
|
+
'"': '"',
|
|
22
|
+
"'": ''',
|
|
23
|
+
};
|
|
24
|
+
const HTML_ESCAPE_RE = /[&<>"']/g;
|
|
25
|
+
/** Para texto entre etiquetas: el contenido de `<title>`, por ejemplo. */
|
|
26
|
+
function escapeHtmlText(value) {
|
|
27
|
+
return String(value ?? '').replace(HTML_ESCAPE_RE, (c) => HTML_ESCAPES[c]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Para el valor de un atributo, que siempre se emite entre comillas dobles.
|
|
31
|
+
*
|
|
32
|
+
* Se escapan las dos comillas aunque sólo una pueda cerrar el atributo: cuesta
|
|
33
|
+
* lo mismo y hace que la función siga siendo correcta si alguien la usa en otro
|
|
34
|
+
* contexto.
|
|
35
|
+
*/
|
|
36
|
+
function escapeAttrValue(value) {
|
|
37
|
+
return escapeHtmlText(value);
|
|
38
|
+
}
|
|
39
|
+
/** Forma válida de un nombre de atributo en HTML. */
|
|
40
|
+
const ATTR_NAME_RE = /^[A-Za-z_:][A-Za-z0-9_.:-]*$/;
|
|
41
|
+
/** Esquemas que ejecutan código si el navegador navega a ellos. */
|
|
42
|
+
const DANGEROUS_VALUE_RE = /^\s*(?:javascript:|data:text\/html)/i;
|
|
43
|
+
/**
|
|
44
|
+
* Valida un nombre de atributo. Devuelve `null` si no se puede emitir.
|
|
45
|
+
*
|
|
46
|
+
* Se rechaza todo lo que empiece con `on`: son manejadores de eventos, y un
|
|
47
|
+
* valor controlado por quien envía la petición dentro de uno es ejecución de
|
|
48
|
+
* código. `srcdoc` va por lo mismo — su contenido es un documento entero.
|
|
49
|
+
*/
|
|
50
|
+
function safeAttrName(name) {
|
|
51
|
+
if (!ATTR_NAME_RE.test(name))
|
|
52
|
+
return null;
|
|
53
|
+
const lower = name.toLowerCase();
|
|
54
|
+
if (lower.startsWith('on'))
|
|
55
|
+
return null;
|
|
56
|
+
if (lower === 'srcdoc')
|
|
57
|
+
return null;
|
|
58
|
+
return name;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Serializa un mapa de atributos ya escapado. Devuelve `''` si no queda ninguno.
|
|
62
|
+
*
|
|
63
|
+
* Un atributo rechazado se descarta. En desarrollo se avisa; en producción no,
|
|
64
|
+
* porque un registro por petición sobre un dato que controla quien ataca es en
|
|
65
|
+
* sí mismo una forma de inundar los logs.
|
|
66
|
+
*/
|
|
67
|
+
function renderAttrs(attrs, ctx) {
|
|
68
|
+
const context = typeof ctx === 'string' ? { where: ctx, isProduction: true } : ctx;
|
|
69
|
+
const parts = [];
|
|
70
|
+
for (const [rawName, rawValue] of Object.entries(attrs ?? {})) {
|
|
71
|
+
const name = safeAttrName(rawName);
|
|
72
|
+
if (!name) {
|
|
73
|
+
discard(context, rawName, 'no es un nombre de atributo admitido');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const value = String(rawValue ?? '');
|
|
77
|
+
if (DANGEROUS_VALUE_RE.test(value)) {
|
|
78
|
+
discard(context, rawName, 'su valor usa un esquema que ejecuta código');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
parts.push(`${name}="${escapeAttrValue(value)}"`);
|
|
82
|
+
}
|
|
83
|
+
return parts.join(' ');
|
|
84
|
+
}
|
|
85
|
+
function discard(ctx, name, why) {
|
|
86
|
+
if (ctx.isProduction)
|
|
87
|
+
return;
|
|
88
|
+
const where = ctx.route ? `${ctx.where} de ${ctx.route}` : ctx.where;
|
|
89
|
+
console.warn(`[ress] atributo descartado en ${where}: "${name}" — ${why}.`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qué puede poner una página en el `<head>`.
|
|
3
|
+
*
|
|
4
|
+
* El `<head>` es el lugar más rentable para inyectar algo: un `<script>` ahí
|
|
5
|
+
* corre antes que la página, un `<meta http-equiv="refresh">` la redirige, y un
|
|
6
|
+
* `<base>` cambia a dónde apunta cada URL relativa del documento. Por eso lo que
|
|
7
|
+
* se admite es una lista cerrada y no una lista de lo prohibido.
|
|
8
|
+
*/
|
|
9
|
+
import { type EscapeContext } from './escape';
|
|
10
|
+
/** Una etiqueta estructurada. F-004 la produce; acá se filtra y se emite. */
|
|
11
|
+
export interface HeadTag {
|
|
12
|
+
tag: string;
|
|
13
|
+
attrs?: Record<string, unknown>;
|
|
14
|
+
/** Contenido textual, para `title` y `style`. */
|
|
15
|
+
children?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const HEAD_TAG_ALLOWLIST: readonly ["meta", "link", "title", "base", "style"];
|
|
18
|
+
export interface HeadTagOptions {
|
|
19
|
+
/** `script` no está en la lista por defecto. Esto lo agrega. */
|
|
20
|
+
allowScriptTags?: boolean;
|
|
21
|
+
/** Etiquetas que el proyecto suma a la lista. */
|
|
22
|
+
extraAllowlist?: readonly string[];
|
|
23
|
+
/** Autoriza los bloques inline ante la política de contenido. */
|
|
24
|
+
nonce?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Filtra y emite las etiquetas adicionales del `<head>`.
|
|
28
|
+
*
|
|
29
|
+
* Nunca se concatena un string de HTML crudo que aporte la aplicación: la
|
|
30
|
+
* entrada es siempre estructurada, y cada atributo pasa por el escapado.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderHeadTags(tags: readonly HeadTag[], ctx: EscapeContext, options?: HeadTagOptions): string;
|