@jantstack/adonis-authz 2.5.0-alpha.3 → 2.5.0-rc.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 (277) hide show
  1. package/README.md +437 -48
  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 +17 -3
  5. package/build/commands/authz_catalog_prune_orphans.js +28 -6
  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 +0 -1
  13. package/build/commands/authz_relations_reconcile.js +56 -8
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +54 -6
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +16 -3
  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 +10 -1
  23. package/build/commands/openfga_provision.js +43 -3
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +9 -7
  27. package/build/index.js +10 -3
  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 +27 -2
  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 +60 -3
  43. package/build/src/drivers/database_driver.js +160 -25
  44. package/build/src/drivers/database_relations_driver.d.ts +11 -5
  45. package/build/src/drivers/database_relations_driver.js +115 -33
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +882 -199
  48. package/build/src/drivers/openfga_facts.d.ts +91 -1
  49. package/build/src/drivers/openfga_facts.js +277 -3
  50. package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
  51. package/build/src/drivers/openfga_relations_driver.js +422 -228
  52. package/build/src/errors.d.ts +160 -1
  53. package/build/src/errors.js +161 -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 +9 -2
  65. package/build/src/identity.js +33 -1
  66. package/build/src/manager.d.ts +24 -1
  67. package/build/src/manager.js +381 -85
  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 +2 -3
  81. package/build/src/openfga.js +4 -2
  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 +0 -1
  87. package/build/src/relations/check_many.js +0 -1
  88. package/build/src/relations/define_relations_config.d.ts +25 -3
  89. package/build/src/relations/define_relations_config.js +8 -3
  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 +14 -7
  95. package/build/src/relations/manager.js +60 -15
  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 +58 -4
  99. package/build/src/relations/reconcile.js +266 -12
  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 +5 -4
  103. package/build/src/relations/tuple_order.js +5 -4
  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 +36 -3
  109. package/build/src/scope_outbox.js +78 -18
  110. package/build/src/shared/backend_guard.d.ts +52 -1
  111. package/build/src/shared/backend_guard.js +164 -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 +26 -3
  119. package/build/src/testing/contract.js +258 -20
  120. package/build/src/testing/main.d.ts +0 -1
  121. package/build/src/testing/main.js +0 -1
  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 +7 -1
  125. package/build/src/testing/relations_contract.js +174 -7
  126. package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
  127. package/build/src/testing/relations_reconcile_contract.js +0 -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 +138 -14
  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/tuple_order.d.ts.map +0 -1
  239. package/build/src/relations/tuple_order.js.map +0 -1
  240. package/build/src/relations_config_store.d.ts.map +0 -1
  241. package/build/src/relations_config_store.js.map +0 -1
  242. package/build/src/relations_reconcile.d.ts.map +0 -1
  243. package/build/src/relations_reconcile.js.map +0 -1
  244. package/build/src/scope_outbox.d.ts.map +0 -1
  245. package/build/src/scope_outbox.js.map +0 -1
  246. package/build/src/shared/backend_guard.d.ts.map +0 -1
  247. package/build/src/shared/backend_guard.js.map +0 -1
  248. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  249. package/build/src/shared/sql_expiry.js.map +0 -1
  250. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  251. package/build/src/shared/transaction_guard.js.map +0 -1
  252. package/build/src/sql_descendants.d.ts.map +0 -1
  253. package/build/src/sql_descendants.js.map +0 -1
  254. package/build/src/testing/contract.d.ts.map +0 -1
  255. package/build/src/testing/contract.js.map +0 -1
  256. package/build/src/testing/main.d.ts.map +0 -1
  257. package/build/src/testing/main.js.map +0 -1
  258. package/build/src/testing/migration_contract.d.ts.map +0 -1
  259. package/build/src/testing/migration_contract.js.map +0 -1
  260. package/build/src/testing/relations_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_contract.js.map +0 -1
  262. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  263. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  264. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  265. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  266. package/build/src/testing/scope_tree.d.ts.map +0 -1
  267. package/build/src/testing/scope_tree.js.map +0 -1
  268. package/build/src/testing/test_clock.d.ts.map +0 -1
  269. package/build/src/testing/test_clock.js.map +0 -1
  270. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  271. package/build/src/traits/authz_scopes.js.map +0 -1
  272. package/build/src/traits/has_uuid.d.ts.map +0 -1
  273. package/build/src/traits/has_uuid.js.map +0 -1
  274. package/build/src/types.d.ts.map +0 -1
  275. package/build/src/types.js.map +0 -1
  276. package/build/stubs/main.d.ts.map +0 -1
  277. package/build/stubs/main.js.map +0 -1
@@ -25,4 +25,3 @@ export default class AuthzCatalogDiff extends BaseCommand {
25
25
  failOnShadows: boolean;
26
26
  run(): Promise<void>;
27
27
  }
28
- //# sourceMappingURL=authz_catalog_diff.d.ts.map
@@ -63,4 +63,3 @@ __decorate([
63
63
  }),
64
64
  __metadata("design:type", Boolean)
65
65
  ], AuthzCatalogDiff.prototype, "failOnShadows", void 0);
66
- //# sourceMappingURL=authz_catalog_diff.js.map
@@ -34,6 +34,11 @@ export declare function orphanLines(orphans: OrphanRow[], purged: Set<string>, s
34
34
  level: 'log' | 'warning';
35
35
  message: string;
36
36
  }>;
37
+ /**
38
+ * La letra de los disparadores del seguro de purga masiva (3b-0b · AA2 + rotación 2 · R2-02), en el orden en que
39
+ * el manager los reporta. Pura, para que el caso la fije.
40
+ */
41
+ export declare function massPurgeLetter(reasons: ReadonlyArray<string>): string;
37
42
  /**
38
43
  * Lista —y con `--force` borra— los roles LOCALES cuyo owner el árbol del
39
44
  * consumidor YA NO conoce (3b-0 · Z2).
@@ -56,6 +61,7 @@ export declare function orphanLines(orphans: OrphanRow[], purged: Set<string>, s
56
61
  * node ace authz:catalog:prune-orphans # --dry-run: solo lista
57
62
  * node ace authz:catalog:prune-orphans --force # purga de verdad
58
63
  * node ace authz:catalog:prune-orphans --force --allow-mass-purge
64
+ * node ace authz:catalog:prune-orphans --max-local-roles=50000 # catálogo local de más de 10 000 roles
59
65
  *
60
66
  * Es una operación de PLATAFORMA, no una escritura que dispare un tenant: no
61
67
  * lleva actor ni mide rangos. Hasta 3G esta limpieza la arrastraba
@@ -63,8 +69,10 @@ export declare function orphanLines(orphans: OrphanRow[], purged: Set<string>, s
63
69
  * resuelve—, y de ahí salieron tres de las cuatro regresiones de la Fase 3.
64
70
  *
65
71
  * `--allow-mass-purge` (3b-0b · AA2) desbloquea la cota que rechaza una
66
- * pasada con la firma de un resolutor ciego (todos los owners huérfanos, o
67
- * más del 50 % de los roles locales): es una decisión humana, no un default.
72
+ * pasada con la firma de un resolutor ciego (todos los owners huérfanos,
73
+ * más del 50 % de los roles locales, o —rotación 2 · R2-02— huérfanos de más
74
+ * de un owner con alguno que todavía concede): es una decisión humana, no un
75
+ * default. `--max-local-roles` (R2-04) sube la cota de la lectura.
68
76
  */
69
77
  export default class AuthzCatalogPruneOrphans extends BaseCommand {
70
78
  static commandName: string;
@@ -72,7 +80,13 @@ export default class AuthzCatalogPruneOrphans extends BaseCommand {
72
80
  static options: CommandOptions;
73
81
  force: boolean | undefined;
74
82
  allowMassPurge: boolean | undefined;
83
+ /**
84
+ * Rotación 2 · R2-04: la cota de la lectura del catálogo local (default 10 000). Hasta este cierre el mensaje
85
+ * de `E_AUTHZ_TOO_MANY_LOCAL_ROLES` pedía «subir la cota» y no había mando: un tenant que pasara de 10 000 roles
86
+ * locales dejaba este comando muerto para todos. Un valor que no es un entero ≥ 1 lo rechaza el manager (500
87
+ * `E_AUTHZ_CONFIG`) antes de leer nada.
88
+ */
89
+ maxLocalRoles: number | undefined;
75
90
  run(): Promise<void>;
76
91
  }
77
92
  export {};
78
- //# sourceMappingURL=authz_catalog_prune_orphans.d.ts.map
@@ -54,6 +54,18 @@ export function orphanLines(orphans, purged, skipped) {
54
54
  }
55
55
  return lines;
56
56
  }
57
+ /**
58
+ * La letra de los disparadores del seguro de purga masiva (3b-0b · AA2 + rotación 2 · R2-02), en el orden en que
59
+ * el manager los reporta. Pura, para que el caso la fije.
60
+ */
61
+ export function massPurgeLetter(reasons) {
62
+ const words = {
63
+ 'all-owners': 'todos los owners salen huérfanos',
64
+ 'proportion': 'más del 50 % de los roles locales',
65
+ 'granting-across-owners': 'huérfanos de más de un owner y alguno TODAVÍA concede o no se sabe',
66
+ };
67
+ return reasons.map((reason) => words[reason] ?? reason).join('; ') || 'sin motivo reportado';
68
+ }
57
69
  /**
58
70
  * Lista —y con `--force` borra— los roles LOCALES cuyo owner el árbol del
59
71
  * consumidor YA NO conoce (3b-0 · Z2).
@@ -76,6 +88,7 @@ export function orphanLines(orphans, purged, skipped) {
76
88
  * node ace authz:catalog:prune-orphans # --dry-run: solo lista
77
89
  * node ace authz:catalog:prune-orphans --force # purga de verdad
78
90
  * node ace authz:catalog:prune-orphans --force --allow-mass-purge
91
+ * node ace authz:catalog:prune-orphans --max-local-roles=50000 # catálogo local de más de 10 000 roles
79
92
  *
80
93
  * Es una operación de PLATAFORMA, no una escritura que dispare un tenant: no
81
94
  * lleva actor ni mide rangos. Hasta 3G esta limpieza la arrastraba
@@ -83,8 +96,10 @@ export function orphanLines(orphans, purged, skipped) {
83
96
  * resuelve—, y de ahí salieron tres de las cuatro regresiones de la Fase 3.
84
97
  *
85
98
  * `--allow-mass-purge` (3b-0b · AA2) desbloquea la cota que rechaza una
86
- * pasada con la firma de un resolutor ciego (todos los owners huérfanos, o
87
- * más del 50 % de los roles locales): es una decisión humana, no un default.
99
+ * pasada con la firma de un resolutor ciego (todos los owners huérfanos,
100
+ * más del 50 % de los roles locales, o —rotación 2 · R2-02— huérfanos de más
101
+ * de un owner con alguno que todavía concede): es una decisión humana, no un
102
+ * default. `--max-local-roles` (R2-04) sube la cota de la lectura.
88
103
  */
89
104
  export default class AuthzCatalogPruneOrphans extends BaseCommand {
90
105
  static commandName = 'authz:catalog:prune-orphans';
@@ -94,9 +109,10 @@ export default class AuthzCatalogPruneOrphans extends BaseCommand {
94
109
  };
95
110
  async run() {
96
111
  const { default: authorization } = await import('../services/main.js');
97
- const { orphans, purged, skipped, massPurge, dryRun } = await authorization.pruneOrphanRoles({
112
+ const { orphans, purged, skipped, massPurge, massPurgeReasons, dryRun } = await authorization.pruneOrphanRoles({
98
113
  force: this.force === true,
99
114
  allowMassPurge: this.allowMassPurge === true,
115
+ ...(this.maxLocalRoles !== undefined ? { maxLocalRoles: this.maxLocalRoles } : {}),
100
116
  });
101
117
  // Los que todavía conceden —y los que NADIE puede demostrar que no
102
118
  // concedan— van aparte, con aviso: purgarlos es revocar permisos vivos,
@@ -110,7 +126,8 @@ export default class AuthzCatalogPruneOrphans extends BaseCommand {
110
126
  }
111
127
  if (dryRun) {
112
128
  if (massPurge) {
113
- this.logger.warning(`${orphans.length} de los roles locales quedarían huérfanos: eso es la firma de un 'scopes.resolveChain' ciego. ` +
129
+ this.logger.warning(`${orphans.length} de los roles locales quedarían huérfanos: eso es la firma de un 'scopes.resolveChain' ciego ` +
130
+ `(${massPurgeLetter(massPurgeReasons ?? [])}). ` +
114
131
  `Con --force esto se rechaza (E_AUTHZ_MASS_PURGE_REFUSED); comprueba el resolutor antes de añadir --allow-mass-purge.`);
115
132
  }
116
133
  this.logger.warning(`${orphans.length} rol(es) local(es) con el owner fuera del árbol. No se ha borrado nada: repite con --force.`);
@@ -128,8 +145,13 @@ __decorate([
128
145
  ], AuthzCatalogPruneOrphans.prototype, "force", void 0);
129
146
  __decorate([
130
147
  flags.boolean({
131
- description: 'Allow a pass that would purge every owner (or more than half the local roles) — check your resolveChain first',
148
+ description: 'Allow a pass that would purge every owner, more than half the local roles, or orphans of several owners that still grant (or cannot be counted) — check your resolveChain first',
132
149
  }),
133
150
  __metadata("design:type", Object)
134
151
  ], AuthzCatalogPruneOrphans.prototype, "allowMassPurge", void 0);
135
- //# sourceMappingURL=authz_catalog_prune_orphans.js.map
152
+ __decorate([
153
+ flags.number({
154
+ description: 'Cap on the local roles this pass reads (default 10000); above it, 500 E_AUTHZ_TOO_MANY_LOCAL_ROLES, never a partial list',
155
+ }),
156
+ __metadata("design:type", Object)
157
+ ], AuthzCatalogPruneOrphans.prototype, "maxLocalRoles", void 0);
@@ -34,4 +34,3 @@ export default class AuthzCatalogSync extends BaseCommand {
34
34
  keepLinks: boolean | undefined;
35
35
  run(): Promise<void>;
36
36
  }
37
- //# sourceMappingURL=authz_catalog_sync.d.ts.map
@@ -78,4 +78,3 @@ __decorate([
78
78
  flags.boolean({ description: 'Do not prune role→permission links missing from the spec' }),
79
79
  __metadata("design:type", Object)
80
80
  ], AuthzCatalogSync.prototype, "keepLinks", void 0);
81
- //# sourceMappingURL=authz_catalog_sync.js.map
@@ -41,4 +41,3 @@ export default class AuthzFreeze extends BaseCommand {
41
41
  leaseMs: number | undefined;
42
42
  run(): Promise<void>;
43
43
  }
44
- //# sourceMappingURL=authz_freeze.d.ts.map
@@ -91,4 +91,3 @@ __decorate([
91
91
  }),
92
92
  __metadata("design:type", Object)
93
93
  ], AuthzFreeze.prototype, "leaseMs", void 0);
94
- //# sourceMappingURL=authz_freeze.js.map
@@ -108,4 +108,3 @@ export default class AuthzReconcile extends BaseCommand {
108
108
  maxTuples: number | undefined;
109
109
  run(): Promise<void>;
110
110
  }
111
- //# sourceMappingURL=authz_reconcile.d.ts.map
@@ -453,4 +453,3 @@ __decorate([
453
453
  }),
454
454
  __metadata("design:type", Object)
455
455
  ], AuthzReconcile.prototype, "maxTuples", void 0);
456
- //# sourceMappingURL=authz_reconcile.js.map
@@ -92,4 +92,3 @@ export default class AuthzRelationsReconcile extends BaseCommand {
92
92
  partitionUuid: string | undefined;
93
93
  run(): Promise<void>;
94
94
  }
95
- //# sourceMappingURL=authz_relations_reconcile.d.ts.map
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { BaseCommand, flags } from '@adonisjs/core/ace';
11
11
  import { RELATION_USERSET_RESIDUE_DELETE } from '../src/relations/define_relations_config.js';
12
+ import { denyReachLine } from '../src/relations/deny_reach.js';
12
13
  import { RelationConfigError } from '../src/errors.js';
13
14
  // Plan 2.5 · lote B · B7: la vía pública vive en su subpath (`src/relations_reconcile.ts`,
14
15
  // `@jantstack/adonis-authz/relations/reconcile`); este módulo es el COMANDO y la importa de allí.
@@ -84,6 +85,34 @@ export function relationsReconcileLines(report) {
84
85
  `el deny concedería lo que el origen niega. Receta: ${refusal.recipe}`,
85
86
  });
86
87
  }
88
+ // Lote RC · sublote (8): el DETECTOR del alcance de los denies (solo si la pasada lo pidió: `--dry-run`). Una línea por
89
+ // hallazgo con su prefijo ESTABLE (punto 7) y su receta: «deny con alcance ENCOGIDO» / «deny HUÉRFANO» (persistida →
90
+ // servida) y «línea base RANCIA» (servida → persistida, firma (8 · rancia)) son deriva (exit ≠ 0). Sin línea base y con
91
+ // denies vivos sin juzgar, la línea «alcance de denies: SIN línea base y N deny(s) vivos sin juzgar» y exit 0 (firma
92
+ // (8 · `--dry-run`), g2: el runbook no persiste la config y no cambia). Sin nada de eso, UNA línea que dice qué se comparó.
93
+ if (report.denyReach !== undefined) {
94
+ const { baseline, findings, stale, unjudged } = report.denyReach;
95
+ for (const finding of [...findings, ...stale]) {
96
+ lines.push({ level: 'error', message: `${denyReachLine(finding)} (lado: ${finding.sides.join(', ')})` });
97
+ }
98
+ if (baseline === 'none' && unjudged > 0) {
99
+ lines.push({
100
+ level: 'warning',
101
+ message: `alcance de denies: SIN línea base y ${unjudged} deny(s) vivos sin juzgar (no hay config persistida en authz_relations_config): ` +
102
+ `solo se buscaron huérfanos, y un estrechamiento de su alcance NO se ve sin línea base. Si usas deny, persistir la config es ` +
103
+ `paso obligatorio de release: adóptala una vez con saveRelationsConfig(spec, { establishBaseline: true }) y vigila con ` +
104
+ `checkRelationsConfigDenyReach (el detector de un solo driver).`,
105
+ });
106
+ }
107
+ else if (findings.length === 0 && stale.length === 0) {
108
+ lines.push({
109
+ level: 'log',
110
+ message: baseline === 'persisted'
111
+ ? 'alcance de denies: ningún deny vivo de la partición encogió ni quedó huérfano entre la config persistida (línea base) y la que sirve este proceso, y la línea base no es más estrecha que lo servido.'
112
+ : 'alcance de denies: SIN línea base (no hay config persistida en authz_relations_config) y ningún deny vivo en la partición que juzgar.',
113
+ });
114
+ }
115
+ }
87
116
  // R-15: la caducada del origen es la pérdida DECLARADA de la migración (como
88
117
  // `expired` en roles, 3b-8 · B2): se dice, no es deriva.
89
118
  if (report.skipped.expired > 0) {
@@ -137,7 +166,9 @@ export function relationsReconcileLines(report) {
137
166
  level: 'error',
138
167
  message: detail.reason === 'userset-unsupported'
139
168
  ? `sin migrar ${what}. Es un residuo pre-2.5 (B3) del store de ORIGEN: el destino no lo admite por ninguna puerta y ningún --prune lo toca. Bórrala a mano (los tres motores): ${RELATION_USERSET_RESIDUE_DELETE} y repite la pasada.`
140
- : `sin migrar ${what}. Es un dato del store de ORIGEN, no una pregunta: corrígela o bórrala a mano (SQL/SDK) y repite la pasada.`,
169
+ : detail.reason === 'invalid-condition'
170
+ ? `sin migrar ${what}. Es una tupla cuya condición el paquete no escribe ni sabe leer (L11): no se migra como permanente ni como caducada. Revísala y corrígela o bórrala a mano (SDK/CLI de OpenFGA) y repite la pasada.`
171
+ : `sin migrar ${what}. Es un dato del store de ORIGEN, no una pregunta: corrígela o bórrala a mano (SQL/SDK) y repite la pasada.`,
141
172
  });
142
173
  continue;
143
174
  }
@@ -164,11 +195,13 @@ export function relationsReconcileLines(report) {
164
195
  level: 'error',
165
196
  message: detail.reason === 'userset-unsupported'
166
197
  ? `sobra ${what}. Es un residuo pre-2.5 (B3) del DESTINO: --prune ${verdict} lo borra${detail.pruned === false ? '' : 'ría'} (nunca un DELETE de plataforma sobre una fila que la API no admite). Bórrala a mano (los tres motores): ${RELATION_USERSET_RESIDUE_DELETE} y repite la pasada.`
167
- : detail.reason === 'deny-conditional'
168
- ? `sobra ${what}. Es un deny CONDICIONAL del DESTINO (con caducidad, escrito fuera del paquete) que el driver destino no puede rehacer permanente (openfga no reemplaza la misma clave en un Write, M5): niega solo mientras viva (si ya venció, no niega nada) y su partición queda NEGADA (línea «partición NEGADA»: cero escrituras en ella).`
169
- : detail.reason === 'undeclared'
170
- ? `sobra ${what}. Es una fila del DESTINO cuya relación (o tipo) el destino YA NO declara (F-05): --prune ${verdict} la borra${detail.pruned === false ? '' : 'ría'} (unrelate la rechaza por F-05 y la pasada no insiste: nunca un DELETE de plataforma). Sigue ahí: retírala a mano (SQL/SDK) o vuelve a declarar la relación en el destino (y re-guarda su config persistida) y repite la pasada.`
171
- : `sobra ${what}. Es un dato del DESTINO fuera de la gramática: --prune ${verdict} la borra${detail.pruned === false ? '' : 'ría'} (ningún driver del paquete la admite en unrelate, R-16, y la pasada no se la pide a nadie: nunca un DELETE de plataforma). Sigue ahí: corrígela o bórrala a mano (SQL/SDK) y repite la pasada.`,
198
+ : detail.reason === 'invalid-condition'
199
+ ? `sobra ${what}. Es una tupla del DESTINO con una condición que el paquete no escribe ni sabe leer (L11): presente e intocable —--prune ${verdict} la borra${detail.pruned === false ? '' : 'ría'}, y su clave no se reescribe desde el origen—. Revísala y bórrala a mano (SDK/CLI de OpenFGA) y repite la pasada.`
200
+ : detail.reason === 'deny-conditional'
201
+ ? `sobra ${what}. Es un deny CONDICIONAL del DESTINO (con caducidad, escrito fuera del paquete) que el driver destino no puede rehacer permanente (openfga no reemplaza la misma clave en un Write, M5): niega solo mientras viva (si ya venció, no niega nada) y su partición queda NEGADA (línea «partición NEGADA»: cero escrituras en ella).`
202
+ : detail.reason === 'undeclared'
203
+ ? `sobra ${what}. Es una fila del DESTINO cuya relación (o tipo) el destino YA NO declara (F-05): --prune ${verdict} la borra${detail.pruned === false ? '' : 'ría'} (unrelate la rechaza por F-05 y la pasada no insiste: nunca un DELETE de plataforma). Sigue ahí: retírala a mano (SQL/SDK) o vuelve a declarar la relación en el destino (y re-guarda su config persistida) y repite la pasada.`
204
+ : `sobra ${what}. Es un dato del DESTINO fuera de la gramática: --prune ${verdict} la borra${detail.pruned === false ? '' : 'ría'} (ningún driver del paquete la admite en unrelate, R-16, y la pasada no se la pide a nadie: nunca un DELETE de plataforma). Sigue ahí: corrígela o bórrala a mano (SQL/SDK) y repite la pasada.`,
172
205
  });
173
206
  }
174
207
  // Cierre 5 de D2 (decisión del dueño §2026-10-16 (10), opción A): la PARTICIÓN NEGADA — ≥ 1 `renewal-unsafe` o
@@ -182,7 +215,7 @@ export function relationsReconcileLines(report) {
182
215
  const later = evolves.length > 0 ? ` OJO, ese estado EVOLUCIONA: ${evolves.slice(0, 5).join('; ')}${evolves.length > 5 ? '; …' : ''}.` : '';
183
216
  lines.push({
184
217
  level: 'error',
185
- message: `partición NEGADA ${refusal.partition}: ${causes.length} causa(s) que el destino no puede escribir en una escritura — ` +
218
+ message: `partición NEGADA ${refusal.partition}: ${causes.length} causa(s) por las que la pasada no puede escribir en ella sin conceder de más (una ventana entre dos Write, una tupla que el paquete no sabe leer, o una cadena de grupos anidados más larga que el techo del destino) — ` +
186
219
  `${causes.slice(0, 5).join('; ')}${causes.length > 5 ? '; …' : ''}. Cero escrituras y cero borrados en esta partición ` +
187
220
  `(también en la pasada real${report.prune === true ? ' y con --prune' : ''}): su destino queda COMO ESTABA, con la deriva ` +
188
221
  `previa a la vista (por escribir ${refusal.pending.write}, por renovar ${refusal.pending.update}, por borrar ` +
@@ -287,6 +320,8 @@ export function relationsReconcileLines(report) {
287
320
  pendingRenewals === 0 &&
288
321
  (report.partitionsRefused?.length ?? 0) === 0 &&
289
322
  (report.denyRefused?.length ?? 0) === 0 &&
323
+ (report.denyReach?.findings.length ?? 0) === 0 &&
324
+ (report.denyReach?.stale.length ?? 0) === 0 &&
290
325
  !(report.massDelete && !massDeleteLifted) &&
291
326
  !(report.frozen?.lapsed === true) &&
292
327
  (!report.dryRun || cambios === 0);
@@ -461,6 +496,19 @@ export default class AuthzRelationsReconcile extends BaseCommand {
461
496
  else {
462
497
  this.logger.info(`Config del destino: leída de authz_relations_config (${toConfig.objectTypes.length} tipo(s)): la deriva del modelo se vigila.`);
463
498
  }
499
+ // Lote RC · sublote (8): el DETECTOR del alcance de los denies, solo en el verificador (`--dry-run`): línea base = la
500
+ // config persistida (`toConfig`), config nueva = la que SIRVE este proceso (`relations.config`). Sin `relations.config`
501
+ // en el proceso no hay con qué comparar: se dice (punto 7) y la pasada sigue.
502
+ let denyReach;
503
+ if (this.dryRun === true) {
504
+ const current = this.app.config?.get('authorization')?.relations?.config;
505
+ if (current && Array.isArray(current.objectTypes)) {
506
+ denyReach = { baseline: toConfig, current };
507
+ }
508
+ else {
509
+ this.logger.info('alcance de denies: NO se vigila en esta pasada (este proceso no tiene relations.config en config/authorization.ts con el que comparar).');
510
+ }
511
+ }
464
512
  // L-1 · J1: la pasada que ESCRIBE corre bajo la ventana durable del
465
513
  // manager de roles (la misma fila `id = 2`, toda la flota); `--dry-run` no.
466
514
  // Los drivers los construye `runRelationsReconcile`, DESPUÉS del guard.
@@ -475,6 +523,7 @@ export default class AuthzRelationsReconcile extends BaseCommand {
475
523
  toKey: this.to,
476
524
  fromKey,
477
525
  ...(toConfig ? { toConfig } : {}),
526
+ ...(denyReach ? { denyReach } : {}),
478
527
  });
479
528
  }
480
529
  catch (error) {
@@ -583,4 +632,3 @@ __decorate([
583
632
  flags.string({ description: 'Partition (tenant) scope uuid; required with --partition-type' }),
584
633
  __metadata("design:type", Object)
585
634
  ], AuthzRelationsReconcile.prototype, "partitionUuid", void 0);
586
- //# sourceMappingURL=authz_relations_reconcile.js.map
@@ -94,4 +94,3 @@ export default class AuthzScopesRelay extends BaseCommand {
94
94
  static parseOnly(values: string[] | undefined): Array<string | number> | undefined;
95
95
  run(): Promise<void>;
96
96
  }
97
- //# sourceMappingURL=authz_scopes_relay.d.ts.map
@@ -207,7 +207,11 @@ export default class AuthzScopesRelay extends BaseCommand {
207
207
  'Revísala en la cola (el consumidor puede volver a notificar el cambio) o reconcilia.');
208
208
  }
209
209
  if (report.busy) {
210
- this.logger.info('Otra pasada del relay tiene el lease de la cola: esta no ha aplicado nada (el relay es escritor único).');
210
+ // Rotación 2 · R2-01b: la rama sigue saliendo 0 —respuesta del dueño en el cierre 2: la línea y `--max-age` son el
211
+ // detector; el caso fija exit 0 exacto (T-05)—, y la línea dice lo que puede ser.
212
+ this.logger.info('Otra pasada del relay tiene el lease de la cola: esta no ha aplicado nada (el relay es escritor único). ' +
213
+ 'En MySQL, durante 2.5.x, el lease puede tenerlo OTRO despliegue del mismo servidor (el nombre de transición no ' +
214
+ 'lleva la base; se retira en 2.6): si se repite en cada pasada, la cola no avanza — vigílala con --max-age.');
211
215
  if (report.dead.length > 0)
212
216
  this.exitCode = 1;
213
217
  return;
@@ -252,8 +256,23 @@ export default class AuthzScopesRelay extends BaseCommand {
252
256
  // Qué fue de cada entrada, una a una (`entries`): la pasada no es atómica
253
257
  // y un contador no permite retomar nada. Lo aparcado ya se dijo arriba.
254
258
  for (const entry of report.entries) {
255
- if (entry.status === 'applied')
256
- this.logger.log(`aplicado: ${linea(entry)}`);
259
+ if (entry.status === 'applied') {
260
+ // Asentar · cierre 1 (auditor ⚪ 8): la auditoría dice el padre APLICADO, no solo el encolado.
261
+ if (entry.reason === 'superseded') {
262
+ this.logger.warning(`aplicado SIN arista (superseded: el nodo ya no existe y su detached está en la cola): ${linea(entry)}`);
263
+ }
264
+ else if (entry.reason === 'consumer-parent' && entry.appliedParent) {
265
+ // Asentar · cierre 2 (C1-1 (b), firma del dueño 2026-10-21; O-1 del re-ataque): exit ≠ 0. La carrera S1 y una
266
+ // caché rancia con dos movimientos del nodo dentro de su TTL son INDISTINGUIBLES para el relay, y un cron no lee
267
+ // avisos: la carrera hace saltar la alarma una vez; la caché, cada vez que pase.
268
+ this.logger.warning(`aplicado con el padre del CONSUMIDOR ${entry.appliedParent.type}:${entry.appliedParent.uuid ?? ''} ` +
269
+ `(no el encolado; otra entrada del nodo lo respalda): ${linea(entry)} — sale ≠ 0: comprueba que el resolutor ` +
270
+ 'del relay lee lo CONFIRMADO, sin caché ni réplica atrasada (authz:reconcile --dry-run lo contrasta)');
271
+ this.exitCode = 1;
272
+ }
273
+ else
274
+ this.logger.log(`aplicado: ${linea(entry)}`);
275
+ }
257
276
  if (entry.status === 'deferred') {
258
277
  const motivo = entry.reason?.startsWith('parked:')
259
278
  ? `aplazado por APARCADO (${entry.reason}): no se aplica hasta que alguien mire esa entrada`
@@ -267,8 +286,38 @@ export default class AuthzScopesRelay extends BaseCommand {
267
286
  notEvaluatedLines();
268
287
  if (report.failures.length > 0) {
269
288
  for (const item of report.failures) {
270
- this.logger.error(`falló #${item.id} (${item.change.op} ` +
271
- `${item.change.child.type}:${item.change.child.uuid ?? ''}): ${item.error}` +
289
+ const target = `#${item.id} (${item.change.op} ${item.change.child.type}:${item.change.child.uuid ?? ''})`;
290
+ // Cierre-2 de L11 (⚪ C2; punto 7): el fallo de DESPLIEGUE tiene su prefijo ESTABLE —el backend SÍ respondió, lo
291
+ // que no cuadra es el despliegue con la config—; «el backend no respondió» queda para el 503 de verdad. Desde R3
292
+ // la marca la ponen también el store/modelo inexistente, las credenciales (401/403) y el 4xx desconocido.
293
+ if (item.deployment === true) {
294
+ this.logger.error(`fallo de DESPLIEGUE ${target}: ${item.error} — el despliegue no es el que espera la config (modelo fijado, ` +
295
+ 'store, credenciales o un 4xx del servidor): este fallo NO consume intento (corrige el despliegue —' +
296
+ 'openfga:provision publica el modelo de la config— y repite)');
297
+ continue;
298
+ }
299
+ // Cierre de R3 (⚪ R3-A7; punto 7): la carrera y el límite de resolución son transitorios, pero el backend SÍ
300
+ // respondió — cada uno con su prefijo ESTABLE.
301
+ if (item.transient && item.code === 'E_AUTHZ_WRITE_CONFLICT') {
302
+ this.logger.error(`CARRERA en ${target}: ${item.error} — otro escritor (p. ej. un grant concurrente) tocó las mismas tuplas: ` +
303
+ 'este fallo NO consume intento (la pasada siguiente relee y re-aplica)');
304
+ continue;
305
+ }
306
+ if (item.transient && item.code === 'E_AUTHZ_RESOLUTION_LIMIT') {
307
+ this.logger.error(`límite de RESOLUCIÓN en ${target}: ${item.error} — el backend respondió que el grafo excede lo que ` +
308
+ 'resuelve (ver «Limits» en el README): este fallo NO consume intento');
309
+ continue;
310
+ }
311
+ // Lote «antes de RC» · F4 (⚪ RC-2 del auditor de R3; punto 7): un deadline vencido —el timeout del cliente, o un
312
+ // 504 / `deadline_exceeded` de una ESCRITURA, que es indeterminada desde el cierre de R3— no es «el backend no
313
+ // respondió»: con un 504 SÍ respondió (o un proxy), y la escritura pudo aplicarse. Prefijo ESTABLE propio.
314
+ if (item.transient && item.code === 'E_AUTHZ_BACKEND_TIMEOUT') {
315
+ this.logger.error(`PLAZO VENCIDO en ${target}: ${item.error} — venció el deadline (timeout del cliente, o un 504/deadline_exceeded ` +
316
+ 'del servidor o de un proxy): si era una escritura pudo aplicarse o no (INDETERMINADA); la pasada siguiente ' +
317
+ 'relee el store y re-aplica: este fallo NO consume intento');
318
+ continue;
319
+ }
320
+ this.logger.error(`falló ${target}: ${item.error}` +
272
321
  (item.transient ? ' — el backend no respondió: este fallo NO consume intento' : ''));
273
322
  }
274
323
  this.logger.warning('Lo que DEPENDE de un cambio fallido no se adelanta (el orden del árbol importa: aparece como «aplazado»), ' +
@@ -341,4 +390,3 @@ __decorate([
341
390
  }),
342
391
  __metadata("design:type", Object)
343
392
  ], AuthzScopesRelay.prototype, "maxAge", void 0);
344
- //# sourceMappingURL=authz_scopes_relay.js.map
@@ -77,4 +77,3 @@ export default class AuthzScopesRequeue extends BaseCommand {
77
77
  static parseIds(values: string[] | undefined): Array<string | number> | undefined;
78
78
  run(): Promise<void>;
79
79
  }
80
- //# sourceMappingURL=authz_scopes_requeue.d.ts.map
@@ -69,7 +69,8 @@ export function requeueLines(input) {
69
69
  level: 'error',
70
70
  message: 'Otra pasada del relay tiene el lease de la cola: con --apply no se ha reencolado NADA (todo o nada: reencolar y no ' +
71
71
  'aplicar dejaría la entrada con un solo intento, que la siguiente pasada gastaría). Reintenta (--wait-lease-ms espera ' +
72
- 'a que la suelten), o reencola sin --apply y deja que la próxima pasada la aplique.',
72
+ 'a que la suelten), o reencola sin --apply y deja que la próxima pasada la aplique. En MySQL, durante 2.5.x, el ' +
73
+ 'lease puede tenerlo OTRO despliegue del mismo servidor (el nombre de transición no lleva la base; se retira en 2.6).',
73
74
  });
74
75
  return { lines, clean: false };
75
76
  }
@@ -90,7 +91,20 @@ export function requeueLines(input) {
90
91
  const relay = report.relay;
91
92
  let clean = true;
92
93
  for (const entry of relay.entries) {
93
- if (entry.status === 'applied')
94
+ // Asentar · cierre 2 (gemelo del relay, C1-1): el padre APLICADO cuando no es el encolado, con su línea propia. No
95
+ // cambia `clean`: aquí hay un operador delante, y es justo la salida de una aparcada por DRIFT (re-notificar el
96
+ // padre actual y reencolar) la que termina en esta línea.
97
+ if (entry.status === 'applied' && entry.reason === 'consumer-parent' && entry.appliedParent) {
98
+ lines.push({
99
+ level: 'warning',
100
+ message: `aplicada con el padre del CONSUMIDOR ${entry.appliedParent.type}:${entry.appliedParent.uuid ?? ''} ` +
101
+ `(no el encolado; otra entrada del nodo lo respalda): ${linea(entry)}`,
102
+ });
103
+ }
104
+ else if (entry.status === 'applied' && entry.reason === 'superseded') {
105
+ lines.push({ level: 'warning', message: `aplicada SIN arista (superseded: el nodo ya no existe y su detached está en la cola): ${linea(entry)}` });
106
+ }
107
+ else if (entry.status === 'applied')
94
108
  lines.push({ level: 'log', message: `aplicada: ${linea(entry)}` });
95
109
  else if (entry.status === 'deferred') {
96
110
  clean = false;
@@ -227,4 +241,3 @@ __decorate([
227
241
  }),
228
242
  __metadata("design:type", Object)
229
243
  ], AuthzScopesRequeue.prototype, "waitLeaseMs", void 0);
230
- //# sourceMappingURL=authz_scopes_requeue.js.map
@@ -42,4 +42,3 @@ export default class AuthzUnfreeze extends BaseCommand {
42
42
  fence: number | undefined;
43
43
  run(): Promise<void>;
44
44
  }
45
- //# sourceMappingURL=authz_unfreeze.d.ts.map
@@ -154,4 +154,3 @@ __decorate([
154
154
  }),
155
155
  __metadata("design:type", Object)
156
156
  ], AuthzUnfreeze.prototype, "fence", void 0);
157
- //# sourceMappingURL=authz_unfreeze.js.map
@@ -14,4 +14,3 @@ export { OpenFgaProvision, AuthzCatalogDiff, AuthzCatalogSync, AuthzCatalogPrune
14
14
  export declare function getMetaData(): Promise<CommandMetaData[]>;
15
15
  /** Contrato de loader de ace (`LoadersContract.getCommand`): la clase, o `null` si el nombre no es nuestro. */
16
16
  export declare function getCommand(metaData: CommandMetaData): Promise<any>;
17
- //# sourceMappingURL=main.d.ts.map
@@ -59,4 +59,3 @@ export function getMetaData() {
59
59
  export function getCommand(metaData) {
60
60
  return loader.getCommand(metaData);
61
61
  }
62
- //# sourceMappingURL=main.js.map
@@ -83,6 +83,16 @@ export default class OpenFgaProvision extends BaseCommand {
83
83
  */
84
84
  prepare(): Promise<void>;
85
85
  run(): Promise<void>;
86
+ /**
87
+ * **La RELECTURA de lo publicado** (lote «antes de RC» · L11, diseño 1.3 D del analista; condición de aceptación C12 del
88
+ * tester): tras `writeAuthorizationModel` se LEE el modelo por su id y se compara ENTERO (`slice: 'all'`) con el que
89
+ * genera la config. Ancla el canonicalizador de L11 contra el servidor REAL en cada publicación: un servidor que
90
+ * serialice distinto (otra versión, un campo nuevo) se descubre en el despliegue, no con un 500 en la primera petición
91
+ * de producción. Si la relectura falla o difiere: exit 1, prefijo ESTABLE «provision RELECTURA FALLIDA:» (punto 7) y la
92
+ * letra que no miente (H11 c): el modelo YA está publicado en el store, y NO hay que fijarlo — por eso la línea
93
+ * `OPENFGA_MODEL_ID=` NO se imprime. Devuelve si se puede fijar.
94
+ */
95
+ private reread;
86
96
  /**
87
97
  * **La guarda contra la MUTILACIÓN del modelo** (plan 2.5 · lote D · D2; analista §4.6, juez ronda 2 · R2-4 c y H3;
88
98
  * cierre 1 de D2 · 🟡 3 y 🟠 2). `openfga:provision --store-id` publica desde la config del PROCESO: si ese proceso ya
@@ -174,4 +184,3 @@ export declare function countLiveDenies(client: any, dropped: string[]): Promise
174
184
  count: number;
175
185
  read: number;
176
186
  }>;
177
- //# sourceMappingURL=openfga_provision.d.ts.map
@@ -105,7 +105,7 @@ export default class OpenFgaProvision extends BaseCommand {
105
105
  }
106
106
  const { OpenFgaClient } = await import('@openfga/sdk');
107
107
  // El subpath `/openfga` es la única puerta al SDK (D9).
108
- const { openFgaFactsModel, provisionOpenFgaStore, assertFactsModelPublishable } = await import('../src/openfga.js');
108
+ const { openFgaFactsModel, provisionOpenFgaStore, assertFactsModelPublishable, factsModelDifferences } = await import('../src/openfga.js');
109
109
  // Los holders del consumidor viven en SU config, no en el paquete.
110
110
  const holderTypes = config?.holderTypes ?? {};
111
111
  // Los PERMISOS del modelo (c2r) salen de los catálogos del config. Se
@@ -130,7 +130,8 @@ export default class OpenFgaProvision extends BaseCommand {
130
130
  // esto salía como error crudo del SDK con una request ya hecha (⚪ N1
131
131
  // del auditor de alpha.4). Cero requests si no es publicable.
132
132
  assertFactsModelPublishable(holderTypes, permissions, undefined, relations);
133
- const client = new OpenFgaClient({ apiUrl, storeId: this.storeId });
133
+ // Injerto 15 del juez (H12): sin reintentos a escondidas (desde el SDK 0.9.6 un cliente sin `retryParams` reintenta).
134
+ const client = new OpenFgaClient({ apiUrl, storeId: this.storeId, retryParams: { maxRetry: 0 } });
134
135
  const next = openFgaFactsModel(holderTypes, permissions, relations);
135
136
  // Lote D · D2: la guarda contra la MUTILACIÓN del modelo (R2-4 c / H3) — ANTES de escribir nada. Cierre 1 de D2:
136
137
  // juzga el ESTADO del store (🟡 3) y devuelve las relaciones que hay que RE-CONTAR justo antes de publicar (🟠 2).
@@ -147,6 +148,8 @@ export default class OpenFgaProvision extends BaseCommand {
147
148
  if (relations)
148
149
  this.logger.info(`Tipos de relación fusionados: ${relations.objectTypes.map((t) => t.type).join(', ')}`);
149
150
  this.logger.log(`OPENFGA_STORE_ID=${this.storeId}`);
151
+ if (!(await this.reread(OpenFgaClient, factsModelDifferences, apiUrl, this.storeId, model.authorization_model_id, holderTypes, relations)))
152
+ return;
150
153
  this.logger.log(`OPENFGA_MODEL_ID=${model.authorization_model_id}`);
151
154
  return;
152
155
  }
@@ -158,8 +161,46 @@ export default class OpenFgaProvision extends BaseCommand {
158
161
  this.logger.log('Añade al .env (junto a AUTHZ_DRIVER=openfga):');
159
162
  this.logger.log(`OPENFGA_URL=${apiUrl}`);
160
163
  this.logger.log(`OPENFGA_STORE_ID=${storeId}`);
164
+ if (!(await this.reread(OpenFgaClient, factsModelDifferences, apiUrl, storeId, modelId, holderTypes, relations)))
165
+ return;
161
166
  this.logger.log(`OPENFGA_MODEL_ID=${modelId}`);
162
167
  }
168
+ /**
169
+ * **La RELECTURA de lo publicado** (lote «antes de RC» · L11, diseño 1.3 D del analista; condición de aceptación C12 del
170
+ * tester): tras `writeAuthorizationModel` se LEE el modelo por su id y se compara ENTERO (`slice: 'all'`) con el que
171
+ * genera la config. Ancla el canonicalizador de L11 contra el servidor REAL en cada publicación: un servidor que
172
+ * serialice distinto (otra versión, un campo nuevo) se descubre en el despliegue, no con un 500 en la primera petición
173
+ * de producción. Si la relectura falla o difiere: exit 1, prefijo ESTABLE «provision RELECTURA FALLIDA:» (punto 7) y la
174
+ * letra que no miente (H11 c): el modelo YA está publicado en el store, y NO hay que fijarlo — por eso la línea
175
+ * `OPENFGA_MODEL_ID=` NO se imprime. Devuelve si se puede fijar.
176
+ */
177
+ async reread(OpenFgaClient, factsModelDifferences, apiUrl, storeId, modelId, holderTypes, relations) {
178
+ let differences;
179
+ try {
180
+ const client = new OpenFgaClient({ apiUrl, storeId, authorizationModelId: modelId, retryParams: { maxRetry: 0 } });
181
+ const response = await client.readAuthorizationModel();
182
+ differences = factsModelDifferences(response?.authorization_model, { holderTypes, relations, slice: 'all' }).differences.map((d) => d.path);
183
+ }
184
+ catch (error) {
185
+ this.logger.error(`provision RELECTURA FALLIDA: el modelo ${modelId} SE PUBLICÓ en el store ${storeId}, pero no se pudo releer y ` +
186
+ `comparar con el que genera la config (${String(error?.message ?? error)}). NO lo fijes (OPENFGA_MODEL_ID): los ` +
187
+ 'drivers lo comparan al arrancar y, sin esta comprobación, podrían negarse a servir. Repite el comando cuando el ' +
188
+ 'servidor responda.');
189
+ this.exitCode = 1;
190
+ return false;
191
+ }
192
+ if (differences.length === 0) {
193
+ // Punto 7 de CLAUDE.md (cierre de L11, 🟡-1 del tester): la rama de éxito también dice que corrió, con prefijo ESTABLE.
194
+ this.logger.success(`provision RELECTURA OK: el modelo ${modelId} releído del store ${storeId} es IGUAL (entero) al que genera la config; ya se puede fijar.`);
195
+ return true;
196
+ }
197
+ this.logger.error(`provision RELECTURA FALLIDA: el modelo ${modelId} SE PUBLICÓ en el store ${storeId}, pero lo que el servidor devuelve ` +
198
+ `NO es igual al que genera la config (L11): ${differences.slice(0, 20).join('; ')}${differences.length > 20 ? ' …' : ''}. ` +
199
+ 'NO lo fijes (OPENFGA_MODEL_ID): los drivers openfga lo compararían igual al arrancar y responderían 500 E_AUTHZ_CONFIG. ' +
200
+ 'Suele ser un servidor de otra versión que serializa el modelo distinto: compruébalo (CI prueba OpenFGA v1.19.0) y repórtalo.');
201
+ this.exitCode = 1;
202
+ return false;
203
+ }
163
204
  /**
164
205
  * **La guarda contra la MUTILACIÓN del modelo** (plan 2.5 · lote D · D2; analista §4.6, juez ronda 2 · R2-4 c y H3;
165
206
  * cierre 1 de D2 · 🟡 3 y 🟠 2). `openfga:provision --store-id` publica desde la config del PROCESO: si ese proceso ya
@@ -411,4 +452,3 @@ export async function countLiveDenies(client, dropped) {
411
452
  } while (continuationToken);
412
453
  return { live, count, read };
413
454
  }
414
- //# sourceMappingURL=openfga_provision.js.map
@@ -18,4 +18,3 @@ import type Configure from '@adonisjs/core/commands/configure';
18
18
  * nada distinto.
19
19
  */
20
20
  export declare function configure(command: Configure): Promise<void>;
21
- //# sourceMappingURL=configure.d.ts.map
@@ -76,4 +76,3 @@ export async function configure(command) {
76
76
  }
77
77
  command.logger.info('Siguiente paso: node ace migration:run');
78
78
  }
79
- //# sourceMappingURL=configure.js.map