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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +837 -51
  3. package/build/commands/authz_catalog_diff.d.ts +28 -0
  4. package/build/commands/authz_catalog_diff.d.ts.map +1 -0
  5. package/build/commands/authz_catalog_diff.js +67 -0
  6. package/build/commands/authz_catalog_diff.js.map +1 -0
  7. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  8. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  9. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  10. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  11. package/build/commands/authz_catalog_sync.d.ts +37 -0
  12. package/build/commands/authz_catalog_sync.d.ts.map +1 -0
  13. package/build/commands/authz_catalog_sync.js +81 -0
  14. package/build/commands/authz_catalog_sync.js.map +1 -0
  15. package/build/commands/authz_freeze.d.ts +44 -0
  16. package/build/commands/authz_freeze.d.ts.map +1 -0
  17. package/build/commands/authz_freeze.js +95 -0
  18. package/build/commands/authz_freeze.js.map +1 -0
  19. package/build/commands/authz_reconcile.d.ts +102 -0
  20. package/build/commands/authz_reconcile.d.ts.map +1 -0
  21. package/build/commands/authz_reconcile.js +294 -0
  22. package/build/commands/authz_reconcile.js.map +1 -0
  23. package/build/commands/authz_relations_reconcile.d.ts +73 -0
  24. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  25. package/build/commands/authz_relations_reconcile.js +225 -0
  26. package/build/commands/authz_relations_reconcile.js.map +1 -0
  27. package/build/commands/authz_scopes_relay.d.ts +47 -0
  28. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  29. package/build/commands/authz_scopes_relay.js +141 -0
  30. package/build/commands/authz_scopes_relay.js.map +1 -0
  31. package/build/commands/authz_unfreeze.d.ts +37 -0
  32. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  33. package/build/commands/authz_unfreeze.js +92 -0
  34. package/build/commands/authz_unfreeze.js.map +1 -0
  35. package/build/commands/main.d.ts +8 -1
  36. package/build/commands/main.d.ts.map +1 -1
  37. package/build/commands/main.js +8 -1
  38. package/build/commands/main.js.map +1 -1
  39. package/build/commands/openfga_provision.d.ts +46 -4
  40. package/build/commands/openfga_provision.d.ts.map +1 -1
  41. package/build/commands/openfga_provision.js +91 -7
  42. package/build/commands/openfga_provision.js.map +1 -1
  43. package/build/configure.d.ts +11 -0
  44. package/build/configure.d.ts.map +1 -1
  45. package/build/configure.js +37 -1
  46. package/build/configure.js.map +1 -1
  47. package/build/index.d.ts +75 -7
  48. package/build/index.d.ts.map +1 -1
  49. package/build/index.js +67 -5
  50. package/build/index.js.map +1 -1
  51. package/build/providers/authz_provider.d.ts +26 -2
  52. package/build/providers/authz_provider.d.ts.map +1 -1
  53. package/build/providers/authz_provider.js +48 -2
  54. package/build/providers/authz_provider.js.map +1 -1
  55. package/build/services/relations.d.ts +14 -0
  56. package/build/services/relations.d.ts.map +1 -0
  57. package/build/services/relations.js +17 -0
  58. package/build/services/relations.js.map +1 -0
  59. package/build/src/catalog/catalog.d.ts +289 -0
  60. package/build/src/catalog/catalog.d.ts.map +1 -0
  61. package/build/src/catalog/catalog.js +859 -0
  62. package/build/src/catalog/catalog.js.map +1 -0
  63. package/build/src/catalog/catalog_cache.d.ts +324 -0
  64. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  65. package/build/src/catalog/catalog_cache.js +666 -0
  66. package/build/src/catalog/catalog_cache.js.map +1 -0
  67. package/build/src/clock.d.ts +24 -0
  68. package/build/src/clock.d.ts.map +1 -0
  69. package/build/src/clock.js +7 -0
  70. package/build/src/clock.js.map +1 -0
  71. package/build/src/define_config.d.ts +201 -7
  72. package/build/src/define_config.d.ts.map +1 -1
  73. package/build/src/define_config.js.map +1 -1
  74. package/build/src/drivers/database_driver.d.ts +324 -15
  75. package/build/src/drivers/database_driver.d.ts.map +1 -1
  76. package/build/src/drivers/database_driver.js +1107 -128
  77. package/build/src/drivers/database_driver.js.map +1 -1
  78. package/build/src/drivers/database_relations_driver.d.ts +75 -0
  79. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  80. package/build/src/drivers/database_relations_driver.js +450 -0
  81. package/build/src/drivers/database_relations_driver.js.map +1 -0
  82. package/build/src/drivers/openfga_driver.d.ts +963 -55
  83. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  84. package/build/src/drivers/openfga_driver.js +2693 -372
  85. package/build/src/drivers/openfga_driver.js.map +1 -1
  86. package/build/src/drivers/openfga_facts.d.ts +369 -0
  87. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  88. package/build/src/drivers/openfga_facts.js +813 -0
  89. package/build/src/drivers/openfga_facts.js.map +1 -0
  90. package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
  91. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  92. package/build/src/drivers/openfga_relations_driver.js +466 -0
  93. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  94. package/build/src/errors.d.ts +586 -0
  95. package/build/src/errors.d.ts.map +1 -1
  96. package/build/src/errors.js +583 -0
  97. package/build/src/errors.js.map +1 -1
  98. package/build/src/expiry.d.ts +27 -0
  99. package/build/src/expiry.d.ts.map +1 -0
  100. package/build/src/expiry.js +50 -0
  101. package/build/src/expiry.js.map +1 -0
  102. package/build/src/freeze.d.ts +120 -0
  103. package/build/src/freeze.d.ts.map +1 -0
  104. package/build/src/freeze.js +172 -0
  105. package/build/src/freeze.js.map +1 -0
  106. package/build/src/hierarchical_resolver.d.ts +56 -0
  107. package/build/src/hierarchical_resolver.d.ts.map +1 -0
  108. package/build/src/hierarchical_resolver.js +87 -0
  109. package/build/src/hierarchical_resolver.js.map +1 -0
  110. package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
  111. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  112. package/build/src/{middleware → http}/app_access_middleware.js +10 -26
  113. package/build/src/http/app_access_middleware.js.map +1 -0
  114. package/build/src/http/resource_access_middleware.d.ts +105 -0
  115. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  116. package/build/src/http/resource_access_middleware.js +81 -0
  117. package/build/src/http/resource_access_middleware.js.map +1 -0
  118. package/build/src/identity.d.ts +228 -0
  119. package/build/src/identity.d.ts.map +1 -0
  120. package/build/src/identity.js +457 -0
  121. package/build/src/identity.js.map +1 -0
  122. package/build/src/manager.d.ts +536 -7
  123. package/build/src/manager.d.ts.map +1 -1
  124. package/build/src/manager.js +2676 -23
  125. package/build/src/manager.js.map +1 -1
  126. package/build/src/memoize_ancestors.d.ts +23 -0
  127. package/build/src/memoize_ancestors.d.ts.map +1 -0
  128. package/build/src/memoize_ancestors.js +42 -0
  129. package/build/src/memoize_ancestors.js.map +1 -0
  130. package/build/src/models/authz_assignment.d.ts +11 -11
  131. package/build/src/models/authz_assignment.d.ts.map +1 -1
  132. package/build/src/models/authz_deny.d.ts +11 -11
  133. package/build/src/models/authz_deny.d.ts.map +1 -1
  134. package/build/src/models/authz_permission.d.ts +17 -11
  135. package/build/src/models/authz_permission.d.ts.map +1 -1
  136. package/build/src/models/authz_permission.js +4 -0
  137. package/build/src/models/authz_permission.js.map +1 -1
  138. package/build/src/models/authz_role.d.ts +19 -12
  139. package/build/src/models/authz_role.d.ts.map +1 -1
  140. package/build/src/models/authz_role.js +6 -1
  141. package/build/src/models/authz_role.js.map +1 -1
  142. package/build/src/models/authz_role_permission.d.ts +11 -11
  143. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  144. package/build/src/openfga.d.ts +29 -0
  145. package/build/src/openfga.d.ts.map +1 -0
  146. package/build/src/openfga.js +26 -0
  147. package/build/src/openfga.js.map +1 -0
  148. package/build/src/reconcile.d.ts +37 -0
  149. package/build/src/reconcile.d.ts.map +1 -0
  150. package/build/src/reconcile.js +69 -0
  151. package/build/src/reconcile.js.map +1 -0
  152. package/build/src/relation_partition_trigger.d.ts +8 -0
  153. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  154. package/build/src/relation_partition_trigger.js +85 -0
  155. package/build/src/relation_partition_trigger.js.map +1 -0
  156. package/build/src/relations/define_relations_config.d.ts +58 -0
  157. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  158. package/build/src/relations/define_relations_config.js +144 -0
  159. package/build/src/relations/define_relations_config.js.map +1 -0
  160. package/build/src/relations/manager.d.ts +38 -0
  161. package/build/src/relations/manager.d.ts.map +1 -0
  162. package/build/src/relations/manager.js +156 -0
  163. package/build/src/relations/manager.js.map +1 -0
  164. package/build/src/relations/reconcile.d.ts +62 -0
  165. package/build/src/relations/reconcile.d.ts.map +1 -0
  166. package/build/src/relations/reconcile.js +138 -0
  167. package/build/src/relations/reconcile.js.map +1 -0
  168. package/build/src/relations_config_store.d.ts +22 -0
  169. package/build/src/relations_config_store.d.ts.map +1 -0
  170. package/build/src/relations_config_store.js +74 -0
  171. package/build/src/relations_config_store.js.map +1 -0
  172. package/build/src/scope_outbox.d.ts +69 -0
  173. package/build/src/scope_outbox.d.ts.map +1 -0
  174. package/build/src/scope_outbox.js +291 -0
  175. package/build/src/scope_outbox.js.map +1 -0
  176. package/build/src/shared/backend_guard.d.ts +106 -0
  177. package/build/src/shared/backend_guard.d.ts.map +1 -0
  178. package/build/src/shared/backend_guard.js +246 -0
  179. package/build/src/shared/backend_guard.js.map +1 -0
  180. package/build/src/shared/sql_expiry.d.ts +53 -0
  181. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  182. package/build/src/shared/sql_expiry.js +66 -0
  183. package/build/src/shared/sql_expiry.js.map +1 -0
  184. package/build/src/sql_descendants.d.ts +97 -0
  185. package/build/src/sql_descendants.d.ts.map +1 -0
  186. package/build/src/sql_descendants.js +203 -0
  187. package/build/src/sql_descendants.js.map +1 -0
  188. package/build/src/testing/contract.d.ts +212 -7
  189. package/build/src/testing/contract.d.ts.map +1 -1
  190. package/build/src/testing/contract.js +3449 -24
  191. package/build/src/testing/contract.js.map +1 -1
  192. package/build/src/testing/main.d.ts +10 -2
  193. package/build/src/testing/main.d.ts.map +1 -1
  194. package/build/src/testing/main.js +5 -1
  195. package/build/src/testing/main.js.map +1 -1
  196. package/build/src/testing/migration_contract.d.ts +284 -0
  197. package/build/src/testing/migration_contract.d.ts.map +1 -0
  198. package/build/src/testing/migration_contract.js +586 -0
  199. package/build/src/testing/migration_contract.js.map +1 -0
  200. package/build/src/testing/relations_contract.d.ts +51 -0
  201. package/build/src/testing/relations_contract.d.ts.map +1 -0
  202. package/build/src/testing/relations_contract.js +654 -0
  203. package/build/src/testing/relations_contract.js.map +1 -0
  204. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  205. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  206. package/build/src/testing/relations_reconcile_contract.js +172 -0
  207. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  208. package/build/src/testing/scope_tree.d.ts +65 -0
  209. package/build/src/testing/scope_tree.d.ts.map +1 -0
  210. package/build/src/testing/scope_tree.js +145 -0
  211. package/build/src/testing/scope_tree.js.map +1 -0
  212. package/build/src/traits/authz_scopes.d.ts +30 -6
  213. package/build/src/traits/authz_scopes.d.ts.map +1 -1
  214. package/build/src/traits/authz_scopes.js +30 -18
  215. package/build/src/traits/authz_scopes.js.map +1 -1
  216. package/build/src/traits/has_uuid.d.ts +12 -12
  217. package/build/src/traits/has_uuid.d.ts.map +1 -1
  218. package/build/src/types.d.ts +1313 -28
  219. package/build/src/types.d.ts.map +1 -1
  220. package/build/src/types.js +20 -4
  221. package/build/src/types.js.map +1 -1
  222. package/build/stubs/config/app_acl.stub +4 -2
  223. package/build/stubs/config/authorization.stub +168 -14
  224. package/build/stubs/migration.stub +183 -13
  225. package/build/stubs/scopes_outbox_migration.stub +57 -0
  226. package/package.json +14 -7
  227. package/build/commands/openfga_import.d.ts +0 -28
  228. package/build/commands/openfga_import.d.ts.map +0 -1
  229. package/build/commands/openfga_import.js +0 -74
  230. package/build/commands/openfga_import.js.map +0 -1
  231. package/build/src/catalog.d.ts +0 -3
  232. package/build/src/catalog.d.ts.map +0 -1
  233. package/build/src/catalog.js +0 -103
  234. package/build/src/catalog.js.map +0 -1
  235. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  236. package/build/src/middleware/app_access_middleware.js.map +0 -1
@@ -1,5 +1,68 @@
1
1
  import type { AuthorizationConfig } from './define_config.js';
2
- import type { AuthorizationDriver, GrantOptions, ScopeRef, ScopeType, SubjectRef } from './types.js';
2
+ import type { FreezeKind, FreezeToken } from './freeze.js';
3
+ import type { AuthorizationDriver, AuthorizedScopes, CatalogRole, CatalogRoleRef, DenyOptions, DenyRef, ExcludedSubtree, GrantOptions, GrantOutcome, RoleQuery, ScopedRoleChanges, ScopedRoleSpec, ReconcileOptions, ReconcileReport, ScopedWriteOptions, ScopeRelayReport, ScopeRef, ScopeTreeWriteOptions, ScopeType, SubjectRef } from './types.js';
4
+ /**
5
+ * Lo que devuelve `AuthorizationManager.forRequest()`: la misma API que el
6
+ * manager (lecturas, escrituras, `scopes.*`), con los ancestros memoizados
7
+ * SOLO en las lecturas. Es un tipo, no otra clase: la vista es un manager
8
+ * hijo que comparte config, driver y hooks con su padre.
9
+ */
10
+ export type AuthorizationView = Pick<AuthorizationManager, 'authorize' | 'hasRole' | 'listSubjects' | 'listScopes' | 'listRoles' | 'listRoleScopes' | 'grant' | 'revoke' | 'deny' | 'removeDeny' | 'scopes' | 'driver' | 'forRequest' | 'isWithin' | 'listDenies' | 'effectivePermissions' | 'authorizeMany' | 'authorizedScopes' | 'expandExcludedSubtrees' | 'defineScopedRole' | 'updateScopedRole' | 'deleteScopedRole'>;
11
+ /**
12
+ * Expande los `excludedSubtrees` de un `all` (2D · F10) a la lista plana de
13
+ * scopes que hay que restar: cada scope denegado y TODOS sus descendientes,
14
+ * con el `descendantsOf` del config. Es la forma correcta del `NOT IN`;
15
+ * restar solo los scopes con deny dejaría dentro sus subárboles.
16
+ */
17
+ export declare function expandExcludedSubtrees(view: AuthorizationView, excluded: ExcludedSubtree[]): Promise<ScopeRef[]>;
18
+ /** Cotas por defecto de `authorizedScopes` (config `scopes.maxScopes` / `scopes.maxDescendants`). */
19
+ export declare const DEFAULT_MAX_SCOPES = 1000;
20
+ export declare const DEFAULT_MAX_DESCENDANTS = 10000;
21
+ /**
22
+ * Tope sano de `maxScopes`/`maxDescendants` (2.5-B, auditor ⚪6): por encima
23
+ * de ~4,29e9 el hint `SET_VAR(cte_max_recursion_depth)` de MySQL sale de
24
+ * rango y un ciclo en la tabla deja de ser el 422 «posible ciclo» del
25
+ * contrato (503); y ya con 1e6 un ciclo cuesta segundos de CPU por llamada.
26
+ * Una cota mayor es config rota (500), nunca una pregunta.
27
+ */
28
+ export declare const MAX_SCOPE_BOUND = 10000000;
29
+ /**
30
+ * Cotas por defecto de `authz:scopes:relay` (3b-2d). El lote es el tamaño de
31
+ * cada `pending()`; el límite, cuántos cambios aplica una pasada antes de
32
+ * volver (lo que quede sigue pendiente: drenar es reanudable por diseño y
33
+ * una pasada eterna no es reanudable).
34
+ */
35
+ export declare const DEFAULT_RELAY_BATCH = 100;
36
+ export declare const DEFAULT_RELAY_LIMIT = 10000;
37
+ /** Vida por defecto de una vista de `forRequest()` para LEER (F9): un request, no un módulo. */
38
+ export declare const DEFAULT_VIEW_MAX_AGE_MS = 30000;
39
+ export interface ForRequestOptions {
40
+ /**
41
+ * Milisegundos durante los que la vista puede LEER (default 30 000).
42
+ * Después, cualquier lectura es 500 `E_AUTHZ_VIEW_EXPIRED`: el memo de
43
+ * ancestros solo es correcto mientras dura el request y una vista guardada
44
+ * por error serviría la cadena vieja para siempre. `0` = sin límite, a
45
+ * sabiendas. Las escrituras e `isWithin` no caducan (resuelven en fresco).
46
+ * Se mide con un reloj MONÓTONO (`performance.now()`, 2E · H3): un salto
47
+ * del reloj de pared hacia atrás no resucita una vista caducada.
48
+ */
49
+ maxAgeMs?: number;
50
+ /**
51
+ * SOLO TESTS: fuente del reloj monótono, en ms (default `performance.now`).
52
+ * Sirve para fijar la frontera exacta de `maxAgeMs` sin dormir; en
53
+ * producción no se toca.
54
+ */
55
+ now?: () => number;
56
+ }
57
+ /** Lo que `freezeStatus()` responde de un freeze VIVO (la fila, legible por cualquiera). */
58
+ export interface FreezeStatus {
59
+ reason: string;
60
+ holder: string;
61
+ kind: FreezeKind | 'unknown';
62
+ /** Instante (ms de pared) en el que el lease vence, o `null` si no caduca. */
63
+ untilMs: number | null;
64
+ fence: number;
65
+ }
3
66
  /**
4
67
  * Manager de autorización — la fachada que usan middleware, services y
5
68
  * seeders. Resuelve el driver activo del config y notifica cada escritura
@@ -12,18 +75,484 @@ import type { AuthorizationDriver, GrantOptions, ScopeRef, ScopeType, SubjectRef
12
75
  export declare class AuthorizationManager {
13
76
  #private;
14
77
  constructor(config: AuthorizationConfig);
78
+ /**
79
+ * Vista por request (2A/A3): las LECTURAS (`authorize`, `hasRole`, `list*`)
80
+ * resuelven ancestros con `memoizeAncestors(config.scopes.resolveChain)`
81
+ * —una llamada al árbol por scope durante la vida de la vista—; las
82
+ * ESCRITURAS (`grant`, `revoke`, `deny`, `removeDeny`, `scopes.*`) siguen
83
+ * resolviendo en fresco, porque una lectura obsoleta caduca sola y un
84
+ * grant sobre una cadena que ya cambió queda escrito para siempre (auditor
85
+ * C3/E3). El memo es de ANCESTROS, nunca de decisiones: un deny escrito
86
+ * entre dos `authorize` de la misma vista cambia la segunda respuesta.
87
+ *
88
+ * Patrón en Adonis: un middleware hace `ctx.authz = authorization.forRequest()`
89
+ * y controladores y policies leen de `ctx.authz`. Sin `AsyncLocalStorage`:
90
+ * la vista es un objeto explícito con la vida que le des. Sin
91
+ * `config.scopes.resolveChain`, o con un driver de terceros sin
92
+ * `withChainResolver`, la vista lee con el driver tal cual (sin memo)
93
+ * y sigue siendo correcta.
94
+ */
95
+ forRequest(options?: ForRequestOptions): AuthorizationView;
96
+ /**
97
+ * El driver activo, TAL CUAL. Es la salida explícita de las barreras del
98
+ * manager (2D · G4, auditor 8): lo que escribas por aquí no pasa por
99
+ * `actor`/`requireActor`, `within`/`requireWithin` ni `onWrite`, y lo que
100
+ * leas no pasa por el memo de ancestros. Está pensado para el código de
101
+ * PLATAFORMA (seeders, comandos, la escritura en la raíz con
102
+ * `requireWithin: 'non-root'`) y para tests; un call-site de tenant nunca
103
+ * debería llamarlo. No se ofrece nada más por aquí a propósito.
104
+ *
105
+ * Lo único que el manager le aplica al resolverlo es el reloj del config
106
+ * (`clock`, 2.5 · J1) vía `withClock`: no es una barrera, es la hora con
107
+ * la que el driver decide, y vale igual para la plataforma, los tests y
108
+ * cada vista de `forRequest()` (todas leen el driver del padre). Un driver
109
+ * sin `withClock` con `clock` declarado es 500 `E_AUTHZ_CONFIG`.
110
+ */
15
111
  driver(): Promise<AuthorizationDriver>;
112
+ /**
113
+ * **Congela las ESCRITURAS del motor, DURABLE** (3b-7; decisión del dueño
114
+ * del 2026-08-31 (3b): B + E-analista). Operación de PLATAFORMA, como
115
+ * `driver()`: no se expone por HTTP.
116
+ *
117
+ * El estado ya NO vive en el proceso: vive en la fila `id = 2` de
118
+ * `authz_catalog_version`, así que alcanza a **todos los procesos que
119
+ * comparten las tablas `authz_*`** (invariante 14: el comando ace y los
120
+ * workers hablan con la misma base). Mientras el freeze está vivo, toda
121
+ * escritura del manager —las cuatro de hechos, las tres de árbol, la API
122
+ * de delegación, `pruneOrphanRoles({force})` y `relayScopeChanges`—
123
+ * responde 503 `E_AUTHZ_FROZEN` **reintentable** y no llega al driver; las
124
+ * LECTURAS siguen respondiendo con normalidad (la asimetría deliberada:
125
+ * `authorize` no se congela ni un milisegundo).
126
+ *
127
+ * Devuelve el **token del dueño** (`{ fence, holder }`): `unfreeze(token)`
128
+ * solo levanta el freeze cuyo token coincide — el `finally` de una ventana
129
+ * ajena o rezagada no puede levantar la tuya (auditor A1.3). Un freeze
130
+ * VIVO de otro dueño ⇒ 423 `E_AUTHZ_FREEZE_HELD`, nunca dos dueños.
131
+ *
132
+ * El **lease** (default 15 s) se renueva solo (`leaseMs / 3`, `unref()`)
133
+ * mientras este proceso vive; si el proceso muere (`SIGKILL`, OOM), el
134
+ * lease vence y la flota vuelve a escribir SOLA en ≤ `leaseMs` — nadie
135
+ * limpia nada a mano. `leaseMs: null` = sin caducidad: la ventana del
136
+ * OPERADOR (`authz:freeze`), que dura hasta su `authz:unfreeze`.
137
+ *
138
+ * Lo que el freeze **NO congela**, a propósito y documentado (auditor
139
+ * 🟠 5): `syncAuthzCatalog` (función libre que no ve al manager),
140
+ * `manager.driver()` (la salida documentada de TODAS las barreras) y el
141
+ * árbol SQL del consumidor (sus tablas, su SQL). Y lo que no puede
142
+ * prometer: una escritura que ya pasó su barrera cuando el freeze aterriza
143
+ * ENTRA (no hay atomicidad entre una fila SQL y un backend externo) — la
144
+ * promesa publicada es «otro proceso recibe 503», jamás «ninguna escritura
145
+ * entra en la ventana».
146
+ */
147
+ freeze(reason?: string, options?: {
148
+ leaseMs?: number | null;
149
+ kind?: FreezeKind;
150
+ }): Promise<FreezeToken>;
151
+ /**
152
+ * Levanta el freeze de ESTE token; uno ajeno o rezagado no toca nada (esa
153
+ * es toda la garantía del fence). Devuelve si de verdad lo levantó.
154
+ */
155
+ unfreeze(token: FreezeToken): Promise<boolean>;
156
+ /**
157
+ * ¿SOSTIENE este manager un freeze? (proceso-local: su token vive aquí.)
158
+ * Para saber si el MOTOR está congelado —por quien sea— pregunta
159
+ * `freezeStatus()`: eso es la fila, no la memoria.
160
+ */
161
+ get frozen(): boolean;
162
+ /** El freeze VIVO de la fila compartida, o `null`. Lo lee cualquiera; solo el token lo levanta. */
163
+ freezeStatus(): Promise<FreezeStatus | null>;
164
+ /**
165
+ * `freeze()` + `finally unfreeze(token)`. El `finally` es la parte que
166
+ * importa: una migración que revienta a la mitad no puede dejar la
167
+ * aplicación sin poder escribir (y si además el proceso muere sin
168
+ * `finally`, el lease vence solo). **El anidado corre DENTRO** (auditor
169
+ * A1.1/A1.3): si este manager ya sostiene el freeze, la ventana interior
170
+ * no toma otro ni lo levanta al salir — la exterior sigue en pie.
171
+ */
172
+ withFrozenWrites<T>(reason: string, fn: () => Promise<T>): Promise<T>;
16
173
  /** Solo tests: fuerza re-resolución del driver. */
17
174
  clearCachedDriver(): void;
175
+ /**
176
+ * El árbol de scopes es un hecho del contrato: el consumidor notifica sus
177
+ * cambios aquí, en TODOS los drivers, y el PAQUETE valida antes de tocar
178
+ * el driver — la raíz no cuelga de nada, el padre tiene que existir y no
179
+ * puede haber ciclos. FGA acepta un ciclo de `parent` y lo evalúa (un grant
180
+ * en cualquier nodo concede en la raíz, S2), así que la barrera es esta.
181
+ * Espía: si la validación falla, cero llamadas al driver.
182
+ */
183
+ readonly scopes: {
184
+ attached: (child: ScopeRef, parent: ScopeRef, options?: ScopeTreeWriteOptions) => Promise<void>;
185
+ /**
186
+ * `moved` NO vuelve a juzgar el catálogo, y no tiene por qué (3b-1 · D3,
187
+ * auditor 3G): mover un scope es un hecho del árbol, no una escritura de
188
+ * catálogo. Lo que hay que tener escrito es la consecuencia: la relación
189
+ * «A ensombrece a B» es función del árbol de HOY, así que un `moved` que
190
+ * mete un subárbol bajo un scope que ya tiene el homónimo **crea la
191
+ * sombra sin que se juzgue ningún rango en ninguna parte** — y el dueño
192
+ * del subárbol movido puede no poder repararla (su rango se mide en la
193
+ * cadena del owner de la sombra). Por eso «sobre un rol solo actúa quien
194
+ * lo supera en rango» (3G · W3) es una comprobación de ESCRITURA y no un
195
+ * invariante del sistema. Es ruidosa: `authz:catalog:diff` la lista como
196
+ * `shadowedByAncestor` (y `--fail-on-shadows` la cuenta como deriva).
197
+ */
198
+ moved: (child: ScopeRef, newParent: ScopeRef, options?: ScopeTreeWriteOptions) => Promise<void>;
199
+ /**
200
+ * Hechos primero (el driver demuestra cero o lanza), arista después
201
+ * (S6): si la purga muere a medias, el subárbol sigue colgado y los
202
+ * denies heredados siguen valiendo. Sin `within` no comprueba que el
203
+ * scope exista (el consumidor puede haber borrado ya su fila); con
204
+ * `within` (2D · F2) el hijo tiene que seguir en el árbol para
205
+ * contrastar su cadena: purga ANTES de borrar la fila.
206
+ *
207
+ * **Purga HECHOS y solo hechos** (invariante 11; 3b-0 · Z1). Entre 3D y
208
+ * 3G esta operación arrastraba además los roles LOCALES cuyo owner era
209
+ * ese scope (y, con `descendantsOf`, los de todo el subárbol), con su
210
+ * propia policy de rango, su degradación y un valor de retorno que
211
+ * contaba lo purgado. Cinco lotes la tocaron y TRES de las cuatro
212
+ * regresiones de la Fase 3 nacieron ahí, siempre por COMPOSICIÓN de
213
+ * piezas correctas por separado (3E · P3 + 3F · S1/S2 ⇒ 3G · W1). El
214
+ * requisito que lo pedía —un rol cuyo owner desaparece queda
215
+ * indeleteable y ocupa su `(slug, nivel)`— se resuelve más simple y
216
+ * fuera del camino de un tenant: el rol queda DORMIDO (no concede, no es
217
+ * membresía, no se asigna) y la PLATAFORMA lo retira con
218
+ * `authz:catalog:prune-orphans` (Z2). Así `scopes.detached` vuelve a ser
219
+ * O(1), sin rango que medir, sin árbol que enumerar y sin nada que
220
+ * declarar a medias.
221
+ */
222
+ detached: (child: ScopeRef, options?: ScopeTreeWriteOptions) => Promise<void>;
223
+ };
224
+ /**
225
+ * **Drena la outbox del árbol y aplica los cambios al driver** (3b-2d).
226
+ * Es lo que hay detrás de `node ace authz:scopes:relay`.
227
+ *
228
+ * Operación de PLATAFORMA, como `pruneOrphanRoles`: se salta `requireActor`
229
+ * y `requireWithin` a propósito —la policy ya se juzgó al ENCOLAR, con el
230
+ * árbol y la sesión de aquel momento— así que **no se expone por HTTP**.
231
+ * Aquí solo se propaga lo que ya se validó.
232
+ *
233
+ * Reanudable y nunca silenciosa: el reporte dice QUÉ se aplicó (no un
234
+ * contador: la pasada no es atómica), qué falló, qué se aplazó y si queda
235
+ * trabajo.
236
+ *
237
+ * **El orden del árbol importa, pero solo entre cambios que se tocan**
238
+ * (3b-2h · 🔴 2, auditor R2). Hasta el 2h la pasada PARABA en el primer
239
+ * fallo, y eso convertía una entrada que ya no se puede aplicar —el padre
240
+ * del `attached` encolado se borró antes del relevo, la arista cerraría
241
+ * ahora un ciclo, el nodo acabó con dos padres— en un **tapón permanente
242
+ * para todos los tenants**: `pending()` devuelve lo no aplicado ordenado
243
+ * por id, así que la envenenada era la cabecera de la cola en TODAS las
244
+ * pasadas siguientes y ningún cambio del árbol volvía a llegar al store
245
+ * (medido: una unit nueva nunca recibía su arista `parent`, el deny de su
246
+ * organization nunca la alcanzaba y un `detached` posterior nunca purgaba).
247
+ * Ahora un fallo **contamina los scopes que nombra**: los cambios
248
+ * posteriores que tocan alguno de ellos se APLAZAN sin intentarse (y
249
+ * contaminan a su vez, así que la dependencia es transitiva), y los demás
250
+ * se aplican. El par ordenado que importaba —`attached(P, org)` antes que
251
+ * `attached(C, P)`, `moved` antes que `detached`— sigue respetado porque
252
+ * comparten scope; lo que ya no pasa es que el tenant A congele el árbol
253
+ * del tenant B.
254
+ *
255
+ * **Escritor ÚNICO** (3b-2h · 🟠 4): si la outbox sabe dar un lease
256
+ * (`acquire`), la pasada lo toma y una segunda pasada simultánea no hace
257
+ * nada y lo dice (`busy`). Sin lease, dos pasadas trabajan sobre el mismo
258
+ * lote —`pending()` no reserva y el lote no se relee— y la rezagada
259
+ * re-aplica cambios viejos sobre el árbol nuevo.
260
+ *
261
+ * Lo que esta pieza NO arregla, y va escrito en el README con estas
262
+ * palabras: entre el commit del consumidor y esta pasada hay un lag
263
+ * (segundos) durante el cual el backend decide con el árbol VIEJO. Es un
264
+ * **fail-open temporal** —el tenant antiguo conserva acceso tras un
265
+ * `moved`, los denies heredados no aplican tras un `attached`—. No hay
266
+ * 2PC; es el precio de tener el árbol en dos sitios.
267
+ */
268
+ relayScopeChanges(options?: {
269
+ limit?: number;
270
+ batchSize?: number;
271
+ dryRun?: boolean;
272
+ }): Promise<ScopeRelayReport>;
273
+ /**
274
+ * **`authz:reconcile --to=<driver>`** (3b-3a): la ÚNICA primitiva de
275
+ * migración y verificación del paquete, y el motivo de la fase entera —
276
+ * «todo en un driver o todo en otro, con una migración idempotente y
277
+ * bidireccional». Sustituye a `openfga:import`, que el 2k borró.
278
+ *
279
+ * Operación de PLATAFORMA, como `driver()` y `relayScopeChanges`: no lleva
280
+ * actor, no mide rangos y **no se expone por HTTP**.
281
+ *
282
+ * El driver de destino se resuelve **por nombre del registro**
283
+ * (`config.drivers[to]`), no por `config.default`: la migración de verdad
284
+ * es «el motor sigue corriendo con `database` mientras se llena el store de
285
+ * `openfga`», y con el default no habría forma de nombrar al destino. Un
286
+ * driver que no sabe reconstruirse lo dice (500 `E_AUTHZ_UNSUPPORTED`
287
+ * nombrando `reconcile`); el driver `database` es ese caso: sus tablas SON
288
+ * el origen y llenarlas desde un store es la otra dirección (3b-3b).
289
+ *
290
+ * Durante la pasada que ESCRIBE, las escrituras del motor están CONGELADAS
291
+ * (`withFrozenWrites`): un `grant` que aterrizara entre la lectura del
292
+ * origen y la escritura del destino no llegaría al destino y no aparecería
293
+ * en ningún contador. Las lecturas siguen. El `finally` descongela pase lo
294
+ * que pase.
295
+ *
296
+ * **`--dry-run` NO congela** (3b-6, panel 3 · juez §3). El verificador es
297
+ * read-only por contrato: no escribe nada, así que no tiene NADA que
298
+ * proteger, y congelar ahí sería apagar las escrituras a cambio de cero.
299
+ * Está publicado para correrlo en CI y en un cron, o sea justo el sitio
300
+ * desde el que un mecanismo de indisponibilidad se dispara solo — hoy
301
+ * contra el proceso del job, y contra la flota entera el día que el freeze
302
+ * sea durable. El único contraargumento posible —que congelar estabiliza
303
+ * sus números— no vale: los números de un verificador read-only no son una
304
+ * garantía de nada.
305
+ *
306
+ * Lo que añade el manager al reporte del driver es lo único que el driver
307
+ * no puede ver: **la ventana del relay** —los cambios del árbol encolados y
308
+ * sin aplicar, que son la deriva que el store todavía no conoce (decisión
309
+ * del dueño del 2026-08-30, consecuencia 4)— y las entradas APARCADAS, que
310
+ * no son una ventana sino una divergencia permanente.
311
+ */
312
+ reconcile(options: {
313
+ to: string;
314
+ from?: string;
315
+ } & ReconcileOptions): Promise<ReconcileReport>;
316
+ /**
317
+ * ¿`outer` contiene a `inner`? = `outer ∈ chain(inner)`, inclusive: un scope
318
+ * se contiene a sí mismo y `APP_SCOPE` contiene todo. Un `inner` que el
319
+ * árbol no conoce no está dentro de nada (`false`). Siempre con el resolutor
320
+ * fresco (nunca el memo por request): la contención decide escrituras.
321
+ */
322
+ isWithin(inner: ScopeRef, outer: ScopeRef): Promise<boolean>;
18
323
  authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
19
- hasRole(subject: SubjectRef, role: string, scope: ScopeRef): Promise<boolean>;
20
- listSubjects(role: string, scope: ScopeRef): Promise<SubjectRef[]>;
324
+ hasRole(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<boolean>;
325
+ /**
326
+ * `authorize` sobre varios scopes, un booleano por posición (2.1, B6).
327
+ * Delegado al driver si trae `authorizeMany` (openfga: un batchCheck);
328
+ * si no, `Promise.all` de `authorize` sobre una vista con los ancestros
329
+ * memoizados (una llamada al árbol por scope distinto, aunque se repita).
330
+ * Idéntico a N `authorize`: duplicados por posición, desconocido ⇒ false,
331
+ * y si una posición no se puede responder, lanza entero. Vacío ⇒ `[]`
332
+ * sin tocar backend ni árbol.
333
+ */
334
+ authorizeMany(subject: SubjectRef, permission: string, scopes: ScopeRef[]): Promise<boolean[]>;
335
+ /** Holders con asignación vigente del rol en ese scope exacto. `{ uuid }` es la forma exacta (3D · M1). */
336
+ listSubjects(role: RoleQuery, scope: ScopeRef): Promise<SubjectRef[]>;
21
337
  listScopes(subject: SubjectRef, permission: string): Promise<ScopeRef[]>;
22
338
  listRoles(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
23
339
  listRoleScopes(subject: SubjectRef, scopeType: ScopeType): Promise<ScopeRef[]>;
24
- grant(subject: SubjectRef, role: string, scope: ScopeRef, options?: GrantOptions): Promise<void>;
25
- revoke(subject: SubjectRef, role: string, scope: ScopeRef): Promise<void>;
26
- deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
27
- removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
340
+ /** Denies directos del holder (scope exacto, o todos). 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo implementa. */
341
+ listDenies(subject: SubjectRef, scope?: ScopeRef): Promise<DenyRef[]>;
342
+ /**
343
+ * Permisos efectivos del holder en un scope (2.1, B5): la unión de lo que
344
+ * conceden sus roles vigentes en toda la cadena (`listRoles` por nivel +
345
+ * catálogo) MENOS lo denegado en cualquier nivel de la cadena
346
+ * (`listDenies` por nivel). Es exactamente el conjunto `{ p | authorize(p) }`,
347
+ * calculado sin preguntar permiso a permiso. Scope desconocido ⇒ `[]`.
348
+ * Prerrequisito de `catalog/` (Fase 3).
349
+ */
350
+ effectivePermissions(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
351
+ /**
352
+ * Define un rol LOCAL a `ownerScope`. Policy, en este orden y antes de
353
+ * escribir nada: `actor` obligatorio y bien formado; `ownerScope` válido,
354
+ * no la raíz (los roles de la raíz son globales: config + sync) y conocido
355
+ * por el árbol (fresco); `spec` bien formado (slug, nivel ≠ `app`, rank
356
+ * entero, permisos); cada permiso en `config.delegablePermissions`, en el
357
+ * catálogo, componible en ese nivel (`assignableAt`, B5) y EFECTIVO para
358
+ * el actor en el owner (lo concede un rol suyo de la cadena y no lo tiene
359
+ * denegado en ella — C2); `0 < rank < min(rank del actor, rank máximo
360
+ * global)`; y ningún rol `(slug, scopeType)` visible en el owner (global,
361
+ * o local a un ancestro) ni local a un descendiente (colisión, 422
362
+ * `E_AUTHZ_CATALOG_CONFLICT`, re-comprobada dentro de la transacción
363
+ * serializada — 3D · M2). `options.within` contiene la escritura contra el
364
+ * OWNER y `requireWithin` la exige, como en las otras ocho (3D · M3).
365
+ * Devuelve el rol y notifica `role_defined`.
366
+ */
367
+ defineScopedRole(actor: SubjectRef, ownerScope: ScopeRef, spec: ScopedRoleSpec, options?: ScopedWriteOptions): Promise<CatalogRole>;
368
+ /**
369
+ * Cambia `name`/`description`/`rank`/`permissions` de un rol LOCAL (nunca
370
+ * su slug, nivel ni owner). Un global es 422 `E_AUTHZ_ROLE_IMMUTABLE`. El
371
+ * actor tiene que tener, en el owner del rol, rank MAYOR que el del rol
372
+ * (no se toca un rol de rango ≥ al propio) y la misma policy que al
373
+ * definir para lo que cambia: los permisos nuevos delegables/efectivos y
374
+ * componibles, el rank nuevo por debajo del suyo. Sin cambios reales no
375
+ * escribe ni notifica (idempotente). Notifica `role_updated`.
376
+ */
377
+ updateScopedRole(actor: SubjectRef, roleUuid: string, changes: ScopedRoleChanges, options?: ScopedWriteOptions): Promise<CatalogRole>;
378
+ /**
379
+ * Purga un rol LOCAL: sus asignaciones en todos los scopes, sus vínculos y
380
+ * el rol (`driver.purgeRole`, B4; 500 `E_AUTHZ_UNSUPPORTED` en un driver
381
+ * que no lo trae, sin tocar nada). Un global es 422
382
+ * `E_AUTHZ_ROLE_IMMUTABLE`; el actor necesita rank MAYOR que el del rol en
383
+ * su owner. Notifica `role_purged`. No necesita `listDenies`.
384
+ */
385
+ deleteScopedRole(actor: SubjectRef, roleUuid: string, options?: ScopedWriteOptions): Promise<void>;
386
+ /**
387
+ * Los roles LOCALES cuyo owner el árbol YA NO conoce, y —con `force`— su
388
+ * purga. Es el motor de `authz:catalog:prune-orphans` (3b-0 · Z2).
389
+ *
390
+ * Un rol así está DORMIDO, y «dormido» significa **exactamente** esto
391
+ * (3b-0b · AA1, auditor 3b-0): no es visible desde ningún scope vivo cuya
392
+ * cadena NO pase por su owner. No significa que no conceda. La regla única
393
+ * de visibilidad (invariante 18) pide que el owner esté en la cadena del
394
+ * scope preguntado, y **un descendiente vivo cuya ruta materializada sigue
395
+ * pasando por el owner la cumple**: ahí el rol concede, es membresía por
396
+ * los seis caminos de lectura y se puede ASIGNAR, por slug y por uuid.
397
+ * Ocurre en cuanto el consumidor borra la fila del owner sin borrar (o sin
398
+ * notificar) la de sus descendientes — el borrado en dos pasos y las rutas
399
+ * materializadas son lo normal. Por eso este barrido es destructivo de
400
+ * verdad y por eso `--dry-run` es el default: puede estar revocando
401
+ * permisos VIVOS, no recogiendo basura inerte. Lo que sí es seguro decir:
402
+ * un rol huérfano SIN asignaciones vigentes no concede nada, y ninguno
403
+ * concede en un scope cuya cadena no pase por el owner.
404
+ *
405
+ * Cada huérfano viene con `assignments` (hechos vigentes) y
406
+ * `stillGranting` (`assignments > 0`), que es la marca CONSERVADORA de
407
+ * «esto no es basura inerte»: cuenta hechos, no comprueba si el scope de
408
+ * cada uno sigue resolviendo. Falso ⇒ no concede seguro; verdadero ⇒
409
+ * míralo antes de `--force`.
410
+ *
411
+ * **Y esos hechos se los cuenta el DRIVER** (3b-2j, decisión del dueño del
412
+ * 2026-08-31 (3)), con `countRoleAssignments` del puerto. Hasta aquí los
413
+ * contaba el propio barrido en `authz_assignments` —la tabla del driver
414
+ * `database`—, así que con `openfga` en modo `facts`, donde los hechos
415
+ * viven en el store, `stillGranting` era SIEMPRE `false`: el barrido
416
+ * declaraba basura inerte, justo antes de un borrado destructivo, un rol
417
+ * que estaba concediendo (medido en el lote 2i). Un driver que no traiga
418
+ * el método deja los DOS campos en **`undefined`**, nunca en `false`: «no
419
+ * lo sé» no puede degradar a «no concede», que es exactamente el bug. Con
420
+ * `undefined` el rol no es demostrablemente inerte y el comando lo lista
421
+ * APARTE, igual que a los que sí conceden.
422
+ *
423
+ * Lo que el rol dormido sí hace en todo caso es ocupar su `(slug, nivel)`
424
+ * dentro del subárbol donde todavía se le vea, y `deleteScopedRole` no lo
425
+ * alcanza (resuelve el owner en fresco y responde 422
426
+ * `E_AUTHZ_UNKNOWN_SCOPE`). Hasta 3G esa limpieza la arrastraba
427
+ * `scopes.detached`, y ahí es donde nacieron tres de las cuatro
428
+ * regresiones de la Fase 3: la operación la dispara un TENANT, sobre un
429
+ * scope que ya no resuelve, así que hubo que inventarle una policy de
430
+ * rango sin cadena donde medirla, una enumeración del subárbol y una
431
+ * degradación — tres piezas que compuestas destruían roles de
432
+ * descendientes VIVOS. Aquí no hay nada de eso: es una operación de
433
+ * PLATAFORMA (una tarea de mantenimiento con acceso al catálogo, como
434
+ * `authz:catalog:sync`), no lleva actor y no mide rangos, exactamente como
435
+ * el `purgeRole` de último recurso que el README ya prometía. Es, junto a
436
+ * `driver()`, **API de plataforma**: se salta `requireActor` y
437
+ * `requireWithin` a propósito, así que no se expone a un controlador.
438
+ *
439
+ * `force: false` (el default, y el del comando: `--dry-run`) NO escribe:
440
+ * devuelve la lista para que un humano la mire. Con `force: true` cada rol
441
+ * se purga con `purgeRole` —atómico: asignaciones + vínculos + fila +
442
+ * versión del catálogo— y se notifica `role_purged` (sin `actor`). El
443
+ * conjunto no es atómico, y por eso el reporte dice QUÉ se purgó
444
+ * (`purged: CatalogRoleRef[]`, 3b-0b · AB3) y no cuántos: si un
445
+ * `purgeRole` falla a mitad, lo anterior ya está borrado —con el hallazgo
446
+ * de AA1 eso puede ser revocación parcial de permisos vivos— y quien
447
+ * recoge el 503 necesita la lista, no un contador. Una pasada
448
+ * interrumpida la recoge la siguiente (el orden es estable por uuid).
449
+ *
450
+ * **Dos seguros contra el barrido a ciegas**, que es el riesgo real
451
+ * (auditor 3b-0):
452
+ *
453
+ * - **Cota de purga masiva** (AA2): si TODOS los owners distintos
454
+ * resultan huérfanos, o si los huérfanos superan el 50 % de los roles
455
+ * locales, `force` es 500 `E_AUTHZ_MASS_PURGE_REFUSED` **antes de
456
+ * borrar nada**. Esa es la firma de un `resolveChain` filtrado por el
457
+ * tenant de la petición o corriendo sin contexto (comando, réplica
458
+ * atrasada): devuelve `null` para todo y la pasada se lleva el catálogo
459
+ * local de TODOS los tenants (medido: 2 de 2 roles vivos). Una poda
460
+ * grande de verdad pasa con `allowMassPurge: true`
461
+ * (`--allow-mass-purge`), que es una decisión humana. El `--dry-run` no
462
+ * lanza —es justo el diagnóstico que hay que poder mirar— pero lo
463
+ * marca en `massPurge`.
464
+ * - **Re-resolución justo antes de cada purga** (AA3): entre la lectura y
465
+ * el borrado cabe un `scopes.attached`/restore concurrente, y la
466
+ * ventana es TODA la pasada (N roles + N `resolveChain`), no un
467
+ * instante. Cada owner se vuelve a resolver en FRESCO inmediatamente
468
+ * antes de su `purgeRole`; si ha vuelto, el rol se salta y se cuenta en
469
+ * `skipped` con `reason: 'owner-came-back'`.
470
+ *
471
+ * Coste: una lectura del catálogo local + un `resolveChain` por OWNER
472
+ * DISTINTO (memoizado) + UNA llamada a `countRoleAssignments` con los
473
+ * uuids de los huérfanos (ninguna si no hay) + un `resolveChain` más por
474
+ * rol purgado (el de AA3). Es O(owners con roles locales) para mirar y
475
+ * O(roles purgados) para borrar, y corre en un comando, no en el camino de
476
+ * una petición.
477
+ */
478
+ pruneOrphanRoles(options?: {
479
+ force?: boolean;
480
+ allowMassPurge?: boolean;
481
+ }): Promise<{
482
+ orphans: Array<{
483
+ role: CatalogRole;
484
+ owner: ScopeRef;
485
+ permissions: string[];
486
+ assignments: number | undefined;
487
+ stillGranting: boolean | undefined;
488
+ }>;
489
+ purged: CatalogRoleRef[];
490
+ skipped: Array<{
491
+ role: CatalogRoleRef;
492
+ reason: 'owner-came-back';
493
+ }>;
494
+ /** ¿La pasada tiene la firma de un resolutor ciego? Con `force` exige `allowMassPurge`. */
495
+ massPurge: boolean;
496
+ dryRun: boolean;
497
+ }>;
498
+ /**
499
+ * Asigna un rol al holder en un scope. `role` es un `RoleQuery` (3D · M1):
500
+ * un slug, `{ slug, scopeType }` o `{ uuid }` — esta última es la forma
501
+ * exacta, la única que responde cuando dos roles locales homónimos son
502
+ * visibles en la misma cadena (las otras dos son 422
503
+ * `E_AUTHZ_AMBIGUOUS_ROLE`, nunca «el más cercano gana»).
504
+ */
505
+ grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
506
+ /**
507
+ * Quita la asignación del rol en ese scope exacto. Por slug se quitan las
508
+ * de TODOS los homónimos `(slug, nivel)` (3B; quitar nunca concede, y el
509
+ * scope puede no existir ya para el árbol); por `{ uuid }`, solo la de ese
510
+ * rol.
511
+ */
512
+ revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: ScopedWriteOptions): Promise<void>;
513
+ deny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: DenyOptions): Promise<void>;
514
+ removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: ScopedWriteOptions): Promise<void>;
515
+ /**
516
+ * Scopes de un tipo donde el holder tiene el permiso (2.1, B3). La ÚNICA
517
+ * API del paquete que enumera descendientes — excepción explícita al
518
+ * invariante 7 (`list*` siguen siendo directos) — y lo hace con el
519
+ * `descendantsOf` del consumidor, nunca con N+1 `resolveChain` a ciegas.
520
+ *
521
+ * Regla:
522
+ * 1. `listScopes(subject, permission)`: los scopes DIRECTOS que conceden,
523
+ * ya sin los bloqueados por un deny en su cadena y sin los que el árbol
524
+ * no conoce. Vacío ⇒ `none`.
525
+ * 2. Si la raíz está entre ellos ⇒ `all`, con `excludedSubtrees` = todos
526
+ * los scopes con deny vivo del permiso (`listDenies`), como subárboles
527
+ * (F10). Nunca `all` sin esa lista (juez cruce 5): un deny vivo tiene
528
+ * que verse.
529
+ * 3. Si no: candidatos = directos ∪ sus descendientes (`descendantsOf`).
530
+ * Cada candidato se contrasta con `resolveChain` (memoizado por
531
+ * request, F3): su cadena tiene que contener el scope concedente —si
532
+ * no, los dos resolutores del consumidor describen árboles distintos y
533
+ * se lanza 503 `E_AUTHZ_RESOLVER_FAILED`, nunca una lista con cruces—
534
+ * y no puede contener un scope denegado (es EXACTAMENTE la regla de
535
+ * `authorize`: deny en la cadena ⇒ false). Se filtran por `scopeType`.
536
+ * 4. Más de `maxScopes` ⇒ 422 `E_AUTHZ_TOO_MANY_SCOPES`, nunca parcial, y
537
+ * se corta en cuanto se sabe (F8): los directos del tipo antes de bajar
538
+ * y el conteo del tipo dentro del bucle. `options.maxScopes` solo puede
539
+ * BAJAR la cota del config.
540
+ * Sin `scopes.descendantsOf` ⇒ 500 `E_AUTHZ_NO_DESCENDANTS_RESOLVER`
541
+ * (antes de mirar nada: un `none` sin árbol sería mentira).
542
+ */
543
+ authorizedScopes(subject: SubjectRef, permission: string, scopeType: ScopeType, options?: {
544
+ maxScopes?: number;
545
+ }): Promise<AuthorizedScopes>;
546
+ /**
547
+ * Los `excludedSubtrees` de un `all` (F10) expandidos: cada scope denegado
548
+ * y todos sus descendientes, con el `descendantsOf` del config. Un scope
549
+ * que `descendantsOf` no conoce (`null`) es 503: restarlo a medias
550
+ * dejaría su subárbol dentro (fail-open). Cotas: `maxDescendants` por
551
+ * subárbol y `maxScopes` (config o por llamada, nunca por encima del
552
+ * config) sobre el total ⇒ 422, nunca parcial.
553
+ */
554
+ expandExcludedSubtrees(excluded: ExcludedSubtree[], options?: {
555
+ maxScopes?: number;
556
+ }): Promise<ScopeRef[]>;
28
557
  }
29
558
  //# sourceMappingURL=manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,KAAK,EACV,mBAAmB,EAGnB,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,UAAU,EACX,MAAM,YAAY,CAAA;AAEnB;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;;gBAInB,MAAM,EAAE,mBAAmB;IAIjC,MAAM,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAgB5C,mDAAmD;IACnD,iBAAiB,IAAI,IAAI;IAInB,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrF,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7E,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAIlE,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAIxE,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAIlE,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAI9E,KAAK,CACT,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAWV,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7E,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CA+B1F"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAyB7D,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAqC1D,OAAO,KAAK,EACV,mBAAmB,EAEnB,gBAAgB,EAGhB,WAAW,EACX,cAAc,EACd,WAAW,EACX,OAAO,EACP,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,SAAS,EAGT,iBAAiB,EACjB,cAAc,EAGd,gBAAgB,EAChB,eAAe,EAIf,kBAAkB,EAElB,gBAAgB,EAChB,QAAQ,EAER,qBAAqB,EACrB,SAAS,EACT,UAAU,EAEX,MAAM,YAAY,CAAA;AAuBnB;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,oBAAoB,EAClB,WAAW,GACX,SAAS,GACT,cAAc,GACd,YAAY,GACZ,WAAW,GACX,gBAAgB,GAChB,OAAO,GACP,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,sBAAsB,GACtB,eAAe,GACf,kBAAkB,GAClB,wBAAwB,GACxB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,CACrB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAEhH;AAED,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,OAAQ,CAAA;AACvC,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAC7C;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,WAAa,CAAA;AACzC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAA;AACtC,eAAO,MAAM,mBAAmB,QAAS,CAAA;AAGzC,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAE7C,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAgBD,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,UAAU,GAAG,SAAS,CAAA;IAC5B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAUD;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;;gBAwBnB,MAAM,EAAE,mBAAmB;IAkCvC;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB;IAmB9D;;;;;;;;;;;;;;OAcG;IACG,MAAM,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAyD5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,UAAU,CAAA;KAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IA4DjH;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBpD;;;;OAIG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,mGAAmG;IAC7F,YAAY,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAYlD;;;;;;;OAOG;IACG,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuH3E,mDAAmD;IACnD,iBAAiB,IAAI,IAAI;IAiCzB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM;0BAOW,QAAQ,UAAU,QAAQ,YAAY,qBAAqB,KAAG,OAAO,CAAC,IAAI,CAAC;QAiBnG;;;;;;;;;;;;WAYG;uBACkB,QAAQ,aAAa,QAAQ,YAAY,qBAAqB,KAAG,OAAO,CAAC,IAAI,CAAC;QAenG;;;;;;;;;;;;;;;;;;;;;;WAsBG;0BACqB,QAAQ,YAAY,qBAAqB,KAAG,OAAO,CAAC,IAAI,CAAC;MA6ClF;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2CG;IACG,iBAAiB,CACrB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GACrE,OAAO,CAAC,gBAAgB,CAAC;IAkJ5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CACb,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,gBAAgB,GACxD,OAAO,CAAC,eAAe,CAAC;IA4Z3B;;;;;OAKG;IACG,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IA8J5D,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrF,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtF;;;;;;;;OAQG;IACG,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IA0BpG,2GAA2G;IACrG,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAKrE,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAKxE,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKlE,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAKpF,mHAAmH;IAC7G,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAM3E;;;;;;;OAOG;IACG,oBAAoB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiKnF;;;;;;;;;;;;;;;OAeG;IACG,gBAAgB,CACpB,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,QAAQ,EACpB,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,WAAW,CAAC;IAmGvB;;;;;;;;OAQG;IACG,gBAAgB,CACpB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,WAAW,CAAC;IA2FvB;;;;;;OAMG;IACG,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBxG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2FG;IACG,gBAAgB,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC;QAC3F,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,WAAW,CAAC;YAAC,KAAK,EAAE,QAAQ,CAAC;YAAC,WAAW,EAAE,MAAM,EAAE,CAAC;YAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;YAAC,aAAa,EAAE,OAAO,GAAG,SAAS,CAAA;SAAE,CAAC,CAAA;QAClJ,MAAM,EAAE,cAAc,EAAE,CAAA;QACxB,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,cAAc,CAAC;YAAC,MAAM,EAAE,iBAAiB,CAAA;SAAE,CAAC,CAAA;QACnE,2FAA2F;QAC3F,SAAS,EAAE,OAAO,CAAA;QAClB,MAAM,EAAE,OAAO,CAAA;KAChB,CAAC;IAshBF;;;;;;OAMG;IACG,KAAK,CACT,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,YAAY,CAAC;IAyCxB;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1G,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IASpG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IASvH;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,gBAAgB,CACpB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GACnC,OAAO,CAAC,gBAAgB,CAAC;IAoE5B;;;;;;;OAOG;IACG,sBAAsB,CAC1B,QAAQ,EAAE,eAAe,EAAE,EAC3B,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GACnC,OAAO,CAAC,QAAQ,EAAE,CAAC;CAmNvB"}