@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 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 getDomainUser(request: Request, as?: string | null): string;
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' }`. `as` (ej. `?as=DOMINIO\usuario`) permite pisarla desde
18
- // afuera del header, típicamente en dev/tests.
19
- export function getDomainUser(request, as) {
20
- return (request.headers.get('x-iisnode-auth_user') ||
21
- request.headers.get('iisnode-remote_user') ||
22
- as ||
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';
@@ -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.2.2",
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.63.0",
61
- "@sveltejs/adapter-auto": "^7.0.1",
62
- "@sveltejs/kit": "^2.63.0",
63
- "@sveltejs/package": "^2.5.8",
64
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
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.13.5",
64
+ "@types/node": "^24.19.2",
67
65
  "dotenv": "^17.4.2",
68
- "eslint": "^10.10.0",
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.19.0",
70
+ "eslint-plugin-svelte": "^3.23.1",
73
71
  "eslint-plugin-unicorn": "^71.1.0",
74
- "globals": "^17.12.0",
72
+ "globals": "^17.13.0",
75
73
  "jiti": "^2.7.0",
76
- "prettier": "^3.9.7",
74
+ "prettier": "^3.9.10",
77
75
  "prettier-plugin-svelte": "^4.1.0",
78
- "publint": "^0.3.21",
79
- "svelte": "^5.57.0",
80
- "svelte-check": "^4.6.0",
81
- "tsx": "^4.23.13",
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.70.0",
84
- "vite": "^8.3.0",
85
- "vitest": "^4.1.8"
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.2"
89
+ "mssql": "^12.7.11"
90
+ },
91
+ "imports": {
92
+ "#lib": "./src/lib/index.js",
93
+ "#lib/*": "./src/lib/*"
92
94
  }
93
95
  }