@jantstack/adonis-authz 2.0.0-alpha.1 → 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 (210) hide show
  1. package/README.md +462 -35
  2. package/build/commands/authz_catalog_diff.js +1 -1
  3. package/build/commands/authz_catalog_diff.js.map +1 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  5. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  6. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  7. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  8. package/build/commands/authz_catalog_sync.d.ts +17 -0
  9. package/build/commands/authz_catalog_sync.d.ts.map +1 -1
  10. package/build/commands/authz_catalog_sync.js +27 -4
  11. package/build/commands/authz_catalog_sync.js.map +1 -1
  12. package/build/commands/authz_freeze.d.ts +44 -0
  13. package/build/commands/authz_freeze.d.ts.map +1 -0
  14. package/build/commands/authz_freeze.js +95 -0
  15. package/build/commands/authz_freeze.js.map +1 -0
  16. package/build/commands/authz_reconcile.d.ts +102 -0
  17. package/build/commands/authz_reconcile.d.ts.map +1 -0
  18. package/build/commands/authz_reconcile.js +294 -0
  19. package/build/commands/authz_reconcile.js.map +1 -0
  20. package/build/commands/authz_relations_reconcile.d.ts +73 -0
  21. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  22. package/build/commands/authz_relations_reconcile.js +225 -0
  23. package/build/commands/authz_relations_reconcile.js.map +1 -0
  24. package/build/commands/authz_scopes_relay.d.ts +47 -0
  25. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  26. package/build/commands/authz_scopes_relay.js +141 -0
  27. package/build/commands/authz_scopes_relay.js.map +1 -0
  28. package/build/commands/authz_unfreeze.d.ts +37 -0
  29. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  30. package/build/commands/authz_unfreeze.js +92 -0
  31. package/build/commands/authz_unfreeze.js.map +1 -0
  32. package/build/commands/main.d.ts +6 -1
  33. package/build/commands/main.d.ts.map +1 -1
  34. package/build/commands/main.js +6 -1
  35. package/build/commands/main.js.map +1 -1
  36. package/build/commands/openfga_provision.d.ts +46 -4
  37. package/build/commands/openfga_provision.d.ts.map +1 -1
  38. package/build/commands/openfga_provision.js +90 -7
  39. package/build/commands/openfga_provision.js.map +1 -1
  40. package/build/configure.d.ts +11 -0
  41. package/build/configure.d.ts.map +1 -1
  42. package/build/configure.js +37 -1
  43. package/build/configure.js.map +1 -1
  44. package/build/index.d.ts +39 -10
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +35 -6
  47. package/build/index.js.map +1 -1
  48. package/build/providers/authz_provider.d.ts +26 -2
  49. package/build/providers/authz_provider.d.ts.map +1 -1
  50. package/build/providers/authz_provider.js +48 -2
  51. package/build/providers/authz_provider.js.map +1 -1
  52. package/build/services/relations.d.ts +14 -0
  53. package/build/services/relations.d.ts.map +1 -0
  54. package/build/services/relations.js +17 -0
  55. package/build/services/relations.js.map +1 -0
  56. package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
  57. package/build/src/catalog/catalog.d.ts.map +1 -0
  58. package/build/src/{catalog.js → catalog/catalog.js} +120 -14
  59. package/build/src/catalog/catalog.js.map +1 -0
  60. package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
  61. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  62. package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
  63. package/build/src/catalog/catalog_cache.js.map +1 -0
  64. package/build/src/define_config.d.ts +101 -3
  65. package/build/src/define_config.d.ts.map +1 -1
  66. package/build/src/define_config.js.map +1 -1
  67. package/build/src/drivers/database_driver.d.ts +86 -4
  68. package/build/src/drivers/database_driver.d.ts.map +1 -1
  69. package/build/src/drivers/database_driver.js +425 -11
  70. package/build/src/drivers/database_driver.js.map +1 -1
  71. package/build/src/drivers/database_relations_driver.d.ts +75 -0
  72. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  73. package/build/src/drivers/database_relations_driver.js +450 -0
  74. package/build/src/drivers/database_relations_driver.js.map +1 -0
  75. package/build/src/drivers/openfga_driver.d.ts +713 -119
  76. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  77. package/build/src/drivers/openfga_driver.js +2048 -476
  78. package/build/src/drivers/openfga_driver.js.map +1 -1
  79. package/build/src/drivers/openfga_facts.d.ts +369 -0
  80. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  81. package/build/src/drivers/openfga_facts.js +813 -0
  82. package/build/src/drivers/openfga_facts.js.map +1 -0
  83. package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
  84. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  85. package/build/src/drivers/openfga_relations_driver.js +466 -0
  86. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  87. package/build/src/errors.d.ts +258 -5
  88. package/build/src/errors.d.ts.map +1 -1
  89. package/build/src/errors.js +238 -7
  90. package/build/src/errors.js.map +1 -1
  91. package/build/src/freeze.d.ts +120 -0
  92. package/build/src/freeze.d.ts.map +1 -0
  93. package/build/src/freeze.js +172 -0
  94. package/build/src/freeze.js.map +1 -0
  95. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  96. package/build/src/http/app_access_middleware.js.map +1 -0
  97. package/build/src/http/resource_access_middleware.d.ts +105 -0
  98. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  99. package/build/src/http/resource_access_middleware.js +81 -0
  100. package/build/src/http/resource_access_middleware.js.map +1 -0
  101. package/build/src/identity.d.ts +74 -1
  102. package/build/src/identity.d.ts.map +1 -1
  103. package/build/src/identity.js +100 -2
  104. package/build/src/identity.js.map +1 -1
  105. package/build/src/manager.d.ts +315 -4
  106. package/build/src/manager.d.ts.map +1 -1
  107. package/build/src/manager.js +1187 -181
  108. package/build/src/manager.js.map +1 -1
  109. package/build/src/models/authz_assignment.d.ts +6 -6
  110. package/build/src/models/authz_assignment.d.ts.map +1 -1
  111. package/build/src/models/authz_deny.d.ts +6 -6
  112. package/build/src/models/authz_deny.d.ts.map +1 -1
  113. package/build/src/models/authz_permission.d.ts +6 -6
  114. package/build/src/models/authz_permission.d.ts.map +1 -1
  115. package/build/src/models/authz_role.d.ts +6 -6
  116. package/build/src/models/authz_role.d.ts.map +1 -1
  117. package/build/src/models/authz_role_permission.d.ts +6 -6
  118. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  119. package/build/src/openfga.d.ts +18 -2
  120. package/build/src/openfga.d.ts.map +1 -1
  121. package/build/src/openfga.js +15 -1
  122. package/build/src/openfga.js.map +1 -1
  123. package/build/src/reconcile.d.ts +37 -0
  124. package/build/src/reconcile.d.ts.map +1 -0
  125. package/build/src/reconcile.js +69 -0
  126. package/build/src/reconcile.js.map +1 -0
  127. package/build/src/relation_partition_trigger.d.ts +8 -0
  128. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  129. package/build/src/relation_partition_trigger.js +85 -0
  130. package/build/src/relation_partition_trigger.js.map +1 -0
  131. package/build/src/relations/define_relations_config.d.ts +58 -0
  132. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  133. package/build/src/relations/define_relations_config.js +144 -0
  134. package/build/src/relations/define_relations_config.js.map +1 -0
  135. package/build/src/relations/manager.d.ts +38 -0
  136. package/build/src/relations/manager.d.ts.map +1 -0
  137. package/build/src/relations/manager.js +156 -0
  138. package/build/src/relations/manager.js.map +1 -0
  139. package/build/src/relations/reconcile.d.ts +62 -0
  140. package/build/src/relations/reconcile.d.ts.map +1 -0
  141. package/build/src/relations/reconcile.js +138 -0
  142. package/build/src/relations/reconcile.js.map +1 -0
  143. package/build/src/relations_config_store.d.ts +22 -0
  144. package/build/src/relations_config_store.d.ts.map +1 -0
  145. package/build/src/relations_config_store.js +74 -0
  146. package/build/src/relations_config_store.js.map +1 -0
  147. package/build/src/scope_outbox.d.ts +69 -0
  148. package/build/src/scope_outbox.d.ts.map +1 -0
  149. package/build/src/scope_outbox.js +291 -0
  150. package/build/src/scope_outbox.js.map +1 -0
  151. package/build/src/{drivers → shared}/backend_guard.d.ts +14 -0
  152. package/build/src/shared/backend_guard.d.ts.map +1 -0
  153. package/build/src/{drivers → shared}/backend_guard.js +26 -1
  154. package/build/src/shared/backend_guard.js.map +1 -0
  155. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  156. package/build/src/shared/sql_expiry.js.map +1 -0
  157. package/build/src/sql_descendants.d.ts +47 -1
  158. package/build/src/sql_descendants.d.ts.map +1 -1
  159. package/build/src/sql_descendants.js +75 -1
  160. package/build/src/sql_descendants.js.map +1 -1
  161. package/build/src/testing/contract.d.ts +74 -0
  162. package/build/src/testing/contract.d.ts.map +1 -1
  163. package/build/src/testing/contract.js +672 -169
  164. package/build/src/testing/contract.js.map +1 -1
  165. package/build/src/testing/main.d.ts +6 -0
  166. package/build/src/testing/main.d.ts.map +1 -1
  167. package/build/src/testing/main.js +3 -0
  168. package/build/src/testing/main.js.map +1 -1
  169. package/build/src/testing/migration_contract.d.ts +284 -0
  170. package/build/src/testing/migration_contract.d.ts.map +1 -0
  171. package/build/src/testing/migration_contract.js +586 -0
  172. package/build/src/testing/migration_contract.js.map +1 -0
  173. package/build/src/testing/relations_contract.d.ts +51 -0
  174. package/build/src/testing/relations_contract.d.ts.map +1 -0
  175. package/build/src/testing/relations_contract.js +654 -0
  176. package/build/src/testing/relations_contract.js.map +1 -0
  177. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  178. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  179. package/build/src/testing/relations_reconcile_contract.js +172 -0
  180. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  181. package/build/src/traits/authz_scopes.js +1 -1
  182. package/build/src/traits/authz_scopes.js.map +1 -1
  183. package/build/src/traits/has_uuid.d.ts +7 -7
  184. package/build/src/traits/has_uuid.d.ts.map +1 -1
  185. package/build/src/types.d.ts +865 -82
  186. package/build/src/types.d.ts.map +1 -1
  187. package/build/src/types.js +10 -0
  188. package/build/src/types.js.map +1 -1
  189. package/build/stubs/config/authorization.stub +104 -4
  190. package/build/stubs/migration.stub +126 -0
  191. package/build/stubs/scopes_outbox_migration.stub +57 -0
  192. package/package.json +4 -2
  193. package/build/commands/openfga_import.d.ts +0 -34
  194. package/build/commands/openfga_import.d.ts.map +0 -1
  195. package/build/commands/openfga_import.js +0 -97
  196. package/build/commands/openfga_import.js.map +0 -1
  197. package/build/src/catalog.d.ts.map +0 -1
  198. package/build/src/catalog.js.map +0 -1
  199. package/build/src/catalog_cache.d.ts.map +0 -1
  200. package/build/src/catalog_cache.js.map +0 -1
  201. package/build/src/drivers/backend_guard.d.ts.map +0 -1
  202. package/build/src/drivers/backend_guard.js.map +0 -1
  203. package/build/src/drivers/sql_expiry.d.ts.map +0 -1
  204. package/build/src/drivers/sql_expiry.js.map +0 -1
  205. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  206. package/build/src/middleware/app_access_middleware.js.map +0 -1
  207. /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
  208. /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
  209. /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
  210. /package/build/src/{drivers → shared}/sql_expiry.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 } 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>` #assigneela 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,30 @@ 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 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.
87
105
  */
88
- 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<{
89
107
  storeId: string;
90
108
  modelId: string;
91
109
  }>;
@@ -96,8 +114,15 @@ export declare function provisionOpenFgaStore(apiUrl: string, name: string, hold
96
114
  * "alguien escribió antes" y se propagan clasificados, con el error del SDK
97
115
  * como causa (D6). Verificado contra OpenFGA v1.19: el duplicado llega como
98
116
  * 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í.
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.
101
126
  */
102
127
  export declare function isDuplicateWrite(error: unknown): boolean;
103
128
  export interface OpenFgaDriverOptions {
@@ -169,7 +194,44 @@ export interface OpenFgaDriverOptions {
169
194
  * producción lo normal es `clock` en el config del manager (`withClock`).
170
195
  */
171
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;
172
214
  }
215
+ /**
216
+ * **El gate de construcción de `facts`** (3b-2d, panel 2 cruce 4 · S5).
217
+ *
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;
173
235
  export declare const DEFAULT_TIMEOUT_MS = 5000;
174
236
  /**
175
237
  * Cota de páginas de una enumeración (1.000.000 de tuplas a 100 por página).
@@ -178,68 +240,36 @@ export declare const DEFAULT_TIMEOUT_MS = 5000;
178
240
  * el deadline es por llamada (D12, auditor H7).
179
241
  */
180
242
  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
243
  /**
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.
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.
240
248
  */
241
- export declare function importAuthzFactsToOpenFga(options: OpenFgaDriverOptions & ImportFactsOptions): Promise<ImportFactsResult>;
249
+ export declare const MAX_SCOPE_CHAIN_HOPS = 1000;
242
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
+ }>;
243
273
  private client;
244
274
  private chainResolver;
245
275
  private holderTypes;
@@ -277,8 +307,10 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
277
307
  private chain;
278
308
  /** La cadena o 422: una escritura no puede ir a un scope que nadie reconoce. */
279
309
  private knownScope;
280
- /** El scope canónico para `revoke`/`removeDeny`/`purgeScope` (ver `canonicalScope`). */
310
+ /** El scope canónico para `scopes.detached`/`purgeScope` (ver `canonicalScope`). */
281
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;
282
314
  /**
283
315
  * Vista de este driver con OTRO resolutor de ancestros y el mismo estado
284
316
  * (cliente, memo del catálogo, deadline, diagnósticos). Es lo que usa
@@ -326,14 +358,30 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
326
358
  * la cadena del scope del BINDING (desde él hacia la raíz).
327
359
  */
328
360
  private declaredRole;
361
+ authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
329
362
  /**
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).
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.
334
383
  */
335
- private checksFor;
336
- authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
384
+ private factsAuthorize;
337
385
  /**
338
386
  * `authorize` sobre N scopes con UN batchCheck (2.1, B6): los checks de
339
387
  * todas las cadenas viajan juntos (el SDK trocea a 50 y paraleliza) y se
@@ -343,13 +391,34 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
343
391
  * Scope desconocido o sin rol que conceda ⇒ false sin checks.
344
392
  */
345
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;
346
406
  grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
347
407
  /**
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).
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).
350
413
  */
351
414
  private writeAssignment;
352
- /** delete + write (dos llamadas: FGA no admite ambas sobre la misma key en una). */
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
+ */
353
422
  private replaceAssignment;
354
423
  /**
355
424
  * Estado actual de una asignación, con TRES resultados posibles y no dos.
@@ -365,6 +434,20 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
365
434
  hasRole(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<boolean>;
366
435
  deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
367
436
  removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
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;
368
451
  /**
369
452
  * Holders con asignación vigente del rol en el scope exacto: `Read` por
370
453
  * objeto exacto, paginado, con la caducidad filtrada en cliente. Antes era
@@ -378,6 +461,17 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
378
461
  private listBindings;
379
462
  /** Scopes (por clave) donde el subject tiene un deny directo del permiso (por su uuid). */
380
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;
381
475
  private parseBindings;
382
476
  private warn;
383
477
  listRoles(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
@@ -394,23 +488,523 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
394
488
  listRoleScopes(subject: SubjectRef, scopeType: ScopeType): Promise<ScopeRef[]>;
395
489
  listScopes(subject: SubjectRef, permission: string): Promise<ScopeRef[]>;
396
490
  /**
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).
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).
401
495
  */
402
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;
403
670
  /**
404
671
  * Purga del scope exacto en FGA (N7, S6, B2). No hay "borrar todo lo de
405
672
  * 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).
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).
412
680
  */
413
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;
414
1008
  /**
415
1009
  * TODAS las tuplas que casan con el filtro, paginando `Read` hasta agotar
416
1010
  * el `continuation_token`, sin las caducadas. Es la única primitiva de