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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +837 -51
  3. package/build/commands/authz_catalog_diff.d.ts +28 -0
  4. package/build/commands/authz_catalog_diff.d.ts.map +1 -0
  5. package/build/commands/authz_catalog_diff.js +67 -0
  6. package/build/commands/authz_catalog_diff.js.map +1 -0
  7. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  8. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  9. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  10. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  11. package/build/commands/authz_catalog_sync.d.ts +37 -0
  12. package/build/commands/authz_catalog_sync.d.ts.map +1 -0
  13. package/build/commands/authz_catalog_sync.js +81 -0
  14. package/build/commands/authz_catalog_sync.js.map +1 -0
  15. package/build/commands/authz_freeze.d.ts +44 -0
  16. package/build/commands/authz_freeze.d.ts.map +1 -0
  17. package/build/commands/authz_freeze.js +95 -0
  18. package/build/commands/authz_freeze.js.map +1 -0
  19. package/build/commands/authz_reconcile.d.ts +102 -0
  20. package/build/commands/authz_reconcile.d.ts.map +1 -0
  21. package/build/commands/authz_reconcile.js +294 -0
  22. package/build/commands/authz_reconcile.js.map +1 -0
  23. package/build/commands/authz_relations_reconcile.d.ts +73 -0
  24. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  25. package/build/commands/authz_relations_reconcile.js +225 -0
  26. package/build/commands/authz_relations_reconcile.js.map +1 -0
  27. package/build/commands/authz_scopes_relay.d.ts +47 -0
  28. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  29. package/build/commands/authz_scopes_relay.js +141 -0
  30. package/build/commands/authz_scopes_relay.js.map +1 -0
  31. package/build/commands/authz_unfreeze.d.ts +37 -0
  32. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  33. package/build/commands/authz_unfreeze.js +92 -0
  34. package/build/commands/authz_unfreeze.js.map +1 -0
  35. package/build/commands/main.d.ts +8 -1
  36. package/build/commands/main.d.ts.map +1 -1
  37. package/build/commands/main.js +8 -1
  38. package/build/commands/main.js.map +1 -1
  39. package/build/commands/openfga_provision.d.ts +46 -4
  40. package/build/commands/openfga_provision.d.ts.map +1 -1
  41. package/build/commands/openfga_provision.js +91 -7
  42. package/build/commands/openfga_provision.js.map +1 -1
  43. package/build/configure.d.ts +11 -0
  44. package/build/configure.d.ts.map +1 -1
  45. package/build/configure.js +37 -1
  46. package/build/configure.js.map +1 -1
  47. package/build/index.d.ts +75 -7
  48. package/build/index.d.ts.map +1 -1
  49. package/build/index.js +67 -5
  50. package/build/index.js.map +1 -1
  51. package/build/providers/authz_provider.d.ts +26 -2
  52. package/build/providers/authz_provider.d.ts.map +1 -1
  53. package/build/providers/authz_provider.js +48 -2
  54. package/build/providers/authz_provider.js.map +1 -1
  55. package/build/services/relations.d.ts +14 -0
  56. package/build/services/relations.d.ts.map +1 -0
  57. package/build/services/relations.js +17 -0
  58. package/build/services/relations.js.map +1 -0
  59. package/build/src/catalog/catalog.d.ts +289 -0
  60. package/build/src/catalog/catalog.d.ts.map +1 -0
  61. package/build/src/catalog/catalog.js +859 -0
  62. package/build/src/catalog/catalog.js.map +1 -0
  63. package/build/src/catalog/catalog_cache.d.ts +324 -0
  64. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  65. package/build/src/catalog/catalog_cache.js +666 -0
  66. package/build/src/catalog/catalog_cache.js.map +1 -0
  67. package/build/src/clock.d.ts +24 -0
  68. package/build/src/clock.d.ts.map +1 -0
  69. package/build/src/clock.js +7 -0
  70. package/build/src/clock.js.map +1 -0
  71. package/build/src/define_config.d.ts +201 -7
  72. package/build/src/define_config.d.ts.map +1 -1
  73. package/build/src/define_config.js.map +1 -1
  74. package/build/src/drivers/database_driver.d.ts +324 -15
  75. package/build/src/drivers/database_driver.d.ts.map +1 -1
  76. package/build/src/drivers/database_driver.js +1107 -128
  77. package/build/src/drivers/database_driver.js.map +1 -1
  78. package/build/src/drivers/database_relations_driver.d.ts +75 -0
  79. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  80. package/build/src/drivers/database_relations_driver.js +450 -0
  81. package/build/src/drivers/database_relations_driver.js.map +1 -0
  82. package/build/src/drivers/openfga_driver.d.ts +963 -55
  83. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  84. package/build/src/drivers/openfga_driver.js +2693 -372
  85. package/build/src/drivers/openfga_driver.js.map +1 -1
  86. package/build/src/drivers/openfga_facts.d.ts +369 -0
  87. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  88. package/build/src/drivers/openfga_facts.js +813 -0
  89. package/build/src/drivers/openfga_facts.js.map +1 -0
  90. package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
  91. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  92. package/build/src/drivers/openfga_relations_driver.js +466 -0
  93. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  94. package/build/src/errors.d.ts +586 -0
  95. package/build/src/errors.d.ts.map +1 -1
  96. package/build/src/errors.js +583 -0
  97. package/build/src/errors.js.map +1 -1
  98. package/build/src/expiry.d.ts +27 -0
  99. package/build/src/expiry.d.ts.map +1 -0
  100. package/build/src/expiry.js +50 -0
  101. package/build/src/expiry.js.map +1 -0
  102. package/build/src/freeze.d.ts +120 -0
  103. package/build/src/freeze.d.ts.map +1 -0
  104. package/build/src/freeze.js +172 -0
  105. package/build/src/freeze.js.map +1 -0
  106. package/build/src/hierarchical_resolver.d.ts +56 -0
  107. package/build/src/hierarchical_resolver.d.ts.map +1 -0
  108. package/build/src/hierarchical_resolver.js +87 -0
  109. package/build/src/hierarchical_resolver.js.map +1 -0
  110. package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
  111. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  112. package/build/src/{middleware → http}/app_access_middleware.js +10 -26
  113. package/build/src/http/app_access_middleware.js.map +1 -0
  114. package/build/src/http/resource_access_middleware.d.ts +105 -0
  115. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  116. package/build/src/http/resource_access_middleware.js +81 -0
  117. package/build/src/http/resource_access_middleware.js.map +1 -0
  118. package/build/src/identity.d.ts +228 -0
  119. package/build/src/identity.d.ts.map +1 -0
  120. package/build/src/identity.js +457 -0
  121. package/build/src/identity.js.map +1 -0
  122. package/build/src/manager.d.ts +536 -7
  123. package/build/src/manager.d.ts.map +1 -1
  124. package/build/src/manager.js +2676 -23
  125. package/build/src/manager.js.map +1 -1
  126. package/build/src/memoize_ancestors.d.ts +23 -0
  127. package/build/src/memoize_ancestors.d.ts.map +1 -0
  128. package/build/src/memoize_ancestors.js +42 -0
  129. package/build/src/memoize_ancestors.js.map +1 -0
  130. package/build/src/models/authz_assignment.d.ts +11 -11
  131. package/build/src/models/authz_assignment.d.ts.map +1 -1
  132. package/build/src/models/authz_deny.d.ts +11 -11
  133. package/build/src/models/authz_deny.d.ts.map +1 -1
  134. package/build/src/models/authz_permission.d.ts +17 -11
  135. package/build/src/models/authz_permission.d.ts.map +1 -1
  136. package/build/src/models/authz_permission.js +4 -0
  137. package/build/src/models/authz_permission.js.map +1 -1
  138. package/build/src/models/authz_role.d.ts +19 -12
  139. package/build/src/models/authz_role.d.ts.map +1 -1
  140. package/build/src/models/authz_role.js +6 -1
  141. package/build/src/models/authz_role.js.map +1 -1
  142. package/build/src/models/authz_role_permission.d.ts +11 -11
  143. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  144. package/build/src/openfga.d.ts +29 -0
  145. package/build/src/openfga.d.ts.map +1 -0
  146. package/build/src/openfga.js +26 -0
  147. package/build/src/openfga.js.map +1 -0
  148. package/build/src/reconcile.d.ts +37 -0
  149. package/build/src/reconcile.d.ts.map +1 -0
  150. package/build/src/reconcile.js +69 -0
  151. package/build/src/reconcile.js.map +1 -0
  152. package/build/src/relation_partition_trigger.d.ts +8 -0
  153. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  154. package/build/src/relation_partition_trigger.js +85 -0
  155. package/build/src/relation_partition_trigger.js.map +1 -0
  156. package/build/src/relations/define_relations_config.d.ts +58 -0
  157. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  158. package/build/src/relations/define_relations_config.js +144 -0
  159. package/build/src/relations/define_relations_config.js.map +1 -0
  160. package/build/src/relations/manager.d.ts +38 -0
  161. package/build/src/relations/manager.d.ts.map +1 -0
  162. package/build/src/relations/manager.js +156 -0
  163. package/build/src/relations/manager.js.map +1 -0
  164. package/build/src/relations/reconcile.d.ts +62 -0
  165. package/build/src/relations/reconcile.d.ts.map +1 -0
  166. package/build/src/relations/reconcile.js +138 -0
  167. package/build/src/relations/reconcile.js.map +1 -0
  168. package/build/src/relations_config_store.d.ts +22 -0
  169. package/build/src/relations_config_store.d.ts.map +1 -0
  170. package/build/src/relations_config_store.js +74 -0
  171. package/build/src/relations_config_store.js.map +1 -0
  172. package/build/src/scope_outbox.d.ts +69 -0
  173. package/build/src/scope_outbox.d.ts.map +1 -0
  174. package/build/src/scope_outbox.js +291 -0
  175. package/build/src/scope_outbox.js.map +1 -0
  176. package/build/src/shared/backend_guard.d.ts +106 -0
  177. package/build/src/shared/backend_guard.d.ts.map +1 -0
  178. package/build/src/shared/backend_guard.js +246 -0
  179. package/build/src/shared/backend_guard.js.map +1 -0
  180. package/build/src/shared/sql_expiry.d.ts +53 -0
  181. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  182. package/build/src/shared/sql_expiry.js +66 -0
  183. package/build/src/shared/sql_expiry.js.map +1 -0
  184. package/build/src/sql_descendants.d.ts +97 -0
  185. package/build/src/sql_descendants.d.ts.map +1 -0
  186. package/build/src/sql_descendants.js +203 -0
  187. package/build/src/sql_descendants.js.map +1 -0
  188. package/build/src/testing/contract.d.ts +212 -7
  189. package/build/src/testing/contract.d.ts.map +1 -1
  190. package/build/src/testing/contract.js +3449 -24
  191. package/build/src/testing/contract.js.map +1 -1
  192. package/build/src/testing/main.d.ts +10 -2
  193. package/build/src/testing/main.d.ts.map +1 -1
  194. package/build/src/testing/main.js +5 -1
  195. package/build/src/testing/main.js.map +1 -1
  196. package/build/src/testing/migration_contract.d.ts +284 -0
  197. package/build/src/testing/migration_contract.d.ts.map +1 -0
  198. package/build/src/testing/migration_contract.js +586 -0
  199. package/build/src/testing/migration_contract.js.map +1 -0
  200. package/build/src/testing/relations_contract.d.ts +51 -0
  201. package/build/src/testing/relations_contract.d.ts.map +1 -0
  202. package/build/src/testing/relations_contract.js +654 -0
  203. package/build/src/testing/relations_contract.js.map +1 -0
  204. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  205. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  206. package/build/src/testing/relations_reconcile_contract.js +172 -0
  207. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  208. package/build/src/testing/scope_tree.d.ts +65 -0
  209. package/build/src/testing/scope_tree.d.ts.map +1 -0
  210. package/build/src/testing/scope_tree.js +145 -0
  211. package/build/src/testing/scope_tree.js.map +1 -0
  212. package/build/src/traits/authz_scopes.d.ts +30 -6
  213. package/build/src/traits/authz_scopes.d.ts.map +1 -1
  214. package/build/src/traits/authz_scopes.js +30 -18
  215. package/build/src/traits/authz_scopes.js.map +1 -1
  216. package/build/src/traits/has_uuid.d.ts +12 -12
  217. package/build/src/traits/has_uuid.d.ts.map +1 -1
  218. package/build/src/types.d.ts +1313 -28
  219. package/build/src/types.d.ts.map +1 -1
  220. package/build/src/types.js +20 -4
  221. package/build/src/types.js.map +1 -1
  222. package/build/stubs/config/app_acl.stub +4 -2
  223. package/build/stubs/config/authorization.stub +168 -14
  224. package/build/stubs/migration.stub +183 -13
  225. package/build/stubs/scopes_outbox_migration.stub +57 -0
  226. package/package.json +14 -7
  227. package/build/commands/openfga_import.d.ts +0 -28
  228. package/build/commands/openfga_import.d.ts.map +0 -1
  229. package/build/commands/openfga_import.js +0 -74
  230. package/build/commands/openfga_import.js.map +0 -1
  231. package/build/src/catalog.d.ts +0 -3
  232. package/build/src/catalog.d.ts.map +0 -1
  233. package/build/src/catalog.js +0 -103
  234. package/build/src/catalog.js.map +0 -1
  235. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  236. package/build/src/middleware/app_access_middleware.js.map +0 -1
@@ -12,10 +12,12 @@
12
12
  * suite corre contra cada driver — es el juez del contrato):
13
13
  *
14
14
  * 1. **Scopes jerárquicos, herencia SOLO hacia abajo.** La cadena de un scope
15
- * es `[scope, ...ancestros]` (`app` es la raíz: su cadena es él mismo).
15
+ * es `[scope canónico, ...ancestros]` (`app` es la raíz: su cadena es él
16
+ * mismo); la identidad de un scope es la que devuelve el resolutor, nunca
17
+ * la forma con la que lo escribió el llamante (2.5-B · K1).
16
18
  * Un grant en un scope autoriza en ese scope y en TODOS sus descendientes;
17
19
  * nunca en hermanos ni ancestros. En T1 solo opera `app` (los ancestros de
18
- * `organization`/`unit` se completan en T2/T3 vía `resolveAncestors`).
20
+ * `organization`/`unit` se completan en T2/T3 vía `resolveChain`).
19
21
  * 2. **Deny explícito gana.** Un deny de un permiso en cualquier scope de la
20
22
  * cadena bloquea `authorize`, aunque un rol lo conceda (el deny también
21
23
  * hereda hacia abajo). Quitar el deny restaura el permiso.
@@ -28,8 +30,12 @@
28
30
  * holder sin asignación vigente → `false`, nunca throw en `authorize`.
29
31
  * En cambio `grant`/`deny` con rol/permiso fuera del catálogo → throw
30
32
  * (error de programación, no de autorización).
31
- * 6. **Idempotencia de escritura.** Re-`grant` no duplica (actualiza
32
- * `expiresAt`); re-`revoke`/re-`deny`/re-`removeDeny` son no-ops seguros.
33
+ * 6. **Idempotencia de escritura.** Re-`grant` no duplica (`expiresAt` en
34
+ * tres estados: omitido preserva, `null` quita, `Date` fija);
35
+ * re-`revoke`/re-`deny`/re-`removeDeny` son no-ops seguros.
36
+ * 7. **El árbol es un hecho del contrato.** Un scope que el resolutor no
37
+ * conoce (`null`) deniega, no lista y no admite escrituras; `purgeScope`
38
+ * borra los hechos del scope exacto (los del catálogo) y demuestra cero.
33
39
  */
34
40
  /** Referencia polimórfica al holder: morph name + uuid. */
35
41
  export interface SubjectRef {
@@ -40,7 +46,7 @@ export interface SubjectRef {
40
46
  * Nivel de scope. El motor solo conoce la raíz (`app`, uuid null); los demás
41
47
  * niveles los define el CONSUMIDOR — `organization`/`unit` en este chasis,
42
48
  * pero podrían ser `project`, `site`, `case`… El árbol lo declara el
43
- * `ScopeAncestorsResolver` que se inyecta a los drivers, así que el motor no
49
+ * `ScopeChainResolver` que se inyecta a los drivers, así que el motor no
44
50
  * necesita conocer la taxonomía.
45
51
  *
46
52
  * Un consumidor que quiera seguridad de tipos define su propia unión:
@@ -56,11 +62,206 @@ export interface ScopeRef {
56
62
  }
57
63
  /** El scope raíz de aplicación (nivel plataforma). */
58
64
  export declare const APP_SCOPE: ScopeRef;
59
- export interface GrantOptions {
60
- /** Expiración de la asignación. `null`/omitido = sin expiración. */
65
+ /**
66
+ * Opciones comunes a TODA escritura del manager (`grant`, `revoke`, `deny`,
67
+ * `removeDeny`, `scopes.*`) — 2.1, B7.
68
+ */
69
+ export interface WriteOptions {
70
+ /**
71
+ * Quién ordena la escritura. Se valida como identidad (422 si está mal
72
+ * formado) y viaja en `AuthzWriteEvent.actor` para que la auditoría del
73
+ * consumidor no dependa de un `AsyncLocalStorage` que el paquete no tiene.
74
+ * Con `requireActor: true` en el config, omitirlo es 422
75
+ * `E_AUTHZ_ACTOR_REQUIRED` antes de tocar el driver. El motor NO lo evalúa:
76
+ * quién puede conceder qué es policy del consumidor (invariante 8).
77
+ */
78
+ actor?: SubjectRef;
79
+ }
80
+ /**
81
+ * Opciones de las NUEVE escrituras del manager (`grant`, `revoke`, `deny`,
82
+ * `removeDeny`, `scopes.attached/moved/detached` y, desde 3D · M3, la API de
83
+ * delegación `defineScopedRole`/`updateScopedRole`/`deleteScopedRole`) —
84
+ * 2.1, B1; 2D · F2.
85
+ */
86
+ export interface ScopedWriteOptions extends WriteOptions {
87
+ /**
88
+ * Contención: el scope de la escritura tiene que estar DENTRO de `within`
89
+ * (`within ∈ chain(scope)`, inclusive; `APP_SCOPE` contiene todo). Si no,
90
+ * 422 `E_AUTHZ_NOT_WITHIN` y nada se escribe. Es lo que impide que el
91
+ * administrador de la organización A conceda en una unit de B pasando un
92
+ * uuid ajeno: el call-site declara "dentro de MI tenant" y el motor lo
93
+ * comprueba contra el árbol, en fresco (nunca con el memo por request).
94
+ * Qué scope se contrasta: el de `grant`/`revoke`/`deny`/`removeDeny`; el
95
+ * PADRE (nuevo) Y la cadena ACTUAL del hijo en `scopes.moved` (origen y
96
+ * destino, 2E · H1: notifica ANTES de recolgar tu fila), lo mismo en
97
+ * `scopes.attached` cuando el hijo ya existe (es un move; un nodo nuevo
98
+ * solo contrasta el padre); el propio hijo en `scopes.detached`; el OWNER
99
+ * del rol en `defineScopedRole`/`updateScopedRole`/`deleteScopedRole`. Con
100
+ * `requireWithin: true` en el config, omitirlo es 422
101
+ * `E_AUTHZ_WITHIN_REQUIRED`; con `'non-root'`, además `APP_SCOPE` como
102
+ * `within` es 422 `E_AUTHZ_WITHIN_ROOT_FORBIDDEN` (no acota nada).
103
+ * `within` viene de la SESIÓN (el tenant autenticado), nunca del cuerpo
104
+ * de la petición: `within = scope` satisface siempre por definición.
105
+ */
106
+ within?: ScopeRef;
107
+ }
108
+ /**
109
+ * Opciones de las TRES notificaciones del árbol (`scopes.attached/moved/
110
+ * detached`) — 3b-2d. Añaden la transacción del consumidor, que solo se usa
111
+ * cuando hay `scopes.outbox` declarada: es lo que hace que el cambio del
112
+ * árbol y su encolado confirmen (o se vayan) juntos.
113
+ */
114
+ export interface ScopeTreeWriteOptions extends ScopedWriteOptions {
115
+ /**
116
+ * La transacción ABIERTA del consumidor (`TransactionClientContract` de
117
+ * Lucid, o lo que use su outbox). El paquete no la interpreta: se la pasa
118
+ * tal cual a `scopes.outbox.enqueue` para que el INSERT del encolado caiga
119
+ * dentro de ella. Sin outbox declarada no hace nada; con outbox declarada
120
+ * y sin transacción, el encolado se confirma solo y vuelve a haber dos
121
+ * confirmaciones distintas (la outbox lo avisa si puede).
122
+ */
123
+ transaction?: unknown;
124
+ }
125
+ export interface GrantOptions extends ScopedWriteOptions {
126
+ /**
127
+ * Caducidad de la asignación, en TRES estados (L0.4):
128
+ * - omitido: no tocar una caducidad vigente (si la asignación ya había
129
+ * expirado, revive sin caducidad);
130
+ * - `null`: quitar la caducidad;
131
+ * - `Date`: fijarla.
132
+ * Antes "omitido" borraba la caducidad: un "asegúrate de que tiene el rol"
133
+ * convertía un acceso temporal en permanente.
134
+ */
61
135
  expiresAt?: Date | null;
62
136
  }
137
+ /** Opciones de `deny` (2.1): contención y actor; sin caducidad (un deny que caduca es fail-open por reloj). */
138
+ export type DenyOptions = ScopedWriteOptions;
139
+ /** Lo que un `grant` hizo, para que el manager audite y el juez lo observe. */
140
+ export interface GrantOutcome {
141
+ /** Ya había una asignación de ese rol en ese scope exacto. */
142
+ existed: boolean;
143
+ /** Caducidad que tenía antes (solo si `existed` y se pudo leer). */
144
+ previousExpiresAt?: Date | null;
145
+ /** Caducidad con la que queda tras la escritura. */
146
+ expiresAt: Date | null;
147
+ }
148
+ /**
149
+ * Cómo se direcciona un rol en el puerto (`grant`, `revoke`, `hasRole`,
150
+ * `listSubjects`).
151
+ *
152
+ * - **string** (slug): en cada nivel de la cadena solo cuenta el rol de ESE
153
+ * nivel (el `owner` de app casa en app y hereda hacia abajo; un `owner` de
154
+ * organization jamás casa en app).
155
+ * - **`{ slug, scopeType }`**: el rol de un nivel concreto; solo los scopes
156
+ * de la cadena de ese tipo cuentan (L0.6).
157
+ * - **`{ uuid }`** (3D · M1): la forma EXACTA. Desde que un rol puede ser
158
+ * local a un scope (3B), el slug NO identifica un rol —dos tenants definen
159
+ * `lead@unit`— y un `scopes.moved` legítimo puede juntar dos homónimos en
160
+ * la misma cadena: entonces las dos formas por slug fallan cerradas con
161
+ * 422 `E_AUTHZ_AMBIGUOUS_ROLE` y esta es la única que responde. El uuid
162
+ * tiene que estar en el catálogo (422 `E_AUTHZ_UNKNOWN_ROLE`) y ser
163
+ * visible en el scope de la operación —declarado para su nivel y global o
164
+ * con el owner en la cadena— (422 `E_AUTHZ_ROLE_NOT_VISIBLE`).
165
+ */
166
+ export type RoleQuery = string | {
167
+ slug: string;
168
+ scopeType: ScopeType;
169
+ } | {
170
+ uuid: string;
171
+ };
172
+ /** `RoleQuery` ya validado (`normalizeRoleQuery`): o nombre, o identidad; nunca las dos. */
173
+ export type NormalizedRoleQuery = {
174
+ slug: string;
175
+ scopeType?: ScopeType;
176
+ uuid?: undefined;
177
+ } | {
178
+ uuid: string;
179
+ slug?: undefined;
180
+ scopeType?: undefined;
181
+ };
182
+ /**
183
+ * **Lo que un driver DECLARA de sí mismo** (3b-2e · E2). No es documentación:
184
+ * el manager lo LEE (el gate de deriva del árbol, E3) y la suite de contrato
185
+ * exige a cada capacidad su caso —el del valor declarado, nunca un `skip`—.
186
+ *
187
+ * Todas son opcionales de declarar (un driver de 2.x que no traiga
188
+ * `capabilities` se trata como todo `false`), pero declarar `true` lo que no
189
+ * se cumple es una promesa sin juez: el contrato lanza al registrarse.
190
+ */
191
+ export interface AuthorizationDriverCapabilities {
192
+ /**
193
+ * El ÁRBOL de scopes vive como hechos del backend y el backend es el PDP
194
+ * (`openfga` con `hierarchy: 'facts'`). Con `true` el manager exige la
195
+ * mitigación de la deriva (`scopes.outbox` o la firma explícita): el árbol
196
+ * está en dos sitios y un `rollback` del consumidor deja al backend
197
+ * adelantado (cruce 4 · S5).
198
+ */
199
+ hierarchyFacts: boolean;
200
+ /**
201
+ * `authorize` es UNA sola llamada al backend: no consulta el árbol del
202
+ * consumidor (`resolveChain`) y el catálogo solo a través del memo.
203
+ */
204
+ singleCheckAuthorize: boolean;
205
+ /**
206
+ * El backend resuelve la MEMBRESÍA por sí mismo. **`false` en los dos
207
+ * drivers del paquete, también en `facts`** (panel 2, cruce 6): `hasRole`,
208
+ * `listRoles`, `listRoleScopes`, `listSubjects` y `listScopes` siguen
209
+ * usando `resolveChain`. Por eso el titular «sin SQL en el camino caliente»
210
+ * está PROHIBIDO a secas: lo cierto es «sin SQL por request en `authorize`».
211
+ */
212
+ roleInheritanceNative: boolean;
213
+ /**
214
+ * Los `list*` enumeran también lo HEREDADO. **`false` siempre en este
215
+ * paquete** (invariante 7): enumerar descendientes sería abierto, y en
216
+ * `openfga` además obligaría a `ListObjects`, que trunca al tope del
217
+ * servidor sin ninguna señal (S16). Los `list*` devuelven hechos DIRECTOS.
218
+ */
219
+ listObjectsInherited: boolean;
220
+ /** El driver implementa `purgeRole` de verdad (sin él no hay roles locales). */
221
+ purgeRole: boolean;
222
+ /**
223
+ * El driver sabe CONTAR los hechos vigentes de un rol
224
+ * (`countRoleAssignments`, 3b-2j). Es lo que hace verdadero el
225
+ * `stillGranting` de `pruneOrphanRoles`, que se lee justo antes de un
226
+ * borrado destructivo. Con `false` el barrido no lo sabe y lo dice
227
+ * (`undefined`), nunca `false`: «no lo sé» no puede degradar a «no
228
+ * concede».
229
+ */
230
+ countRoleAssignments: boolean;
231
+ /**
232
+ * Las LECTURAS canonizan la ortografía del scope contra el árbol del
233
+ * consumidor antes de buscar los hechos (3b-2k · K1 · R2 (c)). Con `true`
234
+ * (driver `database`) `authorize` resuelve la cadena y usa `chain[0]`, la
235
+ * identidad canónica (invariante 17), así que un alias del uuid que TU
236
+ * tabla funde con la fila real —una columna `uuid` de PostgreSQL, una
237
+ * collation `*_ci` de MySQL— encuentra los mismos hechos. Con `false`
238
+ * (`openfga` en modo `facts`) la decisión no pasa por el árbol —es la
239
+ * contrapartida de `singleCheckAuthorize`— y el objeto del store se compone
240
+ * con la ortografía del LLAMANTE: un alias responde `false` donde la forma
241
+ * canónica concede. Es fail-CLOSED y no evade ningún deny, pero no es la
242
+ * misma respuesta: **pasa los uuids exactamente como los guarda tu tabla**.
243
+ * La ESCRITURA canoniza en los dos (3b-2h · 🟠 3).
244
+ */
245
+ canonicalScopeReads: boolean;
246
+ /**
247
+ * El driver sabe ser el **ORIGEN** de una migración: implementa
248
+ * `enumerateFacts` y entrega sus hechos paginados, sin filtrar y con su
249
+ * caducidad (3b-3b). Con `true` es lo que `authz:reconcile --to=<otro>`
250
+ * pasa como `source.facts`. Con `false` el driver no puede ser origen por
251
+ * el puerto y `authz:reconcile` lo DICE (500 `E_AUTHZ_UNSUPPORTED`
252
+ * nombrando `enumerateFacts`), nunca una migración vacía en silencio — que
253
+ * es exactamente el fail-dangerous que se evita: un origen que devuelve
254
+ * cero hechos y un `--prune` detrás borran el destino entero.
255
+ *
256
+ * **`false` en el driver `database` a propósito**: sus hechos son
257
+ * `authz_assignments`/`authz_denies`, el esquema publicado del paquete, y
258
+ * el destino los lee de ahí directamente (`openfga.reconcile`).
259
+ */
260
+ enumerateFacts: boolean;
261
+ }
63
262
  export interface AuthorizationDriver {
263
+ /** Lo que este driver declara poder hacer (3b-2e · E2). Ver `AuthorizationDriverCapabilities`. */
264
+ readonly capabilities?: AuthorizationDriverCapabilities;
64
265
  /**
65
266
  * ¿El holder tiene el permiso en el scope? Evalúa la cadena completa:
66
267
  * sin deny en la cadena Y alguna asignación vigente cuyo rol concede el
@@ -69,26 +270,43 @@ export interface AuthorizationDriver {
69
270
  authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
70
271
  /**
71
272
  * Asigna un rol al holder en un scope. El rol debe existir en el catálogo
72
- * para `scope.type` (throw si no). Idempotente: re-grant actualiza expiresAt.
273
+ * para `scope.type` (422 si no) y el scope para el resolutor (422 si no).
274
+ * Idempotente: re-grant no duplica; `expiresAt` sigue los tres estados de
275
+ * `GrantOptions`. Devuelve qué hizo (`GrantOutcome`).
73
276
  */
74
- grant(subject: SubjectRef, role: string, scope: ScopeRef, options?: GrantOptions): Promise<void>;
75
- /** Quita la asignación del rol en ese scope exacto. No-op si no existía. */
76
- revoke(subject: SubjectRef, role: string, scope: ScopeRef): Promise<void>;
277
+ grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
278
+ /**
279
+ * Quita la asignación del rol en ese scope exacto. El rol debe existir en
280
+ * el catálogo para `scope.type` (422 si no, como `grant`); la asignación
281
+ * puede no existir (no-op).
282
+ */
283
+ revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<void>;
77
284
  /**
78
285
  * ¿El holder tiene el rol (vigente) en el scope o en un ancestro?
79
- * Misma regla de herencia hacia abajo que `authorize`.
286
+ * Misma regla de herencia hacia abajo que `authorize`. Es MEMBRESÍA: el
287
+ * deny no la gobierna, así que nunca decide acceso (para eso, `authorize`).
80
288
  */
81
- hasRole(subject: SubjectRef, role: string, scope: ScopeRef): Promise<boolean>;
289
+ hasRole(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<boolean>;
82
290
  /**
83
291
  * Deny explícito de UN permiso al holder en un scope (y sus descendientes).
84
292
  * El permiso debe existir en el catálogo (throw si no). Idempotente.
85
293
  */
86
294
  deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
87
- /** Levanta el deny en ese scope exacto. No-op si no existía. */
295
+ /**
296
+ * Levanta el deny en ese scope exacto. El permiso debe existir en el
297
+ * catálogo (422 si no, como `deny`); el deny puede no existir (no-op).
298
+ */
88
299
  removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
89
300
  /** Holders con asignación VIGENTE del rol en ese scope exacto (sin herencia). */
90
- listSubjects(role: string, scope: ScopeRef): Promise<SubjectRef[]>;
91
- /** Roles (slugs) con asignación DIRECTA vigente del holder en ese scope exacto. */
301
+ listSubjects(role: RoleQuery, scope: ScopeRef): Promise<SubjectRef[]>;
302
+ /**
303
+ * Roles (slugs) con asignación DIRECTA vigente del holder en ese scope
304
+ * exacto. Es API de MEMBRESÍA y habla en slugs: desde 3B dos roles pueden
305
+ * compartir `(slug, nivel)` con owners distintos, así que un slug de esta
306
+ * lista puede no bastar para volver a direccionar el rol (`grant`/`hasRole`
307
+ * responderían 422 `E_AUTHZ_AMBIGUOUS_ROLE`). La forma sin ambigüedad es
308
+ * `{ uuid }`, y los uuids de la cadena los da `rolesInChain`.
309
+ */
92
310
  listRoles(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
93
311
  /**
94
312
  * Scopes del tipo dado donde el holder tiene alguna asignación DIRECTA
@@ -101,29 +319,837 @@ export interface AuthorizationDriver {
101
319
  * abierto): el caller consulta `authorize` sobre un scope concreto.
102
320
  */
103
321
  listScopes(subject: SubjectRef, permission: string): Promise<ScopeRef[]>;
322
+ /**
323
+ * Borra TODAS las asignaciones y denies del scope EXACTO cuyo rol/permiso
324
+ * está en el catálogo (no de sus descendientes: hasta que exista
325
+ * `descendantsOf`, Fase 2, el consumidor purga cada nodo del subárbol que
326
+ * borra). No consulta el árbol: el scope puede ya no existir para el
327
+ * resolutor. Debe demostrar que ESE conjunto quedó a cero o lanzar (500
328
+ * `E_AUTHZ_PURGE_INCOMPLETE`): un borrado parcial silencioso deja hechos
329
+ * huérfanos e indenegables. Los hechos de roles/permisos retirados del
330
+ * catálogo no son membresía ni conceden nada (las lecturas filtran por el
331
+ * catálogo, D5) y los recoge `authz:reconcile` (3b). La raíz no se purga
332
+ * (422).
333
+ */
334
+ purgeScope(scope: ScopeRef): Promise<void>;
335
+ /**
336
+ * Notificaciones del árbol del consumidor (`manager.scopes.*`), ya
337
+ * validadas por el paquete (raíz, existencia del padre, ciclos). Un driver
338
+ * que materializa el árbol como hechos propios (modo facts) las necesita;
339
+ * `database` no (lee el árbol vía `resolveChain`). Opcionales.
340
+ */
341
+ onScopeAttached?(child: ScopeRef, parent: ScopeRef): Promise<void>;
342
+ onScopeMoved?(child: ScopeRef, newParent: ScopeRef): Promise<void>;
343
+ /** Se llama DESPUÉS de `purgeScope` (hechos primero, arista al final: S6). */
344
+ onScopeDetached?(child: ScopeRef): Promise<void>;
345
+ /**
346
+ * Vista del driver que resuelve la CADENA con OTRO resolutor y comparte
347
+ * todo lo demás (conexión, memo del catálogo, deadline). Opcional (2.1):
348
+ * `AuthorizationManager.forRequest()` la usa para leer con un resolutor
349
+ * memoizado por request; un driver que no la implemente sigue funcionando
350
+ * (la vista lee con el driver tal cual, sin memo). Solo el camino de
351
+ * lectura pasa por aquí: las escrituras del manager van al driver original.
352
+ */
353
+ withChainResolver?(resolveChain: ScopeChainResolver): AuthorizationDriver;
354
+ /**
355
+ * Vista del driver que evalúa el TIEMPO con otro reloj (2.5 · J1) y
356
+ * comparte todo lo demás. `now()` es el instante de pared con el que se
357
+ * decide la caducidad —`expires_at > now` en SQL, `current_time` de cada
358
+ * check de FGA, el filtro de caducidad de las enumeraciones y los tres
359
+ * estados de `resolveGrantExpiry`—. Los sellos de auditoría (`created_at`)
360
+ * NO lo usan (2.5-B · K5): no son decisiones.
361
+ * Opcional: el manager lo aplica si el config trae `clock` (500
362
+ * `E_AUTHZ_CONFIG` si el driver no lo implementa: un reloj que no llega al
363
+ * driver mentiría). El juez lo usa con `injectableClock: true` para fijar
364
+ * la caducidad exacta sin dormir.
365
+ */
366
+ withClock?(now: () => Date): AuthorizationDriver;
367
+ /**
368
+ * Denies DIRECTOS vigentes del holder (2.1, B5): con `scope`, los de ese
369
+ * scope exacto (sin herencia, invariante 7; scope desconocido ⇒ `[]`); sin
370
+ * él, todos los del holder con su scope (los de scopes que el árbol ya no
371
+ * conoce no se listan, D8). Solo permisos del catálogo (D5). Opcional:
372
+ * ambos drivers del paquete lo implementan; sin él, `effectivePermissions`
373
+ * y `authorizedScopes` lanzan 500 `E_AUTHZ_UNSUPPORTED` (nunca un `[]`
374
+ * que significaría "sin denies": fail-open).
375
+ */
376
+ listDenies?(subject: SubjectRef, scope?: ScopeRef): Promise<DenyRef[]>;
377
+ /**
378
+ * `authorize` sobre varios scopes, un booleano por posición (2.1, B6).
379
+ * Opcional: el manager compone `Promise.all` de `authorize` sobre una
380
+ * vista memoizada si el driver no lo trae; `openfga` lo implementa con UN
381
+ * batchCheck para todos los scopes, correlacionado por id (L0.14). Misma
382
+ * respuesta que N `authorize`; si una posición no se puede responder
383
+ * (503), no se responde ninguna. Lista vacía ⇒ `[]` sin tocar el backend.
384
+ */
385
+ authorizeMany?(subject: SubjectRef, permission: string, scopes: ScopeRef[]): Promise<boolean[]>;
386
+ /**
387
+ * Purga un ROL del catálogo con sus hechos (3B · B4): revoca TODAS sus
388
+ * asignaciones en TODOS los scopes, borra sus vínculos rol→permiso y la
389
+ * fila del rol, atómicamente, y sube la versión compartida del catálogo
390
+ * (`withAuthzCatalogWrite`). Es lo que `deleteScopedRole` necesita: un rol
391
+ * borrado sin sus asignaciones dejaría hechos huérfanos que resucitarían
392
+ * al recrear el slug. `uuid` mal formado ⇒ 422 `E_AUTHZ_INVALID_IDENTITY`;
393
+ * desconocido ⇒ 422 `E_AUTHZ_UNKNOWN_ROLE`. No distingue global de local
394
+ * (esa barrera es del manager). Un driver que no pueda purgar (openfga
395
+ * hasta 3b: sus bindings no se enumeran por rol) lo DICE con 500
396
+ * `E_AUTHZ_UNSUPPORTED` y no toca nada — capacidad `purgeRole: false`.
397
+ *
398
+ * OPCIONAL en el puerto (3E · Q4): el manager ya lo trata como opcional
399
+ * (`#optional` ⇒ 500 `E_AUTHZ_UNSUPPORTED` nombrándolo) y declararlo
400
+ * obligatorio rompía al COMPILAR a todo driver de terceros escrito para
401
+ * 2.0/2.1. Un driver que no lo trae no puede tener roles locales:
402
+ * `defineScopedRole` lo dice antes de escribir nada (3E · P4).
403
+ */
404
+ purgeRole?(roleUuid: string): Promise<void>;
405
+ /**
406
+ * Cuántos hechos VIGENTES tiene cada rol, en TODOS los scopes (3b-2j,
407
+ * decisión del dueño del 2026-08-31 (3)). Un hecho es una asignación del
408
+ * rol a un holder que no ha caducado (`expiresAt` nulo o futuro, con el
409
+ * reloj del driver); el rol se identifica por su uuid y la respuesta va
410
+ * POR POSICIÓN, como `authorizeMany`. Un rol sin hechos —o que el backend
411
+ * no conoce— es `0`; `uuid` mal formado ⇒ 422 `E_AUTHZ_INVALID_IDENTITY`.
412
+ *
413
+ * Es lo que `pruneOrphanRoles` (`authz:catalog:prune-orphans`) necesita
414
+ * para decir si un rol huérfano TODAVÍA CONCEDE, y es una pregunta del
415
+ * PUERTO porque los hechos son del driver: hasta 3b-2j el barrido contaba
416
+ * filas de `authz_assignments` —la tabla del driver `database`— y con
417
+ * `openfga` en modo `facts`, donde viven en el store, decía siempre que
418
+ * no. El campo se lee justo antes de un borrado destructivo y su contrato
419
+ * publicado es «falso ⇒ este rol seguro que no concede», así que ese
420
+ * `false` era fail-dangerous.
421
+ *
422
+ * Es CONSERVADOR a propósito: cuenta hechos, no comprueba si el scope de
423
+ * cada uno sigue resolviendo. Cero ⇒ no concede seguro; más de cero ⇒
424
+ * míralo antes de purgar.
425
+ *
426
+ * OPCIONAL en el puerto (**breaking para un driver de 2.2 que no lo
427
+ * traiga**, y por eso opcional y no obligatorio): sin él
428
+ * `pruneOrphanRoles` deja `assignments` y `stillGranting` en `undefined`
429
+ * —jamás en `false`— y el comando lista esos roles APARTE, como los que sí
430
+ * conceden. Capacidad `countRoleAssignments`.
431
+ */
432
+ countRoleAssignments?(roleUuids: string[]): Promise<number[]>;
433
+ /**
434
+ * Rehace la **proyección derivada** del catálogo para UN rol (3b-2e · E4).
435
+ * Opcional: solo la implementa un driver que mantenga esa proyección (el
436
+ * `openfga` en modo `facts`, donde lo que un rol concede son tuplas y no
437
+ * el catálogo local). El manager la llama después de `defineScopedRole` y
438
+ * `updateScopedRole` —las dos escrituras de catálogo que cambian los
439
+ * vínculos de un rol fuera de `syncAuthzCatalog`—, porque si no un rol
440
+ * recién definido no concedería NADA y un rol al que se le quita un permiso
441
+ * lo seguiría concediendo (fail-open). En `database` no existe: el catálogo
442
+ * es la fuente y no hay espejo que rehacer.
443
+ */
444
+ projectCatalogRole?(roleUuid: string): Promise<void>;
445
+ /**
446
+ * La **proyección derivada del catálogo entero** de este driver (3b-2a ·
447
+ * A5), para inyectarla en `syncAuthzCatalog`/`syncCatalogs`. Opcional por
448
+ * el mismo motivo que `projectCatalogRole`: solo la trae un driver que
449
+ * mantenga un espejo del catálogo en su backend (el `openfga` en modo
450
+ * `facts`).
451
+ *
452
+ * Está en el PUERTO porque el camino de recuperación documentado —«un
453
+ * `authz:catalog:sync` reescribe la proyección»— lo ejecuta un comando que
454
+ * solo ve `AuthorizationDriver` (3b-8 · A1): sin esto, el CLI sincronizaba
455
+ * `authz_*` y dejaba el espejo del store SIN TOCAR, o sea que en `facts`
456
+ * un permiso quitado del catálogo seguía concediendo y un rol nuevo no
457
+ * concedía nada.
458
+ */
459
+ catalogProjection?(): CatalogProjection;
460
+ /**
461
+ * **Reconstruye el estado de ESTE driver desde `authz_*` + el árbol del
462
+ * consumidor** (3b-3a). Es lo que hay detrás de `authz:reconcile --to=<este
463
+ * driver>`: hechos, árbol y proyección del catálogo, idempotente
464
+ * (la segunda pasada escribe cero), reanudable por lotes con cursor y
465
+ * **nunca silenciosa** (el reporte cuenta lo escrito, lo actualizado, lo
466
+ * igual, lo que sobra, lo borrado y lo que NO se migró con su motivo).
467
+ *
468
+ * `dryRun` es el VERIFICADOR: mismo recorrido, cero escrituras. Es
469
+ * **read-only por contrato** (panel 2, cruce 4 · S18) — un `--fix` sería un
470
+ * mecanismo de concesión y queda PROHIBIDO.
471
+ *
472
+ * Opcional en el puerto: un driver que no lo trae dice «no sé
473
+ * reconstruirme» y el manager responde 500 `E_AUTHZ_UNSUPPORTED` nombrando
474
+ * el método, nunca una migración a medias en silencio. El driver
475
+ * `database` NO lo implementa: sus tablas SON el origen, y llenarlas desde
476
+ * un store es la otra dirección (3b-3b).
477
+ */
478
+ reconcile?(source: ReconcileSource, options: ReconcileOptions): Promise<ReconcileReport>;
479
+ /**
480
+ * **Los hechos de ESTE driver, paginados, para que otro se reconstruya
481
+ * desde ellos** (3b-3b). Es la otra mitad de `reconcile`: `reconcile` es
482
+ * ser el DESTINO de `authz:reconcile`, `enumerateFacts` es ser el ORIGEN.
483
+ *
484
+ * Contrato: como mucho `limit` hechos por página (más ⇒ 500), orden total
485
+ * y estable, cursor opaco que tiene que AVANZAR (repetirlo ⇒ 500, jamás un
486
+ * bucle), y **nada se filtra**: una asignación caducada sale con su
487
+ * `expiresAt` para que el destino la cuente en `skipped` con su motivo. Lo
488
+ * que el origen no sabe expresar como hecho del puerto sale en `skipped`
489
+ * de la página, nunca descartado en silencio.
490
+ *
491
+ * Opcional: capacidad `enumerateFacts`. El driver `database` **no lo
492
+ * trae** a propósito — sus hechos son `authz_assignments`/`authz_denies`,
493
+ * el esquema publicado del paquete, y el destino los lee de ahí (es lo que
494
+ * hace `openfga.reconcile`). Un driver de terceros que quiera migrar
495
+ * DESDE otro sitio sí lo necesita.
496
+ */
497
+ enumerateFacts?(page: {
498
+ limit: number;
499
+ after?: string;
500
+ }): Promise<ReconcileFactPage>;
501
+ /**
502
+ * Roles DIRECTOS vigentes del holder en cada scope de `chain` (2D · G5),
503
+ * como pares `{ scope, role }`; solo roles que EXISTEN en ese scope (D5 +
504
+ * 3B · B2: declarados para su nivel y visibles por owner desde ese nivel).
505
+ * Opcional: es lo que `effectivePermissions` usa para leer los roles de
506
+ * toda la cadena en UNA lectura; sin él, el manager compone N `listRoles`.
507
+ * La cadena llega ya resuelta y validada.
508
+ *
509
+ * Devuelve `CatalogRoleRef` (uuid + slug + nivel + owner), no un slug (3D ·
510
+ * M1): el manager usa el uuid tal cual y NUNCA vuelve del slug al catálogo
511
+ * en el camino de policy. Volver del slug hacía que `effectivePermissions`
512
+ * y `defineScopedRole` atribuyeran al holder los permisos de un homónimo
513
+ * (auditor V1: escalada reproducida).
514
+ */
515
+ rolesInChain?(subject: SubjectRef, chain: ScopeRef[]): Promise<Array<{
516
+ scope: ScopeRef;
517
+ role: CatalogRoleRef;
518
+ }>>;
519
+ }
520
+ /**
521
+ * Un subárbol excluido de un `all` (2.1, B3; tipo nominal desde 2D · F10):
522
+ * el scope con el deny vivo Y todos sus descendientes. No es una lista de
523
+ * scopes: un `NOT IN (uuids)` con solo `scope` seguiría listando las units
524
+ * de una organización denegada. Expándelo con
525
+ * `authorization.expandExcludedSubtrees(excluded)` (usa tu `descendantsOf`)
526
+ * o resta el subárbol en tu propia consulta (CTE recursiva, `path LIKE`…).
527
+ */
528
+ /**
529
+ * Lo que el manager le presta al driver para reconciliar: el árbol del
530
+ * consumidor (entero y paginado) y su resolutor. El driver pone lo suyo —qué
531
+ * hechos guarda y cómo—; el paquete no le dice cómo migrar, le da la FUENTE.
532
+ */
533
+ export interface ReconcileSource {
534
+ enumerateEdges: ScopeEdgesEnumerator;
535
+ resolveChain: ScopeChainResolver;
536
+ /**
537
+ * Los HECHOS del origen, paginados (3b-3b). Solo hace falta en la
538
+ * dirección en la que el origen NO es `authz_*`: `--to=database` los lee
539
+ * del store con este enumerador, mientras que `--to=openfga` lee las
540
+ * tablas del paquete directamente (son su propio esquema publicado, no el
541
+ * secreto de un driver).
542
+ *
543
+ * Es **perezoso a propósito**: el manager solo resuelve el driver de
544
+ * ORIGEN cuando el destino lo pide, así que una migración que no necesita
545
+ * hechos del puerto no construye nada. Sin origen que lo implemente, la
546
+ * primera llamada es 500 `E_AUTHZ_UNSUPPORTED` nombrando `enumerateFacts`.
547
+ */
548
+ facts?: ReconcileFactsEnumerator;
549
+ /**
550
+ * **Quién es la FUENTE DE VERDAD de los hechos en esta pasada** (3b-5, los
551
+ * dos 🔴 del auditor final). Lo decide el MANAGER, que es el único que sabe
552
+ * qué driver está sirviendo (`config.default`) y qué declara cada uno
553
+ * (`capabilities.hierarchyFacts`), y el destino lo OBEDECE.
554
+ *
555
+ * Sin esto, `--to=openfga` leía siempre `authz_assignments`/`authz_denies`,
556
+ * y en un despliegue `hierarchy: 'facts'` esas tablas **no son** la fuente
557
+ * de verdad de los hechos —lo son las tuplas del store—: la pasada
558
+ * reescribía lo revocado después del cutover, `--prune` borraba los denies
559
+ * vivos y el barrido de visibilidad del invariante 18 no se aplicaba nunca
560
+ * (`forbidden` salía vacío porque `wanted.facts` salía vacío).
561
+ *
562
+ * - `authzTables: true` ⇒ los hechos son las tablas del paquete y el
563
+ * destino las lee él mismo (es la MIGRACIÓN `database` → `openfga`);
564
+ * - `authzTables: false` ⇒ los hechos llegan por el PUERTO (`facts`,
565
+ * `enumerateFacts`) del origen `name`, que puede ser **el propio
566
+ * destino** cuando el destino es el driver ACTIVO y sus hechos son
567
+ * suyos (la pasada de MANTENIMIENTO: rehace lo derivado —marcador,
568
+ * catálogo, árbol y visibilidad— y no inventa ni borra un solo hecho).
569
+ *
570
+ * Ausente = `{ name: 'authz_*', authzTables: true }`: el comportamiento de
571
+ * 3b-3a, que es el que vale cuando el origen es el esquema publicado.
572
+ */
573
+ factsOrigin?: ReconcileFactsOrigin;
574
+ }
575
+ /** Ver `ReconcileSource.factsOrigin` (3b-5). */
576
+ export interface ReconcileFactsOrigin {
577
+ /** Cómo se NOMBRA el origen en el reporte (clave de `drivers`, o `authz_*`). */
578
+ name: string;
579
+ /** `true` ⇒ los hechos son `authz_assignments`/`authz_denies` y los lee el destino. */
580
+ authzTables: boolean;
581
+ }
582
+ /**
583
+ * Un hecho del ORIGEN en el vocabulario del PUERTO, no en el del backend
584
+ * (3b-3b). Es lo que un driver entrega cuando le toca ser el origen de una
585
+ * migración: el destino no sabe si detrás hay tuplas, filas o un fichero.
586
+ *
587
+ * La identidad del rol es el **uuid** (3D · M1), nunca el slug: dos owners
588
+ * definen `lead@unit` y el slug no identifica nada. La del permiso es el
589
+ * **slug**, que es lo que el catálogo local sabe traducir a uuid.
590
+ */
591
+ export interface ReconcileFact {
592
+ kind: 'assignment' | 'deny';
593
+ holder: SubjectRef;
594
+ /** El scope tal como lo guarda el ORIGEN; el destino lo canoniza con SU árbol. */
595
+ scope: ScopeRef;
596
+ /** `assignment`: uuid del rol. */
597
+ roleUuid?: string;
598
+ /** `deny`: slug del permiso. */
599
+ permission?: string;
600
+ /**
601
+ * `assignment`: la caducidad tal como está guardada, **sin filtrar**. Una
602
+ * caducada tiene que LLEGAR para poder contarse en `skipped` con su motivo;
603
+ * un origen que la filtre por su cuenta la haría desaparecer en silencio,
604
+ * que es justo lo que la migración no puede hacer.
605
+ */
606
+ expiresAt?: Date | null;
607
+ /** Cómo lo nombra el origen (para `details`): un motivo sin la fila no se arregla. */
608
+ detail: string;
609
+ }
610
+ /**
611
+ * Una página de hechos del origen. `cursor` es opaco y tiene que AVANZAR
612
+ * (repetirlo ⇒ 500, nunca un bucle); `skipped` es lo que el ORIGEN no supo
613
+ * expresar como hecho del puerto (basura de otra versión, un holder type que
614
+ * el config no declara…) y que el destino suma a su reporte.
615
+ */
616
+ export interface ReconcileFactPage {
617
+ facts: ReconcileFact[];
618
+ skipped?: ReconcileSkip[];
619
+ cursor?: string;
620
+ }
621
+ export type ReconcileFactsEnumerator = (page: {
622
+ limit: number;
623
+ after?: string;
624
+ }) => Promise<ReconcileFactPage>;
625
+ export interface ReconcileOptions {
626
+ /** Mismo recorrido, CERO escrituras. Es el verificador (read-only por contrato). */
627
+ dryRun?: boolean;
628
+ /**
629
+ * Borra del destino los HECHOS que el origen no respalda: los de un scope
630
+ * que ya no resuelve (3b-0b · AA4, «resurrección») y los que sobran (un
631
+ * store escrito por una versión anterior). Sin él se REPORTAN y no se
632
+ * borran. Lo derivado —marcador de raíz, proyección del catálogo y árbol—
633
+ * se rehace siempre: es un espejo de datos locales que nadie más escribe.
634
+ */
635
+ prune?: boolean;
636
+ /** La salida humana de `E_AUTHZ_MASS_RECONCILE_REFUSED`. */
637
+ allowMassDelete?: boolean;
638
+ /** Filas por lote en las lecturas del origen y por `Write` en el destino (default 100). */
639
+ batchSize?: number;
640
+ /**
641
+ * **La cota del volcado del destino** (3b-3b · B5). Reconciliar exige
642
+ * comparar contra el estado ENTERO del destino, y ese volcado entra en
643
+ * memoria: el ORIGEN se lee por lotes con cursor, el destino no. En vez de
644
+ * dejarlo como una sorpresa (un OOM en producción), se declara: pasar de
645
+ * `maxTuples` es 500 `E_AUTHZ_RECONCILE_TOO_LARGE` **antes de escribir
646
+ * nada**, nombrando la cota y cómo subirla. Default
647
+ * `DEFAULT_RECONCILE_MAX_TUPLES`.
648
+ */
649
+ maxTuples?: number;
650
+ }
651
+ /**
652
+ * Cuántas tuplas/filas del destino caben en una pasada de `authz:reconcile`
653
+ * (3b-3b · B5). No es una garantía de memoria: es la cota DECLARADA por
654
+ * encima de la cual la pasada se niega en vez de intentarlo.
655
+ */
656
+ export declare const DEFAULT_RECONCILE_MAX_TUPLES = 1000000;
657
+ /**
658
+ * Algo que la pasada NO migró (una fila del origen) o NO tocó (una tupla del
659
+ * destino), con su motivo. Nunca un contador a secas: un motivo sin la fila
660
+ * no se puede arreglar.
661
+ */
662
+ export interface ReconcileSkip {
663
+ kind: 'assignment' | 'deny' | 'edge' | 'tuple';
664
+ reason: string;
665
+ detail: string;
666
+ }
667
+ /** Los cinco números de una fase (o del total). */
668
+ export interface ReconcileCounts {
669
+ /** Tuplas nuevas en el destino. */
670
+ written: number;
671
+ /** Tuplas que estaban con OTRA caducidad y se han rehecho (delete + write). */
672
+ updated: number;
673
+ /** Tuplas que ya estaban exactamente igual. */
674
+ unchanged: number;
675
+ /** Tuplas del destino que el origen NO respalda. */
676
+ extra: number;
677
+ /** De las anteriores, las que la pasada borra (las que sobran de lo derivado, y con `prune` también los hechos). */
678
+ deleted: number;
679
+ }
680
+ /**
681
+ * Lo que movió una pasada de `authz:reconcile`. Los contadores describen el
682
+ * PLAN: con `dryRun` son exactamente los mismos números y no se escribe nada
683
+ * (lo dice `dryRun: true`), que es lo que hace del verificador un simulacro
684
+ * fiel y no una segunda implementación.
685
+ */
686
+ export interface ReconcileReport extends ReconcileCounts {
687
+ /** El driver de destino (`--to`). */
688
+ to: string;
689
+ /**
690
+ * **De dónde salieron los HECHOS de esta pasada** (3b-5): el nombre del
691
+ * driver ORIGEN, o `authz_*` si fueron las tablas del paquete. No es
692
+ * decoración: es la diferencia entre una migración y una pasada de
693
+ * mantenimiento contra el driver activo, y el comando la imprime — una
694
+ * pasada que lee los hechos del sitio equivocado no puede ser silenciosa.
695
+ */
696
+ factsFrom?: string;
697
+ /**
698
+ * **La garantía del freeze, publicada en vez de supuesta** (3b-7, juez C4).
699
+ * Solo en la pasada que ESCRIBE (el `--dry-run` no congela). `lapsed: true`
700
+ * significa que el lease se perdió a mitad —una pausa más larga que el
701
+ * lease, la base caída, otro dueño— y hubo una ventana en la que otros
702
+ * procesos pudieron escribir: la pasada NO se certifica y el comando sale
703
+ * distinto de cero. `leaseMs: null` = ventana sin renovación (el freeze de
704
+ * OPERADOR dentro del que corrió la pasada, o un lease infinito). Lo pone
705
+ * el MANAGER: el driver no sabe de ventanas.
706
+ */
707
+ frozen?: {
708
+ durable: boolean;
709
+ lapsed: boolean;
710
+ leaseMs: number | null;
711
+ fence: number;
712
+ };
713
+ dryRun: boolean;
714
+ prune: boolean;
715
+ /** Los mismos números por fase: qué es catálogo, qué es árbol y qué son hechos. */
716
+ phases: Record<'root' | 'catalog' | 'tree' | 'facts', ReconcileCounts>;
717
+ /**
718
+ * Motivo → cuántas cosas se quedaron fuera: filas del origen que no se
719
+ * migraron y tuplas del destino que esta pasada no tocó (`extra-fact`, las
720
+ * que solo se van con `--prune`).
721
+ */
722
+ skipped: Record<string, number>;
723
+ /** Y cuáles (acotado por `maxSkipDetails`): un contador no permite arreglar nada. */
724
+ details: ReconcileSkip[];
725
+ /** Ciclos del árbol del ORIGEN: sus aristas NO se escriben (FGA los evalúa y son fail-open). */
726
+ cycles: string[][];
727
+ drift: {
728
+ /** Faltaba el marcador de raíz: sin él el store entero DENIEGA (3b-2i). */
729
+ rootMarker: boolean;
730
+ /** Scopes con más de un padre en el destino (3b-2h · 🟠 4): cruce de tenants. */
731
+ multiParent: string[];
732
+ /**
733
+ * Aristas `scope#binding` que el destino tenía mal (invariante 18): la
734
+ * escritura de visibilidad que `scopes.moved`/`projectCatalogRole`
735
+ * pudieron perder si el relay no pasó.
736
+ */
737
+ roleVisibility: number;
738
+ /** Cambios del árbol encolados y sin relevar: la VENTANA del relay, medida. */
739
+ pendingRelay: number;
740
+ /** Entradas APARCADAS de la outbox: divergencia permanente, no una ventana. */
741
+ deadRelay: number;
742
+ };
743
+ /** La pasada tiene la firma de un origen ciego (ver `E_AUTHZ_MASS_RECONCILE_REFUSED`). */
744
+ massDelete: boolean;
104
745
  }
746
+ export interface ExcludedSubtree {
747
+ scope: ScopeRef;
748
+ /** Siempre `true`: recuerda que lo excluido es el subárbol entero. */
749
+ includesDescendants: true;
750
+ }
751
+ /**
752
+ * Respuesta de `authorizedScopes(subject, permission, scopeType)` (2.1, B3):
753
+ * - `none`: ningún scope de ese tipo;
754
+ * - `some`: exactamente estos (directos del tipo + descendientes vía
755
+ * `descendantsOf`, menos los que tienen un deny en su cadena), nunca más
756
+ * de `maxScopes`. Coherente con `authorize` scope a scope cuando
757
+ * `descendantsOf` y `resolveChain` describen el mismo árbol; si
758
+ * discrepan, lanza 503 `E_AUTHZ_RESOLVER_FAILED` (2D · F3);
759
+ * - `all`: hay un grant vigente en la raíz `app` (ancestro común de todo el
760
+ * tipo) — MENOS `excludedSubtrees`: los scopes con deny vivo del permiso,
761
+ * cada uno con su subárbol entero. Nunca `all` a secas con denies vivos
762
+ * (juez cruce 5, auditor E1): quien liste "todo" tiene que restar esto.
763
+ */
764
+ export type AuthorizedScopes = {
765
+ kind: 'none';
766
+ } | {
767
+ kind: 'some';
768
+ scopes: ScopeRef[];
769
+ } | {
770
+ kind: 'all';
771
+ excludedSubtrees: ExcludedSubtree[];
772
+ };
773
+ /** Un deny directo, tal como lo enumera `listDenies` (2.1). */
774
+ export interface DenyRef {
775
+ permission: string;
776
+ scope: ScopeRef;
777
+ }
778
+ /**
779
+ * Mapa morph name → tipo del modelo FGA (`users` → `user`). Lo consume el
780
+ * driver `openfga` (subpath `@jantstack/adonis-authz/openfga`); vive en el
781
+ * puerto para que `defineConfig` lo tipe sin importar el driver (D9).
782
+ */
783
+ export type HolderTypeMap = Record<string, string>;
105
784
  /** Factory registrable en `config/authorization.ts`. */
106
785
  export type AuthorizationDriverFactory = () => AuthorizationDriver | Promise<AuthorizationDriver>;
107
786
  /**
108
- * Resolutor de ancestros de un scope (del más cercano a la raíz). El paquete
787
+ * Resolutor de la CADENA de un scope (2.5-B · K1): `[scope canónico,
788
+ * ...ancestros]`, del más cercano a la raíz, con `app` al final. El paquete
109
789
  * NO conoce el dominio del consumidor: el chasis inyecta el suyo (que sabe de
110
- * organizations/organization_units) al construir cada driver en el config.
111
- * Default de los drivers sin resolutor: `app` → [], resto → [APP_SCOPE].
790
+ * organizations/organization_units) al construir cada driver y el manager.
791
+ *
792
+ * El elemento 0 es el PROPIO scope tal como está en la tabla del consumidor
793
+ * (la fila leída), no tal como lo escribió el llamante: es la identidad con
794
+ * la que el paquete lee y escribe todos los hechos. Un motor que canoniza
795
+ * ids (el tipo `uuid` de PostgreSQL, una collation `*_ci`) puede encontrar
796
+ * la fila para un alias (mayúsculas, guiones quitados); devolverla canónica es
797
+ * lo que hace que el deny escrito con la forma real siga casando. Devolver
798
+ * otro scope como elemento 0 es 503 `E_AUTHZ_RESOLVER_FAILED`.
799
+ *
800
+ * `null` significa "este scope no existe": el motor deniega (`authorize`/
801
+ * `hasRole` → false) y rechaza escribir sobre él (`grant`/`deny` → 422
802
+ * `E_AUTHZ_UNKNOWN_SCOPE`). Ya no hay default plano: un driver sin resolutor
803
+ * solo conoce la raíz `app`, y cualquier otro tipo es 422
804
+ * `E_AUTHZ_NO_SCOPE_RESOLVER` (L0.3). Un resolutor que devuelva `[scope,
805
+ * APP_SCOPE]` para lo que no conoce vuelve a abrir el defecto: es su
806
+ * responsabilidad no hacerlo, y el vocabulario para no hacerlo es `null`. La
807
+ * raíz nunca se pregunta: su cadena es `[APP_SCOPE]` por definición.
112
808
  */
113
- export type ScopeAncestorsResolver = (scope: ScopeRef) => Promise<ScopeRef[]>;
809
+ export type ScopeChainResolver = (scope: ScopeRef) => Promise<ScopeRef[] | null>;
810
+ /**
811
+ * Una arista del árbol del consumidor: «`child` cuelga de `parent`» (3b-3a).
812
+ * `parent` puede ser `APP_SCOPE`; `child` nunca es la raíz.
813
+ */
814
+ export interface ScopeEdge {
815
+ child: ScopeRef;
816
+ parent: ScopeRef;
817
+ }
818
+ /** Una página de `scopes.enumerateEdges`. Sin `cursor` = no queda nada más. */
819
+ export interface ScopeEdgePage {
820
+ edges: ScopeEdge[];
821
+ /**
822
+ * Continuación OPACA para la siguiente llamada (`after`). Ausente o
823
+ * `undefined` significa «se acabó»: devolver siempre un cursor es un bucle
824
+ * infinito, y el llamante lo denuncia (500) si el cursor no avanza.
825
+ */
826
+ cursor?: string;
827
+ }
828
+ /**
829
+ * **El árbol ENTERO, paginado** (3b-3a). Es la otra mitad de
830
+ * `resolveChain`: aquel responde «¿de qué cuelga ESTE scope?» y este
831
+ * «¿cuáles son todas las aristas?», que es lo que hace falta para
832
+ * reconstruir el árbol de un backend que lo guarda como hechos propios
833
+ * (`authz:reconcile --to=openfga`) y para ver las que sobran (las que el
834
+ * consumidor ya no respalda).
835
+ *
836
+ * Contrato:
837
+ * - devuelve **como mucho `limit`** aristas por página (más ⇒ 500: el
838
+ * llamante no puede paginar lo que no cabe en su lote);
839
+ * - el orden tiene que ser TOTAL y ESTABLE entre llamadas (la clave
840
+ * primaria vale): si no, una pasada reanudada se salta nodos;
841
+ * - `cursor` es opaco para el paquete y vuelve tal cual en `after`; que no
842
+ * avance es 500, nunca un bucle;
843
+ * - una arista cuyo padre no existe en la tabla NO se emite (es un nodo que
844
+ * `resolveChain` tampoco resuelve): el destino la ve como sobrante y
845
+ * `authz:reconcile` la cuenta y la reporta.
846
+ *
847
+ * Sin él, `authz:reconcile --to=openfga` no puede migrar el árbol y lo dice
848
+ * (500 `E_AUTHZ_CONFIG`): NO se inventa un árbol plano.
849
+ * `sqlScopeEdges(...)` lo implementa sobre una tabla con columna padre.
850
+ */
851
+ export type ScopeEdgesEnumerator = (options: {
852
+ limit: number;
853
+ after?: string;
854
+ }) => Promise<ScopeEdgePage>;
855
+ /**
856
+ * Un cambio del ÁRBOL, tal como lo encola la outbox (3b-2d). Es exactamente
857
+ * lo que el consumidor notifica por `manager.scopes.*`, con la identidad ya
858
+ * CANÓNICA (invariante 17): se resuelve al encolar, mientras la fila del
859
+ * consumidor todavía existe, no al relevarla.
860
+ */
861
+ export type ScopeTreeChange = {
862
+ op: 'attached';
863
+ child: ScopeRef;
864
+ parent: ScopeRef;
865
+ } | {
866
+ op: 'moved';
867
+ child: ScopeRef;
868
+ parent: ScopeRef;
869
+ } | {
870
+ op: 'detached';
871
+ child: ScopeRef;
872
+ };
873
+ /** Un cambio pendiente en la outbox, con la identidad de su registro. */
874
+ export interface PendingScopeTreeChange {
875
+ /** Identificador estable del registro; el relay lo devuelve al marcarlo. */
876
+ id: string | number;
877
+ change: ScopeTreeChange;
878
+ /** Intentos fallidos previos, si la outbox los lleva (el reporte los muestra). */
879
+ attempts?: number;
880
+ /** La última causa de fallo, si la outbox la guarda (`dead()` la enseña). */
881
+ lastError?: string;
882
+ /**
883
+ * Quién ordenó el cambio, si el call-site lo declaró y la outbox lo
884
+ * guarda. El relay lo pone en el `AuthzWriteEvent` del `scope_purged` que
885
+ * dispara un `detached`: la auditoría no debe perder al autor por pasar
886
+ * por una cola.
887
+ */
888
+ actor?: SubjectRef;
889
+ }
890
+ /** Lo que el relay aplicó (o aplicaría), pieza a pieza. */
891
+ export interface RelayedScopeChange {
892
+ id: string | number;
893
+ change: ScopeTreeChange;
894
+ attempts?: number;
895
+ /** La causa, en lo que FALLÓ, se APARCÓ o se APLAZÓ (nunca en lo aplicado). */
896
+ error?: string;
897
+ }
898
+ /**
899
+ * Reporte de `authz:scopes:relay` (3b-2d; 3b-2h · 🔴 2). Dice QUÉ se aplicó,
900
+ * no un contador: la pasada no es atómica y un número no permite retomar nada.
901
+ */
902
+ export interface ScopeRelayReport {
903
+ /** Aplicados en esta pasada, en orden. Vacío en `dryRun`. */
904
+ applied: RelayedScopeChange[];
905
+ /**
906
+ * El PRIMER cambio que falló, con la causa (`failures[0]`). Se conserva
907
+ * porque es lo que mira un supervisor; la lista completa está en
908
+ * `failures`.
909
+ */
910
+ failed: {
911
+ id: string | number;
912
+ change: ScopeTreeChange;
913
+ error: string;
914
+ } | null;
915
+ /**
916
+ * TODO lo que falló en esta pasada (3b-2h · 🔴 2). Un fallo ya no para la
917
+ * pasada entera: para lo que DEPENDE de él —los cambios que nombran alguno
918
+ * de sus scopes, que salen en `deferred`— y el resto sigue.
919
+ */
920
+ failures: Array<{
921
+ id: string | number;
922
+ change: ScopeTreeChange;
923
+ error: string;
924
+ }>;
925
+ /**
926
+ * Lo que NO se intentó porque toca un scope contaminado por un fallo o por
927
+ * otro aplazado de esta misma pasada. Es lo que mantiene el ORDEN del árbol
928
+ * (aplicar un `moved` antes que el `attached` de su padre da un árbol que
929
+ * nunca existió) sin dejar que una fila envenenada bloquee a los demás.
930
+ */
931
+ deferred: RelayedScopeChange[];
932
+ /**
933
+ * Entradas APARCADAS por la outbox tras agotar sus intentos (`dead()`), si
934
+ * la implementación lo soporta. No se van a aplicar solas: el árbol del
935
+ * backend está permanentemente divergente en esos nodos y hay que mirarlas.
936
+ */
937
+ dead: RelayedScopeChange[];
938
+ /**
939
+ * Otra pasada tenía el lease de la cola y esta no ha hecho NADA (3b-2h ·
940
+ * 🟠 4). No es un error: el relay es escritor ÚNICO.
941
+ */
942
+ busy: boolean;
943
+ /** Quedan cambios sin aplicar tras la pasada (vuelve a ejecutar). */
944
+ remaining: boolean;
945
+ dryRun: boolean;
946
+ /** Solo con `dryRun`: lo que se aplicaría, en orden. */
947
+ wouldApply: RelayedScopeChange[];
948
+ }
949
+ /**
950
+ * El lease de una pasada del relay (3b-2h · 🟠 4). Lo devuelve
951
+ * `ScopeOutbox.acquire()` y lo suelta el manager en un `finally`.
952
+ */
953
+ export interface ScopeOutboxLease {
954
+ release(): Promise<void>;
955
+ }
956
+ /** Contexto del encolado: la transacción del consumidor y quién lo ordena. */
957
+ export interface ScopeOutboxContext {
958
+ /**
959
+ * Lo que el llamante pasó en `ScopeTreeWriteOptions.transaction`: para
960
+ * Lucid, el `TransactionClientContract` de la transacción en curso. El
961
+ * paquete no lo interpreta —no conoce la BD del consumidor—: lo pasea.
962
+ */
963
+ transaction?: unknown;
964
+ actor?: SubjectRef;
965
+ }
966
+ /**
967
+ * **El puerto de la outbox del árbol** (3b-2d, panel 2 cruce 4 · S5).
968
+ *
969
+ * Sin él, `manager.scopes.attached/moved/detached` escribe en el backend
970
+ * DENTRO de la transacción del consumidor y un `rollback` posterior deja el
971
+ * árbol de FGA diciendo una cosa y la BD del consumidor otra —una escalada
972
+ * persistente e invisible, porque la aplicación lista y audita contra SQL—.
973
+ * Con él, el manager no toca el driver: ENCOLA el cambio con la transacción
974
+ * del consumidor, así que el cambio del árbol y su intención de propagación
975
+ * confirman o se van juntos. Lo aplica después `authz:scopes:relay`.
976
+ *
977
+ * El paquete no impone tabla: define este puerto y publica un stub de
978
+ * migración (`stubs/scopes_outbox_migration.stub`) y una implementación
979
+ * sobre Lucid (`sqlScopeOutbox`) para quien no quiera escribir la suya.
980
+ *
981
+ * Lo que NO arregla, y hay que leerlo así: durante el lag del relay
982
+ * (segundos) FGA decide con el árbol VIEJO. Es un fail-open temporal — el
983
+ * tenant antiguo conserva acceso tras un `moved`, y los denies heredados no
984
+ * aplican tras un `attached`—. No hay 2PC; es el precio de tener el árbol en
985
+ * dos sitios.
986
+ */
987
+ export interface ScopeOutbox {
988
+ /**
989
+ * Encola el cambio en la transacción del consumidor. Debe escribir y
990
+ * volver: nada de aplicarlo aquí. Si lanza, la escritura del manager falla
991
+ * (y la transacción del consumidor se lleva las dos cosas).
992
+ */
993
+ enqueue(change: ScopeTreeChange, context: ScopeOutboxContext): Promise<void>;
994
+ /**
995
+ * Los pendientes MÁS ANTIGUOS primero: el orden del árbol es el del
996
+ * encolado. `after` (3b-2h · 🔴 2) es el id del último registro que el
997
+ * relay ya vio en ESTA pasada: como una entrada que falla ya no para la
998
+ * pasada, se queda pendiente y volvería a salir la primera para siempre.
999
+ * Una implementación que lo ignore sigue siendo válida —el relay detecta
1000
+ * que no avanza y termina la pasada—, pero solo drenará hasta el primer
1001
+ * lote atascado.
1002
+ */
1003
+ pending(limit: number, after?: string | number): Promise<PendingScopeTreeChange[]>;
1004
+ /** Aplicado en el backend: no se vuelve a relevar. */
1005
+ markApplied(id: string | number): Promise<void>;
1006
+ /** Falló al aplicarse: se queda pendiente, con la causa a la vista. */
1007
+ markFailed(id: string | number, error: string): Promise<void>;
1008
+ /**
1009
+ * **Las entradas APARCADAS** (3b-2h · 🔴 2), opcional. Una entrada que ya
1010
+ * no se puede aplicar —su scope padre se borró antes de la pasada— no se
1011
+ * arregla sola: la outbox puede dejar de ofrecerla en `pending()` tras N
1012
+ * intentos y enseñarla aquí. El relay las REPORTA en cada pasada y el
1013
+ * comando sale ≠ 0 mientras haya alguna: un aparcado es una divergencia
1014
+ * permanente del árbol del backend, no un incidente resuelto.
1015
+ */
1016
+ dead?(limit: number): Promise<PendingScopeTreeChange[]>;
1017
+ /**
1018
+ * **El lease del escritor ÚNICO** (3b-2h · 🟠 4), opcional. `pending()` no
1019
+ * reserva nada, así que dos pasadas a la vez (un `CronJob` con
1020
+ * `concurrencyPolicy: Allow`, dos réplicas, una pasada más larga que su
1021
+ * intervalo) trabajan sobre el MISMO lote: la rezagada re-aplica un
1022
+ * `attached` viejo después de que la otra aplicara el `moved` nuevo y deja
1023
+ * el árbol del store REVERTIDO —con un solo padre, así que nada lo
1024
+ * delata— (medido). Con `acquire`, la segunda pasada no hace nada y lo
1025
+ * dice (`busy`). `null` = otra pasada lo tiene.
1026
+ *
1027
+ * CONTRATO: el lease se toma UNA vez al inicio de la pasada y se sostiene
1028
+ * hasta el `finally`; el relay NO lo re-verifica ni lo renueva dentro del
1029
+ * bucle (a diferencia del freeze durable, que sí se re-afirma por lote).
1030
+ * Por eso la implementación DEBE ser un cerrojo SOSTENIDO mientras dura la
1031
+ * pasada, no un TTL que pueda vencer a mitad: los que trae el paquete lo
1032
+ * cumplen (`pg_try_advisory_xact_lock` vive con la transacción; `get_lock`
1033
+ * de MySQL con la sesión; SQLite en proceso). Un `acquire` con TTL
1034
+ * reabriría la ventana del doble escritor que este lease cierra.
1035
+ */
1036
+ acquire?(): Promise<ScopeOutboxLease | null>;
1037
+ }
1038
+ /**
1039
+ * El árbol del consumidor hacia ABAJO (2.1, B2): todos los descendientes de
1040
+ * `scope` (cualquier tipo, cualquier profundidad), en cualquier orden y sin
1041
+ * incluirlo. Lo implementa el consumidor (o `sqlDescendantsOf`, el helper
1042
+ * opt-in del paquete): el paquete NO lo suple con N+1 llamadas a
1043
+ * `resolveChain`. `null` = «este árbol no conoce ese scope».
1044
+ *
1045
+ * Más de `maxNodes` nodos ⇒ el resolutor puede devolver la lista larga (el
1046
+ * paquete la caza con 422 `E_AUTHZ_TOO_MANY_SCOPES`) o lanzar; en
1047
+ * `authorizedScopes` eso es un 422 y en `defineScopedRole`/`updateScopedRole`
1048
+ * DEGRADA a la regla de nivel mínima (3F · S2, y ver el aviso de
1049
+ * `#assertLevelUnderOwner`).
1050
+ *
1051
+ * Solo se llama desde `authorizedScopes`/`expandExcludedSubtrees` y desde la
1052
+ * regla de nivel de la delegación; NUNCA desde `authorize`/`hasRole`/`list*`
1053
+ * (test de arquitectura) ni desde `scopes.detached`, que purga hechos del
1054
+ * scope EXACTO y no baja por el árbol (invariante 11; 3b-0 · Z1).
1055
+ *
1056
+ * (D7: hasta 3G había DOS docblocks seguidos aquí y el viejo contradecía al
1057
+ * nuevo sobre qué se espera al pasarse de `maxNodes`. Queda uno.)
1058
+ */
1059
+ export type ScopeDescendantsResolver = (scope: ScopeRef, options: {
1060
+ maxNodes: number;
1061
+ }) => Promise<ScopeRef[] | null>;
114
1062
  /**
115
1063
  * Escritura del motor, notificada al hook `onWrite` del config. El chasis lo
116
1064
  * usa para auditar/emitir SSE; un consumidor puede loguear, notificar, etc.
117
1065
  */
118
1066
  export interface AuthzWriteEvent {
119
- action: 'granted' | 'revoked' | 'denied' | 'deny_removed';
120
- subject: SubjectRef;
1067
+ /**
1068
+ * `extended`: un re-grant cambió la caducidad de una asignación que ya
1069
+ * existía (alargada, acortada o quitada) — lleva `previousExpiresAt`. Un
1070
+ * re-grant que no cambia nada sigue siendo `granted` (idempotente).
1071
+ * `scope_purged`: `scopes.detached` borró todos los hechos del scope; no
1072
+ * lleva `subject` (afecta a todos los holders del scope).
1073
+ */
1074
+ action: 'granted' | 'extended' | 'revoked' | 'denied' | 'deny_removed' | 'scope_purged';
1075
+ /** Ausente solo en `scope_purged`. */
1076
+ subject?: SubjectRef;
121
1077
  scope: ScopeRef;
122
- /** Presente en granted/revoked. */
123
- role?: string;
1078
+ /**
1079
+ * Quién ordenó la escritura (2.1, B7): lo que el llamante pasó en
1080
+ * `WriteOptions.actor`, ya validado. Ausente si no lo pasó.
1081
+ */
1082
+ actor?: SubjectRef;
1083
+ /**
1084
+ * Presente en granted/extended/revoked: el/los rol(es) RESUELTOS (3E · Q7,
1085
+ * auditor A8), con `uuid`, `slug`, nivel y owner — no la pregunta cruda.
1086
+ *
1087
+ * En 1.x era `role: string` (el slug) y en 3D pasó a `RoleQuery`: un sink
1088
+ * de auditoría que filtraba por slug dejó de casar EN SILENCIO, que es una
1089
+ * pérdida de auditoría, no solo de tipos. Con la forma resuelta el sink
1090
+ * vuelve a tener el slug —`event.roles.some((r) => r.slug === 'admin')`— y
1091
+ * además el uuid, que es lo único que identifica un rol desde 3A.
1092
+ *
1093
+ * Es una LISTA porque un `revoke` por slug quita los hechos de TODOS los
1094
+ * homónimos visibles en el scope (3B); un `grant` resuelve exactamente uno
1095
+ * (con dos sería 422 `E_AUTHZ_AMBIGUOUS_ROLE`). Ausente si el rol no se
1096
+ * pudo resolver (scope que el árbol no conoce, rol fuera del catálogo): el
1097
+ * driver decidirá el resultado, y el evento no inventa.
1098
+ */
1099
+ roles?: CatalogRoleRef[];
124
1100
  /** Presente en denied/deny_removed. */
125
1101
  permission?: string;
1102
+ /** Caducidad con la que queda la asignación (granted/extended). */
126
1103
  expiresAt?: Date | null;
1104
+ /** Caducidad que tenía antes (solo extended). */
1105
+ previousExpiresAt?: Date | null;
1106
+ /**
1107
+ * `true` cuando la escritura venció el deadline (503 `E_AUTHZ_BACKEND_TIMEOUT`)
1108
+ * y el paquete NO sabe si el backend la aplicó: la petición puede aterrizar
1109
+ * después de que el llamante recibiera el error. Se notifica ANTES de
1110
+ * propagar el 503 para que la auditoría registre un resultado desconocido
1111
+ * en vez de un silencio (que se lee como "no pasó nada"). Un 503 que no es
1112
+ * timeout (conexión rechazada) no lo lleva: esa escritura no ocurrió.
1113
+ */
1114
+ indeterminate?: boolean;
1115
+ }
1116
+ /** Un rol del catálogo tal como lo ve el motor (3A · A2/A3, 3B · B2). */
1117
+ export interface CatalogRole {
1118
+ /** Identidad interna: lo que llevan `authz_assignments.role_uuid` y los ids de binding de FGA. */
1119
+ uuid: string;
1120
+ slug: string;
1121
+ scopeType: ScopeType;
1122
+ /** `'global'` o `scopeKey(owner)` (`<tipo>|<uuid>`): el contenedor fuera del cual el rol no existe. */
1123
+ owner: string;
1124
+ /** Metadata de policy (invariante 8): el motor no lo evalúa en `authorize`. */
1125
+ rank: number;
1126
+ }
1127
+ /**
1128
+ * La IDENTIDAD pública de un rol sin su metadata de policy (3D · M1): lo que
1129
+ * devuelve `rolesInChain` y lo que el memo usa para filtrar por owner. El
1130
+ * uuid manda; el slug viaja como etiqueta legible.
1131
+ */
1132
+ export interface CatalogRoleRef {
1133
+ slug: string;
1134
+ uuid: string;
1135
+ scopeType: ScopeType;
1136
+ /** `'global'` o `scopeKey(owner)`. */
1137
+ owner: string;
1138
+ }
1139
+ export interface CatalogPermissionSpec {
1140
+ /** Formato `recurso:accion`. */
1141
+ slug: string;
1142
+ description?: string | null;
1143
+ /**
1144
+ * Niveles (scope types) cuyos roles PUEDEN llevar este permiso (3B · B5):
1145
+ * omitido = cualquiera. Es un control de COMPOSICIÓN: `syncAuthzCatalog`,
1146
+ * `defineScopedRole`/`updateScopedRole` y `grant` rechazan (422
1147
+ * `E_AUTHZ_ROLE_NOT_ASSIGNABLE_AT`) un rol de otro nivel que lo lleve;
1148
+ * `authorize` NO lo mira (invariante 1: lo ya asignado sigue concediendo).
1149
+ * Es lo que cubre «un rol de unit no puede llevar org:settings» sin romper
1150
+ * la herencia hacia abajo (panel 2026-08-28, H).
1151
+ */
1152
+ assignableAt?: ScopeType[];
127
1153
  }
128
1154
  export interface CatalogRoleSpec {
129
1155
  /** UUID fijo opcional (mismo patrón que organization_acl: estable entre entornos). */
@@ -144,10 +1170,269 @@ export interface CatalogRoleSpec {
144
1170
  }
145
1171
  export interface CatalogSpec {
146
1172
  /** Todos los permisos del catálogo (formato `recurso:accion`). */
147
- permissions: Array<{
148
- slug: string;
149
- description?: string | null;
150
- }>;
1173
+ permissions: CatalogPermissionSpec[];
1174
+ /** Roles GLOBALES (owner `global`): un spec nunca declara roles locales. */
151
1175
  roles: CatalogRoleSpec[];
152
1176
  }
1177
+ /**
1178
+ * Un rol LOCAL a un scope, tal como lo define `defineScopedRole(actor,
1179
+ * ownerScope, spec)` (3B · B3). Sin `uuid` (lo genera el motor) y con `rank`
1180
+ * OBLIGATORIO: `0 < rank < min(rank del actor, rank máximo global)`.
1181
+ */
1182
+ export interface ScopedRoleSpec {
1183
+ slug: string;
1184
+ /** Nivel al que el rol es asignable (dentro del owner). Nunca `app`. */
1185
+ scopeType: ScopeType;
1186
+ name?: string;
1187
+ description?: string | null;
1188
+ rank: number;
1189
+ /** Slugs de permisos: ⊆ `config.delegablePermissions` ∩ efectivos del actor en el owner. */
1190
+ permissions: string[];
1191
+ }
1192
+ /** Lo que `updateScopedRole` puede cambiar de un rol local: nunca su slug, nivel ni owner. */
1193
+ export interface ScopedRoleChanges {
1194
+ name?: string;
1195
+ description?: string | null;
1196
+ rank?: number;
1197
+ permissions?: string[];
1198
+ }
1199
+ /**
1200
+ * Escritura del CATÁLOGO por la API de delegación (3B · B3), notificada al
1201
+ * hook `onCatalogWrite` del config. Siempre lleva `actor` (la API lo exige)
1202
+ * y el rol tal como queda (`role_purged`: tal como estaba).
1203
+ */
1204
+ export interface AuthzCatalogWriteEvent {
1205
+ action: 'role_defined' | 'role_updated' | 'role_purged';
1206
+ /**
1207
+ * Quién lo ordenó. La API de delegación lo exige siempre; ausente en los
1208
+ * `role_purged` de `authz:catalog:prune-orphans` (3b-0 · Z2), que es una
1209
+ * operación de PLATAFORMA y no de un actor del árbol.
1210
+ */
1211
+ actor?: SubjectRef;
1212
+ role: CatalogRole;
1213
+ /** El scope owner del rol (`scopeFromKey(role.owner)`). */
1214
+ owner: ScopeRef;
1215
+ /** Permisos con los que queda el rol (o tenía, si se purga). */
1216
+ permissions: string[];
1217
+ /**
1218
+ * Solo en `role_defined` (3F · S3): los roles LOCALES de un DESCENDIENTE
1219
+ * del owner con ese mismo `(slug, nivel)` que el nuevo acaba de
1220
+ * ENSOMBRECER. La autoridad manda —global > local de un ancestro > local
1221
+ * de un descendiente—, así que el dueño del árbol siempre puede definir su
1222
+ * rol aunque alguien de abajo le haya ocupado el nombre; dentro del
1223
+ * subárbol de esos owners toda ruta por slug pasa a 422
1224
+ * `E_AUTHZ_AMBIGUOUS_ROLE` (se opera por `{ uuid }`) hasta que se purgue
1225
+ * uno. Es el mismo trato que `shadowedByGlobal` en el sync, y como allí:
1226
+ * se REPORTA, nunca en silencio.
1227
+ */
1228
+ shadowedByAncestor?: CatalogRoleRef[];
1229
+ }
1230
+ /**
1231
+ * Un rol del catálogo tal como lo ve la PROYECCIÓN: su uuid (la identidad, 3A
1232
+ * · A1) y los slugs de los permisos que vincula.
1233
+ */
1234
+ export interface CatalogProjectionRole {
1235
+ uuid: string;
1236
+ permissions: string[];
1237
+ }
1238
+ /**
1239
+ * Foto del catálogo confirmado que un driver puede materializar en su
1240
+ * backend. Se lee de `authz_*` dentro de la transacción del sync: es
1241
+ * DERIVADA, y por eso se puede reconstruir entera (`authz:reconcile`).
1242
+ */
1243
+ export interface CatalogProjectionSnapshot {
1244
+ /** Todos los slugs de permiso del catálogo (no solo los del spec que se sincroniza). */
1245
+ permissions: string[];
1246
+ /** Todos los roles con sus vínculos rol→permiso. */
1247
+ roles: CatalogProjectionRole[];
1248
+ }
1249
+ /** Lo que una pasada de proyección movió. Nunca un booleano: una proyección silenciosa no se vigila. */
1250
+ export interface CatalogProjectionReport {
1251
+ /** Tuplas nuevas escritas. */
1252
+ written: number;
1253
+ /** Tuplas que sobraban (el catálogo ya no las respalda) y se han borrado. */
1254
+ deleted: number;
1255
+ /** Tuplas que ya estaban exactamente igual. */
1256
+ unchanged: number;
1257
+ }
1258
+ /**
1259
+ * **Proyección derivada del catálogo en el backend de un driver** (regla del
1260
+ * catálogo reescrita — panel 2, cruce 7; decisión del dueño 2026-08-28).
1261
+ *
1262
+ * El catálogo es propiedad LOCAL siempre: roles y permisos viven en `authz_*`
1263
+ * y ningún driver es su fuente de verdad. Un driver PUEDE mantener una
1264
+ * proyección (el modo `facts` de openfga: permisos como relaciones del modelo
1265
+ * + vínculos rol→permiso como tuplas `role:<uuid>#permits_<P>@<holder>:*`) si
1266
+ * y solo si: (a) es reconstruible desde `authz_*`, (b) `authz:reconcile` la
1267
+ * vigila y (c) NUNCA se lee como catálogo.
1268
+ *
1269
+ * Se inyecta en `syncAuthzCatalog` en vez de importarse: `src/catalog.ts` es
1270
+ * la ruta de un consumidor solo-database y no puede tirar del SDK de OpenFGA
1271
+ * (regla 3 de `check_purity.mjs`).
1272
+ */
1273
+ export interface CatalogProjection {
1274
+ /**
1275
+ * ¿El catálogo que va a quedar es publicable en este backend? Se llama
1276
+ * ANTES de escribir nada (cotas de nombre y techo del modelo, A3/A4): un
1277
+ * catálogo que no se puede proyectar no se escribe a medias.
1278
+ */
1279
+ assertPublishable(permissions: readonly string[]): void;
1280
+ /** Rehace la proyección del catálogo ya confirmado: escribe lo que falta y BORRA lo que sobra. */
1281
+ project(snapshot: CatalogProjectionSnapshot): Promise<CatalogProjectionReport>;
1282
+ }
1283
+ /** Un objeto de relaciones: `document:<id>`, `folder:<id>`, `group:<id>`… */
1284
+ export interface RelObject {
1285
+ /** El tipo FGA del objeto (`document`, `folder`, `space`…), declarado en `defineRelationsConfig`. */
1286
+ type: string;
1287
+ /** El id del objeto dentro de su partición. Sin `|`/`#`/`:` (los pone el driver al componer). */
1288
+ id: string;
1289
+ }
1290
+ /**
1291
+ * Un userset como sujeto: `group:eng#member` (todos los miembros del grupo).
1292
+ * Es lo que hace que un `relate(group:g#member, viewer, doc)` conceda `viewer`
1293
+ * a todo el que sea `member` de `g` (`usersetsOf`, un nivel).
1294
+ */
1295
+ export interface RelUserset {
1296
+ object: RelObject;
1297
+ relation: string;
1298
+ }
1299
+ /**
1300
+ * El sujeto de una relación: un HOLDER (`{type,uuid}`, como `SubjectRef`) o un
1301
+ * USERSET (`{object, relation}`). El puerto lo tipa explícito porque los dos
1302
+ * viajan por `relate`/`listSubjects`/`check`; un userset de OTRA partición se
1303
+ * corta por comparación de string en el driver (nunca cruza).
1304
+ */
1305
+ export type RelSubject = SubjectRef | RelUserset;
1306
+ /** Discriminador: ¿este sujeto es un userset (`group:g#member`) y no un holder? */
1307
+ export declare function isRelUserset(subject: RelSubject): subject is RelUserset;
1308
+ /**
1309
+ * La referencia COMPLETA a una tupla de relación, tal como la ve `assertWrite`
1310
+ * (R-13) y `onRelationWrite`: sujeto + relación + objeto + partición, más la
1311
+ * operación. Es puro dato: quien lo recibe decide (auditar, rechazar), nunca
1312
+ * muta el store.
1313
+ */
1314
+ export interface RelationRef {
1315
+ operation: 'relate' | 'unrelate';
1316
+ subject: RelSubject;
1317
+ relation: string;
1318
+ object: RelObject;
1319
+ partition: ScopeRef;
1320
+ }
1321
+ /** El evento de escritura de relaciones (auditoría del consumidor, sin `AsyncLocalStorage`). */
1322
+ export interface RelationWriteEvent extends RelationRef {
1323
+ /** Quién ordenó la escritura (`RelationWriteOptions.actor`), ya validado. Ausente si no lo pasó. */
1324
+ actor?: SubjectRef;
1325
+ }
1326
+ /** Opciones comunes a `relate`/`unrelate`. */
1327
+ export interface RelationWriteOptions {
1328
+ /** Quién ordena la escritura; viaja en `RelationWriteEvent.actor`. */
1329
+ actor?: SubjectRef;
1330
+ }
1331
+ /** Una página de una enumeración de relaciones (cursor opaco que AVANZA, no filtra herencia). */
1332
+ export interface RelationPage {
1333
+ limit?: number;
1334
+ after?: string;
1335
+ }
1336
+ /**
1337
+ * Lo que un `RelationsDriver` DECLARA que puede hacer. Cada valor lleva su par
1338
+ * de casos `{ whenTrue, whenFalse }` en `runRelationsDriverContract` — nunca
1339
+ * un `skip` (3b-2e · E2). El runner FALLA si una capacidad declarada no tiene
1340
+ * poblada la cara que corresponde a su valor.
1341
+ */
1342
+ export interface RelationsDriverCapabilities {
1343
+ /** `check` es UNA sola llamada al backend (`openfga`: un `Check`). */
1344
+ singleCheckRelations: boolean;
1345
+ /**
1346
+ * Los `listObjects` enumeran también lo HEREDADO. **`false` siempre** en
1347
+ * este paquete (invariante 7): en `openfga` obligaría a `ListObjects`, que
1348
+ * trunca al tope del servidor. Devuelven hechos DIRECTOS + lo derivado.
1349
+ */
1350
+ listObjectsInherited: boolean;
1351
+ /** `listSubjects` devuelve también sujetos USERSET (`group:g#member`), no solo holders. */
1352
+ usersetSubjects: boolean;
1353
+ /**
1354
+ * El driver implementa `membersOf` (membresía TRANSITIVA a través de
1355
+ * usersets). Solo `database` (CTE recursiva); `openfga` es `false` (la
1356
+ * transitiva sería `ListUsers`, que trunca) ⇒ `membersOf` es 500
1357
+ * `E_AUTHZ_UNSUPPORTED`.
1358
+ */
1359
+ membersOfNative: boolean;
1360
+ /** El driver sabe ser ORIGEN de `authz:reconcile` de relaciones (`enumerateRelations`). */
1361
+ enumerateRelations: boolean;
1362
+ /**
1363
+ * `listObjects` SEÑALA el truncamiento cuando el backend corta al tope
1364
+ * (`openfga` con `ListObjects`): la página devuelve `truncated: true`, nunca
1365
+ * una lista parcial muda (S16). Capacidad NUEVA, distinta del
1366
+ * `truncationSignal` de los `list*` de roles.
1367
+ */
1368
+ listObjectsTruncation: boolean;
1369
+ }
1370
+ /**
1371
+ * Una página de `listObjects`/`listSubjects`/`enumerateRelations`. `truncated`
1372
+ * dice si el backend cortó al tope (solo con `listObjectsTruncation`): un
1373
+ * consumidor que lo ve sabe que hay MÁS y no toma la lista por completa.
1374
+ */
1375
+ export interface RelationObjectsPage {
1376
+ objects: RelObject[];
1377
+ cursor?: string;
1378
+ truncated?: boolean;
1379
+ }
1380
+ export interface RelationSubjectsPage {
1381
+ subjects: RelSubject[];
1382
+ cursor?: string;
1383
+ truncated?: boolean;
1384
+ }
1385
+ /** Una tupla de relación tal como la enumera `enumerateRelations` (origen de reconcile). */
1386
+ export interface RelationTuple {
1387
+ subject: RelSubject;
1388
+ relation: string;
1389
+ object: RelObject;
1390
+ partition: ScopeRef;
1391
+ }
1392
+ export interface RelationTuplePage {
1393
+ tuples: RelationTuple[];
1394
+ cursor?: string;
1395
+ }
1396
+ /**
1397
+ * El puerto de ReBAC. `partition: ScopeRef` es OBLIGATORIA en TODA operación
1398
+ * (`APP_SCOPE` es válida para mono-tenant): el aislamiento de tenant se corta
1399
+ * por la partición, y el driver la serializa en el id del objeto y del
1400
+ * userset. La whitelist de tipo/relación (F-05) la aplica el manager ANTES de
1401
+ * llamar al driver, pero el driver la re-valida por defensa en profundidad.
1402
+ */
1403
+ export interface RelationsDriver {
1404
+ readonly capabilities?: RelationsDriverCapabilities;
1405
+ /** Crea la relación `subject —relation→ object` en `partition`. Idempotente. */
1406
+ relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
1407
+ /** Retira la relación. No-op seguro si no existe (invariante 6). */
1408
+ unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
1409
+ /** ¿`subject` tiene `relation` sobre `object` en `partition` (directo o derivado por includes/userset)? */
1410
+ check(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef): Promise<boolean>;
1411
+ /** Los objetos de tipo `objectType` sobre los que `subject` tiene `relation`. Directos + derivados, sin herencia abierta. */
1412
+ listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, page?: RelationPage): Promise<RelationObjectsPage>;
1413
+ /** Los sujetos DIRECTOS de `relation` sobre `object` (holders y usersets). Nunca la membresía transitiva (eso es `membersOf`). */
1414
+ listSubjects(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
1415
+ /** Borra todas las tuplas cuyo OBJETO es `object` y demuestra cero, o lanza 500 `E_AUTHZ_PURGE_INCOMPLETE` (invariante 11). */
1416
+ purgeObject(object: RelObject, partition: ScopeRef): Promise<void>;
1417
+ /** Borra todas las tuplas cuyo SUJETO es `subject` y demuestra cero, o lanza 500. */
1418
+ purgeSubject(subject: RelSubject, partition: ScopeRef): Promise<void>;
1419
+ /**
1420
+ * La membresía TRANSITIVA de un objeto-grupo: todos los holders que son
1421
+ * `member` directa o a través de grupos anidados. DISTINTO de
1422
+ * `listSubjects(member, group)`, que devuelve solo los hechos DIRECTOS. Solo
1423
+ * lo trae el driver con `membersOfNative: true`.
1424
+ */
1425
+ membersOf?(object: RelObject, relation: string, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
1426
+ /** ORIGEN de `authz:reconcile` de relaciones: las tuplas paginadas, sin filtrar. Solo con `enumerateRelations: true`. */
1427
+ enumerateRelations?(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
1428
+ }
1429
+ /**
1430
+ * Factory de un `RelationsDriver` (Fase 4, lote 4-6) — el análogo de
1431
+ * `AuthorizationDriverFactory` para el puerto de relaciones. El consumidor la
1432
+ * declara en `config.relations.drivers`, y `authz:relations:reconcile` la
1433
+ * invoca para construir el ORIGEN y el DESTINO de una migración de tuplas. El
1434
+ * driver `openfga` de relaciones entra por el subpath `/openfga` DENTRO de la
1435
+ * factory (como el de roles), así que el comando nunca toca el SDK (pureza).
1436
+ */
1437
+ export type RelationsDriverFactory = () => RelationsDriver | Promise<RelationsDriver>;
153
1438
  //# sourceMappingURL=types.d.ts.map