@jantstack/adonis-authz 1.1.0 → 2.4.0-alpha.1

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 (236) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +837 -51
  3. package/build/commands/authz_catalog_diff.d.ts +28 -0
  4. package/build/commands/authz_catalog_diff.d.ts.map +1 -0
  5. package/build/commands/authz_catalog_diff.js +67 -0
  6. package/build/commands/authz_catalog_diff.js.map +1 -0
  7. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  8. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  9. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  10. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  11. package/build/commands/authz_catalog_sync.d.ts +37 -0
  12. package/build/commands/authz_catalog_sync.d.ts.map +1 -0
  13. package/build/commands/authz_catalog_sync.js +81 -0
  14. package/build/commands/authz_catalog_sync.js.map +1 -0
  15. package/build/commands/authz_freeze.d.ts +44 -0
  16. package/build/commands/authz_freeze.d.ts.map +1 -0
  17. package/build/commands/authz_freeze.js +95 -0
  18. package/build/commands/authz_freeze.js.map +1 -0
  19. package/build/commands/authz_reconcile.d.ts +102 -0
  20. package/build/commands/authz_reconcile.d.ts.map +1 -0
  21. package/build/commands/authz_reconcile.js +294 -0
  22. package/build/commands/authz_reconcile.js.map +1 -0
  23. package/build/commands/authz_relations_reconcile.d.ts +73 -0
  24. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  25. package/build/commands/authz_relations_reconcile.js +225 -0
  26. package/build/commands/authz_relations_reconcile.js.map +1 -0
  27. package/build/commands/authz_scopes_relay.d.ts +47 -0
  28. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  29. package/build/commands/authz_scopes_relay.js +141 -0
  30. package/build/commands/authz_scopes_relay.js.map +1 -0
  31. package/build/commands/authz_unfreeze.d.ts +37 -0
  32. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  33. package/build/commands/authz_unfreeze.js +92 -0
  34. package/build/commands/authz_unfreeze.js.map +1 -0
  35. package/build/commands/main.d.ts +8 -1
  36. package/build/commands/main.d.ts.map +1 -1
  37. package/build/commands/main.js +8 -1
  38. package/build/commands/main.js.map +1 -1
  39. package/build/commands/openfga_provision.d.ts +46 -4
  40. package/build/commands/openfga_provision.d.ts.map +1 -1
  41. package/build/commands/openfga_provision.js +91 -7
  42. package/build/commands/openfga_provision.js.map +1 -1
  43. package/build/configure.d.ts +11 -0
  44. package/build/configure.d.ts.map +1 -1
  45. package/build/configure.js +37 -1
  46. package/build/configure.js.map +1 -1
  47. package/build/index.d.ts +75 -7
  48. package/build/index.d.ts.map +1 -1
  49. package/build/index.js +67 -5
  50. package/build/index.js.map +1 -1
  51. package/build/providers/authz_provider.d.ts +26 -2
  52. package/build/providers/authz_provider.d.ts.map +1 -1
  53. package/build/providers/authz_provider.js +48 -2
  54. package/build/providers/authz_provider.js.map +1 -1
  55. package/build/services/relations.d.ts +14 -0
  56. package/build/services/relations.d.ts.map +1 -0
  57. package/build/services/relations.js +17 -0
  58. package/build/services/relations.js.map +1 -0
  59. package/build/src/catalog/catalog.d.ts +289 -0
  60. package/build/src/catalog/catalog.d.ts.map +1 -0
  61. package/build/src/catalog/catalog.js +859 -0
  62. package/build/src/catalog/catalog.js.map +1 -0
  63. package/build/src/catalog/catalog_cache.d.ts +324 -0
  64. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  65. package/build/src/catalog/catalog_cache.js +666 -0
  66. package/build/src/catalog/catalog_cache.js.map +1 -0
  67. package/build/src/clock.d.ts +24 -0
  68. package/build/src/clock.d.ts.map +1 -0
  69. package/build/src/clock.js +7 -0
  70. package/build/src/clock.js.map +1 -0
  71. package/build/src/define_config.d.ts +201 -7
  72. package/build/src/define_config.d.ts.map +1 -1
  73. package/build/src/define_config.js.map +1 -1
  74. package/build/src/drivers/database_driver.d.ts +324 -15
  75. package/build/src/drivers/database_driver.d.ts.map +1 -1
  76. package/build/src/drivers/database_driver.js +1107 -128
  77. package/build/src/drivers/database_driver.js.map +1 -1
  78. package/build/src/drivers/database_relations_driver.d.ts +75 -0
  79. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  80. package/build/src/drivers/database_relations_driver.js +450 -0
  81. package/build/src/drivers/database_relations_driver.js.map +1 -0
  82. package/build/src/drivers/openfga_driver.d.ts +963 -55
  83. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  84. package/build/src/drivers/openfga_driver.js +2693 -372
  85. package/build/src/drivers/openfga_driver.js.map +1 -1
  86. package/build/src/drivers/openfga_facts.d.ts +369 -0
  87. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  88. package/build/src/drivers/openfga_facts.js +813 -0
  89. package/build/src/drivers/openfga_facts.js.map +1 -0
  90. package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
  91. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  92. package/build/src/drivers/openfga_relations_driver.js +466 -0
  93. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  94. package/build/src/errors.d.ts +586 -0
  95. package/build/src/errors.d.ts.map +1 -1
  96. package/build/src/errors.js +583 -0
  97. package/build/src/errors.js.map +1 -1
  98. package/build/src/expiry.d.ts +27 -0
  99. package/build/src/expiry.d.ts.map +1 -0
  100. package/build/src/expiry.js +50 -0
  101. package/build/src/expiry.js.map +1 -0
  102. package/build/src/freeze.d.ts +120 -0
  103. package/build/src/freeze.d.ts.map +1 -0
  104. package/build/src/freeze.js +172 -0
  105. package/build/src/freeze.js.map +1 -0
  106. package/build/src/hierarchical_resolver.d.ts +56 -0
  107. package/build/src/hierarchical_resolver.d.ts.map +1 -0
  108. package/build/src/hierarchical_resolver.js +87 -0
  109. package/build/src/hierarchical_resolver.js.map +1 -0
  110. package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
  111. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  112. package/build/src/{middleware → http}/app_access_middleware.js +10 -26
  113. package/build/src/http/app_access_middleware.js.map +1 -0
  114. package/build/src/http/resource_access_middleware.d.ts +105 -0
  115. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  116. package/build/src/http/resource_access_middleware.js +81 -0
  117. package/build/src/http/resource_access_middleware.js.map +1 -0
  118. package/build/src/identity.d.ts +228 -0
  119. package/build/src/identity.d.ts.map +1 -0
  120. package/build/src/identity.js +457 -0
  121. package/build/src/identity.js.map +1 -0
  122. package/build/src/manager.d.ts +536 -7
  123. package/build/src/manager.d.ts.map +1 -1
  124. package/build/src/manager.js +2676 -23
  125. package/build/src/manager.js.map +1 -1
  126. package/build/src/memoize_ancestors.d.ts +23 -0
  127. package/build/src/memoize_ancestors.d.ts.map +1 -0
  128. package/build/src/memoize_ancestors.js +42 -0
  129. package/build/src/memoize_ancestors.js.map +1 -0
  130. package/build/src/models/authz_assignment.d.ts +11 -11
  131. package/build/src/models/authz_assignment.d.ts.map +1 -1
  132. package/build/src/models/authz_deny.d.ts +11 -11
  133. package/build/src/models/authz_deny.d.ts.map +1 -1
  134. package/build/src/models/authz_permission.d.ts +17 -11
  135. package/build/src/models/authz_permission.d.ts.map +1 -1
  136. package/build/src/models/authz_permission.js +4 -0
  137. package/build/src/models/authz_permission.js.map +1 -1
  138. package/build/src/models/authz_role.d.ts +19 -12
  139. package/build/src/models/authz_role.d.ts.map +1 -1
  140. package/build/src/models/authz_role.js +6 -1
  141. package/build/src/models/authz_role.js.map +1 -1
  142. package/build/src/models/authz_role_permission.d.ts +11 -11
  143. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  144. package/build/src/openfga.d.ts +29 -0
  145. package/build/src/openfga.d.ts.map +1 -0
  146. package/build/src/openfga.js +26 -0
  147. package/build/src/openfga.js.map +1 -0
  148. package/build/src/reconcile.d.ts +37 -0
  149. package/build/src/reconcile.d.ts.map +1 -0
  150. package/build/src/reconcile.js +69 -0
  151. package/build/src/reconcile.js.map +1 -0
  152. package/build/src/relation_partition_trigger.d.ts +8 -0
  153. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  154. package/build/src/relation_partition_trigger.js +85 -0
  155. package/build/src/relation_partition_trigger.js.map +1 -0
  156. package/build/src/relations/define_relations_config.d.ts +58 -0
  157. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  158. package/build/src/relations/define_relations_config.js +144 -0
  159. package/build/src/relations/define_relations_config.js.map +1 -0
  160. package/build/src/relations/manager.d.ts +38 -0
  161. package/build/src/relations/manager.d.ts.map +1 -0
  162. package/build/src/relations/manager.js +156 -0
  163. package/build/src/relations/manager.js.map +1 -0
  164. package/build/src/relations/reconcile.d.ts +62 -0
  165. package/build/src/relations/reconcile.d.ts.map +1 -0
  166. package/build/src/relations/reconcile.js +138 -0
  167. package/build/src/relations/reconcile.js.map +1 -0
  168. package/build/src/relations_config_store.d.ts +22 -0
  169. package/build/src/relations_config_store.d.ts.map +1 -0
  170. package/build/src/relations_config_store.js +74 -0
  171. package/build/src/relations_config_store.js.map +1 -0
  172. package/build/src/scope_outbox.d.ts +69 -0
  173. package/build/src/scope_outbox.d.ts.map +1 -0
  174. package/build/src/scope_outbox.js +291 -0
  175. package/build/src/scope_outbox.js.map +1 -0
  176. package/build/src/shared/backend_guard.d.ts +106 -0
  177. package/build/src/shared/backend_guard.d.ts.map +1 -0
  178. package/build/src/shared/backend_guard.js +246 -0
  179. package/build/src/shared/backend_guard.js.map +1 -0
  180. package/build/src/shared/sql_expiry.d.ts +53 -0
  181. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  182. package/build/src/shared/sql_expiry.js +66 -0
  183. package/build/src/shared/sql_expiry.js.map +1 -0
  184. package/build/src/sql_descendants.d.ts +97 -0
  185. package/build/src/sql_descendants.d.ts.map +1 -0
  186. package/build/src/sql_descendants.js +203 -0
  187. package/build/src/sql_descendants.js.map +1 -0
  188. package/build/src/testing/contract.d.ts +212 -7
  189. package/build/src/testing/contract.d.ts.map +1 -1
  190. package/build/src/testing/contract.js +3449 -24
  191. package/build/src/testing/contract.js.map +1 -1
  192. package/build/src/testing/main.d.ts +10 -2
  193. package/build/src/testing/main.d.ts.map +1 -1
  194. package/build/src/testing/main.js +5 -1
  195. package/build/src/testing/main.js.map +1 -1
  196. package/build/src/testing/migration_contract.d.ts +284 -0
  197. package/build/src/testing/migration_contract.d.ts.map +1 -0
  198. package/build/src/testing/migration_contract.js +586 -0
  199. package/build/src/testing/migration_contract.js.map +1 -0
  200. package/build/src/testing/relations_contract.d.ts +51 -0
  201. package/build/src/testing/relations_contract.d.ts.map +1 -0
  202. package/build/src/testing/relations_contract.js +654 -0
  203. package/build/src/testing/relations_contract.js.map +1 -0
  204. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  205. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  206. package/build/src/testing/relations_reconcile_contract.js +172 -0
  207. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  208. package/build/src/testing/scope_tree.d.ts +65 -0
  209. package/build/src/testing/scope_tree.d.ts.map +1 -0
  210. package/build/src/testing/scope_tree.js +145 -0
  211. package/build/src/testing/scope_tree.js.map +1 -0
  212. package/build/src/traits/authz_scopes.d.ts +30 -6
  213. package/build/src/traits/authz_scopes.d.ts.map +1 -1
  214. package/build/src/traits/authz_scopes.js +30 -18
  215. package/build/src/traits/authz_scopes.js.map +1 -1
  216. package/build/src/traits/has_uuid.d.ts +12 -12
  217. package/build/src/traits/has_uuid.d.ts.map +1 -1
  218. package/build/src/types.d.ts +1313 -28
  219. package/build/src/types.d.ts.map +1 -1
  220. package/build/src/types.js +20 -4
  221. package/build/src/types.js.map +1 -1
  222. package/build/stubs/config/app_acl.stub +4 -2
  223. package/build/stubs/config/authorization.stub +168 -14
  224. package/build/stubs/migration.stub +183 -13
  225. package/build/stubs/scopes_outbox_migration.stub +57 -0
  226. package/package.json +14 -7
  227. package/build/commands/openfga_import.d.ts +0 -28
  228. package/build/commands/openfga_import.d.ts.map +0 -1
  229. package/build/commands/openfga_import.js +0 -74
  230. package/build/commands/openfga_import.js.map +0 -1
  231. package/build/src/catalog.d.ts +0 -3
  232. package/build/src/catalog.d.ts.map +0 -1
  233. package/build/src/catalog.js +0 -103
  234. package/build/src/catalog.js.map +0 -1
  235. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  236. package/build/src/middleware/app_access_middleware.js.map +0 -1
@@ -1,19 +1,35 @@
1
- import type { AuthorizationDriver, GrantOptions, ScopeAncestorsResolver, ScopeRef, ScopeType, SubjectRef } from '../types.js';
1
+ import type { ClientBatchCheckItem, ClientBatchCheckSingleResponse } from '@openfga/sdk';
2
+ import type { AuthorizationDriver, CatalogProjection, DenyRef, GrantOptions, GrantOutcome, HolderTypeMap, ReconcileFactPage, ReconcileOptions, ReconcileReport, ReconcileSource, RoleQuery, ScopeChainResolver, ScopeOutbox, ScopeRef, ScopeType, SubjectRef } from '../types.js';
3
+ import { CatalogCache } from '../catalog/catalog_cache.js';
4
+ import type { CatalogRevalidate, CatalogRoleRef } from '../catalog/catalog_cache.js';
5
+ import { assertHolderTypes } from './openfga_facts.js';
6
+ import type { FactsRelationsConfig } from './openfga_facts.js';
7
+ import type { Clock } from '../clock.js';
2
8
  /**
3
- * Driver `openfga` — los HECHOS (asignaciones y denies) viven en un servidor
4
- * OpenFGA; el CATÁLOGO (roles/permisos/vínculos) y la JERARQUÍA (orgs/units)
5
- * siguen siendo metadata local en las tablas del chasis (split de propiedad
6
- * de datos del análisis: cambiar de driver = migrar una tabla de hechos).
9
+ * Driver `openfga` — **modo `facts` y solo `facts`** (3b-2k · K2). Los
10
+ * HECHOS (asignaciones y denies), el ÁRBOL de scopes y la PROYECCIÓN del
11
+ * catálogo viven en un servidor OpenFGA, que es el PDP: `authorize` es UN
12
+ * solo `Check` y no consulta el árbol del consumidor. El CATÁLOGO sigue
13
+ * siendo propiedad local en las tablas `authz_*` (la proyección es derivada,
14
+ * reconstruible y nunca se lee como catálogo).
7
15
  *
8
- * Modelo FGA genérico (ver `openFgaAuthorizationModel()`):
9
- * - `role_binding:<scopeKey>|<rol>` #assignee asignación de rol
10
- * - `deny_binding:<scopeKey>|<perm>` #denied deny explícito
16
+ * Modelo (c2r), en `openfga_facts.ts` (`openFgaFactsModel`):
17
+ * - `role:<roleUuid>#permits_<P>@<holder>:*`la proyección del catálogo
18
+ * - `role_binding:<scopeKey>|<roleUuid>` #assignee la asignación
19
+ * (3A · A1: el id lleva el UUID del catálogo, nunca el slug; se parsea
20
+ * desde la derecha), `#role` → su rol
21
+ * - `scope:<scopeKey>` con `#parent` (el árbol), `#binding` (dónde es
22
+ * visible el rol), `#denied_<P>` (el deny explícito) y `#rooted` (la
23
+ * alcanzabilidad de la raíz: `can_<P> = (<P> but not denied_<P>) and
24
+ * rooted`, 3b-2i)
11
25
  * - Expiración vía condition `not_expired` (valid_until en la tupla,
12
26
  * current_time en cada check) — ni scheduler necesita.
13
27
  *
14
- * La herencia se resuelve igual que en el driver database: la cadena de
15
- * scopes se calcula localmente (el árbol lo declara el consumidor vía
16
- * `resolveAncestors`) y se consulta FGA por batchCheck sobre la cadena.
28
+ * **El modo `resolver` ya no existe** (3b-2k · K2, breaking): hasta 2.2 este
29
+ * driver expandía la cadena del consumidor a un `batchCheck` de N×M y
30
+ * guardaba los denies en objetos `deny_binding` propios. Con (c2r) esa rama
31
+ * era código muerto: se borró entera, y con ella `openFgaAuthorizationModel`
32
+ * y `openfga:import` (su sustituto es `authz:reconcile`, 3b-3).
17
33
  *
18
34
  * NADA del dominio está cableado: los holders llegan como `holderTypes`
19
35
  * (morph name → tipo FGA) y los niveles de scope se derivan del propio
@@ -22,95 +38,987 @@ import type { AuthorizationDriver, GrantOptions, ScopeAncestorsResolver, ScopeRe
22
38
  */
23
39
  /**
24
40
  * Mapa morph name → tipo del modelo FGA (`users` → `user`). Debe ser el
25
- * MISMO con el que se generó el authorization model del store.
41
+ * MISMO con el que se generó el authorization model del store. Definido en
42
+ * el puerto (`types.ts`) y re-exportado aquí.
26
43
  */
27
- export type HolderTypeMap = Record<string, string>;
44
+ export type { HolderTypeMap };
28
45
  /**
29
- * El authorization model en formato JSON del API de FGA, generado a partir
30
- * de los holders del consumidor. El mismo mapa debe usarse al construir el
31
- * driver: si difieren, los checks no encuentran las tuplas.
46
+ * La inyectividad de `holderTypes` la comprueba el módulo del modelo
47
+ * (`openfga_facts.ts`, compartido por los dos generadores). Se re-exporta
48
+ * desde aquí porque el subpath `/openfga` es la puerta publicada.
32
49
  */
33
- export declare function openFgaAuthorizationModel(holderTypeMap: HolderTypeMap): any;
50
+ export { assertHolderTypes };
34
51
  /**
35
- * Crea un store nuevo + escribe el authorization model derivado de los
36
- * holders del consumidor. Para bootstrap de un appliance o del harness de
37
- * tests. El `name` lo decide el caller (el comando openfga:provision
38
- * resuelve APP_NAME del entorno el motor no lee env).
52
+ * Id de binding (`<scopeKey>|<uuid>`: `app|<uuid>` o `<tipo>|<uuidScope>|<uuid>`)
53
+ * scope + uuid del ROL (3b-2k · K2: el `deny_binding`, que era el otro
54
+ * consumidor de esta gramática, se fue con el modo `resolver`; el deny es hoy
55
+ * una relación del scope). Se parsea DESDE LA DERECHA (3A · A1): el último componente
56
+ * es el uuid y el resto la clave del scope, que tiene 1 parte (`app`) o 2
57
+ * (`<tipo>|<uuid>`). Antes el último componente era el slug codificado
58
+ * (`docs~read`) y el parseo contaba partes: ambiguo en cuanto la clave del
59
+ * scope variara de longitud (panel 2026-08-28, §2-C), y con un escape no
60
+ * inyectivo desde el llamante (L0.8a).
61
+ *
62
+ * `null` si no tiene la forma del motor O si alguna parte no pasa su
63
+ * gramática —el scope, la de identidad; el uuid, la de UUID canónico del
64
+ * catálogo—: un id que el driver no escribiría no es un hecho del motor
65
+ * aunque esté en el store. Los ids de 1.x/2.0–2.1 (con slug) caen aquí:
66
+ * 2.2 no los lee, y `authz:reconcile` (3b-3) los reportará como deriva.
67
+ * Exportada para probarla sin servidor.
68
+ */
69
+ export declare function parseBindingId(id: string): {
70
+ scope: ScopeRef;
71
+ uuid: string;
72
+ } | null;
73
+ /**
74
+ * Alinea los resultados de un batchCheck con los checks pedidos por
75
+ * `correlationId`, no por posición (L0.14). El SDK reparte el lote en
76
+ * sub-lotes paralelos y concatena las respuestas según llegan: el orden no es
77
+ * el de los checks. Cardinalidad igual no basta —un id duplicado y otro
78
+ * ausente pasan el conteo—: cada check debe tener EXACTAMENTE un resultado y
79
+ * ningún resultado puede ser de un check que no se pidió.
80
+ */
81
+ export declare function correlateBatchResults(checks: ClientBatchCheckItem[], results: ClientBatchCheckSingleResponse[]): ClientBatchCheckSingleResponse[];
82
+ /**
83
+ * Crea un store nuevo + escribe el authorization model **`facts` (c2r)**
84
+ * derivado de los holders y de los PERMISOS del consumidor (3b-2k · K2:
85
+ * antes escribía el modelo del modo `resolver`, que ya no existe). Para
86
+ * bootstrap de un appliance o del harness de tests. El `name` lo decide el
87
+ * caller (el comando `openfga:provision` resuelve APP_NAME del entorno — el
88
+ * motor no lee env).
89
+ *
90
+ * Los permisos entran aquí porque el modelo (c2r) declara CUATRO relaciones
91
+ * por permiso: un store provisionado sin ellos no puede responder a ninguna
92
+ * pregunta. `assertFactsModelPublishable` comprueba antes las cotas (nombre
93
+ * de relación y techo de 262.144 bytes ⇒ 500 `E_AUTHZ_MODEL_TOO_LARGE`).
94
+ * Añadir un permiso al catálogo obliga a republicar el modelo: es lo que
95
+ * hace `syncAuthzCatalog` con la proyección inyectada, y por eso el modelo
96
+ * versionado del store se escribe con `--store-id`.
97
+ *
98
+ * **Y los tipos de RELACIÓN (ReBAC) van FUSIONADOS** (Fase 4-8): si el
99
+ * consumidor declaró `relations.config`, `openfga:provision` los pasa aquí y
100
+ * el modelo publicado los incluye, de modo que un store recién aprovisionado
101
+ * ya acepta tuplas de relación (`document#viewer`…) SIN un
102
+ * `authz:catalog:sync` previo. Sin ellos el modelo es facts-only y una tupla
103
+ * de relación es un `validation_error` del servidor («type 'document' not
104
+ * found»). El gate de bytes mide el modelo FUSIONADO.
39
105
  */
40
- export declare function provisionOpenFgaStore(apiUrl: string, name: string, holderTypeMap: HolderTypeMap): Promise<{
106
+ export declare function provisionOpenFgaStore(apiUrl: string, name: string, holderTypeMap: HolderTypeMap, permissions: readonly string[], relations?: FactsRelationsConfig): Promise<{
41
107
  storeId: string;
42
108
  modelId: string;
43
109
  }>;
110
+ /**
111
+ * ¿El error (o su cadena de causas) es el rechazo de FGA a escribir una tuple
112
+ * key que ya existe? Es la ÚNICA señal de carrera check-then-write que
113
+ * `grant` acepta: un 400 de validación (`validation_error`) o un 5xx no son
114
+ * "alguien escribió antes" y se propagan clasificados, con el error del SDK
115
+ * como causa (D6). Verificado contra OpenFGA v1.19: el duplicado llega como
116
+ * HTTP 400 con `apiErrorCode: 'write_failed_due_to_invalid_input'` y el
117
+ * mensaje "cannot write a tuple which already exists".
118
+ *
119
+ * **Y el 409 tiene nombre propio** (3b-2f · R3, medido contra el servidor):
120
+ * es el `Aborted` de un `Write` transaccional cuyas tuplas escribió otra
121
+ * transacción a la vez ("transactional write failed due to conflict: one or
122
+ * more tuples to write were inserted by another transaction"). Dice lo mismo
123
+ * —otro escritor llegó antes— y se trata igual: releer y re-aplicar. Lo que
124
+ * NO dice es QUÉ tupla chocó, así que quién existía lo decide la relectura y
125
+ * nunca el mensaje.
126
+ */
127
+ export declare function isDuplicateWrite(error: unknown): boolean;
44
128
  export interface OpenFgaDriverOptions {
45
129
  apiUrl: string;
46
130
  storeId: string;
47
131
  /** Pin del model; si se omite, FGA usa el último. */
48
132
  modelId?: string;
49
- /** Jerarquía del consumidor (ver ScopeAncestorsResolver). */
50
- resolveAncestors?: ScopeAncestorsResolver;
133
+ /** Jerarquía del consumidor (ver ScopeChainResolver). */
134
+ resolveChain?: ScopeChainResolver;
51
135
  /**
52
136
  * Holders del consumidor: morph name → tipo FGA. Debe coincidir con el
53
137
  * mapa usado al escribir el authorization model del store.
54
138
  */
55
139
  holderTypes: HolderTypeMap;
56
- }
57
- export interface ImportFactsResult {
58
- assignments: number;
59
- denies: number;
60
- skippedExpired: number;
61
- dryRun: boolean;
140
+ /**
141
+ * Dónde avisar de lo que el driver ve y no puede representar (bindings que
142
+ * no entiende). Inyectado y síncrono a propósito: el logger de la app en
143
+ * producción, la consola por defecto, un array en los tests.
144
+ */
145
+ logger?: {
146
+ warn(message: string): void;
147
+ };
148
+ /**
149
+ * Deadline de cada llamada (catálogo SQL y FGA) en ms, default 5000.
150
+ * Vencido ⇒ 503 `E_AUTHZ_BACKEND_TIMEOUT` (L0.13).
151
+ */
152
+ timeoutMs?: number;
153
+ /**
154
+ * Consistencia pedida al servidor en cada lectura. Default
155
+ * `higher_consistency`: con la caché de Check activada en el servidor
156
+ * (`--check-query-cache-enabled`, TTL 10 s) un revoke o un deny recién
157
+ * escritos seguirían concediendo hasta que expire; el paquete promete que
158
+ * "quitar el deny restaura" y lo garantiza él (S11). `minimize_latency` es
159
+ * el opt-out explícito: "acepto hasta N segundos de fail-open a cambio de
160
+ * latencia".
161
+ */
162
+ consistency?: 'higher_consistency' | 'minimize_latency';
163
+ /**
164
+ * Reintentos del SDK ante errores de red. Default `{ maxRetry: 0 }`: el
165
+ * paquete libera al llamante con 503 al vencer `timeoutMs`, y un SDK que
166
+ * siguiera reintentando en segundo plano haría aterrizar la escritura
167
+ * DESPUÉS del error, sin evento `onWrite` — una escritura fantasma (D2).
168
+ * Con reintentos, el llamante recibe además `indeterminate: true` en el
169
+ * hook cuando la escritura vence el deadline; activarlos es aceptar que un
170
+ * 503 por timeout puede haber escrito.
171
+ */
172
+ retryParams?: {
173
+ maxRetry?: number;
174
+ minWaitInMs?: number;
175
+ };
176
+ /**
177
+ * Memo del catálogo compartido con otro driver del mismo proceso (2A). Si
178
+ * se omite, el driver construye el suyo. Se revalida contra la versión
179
+ * compartida de la base (`authz_catalog_version`) según `catalogRevalidate`.
180
+ */
181
+ catalog?: CatalogCache;
182
+ /**
183
+ * Cuándo contrastar el memo con la versión compartida (2D · F1): `'always'`
184
+ * (default; un SELECT por clave primaria por pregunta) o `{ everyMs }`
185
+ * (opt-in: ventana acotada en la que un sync de OTRO proceso aún no se ve).
186
+ * Solo aplica al memo que construye este driver.
187
+ */
188
+ catalogRevalidate?: CatalogRevalidate;
189
+ /**
190
+ * Reloj de pared del driver (2.5 · J1): el `current_time` de cada check
191
+ * (la condición `not_expired` se evalúa contra él), el filtro de caducidad
192
+ * en cliente de las enumeraciones y los tres estados del re-grant. Default
193
+ * `() => new Date()`. Inyectable para fijar el instante en tests; en
194
+ * producción lo normal es `clock` en el config del manager (`withClock`).
195
+ */
196
+ now?: Clock;
197
+ /**
198
+ * La outbox del árbol del config (`scopes.outbox`), la MISMA instancia
199
+ * (3b-2d). El driver no la usa para nada: quien encola es el manager. Está
200
+ * aquí como EVIDENCIA del gate — un driver `facts` que se construye sin
201
+ * ella y sin `acceptScopeDriftRisk` es un montaje en el que un `rollback`
202
+ * del consumidor deja una escalada persistente e invisible (cruce 4 · S5),
203
+ * y eso se descubre al construir, no en la primera escritura de un tenant.
204
+ */
205
+ outbox?: ScopeOutbox;
206
+ /**
207
+ * «Sé que sin outbox un rollback de mi transacción deja el árbol de FGA
208
+ * adelantado al mío, y lo asumo». Es la salida explícita del gate para
209
+ * quien mueve el árbol solo desde la plataforma, en un proceso que no
210
+ * comparte transacción con nada. Tiene que ser el booleano `true`: un
211
+ * valor «truthy» no es una aceptación.
212
+ */
213
+ acceptScopeDriftRisk?: boolean;
62
214
  }
63
215
  /**
64
- * Migración de hechos database openfga: copia las asignaciones vigentes y
65
- * los denies de las tablas `authz_*` como tuples del store FGA.
216
+ * **El gate de construcción de `facts`** (3b-2d, panel 2 cruce 4 · S5).
66
217
  *
67
- * - COPIA, no mueve: las tablas locales quedan intactas el rollback es
68
- * volver a AUTHZ_DRIVER=database (solo se pierde lo escrito mientras se
69
- * operó con openfga). El catálogo y la jerarquía nunca migran: son
70
- * metadata local para ambos drivers.
71
- * - Idempotente: re-ejecutar no duplica (onDuplicateWrites: Ignore).
72
- * - Las asignaciones ya expiradas se saltan (no tiene sentido copiarlas);
73
- * las de expiración futura viajan con la condition `not_expired`.
218
+ * En `hierarchy: 'facts'` el árbol vive en el store de FGA y FGA es el PDP.
219
+ * El consumidor notifica `scopes.moved` dentro de su transacción, el paquete
220
+ * escribe la arista en FGA… y si esa transacción hace `rollback` —una
221
+ * constraint, una validación, un timeout de pool: no hace falta un crash—
222
+ * la escritura de FGA NO se deshace. SQL sigue diciendo que la unit es del
223
+ * tenant A y FGA que es del B: todos los holders con rol en B tienen acceso
224
+ * a una unidad de A, y la aplicación, que lista y audita contra SQL, no
225
+ * puede verlo. La ventana no es un hueco entre dos operaciones: dura hasta
226
+ * que alguien lo descubra.
227
+ *
228
+ * Por eso `scopes.outbox` no puede ser una recomendación: una recomendación
229
+ * no es un mecanismo. O está el puerto, o está la firma del dueño.
230
+ */
231
+ export declare function assertScopeDriftGuarded(options: {
232
+ outbox?: ScopeOutbox;
233
+ acceptScopeDriftRisk?: boolean;
234
+ }): void;
235
+ export declare const DEFAULT_TIMEOUT_MS = 5000;
236
+ /**
237
+ * Cota de páginas de una enumeración (1.000.000 de tuplas a 100 por página).
238
+ * Un `continuation_token` que no avanza —un servidor roto, un proxy o una
239
+ * caché delante— era un bucle infinito que ningún deadline cortaba, porque
240
+ * el deadline es por llamada (D12, auditor H7).
74
241
  */
75
- export declare function importAuthzFactsToOpenFga(options: OpenFgaDriverOptions & {
76
- dryRun?: boolean;
77
- }): Promise<ImportFactsResult>;
242
+ export declare const MAX_READ_PAGES = 10000;
243
+ /**
244
+ * Tope de saltos al subir el árbol DEL STORE (3b-2e · E1). No es el techo de
245
+ * decisión —ese lo pone el servidor al evaluar (c2) y está medido en
246
+ * `FACTS_MAX_RESOLVE_DEPTH`—: es la red del recorrido, para que un árbol con
247
+ * una deriva que el anti-ciclos no vio no deje el proceso dando vueltas.
248
+ */
249
+ export declare const MAX_SCOPE_CHAIN_HOPS = 1000;
78
250
  export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
251
+ /**
252
+ * Lo que este driver declara (3b-2e · E2). Depende del MODO, así que es un
253
+ * getter y no un campo: una vista por prototipo (`withChainResolver`,
254
+ * `withClock`) declara lo mismo que su original.
255
+ *
256
+ * `roleInheritanceNative` y `listObjectsInherited` son `false` **también en
257
+ * `facts`**, y eso es el cruce 6 del panel: `hasRole`/`listRoles`/
258
+ * `listRoleScopes`/`listSubjects`/`listScopes` siguen usando `resolveChain`
259
+ * (en (c2) no hay alternativa, y está medido), y los `list*` enumeran con
260
+ * `Read` paginado, nunca con `ListObjects` (que trunca al tope del servidor
261
+ * sin señal). Lo único que `facts` cambia es `authorize`.
262
+ */
263
+ get capabilities(): Readonly<{
264
+ hierarchyFacts: true;
265
+ singleCheckAuthorize: true;
266
+ roleInheritanceNative: false;
267
+ listObjectsInherited: false;
268
+ purgeRole: true;
269
+ countRoleAssignments: true;
270
+ canonicalScopeReads: false;
271
+ enumerateFacts: true;
272
+ }>;
79
273
  private client;
80
- private resolveAncestors;
274
+ private chainResolver;
81
275
  private holderTypes;
276
+ /**
277
+ * Contadores observables del driver. `unparseableBindings`: ids del store
278
+ * que el motor no entiende (L0.16). Cada uno es un hecho que las
279
+ * enumeraciones NO muestran; se registra y se cuenta, jamás un `continue`
280
+ * mudo — quien opera el store tiene que poder verlo.
281
+ */
282
+ readonly diagnostics: {
283
+ unparseableBindings: number;
284
+ };
285
+ private logger;
286
+ private timeoutMs;
287
+ private consistency;
288
+ /** Reloj de pared del driver (J1): el ÚNICO `now` de checks, filtros y re-grant. */
289
+ private now;
290
+ /**
291
+ * Memo del catálogo (2A): permisos, roles por nivel y roles que conceden
292
+ * cada permiso se leen de aquí en el camino caliente; antes eran dos
293
+ * consultas SQL por `authorize`. Los hechos siguen en FGA en cada pregunta.
294
+ * Se revalida contra `authz_catalog_version` (2D · F1): cada operación
295
+ * toma la foto UNA vez (`view()`) y lee de ella todo lo que necesita.
296
+ * `catalog.invalidate()` fuerza la recarga de ESTE memo.
297
+ */
298
+ readonly catalog: CatalogCache;
82
299
  constructor(options: OpenFgaDriverOptions);
300
+ /**
301
+ * `[scope canónico, ...ancestros]`, o `null` si el scope no existe
302
+ * (lecturas: denegar). `chain[0]` —la fila del consumidor, no lo que
303
+ * escribió el llamante— es el scope que va en la clave de cada binding
304
+ * (2.5-B · K1): un alias del uuid que el árbol funde con la fila real
305
+ * llega aquí ya canónico y el `deny_binding` escrito canónico casa.
306
+ */
83
307
  private chain;
308
+ /** La cadena o 422: una escritura no puede ir a un scope que nadie reconoce. */
309
+ private knownScope;
310
+ /** El scope canónico para `scopes.detached`/`purgeScope` (ver `canonicalScope`). */
311
+ private canonicalOrSelf;
312
+ /** Los destinos de un delete de hechos (`revoke`/`removeDeny`): canónico, o el fan-out de alias (3b-8 · A4). */
313
+ private canonicalTargets;
314
+ /**
315
+ * Vista de este driver con OTRO resolutor de ancestros y el mismo estado
316
+ * (cliente, memo del catálogo, deadline, diagnósticos). Es lo que usa
317
+ * `AuthorizationManager.forRequest()` para leer con un resolutor memoizado
318
+ * sin tocar el driver compartido: hereda por prototipo y solo sobrescribe
319
+ * el resolutor.
320
+ */
321
+ withChainResolver(resolveChain: ScopeChainResolver): AuthorizationDriver;
322
+ /**
323
+ * Vista de este driver con OTRO reloj de pared (2.5 · J1): mismo cliente,
324
+ * store, memo y resolutor; solo cambia el `now` que viaja como
325
+ * `current_time` y filtra las enumeraciones. Lo aplica el manager con
326
+ * `config.clock` y el juez para fijar el instante.
327
+ */
328
+ withClock(now: Clock): AuthorizationDriver;
329
+ /**
330
+ * El `context` de los checks de UNA operación, con el reloj de ESTE driver
331
+ * (o vista): se construye una vez por operación y viaja en todos sus
332
+ * checks (2.5-B · K9). Antes se leía el reloj por check y un mismo
333
+ * `authorize` evaluaba el deny en un instante y el rol en otro.
334
+ */
335
+ private checkContext;
336
+ /**
337
+ * Consulta al catálogo local clasificando su fallo. Con este driver el
338
+ * catálogo SQL sigue siendo una dependencia dura de cada pregunta: su caída
339
+ * era un error crudo de Lucid que se presentaba como bug de aplicación (N3).
340
+ */
341
+ private sql;
84
342
  private fgaSubject;
85
343
  /**
86
- * batchCheck troceado al límite del servidor FGA (50 checks/request) y con
87
- * verificación de completitud: TODOS los checks deben volver respondidos.
344
+ * Un batchCheck con TODOS los checks (el SDK trocea a 50 por request y
345
+ * paraleliza), cada uno con un `correlationId` propio, y la respuesta
346
+ * alineada por ese id: un resultado por check, ni uno más ni uno menos.
88
347
  */
89
348
  private batchCheckAll;
90
349
  private findPermission;
91
- private findRoleOrFail;
92
- /** Roles del catálogo que conceden el permiso, agrupados por scope_type. */
93
- private rolesGranting;
350
+ /**
351
+ * Filtro por catálogo de las lecturas de membresía (`listRoles`,
352
+ * `listRoleScopes`, `rolesInChain`, `listScopes`): un binding cuyo uuid ya
353
+ * no está en `authz_roles` —o está, pero declarado para OTRO nivel, o es
354
+ * local a un scope que no está en la cadena del binding (3B · B2)— es una
355
+ * tupla huérfana, no una membresía; igual que en `database`, donde el
356
+ * catálogo lo excluye (D5). La tupla la recoge `authz:reconcile`. Desde 3A
357
+ * la resolución es por uuid (`roleByUuid`), nunca por slug. `chainKeys` es
358
+ * la cadena del scope del BINDING (desde él hacia la raíz).
359
+ */
360
+ private declaredRole;
94
361
  authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
95
- grant(subject: SubjectRef, role: string, scope: ScopeRef, options?: GrantOptions): Promise<void>;
362
+ /**
363
+ * **`authorize` del modo `facts` (3b-2c): UN solo `Check`.**
364
+ *
365
+ * `can_<P>` sobre `scope:<key>` con el subject como user. El modelo (c2)
366
+ * ya lleva dentro las tres cosas que el modo `resolver` compone aquí:
367
+ * la herencia hacia abajo (`<P> from parent`), el deny explícito heredado
368
+ * (`denied_<P> from parent`) y la resta que hace ganar al deny
369
+ * (`can_<P> = <P> but not denied_<P>`). No hay `batchCheck`, no se expande
370
+ * la cadena y **no se llama al resolutor del consumidor** (cruce 6 del
371
+ * panel 2, que es también el literal que el README puede prometer).
372
+ *
373
+ * Lo único local que queda es el MEMO del catálogo, y es OBLIGATORIO: lo
374
+ * comprueba el llamante antes de llegar aquí. Sin esa guardia un permiso
375
+ * desconocido sería un `Check` de una relación que el modelo no declara —
376
+ * un 400 del servidor que saldría como 503— en vez del `false` que exige el
377
+ * invariante 5.
378
+ *
379
+ * Un scope que el árbol del consumidor no conoce no tiene tuplas en el
380
+ * store: responde `false` sin preguntar por él (invariante 9), pero aquí
381
+ * eso lo decide el propio store, no `resolveChain`. Cualquier fallo del
382
+ * backend sale como 503 desde el cliente envuelto; jamás un `false` mudo.
383
+ */
384
+ private factsAuthorize;
385
+ /**
386
+ * `authorize` sobre N scopes con UN batchCheck (2.1, B6): los checks de
387
+ * todas las cadenas viajan juntos (el SDK trocea a 50 y paraleliza) y se
388
+ * atribuyen a su scope por posición dentro del lote correlacionado
389
+ * (L0.14). Misma regla que `authorize`, por scope: `error` en cualquier
390
+ * check ⇒ 503 entero (D1); deny `allowed` ⇒ false; rol `allowed` ⇒ true.
391
+ * Scope desconocido o sin rol que conceda ⇒ false sin checks.
392
+ */
393
+ authorizeMany(subject: SubjectRef, permission: string, scopes: ScopeRef[]): Promise<boolean[]>;
394
+ /**
395
+ * `authorizeMany` del modo `facts` (3b-2c): **UN `batchCheck` de N items**,
396
+ * uno por scope DISTINTO. En el modo `resolver` cada scope aporta los
397
+ * denies de su cadena más un check por (nivel, rol que concede): el lote
398
+ * crecía como N×M. Aquí cada scope es exactamente una pregunta,
399
+ * `can_<P>@scope:<key>`, y un scope repetido comparte item y respuesta
400
+ * (G2, CR9) en vez de duplicar el lote.
401
+ *
402
+ * Un `error` en cualquier check sigue siendo 503 entero (invariante 5, D1):
403
+ * lo lanza `batchCheckAll` antes de mirar un solo `allowed`.
404
+ */
405
+ private factsAuthorizeMany;
406
+ grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
407
+ /**
408
+ * Write directo de la asignación CON su estructura (3b-2f · R3); si algo ya
409
+ * estaba, camino largo. Devuelve si existía la ASIGNACIÓN —lo dice la
410
+ * relectura, no el error: el choque puede ser de las aristas, que en (c2)
411
+ * las comparten todos los holders del mismo rol en el mismo scope—.
412
+ * Cualquier otro fallo se propaga tal cual (ya clasificado).
413
+ */
414
+ private writeAssignment;
415
+ /**
416
+ * delete + write (dos llamadas: FGA no admite ambas sobre la misma key en
417
+ * una). El write repone la ESTRUCTURA junto a la asignación: si un
418
+ * `purgeScope` concurrente se llevó la arista `scope#binding` entre medias,
419
+ * lo que queda vuelve a ser coherente en vez de una asignación inerte que
420
+ * `listRoles` ve y `authorize` no (3b-2f · R3).
421
+ */
422
+ private replaceAssignment;
96
423
  /**
97
424
  * Estado actual de una asignación, con TRES resultados posibles y no dos.
98
425
  *
99
426
  * Distinguir `unknown` de `present` sin condición es lo que impide un bug
100
427
  * feo: si un fallo de lectura se pareciera a "existe y sin expiración", un
101
- * grant sin expiración saldría por el atajo del no-op y se perdería
102
- * mientras el hook onWrite ya habría auditado que se concedió.
428
+ * grant sin expiración saldría por el atajo del no-op y se perdería. El
429
+ * error de lectura (ya clasificado como 503) viaja con el resultado para
430
+ * que quien no pueda seguir sin él lo propague.
103
431
  */
104
432
  private readAssignment;
105
- revoke(subject: SubjectRef, role: string, scope: ScopeRef): Promise<void>;
106
- hasRole(subject: SubjectRef, role: string, scope: ScopeRef): Promise<boolean>;
433
+ revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<void>;
434
+ hasRole(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<boolean>;
107
435
  deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
108
436
  removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
109
- listSubjects(role: string, scope: ScopeRef): Promise<SubjectRef[]>;
110
- /** role_binding ids del subject (asignaciones directas vigentes). */
437
+ /**
438
+ * El hecho de un deny (3b-2c): una relación DEL SCOPE,
439
+ * `scope:<key>#denied_<P>@<holder>`. Así el modelo lo hereda hacia abajo
440
+ * por `parent` y `can_<P>` puede restarlo dentro del mismo `Check`
441
+ * (invariante 2) sin que el paquete pasee la cadena. Hasta 3b-2k · K2 el
442
+ * modo `resolver` lo guardaba en un objeto propio
443
+ * (`deny_binding:<scopeKey>|<permissionUuid>`) y el paquete expandía la
444
+ * cadena a un check por nivel; ese tipo se borró con el modo.
445
+ *
446
+ * La relación lleva el SLUG proyectado (no el uuid) porque el modelo la
447
+ * declara por nombre. El slug que llega aquí es el del catálogo: el
448
+ * llamante ya pasó por `findPermission`, que es quien decide qué existe.
449
+ */
450
+ private denyTuple;
451
+ /**
452
+ * Holders con asignación vigente del rol en el scope exacto: `Read` por
453
+ * objeto exacto, paginado, con la caducidad filtrada en cliente. Antes era
454
+ * `ListUsers`, que trunca al tope del servidor sin señal (L0.7).
455
+ */
456
+ listSubjects(role: RoleQuery, scope: ScopeRef): Promise<SubjectRef[]>;
457
+ /**
458
+ * Bindings del subject ya parseados (asignaciones directas vigentes). Los
459
+ * ids que no se entienden se registran y se cuentan, no se descartan.
460
+ */
111
461
  private listBindings;
462
+ /** Scopes (por clave) donde el subject tiene un deny directo del permiso (por su uuid). */
463
+ private deniedScopeKeys;
464
+ /**
465
+ * Los denies DIRECTOS del holder en el modo `facts`, ya traducidos a
466
+ * `(scope, permiso)`. No hay `deny_binding` que enumerar: se leen de una
467
+ * pasada las tuplas del holder sobre objetos `scope:` y se quedan las de la
468
+ * familia `denied_<P>`. La vuelta de relación a slug la da el CATÁLOGO —una
469
+ * relación que ya no declara ningún permiso no es un deny (D5), igual que
470
+ * un `deny_binding` de un permiso retirado—, y una clave de scope que el
471
+ * motor no entiende se cuenta y se registra, nunca se descarta en silencio
472
+ * (L0.16).
473
+ */
474
+ private factsDenies;
475
+ private parseBindings;
476
+ private warn;
112
477
  listRoles(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
478
+ /**
479
+ * Roles directos vigentes del holder en cada scope de la cadena (2D · G5):
480
+ * UNA lectura (`Read` paginado de sus bindings) agrupada por scope, en vez
481
+ * de un `listRoles` por nivel. Solo roles que el catálogo declara para ese
482
+ * nivel (D5). La cadena viene ya resuelta por el manager.
483
+ */
484
+ rolesInChain(subject: SubjectRef, chain: ScopeRef[]): Promise<Array<{
485
+ scope: ScopeRef;
486
+ role: CatalogRoleRef;
487
+ }>>;
113
488
  listRoleScopes(subject: SubjectRef, scopeType: ScopeType): Promise<ScopeRef[]>;
114
489
  listScopes(subject: SubjectRef, permission: string): Promise<ScopeRef[]>;
490
+ /**
491
+ * Denies directos del holder (2.1, B5): `Read` paginado de sus relaciones
492
+ * `denied_<P>` sobre objetos `scope:` (nunca ListObjects, L0.7), filtrados
493
+ * por el catálogo (un permiso retirado no es un deny, D5), por scope exacto
494
+ * si se pide, y por scopes que el árbol conoce (D8).
495
+ */
496
+ listDenies(subject: SubjectRef, scope?: ScopeRef): Promise<DenyRef[]>;
497
+ /**
498
+ * Un nodo tiene como mucho UN padre. El paquete nunca escribe dos (cada
499
+ * `moved` sustituye la arista entera dentro de un `Write`), así que dos
500
+ * padres son DERIVA: alguien más escribe en el store, y mientras tanto la
501
+ * herencia está trayendo hechos de dos ramas. Se lanza; no se "arregla",
502
+ * porque elegir cuál sobrevive sería adivinar cuál de las dos concesiones
503
+ * vivas es la buena (cruce 8: «si devuelve >1 ⇒ drift ⇒ lanza»).
504
+ */
505
+ private assertOneParent;
506
+ /**
507
+ * El consumidor colgó un scope nuevo (o recolgó uno que ya existía: un
508
+ * `attach` sobre un nodo conocido ES un move). En modo `facts` eso es UNA
509
+ * tupla `scope:<hijo>#parent@scope:<padre>`; en modo `resolver` no es nada
510
+ * (la jerarquía la resuelve el paquete en cada pregunta).
511
+ */
512
+ onScopeAttached(child: ScopeRef, parent: ScopeRef): Promise<void>;
513
+ /**
514
+ * El consumidor movió un scope. Procedimiento fijado en el cruce 8 del
515
+ * panel 2: un `Read` del padre actual —obligatorio, porque FGA rechaza
516
+ * borrar una tupla inexistente—, la cadena del padre NUEVO en el store
517
+ * (3b-8 · A5: el anti-ciclos del consumidor no ve un store
518
+ * desincronizado) y **UN solo `Write`** con el delete del padre viejo y el
519
+ * write del nuevo, que es atómico dentro de la request. O(profundidad)
520
+ * requests, UNA mutación.
521
+ */
522
+ onScopeMoved(child: ScopeRef, newParent: ScopeRef): Promise<void>;
523
+ /**
524
+ * **Anti-ciclos, en el PAQUETE y antes de escribir** (cruce 3 del panel 2,
525
+ * bloqueante S2). Medido contra OpenFGA v1.19: el servidor ACEPTA una
526
+ * arista que cierra un ciclo, no se cuelga, responde en 2-7 ms y la
527
+ * herencia se vuelve bidireccional —un grant en un descendiente concede en
528
+ * el ancestro, y con la raíz dentro del ciclo concede en todo el store—.
529
+ * Fail-open mudo: no hay nada que capturar. La suite lo reproduce contra el
530
+ * `:8101` para que nadie proponga delegar esto en el backend.
531
+ *
532
+ * Las tres validaciones del cruce 8, en orden y sin escribir nada si
533
+ * fallan: (i) la raíz no cuelga de nadie; (ii) el padre EXISTE según el
534
+ * árbol del consumidor; (iii) el hijo no es ancestro-o-igual del padre.
535
+ * Las mismas que hace `AuthorizationManager.#assertEdge`: aquí se repiten
536
+ * por defensa en profundidad, porque `manager.driver()` es la salida
537
+ * documentada de todas las barreras del paquete.
538
+ *
539
+ * Devuelve las dos claves CANÓNICAS (invariante 17): un alias del uuid ni
540
+ * evade la comprobación de ciclo ni abre una segunda rama en el store.
541
+ */
542
+ private assertEdge;
543
+ /**
544
+ * El consumidor sacó un scope del árbol. En modo `facts` se borra su
545
+ * arista `#parent` — y **la arista es lo ÚLTIMO** (S6, cruce 9): el
546
+ * manager llama primero a `purgeScope`, que borra los hechos del scope y
547
+ * DEMUESTRA cero o lanza (invariante 11).
548
+ *
549
+ * **El motivo del orden cambió con (c2r) y el orden NO** (3b-2i). La razón
550
+ * que se escribió en 3b-2b —«un scope sin ancestro dejaría de heredar los
551
+ * denies del padre y sus permisos serían INDENEGABLES»— ya no es cierta:
552
+ * ése era exactamente el 🔴 1 del auditor R2 y hoy un scope que no alcanza
553
+ * `app` no concede nada (`can_<P>` exige `rooted`). Lo que sigue justificando
554
+ * el orden es lo otro: una purga que muere a medias tiene que dejar denies
555
+ * de MÁS, nunca de menos, y borrar la arista antes convertiría el fallo en
556
+ * «se quedaron hechos vivos en un nodo que ya nadie purga» (los recoge
557
+ * `authz:reconcile`, pero mientras tanto el nodo es invisible para el
558
+ * árbol). Con la arista al final, una purga fallida se reintenta.
559
+ *
560
+ * No se tocan las aristas de los HIJOS (`scope:<hijo>#parent@scope:<este>`):
561
+ * el consumidor notifica un `detached` por nodo, o un `moved` para
562
+ * recolgarlos — y **desde (c2r) esos hijos, mientras tanto, DENIEGAN** (su
563
+ * cadena ya no llega a la raíz) en vez de conceder de más. Lo que quede sin
564
+ * nodo arriba lo ve `authz:reconcile` (3b-3).
565
+ */
566
+ onScopeDetached(child: ScopeRef): Promise<void>;
567
+ /**
568
+ * Escribe la arista del árbol dejando UNA sola: se lee la que hay y se
569
+ * sustituye en el mismo `Write`. Sin diferencia no se llama al servidor
570
+ * (invariante 6: re-anexar al mismo padre es un no-op seguro, y además
571
+ * escribir una tupla que ya está sería un conflicto con los defaults
572
+ * estrictos del SDK).
573
+ *
574
+ * **Y el choque con otro escritor del árbol no es una caída** (3b-2h ·
575
+ * 🟠 4, invariante 6). Medido contra el `:8101`: dos `attached` del mismo
576
+ * nodo al mismo padre a la vez —lo que hacen dos pasadas del relay sobre el
577
+ * mismo lote, porque `pending()` no reserva nada— y el perdedor se llevaba
578
+ * un **503 «el backend no respondió»**, cuando el backend respondió
579
+ * perfectamente («cannot write a tuple which already exists»). Aquí se hace
580
+ * lo que el invariante 6 manda desde `grant`: releer y re-aplicar sobre lo
581
+ * que quedó —así el re-intento ve la arista del otro y sale por el no-op—,
582
+ * y una contención que no cede en `TREE_WRITE_ATTEMPTS` vueltas sale como
583
+ * 409 `E_AUTHZ_WRITE_CONFLICT`, nunca como un 503.
584
+ *
585
+ * Esto NO convierte dos escritores en uno: dos `attached` del mismo nodo a
586
+ * padres DISTINTOS siguen pudiendo dejar dos aristas (FGA no tiene
587
+ * compare-and-set y el `Read` de arriba es un check-then-write). Lo que
588
+ * impide esa carrera es el ESCRITOR ÚNICO del relay (`ScopeOutbox.acquire`).
589
+ */
590
+ private reparent;
591
+ /**
592
+ * **El barrido del rol local** (3b-2e · E1; decisión del dueño del
593
+ * 2026-08-30, opción 1).
594
+ *
595
+ * En (c2) el modelo no tiene `owner`, así que `authorize` NO vuelve a
596
+ * decidir con el árbol de hoy si un rol LOCAL sigue siendo visible: un
597
+ * `role_binding` concede mientras su scope alcance al que pregunta. Sin
598
+ * esto, mover una unit fuera de la organización dueña de un rol local
599
+ * dejaría de retirar lo concedido (el invariante 18 en `database`), que es
600
+ * un **fail-open** — y encima uno que solo se ve comparando drivers.
601
+ *
602
+ * Lo que se toca es la arista `scope#binding`, que es lo que hace
603
+ * ALCANZABLE la asignación: se BORRA donde el owner del rol ya no está en
604
+ * la cadena y se REESCRIBE donde vuelve a estarlo (invariante 18: volver la
605
+ * unit a su sitio restaura). No se toca el `assignee` —el hecho de la
606
+ * asignación no cambia porque el árbol se mueva, igual que en `database`—
607
+ * ni nada de un rol GLOBAL, cuya visibilidad no depende del árbol.
608
+ *
609
+ * **Por subárbol, no por nodo** (consecuencia 2): los descendientes del
610
+ * nodo movido también cambian de cadena.
611
+ *
612
+ * Coste: si el catálogo no tiene NI UN rol local —el caso de todo consumidor
613
+ * que no usa delegación— son **cero** requests y `moved` sigue siendo el
614
+ * `Read` + `Write` del cruce 8. Con roles locales: una lectura por rol local
615
+ * (sus bindings, por `role_binding#role`), la bajada del subárbol y un
616
+ * `Write` por lote.
617
+ */
618
+ private sweepLocalRoleBindings;
619
+ /**
620
+ * **El barrido por NIVEL** (3b-2g · R1; decisión del dueño del 2026-08-30
621
+ * (2), mismo mecanismo que E1).
622
+ *
623
+ * El modelo (c2) tampoco lleva el NIVEL (`scope_type`) del rol: la
624
+ * proyección dice qué permisos vincula, no en qué nivel se declara. Sin
625
+ * esto, cambiar el `scope_type` de un rol retira lo concedido en `database`
626
+ * —donde `declaredRoleAt` se evalúa en cada pregunta— y **sigue
627
+ * concediendo** en `facts`, que es la divergencia R1 del lote 2e.
628
+ *
629
+ * Se cierra igual que el owner: barriendo la arista `scope#binding` de los
630
+ * bindings de ESE rol con la regla única de visibilidad. Lo llama
631
+ * `projectCatalogRole`, que es el hook de «una escritura de catálogo cambió
632
+ * este rol»: el manager lo dispara tras `defineScopedRole`/`updateScopedRole`
633
+ * y un escritor «a mano» de `authz_*` tiene el mismo deber que ya tenía con
634
+ * el espejo de permisos (sin él, en `facts` un rol recién definido no
635
+ * concedería nada y quitarle un permiso seguiría concediéndolo).
636
+ *
637
+ * Coste: una lectura (los bindings del rol) y, **solo si el rol es LOCAL y
638
+ * tiene bindings**, la cadena del store de cada scope distinto donde cuelga
639
+ * uno; un `Write` por lote si hay algo que barrer. Un rol sin bindings —el
640
+ * caso de todo `defineScopedRole`— son 0 escrituras.
641
+ */
642
+ private sweepRoleVisibility;
643
+ /**
644
+ * La arista `scope#binding` de un binding, a escribir o a borrar según la
645
+ * **regla única de visibilidad** (`declaredRoleAt`, la misma que evalúa
646
+ * `database` en cada pregunta): el rol tiene que estar declarado para el
647
+ * NIVEL de ese scope (3b-2g · R1) y ser global o tener a su owner en la
648
+ * cadena (3b-2e · E1). Visible ⇒ la arista se (re)escribe; no visible ⇒ se
649
+ * borra. El `assignee` no se toca: la asignación existe igual, lo que
650
+ * cambia es dónde se la ve.
651
+ */
652
+ private classifyBindingEdge;
653
+ /**
654
+ * Aplica el barrido en lotes ≤ 100 (el límite del `Write`).
655
+ *
656
+ * `Ignore` en las dos direcciones: el barrido dice el estado que DEBE
657
+ * quedar, no el delta — borrar lo que ya no está y reescribir lo que ya
658
+ * estaba son no-ops, no errores (invariante 6).
659
+ */
660
+ private applyBindingSweep;
661
+ /**
662
+ * `[key, ...ancestros]` según el ÁRBOL DEL STORE (3b-2e · E1), subiendo por
663
+ * `scope#parent`. Es la cadena con la que FGA va a decidir, que es la que
664
+ * tiene que gobernar el barrido; el resolutor del consumidor no participa.
665
+ * Un nodo con más de un padre es deriva y se dice (`assertOneParent`), y el
666
+ * recorrido está acotado por el mismo tope de páginas que las
667
+ * enumeraciones: un ciclo escrito a mano no cuelga el proceso.
668
+ */
669
+ private storeChain;
670
+ /**
671
+ * Purga del scope exacto en FGA (N7, S6, B2). No hay "borrar todo lo de
672
+ * este objeto": se leen por objeto EXACTO los bindings posibles — un
673
+ * `role_binding` por cada rol del catálogo de ese `scope_type` — más el
674
+ * objeto `scope:<key>` (donde viven los `denied_<P>` y el `#binding`),
675
+ * paginando `Read` (nunca ListObjects: trunca sin avisar, L0.7), se borra
676
+ * en lotes ≤ 100 (límite del Write) y se vuelve a leer cada objeto: si
677
+ * queda algo, se lanza. Un rol retirado del catálogo deja bindings
678
+ * inalcanzables por esta vía; es el precio de no tener un índice por
679
+ * objeto, y lo vigilará `authz:reconcile` (3b).
680
+ */
681
+ purgeScope(purged: ScopeRef): Promise<void>;
682
+ /**
683
+ * La **proyección derivada del catálogo** en el store (3b-2a · A5; regla
684
+ * del catálogo reescrita, panel 2 cruce 7). Se pasa a `syncAuthzCatalog`,
685
+ * que la usa en dos momentos: comprueba que el catálogo que va a quedar es
686
+ * publicable (cotas de nombre y techo del modelo) ANTES de escribir, y
687
+ * rehace las tuplas `role:<uuid>#permits_<P>@<holder>:*` con el catálogo ya
688
+ * confirmado.
689
+ *
690
+ * Sigue sin ser el catálogo: es un espejo reconstruible que ningún camino
691
+ * de LECTURA de este driver consulta para responder qué permisos tiene un
692
+ * rol (A6). Quien decide es `authz_*` a través del memo.
693
+ */
694
+ catalogProjection(): CatalogProjection;
695
+ /**
696
+ * **El marcador de raíz de (c2r)** (3b-2i): `scope:app#rooted@<holder>:*`,
697
+ * una tupla por holder type en todo el store y CERO por scope.
698
+ *
699
+ * Va aquí —en la proyección del catálogo, o sea en cada `syncAuthzCatalog`—
700
+ * y no en `attached`, porque el evento que hace falta cubrir es **añadir un
701
+ * holderType al `config`**: sin esto ese holder denegaría en TODO el store
702
+ * aunque el modelo se haya republicado, que es la única forma realista de
703
+ * quedarse sin marcador en un store vivo. Es idempotente: un `Read` y, solo
704
+ * si falta algo, un `Write` con lo que falta (0 escrituras en el caso
705
+ * normal).
706
+ *
707
+ * Solo en modo `facts`: el modelo del modo `resolver` no declara `rooted` y
708
+ * escribirlo sería un 400 del servidor.
709
+ *
710
+ * ⚠️ Sin marcador el store entero DENIEGA (fail-closed, medido). Por eso se
711
+ * repone en cada sync y `authz:reconcile` (3b-3) tiene el deber escrito de
712
+ * reportarlo como deriva cuando falte.
713
+ */
714
+ private ensureFactsRoot;
715
+ /**
716
+ * Espeja los vínculos rol→permiso: escribe lo que falta y BORRA lo que
717
+ * sobra, en UN `Write` por lote con deletes y writes juntos (cruce 8:
718
+ * queda prohibido el patrón `deleteTuples()` + `writeTuples()`, que no es
719
+ * atómico). Con (c2) quitar un permiso de un rol son tantos deletes como
720
+ * holders y ninguna reescritura del modelo.
721
+ *
722
+ * Sin diferencias no se llama al servidor: un `sync` que no cambió el
723
+ * catálogo escribe CERO tuplas.
724
+ */
725
+ private projectCatalog;
726
+ /**
727
+ * **Purga un ROL con sus hechos** (3b-2e · E4; hasta aquí este driver no lo
728
+ * traía y por eso `defineScopedRole` era 500 `E_AUTHZ_UNSUPPORTED` antes de
729
+ * escribir, 3E · P4).
730
+ *
731
+ * Lo que lo hace posible es (c2): el binding APUNTA A SU ROL
732
+ * (`role_binding:…#role@role:<uuid>`), así que los bindings de un rol se
733
+ * enumeran filtrando por `user` — y con la arista `scope#binding` se sabe de
734
+ * qué scope cuelga cada uno. En el modo `resolver` esas dos aristas no
735
+ * existen y el método TAMPOCO: el constructor lo retira (el manager lo lee
736
+ * como «no sé purgar» y se niega antes de escribir).
737
+ *
738
+ * Orden: **hechos primero, catálogo después** (el mismo de `detached`, S6).
739
+ * No hay transacción que abarque FGA y SQL, así que lo que se garantiza es
740
+ * la dirección segura: mientras la fila del rol siga viva, lo que quede en
741
+ * el store es visible y reintentable; al revés quedarían hechos huérfanos
742
+ * que resucitarían al recrear el slug. Y se DEMUESTRA cero (invariante 11):
743
+ * si algo sobrevive, 500 `E_AUTHZ_PURGE_INCOMPLETE` y el catálogo no se
744
+ * toca.
745
+ */
746
+ purgeRole(roleUuid: string): Promise<void>;
747
+ /**
748
+ * Cuántos hechos VIGENTES tiene cada rol, en todos los scopes (3b-2j).
749
+ *
750
+ * Aquí los hechos son TUPLAS, no filas: `role_binding:<scope>|<rol>#assignee@<holder>`,
751
+ * con la caducidad en su *condition*. Por eso esta pregunta es del PUERTO
752
+ * y no del barrido: hasta 3b-2j `pruneOrphanRoles` contaba
753
+ * `authz_assignments` —la tabla del driver `database`, vacía con este— y
754
+ * el `stillGranting` que se lee justo antes de purgar decía SIEMPRE «este
755
+ * rol no concede», sobre roles que concedían (medido en el lote 2i).
756
+ *
757
+ * Lo hace posible lo mismo que hace posible `purgeRole`: el binding apunta
758
+ * a su rol (`role_binding:…#role@role:<uuid>`, (c2)), así que los bindings
759
+ * de un rol se enumeran filtrando por `user`. En modo `resolver` esa arista
760
+ * no existe y el método TAMPOCO (el constructor lo retira, y el manager lo
761
+ * lee como «no lo sé»).
762
+ *
763
+ * La arista estructural se lee con `includeExpired` —no caduca, la
764
+ * caducidad está en el `assignee`— y los assignees sin él: la caducidad es
765
+ * ESTRICTA y con el reloj del driver, igual que en `authorize`. Un `user`
766
+ * que no se entiende como holder se cuenta igual (a diferencia de
767
+ * `listSubjects`, que lo descarta): aquí contar de MÁS es el lado seguro —
768
+ * marca el rol para que un humano lo mire— y contar de menos es decir «no
769
+ * concede» sobre algo que sí.
770
+ *
771
+ * Coste: por rol preguntado, una lectura de sus bindings más una por
772
+ * binding. Lo llama `pruneOrphanRoles` con los HUÉRFANOS de la pasada (no
773
+ * con el catálogo entero) y corre en un comando de plataforma, no en el
774
+ * camino de una petición.
775
+ */
776
+ countRoleAssignments(roleUuids: string[]): Promise<number[]>;
777
+ /**
778
+ * **Rehace la proyección de UN rol** (3b-2e · E4). En (c2) lo que un rol
779
+ * concede son tuplas (`role:<uuid>#permits_<P>@<holder>:*`), no el catálogo
780
+ * local: una escritura de catálogo que no las toque deja un rol que no
781
+ * concede nada (`defineScopedRole`) o que sigue concediendo lo que ya no
782
+ * vincula (`updateScopedRole`) — lo segundo es un fail-open. `syncAuthzCatalog`
783
+ * ya lo hace para el catálogo entero cuando el consumidor le pasa la
784
+ * proyección; esto es lo mismo para las escrituras de la API de delegación,
785
+ * y cuesta una lectura por holder.
786
+ *
787
+ * **Y son DOS proyecciones, no una** (3b-2g · R1): lo que el rol concede
788
+ * (`permits_<P>`) y **dónde es visible** (`scope#binding`, `sweepRoleVisibility`).
789
+ * El modelo (c2) no lleva el NIVEL del rol, así que un `scope_type` que
790
+ * cambia sin barrer deja la asignación concediendo en un nivel que el
791
+ * catálogo ya no declara — retirado en `database` y vivo aquí, que es la
792
+ * divergencia R1 del lote 2e.
793
+ */
794
+ projectCatalogRole(roleUuid: string): Promise<void>;
795
+ /**
796
+ * **Reconstruye el store desde `authz_*` + el árbol del consumidor.**
797
+ *
798
+ * Es la razón de ser de la fase («todo en un driver o todo en otro, con una
799
+ * migración idempotente y bidireccional») y la ÚNICA primitiva de migración
800
+ * del paquete: `openfga:import` se borró en 3b-2k · K2 porque escribía las
801
+ * tuplas de un modelo que ya no existe.
802
+ *
803
+ * Migra las TRES cosas que hacen completo a este driver:
804
+ * 1. el **marcador de raíz** (`scope:app#rooted@<holder>:*`, 3b-2i) — sin
805
+ * él el store entero DENIEGA, así que va primero;
806
+ * 2. la **proyección del catálogo** (`role:<uuid>#permits_<P>`), leída con
807
+ * la MISMA función que usa `syncAuthzCatalog` (`readCatalogProjectionSnapshot`),
808
+ * para que reconcile no "arregle" en cada pasada lo que el sync deja bien;
809
+ * 3. el **árbol** (`scope#parent`) desde `scopes.enumerateEdges`, y
810
+ * 4. los **hechos**: `authz_assignments` (assignee + las dos aristas de
811
+ * (c2)) y `authz_denies` (`scope#denied_<P>`).
812
+ *
813
+ * **Qué borra y qué no.** Lo DERIVADO —marcador, catálogo y árbol— es un
814
+ * espejo de datos locales que nadie más escribe: lo que sobra se borra
815
+ * siempre (cruce 9 · S7 lo exige para las aristas que `enumerateEdges` no
816
+ * respalda, y es lo que repara un nodo con DOS padres, 3b-2h · 🟠 4). Los
817
+ * HECHOS solo se borran con `prune`: son irreversibles y su origen depende
818
+ * de qué driver esté vivo. La excepción, a propósito, es la arista
819
+ * `scope#binding` de una asignación que el origen SÍ respalda pero cuya
820
+ * regla de visibilidad dice que NO (invariante 18): dejarla es fail-OPEN
821
+ * —es justo la escritura que `scopes.moved`/`projectCatalogRole` pudieron
822
+ * perder si el relay no pasó—, así que se borra siempre y se cuenta en
823
+ * `drift.roleVisibility`.
824
+ *
825
+ * **Nada de `Ignore` a ciegas** (cruce 9 · S7): el importador viejo escribía
826
+ * con `onDuplicateWrites: Ignore` y por eso una tupla que ya estaba con OTRA
827
+ * caducidad se quedaba como estaba y encima se contaba como escrita —rompía
828
+ * los invariantes 3 y 6—. Aquí el estado del destino se LEE entero antes de
829
+ * decidir, la diferencia de caducidad se resuelve con delete + write, y los
830
+ * contadores salen del diff, no del write. `Ignore` se conserva solo como
831
+ * red contra una carrera (y por eso la migración va con `manager.freeze()`).
832
+ *
833
+ * `dryRun` es el VERIFICADOR: mismo recorrido, cero escrituras, mismos
834
+ * números. Read-only por contrato (cruce 4 · S18): **un `--fix` está
835
+ * prohibido** y no se implementa ni se deja preparado.
836
+ */
837
+ reconcile(source: ReconcileSource, options?: ReconcileOptions): Promise<ReconcileReport>;
838
+ /**
839
+ * Las relaciones `permits_<P>` que DECLARA el modelo publicado del store, o
840
+ * `null` si no se puede saber (un store sin modelo). No es una barrera de
841
+ * seguridad: es la diferencia entre contar un permiso que este store no
842
+ * puede llevar y morirse con un 400 a mitad de la migración.
843
+ */
844
+ private modelPermissions;
845
+ /**
846
+ * El árbol del ORIGEN, paginado (`scopes.enumerateEdges`), con los ciclos
847
+ * apartados. Un ciclo no se escribe NUNCA: FGA lo evalúa y la herencia pasa
848
+ * a ser bidireccional (un grant en un descendiente concede en el ancestro,
849
+ * cruce 3), así que aquí sale como reporte y sus nodos se quedan sin arista
850
+ * —o sea, sin `rooted`, o sea denegando (fail-closed)—.
851
+ */
852
+ private readSourceTree;
853
+ /**
854
+ * Los HECHOS del origen (`authz_assignments` y `authz_denies`), leídos **por
855
+ * lotes con cursor** sobre la clave primaria: una pasada interrumpida se
856
+ * repite y converge (es idempotente), y una base grande no entra entera en
857
+ * memoria de golpe. Los DOS barridos van en la MISMA transacción de lectura
858
+ * repetible (3b-6, `withSourceSnapshot`): sueltos, componían dos mitades de
859
+ * dos operaciones distintas y FABRICABAN un permiso.
860
+ *
861
+ * Cada fila que NO se migra sale contada y con su motivo:
862
+ * - `unknown-scope`: el árbol del consumidor ya no resuelve ese scope
863
+ * (`detached` de un ancestro). Sus tuplas del store son las de la
864
+ * «resurrección» (3b-0b · AA4) y las borra `--prune`.
865
+ * - `unknown-role` / `unknown-permission`: el catálogo ya no lo declara
866
+ * (un rol retirado; invariante 11 dice que los recoge este comando).
867
+ * - `expired`: la asignación ya no concede; migrarla sería escribir una
868
+ * caducidad pasada.
869
+ * - `role-not-visible`: la asignación existe (y `listRoles`/`hasRole` la
870
+ * enumeran), pero el rol NO es visible en ese scope con el árbol y el
871
+ * catálogo de HOY (invariante 18), así que su arista `scope#binding` no
872
+ * se escribe — y si el store la tiene, se borra.
873
+ * - `unknown-holder-type`: el `holderTypes` del config no declara ese
874
+ * morph name, así que no hay usuario FGA que escribir.
875
+ */
876
+ private readSourceFacts;
877
+ /**
878
+ * **La foto CONSISTENTE del origen** (3b-6; 🔴 3 del panel 3, verificado en
879
+ * el código por el juez).
880
+ *
881
+ * `readSourceFacts` recorre `authz_assignments` y DESPUÉS `authz_denies`.
882
+ * Cada barrido se construía sobre la conexión global, así que entre los dos
883
+ * había un hueco —el tiempo de pasear la primera tabla entera por lotes de
884
+ * 100: segundos o minutos en una base real— por el que se colaban
885
+ * operaciones de negocio COMPUESTAS. Un offboarding es `revoke` +
886
+ * `removeDeny`: si cae ahí, la pasada se queda con la mitad de cada una y
887
+ * escribe en el destino **el rol sin su deny**, o sea un permiso que NI el
888
+ * estado anterior NI el posterior concedían, con el reporte diciendo
889
+ * `written=13 extra=0 skipped={} clean=true`. No es una pérdida: es una
890
+ * ESCALADA fabricada, y el operador no tiene ni un motivo para desconfiar
891
+ * del verde.
892
+ *
893
+ * Con los dos barridos dentro de UNA transacción de lectura repetible, el
894
+ * peor resultado de la ventana deja de ser «un estado que nunca existió» y
895
+ * pasa a ser «el estado consistente de `t0`»: la deriva recuperable que
896
+ * repite la pasada siguiente.
897
+ *
898
+ * **Qué garantiza cada motor, porque no es lo mismo** (Fase 2.5 dixit):
899
+ * - **PostgreSQL**: `BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ`.
900
+ * La foto se toma en la primera sentencia de la transacción y no se
901
+ * mueve; garantía del motor.
902
+ * - **MySQL/InnoDB**: `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ` +
903
+ * `BEGIN`, y la lectura consistente queda fijada en la primera consulta.
904
+ * REPEATABLE READ es su default, pero se DECLARA a propósito: el default
905
+ * es config del servidor y no una promesa del paquete.
906
+ * - **SQLite**: no acepta nivel de aislamiento —knex avisa y lo ignora—,
907
+ * así que no se le pide: su transacción de LECTURA ya es una foto (en
908
+ * WAL el lector conserva su snapshot mientras un escritor confirma; sin
909
+ * WAL el escritor espera al lector). Por eso aquí se pregunta el
910
+ * dialecto en vez de mandar el nivel a ciegas.
911
+ *
912
+ * **Lo que esto NO cubre y queda declarado**: cubre la dirección cuyo
913
+ * origen es `authz_*` (`--to=openfga`). Cuando la fuente de verdad de los
914
+ * hechos es el STORE (`readOriginFacts` → `enumerateFacts`: `--to=database`
915
+ * y la pasada de mantenimiento) las páginas de `Read` **tampoco son una
916
+ * foto consistente** y no hay REPEATABLE READ que valga: ahí la misma
917
+ * composición de media transacción sigue siendo posible. La instrumentación
918
+ * posible en esa dirección es `readChanges({ startTime })` —detectar y
919
+ * nombrar la tupla, no prevenirla—, y no está hecha.
920
+ */
921
+ private withSourceSnapshot;
922
+ /**
923
+ * **Los hechos por el PUERTO** (3b-5): la otra fuente de verdad posible.
924
+ *
925
+ * Se usa cuando `authz_*` NO manda —el caso que faltaba: el motor ya sirve
926
+ * desde este driver y sus hechos son las tuplas del store—, y también sirve
927
+ * para migrar desde otro driver que sepa `enumerateFacts`. El recorrido y
928
+ * los motivos son **los mismos** que los de `readSourceFacts` (`expired`,
929
+ * `unknown-scope`, `unknown-role`, `unknown-holder-type`,
930
+ * `role-not-visible`): lo único que cambia es de dónde salen las filas, que
931
+ * es justo la decisión que no se estaba tomando.
932
+ *
933
+ * Consecuencia buscada: con el store como origen, `wanted` describe lo que
934
+ * el store YA tiene, así que la pasada no escribe ni borra un solo hecho —y
935
+ * sí rehace lo DERIVADO (marcador, catálogo, árbol) y aplica el barrido de
936
+ * visibilidad del invariante 18 con el árbol y el catálogo de HOY, que es
937
+ * la reparación que el invariante promete y no existía.
938
+ *
939
+ * Disciplina del cursor idéntica a la de `--to=database`: como mucho
940
+ * `limit` por página, el cursor tiene que AVANZAR y la cota `maxTuples` se
941
+ * aplica también al origen.
942
+ */
943
+ private readOriginFacts;
944
+ /**
945
+ * Una ASIGNACIÓN del origen (venga de `authz_*` o del puerto) traducida a
946
+ * lo que el store debe tener: el `assignee` con su caducidad, la arista
947
+ * `role_binding#role` y —solo si el rol es VISIBLE ahí— la `scope#binding`.
948
+ * Es la única implementación de esa regla en esta dirección: tenerla dos
949
+ * veces era tenerla distinta según de dónde salieran los hechos.
950
+ */
951
+ private wantAssignment;
952
+ /** Un DENY del origen (permiso ya como slug) traducido a `scope#denied_<P>`. */
953
+ private wantDeny;
954
+ /**
955
+ * Pasea una tabla `authz_*` por lotes de `batchSize` con cursor sobre
956
+ * `uuid` (clave primaria: orden total y estable). Es lo que hace la pasada
957
+ * REANUDABLE y lo que impide que una base grande entre entera de golpe.
958
+ *
959
+ * **El cliente llega por parámetro** (3b-6): antes cada página se construía
960
+ * sobre el `db` global, así que dos barridos consecutivos eran dos fotos
961
+ * distintas. Quien decide la foto es `withSourceSnapshot`, y aquí solo se
962
+ * obedece — un call-site nuevo que pase `db` vuelve a abrir el hueco, y por
963
+ * eso el parámetro es obligatorio y no tiene default.
964
+ */
965
+ private eachRow;
966
+ /**
967
+ * **Los hechos de este store, paginados** (3b-3b): la mitad ORIGEN de la
968
+ * migración, la que hace posible `authz:reconcile --to=database`.
969
+ *
970
+ * Se lee con `Read` y su `continuation_token`, no con `ListObjects`: el
971
+ * plan lo dice con esas palabras y el motivo es que `ListObjects` **no
972
+ * tiene `continuation_token`** —corta al tope del servidor sin señal
973
+ * (S16)—, así que no hay forma de pasear un store entero con él. `Read`
974
+ * sin filtro es además lo único que ve la basura de una versión anterior,
975
+ * que es lo que sale en `skipped`.
976
+ *
977
+ * **Nada se filtra**: una asignación caducada sale con su `expiresAt` para
978
+ * que el DESTINO la cuente con su motivo. Si se filtrara aquí desaparecería
979
+ * sin dejar rastro en ningún contador, que es exactamente lo que una
980
+ * migración no puede hacer. Por lo mismo NO interviene el reloj del driver.
981
+ *
982
+ * Lo que no es un hecho —la estructura de (c2) (`parent`, `binding`,
983
+ * `role`, `rooted`) y la proyección del catálogo (`permits_<P>`)— no se
984
+ * emite ni se cuenta: es DERIVADO, se rehace desde el catálogo y el árbol
985
+ * del consumidor, y en esta dirección ni siquiera se migra. Lo que sí se
986
+ * cuenta es lo que TENDRÍA que ser un hecho y no se entiende.
987
+ */
988
+ enumerateFacts(page: {
989
+ limit: number;
990
+ after?: string;
991
+ }): Promise<ReconcileFactPage>;
992
+ /**
993
+ * El store ENTERO, con la caducidad de cada tupla. Un `Read` sin filtro es
994
+ * la única forma de ver lo que SOBRA —incluida la basura de una versión
995
+ * anterior, cuyos tipos el modelo de hoy ni declara (se lee y se borra, se
996
+ * comprobó contra el servidor)—: filtrando por objeto solo se ve lo que ya
997
+ * se sabe que existe. Paginado y acotado como todas las enumeraciones.
998
+ */
999
+ private readEveryTuple;
1000
+ /**
1001
+ * Aplica el plan en lotes ≤ 100 (el límite del `Write`). `Ignore` en las dos
1002
+ * direcciones es RED, no política: el plan sale de un diff sobre el estado
1003
+ * leído y la migración corre con las escrituras congeladas, así que un
1004
+ * duplicado o un borrado que ya no está solo puede venir de una carrera —y
1005
+ * en una carrera es preferible seguir a abortar la migración entera—.
1006
+ */
1007
+ private applyReconcileWrites;
1008
+ /**
1009
+ * TODAS las tuplas que casan con el filtro, paginando `Read` hasta agotar
1010
+ * el `continuation_token`, sin las caducadas. Es la única primitiva de
1011
+ * enumeración del driver (L0.7): `Read` no tiene tope de resultados —a
1012
+ * diferencia de `ListObjects`/`ListUsers`, que cortan al máximo del
1013
+ * servidor sin ninguna señal— y devuelve la condición de cada tupla, así
1014
+ * que la caducidad se filtra aquí con el mismo reloj que `checkContext`.
1015
+ *
1016
+ * Contrapartida, documentada en el README: `Read` devuelve tuplas
1017
+ * ESCRITAS, no relaciones computadas. Con el modelo que genera este paquete
1018
+ * (`assignee`/`denied` directas) es exactamente lo mismo; un modelo
1019
+ * extendido con relaciones derivadas sobre `role_binding` no se enumeraría
1020
+ * por aquí.
1021
+ */
1022
+ private readAllTuples;
115
1023
  }
116
1024
  //# sourceMappingURL=openfga_driver.d.ts.map