Se guardan en SQLite (`SQLITE_PATH`, por defecto `./data/app.sqlite`, fuera del repo). Un usuario web (`U_WEB_USER`) tiene una lista de patrones de ruta:
| Patrón | Permite |
|---|---|
| `*` | Todo (administrador) |
| `/ventas/*` | `/ventas` y todo lo que cuelga de `/ventas/` (no `/ventas-x`) |
| `/ventas/ordenes` | Solo esa ruta |
| Endpoint | Quién | Descripción |
|---|---|---|
| `GET /api/user/menu` | Cualquier sesión | `{allowedPaths}` del usuario actual |
El frontend arma los parámetros OData y el cuerpo; el backend solo valida y ejecuta (sesión SAP, cola, reintento y errores incluidos). El frontend envía el **nombre del recurso**, nunca la URL.
| Llamada | Descripción |
|---|---|
| `GET /api/sl/:recurso?$select&$filter&$orderby&$top&$skip` | Lista. Devuelve `value` y `odata.count`. `$top` por defecto 50, máx. 500. |
| `GET /api/sl/:recurso/:clave?$select` | Un registro. La clave se valida y escapa según su tipo. |
| `POST /api/sl/:recurso` | Crea; devuelve lo que responde SAP (ej. `DocEntry`, `DocNum`). |
| `PATCH /api/sl/:recurso/:clave` | Modifica (204). Acepta `If-Match` con el `odata.etag` leído (concurrencia optimista: SAP responde 412 si el documento cambió). |
| `POST /api/sl/:recurso/:clave/:accion` | Acción de un registro (hoy `Cancel` de pagos y asientos). Solo las que el recurso declara en `actions`; requiere el permiso de escritura. |
| `POST /api/sl/:accion` | Acción de solo consulta sin registro (`SeriesService_GetDocumentSeries`, series de numeración): cualquier sesión. |
- **Solo recursos registrados** en `src/sl/sl-resources.ts`; el resto es 404. Nunca registrar `EmployeesInfo` (contiene `U_WEB_PASS`).
- Cada recurso declara clave, métodos y la ruta de menú que gobierna lectura/escritura. Los catálogos de consulta (`Items`, `Warehouses`...) se leen con cualquier sesión.
- Parámetros OData fuera de la lista (`$apply`...) → 400. `$expand` solo si el recurso declara qué colecciones admite.
- Cabeceras B1S-* permitidas: `B1S-ReplaceCollectionsOnPatch` (PATCH), `B1S-CaseInsensitive` y `B1S-PageSize` (GET). Se fijan solas: `B1S-PageSize` = `$top` y `B1S-CaseInsensitive` si hay `$filter`.
- No hay DELETE. Las acciones (`Cancel`) existen solo donde el recurso las declara en `actions` (pagos recibidos y efectuados, asientos); `Close` y las demás se agregan por recurso cuando haga falta.
- Las escrituras y las acciones se registran en la auditoría (ver abajo) y dejan una línea `AUDIT` en el log.
Agregar un recurso nuevo = una línea en `SL_RESOURCES`. El test en vivo (`SAP_LIVE=1 npm run test:e2e`) comprueba que todos existan en el Service Layer del cliente.
Tabla `audit_log` en la misma SQLite. Registra inicios de sesión (correctos, fallidos con su motivo y cierres), cada creación, modificación y acción (`sl_action`) que pasa por el proxy (recurso, clave, resultado y **solo los nombres de los campos**, nunca valores ni contraseñas), las operaciones denegadas por permisos y los cambios de permisos de menú. Las lecturas no se auditan. `AuditService.record` nunca lanza: un fallo del registro no cambia la operación del usuario.
Servicio aparte del Service Layer (`APIGATEWAY_*` en el `.env`): su propio login (`/login` con base, usuario, clave e instancia), su propia sesión (se renueva sola ante un 401) y su propia cola (`APIGATEWAY_MAX_CONCURRENCY` reportes a la vez; generar uno pesa). Sin `APIGATEWAY_URL` el módulo responde 503 y el resto de la API no se ve afectado.
Llama a `rs/v1/LoadAuthorizedCRList` (los reportes autorizados), `rs/v1/LoadCR` (sus parámetros) y `rs/v1/ExportPDFData` (el PDF, en base64; responde 201 y, si falla, un texto de error con 200 o `(---)` si el reporte no produjo nada).
| Endpoint | Quién | Descripción |
|---|---|---|
| `GET /api/gateway/authorized` | Administrador | Reportes que el gateway autoriza |
| `GET /api/gateway/authorized/:code/parameters` | Administrador | Parámetros de un reporte, como definiciones de matrícula |
| `GET /api/gateway/documents?screen=/ventas/entrega` | Permiso de menú de esa pantalla | Formatos PDF asociados a un documento (el por defecto primero) |
| `POST /api/gateway/documents/export``{screen, code, docEntry}` | Permiso de menú de esa pantalla | PDF del documento: el `DocEntry` va en el parámetro asociado en la matrícula |
La matrícula (tabla `gateway_reports` de la SQLite) es lo que arma el cuerpo de `ExportPDFData`: el usuario solo manda valores y el servidor los ordena según los parámetros matriculados (nombre y tipo de Crystal), exige los obligatorios y descarta lo que no esté matriculado. La auditoría registra `report_register`, `report_unregister` y `report_export` (solo los nombres de los parámetros, nunca los valores).