@tinya-code/gas-admin-ui 0.2.0-alpha.0 → 0.2.0-alpha.2

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 (37) hide show
  1. package/README.md +114 -1
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs → tinya-code-gas-admin-ui-calendar-view-CquWGzw7.mjs} +4 -4
  3. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs.map → tinya-code-gas-admin-ui-calendar-view-CquWGzw7.mjs.map} +1 -1
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs → tinya-code-gas-admin-ui-condition-evaluator-DmvlAP9z.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DmvlAP9z.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs → tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs} +2 -2
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs.map → tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs → tinya-code-gas-admin-ui-error-mapper.service-dKQNLNRJ.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-dKQNLNRJ.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs → tinya-code-gas-admin-ui-file-CKh878UH.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs.map → tinya-code-gas-admin-ui-file-CKh878UH.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs → tinya-code-gas-admin-ui-form-view-_LpdZb15.mjs} +9 -9
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs.map → tinya-code-gas-admin-ui-form-view-_LpdZb15.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-BlDRZe-F.mjs → tinya-code-gas-admin-ui-image-BPfPNwru.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-BlDRZe-F.mjs.map → tinya-code-gas-admin-ui-image-BPfPNwru.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs → tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs.map → tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs → tinya-code-gas-admin-ui-list-BKr33pde.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs.map → tinya-code-gas-admin-ui-list-BKr33pde.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs → tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs.map → tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-BSqbQB28.mjs → tinya-code-gas-admin-ui-relation-WCflawIU.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-BSqbQB28.mjs.map → tinya-code-gas-admin-ui-relation-WCflawIU.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs → tinya-code-gas-admin-ui-render-registry-ghv7Mb1x.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs.map → tinya-code-gas-admin-ui-render-registry-ghv7Mb1x.mjs.map} +1 -1
  26. package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs → tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs} +117 -9
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs → tinya-code-gas-admin-ui-seed-BnwHE1Tw.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs.map → tinya-code-gas-admin-ui-seed-BnwHE1Tw.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-BUbb36-T.mjs → tinya-code-gas-admin-ui-select-BaF4kSXl.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-BUbb36-T.mjs.map → tinya-code-gas-admin-ui-select-BaF4kSXl.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs → tinya-code-gas-admin-ui-upload.service-C7GzSmuL.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs.map → tinya-code-gas-admin-ui-upload.service-C7GzSmuL.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +8 -8
  35. package/package.json +1 -1
  36. package/types/tinya-code-gas-admin-ui.d.ts +129 -41
  37. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs.map +0 -1
@@ -496,8 +496,19 @@ export interface ResourcePermissions {
496
496
  }
497
497
  /**
498
498
  * Rutas de la API relativas a `API_URL` (base.md §3, api.md §6 Opción A).
499
- * Pueden incluir el marcador `{key}`. Los singleton solo usan
500
- * `get` / `update`.
499
+ *
500
+ * Marcadores que un path acepta — se resuelven ANTES de despachar la petición
501
+ * (y para ambos drivers):
502
+ *
503
+ * - `{key}` — clave del registro actual; sólo aplica a las operaciones con
504
+ * clave (`get`, `update`, `remove`). Si llega sin resolver, es error.
505
+ * - `{<fuente>}` / `{<fuente>.<ruta.anidada>}` — valor de una fuente de
506
+ * contexto: las construidas `auth` (sesión) y `config` (config global), o
507
+ * cualquier fuente registrada en `provideGasAdmin({ valueSources })`.
508
+ *
509
+ * El valor se codifica como segmento de URL. Un marcador sin valor, o con una
510
+ * fuente no registrada, lanza un error que nombra el path y el marcador: nunca
511
+ * se envía un segmento vacío. Los singleton solo usan `get` / `update`.
501
512
  */
502
513
  export interface ResourceEndpoints {
503
514
  list?: string;
@@ -817,6 +828,58 @@ interface FirebaseAuthAdapter {
817
828
  /** Signs out from Firebase (called alongside the app session clear). */
818
829
  signOut(): Promise<void>;
819
830
  }
831
+ /**
832
+ * URL base de la API (baseapi §2.3): es la URL directa del Web App de Apps
833
+ * Script; ahí se hace el `POST` del envelope `{ token, method, path, payload }`.
834
+ * OBLIGATORIO proporcionarlo en `app.config.ts`:
835
+ * `{ provide: API_URL, useValue: 'https://…' }` (Fase 3). Vacío = mismo origen.
836
+ */
837
+ export declare const API_URL: InjectionToken<string>;
838
+ /**
839
+ * Token DI opcional para el adaptador de Firebase Auth.
840
+ * Proporcionado por la aplicación consumidora cuando usa proveedores federados o Firebase Auth.
841
+ */
842
+ export declare const FIREBASE_AUTH_ADAPTER: InjectionToken<FirebaseAuthAdapter>;
843
+ /**
844
+ * Driver de transporte HTTP del motor.
845
+ *
846
+ * Vive acá (capa de infraestructura) y NO en `ResourceSchema` (capa de dominio)
847
+ * porque es una decisión de red, no de modelo de datos. Cambiar el driver
848
+ * no debería requerir tocar ningún schema de recursos.
849
+ *
850
+ * - `'gas'` (default): tunnel `POST text/plain` con envelope
851
+ * `{ token, method, path, payload }` — contrato de Google Apps Script.
852
+ * - `'rest'`: verbos HTTP reales (`GET`, `POST`, `PUT`, `DELETE`) con
853
+ * `Authorization: Bearer <token>` en cabeceras y params en la URL (GET/DELETE)
854
+ * o en el body JSON (POST/PUT/PATCH).
855
+ *
856
+ * Se fija en `provideGasAdmin({ driver })`. Sin declaración explícita el
857
+ * motor usa `'gas'` — cero impacto para consumidores existentes.
858
+ */
859
+ export declare const API_DRIVER: InjectionToken<"gas" | "rest">;
860
+ /**
861
+ * Registro de FUENTES DE CONTEXTO para marcadores en paths de endpoints.
862
+ *
863
+ * Cada clave es el nombre de una fuente — `{ <fuente> }` o
864
+ * `{ <fuente>.<ruta.anidada> }` dentro de un path declarado en
865
+ * `ResourceEndpoints` — y cada valor es un getter sin argumentos que devuelve
866
+ * el dato ACTUAL en el momento de la petición (no al registrar).
867
+ *
868
+ * El motor ya aporta dos fuentes construidas y éstas NO se declaran acá:
869
+ *
870
+ * - `auth` → la sesión que dueña `AuthService` (usuario y token).
871
+ * - `config` → el mapa global que puebla `ConfigService` desde los singletons.
872
+ *
873
+ * Este registro es sólo para fuentes que aporta el consumidor, para que el
874
+ * motor no conozca nombres de dominio: qué campo se lee lo decide el schema,
875
+ * no el engine.
876
+ *
877
+ * Un marcador que apunte a una fuente no registrada, o a un valor ausente,
878
+ * revienta con un error que nombra el path y el marcador — jamás se manda un
879
+ * segmento vacío al backend.
880
+ */
881
+ type ValueSources = Readonly<Record<string, () => unknown>>;
882
+ export declare const VALUE_SOURCES: InjectionToken<Readonly<Record<string, () => unknown>>>;
820
883
  /** Fija el catálogo (uso interno de `provideGasAdmin`; testable directo). */
821
884
  export declare function setSchemaCatalog(schemas: readonly ResourceSchema[], auth?: AuthSchema): void;
822
885
  /** Busca un schema por `id`; `undefined` si no existe. */
@@ -833,6 +896,25 @@ interface GasAdminConfig {
833
896
  schemas: readonly ResourceSchema[];
834
897
  /** Declaración de la pantalla de login (opcional: sin ella, sin `/login`). */
835
898
  auth?: AuthSchema;
899
+ /**
900
+ * Driver de transporte HTTP.
901
+ *
902
+ * - `'gas'` (default): tunnel `POST text/plain` con envelope Apps Script.
903
+ * - `'rest'`: verbos HTTP reales con `Authorization: Bearer` en cabeceras.
904
+ *
905
+ * Sin declaración el motor usa `'gas'` — cero impacto en consumidores existentes.
906
+ */
907
+ driver?: 'gas' | 'rest';
908
+ /**
909
+ * Fuentes de contexto que el consumidor aporta para marcadores en paths de
910
+ * endpoints: `{ <fuente> }` / `{ <fuente>.<ruta> }` se resuelven en el
911
+ * momento de la petición contra este registro más las fuentes construidas
912
+ * `auth` (sesión) y `config` (config global).
913
+ *
914
+ * Opcional: sin él, sólo existen `auth` y `config`. Declararlo no cambia
915
+ * ningún path que no lleve marcadores — cero impacto en schemas existentes.
916
+ */
917
+ valueSources?: ValueSources;
836
918
  }
837
919
  /**
838
920
  * Wiring completo del motor: catálogo + HTTP con el interceptor de
@@ -916,18 +998,6 @@ export declare class ApiError extends Error {
916
998
  * dos: sin este archivo no hay ciclo de imports.
917
999
  */
918
1000
  export declare function renewedTokenOf(body: unknown): string | null;
919
- /**
920
- * URL base de la API (baseapi §2.3): es la URL directa del Web App de Apps
921
- * Script; ahí se hace el `POST` del envelope `{ token, method, path, payload }`.
922
- * OBLIGATORIO proporcionarlo en `app.config.ts`:
923
- * `{ provide: API_URL, useValue: 'https://…' }` (Fase 3). Vacío = mismo origen.
924
- */
925
- export declare const API_URL: InjectionToken<string>;
926
- /**
927
- * Token DI opcional para el adaptador de Firebase Auth.
928
- * Proporcionado por la aplicación consumidora cuando usa proveedores federados o Firebase Auth.
929
- */
930
- export declare const FIREBASE_AUTH_ADAPTER: InjectionToken<FirebaseAuthAdapter>;
931
1001
  /**
932
1002
  * Guards de sesión (auth.md §5.3).
933
1003
  *
@@ -1037,32 +1107,32 @@ export declare class NotificationService {
1037
1107
  /** Métodos que entiende el envelope del backend (baseapi §2.3). */
1038
1108
  type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1039
1109
  /**
1040
- * CRUD genérico sobre el contrato v2 (baseapi §2.3): TODA llamada a la API
1041
- * es un `POST` a `API_URL` con cuerpo `text/plain` y el JSON
1042
- * `{ token, method, path, payload }`. `method: 'GET'` también para lecturas;
1043
- * el token viaja en el cuerpo (nunca en cabeceras: criterio 11 de §17).
1044
- *
1045
- * `text/plain` en lugar de `application/json` evita el preflight CORS
1046
- * (simple request) — criterio 11: sin cabeceras custom.
1047
- *
1048
- * **R2.8 — convención sobre el snippet.** `auth.md` §5.2 llama a las claves
1049
- * `{ route, op, token, payload }`, pero el backend (`parseEnvelope_`) lee
1050
- * `path` y `method`: el snippet es un esquema a medias, no un contrato.
1051
- * Mismo precedente que en R2.5 — **la convención del código gana**. Cambiar
1052
- * el nombre ahora rompería el único transporte que existe.
1110
+ * CRUD genérico sobre el contrato v2 (baseapi §2.3) con soporte dual-driver:
1053
1111
  *
1054
- * **R2.12 — el token sale de `AuthService`, no de configuración.** El
1055
- * `ADMIN_TOKEN` (D21-C) se retiró: no hay credencial fija, hay sesión.
1056
- *
1057
- * Devuelve el JSON plano tal cual, sin transformaciones ocultas
1058
- * (base.md §1, principio 4). Los errores del backend (200 + `{ error: {…} }`)
1059
- * los convierte en `ApiError` el interceptor `apiErrorInterceptor`, que es
1060
- * también quien pasa los `AUTH_*` a `AuthService.applyAuthCode` (R2.9).
1112
+ * - **`gas` (default)**: TODA llamada a la API es un `POST` a `API_URL` con cuerpo
1113
+ * `text/plain` y el JSON `{ token, method, path, payload }`.
1114
+ * - **`rest`**: Verbos HTTP reales (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) hacia
1115
+ * `${API_URL}${path}` con `Authorization: Bearer ${token}`. `payload` va como query
1116
+ * params en GET/DELETE (aplanando objetos anidados como `params`) o como body JSON
1117
+ * en POST/PUT/PATCH.
1061
1118
  */
1062
1119
  export declare class ApiService {
1063
1120
  private readonly http;
1064
1121
  private readonly apiUrl;
1065
1122
  private readonly auth;
1123
+ private readonly driver;
1124
+ /** Fuentes que aporta el consumidor (`provideGasAdmin({ valueSources })`). */
1125
+ private readonly valueSources;
1126
+ /**
1127
+ * Acceso PEREZOSO a `ConfigService` (R2 · plan doc/context-sources-plan.md).
1128
+ *
1129
+ * `config` es fuente construida del motor, pero inyectarla en el constructor
1130
+ * formaría un ciclo: `ConfigService` inyecta `ApiService` (opcional) para
1131
+ * cargar los singletons ⇒ NG0200 en el arranque. Se pide en el momento de la
1132
+ * petición, cuando ambos servicios ya están vivos. Si no existe (TestBed
1133
+ * parcial), devuelve `null` y el resolver falla ruidoso con «sin valor».
1134
+ */
1135
+ private readonly injector;
1066
1136
  /**
1067
1137
  * Listado. El backend envuelve la respuesta cuando la operación declara
1068
1138
  * `shape.envelope: 'list'` (F5) ⇒ `{ items, nextCursor, total }` (R6.4);
@@ -1090,6 +1160,17 @@ export declare class ApiService {
1090
1160
  */
1091
1161
  request<T>(method: EnvelopeMethod, path: string | undefined, payload?: Record<string, unknown>): Observable<T>;
1092
1162
  private send;
1163
+ /**
1164
+ * Fuentes de contexto de ESTA petición: getters, no valores — se evalúan
1165
+ * sólo si el path trae marcadores.
1166
+ *
1167
+ * Las construidas del motor (`auth`, `config`) van ÚLTIMAS: el motor es
1168
+ * dueño de sus fuentes y el registro del consumidor no las pisa.
1169
+ */
1170
+ private buildSources;
1171
+ private sendGas;
1172
+ private sendRest;
1173
+ private buildUrl;
1093
1174
  private withKey;
1094
1175
  static ɵfac: i0.ɵɵFactoryDeclaration<ApiService, never>;
1095
1176
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
@@ -1141,6 +1222,7 @@ export declare class AuthService {
1141
1222
  private readonly apiUrl;
1142
1223
  private readonly router;
1143
1224
  private readonly firebaseAdapter;
1225
+ private readonly driver;
1144
1226
  /**
1145
1227
  * D10-3: **sólo `sessionStorage`.** La opción «recordarme» con
1146
1228
  * `localStorage` quedó denegada en §9.3 — muere con la pestaña ⇒ cero
@@ -1281,14 +1363,20 @@ export declare class AuthService {
1281
1363
  private clear;
1282
1364
  private goLogin;
1283
1365
  private currentUrl;
1366
+ private buildUrl;
1284
1367
  private envelope;
1285
1368
  /**
1286
- * Mismo transporte que `ApiService` (R2.8): cuerpo JSON con
1287
- * `Content-Type: text/plain` ⇒ **simple request**, sin preflight CORS.
1288
- * Los tres endpoints de sesión pasan por acá — si alguno se saltara el
1289
- * header, el navegador añadiría `application/json` y la llamada empezaría
1290
- * a pedir permiso al servidor con un OPTIONS.
1291
- */
1369
+ * Despacho dual-driver para peticiones de autenticación:
1370
+ *
1371
+ * - **`gas` (default)**: `POST` a `API_URL` con cuerpo `text/plain` y envelope
1372
+ * `{ token, method: 'POST', path, payload }`.
1373
+ * - **`rest`**: llamadas REST reales:
1374
+ * - `/auth/me`: `GET` a `${apiUrl}/auth/me` con `Authorization: Bearer ${token}`.
1375
+ * - resto (`/auth/login`, `/auth/firebase`, `/auth/logout`, etc.): `POST` con
1376
+ * `Content-Type: application/json` y payload directo como body JSON. Si existe
1377
+ * token activo, se adjunta en cabecera `Authorization`.
1378
+ */
1379
+ private buildAuthRequest;
1292
1380
  private post;
1293
1381
  static ɵfac: i0.ɵɵFactoryDeclaration<AuthService, never>;
1294
1382
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
@@ -3162,4 +3250,4 @@ export declare function actionsFor(surface: ActionSurface): ActionDescriptor[];
3162
3250
  export declare function actionType(action: ResourceAction): string;
3163
3251
  /** Tipos registrados. Sólo para comprobaciones y tests. */
3164
3252
  export declare function actionTypes(): string[];
3165
- export { type ComponentRenderKind, type FirebaseAuthAdapter, type GasAdminConfig, type RenderKind, type ResolvedShare, type ValidatorName, get as getRenderDescriptor, isSupported as isRenderSupported, register as registerRenderDescriptor, unregister as unregisterRenderDescriptor };
3253
+ export { type ComponentRenderKind, type FirebaseAuthAdapter, type GasAdminConfig, type RenderKind, type ResolvedShare, type ValidatorName, type ValueSources, get as getRenderDescriptor, isSupported as isRenderSupported, register as registerRenderDescriptor, unregister as unregisterRenderDescriptor };
@@ -1 +0,0 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/models/api.model.ts","../../../projects/gas-admin-ui/src/lib/core/tokens.ts","../../../projects/gas-admin-ui/src/lib/core/services/auth.service.ts","../../../projects/gas-admin-ui/src/lib/core/interceptors/api-error.interceptor.ts","../../../projects/gas-admin-ui/src/lib/core/services/api.service.ts","../../../projects/gas-admin-ui/src/lib/core/services/config.service.ts","../../../projects/gas-admin-ui/src/lib/core/registry/schema-catalog.ts"],"sourcesContent":["/**\n * Contrato HTTP con el backend (doc/baseapi.md §8 «Errores»).\n *\n * El backend responde SIEMPRE HTTP 200; los errores viajan en el body\n * con `{ error: { status, message, errors?: [{ path, message }] } }`.\n * El interceptor (Fase 3) los convierte en `ApiError`.\n */\n\n/**\n * Un error con ruta del422 (auth.md §6): `path` para pintarlo en el campo y\n * `code` MAYUS_SNAKE (`VALIDATION_MIN_LENGTH`) para mapearlo sin leer texto.\n */\nexport interface ApiIssue {\n /** Ruta del campo afectado (`faq[2].answer`, `sku`). */\n path: string;\n /** Mensaje en español, listo para pintar. */\n message: string;\n /** Código estable del validador (B11); el mensaje puede cambiar, el código no. */\n code?: string;\n}\n\n/** Shape del body de error (doc/baseapi.md §8 «Errores»). */\nexport interface ApiErrorBody {\n error: {\n status: number;\n message: string;\n /** Ruta del campo afectado cuando el backend la envía (base.md §10). */\n path?: string;\n /**\n * Código de sesión de §4.5 (R2.9): `AUTH_REQUIRED` · `AUTH_INVALID` ·\n * `AUTH_EXPIRED` · `FORBIDDEN` · `RATE_LIMITED`. Aparece SÓLO en esas\n * cinco respuestas — el sobre de error es aditivo (O7 / D10-5).\n */\n code?: string;\n /**\n * Errores de validación con ruta (base.md §5, guía §10): el 422 los manda\n * ACÁ (`error.errors[]`), no en `path`. Sin modelarlos el form veía\n * «Errores de validación» genérico y ningún campo se pintaba (D42).\n */\n errors?: ApiIssue[];\n };\n}\n\n/** Error de API estandarizado que lanza el interceptor de errores. */\nexport class ApiError extends Error {\n readonly status: number;\n /** Ruta del campo que provocó el error (ej. `faq[2].answer`), si existe. */\n readonly fieldPath?: string;\n /** Código de sesión de §4.5 si el backend lo envió; `undefined` en el resto. */\n readonly code?: string;\n /** Issues con ruta (422); vacío cuando el error no trae `errors[]`. */\n readonly issues: readonly ApiIssue[];\n\n constructor(\n status: number,\n message: string,\n fieldPath?: string,\n code?: string,\n issues: readonly ApiIssue[] = [],\n ) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.fieldPath = fieldPath;\n this.code = code;\n this.issues = issues;\n }\n}\n\n/**\n * `meta.renewedToken` (auth.md §3.2/§3.4): el back lo adjunta a la respuesta\n * cuando la vida del token bajó del umbral de renovación. Es el ÚNICO modo\n * de que el front se entere — el token nuevo no viaja en cabeceras ni en la\n * URL. Devuelve `null` cuando no hay renovación (que es lo normal).\n *\n * Vive acá y no en el `ApiService` porque lo consumen DOS servicios\n * (`ApiService` y `AuthService`) y éste modelo no importa a ninguno de los\n * dos: sin este archivo no hay ciclo de imports.\n */\nexport function renewedTokenOf(body: unknown): string | null {\n if (body === null || typeof body !== 'object') return null;\n const meta = (body as { meta?: unknown }).meta;\n if (meta === null || typeof meta !== 'object') return null;\n const token = (meta as { renewedToken?: unknown }).renewedToken;\n return typeof token === 'string' && token !== '' ? token : null;\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * URL base de la API (baseapi §2.3): es la URL directa del Web App de Apps\n * Script; ahí se hace el `POST` del envelope `{ token, method, path, payload }`.\n * OBLIGATORIO proporcionarlo en `app.config.ts`:\n * `{ provide: API_URL, useValue: 'https://…' }` (Fase 3). Vacío = mismo origen.\n */\nexport const API_URL = new InjectionToken<string>('API_URL');\n\nimport type { FirebaseAuthAdapter } from './adapters/firebase-auth.adapter';\n\n/**\n * Token DI opcional para el adaptador de Firebase Auth.\n * Proporcionado por la aplicación consumidora cuando usa proveedores federados o Firebase Auth.\n */\nexport const FIREBASE_AUTH_ADAPTER = new InjectionToken<FirebaseAuthAdapter>(\n 'gas.admin.firebaseAuthAdapter',\n);\n\n","import { HttpClient } from '@angular/common/http';\nimport { computed, inject, Service, signal } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { catchError, from, map, Observable, of, switchMap, tap, throwError } from 'rxjs';\n\nimport type { FieldSchema, ResourceSchema } from '../models/schema.model';\nimport { renewedTokenOf } from '../models/api.model';\nimport { API_URL, FIREBASE_AUTH_ADAPTER } from '../tokens';\nimport { getAuth } from '../registry/schema-catalog';\n\n\n/**\n * Sesión del admin (auth.md §5.2) — **única dueña** del token y del usuario.\n *\n * Qué vive acá y por qué:\n *\n * - **Señales** `user` / `token` / `isAuthenticated` / `role`: el único\n * estado que el resto de la app lee (guards, menú, interceptor).\n * - **`login()` / `logout()` / `logoutAll()` / `restore()`**: operaciones de sesión.\n * El `AuthService` NO inyecta `ApiService` — sólo `HttpClient` — porque\n * `ApiService` inyecta a éste para leer el token, y un ciclo de DI en\n * Angular es un error en tiempo de arranque, no un warning.\n * - **`applyAuthCode()` (R2.9): EL único intérprete de la tabla de §4.5.**\n * Ningún otro archivo decide qué pasa ante `AUTH_*` o `FORBIDDEN`.\n * - **Almacenamiento (R2.12): `sessionStorage`, siempre.** `rememberMe`\n * no puede usar `localStorage` porque D10-3 lo negó — ver más abajo.\n *\n * La sesión arranca desde el storage y `restore()` la revalida contra\n * `/auth/me` en el initializer: un usuario desactivado mientras la pestaña\n * estaba abierta cae al primer reload, no en mitad de una edición.\n */\n\n/** Usuario tal y como lo sirve `publicUser_` (lista blanca, R2.17). */\nexport interface SessionUser {\n id?: string;\n email?: string;\n name?: string;\n role?: string;\n active?: boolean;\n last_login_at?: string;\n created_at?: string;\n}\n\n/** Los cinco códigos de §4.5. El resto de errores no son de sesión. */\nexport type AuthErrorCode =\n 'AUTH_REQUIRED' | 'AUTH_INVALID' | 'AUTH_EXPIRED' | 'FORBIDDEN' | 'RATE_LIMITED';\n\n/** Ruta de login: fuera del shell para que ningún guard la proteja. */\nexport const LOGIN_PATH = '/login';\n\n/**\n * Claves del `sessionStorage`. Exportadas porque **la sesión se restaura\n * desde ahí en el constructor**: un test que quiera una sesión arrancando\n * tiene que sembrarlas antes de inyectar el servicio, y el que las use con\n * otro nombre quebraría el restore en silencio.\n */\nexport const SESSION_TOKEN_KEY = 'admin.session.token';\nexport const SESSION_USER_KEY = 'admin.session.user';\n\ninterface LoginResponse {\n token: string;\n user: SessionUser;\n}\ninterface MeResponse {\n user: SessionUser;\n}\n\n@Service()\nexport class AuthService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly router = inject(Router);\n private readonly firebaseAdapter = inject(FIREBASE_AUTH_ADAPTER, { optional: true });\n\n /**\n * D10-3: **sólo `sessionStorage`.** La opción «recordarme» con\n * `localStorage` quedó denegada en §9.3 — muere con la pestaña ⇒ cero\n * token legible por un XSS futuro. `authSchema.rememberMe` es `false`\n * por la misma razón: declarar `true` sería prometer un storage que\n * este servicio no usa.\n *\n * Nunca en la URL ni en el cuerpo de un GET (§5.4): el token sólo viaja\n * dentro del envelope `{ token, method, path, payload }`.\n */\n private readonly storage: Storage = sessionStorage;\n\n private readonly _user = signal<SessionUser | null>(null);\n private readonly _token = signal<string | null>(null);\n /** Ruta previa a un `AUTH_EXPIRED`, para volver tras reloguear (§4.5). */\n private readonly _returnUrl = signal<string | null>(null);\n\n readonly user = this._user.asReadonly();\n readonly token = this._token.asReadonly();\n readonly returnUrl = this._returnUrl.asReadonly();\n /** Sólo hay sesión con token Y usuario: uno sin el otro es estado corrupto. */\n readonly isAuthenticated = computed(() => this._token() !== null && this._user() !== null);\n readonly role = computed(() => this._user()?.role ?? '');\n\n constructor() {\n const token = this.storage.getItem(SESSION_TOKEN_KEY);\n if (token !== null && token !== '') this._token.set(token);\n const raw = this.storage.getItem(SESSION_USER_KEY);\n if (raw !== null && raw !== '') {\n try {\n this._user.set(JSON.parse(raw) as SessionUser);\n } catch {\n // JSON corrupto ⇒ no hay sesión que restaurar; storage sucio.\n this.storage.removeItem(SESSION_USER_KEY);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this._token.set(null);\n }\n }\n }\n\n // ── Sesión ───────────────────────────────────────────────────────────────\n\n login(email: string, password: string): Observable<SessionUser> {\n return this.post<LoginResponse>('/auth/login', { email, password }).pipe(\n map((res) => {\n this.open(res.token, res.user);\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.applyRenewedToken(renewed);\n return res.user;\n }),\n );\n }\n\n /**\n * Inicia sesión con Google mediante el adaptador de Firebase y canjea\n * el ID token contra el backend.\n */\n loginWithGoogle(): Observable<SessionUser> {\n if (!this.firebaseAdapter) {\n return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));\n }\n return from(this.firebaseAdapter.signInWithGoogle()).pipe(\n switchMap((idToken) => this.exchangeFirebaseToken(idToken)),\n );\n }\n\n /**\n * Inicia sesión con correo y contraseña en Firebase y canjea el ID token\n * resultante contra el backend.\n */\n loginWithFirebasePassword(email: string, password: string): Observable<SessionUser> {\n if (!this.firebaseAdapter) {\n return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));\n }\n return from(this.firebaseAdapter.signInWithEmailAndPassword(email, password)).pipe(\n switchMap((idToken) => this.exchangeFirebaseToken(idToken)),\n );\n }\n\n /**\n * Canjea un token de identidad de Firebase por la sesión propia de la app,\n * manteniendo idéntico el contrato del envelope y almacenamiento en sessionStorage.\n */\n private exchangeFirebaseToken(idToken: string): Observable<SessionUser> {\n const endpoint = getAuth()?.firebase?.exchangeEndpoint ?? '/auth/firebase';\n return this.post<LoginResponse>(endpoint, { idToken }).pipe(\n map((res) => {\n this.open(res.token, res.user);\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.applyRenewedToken(renewed);\n return res.user;\n }),\n );\n }\n\n /**\n * Revocación best-effort (§9.7): se pide al back que ponga el `sid` en la\n * lista de revocados, pero **la sesión local se ciere haya o no respuesta**.\n * Un logout que «falla» por red y deja al usuario dentro es peor que uno\n * que deja una sesión muerta en el servidor.\n */\n logout(): Observable<void> {\n return this.post<{ loggedOut?: boolean }>('/auth/logout').pipe(\n tap({\n next: () => {\n this.clear();\n void this.firebaseAdapter?.signOut();\n },\n error: () => {\n this.clear();\n void this.firebaseAdapter?.signOut();\n },\n }),\n map(() => undefined),\n );\n }\n\n\n /** Revoca las sesiones del usuario en todos sus dispositivos. */\n logoutAll(): Observable<{ effectiveWithinSeconds: number }> {\n return this.post<{ sessionsRevoked: boolean; effectiveWithinSeconds: number }>(\n '/auth/logout-all',\n ).pipe(\n tap(() => {\n this.clear();\n this.goLogin();\n }),\n map((res) => ({ effectiveWithinSeconds: res.effectiveWithinSeconds })),\n );\n }\n\n /** Revalida la sesión restaurada contra `/auth/me`; sin token ⇒ sin viaje. */\n restore(): Observable<void> {\n if (this._token() === null) return of(undefined);\n return this.post<MeResponse>('/auth/me').pipe(\n tap((res) => {\n this.open(this._token(), res.user);\n }),\n // El interceptor ya interpretó el `AUTH_*` (R2.9) antes de llegar\n // acá: esto sólo evita que un fallo rompa el arranque de la app.\n catchError(() => of(undefined)),\n map(() => undefined),\n );\n }\n\n /**\n * Renovación deslizante (§3.2): el back manda `meta.renewedToken` cuando\n * la vida del token bajó del umbral. Se persiste el MISMO día que el\n * token original — el `sid` no cambia, sólo se emite una firma nueva.\n */\n applyRenewedToken(token: string): void {\n if (token === '' || this._token() === null) return;\n this._token.set(token);\n this.storage.setItem(SESSION_TOKEN_KEY, token);\n }\n\n // ── R2.9 · la tabla de §4.5, en un solo sitio ───────────────────────────\n\n /**\n * ÚNICO intérprete de los códigos de sesión (auth.md §4.5 / §5.2).\n * El interceptor `apiErrorInterceptor` lo llama y nada más: cualquier\n * archivo que decida «si ve AUTH_EXPIRED hago X» está fuera de contrato.\n *\n * | Código | Qué hace |\n * | --------------- | ------------------------------------------- |\n * | `AUTH_REQUIRED` | limpia sesión y va a login |\n * | `AUTH_INVALID` | limpia sesión y va a login |\n * | `AUTH_EXPIRED` | guarda la ruta previa, limpia, va a login |\n * | `FORBIDDEN` | **no** cierra sesión (el usuario sigue dentro) |\n * | `RATE_LIMITED` | no hace nada: lo espera el formulario |\n */\n applyAuthCode(code: string | undefined): void {\n switch (code) {\n case 'AUTH_EXPIRED':\n // La ruta previa se guarda ANTES de borrar: después de limpiar no\n // queda desde dónde navegar (y `router.url` ya cambió al llegar).\n this._returnUrl.set(this.currentUrl());\n this.clear();\n this.goLogin();\n break;\n case 'AUTH_REQUIRED':\n case 'AUTH_INVALID':\n // «La sesión terminó» — no hay vuelta atrás que conservar.\n this.clear();\n this.goLogin();\n break;\n case 'FORBIDDEN':\n case 'RATE_LIMITED':\n // Decisión explícita de NO tocar la sesión: §4.5 exige que el\n // usuario vea «sin permiso» o «espera» **siguiendo dentro**.\n break;\n default:\n // No es un código de sesión: nada que interpretar.\n break;\n }\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n /** ¿El rol de la sesión cubre `required`? (mínimo: admin ≥ staff, §4.4) */\n hasRole(required: string): boolean {\n if (required === '' || required === 'session') return this.isAuthenticated();\n const role = this.role();\n if (role === 'admin') return true;\n return role !== '' && role === required;\n }\n\n /**\n * §5.3 — el ÚNICO criterio de visibilidad del front (menú, rutas y\n * botones). `requires` viene del vocabulario de §4.4: `public` no pide\n * sesión; todo lo demás sí; `roles` estrecha.\n *\n * Es **pura decisión de UI**. El back re-verifica `accessLevel` en cada\n * petición (R2.6), así que filtrar acá no protege nada: sólo evita\n * enseñarle a alguien un botón que le va a devolver 403.\n */\n canAccess(access?: { requires?: string; roles?: string[] }): boolean {\n const requires = access?.requires ?? 'session';\n if (requires === 'public') return true;\n if (!this.isAuthenticated()) return false;\n\n // §4.4 escribe el requerimiento como `role:admin`; §5.3 como\n // `roles: ['admin']`. Se aceptan las dos formas en lugar de elegir una:\n // son el mismo dato visto desde el nivel de acceso y desde el recurso.\n const role = requires.startsWith('role:') ? requires.slice('role:'.length) : '';\n const required = role === '' ? (access?.roles ?? []) : [role];\n if (required.length === 0) return true;\n return required.some((one) => this.hasRole(one));\n }\n\n // ── R7.5 · Consumo de políticas en UI (F10) ───────────────────────────────\n\n /**\n * Evalúa si el usuario puede ejecutar una operación específica según\n * `policies.access.operations` del recurso (B8 · R7.4a).\n *\n * El override por operación tiene prioridad sobre el umbral del grupo.\n * Si no hay override, se usa el umbral `policies.access.read` o\n * `policies.access.write` según la operación.\n *\n * Fallback a `permissions` (legacy) si `policies` no existe: mientras\n * los schemas locales no tengan `policies`, se mantiene compatibilidad.\n *\n * Es **pura decisión de UI**. El back vuelve a verificar en cada petición\n * (24-crud), así que esto sólo evita mostrar botones que darían 403.\n */\n canDoOperation(\n schema: ResourceSchema,\n operation: 'list' | 'get' | 'create' | 'update' | 'remove',\n ): boolean {\n // R7.5: intentar policies primero\n const ops = schema.policies?.access?.operations;\n if (ops && ops[operation]) {\n return this.meetsLevel(ops[operation]);\n }\n\n // Sin override en policies: usar el umbral del grupo\n if (schema.policies?.access) {\n const isWrite = operation === 'create' || operation === 'update' || operation === 'remove';\n const groupLevel = isWrite ? schema.policies.access.write : schema.policies.access.read;\n return this.meetsLevel(groupLevel ?? 'admin');\n }\n\n // Fallback a permissions (legacy, R7.5 transición)\n const perms = schema.permissions;\n if (perms) {\n const permMap: Record<string, boolean | undefined> = {\n list: true, // list no tiene en permissions, siempre true si no hay policies\n get: true,\n create: perms.create,\n update: perms.update,\n remove: perms.remove,\n };\n return permMap[operation] !== false;\n }\n\n // Sin policies ni permissions: UX optimista (§5.3 legacy) — el back es\n // la autoridad y responde 403 si el rol no alcanza.\n return true;\n }\n\n /**\n * Evalúa si el usuario puede leer un campo según `field.access.read`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back vuelve a filtrar la respuesta en\n * cada petición (24-crud), así que esto sólo evita mostrar datos que el\n * usuario no debería ver.\n */\n canReadField(field: FieldSchema): boolean {\n if (!field.access || !field.access.read) return true; // sin restricción ⇒ visible\n return this.meetsLevel(field.access.read);\n }\n\n /**\n * Evalúa si el usuario puede escribir un campo según `field.access.write`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back rechaza la escritura con 422 si el\n * usuario no tiene permiso, así que esto sólo pone el campo en\n * solo-lectura en el frontend.\n */\n canWriteField(field: FieldSchema): boolean {\n if (!field.access || !field.access.write) return true; // sin restricción ⇒ editable\n return this.meetsLevel(field.access.write);\n }\n\n /**\n * Verifica si el rol actual cumple un nivel de acceso dado.\n * Usa la misma jerarquía que `ACCESS_ROLE_RANK` del back (11-auth):\n * admin ≥ staff.\n */\n private meetsLevel(level: string): boolean {\n if (level === 'public') return true;\n if (level === 'session') return this.isAuthenticated();\n if (level.startsWith('role:')) {\n const requiredRole = level.slice('role:'.length);\n return this.hasRole(requiredRole);\n }\n // fallback conservador: nivel desconocido ⇒ sólo admin\n return this.hasRole('admin');\n }\n\n private open(token: string | null, user: SessionUser | null): void {\n if (token !== null && token !== '') this._token.set(token);\n this._user.set(user);\n if (token !== null && token !== '') this.storage.setItem(SESSION_TOKEN_KEY, token);\n if (user !== null) this.storage.setItem(SESSION_USER_KEY, JSON.stringify(user));\n }\n\n /**\n * Borra token **y** usuario (§5.4) — en ese orden de importancia: un token\n * sin usuario no autentica, pero un usuario sin token engaña al menú.\n */\n /**\n * Ruta previa guardada por un `AUTH_EXPIRED` (§4.5), **consumida**: se\n * devuelve una sola vez y se borra. Sin el consume, cada login futuro\n * volvería a la misma URL vieja aunque el usuario hubiera llegado al\n * login desde el menú.\n */\n consumeReturnUrl(): string | null {\n const url = this._returnUrl();\n this._returnUrl.set(null);\n return url;\n }\n\n private clear(): void {\n this._token.set(null);\n this._user.set(null);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this.storage.removeItem(SESSION_USER_KEY);\n }\n\n private goLogin(): void {\n if (this.currentUrl().startsWith(LOGIN_PATH)) return;\n // Angular rechaza la promesa cuando no hay ruta que empate (NG04002).\n // Navegar es un efecto, no una operación cuyo resultado alguien espere:\n // el catch evita una rejection sin handler en arranques y en tests.\n void this.router.navigate([LOGIN_PATH]).catch(() => undefined);\n }\n\n private currentUrl(): string {\n return this.router.url;\n }\n\n private envelope(path: string, payload: Record<string, unknown> = {}) {\n return { token: this._token(), method: 'POST' as const, path, payload };\n }\n\n /**\n * Mismo transporte que `ApiService` (R2.8): cuerpo JSON con\n * `Content-Type: text/plain` ⇒ **simple request**, sin preflight CORS.\n * Los tres endpoints de sesión pasan por acá — si alguno se saltara el\n * header, el navegador añadiría `application/json` y la llamada empezaría\n * a pedir permiso al servidor con un OPTIONS.\n */\n private post<T>(path: string, payload: Record<string, unknown> = {}): Observable<T> {\n return this.http.post<T>(this.apiUrl, this.envelope(path, payload), {\n headers: { 'Content-Type': 'text/plain' },\n });\n }\n}\n","import { HttpErrorResponse, HttpInterceptorFn, HttpResponse } from '@angular/common/http';\nimport { inject } from '@angular/core';\nimport { catchError, map, throwError } from 'rxjs';\n\nimport { ApiError } from '../models/api.model';\nimport type { ApiErrorBody } from '../models/api.model';\nimport { AuthService } from '../services/auth.service';\n\n/**\n * Mensaje humano para un error HTTP real (§2).\n *\n * El backend siempre responde 200 con el error dentro del body —eso ya\n * viene en español—, así que esto sólo cubre lo que el transporte falla:\n * red caída, proxy y 5xx. Sin esto el usuario ve el texto crudo de Angular\n * («Http failure response for http://…»), que es inglés técnico.\n */\nfunction httpMessage(status: number): string {\n switch (status) {\n case 0:\n return 'No se pudo conectar con el servidor. Revisa tu conexión.';\n case 401:\n return 'Tu sesión ha expirado. Vuelve a iniciar sesión.';\n case 403:\n return 'No tienes permisos para realizar esta acción.';\n case 404:\n return 'No se encontró el recurso solicitado.';\n case 409:\n return 'El registro cambió o ya existe. Recarga la página y vuelve a intentarlo.';\n case 413:\n return 'El archivo es demasiado grande.';\n case 422:\n return 'Los datos enviados no son válidos.';\n default:\n if (status >= 500) {\n return 'El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.';\n }\n return status >= 400 ? 'No se pudo completar la operación.' : 'Error inesperado.';\n }\n}\n\nfunction isErrorBody(body: unknown): body is ApiErrorBody {\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n const error = (body as ApiErrorBody).error;\n return typeof error === 'object' && error !== null && typeof error.message === 'string';\n}\n\n/**\n * El backend responde SIEMPRE HTTP 200 y pone los errores en el body\n * (api.md §7), así que se detectan en el canal de **éxito** y se relanzan\n * como `ApiError` (con `fieldPath` cuando el backend envía `path`).\n * Los errores HTTP reales (red caída, proxy) también se normalizan.\n *\n * **R2.9 — paso dos de dos.** Éste archivo es el ÚNICO punto donde el\n * sobre de error se abre; lo que decide qué ocurre con un `AUTH_*` es\n * `AuthService.applyAuthCode` (la tabla de §4.5). Aquí sólo se le entrega\n * el código y se propaga el `ApiError` con él — un segundo sitio que\n * «interprete» el código sería una regla duplicada y, a la larga, una que\n * se contradiga con la primera.\n */\nexport const apiErrorInterceptor: HttpInterceptorFn = (req, next) => {\n const auth = inject(AuthService);\n\n return next(req).pipe(\n map((event) => {\n if (event instanceof HttpResponse && isErrorBody(event.body)) {\n const { status, message, path, code, errors } = event.body.error;\n // R2.9: ésta es la ÚNICA llamada a la tabla de §4.5.\n auth.applyAuthCode(code);\n // `errors[]` es el422 con rutas (D42): va entero al ApiError para que\n // `ErrorMapperService` lo convierta en `serverErrors[prefix + key]`.\n throw new ApiError(status, message, path, code, errors ?? []);\n }\n return event;\n }),\n catchError((err: unknown) => {\n if (err instanceof ApiError) {\n return throwError(() => err);\n }\n if (err instanceof HttpErrorResponse) {\n return throwError(() => new ApiError(err.status, httpMessage(err.status)));\n }\n return throwError(() => err);\n }),\n );\n};\n","import { HttpClient } from '@angular/common/http';\nimport { inject, Service } from '@angular/core';\nimport { map, Observable, tap, throwError } from 'rxjs';\n\nimport { renewedTokenOf } from '../models/api.model';\nimport type { ResourceEndpoints } from '../models/schema.model';\nimport { API_URL } from '../tokens';\nimport { AuthService } from './auth.service';\n\n/** Métodos que entiende el envelope del backend (baseapi §2.3). */\nexport type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/**\n * CRUD genérico sobre el contrato v2 (baseapi §2.3): TODA llamada a la API\n * es un `POST` a `API_URL` con cuerpo `text/plain` y el JSON\n * `{ token, method, path, payload }`. `method: 'GET'` también para lecturas;\n * el token viaja en el cuerpo (nunca en cabeceras: criterio 11 de §17).\n *\n * `text/plain` en lugar de `application/json` evita el preflight CORS\n * (simple request) — criterio 11: sin cabeceras custom.\n *\n * **R2.8 — convención sobre el snippet.** `auth.md` §5.2 llama a las claves\n * `{ route, op, token, payload }`, pero el backend (`parseEnvelope_`) lee\n * `path` y `method`: el snippet es un esquema a medias, no un contrato.\n * Mismo precedente que en R2.5 — **la convención del código gana**. Cambiar\n * el nombre ahora rompería el único transporte que existe.\n *\n * **R2.12 — el token sale de `AuthService`, no de configuración.** El\n * `ADMIN_TOKEN` (D21-C) se retiró: no hay credencial fija, hay sesión.\n *\n * Devuelve el JSON plano tal cual, sin transformaciones ocultas\n * (base.md §1, principio 4). Los errores del backend (200 + `{ error: {…} }`)\n * los convierte en `ApiError` el interceptor `apiErrorInterceptor`, que es\n * también quien pasa los `AUTH_*` a `AuthService.applyAuthCode` (R2.9).\n */\n@Service()\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly auth = inject(AuthService);\n\n /**\n * Listado. El backend envuelve la respuesta cuando la operación declara\n * `shape.envelope: 'list'` (F5) ⇒ `{ items, nextCursor, total }` (R6.4);\n * la UI consume `T[]` en todos lados (F7-3) y este es el ÚNICO punto que\n * habla de listas, así que el desenvolvimiento vive acá. `nextCursor` es\n * el token de la página siguiente y `null` en la última — éste servicio\n * lo descarta hoy porque la lista no pagina todavía (backlog-ui-ux).\n * Un listado sin envelope (arreglo plano, el contrato histórico) pasa\n * tal cual.\n *\n * `payload` sólo lo usa una fuente declarada (F8 · R6.5): `{ view, params }`\n * pide el listado filtrado por una vista y sus parámetros. Sin él, la\n * llamada es la de siempre — la lectura llana del recurso.\n */\n list<T>(endpoint: ResourceEndpoints, payload?: Record<string, unknown>): Observable<T[]> {\n return this.send<unknown>(endpoint.list, 'GET', payload).pipe(map((raw) => unwrapList<T>(raw)));\n }\n\n /** `key` solo aplica a colecciones (`{key}` en la plantilla); los singleton no la llevan. */\n get<T>(endpoint: ResourceEndpoints, key?: string): Observable<T> {\n return this.send<T>(this.withKey(endpoint.get, key), 'GET');\n }\n\n create<T>(endpoint: ResourceEndpoints, payload: Record<string, unknown>): Observable<T> {\n return this.send<T>(endpoint.create, 'POST', payload);\n }\n\n update<T>(\n endpoint: ResourceEndpoints,\n key: string | undefined,\n payload: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(this.withKey(endpoint.update, key), 'PUT', payload);\n }\n\n remove(endpoint: ResourceEndpoints, key?: string): Observable<void> {\n return this.send<void>(this.withKey(endpoint.remove, key), 'DELETE');\n }\n\n /**\n * Operación genérica sobre una ruta ya resuelta: reorder de colección\n * (`PUT /admin/x` con `{ reorder: … }`), `/admin/schema`,\n * `/admin/upload-signature`, etc.\n */\n request<T>(\n method: EnvelopeMethod,\n path: string | undefined,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(path, method, payload);\n }\n\n private send<T>(\n path: string | undefined,\n method: EnvelopeMethod,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n if (!path) {\n return throwError(() => new Error('Operación no disponible para este recurso'));\n }\n // Token de la SESIÓN, leído en cada petición (R2.8): no hay credencial\n // fija que inyectar en el arranque.\n const body = { token: this.auth.token(), method, path, payload: payload ?? {} };\n return this.http.post<T>(this.apiUrl, body, { headers: { 'Content-Type': 'text/plain' } }).pipe(\n // Renovación deslizante (§3.2): si el back adjuntó `meta.renewedToken`\n // se adopta YA, con lo que la siguiente petición ya va firmada nueva.\n tap((res) => {\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.auth.applyRenewedToken(renewed);\n }),\n );\n }\n\n private withKey(path: string | undefined, key?: string): string | undefined {\n if (!path) {\n return undefined;\n }\n return path.includes('{key}') ? path.replace('{key}', encodeURIComponent(key ?? '')) : path;\n }\n}\n\n/** `{ items, nextCursor, total }` (F5/R6.4) → `T[]`; arreglo plano → tal cual; otro ⇒ []. */\nfunction unwrapList<T>(raw: unknown): T[] {\n if (Array.isArray(raw)) {\n return raw as T[];\n }\n if (raw !== null && typeof raw === 'object') {\n const items = (raw as { items?: unknown }).items;\n if (Array.isArray(items)) {\n return items as T[];\n }\n }\n return [];\n}\n","import { inject, Service, signal } from '@angular/core';\nimport { catchError, firstValueFrom, forkJoin, of } from 'rxjs';\n\nimport { ApiService } from './api.service';\nimport type { ResourceEndpoints } from '../models/schema.model';\n\n/**\n * Servicio global de configuración: reúne los singletons declarativos y los\n * deja disponibles para interpolación de plantillas (enlaces públicos,\n * mensajes para compartir).\n *\n * Es genérico a propósito: NO conoce ningún singleton concreto. Los\n * endpoints los inyecta `app.config.ts` con `provideAppInitializer`,\n * derivándolos del registry — agregar un singleton de configuración nuevo\n * = declarar su schema, sin tocar este fichero (refactormotor.md §1 — el\n * schema es el único plano de control).\n *\n * No hay efectos en el constructor: la carga es explícita (`load`), así un\n * TestBed que sólo provee un ApiService parcial no recibe una llamada\n * síncrona a `get` durante la construcción.\n */\n@Service()\nexport class ConfigService {\n private readonly api = inject(ApiService, { optional: true });\n\n /** Mapa reactivo de configuración global (p. ej. `public_base_url`). */\n readonly globalConfig = signal<Record<string, unknown>>({\n // Fallback genérico: el origen actual. Lo que declare el singleton\n // (p. ej. `share_message_template`) lo pone el backend, no el front.\n public_base_url: typeof window !== 'undefined' ? window.location.origin : '',\n });\n\n /** Endpoints conocidos; los guarda `load` para que `ensureLoaded` pueda reintentar. */\n private endpoints: readonly ResourceEndpoints[] = [];\n\n /**\n * `true` cuando la última carga terminó SIN fallos. Si algún singleton\n * falló (o ninguno respondió), queda `false` y el próximo punto crítico\n * vuelve a intentar: no hay estado «cargado a medias para siempre».\n */\n private loaded = false;\n\n /** Carga en curso: `ensureLoaded` la reutiliza en vez de duplicar viajes. */\n private inFlight: Promise<void> | null = null;\n\n /**\n * Carga todos los endpoints de configuración recibidos. Silenciosa: un\n * singleton que falle o que devuelva vacío no rompe nada, simplemente no\n * aporta claves (y deja `loaded = false` para poder reintentar).\n */\n load(endpoints: readonly ResourceEndpoints[]): void {\n this.endpoints = endpoints;\n void this.ensure();\n }\n\n /**\n * Garantiza la config global antes de un punto que la necesita.\n *\n * El load del bootstrap NO es bloqueante y sólo intenta una vez: si la app\n * arrancó sin sesión (el 401 de los singletons se traga en el `catchError`)\n * o la red tardó, la sesión seguía con la config vacía y, por ejemplo, el\n * mensaje de `copy-share` degradaba a «sólo la URL» (R8.7). Acá el reintento\n * corre con la sesión ya iniciada y responde bien; si ya se cargó, es un\n * no-op síncrono.\n */\n ensureLoaded(): Promise<void> {\n return this.ensure();\n }\n\n private ensure(): Promise<void> {\n if (this.loaded) return Promise.resolve();\n if (this.inFlight) return this.inFlight;\n if (!this.api || typeof this.api.get !== 'function' || this.endpoints.length === 0) {\n return Promise.resolve();\n }\n\n this.inFlight = firstValueFrom(\n forkJoin(\n this.endpoints.map((endpoint) =>\n this.api!.get<Record<string, unknown>>(endpoint).pipe(\n // `null` marca el fallo: con `of({})` un singleton caído era\n // indistinguible de uno realmente vacío y la carga quedaría\n // «hecha» sin datos.\n catchError(() => of(null)),\n ),\n ),\n ),\n ).then((blocks) => {\n this.inFlight = null;\n blocks.forEach((data) => {\n if (data && typeof data === 'object' && Object.keys(data).length > 0) {\n this.globalConfig.update((current) => ({ ...current, ...data }));\n }\n });\n this.loaded = blocks.every((data) => data !== null);\n });\n\n return this.inFlight;\n }\n\n /**\n * Contexto de datos para interpolar plantillas del schema\n * (`urlTemplate`, `shareTextTemplate`, `postCreate.description`, …).\n *\n * Sólo aporta datos y la base pública — la FORMA de la URL pública\n * (`/p/{ref}`) la declara el propio schema en su `urlTemplate`; este\n * servicio no decide cómo se arma el enlace.\n */\n buildInterpolationContext(record: Record<string, unknown>): Record<string, unknown> {\n const config = this.globalConfig();\n const baseUrl = String(config['public_base_url'] || window.location.origin).replace(/\\/+$/, '');\n\n return {\n ...config,\n ...record,\n public_base_url: baseUrl,\n };\n }\n}\n","import {\n EnvironmentProviders,\n InjectionToken,\n Provider,\n inject,\n provideAppInitializer,\n} from '@angular/core';\nimport { provideHttpClient, withInterceptors } from '@angular/common/http';\n\nimport { apiErrorInterceptor } from '../interceptors/api-error.interceptor';\nimport { AuthService } from '../services/auth.service';\nimport { ConfigService } from '../services/config.service';\nimport type { AuthSchema, ResourceSchema } from '../models/schema.model';\nimport type { FirebaseAuthAdapter } from '../adapters/firebase-auth.adapter';\nimport { FIREBASE_AUTH_ADAPTER } from '../tokens';\n\n\n/**\n * Catálogo de schemas del consumidor (F1 · G-F2).\n *\n * Antes de la extracción el motor importaba `getSchema` del\n * `schemas/registry.ts` de la app. Ese acoplamiento se rompe con un\n * catálogo a nivel módulo: el consumidor llama\n * `provideGasAdmin({ schemas, auth })` en su `app.config.ts` y el motor\n * resuelve en runtime con la MISMA semántica que antes (un solo catálogo\n * por aplicación, seteado una vez en el bootstrap).\n *\n * `provideGasAdmin` se ejecuta al evaluarse el `app.config` (módulo), y\n * todo uso del catálogo (guards, componentes, rutas dinámicas) ocurre en\n * runtime posterior al bootstrap — el orden es determinístico.\n */\n\nlet catalog: readonly ResourceSchema[] = [];\nlet authCatalog: AuthSchema | undefined;\n\n/** Fija el catálogo (uso interno de `provideGasAdmin`; testable directo). */\nexport function setSchemaCatalog(\n schemas: readonly ResourceSchema[],\n auth?: AuthSchema,\n): void {\n catalog = schemas;\n authCatalog = auth;\n}\n\n/** Busca un schema por `id`; `undefined` si no existe. */\nexport function getSchema(id: string): ResourceSchema | undefined {\n return catalog.find((schema) => schema.id === id);\n}\n\n/** Catálogo completo en orden (menú lateral). */\nexport function allSchemas(): readonly ResourceSchema[] {\n return catalog;\n}\n\n/** Declaración de la pantalla de login; `undefined` sin `auth` en config. */\nexport function getAuth(): AuthSchema | undefined {\n return authCatalog;\n}\n\n/** Token DI del catálogo (para inyectarlo donde un `inject()` es posible). */\nexport const GAS_SCHEMAS = new InjectionToken<readonly ResourceSchema[]>(\n 'gas.admin.schemas',\n);\n\n/** Configuración del motor. */\nexport interface GasAdminConfig {\n /** Catálogo ordenado de recursos (menú + rutas + resolución `getSchema`). */\n schemas: readonly ResourceSchema[];\n /** Declaración de la pantalla de login (opcional: sin ella, sin `/login`). */\n auth?: AuthSchema;\n}\n\n/**\n * Wiring completo del motor: catálogo + HTTP con el interceptor de\n * errores del envelope + loaders de config y sesión en el bootstrap\n * (antes en el `app.config` del consumidor).\n *\n * El consumidor conserva `provideRouter` y `{ provide: API_URL, ... }`\n * (su environment decide la URL del Web App).\n */\nexport function provideGasAdmin(\n config: GasAdminConfig,\n): (Provider | EnvironmentProviders)[] {\n setSchemaCatalog(config.schemas, config.auth);\n // Singletons de configuración: los declara el catálogo, no el servicio.\n // Un singleton nuevo con `endpoint.get` se carga solo.\n const configEndpoints = () =>\n config.schemas.flatMap((schema) =>\n schema.kind === 'singleton' && schema.endpoint?.get\n ? [{ get: schema.endpoint.get }]\n : [],\n );\n return [\n { provide: GAS_SCHEMAS, useValue: config.schemas },\n // Sin `authInterceptor`: el token viaja en el cuerpo del envelope\n // (baseapi §2.3, criterio 11: ninguna cabecera custom).\n provideHttpClient(withInterceptors([apiErrorInterceptor])),\n // Carga de config en bootstrap (fuera del constructor del servicio):\n // un singleton que falle no rompe el arranque.\n provideAppInitializer(() => inject(ConfigService).load(configEndpoints())),\n // Sesión (R2.12): revalida contra `/auth/me` el token que restauró el\n // AuthService de `sessionStorage`. Corre ANTES del primer guard.\n provideAppInitializer(() => inject(AuthService).restore()),\n ];\n}\n\n/**\n * Registra una implementación concreta de FirebaseAuthAdapter en el árbol de DI.\n * Usar junto con provideGasAdmin() en app.config.ts al configurar proveedores federados\n * (como 'google') o autenticación por Firebase.\n *\n * @example\n * // app.config.ts\n * provideGasAdmin({ schemas, auth }),\n * provideFirebaseAdapter(new MyFirebaseAuthAdapter(getAuth(firebaseApp))),\n */\nexport function provideFirebaseAdapter(adapter: FirebaseAuthAdapter): Provider {\n return { provide: FIREBASE_AUTH_ADAPTER, useValue: adapter };\n}\n\n"],"mappings":";;;;;AA4CA,IAAa,WAAb,cAA8B,MAAK;CACxB;CAEA;CAEA;CAEA;CAET,YACE,QACA,SACA,WACA,MACA,SAA8B,CAAA,GAAE;EAEhC,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,YAAY;EACjB,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;AACD;AAYD,SAAgB,eAAe,MAAa;CAC1C,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;CACtD,MAAM,OAAQ,KAA4B;CAC1C,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;CACtD,MAAM,QAAS,KAAoC;CACnD,OAAO,OAAO,UAAU,YAAY,UAAU,KAAK,QAAQ;AAC7D;AC7EA,MAAa,UAAU,IAAI,eAAuB,SAAS;AAQ3D,MAAa,wBAAwB,IAAI,eACvC,+BAA+B;AC+BjC,MAAa,aAAa;AAQ1B,MAAa,oBAAoB;AACjC,MAAa,mBAAmB;AAWhC,IAAa,cAAb,MAAa,YAAW;CACL,OAAO,OAAO,UAAU;CACxB,SAAS,OAAO,OAAO;CACvB,SAAS,OAAO,MAAM;CACtB,kBAAkB,OAAO,uBAAuB,EAAE,UAAU,KAAI,CAAE;CAYlE,UAAmB;CAEnB,QAAQ,OAA2B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,SAAS,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAEpC,aAAa,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEhD,OAAO,KAAK,MAAM,WAAU;CAC5B,QAAQ,KAAK,OAAO,WAAU;CAC9B,YAAY,KAAK,WAAW,WAAU;CAEtC,kBAAkB,eAAe,KAAK,OAAM,MAAO,QAAQ,KAAK,MAAK,MAAO,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CACjF,OAAO,eAAe,KAAK,MAAK,CAAE,EAAE,QAAQ,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAExD,cAAA;EACE,MAAM,QAAQ,KAAK,QAAQ,QAAQ,iBAAiB;EACpD,IAAI,UAAU,QAAQ,UAAU,IAAI,KAAK,OAAO,IAAI,KAAK;EACzD,MAAM,MAAM,KAAK,QAAQ,QAAQ,gBAAgB;EACjD,IAAI,QAAQ,QAAQ,QAAQ,IAC1B,IAAI;GACF,KAAK,MAAM,IAAI,KAAK,MAAM,GAAG,CAAgB;EAC/C,QAAQ;GAEN,KAAK,QAAQ,WAAW,gBAAgB;GACxC,KAAK,QAAQ,WAAW,iBAAiB;GACzC,KAAK,OAAO,IAAI,IAAI;EACtB;CAEJ;CAIA,MAAM,OAAe,UAAgB;EACnC,OAAO,KAAK,KAAoB,eAAe;GAAE;GAAO;EAAQ,CAAE,CAAC,CAAC,KAClE,KAAK,QAAO;GACV,KAAK,KAAK,IAAI,OAAO,IAAI,IAAI;GAC7B,MAAM,UAAU,eAAe,GAAG;GAClC,IAAI,YAAY,MAAM,KAAK,kBAAkB,OAAO;GACpD,OAAO,IAAI;EACb,CAAC,CAAC;CAEN;CAMA,kBAAe;EACb,IAAI,CAAC,KAAK,iBACR,OAAO,iCAAiB,IAAI,MAAM,uDAAuD,CAAC;EAE5F,OAAO,KAAK,KAAK,gBAAgB,iBAAgB,CAAE,CAAC,CAAC,KACnD,WAAW,YAAY,KAAK,sBAAsB,OAAO,CAAC,CAAC;CAE/D;CAMA,0BAA0B,OAAe,UAAgB;EACvD,IAAI,CAAC,KAAK,iBACR,OAAO,iCAAiB,IAAI,MAAM,uDAAuD,CAAC;EAE5F,OAAO,KAAK,KAAK,gBAAgB,2BAA2B,OAAO,QAAQ,CAAC,CAAC,CAAC,KAC5E,WAAW,YAAY,KAAK,sBAAsB,OAAO,CAAC,CAAC;CAE/D;CAMQ,sBAAsB,SAAe;EAC3C,MAAM,WAAW,QAAO,CAAE,EAAE,UAAU,oBAAoB;EAC1D,OAAO,KAAK,KAAoB,UAAU,EAAE,QAAO,CAAE,CAAC,CAAC,KACrD,KAAK,QAAO;GACV,KAAK,KAAK,IAAI,OAAO,IAAI,IAAI;GAC7B,MAAM,UAAU,eAAe,GAAG;GAClC,IAAI,YAAY,MAAM,KAAK,kBAAkB,OAAO;GACpD,OAAO,IAAI;EACb,CAAC,CAAC;CAEN;CAQA,SAAM;EACJ,OAAO,KAAK,KAA8B,cAAc,CAAC,CAAC,KACxD,IAAI;GACF,YAAW;IACT,KAAK,MAAK;IACV,AAAK,KAAK,iBAAiB,QAAO;GACpC;GACA,aAAY;IACV,KAAK,MAAK;IACV,AAAK,KAAK,iBAAiB,QAAO;GACpC;EACD,CAAA,GACD,UAAU,MAAS,CAAC;CAExB;CAIA,YAAS;EACP,OAAO,KAAK,KACV,kBAAkB,CACnB,CAAC,KACA,UAAS;GACP,KAAK,MAAK;GACV,KAAK,QAAO;EACd,CAAC,GACD,KAAK,SAAS,EAAE,wBAAwB,IAAI,uBAAsB,EAAG,CAAC;CAE1E;CAGA,UAAO;EACL,IAAI,KAAK,OAAM,MAAO,MAAM,OAAO,GAAG,MAAS;EAC/C,OAAO,KAAK,KAAiB,UAAU,CAAC,CAAC,KACvC,KAAK,QAAO;GACV,KAAK,KAAK,KAAK,OAAM,GAAI,IAAI,IAAI;EACnC,CAAC,GAGD,iBAAiB,GAAG,MAAS,CAAC,GAC9B,UAAU,MAAS,CAAC;CAExB;CAOA,kBAAkB,OAAa;EAC7B,IAAI,UAAU,MAAM,KAAK,OAAM,MAAO,MAAM;EAC5C,KAAK,OAAO,IAAI,KAAK;EACrB,KAAK,QAAQ,QAAQ,mBAAmB,KAAK;CAC/C;CAiBA,cAAc,MAAwB;EACpC,QAAQ,MAAR;GACE,KAAK;IAGH,KAAK,WAAW,IAAI,KAAK,WAAU,CAAE;IACrC,KAAK,MAAK;IACV,KAAK,QAAO;IACZ;GACF,KAAK;GACL,KAAK;IAEH,KAAK,MAAK;IACV,KAAK,QAAO;EAUhB;CACF;CAKA,QAAQ,UAAgB;EACtB,IAAI,aAAa,MAAM,aAAa,WAAW,OAAO,KAAK,gBAAe;EAC1E,MAAM,OAAO,KAAK,KAAI;EACtB,IAAI,SAAS,SAAS,OAAO;EAC7B,OAAO,SAAS,MAAM,SAAS;CACjC;CAWA,UAAU,QAAgD;EACxD,MAAM,WAAW,QAAQ,YAAY;EACrC,IAAI,aAAa,UAAU,OAAO;EAClC,IAAI,CAAC,KAAK,gBAAe,GAAI,OAAO;EAKpC,MAAM,OAAO,SAAS,WAAW,OAAO,IAAI,SAAS,MAAM,CAAc,IAAI;EAC7E,MAAM,WAAW,SAAS,KAAM,QAAQ,SAAS,CAAA,IAAM,CAAC,IAAI;EAC5D,IAAI,SAAS,WAAW,GAAG,OAAO;EAClC,OAAO,SAAS,MAAM,QAAQ,KAAK,QAAQ,GAAG,CAAC;CACjD;CAkBA,eACE,QACA,WAA0D;EAG1D,MAAM,MAAM,OAAO,UAAU,QAAQ;EACrC,IAAI,OAAO,IAAI,YACb,OAAO,KAAK,WAAW,IAAI,UAAU;EAIvC,IAAI,OAAO,UAAU,QAAQ;GAE3B,MAAM,aADU,cAAc,YAAY,cAAc,YAAY,cAAc,WACrD,OAAO,SAAS,OAAO,QAAQ,OAAO,SAAS,OAAO;GACnF,OAAO,KAAK,WAAW,cAAc,OAAO;EAC9C;EAGA,MAAM,QAAQ,OAAO;EACrB,IAAI,OAQF,OAAO;GANL,MAAM;GACN,KAAK;GACL,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,QAAQ,MAAM;EAET,EAAQ,eAAe;EAKhC,OAAO;CACT;CAUA,aAAa,OAAkB;EAC7B,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,OAAO,MAAM,OAAO;EAChD,OAAO,KAAK,WAAW,MAAM,OAAO,IAAI;CAC1C;CAUA,cAAc,OAAkB;EAC9B,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,OAAO,OAAO,OAAO;EACjD,OAAO,KAAK,WAAW,MAAM,OAAO,KAAK;CAC3C;CAOQ,WAAW,OAAa;EAC9B,IAAI,UAAU,UAAU,OAAO;EAC/B,IAAI,UAAU,WAAW,OAAO,KAAK,gBAAe;EACpD,IAAI,MAAM,WAAW,OAAO,GAAG;GAC7B,MAAM,eAAe,MAAM,MAAM,CAAc;GAC/C,OAAO,KAAK,QAAQ,YAAY;EAClC;EAEA,OAAO,KAAK,QAAQ,OAAO;CAC7B;CAEQ,KAAK,OAAsB,MAAwB;EACzD,IAAI,UAAU,QAAQ,UAAU,IAAI,KAAK,OAAO,IAAI,KAAK;EACzD,KAAK,MAAM,IAAI,IAAI;EACnB,IAAI,UAAU,QAAQ,UAAU,IAAI,KAAK,QAAQ,QAAQ,mBAAmB,KAAK;EACjF,IAAI,SAAS,MAAM,KAAK,QAAQ,QAAQ,kBAAkB,KAAK,UAAU,IAAI,CAAC;CAChF;CAYA,mBAAgB;EACd,MAAM,MAAM,KAAK,WAAU;EAC3B,KAAK,WAAW,IAAI,IAAI;EACxB,OAAO;CACT;CAEQ,QAAK;EACX,KAAK,OAAO,IAAI,IAAI;EACpB,KAAK,MAAM,IAAI,IAAI;EACnB,KAAK,QAAQ,WAAW,iBAAiB;EACzC,KAAK,QAAQ,WAAW,gBAAgB;CAC1C;CAEQ,UAAO;EACb,IAAI,KAAK,WAAU,CAAE,CAAC,WAAA,QAAqB,GAAG;EAI9C,AAAK,KAAK,OAAO,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,MAAS;CAC/D;CAEQ,aAAU;EAChB,OAAO,KAAK,OAAO;CACrB;CAEQ,SAAS,MAAc,UAAmC,CAAA,GAAE;EAClE,OAAO;GAAE,OAAO,KAAK,OAAM;GAAI,QAAQ;GAAiB;GAAM;EAAO;CACvE;CASQ,KAAQ,MAAc,UAAmC,CAAA,GAAE;EACjE,OAAO,KAAK,KAAK,KAAQ,KAAK,QAAQ,KAAK,SAAS,MAAM,OAAO,GAAG,EAClE,SAAS,EAAE,gBAAgB,aAAY,EACxC,CAAA;CACH;CAlYW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAW,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAX,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAW,CAAA;;AAAX,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAW,YAAA,CAAA,EADvB,MAAA,QAAA,CAAA;;;ACnDD,SAAS,YAAY,QAAc;CACjC,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT;GACE,IAAI,UAAU,KACZ,OAAO;GAET,OAAO,UAAU,MAAM,uCAAuC;CAClE;AACF;AAEA,SAAS,YAAY,MAAa;CAChC,IAAI,OAAO,SAAS,YAAY,SAAS,MACvC,OAAO;CAET,MAAM,QAAS,KAAsB;CACrC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAO,MAAM,YAAY;AACjF;AAeA,MAAa,uBAA0C,KAAK,SAAQ;CAClE,MAAM,OAAO,OAAO,WAAW;CAE/B,OAAO,KAAK,GAAG,CAAC,CAAC,KACf,KAAK,UAAS;EACZ,IAAI,iBAAiB,gBAAgB,YAAY,MAAM,IAAI,GAAG;GAC5D,MAAM,EAAE,QAAQ,SAAS,MAAM,MAAM,WAAW,MAAM,KAAK;GAE3D,KAAK,cAAc,IAAI;GAGvB,MAAM,IAAI,SAAS,QAAQ,SAAS,MAAM,MAAM,UAAU,CAAA,CAAE;EAC9D;EACA,OAAO;CACT,CAAC,GACD,YAAY,QAAgB;EAC1B,IAAI,eAAe,UACjB,OAAO,iBAAiB,GAAG;EAE7B,IAAI,eAAe,mBACjB,OAAO,iBAAiB,IAAI,SAAS,IAAI,QAAQ,YAAY,IAAI,MAAM,CAAC,CAAC;EAE3E,OAAO,iBAAiB,GAAG;CAC7B,CAAC,CAAC;AAEN;AClDA,IAAa,aAAb,MAAa,WAAU;CACJ,OAAO,OAAO,UAAU;CACxB,SAAS,OAAO,OAAO;CACvB,OAAO,OAAO,WAAW;CAgB1C,KAAQ,UAA6B,SAAiC;EACpE,OAAO,KAAK,KAAc,SAAS,MAAM,OAAO,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,WAAc,GAAG,CAAC,CAAC;CAChG;CAGA,IAAO,UAA6B,KAAY;EAC9C,OAAO,KAAK,KAAQ,KAAK,QAAQ,SAAS,KAAK,GAAG,GAAG,KAAK;CAC5D;CAEA,OAAU,UAA6B,SAAgC;EACrE,OAAO,KAAK,KAAQ,SAAS,QAAQ,QAAQ,OAAO;CACtD;CAEA,OACE,UACA,KACA,SAAgC;EAEhC,OAAO,KAAK,KAAQ,KAAK,QAAQ,SAAS,QAAQ,GAAG,GAAG,OAAO,OAAO;CACxE;CAEA,OAAO,UAA6B,KAAY;EAC9C,OAAO,KAAK,KAAW,KAAK,QAAQ,SAAS,QAAQ,GAAG,GAAG,QAAQ;CACrE;CAOA,QACE,QACA,MACA,SAAiC;EAEjC,OAAO,KAAK,KAAQ,MAAM,QAAQ,OAAO;CAC3C;CAEQ,KACN,MACA,QACA,SAAiC;EAEjC,IAAI,CAAC,MACH,OAAO,iCAAiB,IAAI,MAAM,2CAA2C,CAAC;EAIhF,MAAM,OAAO;GAAE,OAAO,KAAK,KAAK,MAAK;GAAI;GAAQ;GAAM,SAAS,WAAW,CAAA;EAAE;EAC7E,OAAO,KAAK,KAAK,KAAQ,KAAK,QAAQ,MAAM,EAAE,SAAS,EAAE,gBAAgB,aAAY,EAAE,CAAE,CAAC,CAAC,KAGzF,KAAK,QAAO;GACV,MAAM,UAAU,eAAe,GAAG;GAClC,IAAI,YAAY,MAAM,KAAK,KAAK,kBAAkB,OAAO;EAC3D,CAAC,CAAC;CAEN;CAEQ,QAAQ,MAA0B,KAAY;EACpD,IAAI,CAAC,MACH;EAEF,OAAO,KAAK,SAAS,OAAO,IAAI,KAAK,QAAQ,SAAS,mBAAmB,OAAO,EAAE,CAAC,IAAI;CACzF;CAnFW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAU,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAV,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAU,CAAA;;AAAV,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAU,YAAA,CAAA,EADtB,MAAA,QAAA,CAAA;;AAwFD,SAAS,WAAc,KAAY;CACjC,IAAI,MAAM,QAAQ,GAAG,GACnB,OAAO;CAET,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU;EAC3C,MAAM,QAAS,IAA4B;EAC3C,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;CAEX;CACA,OAAO,CAAA;AACT;AChHA,IAAa,gBAAb,MAAa,cAAa;CACP,MAAM,OAAO,YAAY,EAAE,UAAU,KAAI,CAAE;CAGnD,eAAe,OAAgC,EAGtD,iBAAiB,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS,GAC3E,GAAC,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAGM,YAA0C,CAAA;CAO1C,SAAS;CAGT,WAAiC;CAOzC,KAAK,WAAuC;EAC1C,KAAK,YAAY;EACjB,AAAK,KAAK,OAAM;CAClB;CAYA,eAAY;EACV,OAAO,KAAK,OAAM;CACpB;CAEQ,SAAM;EACZ,IAAI,KAAK,QAAQ,OAAO,QAAQ,QAAO;EACvC,IAAI,KAAK,UAAU,OAAO,KAAK;EAC/B,IAAI,CAAC,KAAK,OAAO,OAAO,KAAK,IAAI,QAAQ,cAAc,KAAK,UAAU,WAAW,GAC/E,OAAO,QAAQ,QAAO;EAGxB,KAAK,WAAW,eACd,SACE,KAAK,UAAU,KAAK,aAClB,KAAK,IAAK,IAA6B,QAAQ,CAAC,CAAC,KAI/C,iBAAiB,GAAG,IAAI,CAAC,CAAC,CAC3B,CACF,CACF,CACF,CAAC,MAAM,WAAU;GAChB,KAAK,WAAW;GAChB,OAAO,SAAS,SAAQ;IACtB,IAAI,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,GACjE,KAAK,aAAa,QAAQ,aAAa;KAAE,GAAG;KAAS,GAAG;IAAI,EAAG;GAEnE,CAAC;GACD,KAAK,SAAS,OAAO,OAAO,SAAS,SAAS,IAAI;EACpD,CAAC;EAED,OAAO,KAAK;CACd;CAUA,0BAA0B,QAA+B;EACvD,MAAM,SAAS,KAAK,aAAY;EAChC,MAAM,UAAU,OAAO,OAAO,sBAAsB,OAAO,SAAS,MAAM,CAAC,CAAC,QAAQ,QAAQ,EAAE;EAE9F,OAAO;GACL,GAAG;GACH,GAAG;GACH,iBAAiB;EAClB;CACH;CA/FW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAb,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAa,CAAA;;AAAb,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA,EADzB,MAAA,QAAA,CAAA;;ACWD,IAAI,UAAqC,CAAA;AACzC,IAAI;AAGJ,SAAgB,iBACd,SACA,MAAiB;CAEjB,UAAU;CACV,cAAc;AAChB;AAGA,SAAgB,UAAU,IAAU;CAClC,OAAO,QAAQ,MAAM,WAAW,OAAO,OAAO,EAAE;AAClD;AAGA,SAAgB,aAAU;CACxB,OAAO;AACT;AAGA,SAAgB,UAAO;CACrB,OAAO;AACT;AAGA,MAAa,cAAc,IAAI,eAC7B,mBAAmB;AAmBrB,SAAgB,gBACd,QAAsB;CAEtB,iBAAiB,OAAO,SAAS,OAAO,IAAI;CAG5C,MAAM,wBACJ,OAAO,QAAQ,SAAS,WACtB,OAAO,SAAS,eAAe,OAAO,UAAU,MAC5C,CAAC,EAAE,KAAK,OAAO,SAAS,IAAG,CAAE,IAC7B,CAAA,CAAE;CAEV,OAAO;EACL;GAAE,SAAS;GAAa,UAAU,OAAO;EAAO;EAGhD,kBAAkB,iBAAiB,CAAC,mBAAmB,CAAC,CAAC;EAGzD,4BAA4B,OAAO,aAAa,CAAC,CAAC,KAAK,gBAAe,CAAE,CAAC;EAGzE,4BAA4B,OAAO,WAAW,CAAC,CAAC,QAAO,CAAE;CAC1D;AACH;AAYA,SAAgB,uBAAuB,SAA4B;CACjE,OAAO;EAAE,SAAS;EAAuB,UAAU;CAAO;AAC5D"}