@jantstack/adonis-authz 2.5.0-alpha.2 → 2.5.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +743 -28
  2. package/build/commands/authz_catalog_diff.d.ts +0 -1
  3. package/build/commands/authz_catalog_diff.js +0 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +0 -1
  5. package/build/commands/authz_catalog_prune_orphans.js +0 -1
  6. package/build/commands/authz_catalog_sync.d.ts +0 -1
  7. package/build/commands/authz_catalog_sync.js +0 -1
  8. package/build/commands/authz_freeze.d.ts +0 -1
  9. package/build/commands/authz_freeze.js +0 -1
  10. package/build/commands/authz_reconcile.d.ts +0 -1
  11. package/build/commands/authz_reconcile.js +0 -1
  12. package/build/commands/authz_relations_reconcile.d.ts +6 -1
  13. package/build/commands/authz_relations_reconcile.js +163 -5
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +49 -5
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +14 -2
  18. package/build/commands/authz_unfreeze.d.ts +0 -1
  19. package/build/commands/authz_unfreeze.js +0 -1
  20. package/build/commands/main.d.ts +0 -1
  21. package/build/commands/main.js +0 -1
  22. package/build/commands/openfga_provision.d.ts +72 -1
  23. package/build/commands/openfga_provision.js +236 -4
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +11 -9
  27. package/build/index.js +12 -5
  28. package/build/providers/authz_provider.d.ts +0 -1
  29. package/build/providers/authz_provider.js +0 -1
  30. package/build/services/main.d.ts +0 -1
  31. package/build/services/main.js +0 -1
  32. package/build/services/relations.d.ts +0 -1
  33. package/build/services/relations.js +0 -1
  34. package/build/src/catalog/catalog.d.ts +0 -1
  35. package/build/src/catalog/catalog.js +0 -1
  36. package/build/src/catalog/catalog_cache.d.ts +13 -1
  37. package/build/src/catalog/catalog_cache.js +25 -1
  38. package/build/src/clock.d.ts +0 -1
  39. package/build/src/clock.js +0 -1
  40. package/build/src/define_config.d.ts +0 -1
  41. package/build/src/define_config.js +24 -1
  42. package/build/src/drivers/database_driver.d.ts +6 -1
  43. package/build/src/drivers/database_driver.js +22 -2
  44. package/build/src/drivers/database_relations_driver.d.ts +46 -5
  45. package/build/src/drivers/database_relations_driver.js +297 -59
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +980 -231
  48. package/build/src/drivers/openfga_facts.d.ts +112 -1
  49. package/build/src/drivers/openfga_facts.js +351 -11
  50. package/build/src/drivers/openfga_relations_driver.d.ts +109 -8
  51. package/build/src/drivers/openfga_relations_driver.js +817 -53
  52. package/build/src/errors.d.ts +233 -1
  53. package/build/src/errors.js +283 -1
  54. package/build/src/expiry.d.ts +51 -2
  55. package/build/src/expiry.js +96 -2
  56. package/build/src/freeze.d.ts +0 -1
  57. package/build/src/freeze.js +0 -1
  58. package/build/src/hierarchical_resolver.d.ts +0 -1
  59. package/build/src/hierarchical_resolver.js +0 -1
  60. package/build/src/http/app_access_middleware.d.ts +0 -1
  61. package/build/src/http/app_access_middleware.js +0 -1
  62. package/build/src/http/resource_access_middleware.d.ts +0 -1
  63. package/build/src/http/resource_access_middleware.js +0 -1
  64. package/build/src/identity.d.ts +2 -1
  65. package/build/src/identity.js +4 -1
  66. package/build/src/manager.d.ts +0 -1
  67. package/build/src/manager.js +164 -31
  68. package/build/src/memoize_ancestors.d.ts +0 -1
  69. package/build/src/memoize_ancestors.js +0 -1
  70. package/build/src/models/authz_assignment.d.ts +5 -6
  71. package/build/src/models/authz_assignment.js +0 -1
  72. package/build/src/models/authz_deny.d.ts +5 -6
  73. package/build/src/models/authz_deny.js +0 -1
  74. package/build/src/models/authz_permission.d.ts +5 -6
  75. package/build/src/models/authz_permission.js +0 -1
  76. package/build/src/models/authz_role.d.ts +5 -6
  77. package/build/src/models/authz_role.js +0 -1
  78. package/build/src/models/authz_role_permission.d.ts +5 -6
  79. package/build/src/models/authz_role_permission.js +0 -1
  80. package/build/src/openfga.d.ts +3 -4
  81. package/build/src/openfga.js +5 -3
  82. package/build/src/reconcile.d.ts +0 -1
  83. package/build/src/reconcile.js +0 -1
  84. package/build/src/relation_partition_trigger.d.ts +0 -1
  85. package/build/src/relation_partition_trigger.js +0 -1
  86. package/build/src/relations/check_many.d.ts +1 -2
  87. package/build/src/relations/check_many.js +6 -5
  88. package/build/src/relations/define_relations_config.d.ts +140 -5
  89. package/build/src/relations/define_relations_config.js +205 -6
  90. package/build/src/relations/deny_reach.d.ts +102 -0
  91. package/build/src/relations/deny_reach.js +230 -0
  92. package/build/src/relations/group_depth.d.ts +215 -0
  93. package/build/src/relations/group_depth.js +453 -0
  94. package/build/src/relations/manager.d.ts +46 -8
  95. package/build/src/relations/manager.js +178 -16
  96. package/build/src/relations/page.d.ts +19 -0
  97. package/build/src/relations/page.js +74 -0
  98. package/build/src/relations/reconcile.d.ts +274 -2
  99. package/build/src/relations/reconcile.js +1006 -162
  100. package/build/src/relations/tuple_key.d.ts +0 -1
  101. package/build/src/relations/tuple_key.js +0 -1
  102. package/build/src/relations/tuple_order.d.ts +127 -0
  103. package/build/src/relations/tuple_order.js +599 -0
  104. package/build/src/relations_config_store.d.ts +57 -2
  105. package/build/src/relations_config_store.js +155 -5
  106. package/build/src/relations_reconcile.d.ts +0 -1
  107. package/build/src/relations_reconcile.js +0 -1
  108. package/build/src/scope_outbox.d.ts +0 -1
  109. package/build/src/scope_outbox.js +0 -1
  110. package/build/src/shared/backend_guard.d.ts +60 -1
  111. package/build/src/shared/backend_guard.js +179 -2
  112. package/build/src/shared/sql_expiry.d.ts +0 -1
  113. package/build/src/shared/sql_expiry.js +0 -1
  114. package/build/src/shared/transaction_guard.d.ts +0 -1
  115. package/build/src/shared/transaction_guard.js +0 -1
  116. package/build/src/sql_descendants.d.ts +0 -1
  117. package/build/src/sql_descendants.js +0 -1
  118. package/build/src/testing/contract.d.ts +7 -1
  119. package/build/src/testing/contract.js +77 -1
  120. package/build/src/testing/main.d.ts +1 -2
  121. package/build/src/testing/main.js +1 -2
  122. package/build/src/testing/migration_contract.d.ts +0 -1
  123. package/build/src/testing/migration_contract.js +0 -1
  124. package/build/src/testing/relations_contract.d.ts +25 -1
  125. package/build/src/testing/relations_contract.js +996 -49
  126. package/build/src/testing/relations_reconcile_contract.d.ts +9 -1
  127. package/build/src/testing/relations_reconcile_contract.js +207 -1
  128. package/build/src/testing/scope_outbox_contract.d.ts +0 -1
  129. package/build/src/testing/scope_outbox_contract.js +0 -1
  130. package/build/src/testing/scope_tree.d.ts +0 -1
  131. package/build/src/testing/scope_tree.js +0 -1
  132. package/build/src/testing/test_clock.d.ts +0 -1
  133. package/build/src/testing/test_clock.js +0 -1
  134. package/build/src/traits/authz_scopes.d.ts +0 -1
  135. package/build/src/traits/authz_scopes.js +0 -1
  136. package/build/src/traits/has_uuid.d.ts +5 -6
  137. package/build/src/traits/has_uuid.js +0 -1
  138. package/build/src/types.d.ts +170 -15
  139. package/build/src/types.js +6 -1
  140. package/build/stubs/config/authorization.stub +22 -6
  141. package/build/stubs/main.d.ts +0 -1
  142. package/build/stubs/main.js +0 -1
  143. package/package.json +4 -3
  144. package/build/commands/authz_catalog_diff.d.ts.map +0 -1
  145. package/build/commands/authz_catalog_diff.js.map +0 -1
  146. package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
  147. package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
  148. package/build/commands/authz_catalog_sync.d.ts.map +0 -1
  149. package/build/commands/authz_catalog_sync.js.map +0 -1
  150. package/build/commands/authz_freeze.d.ts.map +0 -1
  151. package/build/commands/authz_freeze.js.map +0 -1
  152. package/build/commands/authz_reconcile.d.ts.map +0 -1
  153. package/build/commands/authz_reconcile.js.map +0 -1
  154. package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
  155. package/build/commands/authz_relations_reconcile.js.map +0 -1
  156. package/build/commands/authz_scopes_relay.d.ts.map +0 -1
  157. package/build/commands/authz_scopes_relay.js.map +0 -1
  158. package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
  159. package/build/commands/authz_scopes_requeue.js.map +0 -1
  160. package/build/commands/authz_unfreeze.d.ts.map +0 -1
  161. package/build/commands/authz_unfreeze.js.map +0 -1
  162. package/build/commands/main.d.ts.map +0 -1
  163. package/build/commands/main.js.map +0 -1
  164. package/build/commands/openfga_provision.d.ts.map +0 -1
  165. package/build/commands/openfga_provision.js.map +0 -1
  166. package/build/configure.d.ts.map +0 -1
  167. package/build/configure.js.map +0 -1
  168. package/build/index.d.ts.map +0 -1
  169. package/build/index.js.map +0 -1
  170. package/build/providers/authz_provider.d.ts.map +0 -1
  171. package/build/providers/authz_provider.js.map +0 -1
  172. package/build/services/main.d.ts.map +0 -1
  173. package/build/services/main.js.map +0 -1
  174. package/build/services/relations.d.ts.map +0 -1
  175. package/build/services/relations.js.map +0 -1
  176. package/build/src/catalog/catalog.d.ts.map +0 -1
  177. package/build/src/catalog/catalog.js.map +0 -1
  178. package/build/src/catalog/catalog_cache.d.ts.map +0 -1
  179. package/build/src/catalog/catalog_cache.js.map +0 -1
  180. package/build/src/clock.d.ts.map +0 -1
  181. package/build/src/clock.js.map +0 -1
  182. package/build/src/define_config.d.ts.map +0 -1
  183. package/build/src/define_config.js.map +0 -1
  184. package/build/src/drivers/database_driver.d.ts.map +0 -1
  185. package/build/src/drivers/database_driver.js.map +0 -1
  186. package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
  187. package/build/src/drivers/database_relations_driver.js.map +0 -1
  188. package/build/src/drivers/openfga_driver.d.ts.map +0 -1
  189. package/build/src/drivers/openfga_driver.js.map +0 -1
  190. package/build/src/drivers/openfga_facts.d.ts.map +0 -1
  191. package/build/src/drivers/openfga_facts.js.map +0 -1
  192. package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
  193. package/build/src/drivers/openfga_relations_driver.js.map +0 -1
  194. package/build/src/errors.d.ts.map +0 -1
  195. package/build/src/errors.js.map +0 -1
  196. package/build/src/expiry.d.ts.map +0 -1
  197. package/build/src/expiry.js.map +0 -1
  198. package/build/src/freeze.d.ts.map +0 -1
  199. package/build/src/freeze.js.map +0 -1
  200. package/build/src/hierarchical_resolver.d.ts.map +0 -1
  201. package/build/src/hierarchical_resolver.js.map +0 -1
  202. package/build/src/http/app_access_middleware.d.ts.map +0 -1
  203. package/build/src/http/app_access_middleware.js.map +0 -1
  204. package/build/src/http/resource_access_middleware.d.ts.map +0 -1
  205. package/build/src/http/resource_access_middleware.js.map +0 -1
  206. package/build/src/identity.d.ts.map +0 -1
  207. package/build/src/identity.js.map +0 -1
  208. package/build/src/manager.d.ts.map +0 -1
  209. package/build/src/manager.js.map +0 -1
  210. package/build/src/memoize_ancestors.d.ts.map +0 -1
  211. package/build/src/memoize_ancestors.js.map +0 -1
  212. package/build/src/models/authz_assignment.d.ts.map +0 -1
  213. package/build/src/models/authz_assignment.js.map +0 -1
  214. package/build/src/models/authz_deny.d.ts.map +0 -1
  215. package/build/src/models/authz_deny.js.map +0 -1
  216. package/build/src/models/authz_permission.d.ts.map +0 -1
  217. package/build/src/models/authz_permission.js.map +0 -1
  218. package/build/src/models/authz_role.d.ts.map +0 -1
  219. package/build/src/models/authz_role.js.map +0 -1
  220. package/build/src/models/authz_role_permission.d.ts.map +0 -1
  221. package/build/src/models/authz_role_permission.js.map +0 -1
  222. package/build/src/openfga.d.ts.map +0 -1
  223. package/build/src/openfga.js.map +0 -1
  224. package/build/src/reconcile.d.ts.map +0 -1
  225. package/build/src/reconcile.js.map +0 -1
  226. package/build/src/relation_partition_trigger.d.ts.map +0 -1
  227. package/build/src/relation_partition_trigger.js.map +0 -1
  228. package/build/src/relations/check_many.d.ts.map +0 -1
  229. package/build/src/relations/check_many.js.map +0 -1
  230. package/build/src/relations/define_relations_config.d.ts.map +0 -1
  231. package/build/src/relations/define_relations_config.js.map +0 -1
  232. package/build/src/relations/manager.d.ts.map +0 -1
  233. package/build/src/relations/manager.js.map +0 -1
  234. package/build/src/relations/reconcile.d.ts.map +0 -1
  235. package/build/src/relations/reconcile.js.map +0 -1
  236. package/build/src/relations/tuple_key.d.ts.map +0 -1
  237. package/build/src/relations/tuple_key.js.map +0 -1
  238. package/build/src/relations_config_store.d.ts.map +0 -1
  239. package/build/src/relations_config_store.js.map +0 -1
  240. package/build/src/relations_reconcile.d.ts.map +0 -1
  241. package/build/src/relations_reconcile.js.map +0 -1
  242. package/build/src/scope_outbox.d.ts.map +0 -1
  243. package/build/src/scope_outbox.js.map +0 -1
  244. package/build/src/shared/backend_guard.d.ts.map +0 -1
  245. package/build/src/shared/backend_guard.js.map +0 -1
  246. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  247. package/build/src/shared/sql_expiry.js.map +0 -1
  248. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  249. package/build/src/shared/transaction_guard.js.map +0 -1
  250. package/build/src/sql_descendants.d.ts.map +0 -1
  251. package/build/src/sql_descendants.js.map +0 -1
  252. package/build/src/testing/contract.d.ts.map +0 -1
  253. package/build/src/testing/contract.js.map +0 -1
  254. package/build/src/testing/main.d.ts.map +0 -1
  255. package/build/src/testing/main.js.map +0 -1
  256. package/build/src/testing/migration_contract.d.ts.map +0 -1
  257. package/build/src/testing/migration_contract.js.map +0 -1
  258. package/build/src/testing/relations_contract.d.ts.map +0 -1
  259. package/build/src/testing/relations_contract.js.map +0 -1
  260. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  262. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  263. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  264. package/build/src/testing/scope_tree.d.ts.map +0 -1
  265. package/build/src/testing/scope_tree.js.map +0 -1
  266. package/build/src/testing/test_clock.d.ts.map +0 -1
  267. package/build/src/testing/test_clock.js.map +0 -1
  268. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  269. package/build/src/traits/authz_scopes.js.map +0 -1
  270. package/build/src/traits/has_uuid.d.ts.map +0 -1
  271. package/build/src/traits/has_uuid.js.map +0 -1
  272. package/build/src/types.d.ts.map +0 -1
  273. package/build/src/types.js.map +0 -1
  274. package/build/stubs/main.d.ts.map +0 -1
  275. package/build/stubs/main.js.map +0 -1
@@ -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
@@ -10,7 +10,7 @@ import { assertKnownScope, canonicalScopeTargets, isAuthzError, resolveChain, ro
10
10
  import { CatalogCache, GLOBAL_OWNER_KEY, invalidateAuthzCatalog, isRoleVisibleWith, readLocalRoles, withAuthzCatalogWrite } from './catalog/catalog_cache.js';
11
11
  import { assertAssignableAt } from './catalog/catalog.js';
12
12
  import { systemClock } from './clock.js';
13
- import { ActorRequiredError, AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, AuthorizationConfigError, AuthorizationInternalError, CatalogConflictError, InvalidIdentityError, MassPurgeRefusedError, PruneInterruptedError, NoDescendantsResolverError, NotWithinError, PermissionNotDelegableError, RankExceededError, RoleImmutableError, RoleLevelAboveOwnerError, ScopeCycleError, ScopeResolverError, TooManyScopesError, UnknownPermissionError, UnknownRoleError, FreezeHeldError, UnsupportedOperationError, ViewExpiredError, WithinRequiredError, WithinRootForbiddenError, ScopeDriftUnguardedError, ScopeDepthLimitError, ScopeRequeueRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, } from './errors.js';
13
+ import { ActorRequiredError, AuthorizationBackendTimeoutError, isPartialWrite, AuthorizationFrozenError, AuthorizationConfigError, AuthorizationInternalError, CatalogConflictError, InvalidIdentityError, MassPurgeRefusedError, PruneInterruptedError, NoDescendantsResolverError, NotWithinError, PermissionNotDelegableError, RankExceededError, RoleImmutableError, RoleLevelAboveOwnerError, ScopeCycleError, ScopeResolverError, ScopeTreeDriftError, TooManyScopesError, UnknownPermissionError, UnknownRoleError, UnknownScopeError, FreezeHeldError, UnsupportedOperationError, ViewExpiredError, WithinRequiredError, WithinRootForbiddenError, ScopeDriftUnguardedError, ScopeDepthLimitError, ScopeRequeueRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, isDeploymentFailure, isTransientRelayFailure, markDeploymentFailure, } from './errors.js';
14
14
  import { APP_SCOPE_TYPE, normalizeAuthorizationCapabilities } from './types.js';
15
15
  import { memoizeAncestors } from './memoize_ancestors.js';
16
16
  import { AUTHZ_TABLES_ORIGIN } from './reconcile.js';
@@ -77,6 +77,14 @@ export const DEFAULT_RELAY_BATCH = 100;
77
77
  export const DEFAULT_RELAY_LIMIT = 10_000;
78
78
  /** Páginas que `authz:reconcile` pasea para MEDIR la ventana del relay (3b-3a). */
79
79
  const RELAY_WINDOW_MAX_PAGES = 1_000;
80
+ /**
81
+ * **Fallos del relay que solo dicen algo de la posición del HIJO** (asentar · cierre 2, O-2 del re-ataque): el
82
+ * `E_AUTHZ_SCOPE_TREE_DRIFT` del respaldo (el consumidor no pone al hijo bajo el padre ENCOLADO). La pasada contamina
83
+ * solo la clave del hijo, no la del padre encolado: un `attached` de OTRO hijo bajo ese padre escribe otra arista y
84
+ * conmuta (la misma lectura que la variante B del censo de aparcadas, alpha.8). Hasta aquí, mientras la entrada fallaba
85
+ * —hasta `maxAttempts` pasadas, y una más tras cada `requeue`— taponaba la org entera para los hijos nuevos.
86
+ */
87
+ const CHILD_ONLY_FAILURES = new WeakSet();
80
88
  /**
81
89
  * **Saltos que la sonda de `only` sigue hacia atrás para heredar el ORIGEN de
82
90
  * un bloqueo** (2.4.0-alpha.6). Cada salto es una consulta `earlierPending`
@@ -1214,6 +1222,7 @@ export class AuthorizationManager {
1214
1222
  const parked = new Set();
1215
1223
  /** El último id visto: la outbox pagina desde ahí (lo saltado se queda). */
1216
1224
  let after;
1225
+ const passContext = { outbox, batchSize, appliedThisPass: [] };
1217
1226
  const defer = (item, keys, reason, error, origin) => {
1218
1227
  for (const key of keys)
1219
1228
  block(key, item.id, origin);
@@ -1295,24 +1304,42 @@ export class AuthorizationManager {
1295
1304
  }
1296
1305
  }
1297
1306
  }
1307
+ let appliedHow;
1298
1308
  try {
1299
- await this.#applyScopeChange(driver, item);
1309
+ appliedHow = await this.#applyScopeChange(driver, item, passContext);
1300
1310
  }
1301
1311
  catch (error) {
1302
1312
  const message = error instanceof Error ? error.message : String(error);
1303
- const transient = _a.#isTransientRelayFailure(error);
1313
+ const transient = isTransientRelayFailure(error);
1314
+ const code = typeof error?.code === 'string' ? error.code : undefined;
1304
1315
  await outbox.markFailed(item.id, message, { transient });
1305
- for (const key of keys)
1316
+ const failedKeys = typeof error === 'object' && error !== null && CHILD_ONLY_FAILURES.has(error) ? [scopeKey(item.change.child)] : keys;
1317
+ for (const key of failedKeys)
1306
1318
  block(key, item.id, 'failed');
1307
1319
  parked.add(id);
1308
- failures.push({ id: item.id, change: item.change, error: message, transient });
1320
+ failures.push({
1321
+ id: item.id,
1322
+ change: item.change,
1323
+ error: message,
1324
+ transient,
1325
+ ...(code !== undefined ? { code } : {}),
1326
+ ...(isDeploymentFailure(error) ? { deployment: true } : {}),
1327
+ });
1309
1328
  entries.push({ id: item.id, change: item.change, status: 'failed', reason: message, attempts: item.attempts });
1310
1329
  continue;
1311
1330
  }
1312
1331
  await outbox.markApplied(item.id);
1313
1332
  done.add(id);
1314
1333
  applied.push({ id: item.id, change: item.change, attempts: item.attempts });
1315
- entries.push({ id: item.id, change: item.change, status: 'applied', attempts: item.attempts });
1334
+ passContext.appliedThisPass.push(item.change);
1335
+ entries.push({
1336
+ id: item.id,
1337
+ change: item.change,
1338
+ status: 'applied',
1339
+ attempts: item.attempts,
1340
+ ...(appliedHow?.reason !== undefined ? { reason: appliedHow.reason } : {}),
1341
+ ...(appliedHow?.appliedParent !== undefined ? { appliedParent: appliedHow.appliedParent } : {}),
1342
+ });
1316
1343
  }
1317
1344
  // Una outbox que ignora `after` devuelve el mismo lote atascado: la
1318
1345
  // pasada termina aquí en vez de dar vueltas (lo que quede, y lo que
@@ -1380,18 +1407,6 @@ export class AuthorizationManager {
1380
1407
  'lo que no se pidió (filtra en el puerto, no en memoria).');
1381
1408
  }
1382
1409
  }
1383
- /**
1384
- * La caída del BACKEND no dice nada de la entrada (alpha.5 · R2a): un
1385
- * `AuthorizationBackendError` (503; el timeout es uno), y en general
1386
- * cualquier 503 (el freeze, el resolutor caído). Todo lo demás —422, 500,
1387
- * `E_AUTHZ_SCOPE_TREE_DRIFT`, el padre inexistente, un error sin status de
1388
- * un driver de terceros— es de la entrada y cuenta hacia el aparcado.
1389
- */
1390
- static #isTransientRelayFailure(error) {
1391
- if (error instanceof AuthorizationBackendError)
1392
- return true;
1393
- return typeof error === 'object' && error !== null && error.status === 503;
1394
- }
1395
1410
  /**
1396
1411
  * Las claves de scope de lo APARCADO ⇒ el id aparcado que las nombra
1397
1412
  * (alpha.5 · R2a). El censo lo lee `#relayInputs`: el `dead` ya leído si
@@ -2112,17 +2127,105 @@ export class AuthorizationManager {
2112
2127
  * —el driver demuestra cero o lanza—, arista al final** (S6). Al revés,
2113
2128
  * una purga muerta a medias dejaría grants vivos en un scope sin ancestro,
2114
2129
  * los denies heredados dejarían de aplicar y esos permisos serían
2115
- * INDENEGABLES (invariante 2).
2130
+ * INDENEGABLES (invariante 2). **Salvo el PADRE** (lote C · TREE): un
2131
+ * `attached`/`moved` se aplica con el padre FRESCO del consumidor, no con
2132
+ * el encolado (ver el cuerpo); devuelve `'superseded'` si el nodo ya no
2133
+ * existe en el consumidor (aplicada sin arista).
2116
2134
  */
2117
- async #applyScopeChange(driver, item) {
2135
+ async #applyScopeChange(driver, item, context) {
2118
2136
  const change = item.change;
2119
- if (change.op === 'attached') {
2120
- await driver.onScopeAttached?.(change.child, change.parent);
2121
- return;
2122
- }
2123
- if (change.op === 'moved') {
2124
- await driver.onScopeMoved?.(change.child, change.parent);
2125
- return;
2137
+ if (change.op === 'attached' || change.op === 'moved') {
2138
+ // **Asentar · cierre 1 (B4 del auditor del panel de estrategia)**: el padre FRESCO es SOLO para un driver que
2139
+ // mantiene el árbol como HECHOS en su store (`capabilities.hierarchyFacts`: `openfga`). En `database` no hay árbol
2140
+ // que pueda divergir —decide `resolveChain` en cada pregunta—, así que su relay se comporta EXACTAMENTE como en
2141
+ // `f19fd37`: el padre ENCOLADO, sin resolutor ni modos de fallo nuevos (caso de paridad en `scope_outbox.spec`).
2142
+ if (driver.capabilities?.hierarchyFacts !== true) {
2143
+ if (change.op === 'attached')
2144
+ await driver.onScopeAttached?.(change.child, change.parent);
2145
+ else
2146
+ await driver.onScopeMoved?.(change.child, change.parent);
2147
+ return undefined;
2148
+ }
2149
+ // **El relay aplica el padre FRESCO del consumidor** (lote C · TREE, decisión del dueño 2026-10-20 (1), (d)
2150
+ // corregida del juez; 🟠 «TREE con outbox»). El padre ENCOLADO es el que el consumidor quería al notificar, y la
2151
+ // receta publicada notifica ANTES de confirmar la fila: dos `moved` del MISMO nodo pueden confirmar en un orden y
2152
+ // encolarse en el otro, y relevar el padre encolado dejaba el store, para siempre y en silencio, con OTRO padre que
2153
+ // la base del consumidor (medido: 1000/1000 forzado en PG y 200/200 en MySQL). Convergencia (juez TREE §2): la
2154
+ // entrada del ÚLTIMO commit de un nodo se aplica después de ese commit y lee el estado final.
2155
+ //
2156
+ // **Pero el resolutor no es fiable por construcción** (🔴 1 del auditor del paso 4; firma del dueño 2026-10-21): uno
2157
+ // filtrado por tenant responde `null` de todo en un comando (README «the realistic accident») y uno con caché/réplica
2158
+ // responde el padre VIEJO. Aplicar a ciegas convertía un fallo ruidoso en un TREE persistente y MUDO. Regla mínima:
2159
+ // el padre fresco se aplica cuando lo RESPALDA la propia cola —otra entrada del mismo hijo que lo dice (pendiente,
2160
+ // aplicada en esta pasada o aplicada después)—; si no, la entrada FALLA con un error que CONSUME intento (y se aparca
2161
+ // con su motivo tras `maxAttempts`, exit ≠ 0 en cada pasada):
2162
+ // - cadena con padre = encolado ⇒ se aplica (el caso normal);
2163
+ // - padre ≠ encolado RESPALDADO (otra entrada del hijo con ese padre) ⇒ se aplica el fresco, `reason:
2164
+ // 'consumer-parent'` y `appliedParent` en el reporte (la carrera S1: el store converge);
2165
+ // - padre ≠ encolado SIN respaldo ⇒ 500 `E_AUTHZ_SCOPE_TREE_DRIFT` (resolutor rancio, o un cambio no notificado);
2166
+ // con un resolutor que se refresca (TTL), un reintento posterior ve el padre encolado y lo aplica;
2167
+ // - `null` RESPALDADO por un `detached` del hijo en la cola ⇒ aplicada SIN arista, `reason: 'superseded'` (D×D no
2168
+ // resucita un nodo borrado);
2169
+ // - `null` sin respaldo ⇒ 422 `E_AUTHZ_UNKNOWN_SCOPE` (resolutor filtrado por tenant, o hijo huérfano en la base);
2170
+ // - cadena TRUNCADA (`[hijo]` de un nodo que no es la raíz: le falta `app`) ⇒ 503 `E_AUTHZ_RESOLVER_FAILED`
2171
+ // (auditor ⚪ 7: jamás inventar la raíz);
2172
+ // - el resolutor LANZA ⇒ 503 transitorio, no consume intento (regla de alpha.5);
2173
+ // - sin `scopes.resolveChain` ⇒ 500 `E_AUTHZ_CONFIG` (desviación 6 del paso 4).
2174
+ // Residuo declarado: con un resolutor rancio Y otra entrada del mismo hijo que respalde justo el padre rancio (dos
2175
+ // movimientos seguidos del nodo dentro del TTL de la caché) el relay no puede distinguirlo de la carrera S1 y aplica;
2176
+ // el reporte lo dice (`consumer-parent`, exit 1 del relay) y la red (`authz:reconcile --dry-run`, su fase `tree`) lo
2177
+ // ve; el campo `drift.treeMismatch` NO existe todavía (llega en 2.6). Límite declarado `openfga.relay.stale-resolver`.
2178
+ let resolver;
2179
+ try {
2180
+ resolver = this.#resolver('authz:scopes:relay');
2181
+ }
2182
+ catch (error) {
2183
+ // Desviación 6 del paso 4, con su caso (asentar · cierre 1, 🟡-3 del tester): un proceso relay sin
2184
+ // `scopes.resolveChain` es un DESPLIEGUE mal configurado ⇒ transitorio para el relay (inv. 5): no consume intento
2185
+ // ni aparca la cola entera. Hasta el caso, el 500 contaba intento y la aparcaba en `maxAttempts` pasadas.
2186
+ throw markDeploymentFailure(error);
2187
+ }
2188
+ const chain = await resolveChain(resolver, change.child, 'authz:scopes:relay');
2189
+ const childKey = scopeKey(change.child);
2190
+ if (chain === null) {
2191
+ if (await this.#relayBacking(context, item, (other) => other.op === 'detached' && scopeKey(other.child) === childKey)) {
2192
+ return { reason: 'superseded', appliedParent: null };
2193
+ }
2194
+ throw new UnknownScopeError(`authz:scopes:relay: el resolutor del consumidor no conoce ${childKey} (devolvió null) y ninguna entrada ` +
2195
+ `'detached' de ese nodo en la cola lo respalda. No se aplica nada: un resolutor filtrado por tenant (sin ` +
2196
+ `petición en el comando) o con caché, o una fila huérfana en tu árbol. El resolutor del relay tiene que leer ` +
2197
+ `lo CONFIRMADO, sin contexto de tenant. La entrada consume intento y se aparca si persiste.`);
2198
+ }
2199
+ if (chain.length < 2) {
2200
+ throw new ScopeResolverError('authz:scopes:relay', new TypeError(`resolveChain devolvió una cadena de un solo elemento para ${childKey}: le falta la raíz 'app'`));
2201
+ }
2202
+ const parent = chain[1];
2203
+ const parentKey = scopeKey(parent);
2204
+ let reason;
2205
+ if (parentKey !== scopeKey(change.parent)) {
2206
+ const backed = await this.#relayBacking(context, item, (other) => other.op !== 'detached' && scopeKey(other.child) === childKey && scopeKey(other.parent) === parentKey);
2207
+ if (!backed) {
2208
+ const drift = new ScopeTreeDriftError(`authz:scopes:relay: el padre de ${childKey} que da el resolutor del consumidor (${parentKey}) no es el ` +
2209
+ `encolado (${scopeKey(change.parent)}) y ninguna otra entrada de ese nodo en la cola lo respalda. No se ` +
2210
+ `aplica nada: un resolutor con caché o réplica atrasada, o un cambio de árbol no notificado. El resolutor del ` +
2211
+ `relay tiene que leer lo CONFIRMADO, sin TTL. La entrada consume intento y se aparca si persiste. Si tu base ` +
2212
+ `ya tiene a ${childKey} bajo ${parentKey} y esta entrada es una intención VIEJA (otra entrada del nodo se ` +
2213
+ `aplicó antes en otra pasada), la salida es volver a notificar scopes.moved(${childKey}, ${parentKey}) —la ` +
2214
+ `entrada nueva la respalda— y reencolar #${String(item.id)} con authz:scopes:requeue ` +
2215
+ `--id=${String(item.id)} --apply. Si ese requeue se niega (E_AUTHZ_REQUEUE_REFUSED) solo por cambios ` +
2216
+ `aplicados de OTROS hijos bajo ${scopeKey(change.parent)}, el conflicto es falso: authz:scopes:requeue ` +
2217
+ `--id=${String(item.id)} --force y después authz:scopes:relay (el relay vuelve a resolver el padre en ` +
2218
+ `fresco y exige respaldo: no revierte nada).`);
2219
+ CHILD_ONLY_FAILURES.add(drift);
2220
+ throw drift;
2221
+ }
2222
+ reason = 'consumer-parent';
2223
+ }
2224
+ if (change.op === 'attached')
2225
+ await driver.onScopeAttached?.(change.child, parent);
2226
+ else
2227
+ await driver.onScopeMoved?.(change.child, parent);
2228
+ return { ...(reason !== undefined ? { reason } : {}), appliedParent: parent };
2126
2229
  }
2127
2230
  // La auditoría no pierde al autor por haber pasado por una cola.
2128
2231
  const event = {
@@ -2134,6 +2237,32 @@ export class AuthorizationManager {
2134
2237
  await driver.onScopeDetached?.(change.child);
2135
2238
  await this.#notify(event);
2136
2239
  }
2240
+ /**
2241
+ * **¿La cola RESPALDA el padre fresco?** (asentar · cierre 1, 🔴 1 del auditor). Otra entrada del mismo hijo que lo
2242
+ * dice: aplicada en ESTA pasada, PENDIENTE (cualquier id; se pagina la cola entera, solo cuando el fresco no coincide
2243
+ * con el encolado: raro) o APLICADA con un id posterior (`laterApplied`, si la outbox lo trae). Nunca una aplicada
2244
+ * ANTERIOR de otra pasada: una caché rancia diría justo el padre de esa entrada vieja.
2245
+ */
2246
+ async #relayBacking(context, item, matches) {
2247
+ if (context.appliedThisPass.some(matches))
2248
+ return true;
2249
+ const mine = String(item.id);
2250
+ let after;
2251
+ for (let page = 0; page < RELAY_WINDOW_MAX_PAGES; page++) {
2252
+ const batch = await context.outbox.pending(context.batchSize, after);
2253
+ if (batch.some((other) => String(other.id) !== mine && matches(other.change)))
2254
+ return true;
2255
+ if (batch.length < context.batchSize)
2256
+ break;
2257
+ after = batch[batch.length - 1].id;
2258
+ }
2259
+ if (typeof context.outbox.laterApplied === 'function') {
2260
+ const later = await context.outbox.laterApplied(item.id, [item.change.child], RELAY_WINDOW_MAX_PAGES);
2261
+ if (later.some((other) => matches(other.change)))
2262
+ return true;
2263
+ }
2264
+ return false;
2265
+ }
2137
2266
  /** Cota entera positiva de las opciones del relay (500 si llega otra cosa). */
2138
2267
  /**
2139
2268
  * **Una opción desconocida es 500, nunca silencio** (2.4.0-alpha.8 · X-1;
@@ -3767,8 +3896,11 @@ export class AuthorizationManager {
3767
3896
  * servidor pueden aplicarla después de que el llamante reciba el error
3768
3897
  * (D2, auditor H1). Antes de propagar se notifica el mismo evento con
3769
3898
  * `indeterminate: true`, para que quien audita registre "puede haber
3770
- * ocurrido" en vez de nada. Cualquier otro fallo (422, conexión rechazada)
3771
- * significa que la escritura no ocurrió y se propaga sin evento.
3899
+ * ocurrido" en vez de nada. Lo mismo si el driver marcó el error como
3900
+ * escritura PARCIAL (`markPartialWrite`; cierre 3 de D2 · C2-5: `openfga`
3901
+ * renueva un grant con delete + write y el 2.º falló). Cualquier otro fallo
3902
+ * (422, conexión rechazada) significa que la escritura no ocurrió y se
3903
+ * propaga sin evento.
3772
3904
  *
3773
3905
  * **Dentro de la transacción del llamante (L-3) el deadline sigue siendo
3774
3906
  * `indeterminate: true`**, y el evento lleva además `transactional: true`.
@@ -3786,7 +3918,9 @@ export class AuthorizationManager {
3786
3918
  return await fn();
3787
3919
  }
3788
3920
  catch (error) {
3789
- if (error instanceof AuthorizationBackendTimeoutError) {
3921
+ // Cierre 3 de D2 · ⚪ C2-5: una escritura PARCIAL (el driver la marca con `markPartialWrite`: `openfga` renueva un
3922
+ // grant con delete + write y el 2.º falló) también es indeterminada — la misma rama que el `RelationsManager`.
3923
+ if (error instanceof AuthorizationBackendTimeoutError || isPartialWrite(error)) {
3790
3924
  await this.#notify({ ...event, indeterminate: true });
3791
3925
  }
3792
3926
  throw error;
@@ -3821,4 +3955,3 @@ export class AuthorizationManager {
3821
3955
  }
3822
3956
  }
3823
3957
  _a = AuthorizationManager;
3824
- //# sourceMappingURL=manager.js.map
@@ -20,4 +20,3 @@ import type { ScopeChainResolver } from './types.js';
20
20
  * respétala tú.
21
21
  */
22
22
  export declare function memoizeAncestors(resolver: ScopeChainResolver): ScopeChainResolver;
23
- //# sourceMappingURL=memoize_ancestors.d.ts.map
@@ -39,4 +39,3 @@ export function memoizeAncestors(resolver) {
39
39
  return pending;
40
40
  };
41
41
  }
42
- //# sourceMappingURL=memoize_ancestors.js.map
@@ -77,6 +77,11 @@ declare const AuthzAssignment_base: {
77
77
  <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelPaginatorContract<InstanceType<Model>>, "paginate">): void;
78
78
  <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
79
79
  };
80
+ before: {
81
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends "find" | "fetch">(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, Event>): void;
82
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>], "paginate">): void;
83
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
84
+ };
80
85
  readonly booted: boolean;
81
86
  $columnsDefinitions: Map<string, import("@adonisjs/lucid/types/model").ModelColumnOptions>;
82
87
  $relationsDefinitions: Map<string, import("@adonisjs/lucid/types/relations").RelationshipsContract>;
@@ -113,11 +118,6 @@ declare const AuthzAssignment_base: {
113
118
  };
114
119
  $defineProperty: <Model extends import("@adonisjs/lucid/types/model").LucidModel, Prop extends keyof Model>(this: Model, propertyName: Prop, defaultValue: Model[Prop], strategy: "inherit" | "define" | ((value: Model[Prop]) => Model[Prop])) => void;
115
120
  boot: () => void;
116
- before: {
117
- <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends "find" | "fetch">(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, Event>): void;
118
- <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>], "paginate">): void;
119
- <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
120
- };
121
121
  create: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
122
122
  createQuietly: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
123
123
  createMany: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>[]>;
@@ -163,4 +163,3 @@ export default class AuthzAssignment extends AuthzAssignment_base {
163
163
  createdAt: DateTime;
164
164
  }
165
165
  export {};
166
- //# sourceMappingURL=authz_assignment.d.ts.map
@@ -57,4 +57,3 @@ __decorate([
57
57
  column.dateTime({ autoCreate: true, columnName: 'created_at' }),
58
58
  __metadata("design:type", DateTime)
59
59
  ], AuthzAssignment.prototype, "createdAt", void 0);
60
- //# sourceMappingURL=authz_assignment.js.map
@@ -75,6 +75,11 @@ declare const AuthzDeny_base: {
75
75
  <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelPaginatorContract<InstanceType<Model>>, "paginate">): void;
76
76
  <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
77
77
  };
78
+ before: {
79
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends "find" | "fetch">(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, Event>): void;
80
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>], "paginate">): void;
81
+ <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
82
+ };
78
83
  readonly booted: boolean;
79
84
  $columnsDefinitions: Map<string, import("@adonisjs/lucid/types/model").ModelColumnOptions>;
80
85
  $relationsDefinitions: Map<string, import("@adonisjs/lucid/types/relations").RelationshipsContract>;
@@ -111,11 +116,6 @@ declare const AuthzDeny_base: {
111
116
  };
112
117
  $defineProperty: <Model extends import("@adonisjs/lucid/types/model").LucidModel, Prop extends keyof Model>(this: Model, propertyName: Prop, defaultValue: Model[Prop], strategy: "inherit" | "define" | ((value: Model[Prop]) => Model[Prop])) => void;
113
118
  boot: () => void;
114
- before: {
115
- <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends "find" | "fetch">(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, Event>): void;
116
- <Model extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model>], "paginate">): void;
117
- <Model extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model>, Event>): void;
118
- };
119
119
  create: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
120
120
  createQuietly: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
121
121
  createMany: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>[]>;
@@ -158,4 +158,3 @@ export default class AuthzDeny extends AuthzDeny_base {
158
158
  createdAt: DateTime;
159
159
  }
160
160
  export {};
161
- //# sourceMappingURL=authz_deny.d.ts.map
@@ -47,4 +47,3 @@ __decorate([
47
47
  column.dateTime({ autoCreate: true, columnName: 'created_at' }),
48
48
  __metadata("design:type", DateTime)
49
49
  ], AuthzDeny.prototype, "createdAt", void 0);
50
- //# sourceMappingURL=authz_deny.js.map