@jantstack/adonis-authz 2.5.0-alpha.3 → 2.5.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +437 -48
  2. package/build/commands/authz_catalog_diff.d.ts +0 -1
  3. package/build/commands/authz_catalog_diff.js +0 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +17 -3
  5. package/build/commands/authz_catalog_prune_orphans.js +28 -6
  6. package/build/commands/authz_catalog_sync.d.ts +0 -1
  7. package/build/commands/authz_catalog_sync.js +0 -1
  8. package/build/commands/authz_freeze.d.ts +0 -1
  9. package/build/commands/authz_freeze.js +0 -1
  10. package/build/commands/authz_reconcile.d.ts +0 -1
  11. package/build/commands/authz_reconcile.js +0 -1
  12. package/build/commands/authz_relations_reconcile.d.ts +0 -1
  13. package/build/commands/authz_relations_reconcile.js +56 -8
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +54 -6
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +16 -3
  18. package/build/commands/authz_unfreeze.d.ts +0 -1
  19. package/build/commands/authz_unfreeze.js +0 -1
  20. package/build/commands/main.d.ts +0 -1
  21. package/build/commands/main.js +0 -1
  22. package/build/commands/openfga_provision.d.ts +10 -1
  23. package/build/commands/openfga_provision.js +43 -3
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +9 -7
  27. package/build/index.js +10 -3
  28. package/build/providers/authz_provider.d.ts +0 -1
  29. package/build/providers/authz_provider.js +0 -1
  30. package/build/services/main.d.ts +0 -1
  31. package/build/services/main.js +0 -1
  32. package/build/services/relations.d.ts +0 -1
  33. package/build/services/relations.js +0 -1
  34. package/build/src/catalog/catalog.d.ts +0 -1
  35. package/build/src/catalog/catalog.js +0 -1
  36. package/build/src/catalog/catalog_cache.d.ts +13 -1
  37. package/build/src/catalog/catalog_cache.js +27 -2
  38. package/build/src/clock.d.ts +0 -1
  39. package/build/src/clock.js +0 -1
  40. package/build/src/define_config.d.ts +0 -1
  41. package/build/src/define_config.js +24 -1
  42. package/build/src/drivers/database_driver.d.ts +60 -3
  43. package/build/src/drivers/database_driver.js +160 -25
  44. package/build/src/drivers/database_relations_driver.d.ts +11 -5
  45. package/build/src/drivers/database_relations_driver.js +115 -33
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +882 -199
  48. package/build/src/drivers/openfga_facts.d.ts +91 -1
  49. package/build/src/drivers/openfga_facts.js +277 -3
  50. package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
  51. package/build/src/drivers/openfga_relations_driver.js +422 -228
  52. package/build/src/errors.d.ts +160 -1
  53. package/build/src/errors.js +161 -1
  54. package/build/src/expiry.d.ts +51 -2
  55. package/build/src/expiry.js +96 -2
  56. package/build/src/freeze.d.ts +0 -1
  57. package/build/src/freeze.js +0 -1
  58. package/build/src/hierarchical_resolver.d.ts +0 -1
  59. package/build/src/hierarchical_resolver.js +0 -1
  60. package/build/src/http/app_access_middleware.d.ts +0 -1
  61. package/build/src/http/app_access_middleware.js +0 -1
  62. package/build/src/http/resource_access_middleware.d.ts +0 -1
  63. package/build/src/http/resource_access_middleware.js +0 -1
  64. package/build/src/identity.d.ts +9 -2
  65. package/build/src/identity.js +33 -1
  66. package/build/src/manager.d.ts +24 -1
  67. package/build/src/manager.js +381 -85
  68. package/build/src/memoize_ancestors.d.ts +0 -1
  69. package/build/src/memoize_ancestors.js +0 -1
  70. package/build/src/models/authz_assignment.d.ts +5 -6
  71. package/build/src/models/authz_assignment.js +0 -1
  72. package/build/src/models/authz_deny.d.ts +5 -6
  73. package/build/src/models/authz_deny.js +0 -1
  74. package/build/src/models/authz_permission.d.ts +5 -6
  75. package/build/src/models/authz_permission.js +0 -1
  76. package/build/src/models/authz_role.d.ts +5 -6
  77. package/build/src/models/authz_role.js +0 -1
  78. package/build/src/models/authz_role_permission.d.ts +5 -6
  79. package/build/src/models/authz_role_permission.js +0 -1
  80. package/build/src/openfga.d.ts +2 -3
  81. package/build/src/openfga.js +4 -2
  82. package/build/src/reconcile.d.ts +0 -1
  83. package/build/src/reconcile.js +0 -1
  84. package/build/src/relation_partition_trigger.d.ts +0 -1
  85. package/build/src/relation_partition_trigger.js +0 -1
  86. package/build/src/relations/check_many.d.ts +0 -1
  87. package/build/src/relations/check_many.js +0 -1
  88. package/build/src/relations/define_relations_config.d.ts +25 -3
  89. package/build/src/relations/define_relations_config.js +8 -3
  90. package/build/src/relations/deny_reach.d.ts +102 -0
  91. package/build/src/relations/deny_reach.js +230 -0
  92. package/build/src/relations/group_depth.d.ts +215 -0
  93. package/build/src/relations/group_depth.js +453 -0
  94. package/build/src/relations/manager.d.ts +14 -7
  95. package/build/src/relations/manager.js +60 -15
  96. package/build/src/relations/page.d.ts +19 -0
  97. package/build/src/relations/page.js +74 -0
  98. package/build/src/relations/reconcile.d.ts +58 -4
  99. package/build/src/relations/reconcile.js +266 -12
  100. package/build/src/relations/tuple_key.d.ts +0 -1
  101. package/build/src/relations/tuple_key.js +0 -1
  102. package/build/src/relations/tuple_order.d.ts +5 -4
  103. package/build/src/relations/tuple_order.js +5 -4
  104. package/build/src/relations_config_store.d.ts +57 -2
  105. package/build/src/relations_config_store.js +155 -5
  106. package/build/src/relations_reconcile.d.ts +0 -1
  107. package/build/src/relations_reconcile.js +0 -1
  108. package/build/src/scope_outbox.d.ts +36 -3
  109. package/build/src/scope_outbox.js +78 -18
  110. package/build/src/shared/backend_guard.d.ts +52 -1
  111. package/build/src/shared/backend_guard.js +164 -2
  112. package/build/src/shared/sql_expiry.d.ts +0 -1
  113. package/build/src/shared/sql_expiry.js +0 -1
  114. package/build/src/shared/transaction_guard.d.ts +0 -1
  115. package/build/src/shared/transaction_guard.js +0 -1
  116. package/build/src/sql_descendants.d.ts +0 -1
  117. package/build/src/sql_descendants.js +0 -1
  118. package/build/src/testing/contract.d.ts +26 -3
  119. package/build/src/testing/contract.js +258 -20
  120. package/build/src/testing/main.d.ts +0 -1
  121. package/build/src/testing/main.js +0 -1
  122. package/build/src/testing/migration_contract.d.ts +0 -1
  123. package/build/src/testing/migration_contract.js +0 -1
  124. package/build/src/testing/relations_contract.d.ts +7 -1
  125. package/build/src/testing/relations_contract.js +174 -7
  126. package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
  127. package/build/src/testing/relations_reconcile_contract.js +0 -1
  128. package/build/src/testing/scope_outbox_contract.d.ts +0 -1
  129. package/build/src/testing/scope_outbox_contract.js +0 -1
  130. package/build/src/testing/scope_tree.d.ts +0 -1
  131. package/build/src/testing/scope_tree.js +0 -1
  132. package/build/src/testing/test_clock.d.ts +0 -1
  133. package/build/src/testing/test_clock.js +0 -1
  134. package/build/src/traits/authz_scopes.d.ts +0 -1
  135. package/build/src/traits/authz_scopes.js +0 -1
  136. package/build/src/traits/has_uuid.d.ts +5 -6
  137. package/build/src/traits/has_uuid.js +0 -1
  138. package/build/src/types.d.ts +138 -14
  139. package/build/src/types.js +6 -1
  140. package/build/stubs/config/authorization.stub +22 -6
  141. package/build/stubs/main.d.ts +0 -1
  142. package/build/stubs/main.js +0 -1
  143. package/package.json +4 -3
  144. package/build/commands/authz_catalog_diff.d.ts.map +0 -1
  145. package/build/commands/authz_catalog_diff.js.map +0 -1
  146. package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
  147. package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
  148. package/build/commands/authz_catalog_sync.d.ts.map +0 -1
  149. package/build/commands/authz_catalog_sync.js.map +0 -1
  150. package/build/commands/authz_freeze.d.ts.map +0 -1
  151. package/build/commands/authz_freeze.js.map +0 -1
  152. package/build/commands/authz_reconcile.d.ts.map +0 -1
  153. package/build/commands/authz_reconcile.js.map +0 -1
  154. package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
  155. package/build/commands/authz_relations_reconcile.js.map +0 -1
  156. package/build/commands/authz_scopes_relay.d.ts.map +0 -1
  157. package/build/commands/authz_scopes_relay.js.map +0 -1
  158. package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
  159. package/build/commands/authz_scopes_requeue.js.map +0 -1
  160. package/build/commands/authz_unfreeze.d.ts.map +0 -1
  161. package/build/commands/authz_unfreeze.js.map +0 -1
  162. package/build/commands/main.d.ts.map +0 -1
  163. package/build/commands/main.js.map +0 -1
  164. package/build/commands/openfga_provision.d.ts.map +0 -1
  165. package/build/commands/openfga_provision.js.map +0 -1
  166. package/build/configure.d.ts.map +0 -1
  167. package/build/configure.js.map +0 -1
  168. package/build/index.d.ts.map +0 -1
  169. package/build/index.js.map +0 -1
  170. package/build/providers/authz_provider.d.ts.map +0 -1
  171. package/build/providers/authz_provider.js.map +0 -1
  172. package/build/services/main.d.ts.map +0 -1
  173. package/build/services/main.js.map +0 -1
  174. package/build/services/relations.d.ts.map +0 -1
  175. package/build/services/relations.js.map +0 -1
  176. package/build/src/catalog/catalog.d.ts.map +0 -1
  177. package/build/src/catalog/catalog.js.map +0 -1
  178. package/build/src/catalog/catalog_cache.d.ts.map +0 -1
  179. package/build/src/catalog/catalog_cache.js.map +0 -1
  180. package/build/src/clock.d.ts.map +0 -1
  181. package/build/src/clock.js.map +0 -1
  182. package/build/src/define_config.d.ts.map +0 -1
  183. package/build/src/define_config.js.map +0 -1
  184. package/build/src/drivers/database_driver.d.ts.map +0 -1
  185. package/build/src/drivers/database_driver.js.map +0 -1
  186. package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
  187. package/build/src/drivers/database_relations_driver.js.map +0 -1
  188. package/build/src/drivers/openfga_driver.d.ts.map +0 -1
  189. package/build/src/drivers/openfga_driver.js.map +0 -1
  190. package/build/src/drivers/openfga_facts.d.ts.map +0 -1
  191. package/build/src/drivers/openfga_facts.js.map +0 -1
  192. package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
  193. package/build/src/drivers/openfga_relations_driver.js.map +0 -1
  194. package/build/src/errors.d.ts.map +0 -1
  195. package/build/src/errors.js.map +0 -1
  196. package/build/src/expiry.d.ts.map +0 -1
  197. package/build/src/expiry.js.map +0 -1
  198. package/build/src/freeze.d.ts.map +0 -1
  199. package/build/src/freeze.js.map +0 -1
  200. package/build/src/hierarchical_resolver.d.ts.map +0 -1
  201. package/build/src/hierarchical_resolver.js.map +0 -1
  202. package/build/src/http/app_access_middleware.d.ts.map +0 -1
  203. package/build/src/http/app_access_middleware.js.map +0 -1
  204. package/build/src/http/resource_access_middleware.d.ts.map +0 -1
  205. package/build/src/http/resource_access_middleware.js.map +0 -1
  206. package/build/src/identity.d.ts.map +0 -1
  207. package/build/src/identity.js.map +0 -1
  208. package/build/src/manager.d.ts.map +0 -1
  209. package/build/src/manager.js.map +0 -1
  210. package/build/src/memoize_ancestors.d.ts.map +0 -1
  211. package/build/src/memoize_ancestors.js.map +0 -1
  212. package/build/src/models/authz_assignment.d.ts.map +0 -1
  213. package/build/src/models/authz_assignment.js.map +0 -1
  214. package/build/src/models/authz_deny.d.ts.map +0 -1
  215. package/build/src/models/authz_deny.js.map +0 -1
  216. package/build/src/models/authz_permission.d.ts.map +0 -1
  217. package/build/src/models/authz_permission.js.map +0 -1
  218. package/build/src/models/authz_role.d.ts.map +0 -1
  219. package/build/src/models/authz_role.js.map +0 -1
  220. package/build/src/models/authz_role_permission.d.ts.map +0 -1
  221. package/build/src/models/authz_role_permission.js.map +0 -1
  222. package/build/src/openfga.d.ts.map +0 -1
  223. package/build/src/openfga.js.map +0 -1
  224. package/build/src/reconcile.d.ts.map +0 -1
  225. package/build/src/reconcile.js.map +0 -1
  226. package/build/src/relation_partition_trigger.d.ts.map +0 -1
  227. package/build/src/relation_partition_trigger.js.map +0 -1
  228. package/build/src/relations/check_many.d.ts.map +0 -1
  229. package/build/src/relations/check_many.js.map +0 -1
  230. package/build/src/relations/define_relations_config.d.ts.map +0 -1
  231. package/build/src/relations/define_relations_config.js.map +0 -1
  232. package/build/src/relations/manager.d.ts.map +0 -1
  233. package/build/src/relations/manager.js.map +0 -1
  234. package/build/src/relations/reconcile.d.ts.map +0 -1
  235. package/build/src/relations/reconcile.js.map +0 -1
  236. package/build/src/relations/tuple_key.d.ts.map +0 -1
  237. package/build/src/relations/tuple_key.js.map +0 -1
  238. package/build/src/relations/tuple_order.d.ts.map +0 -1
  239. package/build/src/relations/tuple_order.js.map +0 -1
  240. package/build/src/relations_config_store.d.ts.map +0 -1
  241. package/build/src/relations_config_store.js.map +0 -1
  242. package/build/src/relations_reconcile.d.ts.map +0 -1
  243. package/build/src/relations_reconcile.js.map +0 -1
  244. package/build/src/scope_outbox.d.ts.map +0 -1
  245. package/build/src/scope_outbox.js.map +0 -1
  246. package/build/src/shared/backend_guard.d.ts.map +0 -1
  247. package/build/src/shared/backend_guard.js.map +0 -1
  248. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  249. package/build/src/shared/sql_expiry.js.map +0 -1
  250. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  251. package/build/src/shared/transaction_guard.js.map +0 -1
  252. package/build/src/sql_descendants.d.ts.map +0 -1
  253. package/build/src/sql_descendants.js.map +0 -1
  254. package/build/src/testing/contract.d.ts.map +0 -1
  255. package/build/src/testing/contract.js.map +0 -1
  256. package/build/src/testing/main.d.ts.map +0 -1
  257. package/build/src/testing/main.js.map +0 -1
  258. package/build/src/testing/migration_contract.d.ts.map +0 -1
  259. package/build/src/testing/migration_contract.js.map +0 -1
  260. package/build/src/testing/relations_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_contract.js.map +0 -1
  262. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  263. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  264. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  265. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  266. package/build/src/testing/scope_tree.d.ts.map +0 -1
  267. package/build/src/testing/scope_tree.js.map +0 -1
  268. package/build/src/testing/test_clock.d.ts.map +0 -1
  269. package/build/src/testing/test_clock.js.map +0 -1
  270. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  271. package/build/src/traits/authz_scopes.js.map +0 -1
  272. package/build/src/traits/has_uuid.d.ts.map +0 -1
  273. package/build/src/traits/has_uuid.js.map +0 -1
  274. package/build/src/types.d.ts.map +0 -1
  275. package/build/src/types.js.map +0 -1
  276. package/build/stubs/main.d.ts.map +0 -1
  277. package/build/stubs/main.js.map +0 -1
@@ -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
@@ -135,6 +147,33 @@ export declare function isThenable(value: unknown): boolean;
135
147
  export declare function markPartialWrite<E extends object>(error: E): E & {
136
148
  partialWrite: true;
137
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;
138
177
  /** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
139
178
  export declare function isPartialWrite(error: unknown): boolean;
140
179
  /**
@@ -156,6 +195,24 @@ export declare class AuthorizationInternalError extends Exception {
156
195
  export declare class UnknownScopeError extends Exception {
157
196
  static status: number;
158
197
  static code: string;
198
+ /**
199
+ * **Para el OPERADOR, no para el tenant** (rotación 2 · cierre 2 · RC2-A): en `updateScopedRole`/`deleteScopedRole` el
200
+ * scope que no resuelve es el OWNER del rol (dormido), que el llamante no pasó y puede ser de otro tenant: el `message`
201
+ * nombra el rol y el owner va aquí, NO enumerable (mismo patrón que `NotWithinError.details`). Ausente en el resto de
202
+ * rutas, donde el scope desconocido es el que pasó el llamante y el `message` lo nombra.
203
+ */
204
+ readonly details: {
205
+ owner: {
206
+ type: string;
207
+ uuid: string | null;
208
+ };
209
+ } | undefined;
210
+ constructor(message?: string, details?: {
211
+ owner: {
212
+ type: string;
213
+ uuid: string | null;
214
+ };
215
+ });
159
216
  }
160
217
  /**
161
218
  * Se preguntó por un scope que no es la raíz y el driver no tiene resolutor
@@ -362,6 +419,14 @@ export declare class RenewalUnsafeError extends Exception {
362
419
  static status: number;
363
420
  static code: string;
364
421
  static carrier(membership: string, denied: string): RenewalUnsafeError;
422
+ /**
423
+ * **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
424
+ * del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
425
+ * (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
426
+ * (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
427
+ * con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
428
+ */
429
+ static foreignCondition(operation: string, tuple: string, detail: string): RenewalUnsafeError;
365
430
  }
366
431
  /**
367
432
  * **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
@@ -411,6 +476,27 @@ export declare class ActorRequiredError extends Exception {
411
476
  export declare class NotWithinError extends Exception {
412
477
  static status: number;
413
478
  static code: string;
479
+ /**
480
+ * **Para el OPERADOR, no para el tenant** (rotación 2 de M4 · R2-03, decisión del dueño 2026-10-22 (3)). Hasta
481
+ * 2.5.0-alpha.4 el `message` imprimía la cadena ENTERA del scope (`unit:<B1x> → unit:<B1> → organization:<org de B>
482
+ * → app`): el admin de A que conoce un uuid de B aprendía a qué tenant pertenece y su ruta, y un 422 es lo que un
483
+ * framework devuelve al cliente tal cual (la misma clase de fuga que 3E · Q2 cerró en `ROLE_NOT_VISIBLE`). Ahora el
484
+ * `message` nombra solo lo que el llamante ya tiene —el scope que pasó y su `within`— y la cadena va aquí, NO
485
+ * enumerable como `ScopeDepthLimitError.details` (alpha.8): `JSON.stringify(error)` —lo que un handler genérico
486
+ * manda— no la lleva, y el log del servidor la lee por `error.details.chain`. El CÓDIGO no cambia.
487
+ */
488
+ readonly details: {
489
+ chain: ReadonlyArray<{
490
+ type: string;
491
+ uuid: string | null;
492
+ }>;
493
+ } | undefined;
494
+ constructor(message: string, details?: {
495
+ chain: ReadonlyArray<{
496
+ type: string;
497
+ uuid: string | null;
498
+ }>;
499
+ });
414
500
  }
415
501
  /**
416
502
  * `config.requireWithin` (`true` o `'non-root'`) y una escritura llegó sin
@@ -495,6 +581,31 @@ export declare class ScopeDepthLimitError extends Exception {
495
581
  server: string;
496
582
  });
497
583
  }
584
+ /**
585
+ * **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
586
+ * 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
587
+ * LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
588
+ * un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
589
+ * 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
590
+ * `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
591
+ * backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
592
+ * en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
593
+ */
594
+ export declare class GroupDepthLimitError extends Exception {
595
+ static status: number;
596
+ static code: string;
597
+ readonly details: {
598
+ reason: 'too-deep' | 'cycle' | 'too-many-groups';
599
+ edge: string;
600
+ depth: number | null;
601
+ maxGroupDepth: number;
602
+ /** Solo con `too-many-groups`: qué cota se pasó (grupos, requests, tuplas leídas o el deadline total). */
603
+ budget?: 'nodes' | 'reads' | 'tuples' | 'deadline';
604
+ why: string;
605
+ server: string;
606
+ };
607
+ constructor(message: string, details: GroupDepthLimitError['details']);
608
+ }
498
609
  /**
499
610
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
500
611
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -531,6 +642,33 @@ export declare class UnsupportedOperationError extends Exception {
531
642
  * entre procesos (invariante 14); moverla al commit del consumidor lo anula.
532
643
  */
533
644
  static transactionalCatalog(operation: string): UnsupportedOperationError;
645
+ /**
646
+ * **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
647
+ * 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
648
+ * hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
649
+ * `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
650
+ * una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
651
+ * como métodos aparte (`*Page`, 2.6) con capacidad propia.
652
+ */
653
+ static listPage(operation: string, driver: string): UnsupportedOperationError;
654
+ /**
655
+ * **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
656
+ * devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
657
+ * llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
658
+ */
659
+ static enumerateCursor(operation: string, driver: string): UnsupportedOperationError;
660
+ }
661
+ /**
662
+ * **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
663
+ * `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
664
+ * `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
665
+ * pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
666
+ * `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
667
+ * encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
668
+ */
669
+ export declare class InvalidRelationPageError extends Exception {
670
+ static status: number;
671
+ static code: string;
534
672
  }
535
673
  /**
536
674
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -944,4 +1082,25 @@ export declare class DenyMigrationRefusedError extends Exception {
944
1082
  message: string;
945
1083
  }>;
946
1084
  }
947
- //# sourceMappingURL=errors.d.ts.map
1085
+ /**
1086
+ * **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
1087
+ * decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
1088
+ * vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
1089
+ * ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
1090
+ * línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
1091
+ * levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
1092
+ * precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
1093
+ * estado del despliegue.
1094
+ */
1095
+ export declare class DenyReachRefusedError extends Exception {
1096
+ static status: number;
1097
+ static code: string;
1098
+ findings: ReadonlyArray<{
1099
+ objectType: string;
1100
+ relation: string;
1101
+ reason: string;
1102
+ count: number;
1103
+ lost: string[];
1104
+ recipe: string;
1105
+ }>;
1106
+ }
@@ -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
@@ -177,6 +198,52 @@ export function markPartialWrite(error) {
177
198
  }
178
199
  return error;
179
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
+ }
180
247
  /** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
181
248
  export function isPartialWrite(error) {
182
249
  return error !== null && typeof error === 'object' && error.partialWrite === true;
@@ -200,6 +267,10 @@ export class AuthorizationInternalError extends Exception {
200
267
  export class UnknownScopeError extends Exception {
201
268
  static status = 422;
202
269
  static code = 'E_AUTHZ_UNKNOWN_SCOPE';
270
+ constructor(message, details) {
271
+ super(message);
272
+ Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
273
+ }
203
274
  }
204
275
  /**
205
276
  * Se preguntó por un scope que no es la raíz y el driver no tiene resolutor
@@ -428,6 +499,19 @@ export class RenewalUnsafeError extends Exception {
428
499
  'membresía ACEPTANDO la ventana — mientras dura, el sujeto no recibe el deny del grupo y concede lo que otro camino le ' +
429
500
  'dé y ese deny corte.');
430
501
  }
502
+ /**
503
+ * **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
504
+ * del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
505
+ * (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
506
+ * (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
507
+ * con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
508
+ */
509
+ static foreignCondition(operation, tuple, detail) {
510
+ return new RenewalUnsafeError(`${operation}: la tupla ${tuple} ya existe en el store con una condición que este paquete no escribe ni sabe leer ` +
511
+ `(${detail}). No se reescribe a ciegas: el paquete no sabe qué concede (la puede haber escrito otra herramienta, o ` +
512
+ 'un modelo anterior). No se ha escrito nada. Receta: revísala y bórrala a mano (SDK/CLI de OpenFGA, o unrelate/' +
513
+ 'revoke por el paquete) y repite.');
514
+ }
431
515
  }
432
516
  /**
433
517
  * **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
@@ -485,6 +569,10 @@ export class ActorRequiredError extends Exception {
485
569
  export class NotWithinError extends Exception {
486
570
  static status = 422;
487
571
  static code = 'E_AUTHZ_NOT_WITHIN';
572
+ constructor(message, details) {
573
+ super(message);
574
+ Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
575
+ }
488
576
  }
489
577
  /**
490
578
  * `config.requireWithin` (`true` o `'non-root'`) y una escritura llegó sin
@@ -555,6 +643,24 @@ export class ScopeDepthLimitError extends Exception {
555
643
  Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
556
644
  }
557
645
  }
646
+ /**
647
+ * **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
648
+ * 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
649
+ * LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
650
+ * un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
651
+ * 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
652
+ * `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
653
+ * backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
654
+ * en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
655
+ */
656
+ export class GroupDepthLimitError extends Exception {
657
+ static status = 422;
658
+ static code = 'E_AUTHZ_GROUP_DEPTH_LIMIT';
659
+ constructor(message, details) {
660
+ super(message);
661
+ Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
662
+ }
663
+ }
558
664
  /**
559
665
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
560
666
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -620,6 +726,46 @@ export class UnsupportedOperationError extends Exception {
620
726
  `removeDeny (hechos) y relate/unrelate/purge* (relaciones) admiten { transaction }.`;
621
727
  return error;
622
728
  }
729
+ /**
730
+ * **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
731
+ * 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
732
+ * hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
733
+ * `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
734
+ * una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
735
+ * como métodos aparte (`*Page`, 2.6) con capacidad propia.
736
+ */
737
+ static listPage(operation, driver) {
738
+ const error = new UnsupportedOperationError('page', operation, driver);
739
+ error.message =
740
+ `${operation}: se pasó una página (page) y ${operation} no pagina en 2.5 — devuelve el conjunto COMPLETO (o ` +
741
+ `señala truncated), nunca un cursor. No se ha llamado al backend ('${driver}'). Quita el argumento page; la ` +
742
+ `paginación de los listados llegará como métodos aparte (*Page) con capacidad propia.`;
743
+ return error;
744
+ }
745
+ /**
746
+ * **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
747
+ * devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
748
+ * llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
749
+ */
750
+ static enumerateCursor(operation, driver) {
751
+ const error = new UnsupportedOperationError('page.after', operation, driver);
752
+ error.message =
753
+ `${operation}: el driver '${driver}' no emite cursor (devuelve todas las tuplas en una página), así que un ` +
754
+ `page.after no salió de él y no se acepta. No se ha llamado al backend. Llama sin after.`;
755
+ return error;
756
+ }
757
+ }
758
+ /**
759
+ * **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
760
+ * `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
761
+ * `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
762
+ * pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
763
+ * `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
764
+ * encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
765
+ */
766
+ export class InvalidRelationPageError extends Exception {
767
+ static status = 422;
768
+ static code = 'E_AUTHZ_INVALID_PAGE';
623
769
  }
624
770
  /**
625
771
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -1006,4 +1152,18 @@ export class DenyMigrationRefusedError extends Exception {
1006
1152
  static code = 'E_AUTHZ_DENY_MIGRATION_REFUSED';
1007
1153
  refusals = [];
1008
1154
  }
1009
- //# sourceMappingURL=errors.js.map
1155
+ /**
1156
+ * **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
1157
+ * decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
1158
+ * vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
1159
+ * ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
1160
+ * línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
1161
+ * levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
1162
+ * precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
1163
+ * estado del despliegue.
1164
+ */
1165
+ export class DenyReachRefusedError extends Exception {
1166
+ static status = 500;
1167
+ static code = 'E_AUTHZ_DENY_REACH_REFUSED';
1168
+ findings = [];
1169
+ }
@@ -20,8 +20,57 @@ export declare function isActiveExpiry(expiresAt: Date | null, now?: Date): bool
20
20
  * una reescritura (que en openfga es delete+write con ventana de denegación).
21
21
  */
22
22
  export declare function sameInstant(a: Date | null | undefined, b: Date | null | undefined): boolean;
23
- /** Lo que devuelve el backend como caducidad (número, ISO, Date) → Date | null. */
23
+ /**
24
+ * Lo que devuelve el backend SQL como caducidad (número, ISO, Date) → Date | null. **Solo SQL** (`sql_expiry.ts`): una
25
+ * tupla de OpenFGA se lee con `fgaTupleExpiry` (L11), nunca con esto — aquí una cadena ilegible es `null` =
26
+ * PERMANENTE, que para una tupla del store era fail-open (C6-1/C6-2).
27
+ */
24
28
  export declare function toExpiryDate(value: unknown): Date | null;
25
29
  /** ¿El outcome de un grant cambió la caducidad de una asignación que ya existía? */
26
30
  export declare function expiryChanged(outcome: GrantOutcome): boolean;
27
- //# sourceMappingURL=expiry.d.ts.map
31
+ /**
32
+ * El nombre de la condición de caducidad que escribe el paquete (`current_time < valid_until`). Es la MISMA cadena que
33
+ * `FACTS_EXPIRY_CONDITION` del generador del modelo (`openfga_facts.ts` la re-exporta desde aquí: una sola fuente).
34
+ */
35
+ export declare const FGA_EXPIRY_CONDITION = "not_expired";
36
+ /**
37
+ * **Lo que dice la condición de una tupla LEÍDA de OpenFGA** (lote «antes de RC» · L11, decisión del dueño 2026-10-17
38
+ * (1); injerto 2 del juez; F0 · M-a(2)/M-b). Cuatro estados, y ninguno es «permanente» por defecto:
39
+ *
40
+ * - `none`: la tupla no lleva condición. Con el modelo GENERADO toda relación la admite así y el servidor la evalúa
41
+ * PERMANENTE (medido, F0 · M-a(2)); la puerta L11 garantiza que el modelo que sirve el driver es ése.
42
+ * - `expiry`: `not_expired` con el contexto EXACTAMENTE `{ valid_until }` y una marca RFC3339 de la gramática que
43
+ * acepta el servidor. `at` es el milisegundo SIGUIENTE a la fracción (techo al ms: el servidor compara en ns y el
44
+ * paquete manda `current_time` en ms, así que `now < at` ⇔ `ns(now) < trunc9(ns(valid_until))`; medido 58/58, el
45
+ * `floor` de `new Date()` fallaba 30/60 — F0 · M-b).
46
+ * - `foreign`: otra condición (`inverted`…), o `not_expired` con un contexto que trae MÁS que `valid_until` (un
47
+ * `current_time` congelado en la tupla gana al de la petición: medido por el auditor, H2). El paquete no la
48
+ * escribe y no sabe qué concede.
49
+ * - `unparseable`: `not_expired` sin contexto o sin `valid_until` (el servidor responde 400 al evaluarla), o una marca
50
+ * que el servidor acepta y este parser no reproduce. Hasta L11 eso era `null` = PERMANENTE (`toExpiryDate`).
51
+ *
52
+ * Qué hace cada consumidor con `foreign`/`unparseable` lo decide él, siempre fail-closed (tabla en el README): VIVA
53
+ * donde la tupla NIEGA o transporta un deny, AUSENTE donde CONCEDE, intocable en el destino de un reconcile, 409 con
54
+ * receta si una escritura del paquete tendría que reemplazarla, borrada igual en una purga.
55
+ */
56
+ export type FgaTupleExpiry = {
57
+ kind: 'none';
58
+ } | {
59
+ kind: 'expiry';
60
+ at: Date;
61
+ } | {
62
+ kind: 'foreign';
63
+ detail: string;
64
+ } | {
65
+ kind: 'unparseable';
66
+ detail: string;
67
+ };
68
+ export declare function fgaTupleExpiry(condition: unknown): FgaTupleExpiry;
69
+ /**
70
+ * **La gramática de marca de tiempo que acepta OpenFGA v1.19 en `valid_until`, en nanosegundos desde la época**
71
+ * (F0 · M-b, medido): `YYYY-MM-DDTHH:MM:SS[(.|,)d+](Z|±hh:mm)`, fracción de cualquier longitud TRUNCADA a 9 dígitos
72
+ * (sin redondeo), coma decimal admitida, desfase con `hh` de 0 a 24 y `mm` de 0 a 59 (`+24:00`/`-24:00`/`+24:01` se
73
+ * aceptan; `+99:00` no; 25–98 sin medir ⇒ aquí `null`, fail-closed), fecha y hora válidas del calendario gregoriano
74
+ * proléptico (año 0000–9999; `02-30`, `T24:00:00`, minuto/segundo 60 ⇒ `null`). Todo lo demás ⇒ `null`.
75
+ */
76
+ export declare function fgaTimestampNs(value: string): bigint | null;
@@ -32,7 +32,11 @@ export function sameInstant(a, b) {
32
32
  return false;
33
33
  return a.getTime() === b.getTime();
34
34
  }
35
- /** Lo que devuelve el backend como caducidad (número, ISO, Date) → Date | null. */
35
+ /**
36
+ * Lo que devuelve el backend SQL como caducidad (número, ISO, Date) → Date | null. **Solo SQL** (`sql_expiry.ts`): una
37
+ * tupla de OpenFGA se lee con `fgaTupleExpiry` (L11), nunca con esto — aquí una cadena ilegible es `null` =
38
+ * PERMANENTE, que para una tupla del store era fail-open (C6-1/C6-2).
39
+ */
36
40
  export function toExpiryDate(value) {
37
41
  if (value === null || value === undefined || value === '')
38
42
  return null;
@@ -47,4 +51,94 @@ export function expiryChanged(outcome) {
47
51
  outcome.previousExpiresAt !== undefined &&
48
52
  !sameInstant(outcome.previousExpiresAt, outcome.expiresAt));
49
53
  }
50
- //# sourceMappingURL=expiry.js.map
54
+ /* ── L11 · la caducidad de una TUPLA de OpenFGA, en UN sitio ───────────── */
55
+ /**
56
+ * El nombre de la condición de caducidad que escribe el paquete (`current_time < valid_until`). Es la MISMA cadena que
57
+ * `FACTS_EXPIRY_CONDITION` del generador del modelo (`openfga_facts.ts` la re-exporta desde aquí: una sola fuente).
58
+ */
59
+ export const FGA_EXPIRY_CONDITION = 'not_expired';
60
+ const NONE = Object.freeze({ kind: 'none' });
61
+ export function fgaTupleExpiry(condition) {
62
+ if (condition === undefined || condition === null)
63
+ return NONE;
64
+ if (typeof condition !== 'object' || Array.isArray(condition)) {
65
+ return { kind: 'unparseable', detail: `la condición no es un objeto (${JSON.stringify(condition)})` };
66
+ }
67
+ const { name, context } = condition;
68
+ if (typeof name !== 'string' || name === '') {
69
+ return { kind: 'unparseable', detail: `condición sin nombre (${JSON.stringify(condition)})` };
70
+ }
71
+ if (name !== FGA_EXPIRY_CONDITION)
72
+ return { kind: 'foreign', detail: `condición '${name}'` };
73
+ if (context === undefined || context === null || typeof context !== 'object' || Array.isArray(context)) {
74
+ return { kind: 'unparseable', detail: `${FGA_EXPIRY_CONDITION} sin contexto (sin valid_until el servidor responde 400 al evaluarla)` };
75
+ }
76
+ const keys = Object.keys(context);
77
+ if (!keys.includes('valid_until')) {
78
+ return { kind: 'unparseable', detail: `${FGA_EXPIRY_CONDITION} sin valid_until en el contexto (${keys.join(', ') || 'vacío'})` };
79
+ }
80
+ if (keys.length !== 1) {
81
+ return {
82
+ kind: 'foreign',
83
+ detail: `${FGA_EXPIRY_CONDITION} con contexto {${keys.sort().join(', ')}}: el paquete solo escribe {valid_until} (un current_time en la tupla gana al de la petición)`,
84
+ };
85
+ }
86
+ const raw = context.valid_until;
87
+ const ns = typeof raw === 'string' ? fgaTimestampNs(raw) : null;
88
+ if (ns === null)
89
+ return { kind: 'unparseable', detail: `valid_until ilegible (${JSON.stringify(raw)})` };
90
+ return { kind: 'expiry', at: new Date(Number(ceilDiv(ns, 1000000n))) };
91
+ }
92
+ /**
93
+ * **La gramática de marca de tiempo que acepta OpenFGA v1.19 en `valid_until`, en nanosegundos desde la época**
94
+ * (F0 · M-b, medido): `YYYY-MM-DDTHH:MM:SS[(.|,)d+](Z|±hh:mm)`, fracción de cualquier longitud TRUNCADA a 9 dígitos
95
+ * (sin redondeo), coma decimal admitida, desfase con `hh` de 0 a 24 y `mm` de 0 a 59 (`+24:00`/`-24:00`/`+24:01` se
96
+ * aceptan; `+99:00` no; 25–98 sin medir ⇒ aquí `null`, fail-closed), fecha y hora válidas del calendario gregoriano
97
+ * proléptico (año 0000–9999; `02-30`, `T24:00:00`, minuto/segundo 60 ⇒ `null`). Todo lo demás ⇒ `null`.
98
+ */
99
+ export function fgaTimestampNs(value) {
100
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:[.,](\d+))?(?:(Z)|([+-])(\d{2}):(\d{2}))$/.exec(value);
101
+ if (!match)
102
+ return null;
103
+ const [, ys, mos, ds, hs, mis, ss, fraction, zulu, sign, ohs, oms] = match;
104
+ const year = Number(ys);
105
+ const month = Number(mos);
106
+ const day = Number(ds);
107
+ const hour = Number(hs);
108
+ const minute = Number(mis);
109
+ const second = Number(ss);
110
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
111
+ return null;
112
+ if (hour > 23 || minute > 59 || second > 59)
113
+ return null;
114
+ let offsetSeconds = 0;
115
+ if (!zulu) {
116
+ const offsetHours = Number(ohs);
117
+ const offsetMinutes = Number(oms);
118
+ if (offsetHours > 24 || offsetMinutes > 59)
119
+ return null;
120
+ offsetSeconds = (sign === '-' ? -1 : 1) * (offsetHours * 3600 + offsetMinutes * 60);
121
+ }
122
+ const seconds = BigInt(daysFromCivil(year, month, day)) * 86400n + BigInt(hour * 3600 + minute * 60 + second - offsetSeconds);
123
+ const nanos = fraction ? BigInt(fraction.slice(0, 9).padEnd(9, '0')) : 0n;
124
+ return seconds * 1000000000n + nanos;
125
+ }
126
+ function daysInMonth(year, month) {
127
+ if (month === 2)
128
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
129
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
130
+ }
131
+ /** Días desde 1970-01-01 de una fecha del gregoriano proléptico (H. Hinnant, `days_from_civil`). */
132
+ function daysFromCivil(year, month, day) {
133
+ const y = month <= 2 ? year - 1 : year;
134
+ const era = Math.floor(y / 400);
135
+ const yoe = y - era * 400;
136
+ const doy = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
137
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
138
+ return era * 146_097 + doe - 719_468;
139
+ }
140
+ /** Techo de una división entera de BigInt (la de BigInt trunca hacia cero). */
141
+ function ceilDiv(n, d) {
142
+ const q = n / d;
143
+ return n % d !== 0n && n > 0n ? q + 1n : q;
144
+ }
@@ -197,4 +197,3 @@ export declare function releaseFreeze(token: FreezeToken, input: {
197
197
  export declare function isOperatorHolder(holder: string | null): boolean;
198
198
  /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
199
199
  export declare function freezeKindOf(holder: string | null): FreezeKind | 'unknown';
200
- //# sourceMappingURL=freeze.d.ts.map
@@ -303,4 +303,3 @@ export function freezeKindOf(holder) {
303
303
  const prefix = holder.slice(0, holder.indexOf(':'));
304
304
  return prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
305
305
  }
306
- //# sourceMappingURL=freeze.js.map
@@ -53,4 +53,3 @@ export interface HierarchicalResolverOptions {
53
53
  * ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
54
54
  */
55
55
  export declare function hierarchicalScopeResolver(options: HierarchicalResolverOptions): ScopeChainResolver;
56
- //# sourceMappingURL=hierarchical_resolver.d.ts.map
@@ -84,4 +84,3 @@ export function hierarchicalScopeResolver(options) {
84
84
  }
85
85
  };
86
86
  }
87
- //# sourceMappingURL=hierarchical_resolver.js.map
@@ -22,4 +22,3 @@ export interface AppAccessOptions {
22
22
  export default class AppAccessMiddleware {
23
23
  handle(ctx: HttpContext, next: NextFn, options: AppAccessOptions): Promise<any>;
24
24
  }
25
- //# sourceMappingURL=app_access_middleware.d.ts.map
@@ -41,4 +41,3 @@ export default class AppAccessMiddleware {
41
41
  return next();
42
42
  }
43
43
  }
44
- //# sourceMappingURL=app_access_middleware.js.map
@@ -102,4 +102,3 @@ export interface ResourceAccessOptions {
102
102
  export default class ResourceAccessMiddleware {
103
103
  handle(ctx: HttpContext, next: NextFn, options: ResourceAccessOptions): Promise<any>;
104
104
  }
105
- //# sourceMappingURL=resource_access_middleware.d.ts.map
@@ -78,4 +78,3 @@ export default class ResourceAccessMiddleware {
78
78
  return next();
79
79
  }
80
80
  }
81
- //# sourceMappingURL=resource_access_middleware.js.map