@opensas/xml-rest-adapter 0.2.2 → 0.3.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 +28 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +46 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/dist/request-to-xml.js +13 -1
- package/package.json +26 -24
package/README.md
CHANGED
|
@@ -131,6 +131,34 @@ server.listen(3000);
|
|
|
131
131
|
Prueba mínima sin siquiera levantar un servidor HTTP (construyendo los `Request` a mano y
|
|
132
132
|
llamando a `handle()` directo) en [`examples/consumer/test.mjs`](examples/consumer/test.mjs).
|
|
133
133
|
|
|
134
|
+
## Autenticación con IIS (`iis-node`)
|
|
135
|
+
|
|
136
|
+
Con `authentication: { type: 'iis-node' }`, `resolveUser(request)` toma el usuario de Windows
|
|
137
|
+
(`DOMINIO\usuario`) de la cabecera `x-iisnode-auth_user`, que arma iisnode con la variable
|
|
138
|
+
`AUTH_USER` de IIS. Para que esa cabecera sea confiable, el sitio de IIS tiene que:
|
|
139
|
+
|
|
140
|
+
1. **Promover `AUTH_USER`** en el `web.config`. Sin esto iisnode no toca la cabecera: si la manda
|
|
141
|
+
el cliente, llega intacta y se toma como el usuario autenticado.
|
|
142
|
+
|
|
143
|
+
```xml
|
|
144
|
+
<iisnode promoteServerVars="AUTH_USER" />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
2. **Tener Windows Authentication habilitada.** Con Anonymous también habilitada, un pedido
|
|
148
|
+
anónimo llega sin usuario y `resolveUser` responde `UNAUTHORIZED`.
|
|
149
|
+
3. **Ser el único camino a Node:** que el proceso escuche en el named pipe de iisnode y no en un
|
|
150
|
+
puerto expuesto.
|
|
151
|
+
|
|
152
|
+
La librería, por su lado:
|
|
153
|
+
|
|
154
|
+
- Lee la cabecera solo detrás de IIS (`PORT` o `SOCKET_PATH` es un named pipe `\\.\pipe\…`).
|
|
155
|
+
Fuera de IIS la ignora y avisa con `console.warn`.
|
|
156
|
+
- Se queda con el último valor: iisnode no reemplaza una cabecera que mande el cliente, le agrega
|
|
157
|
+
el suyo con una coma (`DOMINIO\admin,DOMINIO\juan`). También avisa.
|
|
158
|
+
- No lee ninguna otra cabecera para la identidad.
|
|
159
|
+
|
|
160
|
+
En desarrollo y tests (sin IIS), el usuario se pasa como string: `adapter.resolveUser('DOMINIO\\usuario')`.
|
|
161
|
+
|
|
134
162
|
## Más
|
|
135
163
|
|
|
136
164
|
- [`docs/uso-interno.md`](docs/uso-interno.md) — gotchas y detalles que no son obvios a la
|
package/dist/auth.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { type AppError } from './errors.js';
|
|
|
2
2
|
import { type Result } from './result.js';
|
|
3
3
|
import type { Aplicacion, Permisos, Usuario } from './types.js';
|
|
4
4
|
export declare const ANONYMOUS_USER: Usuario;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function isBehindIis(env?: Record<string, string | undefined>): boolean;
|
|
6
|
+
export declare function getDomainUser(request: Request): string;
|
|
6
7
|
export declare function parseGrupos(raw: string): string[];
|
|
7
8
|
export declare function parsePermisos(raw: string): Permisos;
|
|
8
9
|
export declare function resolveUser(app: Aplicacion, identity: string | Request): Promise<Result<Usuario, AppError>>;
|
package/dist/auth.js
CHANGED
|
@@ -13,14 +13,47 @@ export const ANONYMOUS_USER = {
|
|
|
13
13
|
permisos: {},
|
|
14
14
|
domainUser: '',
|
|
15
15
|
};
|
|
16
|
+
const IISNODE_VALUE_SEPARATOR = ',';
|
|
17
|
+
const IISNODE_PIPE_PREFIX = '\\\\.\\pipe\\';
|
|
18
|
+
// iisnode le pasa a Node un named pipe (`\\.\pipe\…`) en PORT, no un número. Algunos adapters lo
|
|
19
|
+
// mueven a SOCKET_PATH (el server.cjs de svelte-adapter-iis hace `SOCKET_PATH = PORT` y borra
|
|
20
|
+
// PORT), así que se miran las dos. A ese pipe solo se conecta IIS: si la app escucha en uno, el
|
|
21
|
+
// pedido pasó por IIS y las cabeceras de iisnode las puso iisnode.
|
|
22
|
+
export function isBehindIis(env = process.env) {
|
|
23
|
+
return [env.PORT, env.SOCKET_PATH].some((valor) => valor?.startsWith(IISNODE_PIPE_PREFIX));
|
|
24
|
+
}
|
|
25
|
+
// Un usuario de Windows no tiene comas ni llega dos veces en la cabecera: si pasa, alguien la está
|
|
26
|
+
// armando a mano. Sin estrategia de logging todavía (docs/todos/logging.md), va a console.warn.
|
|
27
|
+
function warn(mensaje) {
|
|
28
|
+
console.warn(`[xml-rest-adapter] ${mensaje}`);
|
|
29
|
+
}
|
|
16
30
|
// Identidad cruda tal como la inyecta IIS-node (Windows Integrated Auth) — ver
|
|
17
|
-
// `authentication: { type: 'iis-node' }`.
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
// `authentication: { type: 'iis-node' }`. Interna: la única forma pública de obtener el usuario es
|
|
32
|
+
// `resolveUser`, que la llama cuando recibe un `Request`. Para simular un usuario en dev/tests
|
|
33
|
+
// (`?as=`), el caller le pasa a `resolveUser` el string ya resuelto.
|
|
34
|
+
//
|
|
35
|
+
// iisnode no reemplaza una cabecera que mande el cliente: le agrega su valor separado por coma
|
|
36
|
+
// (`DOMINIO\admin,DOMINIO\juan`, o `DOMINIO\admin,` si el sitio permite anónimos). El valor de
|
|
37
|
+
// iisnode es el último; lo de antes lo mandó el cliente (ver docs/todos/iisnode-cabecera-con-coma.md).
|
|
38
|
+
//
|
|
39
|
+
// Fuera de IIS (Node directo, vite dev) nadie autentica y cualquiera puede mandar la cabecera: no
|
|
40
|
+
// se lee (ver docs/todos/iisnode-cabecera-fuera-de-iis.md).
|
|
41
|
+
//
|
|
42
|
+
// Solo `x-iisnode-auth_user`: iisnode promueve las variables de `promoteServerVars` como
|
|
43
|
+
// `x-iisnode-<variable>`, y el web.config tiene que incluir AUTH_USER (ver README, "Autenticación
|
|
44
|
+
// con IIS"). Una cabecera que iisnode no arma (la `iisnode-remote_user` que se leía antes) solo
|
|
45
|
+
// puede venir del cliente.
|
|
46
|
+
export function getDomainUser(request) {
|
|
47
|
+
const header = request.headers.get('x-iisnode-auth_user') ?? '';
|
|
48
|
+
if (!isBehindIis()) {
|
|
49
|
+
if (header)
|
|
50
|
+
warn(`cabecera de iisnode sin IIS adelante, se ignora: ${JSON.stringify(header)}`);
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
if (header.includes(IISNODE_VALUE_SEPARATOR)) {
|
|
54
|
+
warn(`cabecera de iisnode con más de un valor, se usa el último: ${JSON.stringify(header)}`);
|
|
55
|
+
}
|
|
56
|
+
return header.split(IISNODE_VALUE_SEPARATOR).at(-1)?.trim() ?? '';
|
|
24
57
|
}
|
|
25
58
|
export function parseGrupos(raw) {
|
|
26
59
|
return raw
|
|
@@ -119,6 +152,12 @@ export async function resolveUser(app, identity) {
|
|
|
119
152
|
// login de Windows real de quien hizo el request.
|
|
120
153
|
if (!domainUser)
|
|
121
154
|
return err(appError('Usuario desconocido', 'UNAUTHORIZED'));
|
|
155
|
+
// Con un Request, getDomainUser ya se quedó con el valor de iisnode. Con un string, una coma es
|
|
156
|
+
// la cabecera cruda (con lo que agregó el cliente) pasada tal cual: no se adivina cuál es.
|
|
157
|
+
if (domainUser.includes(IISNODE_VALUE_SEPARATOR)) {
|
|
158
|
+
warn(`usuario con coma, se rechaza: ${JSON.stringify(domainUser)}`);
|
|
159
|
+
return err(appError('Usuario desconocido', 'UNAUTHORIZED'));
|
|
160
|
+
}
|
|
122
161
|
if (app.authorization.type === 'none')
|
|
123
162
|
return ok({ ...ANONYMOUS_USER, domainUser });
|
|
124
163
|
return readUsuario(app.authorization.dbInfo, aplicacionCodigo, domainUser);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { createXmlRestAdapter } from './create-adapter.js';
|
|
2
2
|
export { endpoints, type PartialEndpoint } from './endpoints.js';
|
|
3
|
-
export { getDomainUser } from './auth.js';
|
|
4
3
|
export { appError, jsonError, type AppError, type AppErrorCode } from './errors.js';
|
|
5
4
|
export { closeAllPools } from './db.js';
|
|
6
5
|
export type { Result } from './result.js';
|
package/dist/index.js
CHANGED
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
// };
|
|
13
13
|
export { createXmlRestAdapter } from './create-adapter.js';
|
|
14
14
|
export { endpoints } from './endpoints.js';
|
|
15
|
-
// Identidad: getDomainUser es el único helper que un consumidor necesita para armar su propia
|
|
16
|
-
// ruta de prueba/dev con impersonation vía `?as=` (ver src/routes/fixture/[...path]/+server.ts)
|
|
17
|
-
// — a propósito NO es un feature de resolveUser (sería impersonation de fábrica).
|
|
18
|
-
export { getDomainUser } from './auth.js';
|
|
19
15
|
// Errores: mismo vocabulario que usa la librería puertas adentro, para que un consumidor pueda
|
|
20
16
|
// devolver un error propio (antes de llegar a `handle`, por ejemplo) con la misma forma.
|
|
21
17
|
export { appError, jsonError } from './errors.js';
|
package/dist/request-to-xml.js
CHANGED
|
@@ -27,6 +27,16 @@ const getXmlOrden = (orden) => orden
|
|
|
27
27
|
})
|
|
28
28
|
.join(',');
|
|
29
29
|
const IDENTIFICADOR_SQL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
30
|
+
// ORDER BY posicional ("_order=1" → columna 1 del select) — solo dígitos, no hay forma de
|
|
31
|
+
// convertir esto en nada más que un entero literal.
|
|
32
|
+
const POSICION_SQL = /^[0-9]+$/;
|
|
33
|
+
// count(*)/min(Columna)/max(Columna), con alias obligatorio — no es una apertura genérica a
|
|
34
|
+
// "cualquier función SQL": la lista de funciones es fija (las que consumidores reales necesitan
|
|
35
|
+
// hoy, ver docs/casos-de-uso-campos-orden.md) y el argumento solo puede ser '*' o un identificador
|
|
36
|
+
// bare — nada de subconsultas, operadores ni paréntesis anidados. Generaliza como gramática
|
|
37
|
+
// verificable la excepción puntual que ya existía para el literal interno 'Count(*) as count'
|
|
38
|
+
// (ver comentario de execXmlConsulta en db.ts).
|
|
39
|
+
const AGREGADO_SQL = /^(count|min|max)\(\s*(\*|[A-Za-z_][A-Za-z0-9_]*)\s*\)\s+[A-Za-z_][A-Za-z0-9_]*$/i;
|
|
30
40
|
// Defensa en profundidad — espejo de dbo.esListaDeCampos (docker/fixture/scripts/02-support.sql,
|
|
31
41
|
// ver también docs/todos/sql-injection-campos-orden.md en convenios_web). La capa autoritativa es
|
|
32
42
|
// esa función en SQL Server: cubre también a los consumidores legacy (apps ASP clásico) que le
|
|
@@ -43,13 +53,15 @@ function esListaDeCampos(lista, permiteAscDesc) {
|
|
|
43
53
|
const campo = item.trim();
|
|
44
54
|
if (campo === '')
|
|
45
55
|
return false;
|
|
56
|
+
if (AGREGADO_SQL.test(campo))
|
|
57
|
+
return true;
|
|
46
58
|
if (!permiteAscDesc)
|
|
47
59
|
return IDENTIFICADOR_SQL.test(campo);
|
|
48
60
|
const partes = campo.split(/\s+/);
|
|
49
61
|
if (partes.length > 2)
|
|
50
62
|
return false;
|
|
51
63
|
const [nombre, direccion] = partes;
|
|
52
|
-
if (!IDENTIFICADOR_SQL.test(nombre))
|
|
64
|
+
if (!IDENTIFICADOR_SQL.test(nombre) && !POSICION_SQL.test(nombre))
|
|
53
65
|
return false;
|
|
54
66
|
return direccion === undefined || ['ASC', 'DESC'].includes(direccion.toUpperCase());
|
|
55
67
|
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opensas/xml-rest-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"packageManager": "pnpm@12.14.0",
|
|
4
5
|
"publishConfig": {
|
|
5
6
|
"access": "public"
|
|
6
7
|
},
|
|
7
|
-
"volta": {
|
|
8
|
-
"node": "24.19.0",
|
|
9
|
-
"pnpm": "11.21.0",
|
|
10
|
-
"npm": "12.0.2"
|
|
11
|
-
},
|
|
12
8
|
"scripts": {
|
|
13
9
|
"dev": "vite dev",
|
|
14
10
|
"build": "vite build && npm run prepack",
|
|
@@ -20,8 +16,10 @@
|
|
|
20
16
|
"check:all": "npm run format && eslint . --fix --cache && npm run check",
|
|
21
17
|
"format": "prettier --write --cache .",
|
|
22
18
|
"lint": "eslint . --cache",
|
|
19
|
+
"lint:clean": "node -e \"for (const p of ['.eslintcache', 'node_modules/.cache/prettier']) require('node:fs').rmSync(p, { recursive: true, force: true })\"",
|
|
23
20
|
"verify": "npm run format & eslint . --fix --cache & npm run check",
|
|
24
21
|
"up:safe": "pnpm --config.minimumReleaseAge=1440 up -i",
|
|
22
|
+
"sv:sync-deps": "tsx scripts/sv-sync-deps.ts",
|
|
25
23
|
"test:unit": "vitest run --project=unit-tests",
|
|
26
24
|
"test:vite": "vitest run",
|
|
27
25
|
"test:api": "playwright test --project=api-tests --pass-with-no-tests",
|
|
@@ -57,37 +55,41 @@
|
|
|
57
55
|
},
|
|
58
56
|
"devDependencies": {
|
|
59
57
|
"@eslint/js": "^10.0.1",
|
|
60
|
-
"@playwright/test": "^1.
|
|
61
|
-
"@sveltejs/adapter-auto": "^
|
|
62
|
-
"@sveltejs/kit": "^
|
|
63
|
-
"@sveltejs/package": "^
|
|
64
|
-
"@sveltejs/vite-plugin-svelte": "^7.1
|
|
58
|
+
"@playwright/test": "^1.64.1",
|
|
59
|
+
"@sveltejs/adapter-auto": "^8.0.0",
|
|
60
|
+
"@sveltejs/kit": "^3.1.0",
|
|
61
|
+
"@sveltejs/package": "^3.0.0",
|
|
62
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
65
63
|
"@types/mssql": "^12.3.0",
|
|
66
|
-
"@types/node": "^24.
|
|
64
|
+
"@types/node": "^24.19.2",
|
|
67
65
|
"dotenv": "^17.4.2",
|
|
68
|
-
"eslint": "^10.
|
|
66
|
+
"eslint": "^10.13.0",
|
|
69
67
|
"eslint-config-prettier": "^10.1.8",
|
|
70
68
|
"eslint-plugin-check-file": "^3.3.2",
|
|
71
69
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
72
|
-
"eslint-plugin-svelte": "^3.
|
|
70
|
+
"eslint-plugin-svelte": "^3.23.1",
|
|
73
71
|
"eslint-plugin-unicorn": "^71.1.0",
|
|
74
|
-
"globals": "^17.
|
|
72
|
+
"globals": "^17.13.0",
|
|
75
73
|
"jiti": "^2.7.0",
|
|
76
|
-
"prettier": "^3.9.
|
|
74
|
+
"prettier": "^3.9.10",
|
|
77
75
|
"prettier-plugin-svelte": "^4.1.0",
|
|
78
|
-
"publint": "^0.3.
|
|
79
|
-
"svelte": "^5.57.
|
|
80
|
-
"svelte-check": "^4.
|
|
81
|
-
"tsx": "^4.23.
|
|
76
|
+
"publint": "^0.3.25",
|
|
77
|
+
"svelte": "^5.57.2",
|
|
78
|
+
"svelte-check": "^4.7.7",
|
|
79
|
+
"tsx": "^4.23.15",
|
|
82
80
|
"typescript": "^6.0.3",
|
|
83
|
-
"typescript-eslint": "^8.
|
|
84
|
-
"vite": "^8.3.
|
|
85
|
-
"vitest": "^4.1.
|
|
81
|
+
"typescript-eslint": "^8.72.0",
|
|
82
|
+
"vite": "^8.3.5",
|
|
83
|
+
"vitest": "^4.1.12"
|
|
86
84
|
},
|
|
87
85
|
"keywords": [
|
|
88
86
|
"svelte"
|
|
89
87
|
],
|
|
90
88
|
"dependencies": {
|
|
91
|
-
"mssql": "^12.7.
|
|
89
|
+
"mssql": "^12.7.11"
|
|
90
|
+
},
|
|
91
|
+
"imports": {
|
|
92
|
+
"#lib": "./src/lib/index.js",
|
|
93
|
+
"#lib/*": "./src/lib/*"
|
|
92
94
|
}
|
|
93
95
|
}
|