@jantstack/adonis-authz 2.0.0-alpha.1 → 2.4.0-alpha.10

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