@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
@@ -41,6 +41,18 @@ export declare class AuthorizationBackendTimeoutError extends AuthorizationBacke
41
41
  static code: string;
42
42
  constructor(driver: string, operation: string, timeoutMs: number, cause?: unknown);
43
43
  }
44
+ /**
45
+ * **El grafo excede lo que el motor resuelve** (lote «antes de RC» · R3, decisión del dueño 2026-10-17 (4)): OpenFGA
46
+ * respondió `authorization_model_resolution_too_complex` —una cadena de scopes o una anidación de grupos más profunda que
47
+ * su `--resolve-node-limit`—. Es un `AuthorizationBackendError` (503, se maneja igual y el relay lo trata como
48
+ * transitorio) con código propio, porque NO es una caída: en el BORDE es probabilístico (a 23 saltos falla entre un 4 % y
49
+ * un 26 % según la carga, medido en 3b-2e) y un reintento puede acertar, pero por ENCIMA de la cota falla siempre. Las
50
+ * cotas están en la tabla «Limits» del README (`FACTS_MAX_RESOLVE_DEPTH`). Nunca un `false`: deniega, pero ruidoso.
51
+ */
52
+ export declare class ResolutionLimitError extends AuthorizationBackendError {
53
+ static code: string;
54
+ constructor(driver: string, operation: string, cause?: unknown);
55
+ }
44
56
  /**
45
57
  * El resolutor de ancestros del consumidor lanzó. Es una dependencia más de
46
58
  * cada pregunta (el árbol de scopes), así que su caída se clasifica como la
@@ -78,6 +90,17 @@ export declare class InvalidSlugError extends Exception {
78
90
  export declare class AuthorizationConfigError extends Exception {
79
91
  static status: number;
80
92
  static code: string;
93
+ /**
94
+ * **Un deny CONDICIONAL ya en el store que `openfga` no puede reemplazar** (plan 2.5 · lote D · cierre 1 de D2, 🟡 4).
95
+ * El deny del paquete no caduca (D3): una tupla `denied_<rel>` CON condición solo puede venir de fuera (escrita a
96
+ * mano). Re-escribirla sin condición es 409 «already exists with a different condition» (medido en v1.19.0; M5: FGA
97
+ * no reemplaza la misma clave en un `Write`), que hasta el cierre salía como 503 «transitorio» y se repetía para
98
+ * siempre — y el deny CADUCABA. Ahora es 500 con la receta; `denyConditional: true` deja que el reconcile lo
99
+ * clasifique como residuo `deny-conditional` del destino (nunca 503 repetido).
100
+ */
101
+ static conditionalDeny(operation: string, tuple: string): AuthorizationConfigError & {
102
+ denyConditional: true;
103
+ };
81
104
  /**
82
105
  * **`assertWrite` devolvió una promesa** (2.4.0-alpha.3 · D3, hallazgo H2).
83
106
  * `assertWrite(ref): void` es SÍNCRONO y puro a propósito (R-13), pero
@@ -124,6 +147,33 @@ export declare function isThenable(value: unknown): boolean;
124
147
  export declare function markPartialWrite<E extends object>(error: E): E & {
125
148
  partialWrite: true;
126
149
  };
150
+ /**
151
+ * **Marca de fallo de DESPLIEGUE** (lote «antes de RC» · cierre de L11, 🟠 A1 del auditor = H1; adelanta SOLO esta pieza
152
+ * de la decisión del dueño 2026-10-17 (3), que R3 generaliza con `classifyFgaFailure`): el error dice algo del
153
+ * DESPLIEGUE —el modelo fijado no es el que genera la config (la puerta L11), o el generador no puede generar con él—,
154
+ * no de la entrada que se estaba aplicando. Para el relay es TRANSITORIO (`markFailed(…, { transient: true })`: no
155
+ * consume intento ni aparca): aparcar un `moved`/`detached` por un despliegue a medias era fail-open PERSISTENTE (los
156
+ * admins de la org vieja conservaban el acceso hasta un `requeue` manual). Para el llamante sigue siendo lo que es
157
+ * (500 `E_AUTHZ_CONFIG`, «no es transitorio»): la marca es una propiedad, no una clase, y no cambia ni `status` ni
158
+ * `code`. Interna: no se exporta por `index.ts` (R3 decide la forma pública).
159
+ */
160
+ export declare function markDeploymentFailure<E extends object>(error: E): E & {
161
+ deploymentFailure: true;
162
+ };
163
+ /** ¿El error viene marcado como fallo de despliegue (`markDeploymentFailure`)? */
164
+ export declare function isDeploymentFailure(error: unknown): boolean;
165
+ /**
166
+ * **¿Es un fallo TRANSITORIO para el relay?** (no consume intento ni aparca; `markFailed(…, { transient: true })`).
167
+ * PUNTO DE PASO ÚNICO de la regla (el manager la llama en cada fallo de la pasada; la guardia `fga.errors.classify` la
168
+ * juzga sobre las formas medidas). Transitorio: un 503 del backend (`AuthorizationBackendError` y subclases —timeout,
169
+ * `E_AUTHZ_RESOLUTION_LIMIT`—, o cualquier `status` 503), un fallo marcado de DESPLIEGUE (`markDeploymentFailure`) y
170
+ * **una CARRERA que escapa** (409 `E_AUTHZ_WRITE_CONFLICT`: otro escritor —un `grant` concurrente sobre la misma
171
+ * arista `scope#binding` del barrido, o la contención del árbol de `reparent`— llegó antes). Cierre de R3 (🟠 R3-A1 del
172
+ * auditor, decisión (3) firmada «carrera ⇒ como hoy»: antes de R3 la carrera que escapaba del guard era un 503
173
+ * transitorio): para el LLAMANTE sigue siendo 409; para el relay, aparcar por contención era H1 (un `moved` aparcado
174
+ * deja el padre viejo concediendo y tapona su clave). Lo demás —422, 500 sin marca, error sin `status`— cuenta intento.
175
+ */
176
+ export declare function isTransientRelayFailure(error: unknown): boolean;
127
177
  /** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
128
178
  export declare function isPartialWrite(error: unknown): boolean;
129
179
  /**
@@ -331,6 +381,54 @@ export declare class WriteConflictError extends Exception {
331
381
  static status: number;
332
382
  static code: string;
333
383
  }
384
+ /**
385
+ * **Renovar esta membresía abriría un hueco en un deny** (plan 2.5 · lote D · cierre 2 de D2, 🟠 N1; decisión del dueño
386
+ * 2026-10-15 (9)): `relate` sobre una membresía `group:*#member` EXISTENTE con otra caducidad, en un driver que renueva
387
+ * con delete + write (`atomicRenewal: false`, `openfga`: FGA no reemplaza la misma clave en un `Write`, M5), cuando ese
388
+ * grupo —o uno por encima de él— es sujeto de un deny vivo de la partición. Entre los dos `Write` el sujeto no tendría el
389
+ * deny y CONCEDERÍA; si el 2.º fallara, para siempre. 409 y CERO escrituras: el estado del store no admite esta
390
+ * escritura tal cual. **Siempre cero escrituras** (cierre 4 · ⚪ C3-3): la renovación que SÍ se aplicó y en cuya
391
+ * ventana cayó un deny concurrente es `RenewalRacedError` (`E_AUTHZ_RENEWAL_RACED`), con otra receta.
392
+ *
393
+ * **La receta, honesta** (cierre 5 de D2, decisión del dueño §2026-10-16 (10), opción A): en `openfga` NO hay forma sin
394
+ * ventana de cambiar esa caducidad —FGA no tiene upsert, openfga/openfga#2151—. (1) Patrón recomendado: membresías SIN
395
+ * caducidad en los grupos con deny, que se terminan con `unrelate`. (2) Si hay que cambiarla: `unrelate` + `relate`
396
+ * ACEPTANDO la ventana, dicho como tal. Hasta el cierre 5 el mensaje ofrecía además «retira antes el deny, renueva y
397
+ * vuelve a escribirlo» como si fuera la salida segura: también abre la ventana (sin el deny, el sujeto concede). Cuando
398
+ * OpenFGA ofrezca upsert, la renovación será UNA escritura y `atomicRenewal` pasará a `true`.
399
+ */
400
+ export declare class RenewalUnsafeError extends Exception {
401
+ static status: number;
402
+ static code: string;
403
+ static carrier(membership: string, denied: string): RenewalUnsafeError;
404
+ /**
405
+ * **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
406
+ * del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
407
+ * (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
408
+ * (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
409
+ * con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
410
+ */
411
+ static foreignCondition(operation: string, tuple: string, detail: string): RenewalUnsafeError;
412
+ }
413
+ /**
414
+ * **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
415
+ * cierre 4 · ⚪ C3-3): la renovación (dos `Write`) de una membresía cuyo grupo NO transportaba ningún deny al
416
+ * comprobarlo, y la RE-COMPROBACIÓN tras los dos `Write` encuentra uno —un `deny` concurrente entró entre la comprobación
417
+ * y la renovación—. Estado final correcto (la membresía está y el deny vuelve a alcanzar al sujeto), pero durante la
418
+ * ventana entre los dos `Write` el deny ya confirmado NO alcanzó al sujeto. Va marcado parcial (`markPartialWrite`: el
419
+ * manager notifica `indeterminate: true`) y lleva `carriedDeny` y `renewalApplied: true`.
420
+ *
421
+ * Hasta el cierre 4 era el MISMO 409 `E_AUTHZ_RENEWAL_UNSAFE` («cero escrituras, aplica la receta»): un consumidor que
422
+ * automatizara esa receta (`unrelate` + `relate`) ante este caso reabría la ventana con el deny YA dentro del grupo. Por
423
+ * eso tiene su código y su receta, que NO es esa.
424
+ */
425
+ export declare class RenewalRacedError extends Exception {
426
+ static status: number;
427
+ static code: string;
428
+ carriedDeny: string;
429
+ renewalApplied: true;
430
+ static raced(membership: string, denied: string): RenewalRacedError;
431
+ }
334
432
  /**
335
433
  * **El lote de `checkMany` no tiene la forma admitida** (plan 2.5 · lote A,
336
434
  * P4): vacío, o por encima de `RELATIONS_CHECK_MANY_MAX` (50). Es 422 con
@@ -444,6 +542,31 @@ export declare class ScopeDepthLimitError extends Exception {
444
542
  server: string;
445
543
  });
446
544
  }
545
+ /**
546
+ * **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
547
+ * 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
548
+ * LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
549
+ * un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
550
+ * es el grafo el que no cabe. Gemelo de `E_AUTHZ_SCOPE_DEPTH_LIMIT` y con su misma regla de letra (alpha.8, 7.2): el
551
+ * `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
552
+ * backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
553
+ * en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
554
+ */
555
+ export declare class GroupDepthLimitError extends Exception {
556
+ static status: number;
557
+ static code: string;
558
+ readonly details: {
559
+ reason: 'too-deep' | 'cycle' | 'too-many-groups';
560
+ edge: string;
561
+ depth: number | null;
562
+ maxGroupDepth: number;
563
+ /** Solo con `too-many-groups`: qué cota se pasó (grupos, requests, tuplas leídas o el deadline total). */
564
+ budget?: 'nodes' | 'reads' | 'tuples' | 'deadline';
565
+ why: string;
566
+ server: string;
567
+ };
568
+ constructor(message: string, details: GroupDepthLimitError['details']);
569
+ }
447
570
  /**
448
571
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
449
572
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -480,6 +603,33 @@ export declare class UnsupportedOperationError extends Exception {
480
603
  * entre procesos (invariante 14); moverla al commit del consumidor lo anula.
481
604
  */
482
605
  static transactionalCatalog(operation: string): UnsupportedOperationError;
606
+ /**
607
+ * **Los `list*` de relaciones no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez, decisión del dueño
608
+ * 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
609
+ * hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
610
+ * `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
611
+ * una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
612
+ * como métodos aparte (`*Page`, 2.6) con capacidad propia.
613
+ */
614
+ static listPage(operation: string, driver: string): UnsupportedOperationError;
615
+ /**
616
+ * **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
617
+ * devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
618
+ * llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
619
+ */
620
+ static enumerateCursor(operation: string, driver: string): UnsupportedOperationError;
621
+ }
622
+ /**
623
+ * **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
624
+ * `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
625
+ * `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
626
+ * pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
627
+ * `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
628
+ * encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
629
+ */
630
+ export declare class InvalidRelationPageError extends Exception {
631
+ static status: number;
632
+ static code: string;
483
633
  }
484
634
  /**
485
635
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -832,4 +982,86 @@ export declare class RelationUsersetUnsupportedError extends Exception {
832
982
  static status: number;
833
983
  static code: string;
834
984
  }
835
- //# sourceMappingURL=errors.d.ts.map
985
+ /**
986
+ * **Deny sobre algo que no se puede denegar** (plan 2.5 · lote D · D1; panel
987
+ * tenencia ronda 2, D10/D-17). `deny(…)` sobre un tipo cuya spec no declara
988
+ * `deny: 'write'` (fase 2 del opt-in: sin opt-in, o en `deny: 'read'`, la fase
989
+ * en la que el modelo y las lecturas ya honran el deny pero aún no se aceptan
990
+ * escrituras), sobre el tipo built-in `group`, o con el comodín `*` como
991
+ * sujeto. 422 ANTES de tocar el driver (cero llamadas, cero eventos): una
992
+ * pregunta inválida, no una caída.
993
+ */
994
+ export declare class RelationNotDenyableError extends Exception {
995
+ static status: number;
996
+ static code: string;
997
+ }
998
+ /**
999
+ * **`expiresAt` en un deny** (plan 2.5 · lote D · D3, decisión del dueño
1000
+ * 2026-10-14): el deny de relaciones NO caduca en v1. Medido (M5, OpenFGA
1001
+ * v1.19.0): FGA rechaza delete+write de la misma clave en un `Write` (400
1002
+ * `cannot_allow_duplicate_tuples_in_one_request`) y `on_duplicate: ignore` con
1003
+ * otra condición es 409, así que cambiar la caducidad de un deny VIVO exige dos
1004
+ * `Write` con una ventana en la que deja de bloquear — y si el segundo cae, el
1005
+ * deny DESAPARECE. 422 ANTES de tocar el driver, con la razón medida.
1006
+ */
1007
+ export declare class RelationDenyExpiryError extends Exception {
1008
+ static status: number;
1009
+ static code: string;
1010
+ }
1011
+ /**
1012
+ * **`membersOf` de `database` sobre un objeto con deny y más candidatos que la cota** (lote D · cierre 1 de D1, 🟡 4 del
1013
+ * auditor). Con una fila `denied_*` vigente en el objeto, cada candidato se juzga con un `check` (el corte de camino);
1014
+ * sin cota eran N consultas en serie —una amplificación barata para quien puede escribir un deny—. Pasada
1015
+ * `RELATIONS_MEMBERS_DENY_CHECK_MAX` (1 000) ⇒ 500, NUNCA una lista parcial; el deadline TOTAL del bucle es 503
1016
+ * `E_AUTHZ_BACKEND_TIMEOUT`. **500 y no 422** (decisión 1 del cierre 1, ratificada en el cierre 2): no es una pregunta
1017
+ * inválida del llamante —la misma pregunta es válida con menos candidatos— sino una cota del DESPLIEGUE (precedente
1018
+ * `E_AUTHZ_TOO_MANY_LOCAL_ROLES`); y no 503 porque no es reintentable. `authorizedScopes` (`E_AUTHZ_TOO_MANY_SCOPES`,
1019
+ * 422) acota una enumeración que el llamante pide con su `maxScopes`; aquí no hay cota del llamante.
1020
+ */
1021
+ export declare class MembersOfDenyLimitError extends Exception {
1022
+ static status: number;
1023
+ static code: string;
1024
+ }
1025
+ /**
1026
+ * **`authz:relations:reconcile` se NIEGA a migrar denies** (plan 2.5 · lote D · D2; auditor R2-5, tester S6, juez ronda 2
1027
+ * fila D). El origen trae al menos un deny vivo y el destino no puede escribirlo: no declara `relationDenies` (o no trae
1028
+ * `deny`/`removeDeny`/`denyPhase`), el tipo no está en la fase `'write'` del opt-in EN EL DESTINO (su config, no la spec
1029
+ * persistida), el par tipo/relación no está declarado allí, su modelo fijado no declara `denied_<rel>` (D-31), o el
1030
+ * origen enumera un deny SIN marcar `kind` (`unmarked`). Se decide en la FASE DE PLAN, antes de la primera escritura en
1031
+ * cualquier partición: **CERO escrituras**, y **jamás `skipped`** — perder un deny no es una pérdida que se pueda declarar
1032
+ * (escribir los allows sin él concede lo que el origen niega). `--dry-run` no lanza: lo marca en `report.denyRefused` y es
1033
+ * deriva. Lleva `refusals` (una entrada por tipo/relación y motivo). 500 y no 422: no es una pregunta del llamante sino
1034
+ * el estado del despliegue (precedente `E_AUTHZ_MASS_RECONCILE_REFUSED`).
1035
+ */
1036
+ export declare class DenyMigrationRefusedError extends Exception {
1037
+ static status: number;
1038
+ static code: string;
1039
+ refusals: ReadonlyArray<{
1040
+ objectType: string;
1041
+ relation: string;
1042
+ reason: string;
1043
+ message: string;
1044
+ }>;
1045
+ }
1046
+ /**
1047
+ * **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
1048
+ * decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
1049
+ * vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
1050
+ * ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
1051
+ * línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
1052
+ * levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
1053
+ * precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
1054
+ * estado del despliegue.
1055
+ */
1056
+ export declare class DenyReachRefusedError extends Exception {
1057
+ static status: number;
1058
+ static code: string;
1059
+ findings: ReadonlyArray<{
1060
+ objectType: string;
1061
+ relation: string;
1062
+ reason: string;
1063
+ count: number;
1064
+ lost: string[];
1065
+ recipe: string;
1066
+ }>;
1067
+ }
@@ -45,6 +45,27 @@ export class AuthorizationBackendTimeoutError extends AuthorizationBackendError
45
45
  this.message = `El backend de autorización '${driver}' no respondió en ${timeoutMs} ms (${operation})`;
46
46
  }
47
47
  }
48
+ /**
49
+ * **El grafo excede lo que el motor resuelve** (lote «antes de RC» · R3, decisión del dueño 2026-10-17 (4)): OpenFGA
50
+ * respondió `authorization_model_resolution_too_complex` —una cadena de scopes o una anidación de grupos más profunda que
51
+ * su `--resolve-node-limit`—. Es un `AuthorizationBackendError` (503, se maneja igual y el relay lo trata como
52
+ * transitorio) con código propio, porque NO es una caída: en el BORDE es probabilístico (a 23 saltos falla entre un 4 % y
53
+ * un 26 % según la carga, medido en 3b-2e) y un reintento puede acertar, pero por ENCIMA de la cota falla siempre. Las
54
+ * cotas están en la tabla «Limits» del README (`FACTS_MAX_RESOLVE_DEPTH`). Nunca un `false`: deniega, pero ruidoso.
55
+ */
56
+ export class ResolutionLimitError extends AuthorizationBackendError {
57
+ static code = 'E_AUTHZ_RESOLUTION_LIMIT';
58
+ constructor(driver, operation, cause) {
59
+ super(driver, operation, cause);
60
+ this.message =
61
+ `El backend de autorización '${driver}' no pudo resolver ${operation}: el grafo (la cadena de scopes o la ` +
62
+ 'anidación de grupos) excede lo que OpenFGA resuelve (resolution_too_complex). En el borde es probabilístico y un ' +
63
+ 'reintento puede acertar; por encima de la cota falla siempre: ver «Limits» en el README (FACTS_MAX_RESOLVE_DEPTH ' +
64
+ 'para la cadena de scopes; maxGroupDepth (defineRelationsConfig) para la anidación de grupos —el driver ' +
65
+ 'openfga lo calibra contra el servidor, 22 en v1.19 por defecto—; el mando del servidor es OPENFGA_RESOLVE_NODE_LIMIT / --resolve-node-limit). ' +
66
+ 'Nunca es un «sin permiso».';
67
+ }
68
+ }
48
69
  /**
49
70
  * El resolutor de ancestros del consumidor lanzó. Es una dependencia más de
50
71
  * cada pregunta (el árbol de scopes), así que su caída se clasifica como la
@@ -84,6 +105,24 @@ export class InvalidSlugError extends Exception {
84
105
  export class AuthorizationConfigError extends Exception {
85
106
  static status = 500;
86
107
  static code = 'E_AUTHZ_CONFIG';
108
+ /**
109
+ * **Un deny CONDICIONAL ya en el store que `openfga` no puede reemplazar** (plan 2.5 · lote D · cierre 1 de D2, 🟡 4).
110
+ * El deny del paquete no caduca (D3): una tupla `denied_<rel>` CON condición solo puede venir de fuera (escrita a
111
+ * mano). Re-escribirla sin condición es 409 «already exists with a different condition» (medido en v1.19.0; M5: FGA
112
+ * no reemplaza la misma clave en un `Write`), que hasta el cierre salía como 503 «transitorio» y se repetía para
113
+ * siempre — y el deny CADUCABA. Ahora es 500 con la receta; `denyConditional: true` deja que el reconcile lo
114
+ * clasifique como residuo `deny-conditional` del destino (nunca 503 repetido).
115
+ */
116
+ static conditionalDeny(operation, tuple) {
117
+ const error = new AuthorizationConfigError(`${operation}: en el store ya hay un deny CONDICIONAL (con caducidad) sobre ${tuple}. El deny del paquete no caduca ` +
118
+ '(D3), así que esa tupla se escribió desde fuera del paquete; OpenFGA no puede REEMPLAZAR la misma clave en un ' +
119
+ 'Write (409 «already exists with a different condition», M5), así que este driver no puede rehacerlo permanente ' +
120
+ 'y repetir no sirve: no es una caída. Mientras viva niega; cuando caduque concederá lo que cortaba. Receta: ' +
121
+ 'removeDeny + deny (entre los dos hay una VENTANA en la que el deny no está y lo que cortaba se concede: hazlo ' +
122
+ 'con la flota sin tráfico sobre ese objeto o asúmela). No se ha escrito nada.');
123
+ error.denyConditional = true;
124
+ return error;
125
+ }
87
126
  /**
88
127
  * **`assertWrite` devolvió una promesa** (2.4.0-alpha.3 · D3, hallazgo H2).
89
128
  * `assertWrite(ref): void` es SÍNCRONO y puro a propósito (R-13), pero
@@ -159,6 +198,52 @@ export function markPartialWrite(error) {
159
198
  }
160
199
  return error;
161
200
  }
201
+ /**
202
+ * **Marca de fallo de DESPLIEGUE** (lote «antes de RC» · cierre de L11, 🟠 A1 del auditor = H1; adelanta SOLO esta pieza
203
+ * de la decisión del dueño 2026-10-17 (3), que R3 generaliza con `classifyFgaFailure`): el error dice algo del
204
+ * DESPLIEGUE —el modelo fijado no es el que genera la config (la puerta L11), o el generador no puede generar con él—,
205
+ * no de la entrada que se estaba aplicando. Para el relay es TRANSITORIO (`markFailed(…, { transient: true })`: no
206
+ * consume intento ni aparca): aparcar un `moved`/`detached` por un despliegue a medias era fail-open PERSISTENTE (los
207
+ * admins de la org vieja conservaban el acceso hasta un `requeue` manual). Para el llamante sigue siendo lo que es
208
+ * (500 `E_AUTHZ_CONFIG`, «no es transitorio»): la marca es una propiedad, no una clase, y no cambia ni `status` ni
209
+ * `code`. Interna: no se exporta por `index.ts` (R3 decide la forma pública).
210
+ */
211
+ export function markDeploymentFailure(error) {
212
+ try {
213
+ Object.defineProperty(error, 'deploymentFailure', { value: true, enumerable: false, configurable: true });
214
+ }
215
+ catch {
216
+ // Un error congelado no admite la marca: se propaga sin ella (cuenta intento, como antes de la marca).
217
+ }
218
+ return error;
219
+ }
220
+ /** ¿El error viene marcado como fallo de despliegue (`markDeploymentFailure`)? */
221
+ export function isDeploymentFailure(error) {
222
+ return error !== null && typeof error === 'object' && error.deploymentFailure === true;
223
+ }
224
+ /**
225
+ * **¿Es un fallo TRANSITORIO para el relay?** (no consume intento ni aparca; `markFailed(…, { transient: true })`).
226
+ * PUNTO DE PASO ÚNICO de la regla (el manager la llama en cada fallo de la pasada; la guardia `fga.errors.classify` la
227
+ * juzga sobre las formas medidas). Transitorio: un 503 del backend (`AuthorizationBackendError` y subclases —timeout,
228
+ * `E_AUTHZ_RESOLUTION_LIMIT`—, o cualquier `status` 503), un fallo marcado de DESPLIEGUE (`markDeploymentFailure`) y
229
+ * **una CARRERA que escapa** (409 `E_AUTHZ_WRITE_CONFLICT`: otro escritor —un `grant` concurrente sobre la misma
230
+ * arista `scope#binding` del barrido, o la contención del árbol de `reparent`— llegó antes). Cierre de R3 (🟠 R3-A1 del
231
+ * auditor, decisión (3) firmada «carrera ⇒ como hoy»: antes de R3 la carrera que escapaba del guard era un 503
232
+ * transitorio): para el LLAMANTE sigue siendo 409; para el relay, aparcar por contención era H1 (un `moved` aparcado
233
+ * deja el padre viejo concediendo y tapona su clave). Lo demás —422, 500 sin marca, error sin `status`— cuenta intento.
234
+ */
235
+ export function isTransientRelayFailure(error) {
236
+ if (error instanceof AuthorizationBackendError)
237
+ return true;
238
+ if (isDeploymentFailure(error))
239
+ return true;
240
+ if (typeof error !== 'object' || error === null)
241
+ return false;
242
+ const { status, code } = error;
243
+ if (code === WriteConflictError.code)
244
+ return true;
245
+ return status === 503;
246
+ }
162
247
  /** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
163
248
  export function isPartialWrite(error) {
164
249
  return error !== null && typeof error === 'object' && error.partialWrite === true;
@@ -382,6 +467,75 @@ export class WriteConflictError extends Exception {
382
467
  static status = 409;
383
468
  static code = 'E_AUTHZ_WRITE_CONFLICT';
384
469
  }
470
+ /**
471
+ * **Renovar esta membresía abriría un hueco en un deny** (plan 2.5 · lote D · cierre 2 de D2, 🟠 N1; decisión del dueño
472
+ * 2026-10-15 (9)): `relate` sobre una membresía `group:*#member` EXISTENTE con otra caducidad, en un driver que renueva
473
+ * con delete + write (`atomicRenewal: false`, `openfga`: FGA no reemplaza la misma clave en un `Write`, M5), cuando ese
474
+ * grupo —o uno por encima de él— es sujeto de un deny vivo de la partición. Entre los dos `Write` el sujeto no tendría el
475
+ * deny y CONCEDERÍA; si el 2.º fallara, para siempre. 409 y CERO escrituras: el estado del store no admite esta
476
+ * escritura tal cual. **Siempre cero escrituras** (cierre 4 · ⚪ C3-3): la renovación que SÍ se aplicó y en cuya
477
+ * ventana cayó un deny concurrente es `RenewalRacedError` (`E_AUTHZ_RENEWAL_RACED`), con otra receta.
478
+ *
479
+ * **La receta, honesta** (cierre 5 de D2, decisión del dueño §2026-10-16 (10), opción A): en `openfga` NO hay forma sin
480
+ * ventana de cambiar esa caducidad —FGA no tiene upsert, openfga/openfga#2151—. (1) Patrón recomendado: membresías SIN
481
+ * caducidad en los grupos con deny, que se terminan con `unrelate`. (2) Si hay que cambiarla: `unrelate` + `relate`
482
+ * ACEPTANDO la ventana, dicho como tal. Hasta el cierre 5 el mensaje ofrecía además «retira antes el deny, renueva y
483
+ * vuelve a escribirlo» como si fuera la salida segura: también abre la ventana (sin el deny, el sujeto concede). Cuando
484
+ * OpenFGA ofrezca upsert, la renovación será UNA escritura y `atomicRenewal` pasará a `true`.
485
+ */
486
+ export class RenewalUnsafeError extends Exception {
487
+ static status = 409;
488
+ static code = 'E_AUTHZ_RENEWAL_UNSAFE';
489
+ static carrier(membership, denied) {
490
+ return new RenewalUnsafeError(`relate: no se renueva ${membership}: el grupo TRANSPORTA un deny vivo (${denied}) y este driver renueva con DOS ` +
491
+ 'Write —delete y write: OpenFGA no reemplaza la misma clave en un Write (M5; sin upsert, openfga/openfga#2151)—; ' +
492
+ 'entre los dos el deny no alcanzaría al sujeto y concedería (y si el segundo fallara, para siempre). No se ha escrito ' +
493
+ 'nada. En este driver NO hay forma sin ventana de cambiar esta caducidad. (1) Patrón recomendado: membresías SIN ' +
494
+ 'caducidad en los grupos con deny, que se terminan con unrelate. (2) Si hay que cambiarla: unrelate + relate de esa ' +
495
+ 'membresía ACEPTANDO la ventana — mientras dura, el sujeto no recibe el deny del grupo y concede lo que otro camino le ' +
496
+ 'dé y ese deny corte.');
497
+ }
498
+ /**
499
+ * **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
500
+ * del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
501
+ * (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
502
+ * (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
503
+ * con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
504
+ */
505
+ static foreignCondition(operation, tuple, detail) {
506
+ return new RenewalUnsafeError(`${operation}: la tupla ${tuple} ya existe en el store con una condición que este paquete no escribe ni sabe leer ` +
507
+ `(${detail}). No se reescribe a ciegas: el paquete no sabe qué concede (la puede haber escrito otra herramienta, o ` +
508
+ 'un modelo anterior). No se ha escrito nada. Receta: revísala y bórrala a mano (SDK/CLI de OpenFGA, o unrelate/' +
509
+ 'revoke por el paquete) y repite.');
510
+ }
511
+ }
512
+ /**
513
+ * **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
514
+ * cierre 4 · ⚪ C3-3): la renovación (dos `Write`) de una membresía cuyo grupo NO transportaba ningún deny al
515
+ * comprobarlo, y la RE-COMPROBACIÓN tras los dos `Write` encuentra uno —un `deny` concurrente entró entre la comprobación
516
+ * y la renovación—. Estado final correcto (la membresía está y el deny vuelve a alcanzar al sujeto), pero durante la
517
+ * ventana entre los dos `Write` el deny ya confirmado NO alcanzó al sujeto. Va marcado parcial (`markPartialWrite`: el
518
+ * manager notifica `indeterminate: true`) y lleva `carriedDeny` y `renewalApplied: true`.
519
+ *
520
+ * Hasta el cierre 4 era el MISMO 409 `E_AUTHZ_RENEWAL_UNSAFE` («cero escrituras, aplica la receta»): un consumidor que
521
+ * automatizara esa receta (`unrelate` + `relate`) ante este caso reabría la ventana con el deny YA dentro del grupo. Por
522
+ * eso tiene su código y su receta, que NO es esa.
523
+ */
524
+ export class RenewalRacedError extends Exception {
525
+ static status = 409;
526
+ static code = 'E_AUTHZ_RENEWAL_RACED';
527
+ static raced(membership, denied) {
528
+ const error = new RenewalRacedError(`relate: la renovación de ${membership} SE APLICÓ, pero un deny concurrente (${denied}) entró entre la comprobación ` +
529
+ 'de portadora y los dos Write: durante la ventana entre el delete y el write ese deny, ya confirmado, NO alcanzó al ' +
530
+ 'sujeto. El estado final es correcto (la membresía está y el deny la alcanza): NO hay nada que reescribir, y la receta ' +
531
+ 'de E_AUTHZ_RENEWAL_UNSAFE (unrelate + relate) reabriría la ventana con el deny ya dentro. Receta: revisa las ' +
532
+ 'decisiones de acceso de ese sujeto en esa ventana; para no repetirla, serializa los deny y las renovaciones de ese ' +
533
+ 'grupo, o renueva con removeDeny antes y deny después.');
534
+ error.carriedDeny = denied;
535
+ error.renewalApplied = true;
536
+ return error;
537
+ }
538
+ }
385
539
  /**
386
540
  * **El lote de `checkMany` no tiene la forma admitida** (plan 2.5 · lote A,
387
541
  * P4): vacío, o por encima de `RELATIONS_CHECK_MANY_MAX` (50). Es 422 con
@@ -481,6 +635,24 @@ export class ScopeDepthLimitError extends Exception {
481
635
  Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
482
636
  }
483
637
  }
638
+ /**
639
+ * **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
640
+ * 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
641
+ * LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
642
+ * un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
643
+ * es el grafo el que no cabe. Gemelo de `E_AUTHZ_SCOPE_DEPTH_LIMIT` y con su misma regla de letra (alpha.8, 7.2): el
644
+ * `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
645
+ * backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
646
+ * en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
647
+ */
648
+ export class GroupDepthLimitError extends Exception {
649
+ static status = 422;
650
+ static code = 'E_AUTHZ_GROUP_DEPTH_LIMIT';
651
+ constructor(message, details) {
652
+ super(message);
653
+ Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
654
+ }
655
+ }
484
656
  /**
485
657
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
486
658
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -546,6 +718,46 @@ export class UnsupportedOperationError extends Exception {
546
718
  `removeDeny (hechos) y relate/unrelate/purge* (relaciones) admiten { transaction }.`;
547
719
  return error;
548
720
  }
721
+ /**
722
+ * **Los `list*` de relaciones no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez, decisión del dueño
723
+ * 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
724
+ * hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
725
+ * `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
726
+ * una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
727
+ * como métodos aparte (`*Page`, 2.6) con capacidad propia.
728
+ */
729
+ static listPage(operation, driver) {
730
+ const error = new UnsupportedOperationError('page', operation, driver);
731
+ error.message =
732
+ `${operation}: se pasó una página (page) y ${operation} no pagina en 2.5 — devuelve el conjunto COMPLETO (o ` +
733
+ `señala truncated), nunca un cursor. No se ha llamado al backend ('${driver}'). Quita el argumento page; la ` +
734
+ `paginación de los listados llegará como métodos aparte (*Page) con capacidad propia.`;
735
+ return error;
736
+ }
737
+ /**
738
+ * **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
739
+ * devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
740
+ * llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
741
+ */
742
+ static enumerateCursor(operation, driver) {
743
+ const error = new UnsupportedOperationError('page.after', operation, driver);
744
+ error.message =
745
+ `${operation}: el driver '${driver}' no emite cursor (devuelve todas las tuplas en una página), así que un ` +
746
+ `page.after no salió de él y no se acepta. No se ha llamado al backend. Llama sin after.`;
747
+ return error;
748
+ }
749
+ }
750
+ /**
751
+ * **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
752
+ * `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
753
+ * `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
754
+ * pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
755
+ * `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
756
+ * encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
757
+ */
758
+ export class InvalidRelationPageError extends Exception {
759
+ static status = 422;
760
+ static code = 'E_AUTHZ_INVALID_PAGE';
549
761
  }
550
762
  /**
551
763
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -876,4 +1088,74 @@ export class RelationUsersetUnsupportedError extends Exception {
876
1088
  static status = 422;
877
1089
  static code = 'E_AUTHZ_RELATION_USERSET_UNSUPPORTED';
878
1090
  }
879
- //# sourceMappingURL=errors.js.map
1091
+ /**
1092
+ * **Deny sobre algo que no se puede denegar** (plan 2.5 · lote D · D1; panel
1093
+ * tenencia ronda 2, D10/D-17). `deny(…)` sobre un tipo cuya spec no declara
1094
+ * `deny: 'write'` (fase 2 del opt-in: sin opt-in, o en `deny: 'read'`, la fase
1095
+ * en la que el modelo y las lecturas ya honran el deny pero aún no se aceptan
1096
+ * escrituras), sobre el tipo built-in `group`, o con el comodín `*` como
1097
+ * sujeto. 422 ANTES de tocar el driver (cero llamadas, cero eventos): una
1098
+ * pregunta inválida, no una caída.
1099
+ */
1100
+ export class RelationNotDenyableError extends Exception {
1101
+ static status = 422;
1102
+ static code = 'E_AUTHZ_RELATION_NOT_DENYABLE';
1103
+ }
1104
+ /**
1105
+ * **`expiresAt` en un deny** (plan 2.5 · lote D · D3, decisión del dueño
1106
+ * 2026-10-14): el deny de relaciones NO caduca en v1. Medido (M5, OpenFGA
1107
+ * v1.19.0): FGA rechaza delete+write de la misma clave en un `Write` (400
1108
+ * `cannot_allow_duplicate_tuples_in_one_request`) y `on_duplicate: ignore` con
1109
+ * otra condición es 409, así que cambiar la caducidad de un deny VIVO exige dos
1110
+ * `Write` con una ventana en la que deja de bloquear — y si el segundo cae, el
1111
+ * deny DESAPARECE. 422 ANTES de tocar el driver, con la razón medida.
1112
+ */
1113
+ export class RelationDenyExpiryError extends Exception {
1114
+ static status = 422;
1115
+ static code = 'E_AUTHZ_DENY_EXPIRY_UNSUPPORTED';
1116
+ }
1117
+ /**
1118
+ * **`membersOf` de `database` sobre un objeto con deny y más candidatos que la cota** (lote D · cierre 1 de D1, 🟡 4 del
1119
+ * auditor). Con una fila `denied_*` vigente en el objeto, cada candidato se juzga con un `check` (el corte de camino);
1120
+ * sin cota eran N consultas en serie —una amplificación barata para quien puede escribir un deny—. Pasada
1121
+ * `RELATIONS_MEMBERS_DENY_CHECK_MAX` (1 000) ⇒ 500, NUNCA una lista parcial; el deadline TOTAL del bucle es 503
1122
+ * `E_AUTHZ_BACKEND_TIMEOUT`. **500 y no 422** (decisión 1 del cierre 1, ratificada en el cierre 2): no es una pregunta
1123
+ * inválida del llamante —la misma pregunta es válida con menos candidatos— sino una cota del DESPLIEGUE (precedente
1124
+ * `E_AUTHZ_TOO_MANY_LOCAL_ROLES`); y no 503 porque no es reintentable. `authorizedScopes` (`E_AUTHZ_TOO_MANY_SCOPES`,
1125
+ * 422) acota una enumeración que el llamante pide con su `maxScopes`; aquí no hay cota del llamante.
1126
+ */
1127
+ export class MembersOfDenyLimitError extends Exception {
1128
+ static status = 500;
1129
+ static code = 'E_AUTHZ_MEMBERS_DENY_LIMIT';
1130
+ }
1131
+ /**
1132
+ * **`authz:relations:reconcile` se NIEGA a migrar denies** (plan 2.5 · lote D · D2; auditor R2-5, tester S6, juez ronda 2
1133
+ * fila D). El origen trae al menos un deny vivo y el destino no puede escribirlo: no declara `relationDenies` (o no trae
1134
+ * `deny`/`removeDeny`/`denyPhase`), el tipo no está en la fase `'write'` del opt-in EN EL DESTINO (su config, no la spec
1135
+ * persistida), el par tipo/relación no está declarado allí, su modelo fijado no declara `denied_<rel>` (D-31), o el
1136
+ * origen enumera un deny SIN marcar `kind` (`unmarked`). Se decide en la FASE DE PLAN, antes de la primera escritura en
1137
+ * cualquier partición: **CERO escrituras**, y **jamás `skipped`** — perder un deny no es una pérdida que se pueda declarar
1138
+ * (escribir los allows sin él concede lo que el origen niega). `--dry-run` no lanza: lo marca en `report.denyRefused` y es
1139
+ * deriva. Lleva `refusals` (una entrada por tipo/relación y motivo). 500 y no 422: no es una pregunta del llamante sino
1140
+ * el estado del despliegue (precedente `E_AUTHZ_MASS_RECONCILE_REFUSED`).
1141
+ */
1142
+ export class DenyMigrationRefusedError extends Exception {
1143
+ static status = 500;
1144
+ static code = 'E_AUTHZ_DENY_MIGRATION_REFUSED';
1145
+ refusals = [];
1146
+ }
1147
+ /**
1148
+ * **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
1149
+ * decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
1150
+ * vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
1151
+ * ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
1152
+ * línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
1153
+ * levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
1154
+ * precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
1155
+ * estado del despliegue.
1156
+ */
1157
+ export class DenyReachRefusedError extends Exception {
1158
+ static status = 500;
1159
+ static code = 'E_AUTHZ_DENY_REACH_REFUSED';
1160
+ findings = [];
1161
+ }