@jantstack/adonis-authz 2.5.0-alpha.3 → 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 (277) hide show
  1. package/README.md +414 -35
  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 +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 +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 +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 +7 -5
  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 +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 +11 -5
  45. package/build/src/drivers/database_relations_driver.js +90 -18
  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 +121 -1
  53. package/build/src/errors.js +153 -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 +156 -28
  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 +0 -1
  109. package/build/src/scope_outbox.js +0 -1
  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 +7 -1
  119. package/build/src/testing/contract.js +77 -1
  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 +89 -14
  139. package/build/src/types.js +4 -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
  /**
@@ -362,6 +401,14 @@ export declare class RenewalUnsafeError extends Exception {
362
401
  static status: number;
363
402
  static code: string;
364
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;
365
412
  }
366
413
  /**
367
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
@@ -495,6 +542,31 @@ export declare class ScopeDepthLimitError extends Exception {
495
542
  server: string;
496
543
  });
497
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
+ }
498
570
  /**
499
571
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
500
572
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -531,6 +603,33 @@ export declare class UnsupportedOperationError extends Exception {
531
603
  * entre procesos (invariante 14); moverla al commit del consumidor lo anula.
532
604
  */
533
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;
534
633
  }
535
634
  /**
536
635
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -944,4 +1043,25 @@ export declare class DenyMigrationRefusedError extends Exception {
944
1043
  message: string;
945
1044
  }>;
946
1045
  }
947
- //# sourceMappingURL=errors.d.ts.map
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
@@ -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;
@@ -428,6 +495,19 @@ export class RenewalUnsafeError extends Exception {
428
495
  'membresía ACEPTANDO la ventana — mientras dura, el sujeto no recibe el deny del grupo y concede lo que otro camino le ' +
429
496
  'dé y ese deny corte.');
430
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
+ }
431
511
  }
432
512
  /**
433
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
@@ -555,6 +635,24 @@ export class ScopeDepthLimitError extends Exception {
555
635
  Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
556
636
  }
557
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
+ }
558
656
  /**
559
657
  * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
560
658
  * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
@@ -620,6 +718,46 @@ export class UnsupportedOperationError extends Exception {
620
718
  `removeDeny (hechos) y relate/unrelate/purge* (relaciones) admiten { transaction }.`;
621
719
  return error;
622
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';
623
761
  }
624
762
  /**
625
763
  * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
@@ -1006,4 +1144,18 @@ export class DenyMigrationRefusedError extends Exception {
1006
1144
  static code = 'E_AUTHZ_DENY_MIGRATION_REFUSED';
1007
1145
  refusals = [];
1008
1146
  }
1009
- //# sourceMappingURL=errors.js.map
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
+ }
@@ -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
@@ -173,6 +173,8 @@ export declare function scopeSpellings(scope: ScopeRef): ScopeRef[];
173
173
  export declare function isDashlessUuid(value: unknown): value is string;
174
174
  /** La forma 8-4-4-4-12 de un uuid de 32 hex (la ortografía CANÓNICA). */
175
175
  export declare function dashedUuid(dashless: string): string;
176
+ /** ¿Es un uuid en su ortografía CANÓNICA (8-4-4-4-12, minúsculas)? (7.4: la forma del cursor de `enumerate*` de `database`.) */
177
+ export declare function isCanonicalUuid(value: unknown): value is string;
176
178
  /**
177
179
  * **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
178
180
  * auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
@@ -336,4 +338,3 @@ export interface IdentityParts {
336
338
  * cuesta una consulta ni deja rastro.
337
339
  */
338
340
  export declare function assertIdentity(parts: IdentityParts): void;
339
- //# sourceMappingURL=identity.d.ts.map
@@ -306,6 +306,10 @@ export function dashedUuid(dashless) {
306
306
  /* ── Partición de RELACIONES (D4, plan 2.5 · lote A) ───────────────────── */
307
307
  /** La ortografía CANÓNICA de un uuid: exactamente 8-4-4-4-12, minúsculas. */
308
308
  const CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
309
+ /** ¿Es un uuid en su ortografía CANÓNICA (8-4-4-4-12, minúsculas)? (7.4: la forma del cursor de `enumerate*` de `database`.) */
310
+ export function isCanonicalUuid(value) {
311
+ return typeof value === 'string' && CANONICAL_UUID.test(value);
312
+ }
309
313
  /**
310
314
  * **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
311
315
  * auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
@@ -606,4 +610,3 @@ export function assertIdentity(parts) {
606
610
  if ('expiresAt' in parts)
607
611
  assertExpiresAt(parts.expiresAt);
608
612
  }
609
- //# sourceMappingURL=identity.js.map
@@ -787,4 +787,3 @@ export declare class AuthorizationManager {
787
787
  maxScopes?: number;
788
788
  }): Promise<ScopeRef[]>;
789
789
  }
790
- //# sourceMappingURL=manager.d.ts.map