@jantstack/adonis-authz 2.5.0-alpha.2 → 2.5.0-alpha.4

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 (275) hide show
  1. package/README.md +743 -28
  2. package/build/commands/authz_catalog_diff.d.ts +0 -1
  3. package/build/commands/authz_catalog_diff.js +0 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +0 -1
  5. package/build/commands/authz_catalog_prune_orphans.js +0 -1
  6. package/build/commands/authz_catalog_sync.d.ts +0 -1
  7. package/build/commands/authz_catalog_sync.js +0 -1
  8. package/build/commands/authz_freeze.d.ts +0 -1
  9. package/build/commands/authz_freeze.js +0 -1
  10. package/build/commands/authz_reconcile.d.ts +0 -1
  11. package/build/commands/authz_reconcile.js +0 -1
  12. package/build/commands/authz_relations_reconcile.d.ts +6 -1
  13. package/build/commands/authz_relations_reconcile.js +163 -5
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +49 -5
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +14 -2
  18. package/build/commands/authz_unfreeze.d.ts +0 -1
  19. package/build/commands/authz_unfreeze.js +0 -1
  20. package/build/commands/main.d.ts +0 -1
  21. package/build/commands/main.js +0 -1
  22. package/build/commands/openfga_provision.d.ts +72 -1
  23. package/build/commands/openfga_provision.js +236 -4
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +11 -9
  27. package/build/index.js +12 -5
  28. package/build/providers/authz_provider.d.ts +0 -1
  29. package/build/providers/authz_provider.js +0 -1
  30. package/build/services/main.d.ts +0 -1
  31. package/build/services/main.js +0 -1
  32. package/build/services/relations.d.ts +0 -1
  33. package/build/services/relations.js +0 -1
  34. package/build/src/catalog/catalog.d.ts +0 -1
  35. package/build/src/catalog/catalog.js +0 -1
  36. package/build/src/catalog/catalog_cache.d.ts +13 -1
  37. package/build/src/catalog/catalog_cache.js +25 -1
  38. package/build/src/clock.d.ts +0 -1
  39. package/build/src/clock.js +0 -1
  40. package/build/src/define_config.d.ts +0 -1
  41. package/build/src/define_config.js +24 -1
  42. package/build/src/drivers/database_driver.d.ts +6 -1
  43. package/build/src/drivers/database_driver.js +22 -2
  44. package/build/src/drivers/database_relations_driver.d.ts +46 -5
  45. package/build/src/drivers/database_relations_driver.js +297 -59
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +980 -231
  48. package/build/src/drivers/openfga_facts.d.ts +112 -1
  49. package/build/src/drivers/openfga_facts.js +351 -11
  50. package/build/src/drivers/openfga_relations_driver.d.ts +109 -8
  51. package/build/src/drivers/openfga_relations_driver.js +817 -53
  52. package/build/src/errors.d.ts +233 -1
  53. package/build/src/errors.js +283 -1
  54. package/build/src/expiry.d.ts +51 -2
  55. package/build/src/expiry.js +96 -2
  56. package/build/src/freeze.d.ts +0 -1
  57. package/build/src/freeze.js +0 -1
  58. package/build/src/hierarchical_resolver.d.ts +0 -1
  59. package/build/src/hierarchical_resolver.js +0 -1
  60. package/build/src/http/app_access_middleware.d.ts +0 -1
  61. package/build/src/http/app_access_middleware.js +0 -1
  62. package/build/src/http/resource_access_middleware.d.ts +0 -1
  63. package/build/src/http/resource_access_middleware.js +0 -1
  64. package/build/src/identity.d.ts +2 -1
  65. package/build/src/identity.js +4 -1
  66. package/build/src/manager.d.ts +0 -1
  67. package/build/src/manager.js +164 -31
  68. package/build/src/memoize_ancestors.d.ts +0 -1
  69. package/build/src/memoize_ancestors.js +0 -1
  70. package/build/src/models/authz_assignment.d.ts +5 -6
  71. package/build/src/models/authz_assignment.js +0 -1
  72. package/build/src/models/authz_deny.d.ts +5 -6
  73. package/build/src/models/authz_deny.js +0 -1
  74. package/build/src/models/authz_permission.d.ts +5 -6
  75. package/build/src/models/authz_permission.js +0 -1
  76. package/build/src/models/authz_role.d.ts +5 -6
  77. package/build/src/models/authz_role.js +0 -1
  78. package/build/src/models/authz_role_permission.d.ts +5 -6
  79. package/build/src/models/authz_role_permission.js +0 -1
  80. package/build/src/openfga.d.ts +3 -4
  81. package/build/src/openfga.js +5 -3
  82. package/build/src/reconcile.d.ts +0 -1
  83. package/build/src/reconcile.js +0 -1
  84. package/build/src/relation_partition_trigger.d.ts +0 -1
  85. package/build/src/relation_partition_trigger.js +0 -1
  86. package/build/src/relations/check_many.d.ts +1 -2
  87. package/build/src/relations/check_many.js +6 -5
  88. package/build/src/relations/define_relations_config.d.ts +140 -5
  89. package/build/src/relations/define_relations_config.js +205 -6
  90. package/build/src/relations/deny_reach.d.ts +102 -0
  91. package/build/src/relations/deny_reach.js +230 -0
  92. package/build/src/relations/group_depth.d.ts +215 -0
  93. package/build/src/relations/group_depth.js +453 -0
  94. package/build/src/relations/manager.d.ts +46 -8
  95. package/build/src/relations/manager.js +178 -16
  96. package/build/src/relations/page.d.ts +19 -0
  97. package/build/src/relations/page.js +74 -0
  98. package/build/src/relations/reconcile.d.ts +274 -2
  99. package/build/src/relations/reconcile.js +1006 -162
  100. package/build/src/relations/tuple_key.d.ts +0 -1
  101. package/build/src/relations/tuple_key.js +0 -1
  102. package/build/src/relations/tuple_order.d.ts +127 -0
  103. package/build/src/relations/tuple_order.js +599 -0
  104. package/build/src/relations_config_store.d.ts +57 -2
  105. package/build/src/relations_config_store.js +155 -5
  106. package/build/src/relations_reconcile.d.ts +0 -1
  107. package/build/src/relations_reconcile.js +0 -1
  108. package/build/src/scope_outbox.d.ts +0 -1
  109. package/build/src/scope_outbox.js +0 -1
  110. package/build/src/shared/backend_guard.d.ts +60 -1
  111. package/build/src/shared/backend_guard.js +179 -2
  112. package/build/src/shared/sql_expiry.d.ts +0 -1
  113. package/build/src/shared/sql_expiry.js +0 -1
  114. package/build/src/shared/transaction_guard.d.ts +0 -1
  115. package/build/src/shared/transaction_guard.js +0 -1
  116. package/build/src/sql_descendants.d.ts +0 -1
  117. package/build/src/sql_descendants.js +0 -1
  118. package/build/src/testing/contract.d.ts +7 -1
  119. package/build/src/testing/contract.js +77 -1
  120. package/build/src/testing/main.d.ts +1 -2
  121. package/build/src/testing/main.js +1 -2
  122. package/build/src/testing/migration_contract.d.ts +0 -1
  123. package/build/src/testing/migration_contract.js +0 -1
  124. package/build/src/testing/relations_contract.d.ts +25 -1
  125. package/build/src/testing/relations_contract.js +996 -49
  126. package/build/src/testing/relations_reconcile_contract.d.ts +9 -1
  127. package/build/src/testing/relations_reconcile_contract.js +207 -1
  128. package/build/src/testing/scope_outbox_contract.d.ts +0 -1
  129. package/build/src/testing/scope_outbox_contract.js +0 -1
  130. package/build/src/testing/scope_tree.d.ts +0 -1
  131. package/build/src/testing/scope_tree.js +0 -1
  132. package/build/src/testing/test_clock.d.ts +0 -1
  133. package/build/src/testing/test_clock.js +0 -1
  134. package/build/src/traits/authz_scopes.d.ts +0 -1
  135. package/build/src/traits/authz_scopes.js +0 -1
  136. package/build/src/traits/has_uuid.d.ts +5 -6
  137. package/build/src/traits/has_uuid.js +0 -1
  138. package/build/src/types.d.ts +170 -15
  139. package/build/src/types.js +6 -1
  140. package/build/stubs/config/authorization.stub +22 -6
  141. package/build/stubs/main.d.ts +0 -1
  142. package/build/stubs/main.js +0 -1
  143. package/package.json +4 -3
  144. package/build/commands/authz_catalog_diff.d.ts.map +0 -1
  145. package/build/commands/authz_catalog_diff.js.map +0 -1
  146. package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
  147. package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
  148. package/build/commands/authz_catalog_sync.d.ts.map +0 -1
  149. package/build/commands/authz_catalog_sync.js.map +0 -1
  150. package/build/commands/authz_freeze.d.ts.map +0 -1
  151. package/build/commands/authz_freeze.js.map +0 -1
  152. package/build/commands/authz_reconcile.d.ts.map +0 -1
  153. package/build/commands/authz_reconcile.js.map +0 -1
  154. package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
  155. package/build/commands/authz_relations_reconcile.js.map +0 -1
  156. package/build/commands/authz_scopes_relay.d.ts.map +0 -1
  157. package/build/commands/authz_scopes_relay.js.map +0 -1
  158. package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
  159. package/build/commands/authz_scopes_requeue.js.map +0 -1
  160. package/build/commands/authz_unfreeze.d.ts.map +0 -1
  161. package/build/commands/authz_unfreeze.js.map +0 -1
  162. package/build/commands/main.d.ts.map +0 -1
  163. package/build/commands/main.js.map +0 -1
  164. package/build/commands/openfga_provision.d.ts.map +0 -1
  165. package/build/commands/openfga_provision.js.map +0 -1
  166. package/build/configure.d.ts.map +0 -1
  167. package/build/configure.js.map +0 -1
  168. package/build/index.d.ts.map +0 -1
  169. package/build/index.js.map +0 -1
  170. package/build/providers/authz_provider.d.ts.map +0 -1
  171. package/build/providers/authz_provider.js.map +0 -1
  172. package/build/services/main.d.ts.map +0 -1
  173. package/build/services/main.js.map +0 -1
  174. package/build/services/relations.d.ts.map +0 -1
  175. package/build/services/relations.js.map +0 -1
  176. package/build/src/catalog/catalog.d.ts.map +0 -1
  177. package/build/src/catalog/catalog.js.map +0 -1
  178. package/build/src/catalog/catalog_cache.d.ts.map +0 -1
  179. package/build/src/catalog/catalog_cache.js.map +0 -1
  180. package/build/src/clock.d.ts.map +0 -1
  181. package/build/src/clock.js.map +0 -1
  182. package/build/src/define_config.d.ts.map +0 -1
  183. package/build/src/define_config.js.map +0 -1
  184. package/build/src/drivers/database_driver.d.ts.map +0 -1
  185. package/build/src/drivers/database_driver.js.map +0 -1
  186. package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
  187. package/build/src/drivers/database_relations_driver.js.map +0 -1
  188. package/build/src/drivers/openfga_driver.d.ts.map +0 -1
  189. package/build/src/drivers/openfga_driver.js.map +0 -1
  190. package/build/src/drivers/openfga_facts.d.ts.map +0 -1
  191. package/build/src/drivers/openfga_facts.js.map +0 -1
  192. package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
  193. package/build/src/drivers/openfga_relations_driver.js.map +0 -1
  194. package/build/src/errors.d.ts.map +0 -1
  195. package/build/src/errors.js.map +0 -1
  196. package/build/src/expiry.d.ts.map +0 -1
  197. package/build/src/expiry.js.map +0 -1
  198. package/build/src/freeze.d.ts.map +0 -1
  199. package/build/src/freeze.js.map +0 -1
  200. package/build/src/hierarchical_resolver.d.ts.map +0 -1
  201. package/build/src/hierarchical_resolver.js.map +0 -1
  202. package/build/src/http/app_access_middleware.d.ts.map +0 -1
  203. package/build/src/http/app_access_middleware.js.map +0 -1
  204. package/build/src/http/resource_access_middleware.d.ts.map +0 -1
  205. package/build/src/http/resource_access_middleware.js.map +0 -1
  206. package/build/src/identity.d.ts.map +0 -1
  207. package/build/src/identity.js.map +0 -1
  208. package/build/src/manager.d.ts.map +0 -1
  209. package/build/src/manager.js.map +0 -1
  210. package/build/src/memoize_ancestors.d.ts.map +0 -1
  211. package/build/src/memoize_ancestors.js.map +0 -1
  212. package/build/src/models/authz_assignment.d.ts.map +0 -1
  213. package/build/src/models/authz_assignment.js.map +0 -1
  214. package/build/src/models/authz_deny.d.ts.map +0 -1
  215. package/build/src/models/authz_deny.js.map +0 -1
  216. package/build/src/models/authz_permission.d.ts.map +0 -1
  217. package/build/src/models/authz_permission.js.map +0 -1
  218. package/build/src/models/authz_role.d.ts.map +0 -1
  219. package/build/src/models/authz_role.js.map +0 -1
  220. package/build/src/models/authz_role_permission.d.ts.map +0 -1
  221. package/build/src/models/authz_role_permission.js.map +0 -1
  222. package/build/src/openfga.d.ts.map +0 -1
  223. package/build/src/openfga.js.map +0 -1
  224. package/build/src/reconcile.d.ts.map +0 -1
  225. package/build/src/reconcile.js.map +0 -1
  226. package/build/src/relation_partition_trigger.d.ts.map +0 -1
  227. package/build/src/relation_partition_trigger.js.map +0 -1
  228. package/build/src/relations/check_many.d.ts.map +0 -1
  229. package/build/src/relations/check_many.js.map +0 -1
  230. package/build/src/relations/define_relations_config.d.ts.map +0 -1
  231. package/build/src/relations/define_relations_config.js.map +0 -1
  232. package/build/src/relations/manager.d.ts.map +0 -1
  233. package/build/src/relations/manager.js.map +0 -1
  234. package/build/src/relations/reconcile.d.ts.map +0 -1
  235. package/build/src/relations/reconcile.js.map +0 -1
  236. package/build/src/relations/tuple_key.d.ts.map +0 -1
  237. package/build/src/relations/tuple_key.js.map +0 -1
  238. package/build/src/relations_config_store.d.ts.map +0 -1
  239. package/build/src/relations_config_store.js.map +0 -1
  240. package/build/src/relations_reconcile.d.ts.map +0 -1
  241. package/build/src/relations_reconcile.js.map +0 -1
  242. package/build/src/scope_outbox.d.ts.map +0 -1
  243. package/build/src/scope_outbox.js.map +0 -1
  244. package/build/src/shared/backend_guard.d.ts.map +0 -1
  245. package/build/src/shared/backend_guard.js.map +0 -1
  246. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  247. package/build/src/shared/sql_expiry.js.map +0 -1
  248. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  249. package/build/src/shared/transaction_guard.js.map +0 -1
  250. package/build/src/sql_descendants.d.ts.map +0 -1
  251. package/build/src/sql_descendants.js.map +0 -1
  252. package/build/src/testing/contract.d.ts.map +0 -1
  253. package/build/src/testing/contract.js.map +0 -1
  254. package/build/src/testing/main.d.ts.map +0 -1
  255. package/build/src/testing/main.js.map +0 -1
  256. package/build/src/testing/migration_contract.d.ts.map +0 -1
  257. package/build/src/testing/migration_contract.js.map +0 -1
  258. package/build/src/testing/relations_contract.d.ts.map +0 -1
  259. package/build/src/testing/relations_contract.js.map +0 -1
  260. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  262. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  263. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  264. package/build/src/testing/scope_tree.d.ts.map +0 -1
  265. package/build/src/testing/scope_tree.js.map +0 -1
  266. package/build/src/testing/test_clock.d.ts.map +0 -1
  267. package/build/src/testing/test_clock.js.map +0 -1
  268. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  269. package/build/src/traits/authz_scopes.js.map +0 -1
  270. package/build/src/traits/has_uuid.d.ts.map +0 -1
  271. package/build/src/traits/has_uuid.js.map +0 -1
  272. package/build/src/types.d.ts.map +0 -1
  273. package/build/src/types.js.map +0 -1
  274. package/build/stubs/main.d.ts.map +0 -1
  275. package/build/stubs/main.js.map +0 -1
@@ -0,0 +1,599 @@
1
+ /**
2
+ * **El ORDEN de las escrituras y de los borrados de relaciones — punto de paso ÚNICO** (plan 2.5 · lote D; linaje L10
3
+ * «el estado intermedio de una operación multi-request concede», PROMOVIDO: 3b-2f R3, 3b-6 P1, D1 🟠 1 —la purga de
4
+ * `openfga`—, D2 🟠 1 —el reconcile— y, en el cierre 2 de D2, 🟡 N3 —escrituras y poda COMPUESTAS—). Lo usan los DOS
5
+ * caminos que escriben o borran relaciones en varias requests: `applyPartition` del reconcile
6
+ * (`src/relations/reconcile.ts`) y `purgeWrites` de las purgas de `openfga` (`src/drivers/openfga_relations_driver.ts`).
7
+ *
8
+ * **Cierre 2 de D2 · 🟡 N3 (estructural).** Hasta aquí había DOS órdenes (uno de escritura y otro de borrado) y el reconcile
9
+ * los encadenaba: TODAS las escrituras y luego TODA la poda. Un ex-miembro componía con un allow nuevo (U ∈ g sobra,
10
+ * `g#member viewer X` es nuevo: cortada tras la 1.ª escritura, U concedía sobre X y ni el inicial ni el final lo hacían).
11
+ * Ahora hay UNA función, `relationTransitionOrder`, que ordena la TRANSICIÓN entera (escrituras, borrados y renovaciones)
12
+ * y una propiedad que se DEMUESTRA por fuerza bruta (`tests/helpers/order_bruteforce.ts`, propiedad
13
+ * `relations.order.exhaustive` de la guardia y `tests/relations_order_exhaustive.spec.ts`).
14
+ *
15
+ * **Cinco fases** (las cuatro de los extremos solo restringen o solo amplían; lo difícil es la de en medio):
16
+ *
17
+ * 1. escribir los DENIES nuevos (y rehacer los que cambian) — solo restringe;
18
+ * 2. borrar los ALLOWS sobrantes sobre objetos que no son `group` — solo restringe;
19
+ * 3. las MEMBRESÍAS `group:*#member` que entran y que salen, en el orden de un GRAFO DE PRECEDENCIAS (abajo); con los
20
+ * allows reducidos a los que están en los dos estados y TODOS los denies de los dos estados presentes, que es el
21
+ * contexto más restrictivo posible para el paso difícil;
22
+ * 4. escribir los ALLOWS nuevos (y las renovaciones que no son denies) — con las membresías y los denies finales
23
+ * presentes, solo puede conceder lo que concede el final;
24
+ * 5. borrar los DENIES sobrantes, los ÚLTIMOS — idem.
25
+ *
26
+ * **El grafo de la fase 3.** Para cada membresía que cambia (arista `sujeto → grupo`) se calcula, en el grafo UNIÓN de
27
+ * los dos estados, qué objetos DENIEGA lo que alcanza su grupo (`D`) y qué objetos le CONCEDE de forma efectiva (`A`: los
28
+ * allows que están en los dos estados, menos los que cortan los denies que su grupo alcanza por aristas que no cambian —
29
+ * el «auto-corte», que solo se aplica con el predicado `cuts` de la config: sin él, ningún allow se da por cortado, lo
30
+ * conservador). Las precedencias, entre aristas que comparten algún sujeto:
31
+ *
32
+ * - **C1** (dos que entran): la que trae un deny sobre X antes que la que trae un allow sobre X;
33
+ * - **C2** (dos que salen): la que lleva un allow sobre X antes que la que lleva un deny sobre X (salvo que el sujeto
34
+ * solo alcance la primera a través de la segunda);
35
+ * - **C3** (una que sale y otra que entra en el MISMO camino anidado, `U ∈ h` sobra y `h ⊂ g` es nueva): la que sale
36
+ * antes, si el camino compuesto llevaría un allow efectivo — es el 🟡 N3 con anidamiento;
37
+ * - **C4** (una que entra y otra que sale y las dos traen un deny sobre X que protege un allow del sujeto): la que entra
38
+ * antes — el sujeto cambia de grupo denegado sin un instante sin deny.
39
+ *
40
+ * Se ordena por componentes fuertemente conexas (Tarjan) en orden topológico; entre las disponibles, escrituras antes que
41
+ * borrados, las escrituras de más lejos del sujeto primero y los borrados de más cerca primero, y (objeto, sujeto): el
42
+ * reparto NO depende del orden de enumeración del origen ni del `Read`.
43
+ *
44
+ * **Lo que garantiza, exactamente** (demostrado por fuerza bruta; README «Denies migrate as denies»): **si el grafo de
45
+ * precedencias no tiene un CICLO, ningún PREFIJO (el estado del store tras cada `Write` de la secuencia) concede algo que
46
+ * no concedían el estado inicial o el final, y ningún sujeto denegado en los dos concede en ninguno.** Es una garantía
47
+ * sobre PREFIJOS —una operación CORTADA—, no sobre lectores: un `Check` que abarca dos `Write` consecutivos puede mezclar
48
+ * lecturas de uno y otro (lector rasgado, fila «torn read» de Limits; decisión (6a) del dueño, linaje L12). El RESIDUO es exactamente el ciclo (`residue`): dos membresías
49
+ * que cambian en la misma pasada cuyas exigencias se contradicen —la forma típica: el mismo sujeto entra (o sale) a la
50
+ * vez en dos grupos y cada uno trae un deny que corta un allow que trae el otro—. Ahí NO existe en general ningún orden
51
+ * seguro (la fuerza bruta lo mide: de los casos del ciclo que conceden, una parte no admite NINGUNA permutación del diff
52
+ * segura), así que el orden aplica las aristas del ciclo en el orden de prioridad y lo DEVUELVE para que la pasada lo diga.
53
+ *
54
+ * Pura (regla 2 de pureza): no importa nada del paquete. Coste: lineal en las tuplas más los pares de membresías que
55
+ * cambian de un MISMO sujeto, y los de cada arista anidada (`group:x#member`) con las de los sujetos que tiene debajo.
56
+ */
57
+ /** El tipo built-in que transporta usersets (`group:g#member`). */
58
+ const GROUP_PREFIX = 'group:';
59
+ const MEMBER_SUFFIX = '#member';
60
+ export function relationOrderClass(view) {
61
+ if (view.deny)
62
+ return 'deny';
63
+ return view.object.startsWith(GROUP_PREFIX) ? 'membership' : 'allow';
64
+ }
65
+ /** `group:<id>#member` ⇒ `group:<id>`; cualquier otro sujeto ⇒ `null`. */
66
+ function usersetGroup(user) {
67
+ return user.startsWith(GROUP_PREFIX) && user.endsWith(MEMBER_SUFFIX) ? user.slice(0, -MEMBER_SUFFIX.length) : null;
68
+ }
69
+ function viewKey(v) {
70
+ return `${v.deny ? 'd' : 'a'}\u0000${v.object}\u0000${v.user}\u0000${v.relation ?? ''}`;
71
+ }
72
+ function objectTypeOf(object) {
73
+ const colon = object.indexOf(':');
74
+ return colon < 0 ? object : object.slice(0, colon);
75
+ }
76
+ /**
77
+ * Los grupos PORTADORES de deny: el `#member` de cada uno es sujeto de algún deny de `views`, más el cierre por la
78
+ * anidación (`group:G#member@group:H#member` con G portador ⇒ H portador). Devuelve los OBJETOS (`group:<id>`). Lo usa
79
+ * el reconcile para clasificar la renovación de una membresía PORTADORA (cierre 2 de D2 · 🟠 N1, `renewal-unsafe`).
80
+ */
81
+ export function denyCarrierGroups(views) {
82
+ const all = [...views];
83
+ const carriers = new Set();
84
+ for (const v of all) {
85
+ const group = v.deny ? usersetGroup(v.user) : null;
86
+ if (group !== null)
87
+ carriers.add(group);
88
+ }
89
+ const nested = all.filter((v) => relationOrderClass(v) === 'membership' && usersetGroup(v.user) !== null);
90
+ for (let grew = true; grew;) {
91
+ grew = false;
92
+ for (const v of nested) {
93
+ const inner = usersetGroup(v.user);
94
+ if (carriers.has(v.object) && !carriers.has(inner)) {
95
+ carriers.add(inner);
96
+ grew = true;
97
+ }
98
+ }
99
+ }
100
+ return carriers;
101
+ }
102
+ /**
103
+ * **Los denies que TRANSPORTA un grupo** (cierre 6 de D2 · ⚪ D5-3, para la letra): los `deny` cuyo sujeto es el `#member`
104
+ * del grupo o de un grupo que lo contiene, transitivamente (`group:G#member@group:H#member`: los miembros de H heredan los
105
+ * denies de G). Devuelve las VISTAS de esos denies, en el orden de entrada. Misma regla que `denyCarrierGroups`, al revés.
106
+ */
107
+ export function carriedDenies(views, group) {
108
+ const all = [...views];
109
+ const reach = new Set([group]);
110
+ const nested = all.filter((v) => relationOrderClass(v) === 'membership' && usersetGroup(v.user) !== null);
111
+ for (let grew = true; grew;) {
112
+ grew = false;
113
+ for (const v of nested) {
114
+ if (reach.has(usersetGroup(v.user)) && !reach.has(v.object)) {
115
+ reach.add(v.object);
116
+ grew = true;
117
+ }
118
+ }
119
+ }
120
+ return all.filter((v) => v.deny && reach.has(usersetGroup(v.user) ?? ''));
121
+ }
122
+ /** Un grafo de membresías (aristas `sujeto → grupo`): hacia arriba (`up`), hacia abajo (`below`) y altura. */
123
+ class MembershipGraph {
124
+ #parents = new Map();
125
+ #into = new Map();
126
+ #childGroups = new Map();
127
+ #up = new Map();
128
+ #below = new Map();
129
+ #height = new Map();
130
+ constructor(edges) {
131
+ for (const e of edges) {
132
+ let list = this.#into.get(e.object);
133
+ if (!list)
134
+ this.#into.set(e.object, (list = []));
135
+ list.push(e);
136
+ const child = usersetGroup(e.user);
137
+ if (child === null)
138
+ continue;
139
+ let parents = this.#parents.get(child);
140
+ if (!parents)
141
+ this.#parents.set(child, (parents = new Set()));
142
+ parents.add(e.object);
143
+ let children = this.#childGroups.get(e.object);
144
+ if (!children)
145
+ this.#childGroups.set(e.object, (children = new Set()));
146
+ children.add(child);
147
+ }
148
+ }
149
+ /** Los grupos que alcanza `group` (él incluido). */
150
+ up(group) {
151
+ let seen = this.#up.get(group);
152
+ if (seen)
153
+ return seen;
154
+ seen = new Set([group]);
155
+ const stack = [group];
156
+ while (stack.length > 0) {
157
+ for (const parent of this.#parents.get(stack.pop()) ?? []) {
158
+ if (!seen.has(parent)) {
159
+ seen.add(parent);
160
+ stack.push(parent);
161
+ }
162
+ }
163
+ }
164
+ this.#up.set(group, seen);
165
+ return seen;
166
+ }
167
+ /** Los sujetos (holders y usersets) que alcanzan al sujeto `user` (él incluido), opcionalmente SIN la arista `without`. */
168
+ below(user, without) {
169
+ if (usersetGroup(user) === null)
170
+ return new Set([user]);
171
+ if (without === undefined) {
172
+ const memo = this.#below.get(user);
173
+ if (memo)
174
+ return memo;
175
+ }
176
+ const out = new Set([user]);
177
+ const stack = [user];
178
+ while (stack.length > 0) {
179
+ const group = usersetGroup(stack.pop());
180
+ if (group === null)
181
+ continue;
182
+ for (const e of this.#into.get(group) ?? []) {
183
+ if (without !== undefined && viewKey(e) === without)
184
+ continue;
185
+ if (!out.has(e.user)) {
186
+ out.add(e.user);
187
+ stack.push(e.user);
188
+ }
189
+ }
190
+ }
191
+ if (without === undefined)
192
+ this.#below.set(user, out);
193
+ return out;
194
+ }
195
+ /** Distancia de un sujeto a los holders: 0 un holder; un userset, 1 + la de su grupo hijo más profundo. */
196
+ depth(user) {
197
+ const group = usersetGroup(user);
198
+ return group === null ? 0 : this.#groupHeight(group, new Set());
199
+ }
200
+ #groupHeight(group, path) {
201
+ const memo = this.#height.get(group);
202
+ if (memo !== undefined)
203
+ return memo;
204
+ if (path.has(group))
205
+ return 1;
206
+ path.add(group);
207
+ let height = 1;
208
+ for (const child of this.#childGroups.get(group) ?? [])
209
+ height = Math.max(height, 1 + this.#groupHeight(child, path));
210
+ path.delete(group);
211
+ this.#height.set(group, height);
212
+ return height;
213
+ }
214
+ }
215
+ /** Índice de tuplas (allows o denies) por sujeto: `sujeto → objeto → relaciones`. */
216
+ function bySubject(views) {
217
+ const out = new Map();
218
+ for (const v of views) {
219
+ let objects = out.get(v.user);
220
+ if (!objects)
221
+ out.set(v.user, (objects = new Map()));
222
+ let relations = objects.get(v.object);
223
+ if (!relations)
224
+ objects.set(v.object, (relations = new Set()));
225
+ relations.add(v.relation ?? '');
226
+ }
227
+ return out;
228
+ }
229
+ /** Lo que alcanza un grupo por un grafo: `objeto → relaciones` (los `#member` de los grupos de `up`). */
230
+ function reach(graph, index, group) {
231
+ const out = new Map();
232
+ for (const g of graph.up(group)) {
233
+ for (const [object, relations] of index.get(g + MEMBER_SUFFIX) ?? []) {
234
+ let set = out.get(object);
235
+ if (!set)
236
+ out.set(object, (set = new Set()));
237
+ for (const r of relations)
238
+ set.add(r);
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+ /**
244
+ * **El orden de una TRANSICIÓN** (cierre 2 de D2 · 🟡 N3): ver el docblock del módulo. `residue` = las membresías de los
245
+ * ciclos del grafo de precedencias (sin garantía); vacío ⇒ ningún PREFIJO concede fuera de inicial ∪ final.
246
+ */
247
+ export function relationTransitionOrder(transition, view, options = {}) {
248
+ const writes = (transition.writes ?? []).map((item) => ({ item, view: view(item) }));
249
+ const deletes = (transition.deletes ?? []).map((item) => ({ item, view: view(item) }));
250
+ const renewals = (transition.renewals ?? []).map((item) => ({ item, view: view(item) }));
251
+ const shortening = new Set(transition.shortening ?? []);
252
+ const renewedMemberships = renewals.filter((r) => relationOrderClass(r.view) === 'membership');
253
+ const changing = new Set([...writes, ...deletes, ...renewedMemberships].map((w) => viewKey(w.view)));
254
+ const everything = new Map();
255
+ for (const v of [...(transition.context ?? []), ...writes.map((w) => w.view), ...deletes.map((d) => d.view), ...renewals.map((r) => r.view)]) {
256
+ everything.set(viewKey(v), v);
257
+ }
258
+ const edges = [];
259
+ const keptAllows = [];
260
+ const denies = [];
261
+ for (const [key, v] of everything) {
262
+ const cls = relationOrderClass(v);
263
+ if (cls === 'deny')
264
+ denies.push(v);
265
+ else if (cls === 'membership')
266
+ edges.push(v);
267
+ else if (!changing.has(key))
268
+ keptAllows.push(v);
269
+ }
270
+ const renewedAs = (r) => (shortening.has(r.item) ? 'delete' : 'write');
271
+ const membership = planMemberships([
272
+ ...writes.filter((w) => relationOrderClass(w.view) === 'membership').map((w) => ({ ...w, emit: 'write' })),
273
+ ...renewedMemberships.filter((r) => renewedAs(r) === 'write').map((r) => ({ ...r, emit: 'renew' })),
274
+ ], [
275
+ ...deletes.filter((d) => relationOrderClass(d.view) === 'membership').map((d) => ({ ...d, emit: 'delete' })),
276
+ ...renewedMemberships.filter((r) => renewedAs(r) === 'delete').map((r) => ({ ...r, emit: 'renew' })),
277
+ ], { edges, keptAllows, denies, changing, cuts: options.cuts });
278
+ const steps = [];
279
+ // 1. denies nuevos (y los que se rehacen: un deny no caduca, D3) — solo restringen.
280
+ for (const w of writes)
281
+ if (w.view.deny)
282
+ steps.push({ item: w.item, op: 'write' });
283
+ for (const r of renewals)
284
+ if (r.view.deny)
285
+ steps.push({ item: r.item, op: 'renew' });
286
+ // 2. allows sobrantes sobre objetos que no son `group` — solo restringen; y (cierre 4) las renovaciones de allows que
287
+ // ACORTAN: en su caducidad nueva salen.
288
+ for (const d of deletes)
289
+ if (relationOrderClass(d.view) === 'allow')
290
+ steps.push({ item: d.item, op: 'delete' });
291
+ for (const r of renewals)
292
+ if (relationOrderClass(r.view) === 'allow' && renewedAs(r) === 'delete')
293
+ steps.push({ item: r.item, op: 'renew' });
294
+ // 3. membresías (y, cierre 4, sus renovaciones como la entrada o la salida que son en el horizonte), por el grafo.
295
+ steps.push(...membership.steps);
296
+ // 4. allows nuevos y las renovaciones de allows que ALARGAN (en `now` no cambian lo que se concede).
297
+ for (const w of writes)
298
+ if (relationOrderClass(w.view) === 'allow')
299
+ steps.push({ item: w.item, op: 'write' });
300
+ for (const r of renewals)
301
+ if (relationOrderClass(r.view) === 'allow' && renewedAs(r) === 'write')
302
+ steps.push({ item: r.item, op: 'renew' });
303
+ // 5. denies sobrantes, los ÚLTIMOS.
304
+ for (const d of deletes)
305
+ if (d.view.deny)
306
+ steps.push({ item: d.item, op: 'delete' });
307
+ return { steps, residue: membership.residue };
308
+ }
309
+ function planMemberships(writes, deletes, context) {
310
+ if (writes.length + deletes.length === 0)
311
+ return { steps: [], residue: [] };
312
+ const union = new MembershipGraph(context.edges);
313
+ const kept = new MembershipGraph(context.edges.filter((e) => !context.changing.has(viewKey(e))));
314
+ const allowIndex = bySubject(context.keptAllows);
315
+ const denyIndex = bySubject(context.denies);
316
+ const cuts = context.cuts;
317
+ const deniedBy = new Map();
318
+ /** Objetos denegados por lo que alcanza `group` en el grafo unión. */
319
+ const deniedObjects = (group) => {
320
+ let out = deniedBy.get(group);
321
+ if (!out)
322
+ deniedBy.set(group, (out = new Set(reach(union, denyIndex, group).keys())));
323
+ return out;
324
+ };
325
+ /**
326
+ * Objetos que concede de forma EFECTIVA lo que alcanza `group` (grafo unión, allows de los dos estados), menos los que
327
+ * corta TODO lo que deniegan, por aristas que NO cambian, los grupos `cutters` (el auto-corte). Sin `cuts`, nada se corta.
328
+ */
329
+ const effective = (group, cutters) => {
330
+ const allowed = reach(union, allowIndex, group);
331
+ if (!cuts)
332
+ return new Set(allowed.keys());
333
+ const keptDenies = new Map();
334
+ for (const cutter of cutters) {
335
+ for (const [object, relations] of reach(kept, denyIndex, cutter)) {
336
+ let set = keptDenies.get(object);
337
+ if (!set)
338
+ keptDenies.set(object, (set = new Set()));
339
+ for (const r of relations)
340
+ set.add(r);
341
+ }
342
+ }
343
+ const out = new Set();
344
+ for (const [object, relations] of allowed) {
345
+ const denied = keptDenies.get(object);
346
+ const type = objectTypeOf(object);
347
+ const cut = denied !== undefined && [...relations].every((a) => [...denied].some((d) => cuts(type, d, a)));
348
+ if (!cut)
349
+ out.add(object);
350
+ }
351
+ return out;
352
+ };
353
+ const effectiveMemo = new Map();
354
+ const ops = [];
355
+ for (const [list, op] of [
356
+ [writes, 'write'],
357
+ [deletes, 'delete'],
358
+ ]) {
359
+ for (const { item, view, emit } of list) {
360
+ let allows = effectiveMemo.get(view.object);
361
+ if (!allows)
362
+ effectiveMemo.set(view.object, (allows = effective(view.object, [view.object])));
363
+ ops.push({
364
+ item,
365
+ view,
366
+ op,
367
+ emit,
368
+ key: viewKey(view),
369
+ head: view.object,
370
+ tail: usersetGroup(view.user),
371
+ denies: deniedObjects(view.object),
372
+ allows,
373
+ depth: union.depth(view.user),
374
+ });
375
+ }
376
+ }
377
+ const subjectAllows = new Map();
378
+ /** Los objetos con algún allow (de los dos estados) que alcanza el sujeto, directo o por cualquier grupo (grafo unión). */
379
+ const allowsOfSubject = (subject) => {
380
+ let out = subjectAllows.get(subject);
381
+ if (out)
382
+ return out;
383
+ out = new Set([...(allowIndex.get(subject)?.keys() ?? [])]);
384
+ for (const e of context.edges) {
385
+ if (e.user !== subject)
386
+ continue;
387
+ for (const object of reach(union, allowIndex, e.object).keys())
388
+ out.add(object);
389
+ }
390
+ subjectAllows.set(subject, out);
391
+ return out;
392
+ };
393
+ const intersects = (a, b) => {
394
+ const [small, large] = a.size <= b.size ? [a, b] : [b, a];
395
+ for (const x of small)
396
+ if (large.has(x))
397
+ return true;
398
+ return false;
399
+ };
400
+ /**
401
+ * C3: ¿`d` (sale) y `w` (entra) componen un camino NUEVO? Cierre 5 de D2 (D4-2; el oráculo cuenta los objetos `group:`):
402
+ * el camino compuesto concede SIEMPRE algo —la propia membresía del sujeto en el grupo de arriba (`check(member)`,
403
+ * `membersOf`)—, así que ya no se exige un allow efectivo al final del camino (hasta el cierre 4 sí, y un corte entre
404
+ * `+U∈h` y `-h⊂g` hacía a U miembro de g, cosa que ni el inicial ni el final hacían).
405
+ */
406
+ const composes = (d, w) => {
407
+ if (w.tail !== null && union.up(d.head).has(w.tail))
408
+ return true;
409
+ if (d.tail !== null && union.up(w.head).has(d.tail))
410
+ return true;
411
+ return false;
412
+ };
413
+ /** ¿`a` va ANTES que `b`? (C1–C4). */
414
+ const before = (a, b) => {
415
+ if (a.op === 'write' && b.op === 'write') {
416
+ return [...a.denies].some((x) => b.allows.has(x)) && intersects(union.below(a.view.user), union.below(b.view.user));
417
+ }
418
+ if (a.op === 'delete' && b.op === 'delete') {
419
+ return [...a.allows].some((x) => b.denies.has(x)) && intersects(union.below(a.view.user, b.key), union.below(b.view.user));
420
+ }
421
+ if (a.op === 'delete')
422
+ return composes(a, b);
423
+ // C4, con la exclusión de C2 (cierre 5 de D2): un sujeto que solo alcanza la que sale A TRAVÉS de la que entra (o la
424
+ // que entra a través de la que sale) no tenía ese deny en el inicial (o no lo tiene en el final): no hay «instante sin
425
+ // deny» que evitar. Sin la exclusión, C4 chocaba con C3 —que ahora cuenta la membresía como concesión— en ciclos
426
+ // espurios (`+U∈h`, `-h⊂g` con el deny en g: U nunca estuvo en g).
427
+ const belowB = union.below(b.view.user, a.key);
428
+ const common = [...union.below(a.view.user, b.key)].filter((s) => belowB.has(s));
429
+ if (common.length === 0)
430
+ return false;
431
+ return [...a.denies].some((x) => b.denies.has(x) && common.some((s) => allowsOfSubject(s).has(x)));
432
+ };
433
+ // Los pares que pueden tener precedencia: los de un MISMO sujeto holder, y los de cada arista anidada con todas las
434
+ // que tienen un sujeto por debajo de ella o un grupo en su camino.
435
+ const succ = ops.map(() => []);
436
+ const link = (i, j) => {
437
+ if (before(ops[i], ops[j]))
438
+ succ[i].push(j);
439
+ };
440
+ const byHolder = new Map();
441
+ const nested = [];
442
+ ops.forEach((o, i) => {
443
+ if (o.tail !== null)
444
+ nested.push(i);
445
+ else {
446
+ let list = byHolder.get(o.view.user);
447
+ if (!list)
448
+ byHolder.set(o.view.user, (list = []));
449
+ list.push(i);
450
+ }
451
+ });
452
+ for (const list of byHolder.values())
453
+ for (const i of list)
454
+ for (const j of list)
455
+ if (i !== j)
456
+ link(i, j);
457
+ for (const i of nested) {
458
+ for (let j = 0; j < ops.length; j++) {
459
+ if (i === j || (ops[j].tail !== null && j < i))
460
+ continue;
461
+ link(i, j);
462
+ link(j, i);
463
+ }
464
+ }
465
+ const order = orderByPrecedence(ops, succ);
466
+ return {
467
+ steps: order.order.map((i) => ({ item: ops[i].item, op: ops[i].emit })),
468
+ residue: order.cyclic.map((i) => ops[i].item),
469
+ };
470
+ }
471
+ /** Prioridad entre disponibles: escrituras antes que borrados; escrituras de lejos primero, borrados de cerca; (objeto, sujeto). */
472
+ function priority(a, b) {
473
+ if (a.op !== b.op)
474
+ return a.op === 'write' ? -1 : 1;
475
+ if (a.depth !== b.depth)
476
+ return a.op === 'write' ? b.depth - a.depth : a.depth - b.depth;
477
+ if (a.view.object !== b.view.object)
478
+ return a.view.object < b.view.object ? -1 : 1;
479
+ if (a.view.user !== b.view.user)
480
+ return a.view.user < b.view.user ? -1 : 1;
481
+ return 0;
482
+ }
483
+ /**
484
+ * Orden topológico por componentes fuertemente conexas (Tarjan, iterativo): una componente sale cuando no le queda
485
+ * ninguna predecesora; entre las disponibles, la de mayor prioridad. Dentro de una componente con CICLO, Kahn sobre sus
486
+ * aristas internas y, cuando se atasca, la de mayor prioridad (`cyclic` las devuelve todas: el residuo).
487
+ */
488
+ function orderByPrecedence(ops, succ) {
489
+ const n = ops.length;
490
+ const index = new Array(n).fill(-1);
491
+ const low = new Array(n).fill(0);
492
+ const onStack = new Array(n).fill(false);
493
+ const component = new Array(n).fill(-1);
494
+ const stack = [];
495
+ const members = [];
496
+ let counter = 0;
497
+ for (let root = 0; root < n; root++) {
498
+ if (index[root] !== -1)
499
+ continue;
500
+ const work = [{ v: root, next: 0 }];
501
+ index[root] = low[root] = counter++;
502
+ stack.push(root);
503
+ onStack[root] = true;
504
+ while (work.length > 0) {
505
+ const frame = work[work.length - 1];
506
+ const edges = succ[frame.v];
507
+ if (frame.next < edges.length) {
508
+ const w = edges[frame.next++];
509
+ if (index[w] === -1) {
510
+ index[w] = low[w] = counter++;
511
+ stack.push(w);
512
+ onStack[w] = true;
513
+ work.push({ v: w, next: 0 });
514
+ }
515
+ else if (onStack[w])
516
+ low[frame.v] = Math.min(low[frame.v], index[w]);
517
+ continue;
518
+ }
519
+ work.pop();
520
+ if (work.length > 0)
521
+ low[work[work.length - 1].v] = Math.min(low[work[work.length - 1].v], low[frame.v]);
522
+ if (low[frame.v] === index[frame.v]) {
523
+ const list = [];
524
+ let w;
525
+ do {
526
+ w = stack.pop();
527
+ onStack[w] = false;
528
+ component[w] = members.length;
529
+ list.push(w);
530
+ } while (w !== frame.v);
531
+ members.push(list);
532
+ }
533
+ }
534
+ }
535
+ const pending = new Array(members.length).fill(0);
536
+ for (let v = 0; v < n; v++)
537
+ for (const w of succ[v])
538
+ if (component[v] !== component[w])
539
+ pending[component[w]]++;
540
+ const best = members.map((list) => list.reduce((a, b) => (priority(ops[a], ops[b]) <= 0 ? a : b)));
541
+ const ready = members.map((_, c) => c).filter((c) => pending[c] === 0);
542
+ const order = [];
543
+ const cyclic = [];
544
+ while (ready.length > 0) {
545
+ let pick = 0;
546
+ for (let k = 1; k < ready.length; k++)
547
+ if (priority(ops[best[ready[k]]], ops[best[ready[pick]]]) < 0)
548
+ pick = k;
549
+ const c = ready.splice(pick, 1)[0];
550
+ const list = members[c];
551
+ if (list.length === 1)
552
+ order.push(list[0]);
553
+ else {
554
+ cyclic.push(...list);
555
+ const inside = new Set(list);
556
+ const done = new Set();
557
+ const preds = new Map(list.map((v) => [v, 0]));
558
+ for (const v of list)
559
+ for (const w of succ[v])
560
+ if (inside.has(w))
561
+ preds.set(w, preds.get(w) + 1);
562
+ while (done.size < list.length) {
563
+ const left = list.filter((v) => !done.has(v));
564
+ const free = left.filter((v) => preds.get(v) === 0);
565
+ const pool = free.length > 0 ? free : left;
566
+ const v = pool.reduce((a, b) => (priority(ops[a], ops[b]) <= 0 ? a : b));
567
+ done.add(v);
568
+ order.push(v);
569
+ for (const w of succ[v])
570
+ if (inside.has(w) && !done.has(w))
571
+ preds.set(w, preds.get(w) - 1);
572
+ }
573
+ }
574
+ for (const v of list)
575
+ for (const w of succ[v])
576
+ if (component[w] !== c && --pending[component[w]] === 0)
577
+ ready.push(component[w]);
578
+ }
579
+ return { order, cyclic };
580
+ }
581
+ /**
582
+ * **El orden de ESCRITURA** de un conjunto que solo ENTRA (atajo de `relationTransitionOrder` sin borrados): `context` =
583
+ * las tuplas que existen y no se escriben.
584
+ */
585
+ export function relationWriteOrder(items, view, context = []) {
586
+ return relationTransitionOrder({ writes: items, context }, view).steps.map((s) => s.item);
587
+ }
588
+ /** **El orden de BORRADO** de un conjunto que solo SALE (las purgas de `openfga`): allows → membresías → denies. */
589
+ export function relationDeleteOrder(items, view, context = []) {
590
+ return relationTransitionOrder({ deletes: items, context }, view).steps.map((s) => s.item);
591
+ }
592
+ /*
593
+ * Cierre 5 de D2 (decisión del dueño §2026-10-16 (10), opción A): aquí vivía `relationWithheldWrites` (cierre 4), la
594
+ * RETENCIÓN de lo que un deny que no viaja (`renewal-unsafe`, `deny-conditional`) dejaría conceder. Se fue entera: en vez
595
+ * de retener tupla a tupla —con su cascada, su conflicto global `carrier-conflict` y el hueco D4-1 que el auditor midió—,
596
+ * la PARTICIÓN afectada recibe CERO escrituras y cero borrados (`src/relations/reconcile.ts`, `planPartition` ⇒
597
+ * `refusal`), y el resto migra. Una partición negada queda exactamente como estaba, que es lo único que se puede
598
+ * certificar sin una escritura atómica de la renovación (openfga/openfga#2151).
599
+ */
@@ -1,8 +1,33 @@
1
1
  import type { RelationsConfig, RelationsConfigSpec } from './relations/define_relations_config.js';
2
+ import type { DenyReachFinding } from './relations/deny_reach.js';
2
3
  export interface RelationsConfigStoreOptions {
3
4
  connection?: string;
4
5
  timeoutMs?: number;
5
6
  }
7
+ /**
8
+ * Opciones de `saveRelationsConfig`: las del store más las DOS salidas con nombre de la guarda del alcance de los denies
9
+ * (8), separadas a propósito (A3 del auditor del panel: una salida que se usa como paso rutinario de adopción y no caduca es
10
+ * un interruptor de apagado).
11
+ */
12
+ export interface SaveRelationsConfigOptions extends RelationsConfigStoreOptions {
13
+ /**
14
+ * **La adopción de la línea base** (firma (8 · línea base)): levanta SOLO el motivo `no-baseline`, y SOLO cuando no hay
15
+ * línea base utilizable (sin fila, o una fila que la lectura TODO O NADA no puede tipar); con una línea base utilizable es
16
+ * INERTE (no sirve para estrechar después). Sigue juzgando los huérfanos. Solo `true` la levanta; un valor que no es
17
+ * boolean es 500 `E_AUTHZ_CONFIG` (L5). Úsala UNA vez, con la spec que la flota sirve HOY, en una release que no cambie
18
+ * `relations.config` (adoptar en una release que estrecha certifica el estrechamiento: A4).
19
+ */
20
+ establishBaseline?: boolean;
21
+ /**
22
+ * **La salida forzada ACOTADA** (firma (8 · salida forzada)): la lista de `tipo#relación` cuyos hallazgos `shrunk`/`orphan`
23
+ * se guardan aunque encojan el alcance de sus denies vivos. Exime SOLO esos: un hallazgo no nombrado se niega igual (un
24
+ * encogimiento nuevo en otra relación no viaja en el mismo guardado forzado). No exime `no-baseline` (esa es
25
+ * `establishBaseline`). No hay `true` global: cualquier forma que no sea un array de `tipo#relación` es 500
26
+ * `E_AUTHZ_CONFIG` (L5). Y un nombre que en ESE guardado no exime ningún `shrunk`/`orphan` también es 500
27
+ * `E_AUTHZ_CONFIG` sin escribir (cierre 1 del sublote (8)): la lista se retira tras el guardado que la necesita.
28
+ */
29
+ allowDenyReachShrink?: readonly string[];
30
+ }
6
31
  /**
7
32
  * **Guarda (persiste) la config de relaciones** bajo el gate de versión: la
8
33
  * valida (⚪4/F-05 de config), la serializa a JSON y hace UPSERT de la fila
@@ -10,7 +35,38 @@ export interface RelationsConfigStoreOptions {
10
35
  * última sentencia (invariante 14): un sync en otro proceso la ve en la
11
36
  * siguiente pregunta—. Devuelve la config congelada.
12
37
  */
13
- export declare function saveRelationsConfig(spec: RelationsConfigSpec, options?: RelationsConfigStoreOptions): Promise<RelationsConfig>;
38
+ export declare function saveRelationsConfig(spec: RelationsConfigSpec, options?: SaveRelationsConfigOptions): Promise<RelationsConfig>;
39
+ /** Lo que devuelve `checkRelationsConfigDenyReach`. */
40
+ export interface RelationsConfigDenyReachCheck {
41
+ /** `'persisted'`: hay línea base utilizable; `'none'`: no la hay (sin fila, o una fila que no se puede tipar). */
42
+ baseline: 'persisted' | 'none';
43
+ /**
44
+ * Persistida → servida: `shrunk`, `orphan` (también sin línea base) y, con `baseline: 'none'` y denies vivos,
45
+ * `no-baseline` — lo mismo que juzgaría la guarda al guardar la config servida, sin salidas forzadas.
46
+ */
47
+ findings: DenyReachFinding[];
48
+ /** Servida → persistida: `stale` — la línea base es más estrecha que lo servido en grupos con denies vivos (A1/S-B). */
49
+ staleBaseline: DenyReachFinding[];
50
+ /** `true` sii no hay ningún hallazgo en ninguna dirección (`no-baseline` cuenta como hallazgo). */
51
+ ok: boolean;
52
+ /** Una línea por hallazgo, con el prefijo estable de su rama (`denyReachLine`). */
53
+ lines: string[];
54
+ }
55
+ /**
56
+ * **El detector de UN solo driver** (firma (8 · detector de un driver), R4 del analista del panel): en un despliegue
57
+ * solo-`database` el detector de `authz:relations:reconcile --dry-run` no corre (necesita DOS drivers de relaciones:
58
+ * origen = destino es 500). Esta función compara la config que SIRVE el proceso (`served`) con la línea base persistida
59
+ * sobre los denies vivos de `authz_relations`, en las DOS direcciones:
60
+ * - persistida → servida: lo que la guarda negaría (`shrunk`, `orphan`, `no-baseline`);
61
+ * - servida → persistida: la línea base RANCIA (`stale`), que la guarda no puede ver.
62
+ *
63
+ * Solo LEE (la fila y un `GROUP BY` de `authz_relations`, por `options.connection` o la primaria), sin transacción y sin el
64
+ * cerrojo del catálogo, con el MISMO punto de paso que la guarda (`baselineOf` + `liveDenyGroups` + `denyReachCheck`).
65
+ * **Receta**: en CI, DESPUÉS del paso que guarda la config y ANTES de servir, y en un cron; falla el job si `ok` es `false`.
66
+ * **Nunca** como fallo duro en `ready`: durante un despliegue un pod viejo que se reinicia sirve la config estrecha contra la
67
+ * fila nueva y entraría en un bucle de reinicios.
68
+ */
69
+ export declare function checkRelationsConfigDenyReach(served: Pick<RelationsConfig, 'objectTypes'>, options?: RelationsConfigStoreOptions): Promise<RelationsConfigDenyReachCheck>;
14
70
  /** Lee el spec persistido (o `null` si no hay relaciones declaradas — opt-in). */
15
71
  export declare function readRelationsConfigSpec(options?: RelationsConfigStoreOptions): Promise<RelationsConfigSpec | null>;
16
72
  /**
@@ -19,4 +75,3 @@ export declare function readRelationsConfigSpec(options?: RelationsConfigStoreOp
19
75
  * relación de la base, así que el modelo que republique los lleva.
20
76
  */
21
77
  export declare function readRelationsConfig(options?: RelationsConfigStoreOptions): Promise<RelationsConfig | null>;
22
- //# sourceMappingURL=relations_config_store.d.ts.map