@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
@@ -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
@@ -75,4 +75,3 @@ export default class AuthzCatalogPruneOrphans extends BaseCommand {
75
75
  run(): Promise<void>;
76
76
  }
77
77
  export {};
78
- //# sourceMappingURL=authz_catalog_prune_orphans.d.ts.map
@@ -132,4 +132,3 @@ __decorate([
132
132
  }),
133
133
  __metadata("design:type", Object)
134
134
  ], AuthzCatalogPruneOrphans.prototype, "allowMassPurge", void 0);
135
- //# sourceMappingURL=authz_catalog_prune_orphans.js.map
@@ -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
@@ -41,6 +41,12 @@ export declare function relationsReconcileLines(report: RelationsReconcileReport
41
41
  }>;
42
42
  clean: boolean;
43
43
  };
44
+ /**
45
+ * **La pasada que revienta A MITAD dice qué quedó escrito, por partición** (plan 2.5 · lote D · D2; roadmap 2.5, 🟡 3
46
+ * del auditor de alpha.6): con el reporte parcial que viaja en el error (`error.report`, `partial: true`). Prefijo
47
+ * ESTABLE «pasada PARCIAL:» (punto 7). Función PURA.
48
+ */
49
+ export declare function partialReportLines(report: RelationsReconcileReport): string[];
44
50
  /**
45
51
  * **`authz:relations:reconcile`** (Fase 4, lote 4-6) — migra las tuplas de
46
52
  * RELACIÓN de un `RelationsDriver` a otro, idempotente y bidireccional, nunca
@@ -86,4 +92,3 @@ export default class AuthzRelationsReconcile extends BaseCommand {
86
92
  partitionUuid: string | undefined;
87
93
  run(): Promise<void>;
88
94
  }
89
- //# 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í.
@@ -63,6 +64,55 @@ export function relationsReconcileLines(report) {
63
64
  message: `relaciones: escritas ${report.written}, actualizadas ${report.updated}, borradas ${report.deleted}, ` +
64
65
  `iguales ${report.unchanged}, sobran ${report.extra}`,
65
66
  });
67
+ // Lote D · D2: los denies, en su PROPIA línea (prefijo estable «denies:»), solo si la pasada tocó o vio alguno. Los
68
+ // contadores de arriba son allows + denies; esta línea es el subconjunto (analista §4.7).
69
+ const denies = report.denies;
70
+ if (denies && Object.values(denies).some((n) => n > 0)) {
71
+ lines.push({
72
+ level: 'log',
73
+ message: `denies: escritos ${denies.written}, rehechos ${denies.updated}, borrados ${denies.deleted}, ` +
74
+ `iguales ${denies.unchanged}, sobran ${denies.extra}`,
75
+ });
76
+ }
77
+ // Lote D · D2 (S6/R2-5): un deny que la pasada NO puede migrar. Solo llega en `--dry-run` (la real lanza 500 con la
78
+ // misma letra): es DERIVA, una línea por tipo/relación y motivo, con el prefijo ESTABLE «deny NO migrable».
79
+ for (const refusal of report.denyRefused ?? []) {
80
+ lines.push({
81
+ level: 'error',
82
+ message: `deny NO migrable (${refusal.reason}) ${refusal.objectType}#${refusal.relation}: ${refusal.message}. ` +
83
+ 'La pasada real se NIEGA (500 E_AUTHZ_DENY_MIGRATION_REFUSED) sin escribir nada — ni los allows: escribirlos sin ' +
84
+ // Cierre 1 de D2 · 🟡 6: la receta de SU motivo (la decide el plan, `refusal.recipe`), no una genérica.
85
+ `el deny concedería lo que el origen niega. Receta: ${refusal.recipe}`,
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
+ }
66
116
  // R-15: la caducada del origen es la pérdida DECLARADA de la migración (como
67
117
  // `expired` en roles, 3b-8 · B2): se dice, no es deriva.
68
118
  if (report.skipped.expired > 0) {
@@ -108,6 +158,7 @@ export function relationsReconcileLines(report) {
108
158
  // exit 0 y el consejo falso «repite con --prune» (que tampoco lo quitaba).
109
159
  // El verificador queda rojo HASTA que se ejecute la receta — y dice qué y cómo.
110
160
  let destResidue = 0;
161
+ let pendingRenewals = 0;
111
162
  for (const detail of report.details ?? []) {
112
163
  const what = `(${detail.reason}) ${detail.side}: ${detail.tuple} — ${detail.message}`;
113
164
  if (detail.side === 'origen') {
@@ -115,10 +166,19 @@ export function relationsReconcileLines(report) {
115
166
  level: 'error',
116
167
  message: detail.reason === 'userset-unsupported'
117
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.`
118
- : `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.`,
119
172
  });
120
173
  continue;
121
174
  }
175
+ if (detail.reason === 'renewal-unsafe') {
176
+ // Cierre 2 de D2 · 🟠 N1: la membresía NO se renovó (su grupo transporta un deny y el destino renueva con dos
177
+ // Write). Deriva con su receta; prefijo ESTABLE «sin renovar (renewal-unsafe) destino:» (punto 7).
178
+ pendingRenewals += 1;
179
+ lines.push({ level: 'error', message: `sin renovar ${what}.` });
180
+ continue;
181
+ }
122
182
  if (detail.pruned === true) {
123
183
  // El paquete ya no lo emite (cierre 2 · N3): un reporte de un harness de terceros anterior.
124
184
  lines.push({ level: 'log', message: `BORRADA por --prune ${what}.` });
@@ -135,9 +195,45 @@ export function relationsReconcileLines(report) {
135
195
  level: 'error',
136
196
  message: detail.reason === 'userset-unsupported'
137
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.`
138
- : detail.reason === 'undeclared'
139
- ? `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.`
140
- : `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.`,
205
+ });
206
+ }
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
208
+ // `deny-conditional`: cero escrituras y cero borrados en ella; el resto migra. Deriva (exit ≠ 0) con el prefijo ESTABLE
209
+ // «partición NEGADA» (punto 7): qué partición, por qué, que su destino queda COMO ESTABA, la deriva que queda a la vista
210
+ // y la receta (honesta: sin atomicRenewal no hay forma sin ventana).
211
+ for (const refusal of report.partitionsRefused ?? []) {
212
+ const causes = refusal.causes.map((cause) => `(${cause.reason}) ${cause.tuple}`);
213
+ // Cierre 6 de D2 · ⚪ D5-3 (E3): «COMO ESTABA» no es estático — cuándo empieza a conceder el destino y qué.
214
+ const evolves = (refusal.evolves ?? []).map((e) => `a partir de ${e.at} el destino concederá a ${e.member} lo que cortan ${e.cut.join(', ')} (si otro camino se lo da; el origen lo sigue negando)`);
215
+ const later = evolves.length > 0 ? ` OJO, ese estado EVOLUCIONA: ${evolves.slice(0, 5).join('; ')}${evolves.length > 5 ? '; …' : ''}.` : '';
216
+ lines.push({
217
+ level: 'error',
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) — ` +
219
+ `${causes.slice(0, 5).join('; ')}${causes.length > 5 ? '; …' : ''}. Cero escrituras y cero borrados en esta partición ` +
220
+ `(también en la pasada real${report.prune === true ? ' y con --prune' : ''}): su destino queda COMO ESTABA, con la deriva ` +
221
+ `previa a la vista (por escribir ${refusal.pending.write}, por renovar ${refusal.pending.update}, por borrar ` +
222
+ `${refusal.pending.delete}).${later} El resto de particiones migra. Receta: ${refusal.recipe}`,
223
+ });
224
+ }
225
+ // Cierre 2 de D2 · ⚪ N3/🟡 N3: el RESIDUO del orden — membresías de un ciclo del grafo de precedencias. El estado final
226
+ // es el correcto (no es deriva): se AVISA, con el prefijo ESTABLE «orden SIN garantía». Cierre 5 de D2 (⚪ D4-4): desde
227
+ // el cierre 4 una RENOVACIÓN de membresía entra en el grafo (en `now` ni entra ni sale: lo hace cuando vence la
228
+ // caducidad más corta), así que la letra lo dice, y dice que el corte puede conceder en un instante POSTERIOR.
229
+ if ((report.orderResidue?.length ?? 0) > 0) {
230
+ lines.push({
231
+ level: 'warning',
232
+ message: `orden SIN garantía: ${report.orderResidue.length} membresía(s) de esta pasada se contradicen en el orden (el mismo ` +
233
+ 'sujeto entra o sale de grupos que se cortan entre sí —o una RENOVACIÓN, que entra o sale cuando vence la caducidad ' +
234
+ 'más corta—: cada uno trae un deny que corta un allow del otro); un corte entre ellas puede conceder, ya o en un ' +
235
+ 'instante posterior, lo que ni el estado inicial ni el final concedían en ese instante. El estado final es el ' +
236
+ `correcto. Si eso importa, aplícalas en dos pasadas. (${report.orderResidue.slice(0, 5).join('; ')}${report.orderResidue.length > 5 ? '; …' : ''})`,
141
237
  });
142
238
  }
143
239
  // R-17: el seguro de borrado masivo. En una pasada REAL sin --allow-mass-delete
@@ -221,11 +317,38 @@ export function relationsReconcileLines(report) {
221
317
  (report.skipped.invalid ?? 0) === 0 &&
222
318
  (report.skipped.usersetUnsupported ?? 0) === 0 &&
223
319
  destResidue === 0 &&
320
+ pendingRenewals === 0 &&
321
+ (report.partitionsRefused?.length ?? 0) === 0 &&
322
+ (report.denyRefused?.length ?? 0) === 0 &&
323
+ (report.denyReach?.findings.length ?? 0) === 0 &&
324
+ (report.denyReach?.stale.length ?? 0) === 0 &&
224
325
  !(report.massDelete && !massDeleteLifted) &&
225
326
  !(report.frozen?.lapsed === true) &&
226
327
  (!report.dryRun || cambios === 0);
227
328
  return { lines, clean };
228
329
  }
330
+ /**
331
+ * **La pasada que revienta A MITAD dice qué quedó escrito, por partición** (plan 2.5 · lote D · D2; roadmap 2.5, 🟡 3
332
+ * del auditor de alpha.6): con el reporte parcial que viaja en el error (`error.report`, `partial: true`). Prefijo
333
+ * ESTABLE «pasada PARCIAL:» (punto 7). Función PURA.
334
+ */
335
+ export function partialReportLines(report) {
336
+ const lines = [
337
+ `pasada PARCIAL: escritas ${report.written}, actualizadas ${report.updated}, borradas ${report.deleted} antes del fallo ` +
338
+ '(la pasada es idempotente: repítela para terminar).',
339
+ ];
340
+ // Cierre 2 de D2 · 🟠 N1: la escritura que FALLÓ ya había aplicado parte — una renovación de `openfga` (delete + write)
341
+ // cuyo write no entró: los contadores no la cuentan, pero el destino CAMBIÓ (jamás «sin escribir nada»).
342
+ if (report.written + report.updated + report.deleted === 0) {
343
+ lines.push('pasada PARCIAL: la escritura que falló YA había aplicado parte (una renovación del destino: el delete entró y el write ' +
344
+ 'no) — el destino CAMBIÓ aunque los contadores digan 0; repite la pasada.');
345
+ }
346
+ for (const row of report.partitions ?? []) {
347
+ lines.push(`pasada PARCIAL: partición ${row.partition} · ${row.status === 'done' ? 'TERMINADA' : row.status === 'failed' ? 'REVENTÓ aquí' : row.status === 'refused' ? 'NEGADA (cero escrituras)' : 'sin tocar'}` +
348
+ ` (escritas ${row.written}, actualizadas ${row.updated}, borradas ${row.deleted}; denies escritos ${row.denies.written})`);
349
+ }
350
+ return lines;
351
+ }
229
352
  /**
230
353
  * **`authz:relations:reconcile`** (Fase 4, lote 4-6) — migra las tuplas de
231
354
  * RELACIÓN de un `RelationsDriver` a otro, idempotente y bidireccional, nunca
@@ -373,6 +496,19 @@ export default class AuthzRelationsReconcile extends BaseCommand {
373
496
  else {
374
497
  this.logger.info(`Config del destino: leída de authz_relations_config (${toConfig.objectTypes.length} tipo(s)): la deriva del modelo se vigila.`);
375
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
+ }
376
512
  // L-1 · J1: la pasada que ESCRIBE corre bajo la ventana durable del
377
513
  // manager de roles (la misma fila `id = 2`, toda la flota); `--dry-run` no.
378
514
  // Los drivers los construye `runRelationsReconcile`, DESPUÉS del guard.
@@ -387,6 +523,7 @@ export default class AuthzRelationsReconcile extends BaseCommand {
387
523
  toKey: this.to,
388
524
  fromKey,
389
525
  ...(toConfig ? { toConfig } : {}),
526
+ ...(denyReach ? { denyReach } : {}),
390
527
  });
391
528
  }
392
529
  catch (error) {
@@ -397,6 +534,28 @@ export default class AuthzRelationsReconcile extends BaseCommand {
397
534
  this.exitCode = 1;
398
535
  return;
399
536
  }
537
+ // Lote D · D2 (S6/R2-5): la puerta del deny del DESTINO negó la pasada en la fase de plan, con CERO escrituras.
538
+ // La letra del error empieza por el prefijo estable «deny NO migrable:».
539
+ if (error?.code === 'E_AUTHZ_DENY_MIGRATION_REFUSED') {
540
+ this.logger.error(String(error.message ?? error));
541
+ this.exitCode = 1;
542
+ return;
543
+ }
544
+ // Lote D · D2: la pasada reventó A MITAD — el reporte parcial por partición viaja en el error.
545
+ if (error?.report?.partial === true) {
546
+ for (const line of partialReportLines(error.report))
547
+ this.logger.error(line);
548
+ this.logger.error(String(error?.message ?? error));
549
+ this.exitCode = 1;
550
+ return;
551
+ }
552
+ // ⚪ 10 (cierre 1 de D2): reventó ANTES de escribir nada — el reporte viaja con `partial: false`. La línea del
553
+ // error con su prefijo ESTABLE, sin «PARCIAL» (la marca de escritura parcial tampoco está).
554
+ if (error?.report && error.report.partial === false) {
555
+ this.logger.error(`pasada FALLIDA sin escribir nada: ${String(error?.message ?? error)} (la pasada es idempotente: repítela).`);
556
+ this.exitCode = 1;
557
+ return;
558
+ }
400
559
  // L-1 · J1: un freeze vivo de OTRA pasada (423): dos migraciones no se
401
560
  // pisan; el mensaje nombra al dueño y cómo se levanta.
402
561
  if (error?.code === 'E_AUTHZ_FREEZE_HELD' || error?.code === 'E_AUTHZ_OPERATOR_WINDOW_EXPIRED') {
@@ -473,4 +632,3 @@ __decorate([
473
632
  flags.string({ description: 'Partition (tenant) scope uuid; required with --partition-type' }),
474
633
  __metadata("design:type", Object)
475
634
  ], AuthzRelationsReconcile.prototype, "partitionUuid", void 0);
476
- //# 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
@@ -252,8 +252,23 @@ export default class AuthzScopesRelay extends BaseCommand {
252
252
  // Qué fue de cada entrada, una a una (`entries`): la pasada no es atómica
253
253
  // y un contador no permite retomar nada. Lo aparcado ya se dijo arriba.
254
254
  for (const entry of report.entries) {
255
- if (entry.status === 'applied')
256
- this.logger.log(`aplicado: ${linea(entry)}`);
255
+ if (entry.status === 'applied') {
256
+ // Asentar · cierre 1 (auditor ⚪ 8): la auditoría dice el padre APLICADO, no solo el encolado.
257
+ if (entry.reason === 'superseded') {
258
+ this.logger.warning(`aplicado SIN arista (superseded: el nodo ya no existe y su detached está en la cola): ${linea(entry)}`);
259
+ }
260
+ else if (entry.reason === 'consumer-parent' && entry.appliedParent) {
261
+ // 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
262
+ // caché rancia con dos movimientos del nodo dentro de su TTL son INDISTINGUIBLES para el relay, y un cron no lee
263
+ // avisos: la carrera hace saltar la alarma una vez; la caché, cada vez que pase.
264
+ this.logger.warning(`aplicado con el padre del CONSUMIDOR ${entry.appliedParent.type}:${entry.appliedParent.uuid ?? ''} ` +
265
+ `(no el encolado; otra entrada del nodo lo respalda): ${linea(entry)} — sale ≠ 0: comprueba que el resolutor ` +
266
+ 'del relay lee lo CONFIRMADO, sin caché ni réplica atrasada (authz:reconcile --dry-run lo contrasta)');
267
+ this.exitCode = 1;
268
+ }
269
+ else
270
+ this.logger.log(`aplicado: ${linea(entry)}`);
271
+ }
257
272
  if (entry.status === 'deferred') {
258
273
  const motivo = entry.reason?.startsWith('parked:')
259
274
  ? `aplazado por APARCADO (${entry.reason}): no se aplica hasta que alguien mire esa entrada`
@@ -267,8 +282,38 @@ export default class AuthzScopesRelay extends BaseCommand {
267
282
  notEvaluatedLines();
268
283
  if (report.failures.length > 0) {
269
284
  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}` +
285
+ const target = `#${item.id} (${item.change.op} ${item.change.child.type}:${item.change.child.uuid ?? ''})`;
286
+ // Cierre-2 de L11 (⚪ C2; punto 7): el fallo de DESPLIEGUE tiene su prefijo ESTABLE —el backend SÍ respondió, lo
287
+ // que no cuadra es el despliegue con la config—; «el backend no respondió» queda para el 503 de verdad. Desde R3
288
+ // la marca la ponen también el store/modelo inexistente, las credenciales (401/403) y el 4xx desconocido.
289
+ if (item.deployment === true) {
290
+ this.logger.error(`fallo de DESPLIEGUE ${target}: ${item.error} — el despliegue no es el que espera la config (modelo fijado, ` +
291
+ 'store, credenciales o un 4xx del servidor): este fallo NO consume intento (corrige el despliegue —' +
292
+ 'openfga:provision publica el modelo de la config— y repite)');
293
+ continue;
294
+ }
295
+ // Cierre de R3 (⚪ R3-A7; punto 7): la carrera y el límite de resolución son transitorios, pero el backend SÍ
296
+ // respondió — cada uno con su prefijo ESTABLE.
297
+ if (item.transient && item.code === 'E_AUTHZ_WRITE_CONFLICT') {
298
+ this.logger.error(`CARRERA en ${target}: ${item.error} — otro escritor (p. ej. un grant concurrente) tocó las mismas tuplas: ` +
299
+ 'este fallo NO consume intento (la pasada siguiente relee y re-aplica)');
300
+ continue;
301
+ }
302
+ if (item.transient && item.code === 'E_AUTHZ_RESOLUTION_LIMIT') {
303
+ this.logger.error(`límite de RESOLUCIÓN en ${target}: ${item.error} — el backend respondió que el grafo excede lo que ` +
304
+ 'resuelve (ver «Limits» en el README): este fallo NO consume intento');
305
+ continue;
306
+ }
307
+ // Lote «antes de RC» · F4 (⚪ RC-2 del auditor de R3; punto 7): un deadline vencido —el timeout del cliente, o un
308
+ // 504 / `deadline_exceeded` de una ESCRITURA, que es indeterminada desde el cierre de R3— no es «el backend no
309
+ // respondió»: con un 504 SÍ respondió (o un proxy), y la escritura pudo aplicarse. Prefijo ESTABLE propio.
310
+ if (item.transient && item.code === 'E_AUTHZ_BACKEND_TIMEOUT') {
311
+ this.logger.error(`PLAZO VENCIDO en ${target}: ${item.error} — venció el deadline (timeout del cliente, o un 504/deadline_exceeded ` +
312
+ 'del servidor o de un proxy): si era una escritura pudo aplicarse o no (INDETERMINADA); la pasada siguiente ' +
313
+ 'relee el store y re-aplica: este fallo NO consume intento');
314
+ continue;
315
+ }
316
+ this.logger.error(`falló ${target}: ${item.error}` +
272
317
  (item.transient ? ' — el backend no respondió: este fallo NO consume intento' : ''));
273
318
  }
274
319
  this.logger.warning('Lo que DEPENDE de un cambio fallido no se adelanta (el orden del árbol importa: aparece como «aplazado»), ' +
@@ -341,4 +386,3 @@ __decorate([
341
386
  }),
342
387
  __metadata("design:type", Object)
343
388
  ], 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
@@ -90,7 +90,20 @@ export function requeueLines(input) {
90
90
  const relay = report.relay;
91
91
  let clean = true;
92
92
  for (const entry of relay.entries) {
93
- if (entry.status === 'applied')
93
+ // Asentar · cierre 2 (gemelo del relay, C1-1): el padre APLICADO cuando no es el encolado, con su línea propia. No
94
+ // cambia `clean`: aquí hay un operador delante, y es justo la salida de una aparcada por DRIFT (re-notificar el
95
+ // padre actual y reencolar) la que termina en esta línea.
96
+ if (entry.status === 'applied' && entry.reason === 'consumer-parent' && entry.appliedParent) {
97
+ lines.push({
98
+ level: 'warning',
99
+ message: `aplicada con el padre del CONSUMIDOR ${entry.appliedParent.type}:${entry.appliedParent.uuid ?? ''} ` +
100
+ `(no el encolado; otra entrada del nodo lo respalda): ${linea(entry)}`,
101
+ });
102
+ }
103
+ else if (entry.status === 'applied' && entry.reason === 'superseded') {
104
+ lines.push({ level: 'warning', message: `aplicada SIN arista (superseded: el nodo ya no existe y su detached está en la cola): ${linea(entry)}` });
105
+ }
106
+ else if (entry.status === 'applied')
94
107
  lines.push({ level: 'log', message: `aplicada: ${linea(entry)}` });
95
108
  else if (entry.status === 'deferred') {
96
109
  clean = false;
@@ -227,4 +240,3 @@ __decorate([
227
240
  }),
228
241
  __metadata("design:type", Object)
229
242
  ], 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
@@ -55,6 +55,13 @@ export default class OpenFgaProvision extends BaseCommand {
55
55
  url: string | undefined;
56
56
  storeId: string | undefined;
57
57
  name: string | undefined;
58
+ /**
59
+ * **La salida humana de la guarda contra la MUTILACIÓN del modelo** (plan 2.5 · lote D · D2; analista §4.6, juez
60
+ * ronda 2 · R2-4 c). Sin ella, `--store-id` con un modelo nuevo que QUITA un `denied_<rel>` que el modelo actual del
61
+ * store tiene es exit 1 sin escribir nada. Con ella, el comando BARRE el store y solo publica si no queda NI UN deny
62
+ * vivo de esas relaciones: la bandera nunca autoriza apagar denies vivos.
63
+ */
64
+ allowDropDeny: boolean | undefined;
58
65
  /**
59
66
  * Arranca la app hasta `boot()` — sin `start()` — para que `config/*`
60
67
  * exista. Va en `prepare()` y no en `run()` porque `BaseCommand.exec()`
@@ -76,6 +83,46 @@ export default class OpenFgaProvision extends BaseCommand {
76
83
  */
77
84
  prepare(): Promise<void>;
78
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;
96
+ /**
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;
98
+ * cierre 1 de D2 · 🟡 3 y 🟠 2). `openfga:provision --store-id` publica desde la config del PROCESO: si ese proceso ya
99
+ * no declara el opt-in de un tipo (un refactor, otra rama del config, un binario viejo), el modelo nuevo no trae
100
+ * `denied_<rel>` y TODOS los denies de ese tipo dejan de bloquear en cuanto un pod lo resuelve — fail-open en silencio.
101
+ *
102
+ * - **El ESTADO del store manda** (🟡 3): se leen TODOS los modelos del store (`readAuthorizationModels`, paginado y
103
+ * acotado por `PROVISION_MODEL_HISTORY_MAX`) y se barre por las `<tipo>#denied_<rel>` que ALGÚN modelo declaró y el
104
+ * nuevo no (una tupla `denied_*` solo puede existir si un modelo la declaró al escribirla). Un deny VIVO de esas
105
+ * relaciones ⇒ exit 1 con o sin la bandera. Hasta el cierre se comparaba solo con el ÚLTIMO modelo: un modelo
106
+ * intermedio sin `denied_*` (un ensayo de rollback con alpha.10) desactivaba la guarda.
107
+ * - **La INTENCIÓN la dice la bandera**: si el nuevo quita respecto del ÚLTIMO modelo, sin `--allow-drop-deny` es exit
108
+ * 1 aunque no quede ningún deny (retirar un opt-in se pide en voz alta).
109
+ * - Devuelve las relaciones a RE-CONTAR antes de publicar (🟠 2), o `null` si negó. Todas las ramas fallidas empiezan
110
+ * por el prefijo ESTABLE «provision NEGADO:» (punto 7), también un barrido que falla a medias (⚪ 10 c).
111
+ */
112
+ private guardDroppedDenies;
113
+ /**
114
+ * El barrido; `null` (y exit 1 con el prefijo estable) si falla a medias (⚪ 10 c). Cierre 2 de D2 · 🟡 N4: en STREAMING
115
+ * —página a página, sin cargar el store en memoria y SIN cota de tuplas—: hasta el cierre 2, un store de más de 1 M
116
+ * tuplas dejaba `provision --store-id` en exit 1 PARA SIEMPRE tras retirar un opt-in (la historia de modelos no se
117
+ * borra en FGA), sin bandera que lo salvara. Lo que sigue siendo ruidoso: un servidor que no avanza (token repetido).
118
+ */
119
+ private sweep;
120
+ /**
121
+ * **El RECUENTO justo antes de publicar** (cierre 1 de D2 · 🟠 2, TOCTOU): otro barrido de las mismas relaciones; un
122
+ * deny que llegó DURANTE o DESPUÉS del primero ⇒ exit 1 y CERO modelos. Es la señal de que la flota sigue en
123
+ * `deny: 'write'` (el paso 0 del procedimiento no se hizo).
124
+ */
125
+ private recountBeforePublish;
79
126
  /**
80
127
  * Los permisos del modelo, o `null` (y exit 1) si no hay ninguno: un modelo
81
128
  * `facts` sin permisos no puede responder a nada, y descubrirlo aquí es
@@ -112,4 +159,28 @@ export declare function permissionsOfCatalogs(config: any): Promise<string[]>;
112
159
  export declare function relationsConfigOf(config: any): {
113
160
  objectTypes: readonly any[];
114
161
  } | undefined;
115
- //# sourceMappingURL=openfga_provision.d.ts.map
162
+ /**
163
+ * Cierre 1 de D2 · 🟡 3: tope de la HISTORIA de modelos que `provision --store-id` lee para juzgar el estado del store
164
+ * (cada modelo publicado es una versión): pasarse es exit 1 (`provision NEGADO:`), nunca juzgar con una historia cortada.
165
+ */
166
+ export declare const PROVISION_MODEL_HISTORY_MAX = 10000;
167
+ /**
168
+ * **Las `<tipo>#denied_<rel>` de TIPOS DE RELACIÓN que el modelo actual declara y el nuevo no** (plan 2.5 · lote D ·
169
+ * D2): la mutilación que la guarda de `openfga:provision` niega. Función PURA (dos modelos JSON en la forma de
170
+ * `readLatestAuthorizationModel` / `openFgaFactsModel`). Los `denied_<P>` de `scope` (roles) quedan fuera: retirar un
171
+ * permiso del catálogo no es retirar el opt-in de un tipo.
172
+ */
173
+ export declare function droppedDenyRelations(current: any, next: any): string[];
174
+ /**
175
+ * **El barrido de `--allow-drop-deny`, en STREAMING** (cierre 2 de D2 · 🟡 N4): cuenta las tuplas vivas cuyo
176
+ * `<tipo>#<relación>` está en `dropped` leyendo el store ENTERO por páginas (`Read({})`: un `Read` por prefijo de objeto
177
+ * es 200 con cero tuplas) sin acumularlo — guarda el recuento y TRES ejemplos para la letra, nada más — y SIN cota de
178
+ * tuplas (hasta el cierre 2, 1 M ⇒ exit 1 para siempre). Exhaustivo o ruidoso: una página sin `tuples` o un
179
+ * `continuation_token` que se repite (el servidor no avanza) LANZA (el comando lo dice como «el barrido falló a medias»).
180
+ * Exportada para el caso que la juzga sin servidor.
181
+ */
182
+ export declare function countLiveDenies(client: any, dropped: string[]): Promise<{
183
+ live: string[];
184
+ count: number;
185
+ read: number;
186
+ }>;