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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +437 -48
  2. package/build/commands/authz_catalog_diff.d.ts +0 -1
  3. package/build/commands/authz_catalog_diff.js +0 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +17 -3
  5. package/build/commands/authz_catalog_prune_orphans.js +28 -6
  6. package/build/commands/authz_catalog_sync.d.ts +0 -1
  7. package/build/commands/authz_catalog_sync.js +0 -1
  8. package/build/commands/authz_freeze.d.ts +0 -1
  9. package/build/commands/authz_freeze.js +0 -1
  10. package/build/commands/authz_reconcile.d.ts +0 -1
  11. package/build/commands/authz_reconcile.js +0 -1
  12. package/build/commands/authz_relations_reconcile.d.ts +0 -1
  13. package/build/commands/authz_relations_reconcile.js +56 -8
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +54 -6
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +16 -3
  18. package/build/commands/authz_unfreeze.d.ts +0 -1
  19. package/build/commands/authz_unfreeze.js +0 -1
  20. package/build/commands/main.d.ts +0 -1
  21. package/build/commands/main.js +0 -1
  22. package/build/commands/openfga_provision.d.ts +10 -1
  23. package/build/commands/openfga_provision.js +43 -3
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +9 -7
  27. package/build/index.js +10 -3
  28. package/build/providers/authz_provider.d.ts +0 -1
  29. package/build/providers/authz_provider.js +0 -1
  30. package/build/services/main.d.ts +0 -1
  31. package/build/services/main.js +0 -1
  32. package/build/services/relations.d.ts +0 -1
  33. package/build/services/relations.js +0 -1
  34. package/build/src/catalog/catalog.d.ts +0 -1
  35. package/build/src/catalog/catalog.js +0 -1
  36. package/build/src/catalog/catalog_cache.d.ts +13 -1
  37. package/build/src/catalog/catalog_cache.js +27 -2
  38. package/build/src/clock.d.ts +0 -1
  39. package/build/src/clock.js +0 -1
  40. package/build/src/define_config.d.ts +0 -1
  41. package/build/src/define_config.js +24 -1
  42. package/build/src/drivers/database_driver.d.ts +60 -3
  43. package/build/src/drivers/database_driver.js +160 -25
  44. package/build/src/drivers/database_relations_driver.d.ts +11 -5
  45. package/build/src/drivers/database_relations_driver.js +115 -33
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +882 -199
  48. package/build/src/drivers/openfga_facts.d.ts +91 -1
  49. package/build/src/drivers/openfga_facts.js +277 -3
  50. package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
  51. package/build/src/drivers/openfga_relations_driver.js +422 -228
  52. package/build/src/errors.d.ts +160 -1
  53. package/build/src/errors.js +161 -1
  54. package/build/src/expiry.d.ts +51 -2
  55. package/build/src/expiry.js +96 -2
  56. package/build/src/freeze.d.ts +0 -1
  57. package/build/src/freeze.js +0 -1
  58. package/build/src/hierarchical_resolver.d.ts +0 -1
  59. package/build/src/hierarchical_resolver.js +0 -1
  60. package/build/src/http/app_access_middleware.d.ts +0 -1
  61. package/build/src/http/app_access_middleware.js +0 -1
  62. package/build/src/http/resource_access_middleware.d.ts +0 -1
  63. package/build/src/http/resource_access_middleware.js +0 -1
  64. package/build/src/identity.d.ts +9 -2
  65. package/build/src/identity.js +33 -1
  66. package/build/src/manager.d.ts +24 -1
  67. package/build/src/manager.js +381 -85
  68. package/build/src/memoize_ancestors.d.ts +0 -1
  69. package/build/src/memoize_ancestors.js +0 -1
  70. package/build/src/models/authz_assignment.d.ts +5 -6
  71. package/build/src/models/authz_assignment.js +0 -1
  72. package/build/src/models/authz_deny.d.ts +5 -6
  73. package/build/src/models/authz_deny.js +0 -1
  74. package/build/src/models/authz_permission.d.ts +5 -6
  75. package/build/src/models/authz_permission.js +0 -1
  76. package/build/src/models/authz_role.d.ts +5 -6
  77. package/build/src/models/authz_role.js +0 -1
  78. package/build/src/models/authz_role_permission.d.ts +5 -6
  79. package/build/src/models/authz_role_permission.js +0 -1
  80. package/build/src/openfga.d.ts +2 -3
  81. package/build/src/openfga.js +4 -2
  82. package/build/src/reconcile.d.ts +0 -1
  83. package/build/src/reconcile.js +0 -1
  84. package/build/src/relation_partition_trigger.d.ts +0 -1
  85. package/build/src/relation_partition_trigger.js +0 -1
  86. package/build/src/relations/check_many.d.ts +0 -1
  87. package/build/src/relations/check_many.js +0 -1
  88. package/build/src/relations/define_relations_config.d.ts +25 -3
  89. package/build/src/relations/define_relations_config.js +8 -3
  90. package/build/src/relations/deny_reach.d.ts +102 -0
  91. package/build/src/relations/deny_reach.js +230 -0
  92. package/build/src/relations/group_depth.d.ts +215 -0
  93. package/build/src/relations/group_depth.js +453 -0
  94. package/build/src/relations/manager.d.ts +14 -7
  95. package/build/src/relations/manager.js +60 -15
  96. package/build/src/relations/page.d.ts +19 -0
  97. package/build/src/relations/page.js +74 -0
  98. package/build/src/relations/reconcile.d.ts +58 -4
  99. package/build/src/relations/reconcile.js +266 -12
  100. package/build/src/relations/tuple_key.d.ts +0 -1
  101. package/build/src/relations/tuple_key.js +0 -1
  102. package/build/src/relations/tuple_order.d.ts +5 -4
  103. package/build/src/relations/tuple_order.js +5 -4
  104. package/build/src/relations_config_store.d.ts +57 -2
  105. package/build/src/relations_config_store.js +155 -5
  106. package/build/src/relations_reconcile.d.ts +0 -1
  107. package/build/src/relations_reconcile.js +0 -1
  108. package/build/src/scope_outbox.d.ts +36 -3
  109. package/build/src/scope_outbox.js +78 -18
  110. package/build/src/shared/backend_guard.d.ts +52 -1
  111. package/build/src/shared/backend_guard.js +164 -2
  112. package/build/src/shared/sql_expiry.d.ts +0 -1
  113. package/build/src/shared/sql_expiry.js +0 -1
  114. package/build/src/shared/transaction_guard.d.ts +0 -1
  115. package/build/src/shared/transaction_guard.js +0 -1
  116. package/build/src/sql_descendants.d.ts +0 -1
  117. package/build/src/sql_descendants.js +0 -1
  118. package/build/src/testing/contract.d.ts +26 -3
  119. package/build/src/testing/contract.js +258 -20
  120. package/build/src/testing/main.d.ts +0 -1
  121. package/build/src/testing/main.js +0 -1
  122. package/build/src/testing/migration_contract.d.ts +0 -1
  123. package/build/src/testing/migration_contract.js +0 -1
  124. package/build/src/testing/relations_contract.d.ts +7 -1
  125. package/build/src/testing/relations_contract.js +174 -7
  126. package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
  127. package/build/src/testing/relations_reconcile_contract.js +0 -1
  128. package/build/src/testing/scope_outbox_contract.d.ts +0 -1
  129. package/build/src/testing/scope_outbox_contract.js +0 -1
  130. package/build/src/testing/scope_tree.d.ts +0 -1
  131. package/build/src/testing/scope_tree.js +0 -1
  132. package/build/src/testing/test_clock.d.ts +0 -1
  133. package/build/src/testing/test_clock.js +0 -1
  134. package/build/src/traits/authz_scopes.d.ts +0 -1
  135. package/build/src/traits/authz_scopes.js +0 -1
  136. package/build/src/traits/has_uuid.d.ts +5 -6
  137. package/build/src/traits/has_uuid.js +0 -1
  138. package/build/src/types.d.ts +138 -14
  139. package/build/src/types.js +6 -1
  140. package/build/stubs/config/authorization.stub +22 -6
  141. package/build/stubs/main.d.ts +0 -1
  142. package/build/stubs/main.js +0 -1
  143. package/package.json +4 -3
  144. package/build/commands/authz_catalog_diff.d.ts.map +0 -1
  145. package/build/commands/authz_catalog_diff.js.map +0 -1
  146. package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
  147. package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
  148. package/build/commands/authz_catalog_sync.d.ts.map +0 -1
  149. package/build/commands/authz_catalog_sync.js.map +0 -1
  150. package/build/commands/authz_freeze.d.ts.map +0 -1
  151. package/build/commands/authz_freeze.js.map +0 -1
  152. package/build/commands/authz_reconcile.d.ts.map +0 -1
  153. package/build/commands/authz_reconcile.js.map +0 -1
  154. package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
  155. package/build/commands/authz_relations_reconcile.js.map +0 -1
  156. package/build/commands/authz_scopes_relay.d.ts.map +0 -1
  157. package/build/commands/authz_scopes_relay.js.map +0 -1
  158. package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
  159. package/build/commands/authz_scopes_requeue.js.map +0 -1
  160. package/build/commands/authz_unfreeze.d.ts.map +0 -1
  161. package/build/commands/authz_unfreeze.js.map +0 -1
  162. package/build/commands/main.d.ts.map +0 -1
  163. package/build/commands/main.js.map +0 -1
  164. package/build/commands/openfga_provision.d.ts.map +0 -1
  165. package/build/commands/openfga_provision.js.map +0 -1
  166. package/build/configure.d.ts.map +0 -1
  167. package/build/configure.js.map +0 -1
  168. package/build/index.d.ts.map +0 -1
  169. package/build/index.js.map +0 -1
  170. package/build/providers/authz_provider.d.ts.map +0 -1
  171. package/build/providers/authz_provider.js.map +0 -1
  172. package/build/services/main.d.ts.map +0 -1
  173. package/build/services/main.js.map +0 -1
  174. package/build/services/relations.d.ts.map +0 -1
  175. package/build/services/relations.js.map +0 -1
  176. package/build/src/catalog/catalog.d.ts.map +0 -1
  177. package/build/src/catalog/catalog.js.map +0 -1
  178. package/build/src/catalog/catalog_cache.d.ts.map +0 -1
  179. package/build/src/catalog/catalog_cache.js.map +0 -1
  180. package/build/src/clock.d.ts.map +0 -1
  181. package/build/src/clock.js.map +0 -1
  182. package/build/src/define_config.d.ts.map +0 -1
  183. package/build/src/define_config.js.map +0 -1
  184. package/build/src/drivers/database_driver.d.ts.map +0 -1
  185. package/build/src/drivers/database_driver.js.map +0 -1
  186. package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
  187. package/build/src/drivers/database_relations_driver.js.map +0 -1
  188. package/build/src/drivers/openfga_driver.d.ts.map +0 -1
  189. package/build/src/drivers/openfga_driver.js.map +0 -1
  190. package/build/src/drivers/openfga_facts.d.ts.map +0 -1
  191. package/build/src/drivers/openfga_facts.js.map +0 -1
  192. package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
  193. package/build/src/drivers/openfga_relations_driver.js.map +0 -1
  194. package/build/src/errors.d.ts.map +0 -1
  195. package/build/src/errors.js.map +0 -1
  196. package/build/src/expiry.d.ts.map +0 -1
  197. package/build/src/expiry.js.map +0 -1
  198. package/build/src/freeze.d.ts.map +0 -1
  199. package/build/src/freeze.js.map +0 -1
  200. package/build/src/hierarchical_resolver.d.ts.map +0 -1
  201. package/build/src/hierarchical_resolver.js.map +0 -1
  202. package/build/src/http/app_access_middleware.d.ts.map +0 -1
  203. package/build/src/http/app_access_middleware.js.map +0 -1
  204. package/build/src/http/resource_access_middleware.d.ts.map +0 -1
  205. package/build/src/http/resource_access_middleware.js.map +0 -1
  206. package/build/src/identity.d.ts.map +0 -1
  207. package/build/src/identity.js.map +0 -1
  208. package/build/src/manager.d.ts.map +0 -1
  209. package/build/src/manager.js.map +0 -1
  210. package/build/src/memoize_ancestors.d.ts.map +0 -1
  211. package/build/src/memoize_ancestors.js.map +0 -1
  212. package/build/src/models/authz_assignment.d.ts.map +0 -1
  213. package/build/src/models/authz_assignment.js.map +0 -1
  214. package/build/src/models/authz_deny.d.ts.map +0 -1
  215. package/build/src/models/authz_deny.js.map +0 -1
  216. package/build/src/models/authz_permission.d.ts.map +0 -1
  217. package/build/src/models/authz_permission.js.map +0 -1
  218. package/build/src/models/authz_role.d.ts.map +0 -1
  219. package/build/src/models/authz_role.js.map +0 -1
  220. package/build/src/models/authz_role_permission.d.ts.map +0 -1
  221. package/build/src/models/authz_role_permission.js.map +0 -1
  222. package/build/src/openfga.d.ts.map +0 -1
  223. package/build/src/openfga.js.map +0 -1
  224. package/build/src/reconcile.d.ts.map +0 -1
  225. package/build/src/reconcile.js.map +0 -1
  226. package/build/src/relation_partition_trigger.d.ts.map +0 -1
  227. package/build/src/relation_partition_trigger.js.map +0 -1
  228. package/build/src/relations/check_many.d.ts.map +0 -1
  229. package/build/src/relations/check_many.js.map +0 -1
  230. package/build/src/relations/define_relations_config.d.ts.map +0 -1
  231. package/build/src/relations/define_relations_config.js.map +0 -1
  232. package/build/src/relations/manager.d.ts.map +0 -1
  233. package/build/src/relations/manager.js.map +0 -1
  234. package/build/src/relations/reconcile.d.ts.map +0 -1
  235. package/build/src/relations/reconcile.js.map +0 -1
  236. package/build/src/relations/tuple_key.d.ts.map +0 -1
  237. package/build/src/relations/tuple_key.js.map +0 -1
  238. package/build/src/relations/tuple_order.d.ts.map +0 -1
  239. package/build/src/relations/tuple_order.js.map +0 -1
  240. package/build/src/relations_config_store.d.ts.map +0 -1
  241. package/build/src/relations_config_store.js.map +0 -1
  242. package/build/src/relations_reconcile.d.ts.map +0 -1
  243. package/build/src/relations_reconcile.js.map +0 -1
  244. package/build/src/scope_outbox.d.ts.map +0 -1
  245. package/build/src/scope_outbox.js.map +0 -1
  246. package/build/src/shared/backend_guard.d.ts.map +0 -1
  247. package/build/src/shared/backend_guard.js.map +0 -1
  248. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  249. package/build/src/shared/sql_expiry.js.map +0 -1
  250. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  251. package/build/src/shared/transaction_guard.js.map +0 -1
  252. package/build/src/sql_descendants.d.ts.map +0 -1
  253. package/build/src/sql_descendants.js.map +0 -1
  254. package/build/src/testing/contract.d.ts.map +0 -1
  255. package/build/src/testing/contract.js.map +0 -1
  256. package/build/src/testing/main.d.ts.map +0 -1
  257. package/build/src/testing/main.js.map +0 -1
  258. package/build/src/testing/migration_contract.d.ts.map +0 -1
  259. package/build/src/testing/migration_contract.js.map +0 -1
  260. package/build/src/testing/relations_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_contract.js.map +0 -1
  262. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  263. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  264. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  265. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  266. package/build/src/testing/scope_tree.d.ts.map +0 -1
  267. package/build/src/testing/scope_tree.js.map +0 -1
  268. package/build/src/testing/test_clock.d.ts.map +0 -1
  269. package/build/src/testing/test_clock.js.map +0 -1
  270. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  271. package/build/src/traits/authz_scopes.js.map +0 -1
  272. package/build/src/traits/has_uuid.d.ts.map +0 -1
  273. package/build/src/traits/has_uuid.js.map +0 -1
  274. package/build/src/types.d.ts.map +0 -1
  275. package/build/src/types.js.map +0 -1
  276. package/build/stubs/main.d.ts.map +0 -1
  277. package/build/stubs/main.js.map +0 -1
@@ -22,14 +22,45 @@
22
22
  * sin uso (cleanup futuro).
23
23
  */
24
24
  import { systemClock } from './clock.js';
25
- import { RelationConfigError } from './errors.js';
25
+ import { AuthorizationConfigError, DenyReachRefusedError, RelationConfigError } from './errors.js';
26
26
  import { guardSql } from './shared/backend_guard.js';
27
27
  import db from '@adonisjs/lucid/services/db';
28
28
  import { invalidateAuthzCatalog, withAuthzCatalogWrite } from './catalog/catalog_cache.js';
29
29
  import { defineRelationsConfig } from './relations/define_relations_config.js';
30
+ import { DENY_REACH_KEY_PATTERN, deniedPublicRelation, denyReachBaselineOf, denyReachCheck, denyReachKey, denyReachLine, judgeDenyReach } from './relations/deny_reach.js';
30
31
  const TABLE = 'authz_relations_config';
31
32
  const ROW_ID = 1;
32
33
  const DEFAULT_TIMEOUT_MS = 5_000;
34
+ /** La lista de la salida forzada acotada, validada (L5): `undefined` ⇒ ninguna; array de `tipo#relación` ⇒ esa; otra cosa ⇒ 500. */
35
+ function namedShrinkExemptions(value) {
36
+ if (value === undefined)
37
+ return new Set();
38
+ const ok = Array.isArray(value) && value.every((key) => typeof key === 'string' && DENY_REACH_KEY_PATTERN.test(key));
39
+ if (!ok) {
40
+ throw new AuthorizationConfigError(`saveRelationsConfig: allowDenyReachShrink tiene que ser una lista de 'tipo#relación' (llegó ${JSON.stringify(value) ?? String(value)}). ` +
41
+ `Desde el sublote (8) la salida forzada exime SOLO los hallazgos que nombras —p. ej. { allowDenyReachShrink: ['note#editor'] }—; ` +
42
+ `no hay un true global.`);
43
+ }
44
+ return new Set(value);
45
+ }
46
+ /** Los nombres de la lista que no eximen ningún hallazgo `shrunk`/`orphan` de este guardado (orden de la lista). */
47
+ function unusedShrinkExemptions(exempt, judged) {
48
+ const used = new Set(judged.filter((finding) => finding.reason !== 'no-baseline').map(denyReachKey));
49
+ return [...exempt].filter((key) => !used.has(key));
50
+ }
51
+ function unusedShrinkExemptionsError(unused) {
52
+ return new AuthorizationConfigError(`saveRelationsConfig: allowDenyReachShrink nombra ${JSON.stringify(unused)}, y en este guardado ningún deny vivo de ` +
53
+ `esa(s) relación(es) encoge ni queda huérfano: no exime nada. Quita ese nombre de la lista (la salida forzada acotada ` +
54
+ `se usa en el guardado que la necesita y se retira después; pegada, eximiría en silencio un encogimiento futuro). ` +
55
+ `No se ha escrito nada (ni la fila ni la versión del catálogo).`);
56
+ }
57
+ /** `establishBaseline` validado (L5): solo `undefined`, `false` o `true`. */
58
+ function establishBaselineOf(value) {
59
+ if (value === undefined || value === false || value === true)
60
+ return value === true;
61
+ throw new AuthorizationConfigError(`saveRelationsConfig: establishBaseline tiene que ser true o false (llegó ${JSON.stringify(value) ?? String(value)}): ` +
62
+ `solo true adopta la línea base, y una opción mal escrita no puede levantar la guarda.`);
63
+ }
33
64
  function connectionOf(options) {
34
65
  return options?.connection ? db.connection(options.connection) : db;
35
66
  }
@@ -41,21 +72,141 @@ function connectionOf(options) {
41
72
  * siguiente pregunta—. Devuelve la config congelada.
42
73
  */
43
74
  export async function saveRelationsConfig(spec, options = {}) {
44
- // Valida ANTES de escribir: una config inválida no ensucia la tabla.
75
+ // Valida ANTES de escribir: una config inválida no ensucia la tabla. Las dos salidas de la guarda (8), también antes
76
+ // (L5: una forma rota es 500 sin tomar el cerrojo).
45
77
  const config = defineRelationsConfig(spec);
46
- const json = JSON.stringify(spec);
78
+ const json = JSON.stringify(persistableRelationsSpec(spec));
79
+ const { allowDenyReachShrink, establishBaseline, ...storeOptions } = options;
80
+ const exempt = namedShrinkExemptions(allowDenyReachShrink);
81
+ const adopt = establishBaselineOf(establishBaseline);
47
82
  await withAuthzCatalogWrite(async (trx) => {
48
83
  const existing = await trx.from(TABLE).where('id', ROW_ID).first();
84
+ // **La guarda (8) de `database`**, DENTRO de la transacción del catálogo (cerrojo de la fila de versión) y ANTES de la
85
+ // primera escritura: la línea base es la fila que acabamos de leer (TODO O NADA) y los denies vivos se leen por la
86
+ // MISMA trx. Cada hallazgo se niega salvo su salida con nombre: `no-baseline` ⇐ `establishBaseline: true` (solo existe
87
+ // sin línea base utilizable: con ella es inerte); `shrunk`/`orphan` ⇐ su `tipo#relación` en `allowDenyReachShrink`.
88
+ const judged = judgeDenyReach(baselineOf(existing), config, await liveDenyGroups(trx));
89
+ const refused = judged.filter((finding) => (finding.reason === 'no-baseline' ? !adopt : !exempt.has(denyReachKey(finding))));
90
+ if (refused.length > 0)
91
+ throw denyReachRefused(refused);
92
+ // **Un nombre de la salida acotada que no exime NADA en ESTE guardado ⇒ 500** (firma «(8 · cierre 1 · ronda)», 🟡 1 del
93
+ // auditor del cierre 1): «casa» = exime un `shrunk`/`orphan` de este guardado (un `no-baseline` no cuenta), calculado
94
+ // aquí, dentro de la trx. Así la salida se APAGA SOLA: una lista que se queda pegada en el pipeline no exime en silencio
95
+ // el encogimiento FUTURO de esa relación. Un huérfano heredado sigue siendo hallazgo en cada guardado: su nombre casa.
96
+ const unused = unusedShrinkExemptions(exempt, judged);
97
+ if (unused.length > 0)
98
+ throw unusedShrinkExemptionsError(unused);
49
99
  if (existing) {
50
100
  await trx.from(TABLE).where('id', ROW_ID).update({ spec: json, updated_at: systemClock() });
51
101
  }
52
102
  else {
53
103
  await trx.table(TABLE).insert({ id: ROW_ID, spec: json, model_id: null, updated_at: systemClock() });
54
104
  }
55
- }, options);
105
+ }, storeOptions);
56
106
  invalidateAuthzCatalog();
57
107
  return config;
58
108
  }
109
+ /**
110
+ * La LÍNEA BASE de la guarda (8) y del detector de un driver: la config persistida, leída TODO O NADA
111
+ * (`denyReachBaselineOf`), o `null` = NO UTILIZABLE (sin fila, JSON roto, o una forma que la lectura no puede tipar). Una
112
+ * fila que no valida en 2.5 pero cuyos campos de alcance se tipan (p. ej. una clave que 2.5 no conoce) SÍ es línea base:
113
+ * re-guardarla con la spec corregida (la receta de B5) entra sin forzar.
114
+ */
115
+ function baselineOf(row) {
116
+ if (!row)
117
+ return null;
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(String(row.spec));
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ return denyReachBaselineOf(parsed);
126
+ }
127
+ /**
128
+ * Los denies VIVOS de `authz_relations` agrupados por tipo y relación (las filas del driver `database`), por la conexión o
129
+ * la trx que se le pase. Todas las filas `denied_*`: el deny no caduca (D3) y una fila con `expires_at` escrita a mano cuenta
130
+ * igual (contar de más rechaza de más: lo conservador). El filtro del deny es UNO, aquí (`deniedPublicRelation`): una fila
131
+ * que no es un deny no pide línea base (S-A0).
132
+ */
133
+ async function liveDenyGroups(connection) {
134
+ return denyGroupsOf(await liveDenyQuery(connection));
135
+ }
136
+ /** La consulta (un `GROUP BY` de `authz_relations`): aparte para que el detector de un driver le ponga su deadline. */
137
+ function liveDenyQuery(connection) {
138
+ return connection.from('authz_relations').select('object_type', 'relation').count('* as n').groupBy('object_type', 'relation');
139
+ }
140
+ function denyGroupsOf(rows) {
141
+ const groups = [];
142
+ for (const row of rows) {
143
+ const relation = deniedPublicRelation(String(row.relation));
144
+ if (relation !== null)
145
+ groups.push({ objectType: String(row.object_type), relation, count: Number(row.n) });
146
+ }
147
+ return groups;
148
+ }
149
+ function denyReachRefused(findings) {
150
+ const shrink = findings.filter((f) => f.reason !== 'no-baseline').map(denyReachKey);
151
+ const exits = [
152
+ ...(findings.some((f) => f.reason === 'no-baseline')
153
+ ? ['sin línea base utilizable, la adopción es saveRelationsConfig(spec, { establishBaseline: true }) con la spec que la flota sirve HOY, sin cambios']
154
+ : []),
155
+ ...(shrink.length > 0
156
+ ? [`si de verdad quieres que esos denies dejen de cortar, la salida forzada ACOTADA es saveRelationsConfig(spec, { allowDenyReachShrink: ${JSON.stringify(shrink)} })`]
157
+ : []),
158
+ ];
159
+ const error = new DenyReachRefusedError(`saveRelationsConfig NEGADO (E_AUTHZ_DENY_REACH_REFUSED): ${findings.length} hallazgo(s) sobre denies vivos de authz_relations — ` +
160
+ `una config que encoge su alcance haría conceder, de forma persistente, lo que negaban (decisión (8): para todo deny ` +
161
+ `vivo, alcance nuevo ⊇ alcance viejo). ${findings.map(denyReachLine).join(' ')} No se ha escrito nada (ni la fila ni la ` +
162
+ `versión del catálogo). ${exits.join('; ')}.`);
163
+ error.findings = findings.map(({ objectType, relation, reason, count, lost, recipe }) => ({ objectType, relation, reason, count, lost, recipe }));
164
+ return error;
165
+ }
166
+ /**
167
+ * **El detector de UN solo driver** (firma (8 · detector de un driver), R4 del analista del panel): en un despliegue
168
+ * solo-`database` el detector de `authz:relations:reconcile --dry-run` no corre (necesita DOS drivers de relaciones:
169
+ * origen = destino es 500). Esta función compara la config que SIRVE el proceso (`served`) con la línea base persistida
170
+ * sobre los denies vivos de `authz_relations`, en las DOS direcciones:
171
+ * - persistida → servida: lo que la guarda negaría (`shrunk`, `orphan`, `no-baseline`);
172
+ * - servida → persistida: la línea base RANCIA (`stale`), que la guarda no puede ver.
173
+ *
174
+ * Solo LEE (la fila y un `GROUP BY` de `authz_relations`, por `options.connection` o la primaria), sin transacción y sin el
175
+ * cerrojo del catálogo, con el MISMO punto de paso que la guarda (`baselineOf` + `liveDenyGroups` + `denyReachCheck`).
176
+ * **Receta**: en CI, DESPUÉS del paso que guarda la config y ANTES de servir, y en un cron; falla el job si `ok` es `false`.
177
+ * **Nunca** como fallo duro en `ready`: durante un despliegue un pod viejo que se reinicia sirve la config estrecha contra la
178
+ * fila nueva y entraría en un bucle de reinicios.
179
+ */
180
+ export async function checkRelationsConfigDenyReach(served, options = {}) {
181
+ if (!served || !Array.isArray(served.objectTypes)) {
182
+ throw new AuthorizationConfigError('checkRelationsConfigDenyReach: pasa la config de relaciones que SIRVE este proceso (relations.config de config/authorization.ts, la de defineRelationsConfig).');
183
+ }
184
+ const connection = connectionOf(options);
185
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
186
+ const row = await guardSql('relations-config', 'read', timeoutMs, () => connection.from(TABLE).where('id', ROW_ID).first());
187
+ const groups = denyGroupsOf(await guardSql('relations-config', 'read', timeoutMs, () => liveDenyQuery(connection)));
188
+ const check = denyReachCheck(baselineOf(row), served, groups);
189
+ return {
190
+ ...check,
191
+ ok: check.findings.length === 0 && check.staleBaseline.length === 0,
192
+ lines: [...check.findings, ...check.staleBaseline].map(denyReachLine),
193
+ };
194
+ }
195
+ /**
196
+ * **Lo que se persiste de la spec** (cierre-2 de R1 · P1, decisión del dueño 2026-10-20): `maxGroupDepth` y
197
+ * `database.enforceGroupDepth` son claves de DESPLIEGUE, no de modelo. Se VALIDAN (`defineRelationsConfig`, arriba: un
198
+ * valor malo es el mismo 500) pero NO se guardan. Nadie las lee de la base (el plan del reconcile toma el techo de
199
+ * `to.groupDepthLimit()`, no de la config persistida), y guardarlas rompía un despliegue mezclado: un proceso alpha.2/
200
+ * alpha.3 relee la fila con su lista blanca (B2, sin esas claves) y su `authz:relations:reconcile` sale con 1. No es el
201
+ * «descartar en silencio» de B2: se conocen y se validan; solo no se escriben. El resto de la spec va TAL CUAL.
202
+ */
203
+ function persistableRelationsSpec(spec) {
204
+ const { maxGroupDepth: _deployment, ...rest } = spec;
205
+ if (rest.database === undefined || rest.database === null || typeof rest.database !== 'object')
206
+ return rest;
207
+ const { enforceGroupDepth: _enforce, ...database } = rest.database;
208
+ return { ...rest, database };
209
+ }
59
210
  /** Lee el spec persistido (o `null` si no hay relaciones declaradas — opt-in). */
60
211
  export async function readRelationsConfigSpec(options = {}) {
61
212
  const row = await guardSql('relations-config', 'read', options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => connectionOf(options).from(TABLE).where('id', ROW_ID).first());
@@ -93,4 +244,3 @@ export async function readRelationsConfig(options = {}) {
93
244
  const spec = await readRelationsConfigSpec(options);
94
245
  return spec ? defineRelationsConfig(spec) : null;
95
246
  }
96
- //# sourceMappingURL=relations_config_store.js.map
@@ -76,4 +76,3 @@ export type RunRelationsReconcileOptions = Omit<RelationsReconcileOptions, 'from
76
76
  * Pásale el manager del servicio, nunca uno construido o envuelto a mano.
77
77
  */
78
78
  export declare function runRelationsReconcile(authorization: Pick<AuthorizationManager, 'withFrozenWrites' | 'relationsDriverKey' | 'relationsDriverFactory'>, options: RunRelationsReconcileOptions): Promise<RelationsReconcileReport>;
79
- //# sourceMappingURL=relations_reconcile.d.ts.map
@@ -82,4 +82,3 @@ export async function runRelationsReconcile(authorization, options) {
82
82
  return report;
83
83
  }, { kind: 'reconcile', operatorAsContext: true });
84
84
  }
85
- //# sourceMappingURL=relations_reconcile.js.map
@@ -89,7 +89,9 @@ interface DatabaseLike {
89
89
  * coincidieran en los primeros 64 COMPARTÍAN lease y se excluían para siempre
90
90
  * — y el modo de fallo era `busy: true`, que no lanza: el relay de uno de los
91
91
  * dos no corría nunca, en silencio. Desde alpha.5 el nombre es un hash
92
- * ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona.
92
+ * ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona entre
93
+ * TABLAS — entre BASES del mismo servidor sí: el «nombre completo» no llevaba la
94
+ * base (rotación 2 · R2-01, abajo).
93
95
  *
94
96
  * **Transición**: `sqlScopeOutbox.acquire()` toma el lock viejo (`legacy`, el
95
97
  * recortado) Y el nuevo durante alpha.5, porque en un rolling deploy un
@@ -118,10 +120,41 @@ export declare const SCOPE_OUTBOX_LEGACY_LOCK_UNTIL = "2.4.0-alpha.6";
118
120
  * desde ≤ alpha.4 en MySQL exige parada completa del relay, no despliegue rodante.
119
121
  */
120
122
  export declare const SCOPE_OUTBOX_TAKES_LEGACY_LOCK = false;
121
- export declare function scopeOutboxLockNames(connection: string | undefined, table: string): {
123
+ /**
124
+ * **La identidad de la BASE en el nombre del lease** (rotación 2 de M4 · R2-01, decisión del dueño 2026-10-22 (1)).
125
+ * `GET_LOCK` es un cerrojo de SERVIDOR, y hasta 2.5.0-alpha.4 el nombre era `<conexión>:<tabla>`: dos despliegues en
126
+ * BASES distintas del mismo MySQL con la conexión por defecto de Adonis y la tabla del stub calculaban el MISMO nombre y
127
+ * se excluían (`busy`, exit 0) — el síntoma que R6 de alpha.5 daba por cerrado nunca lo estuvo, porque el «nombre
128
+ * completo» no llevaba la base. Desde este cierre `current` lleva la base que la PROPIA conexión que sostiene el lease
129
+ * lee con `DATABASE()` (no la del config: es la base contra la que esa sesión resuelve la tabla sin cualificar, o sea la
130
+ * identidad real de la cola; un alias de host o una URL distinta que apunte a la misma base dan el mismo nombre).
131
+ *
132
+ * En MINÚSCULAS a propósito: los dos errores posibles no cuestan lo mismo. Fundir `App` y `app` (dos bases distintas en
133
+ * un servidor sensible a mayúsculas) solo hace que esos dos despliegues compartan lease —vivacidad, `busy`—; separar la
134
+ * misma base escrita de dos formas por dos procesos del MISMO despliegue serían dos escritores —seguridad—. Se elige el
135
+ * error barato. `DATABASE()` `NULL` (sesión sin base por defecto: la tabla tiene que ir cualificada) entra como cadena
136
+ * vacía: los procesos de ese despliegue siguen calculando el mismo nombre.
137
+ *
138
+ * Los tres nombres: `legacy` (el recortado a 64 de ≤ alpha.4, retirado en alpha.7, se conserva para el caso que lo
139
+ * fija), `unscoped` (el sha1 sin base de 2.4.0-alpha.5..2.5.0-alpha.4) y `current` (el sha1 con base). Sin `database`
140
+ * —quien no ha abierto conexión: los casos de identidad pura— `current` es `unscoped`.
141
+ */
142
+ export declare function scopeOutboxLockNames(connection: string | undefined, table: string, database?: string | null): {
122
143
  legacy: string;
144
+ unscoped: string;
123
145
  current: string;
124
146
  };
147
+ /**
148
+ * **La transición del nombre sin base** (rotación 2 · R2-01, firmada: «como alpha.5»). En 2.5.x `acquire()` toma el
149
+ * nombre VIEJO (`unscoped`, el de 2.4.0-alpha.5..2.5.0-alpha.4) y DESPUÉS el nuevo (`current`, con la base), y suelta
150
+ * los dos: en un despliegue rodante un proceso de alpha.4 solo conoce el viejo, y los dos tienen que seguir
151
+ * excluyéndose. **El precio, declarado**: mientras se tome el viejo, dos despliegues en bases distintas del mismo
152
+ * servidor SIGUEN compartiéndolo — R2-01 queda abierto en 2.5.x y se cierra al retirarlo en 2.6 (README «Limits»,
153
+ * `relay.mysql-unscoped-lease`; backlog 2.6). Atado a la versión como en alpha.6: `tests/rotacion2_cierre.spec.ts` exige
154
+ * que valga exactamente «`package.json` < `SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN`» (`2.6.0-alpha.0`: toda 2.6, sus
155
+ * prereleases incluidas), así que el bump a 2.6 pone la suite en rojo hasta retirarlo.
156
+ */
157
+ export declare const SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN = "2.6.0-alpha.0";
158
+ export declare const SCOPE_OUTBOX_TAKES_UNSCOPED_LOCK = true;
125
159
  export declare function sqlScopeOutbox(options?: SqlScopeOutboxOptions, database?: DatabaseLike): ScopeOutbox;
126
160
  export {};
127
- //# sourceMappingURL=scope_outbox.d.ts.map
@@ -85,7 +85,9 @@ const MYSQL_LOCK_NAME_MAX = 64;
85
85
  * coincidieran en los primeros 64 COMPARTÍAN lease y se excluían para siempre
86
86
  * — y el modo de fallo era `busy: true`, que no lanza: el relay de uno de los
87
87
  * dos no corría nunca, en silencio. Desde alpha.5 el nombre es un hash
88
- * ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona.
88
+ * ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona entre
89
+ * TABLAS — entre BASES del mismo servidor sí: el «nombre completo» no llevaba la
90
+ * base (rotación 2 · R2-01, abajo).
89
91
  *
90
92
  * **Transición**: `sqlScopeOutbox.acquire()` toma el lock viejo (`legacy`, el
91
93
  * recortado) Y el nuevo durante alpha.5, porque en un rolling deploy un
@@ -114,13 +116,47 @@ export const SCOPE_OUTBOX_LEGACY_LOCK_UNTIL = '2.4.0-alpha.6';
114
116
  * desde ≤ alpha.4 en MySQL exige parada completa del relay, no despliegue rodante.
115
117
  */
116
118
  export const SCOPE_OUTBOX_TAKES_LEGACY_LOCK = false;
117
- export function scopeOutboxLockNames(connection, table) {
119
+ /**
120
+ * **La identidad de la BASE en el nombre del lease** (rotación 2 de M4 · R2-01, decisión del dueño 2026-10-22 (1)).
121
+ * `GET_LOCK` es un cerrojo de SERVIDOR, y hasta 2.5.0-alpha.4 el nombre era `<conexión>:<tabla>`: dos despliegues en
122
+ * BASES distintas del mismo MySQL con la conexión por defecto de Adonis y la tabla del stub calculaban el MISMO nombre y
123
+ * se excluían (`busy`, exit 0) — el síntoma que R6 de alpha.5 daba por cerrado nunca lo estuvo, porque el «nombre
124
+ * completo» no llevaba la base. Desde este cierre `current` lleva la base que la PROPIA conexión que sostiene el lease
125
+ * lee con `DATABASE()` (no la del config: es la base contra la que esa sesión resuelve la tabla sin cualificar, o sea la
126
+ * identidad real de la cola; un alias de host o una URL distinta que apunte a la misma base dan el mismo nombre).
127
+ *
128
+ * En MINÚSCULAS a propósito: los dos errores posibles no cuestan lo mismo. Fundir `App` y `app` (dos bases distintas en
129
+ * un servidor sensible a mayúsculas) solo hace que esos dos despliegues compartan lease —vivacidad, `busy`—; separar la
130
+ * misma base escrita de dos formas por dos procesos del MISMO despliegue serían dos escritores —seguridad—. Se elige el
131
+ * error barato. `DATABASE()` `NULL` (sesión sin base por defecto: la tabla tiene que ir cualificada) entra como cadena
132
+ * vacía: los procesos de ese despliegue siguen calculando el mismo nombre.
133
+ *
134
+ * Los tres nombres: `legacy` (el recortado a 64 de ≤ alpha.4, retirado en alpha.7, se conserva para el caso que lo
135
+ * fija), `unscoped` (el sha1 sin base de 2.4.0-alpha.5..2.5.0-alpha.4) y `current` (el sha1 con base). Sin `database`
136
+ * —quien no ha abierto conexión: los casos de identidad pura— `current` es `unscoped`.
137
+ */
138
+ export function scopeOutboxLockNames(connection, table, database) {
118
139
  const full = `authz_scope_outbox:${connection ?? 'default'}:${table}`;
140
+ const sha = (value) => `authz_scope_outbox:${createHash('sha1').update(value).digest('hex')}`;
141
+ const unscoped = sha(full);
119
142
  return {
120
143
  legacy: full.slice(0, MYSQL_LOCK_NAME_MAX),
121
- current: `authz_scope_outbox:${createHash('sha1').update(full).digest('hex')}`,
144
+ unscoped,
145
+ current: database === undefined ? unscoped : sha(`${full}@db:${String(database ?? '').toLowerCase()}`),
122
146
  };
123
147
  }
148
+ /**
149
+ * **La transición del nombre sin base** (rotación 2 · R2-01, firmada: «como alpha.5»). En 2.5.x `acquire()` toma el
150
+ * nombre VIEJO (`unscoped`, el de 2.4.0-alpha.5..2.5.0-alpha.4) y DESPUÉS el nuevo (`current`, con la base), y suelta
151
+ * los dos: en un despliegue rodante un proceso de alpha.4 solo conoce el viejo, y los dos tienen que seguir
152
+ * excluyéndose. **El precio, declarado**: mientras se tome el viejo, dos despliegues en bases distintas del mismo
153
+ * servidor SIGUEN compartiéndolo — R2-01 queda abierto en 2.5.x y se cierra al retirarlo en 2.6 (README «Limits»,
154
+ * `relay.mysql-unscoped-lease`; backlog 2.6). Atado a la versión como en alpha.6: `tests/rotacion2_cierre.spec.ts` exige
155
+ * que valga exactamente «`package.json` < `SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN`» (`2.6.0-alpha.0`: toda 2.6, sus
156
+ * prereleases incluidas), así que el bump a 2.6 pone la suite en rojo hasta retirarlo.
157
+ */
158
+ export const SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN = '2.6.0-alpha.0';
159
+ export const SCOPE_OUTBOX_TAKES_UNSCOPED_LOCK = true;
124
160
  /** Hash de 32 bits con signo (lo que acepta `pg_try_advisory_xact_lock`). */
125
161
  function hash32(value) {
126
162
  let hash = 0;
@@ -374,7 +410,6 @@ export function sqlScopeOutbox(options = {}, database = db) {
374
410
  * pasada, que es el precio de que dos pasadas no se pisen.
375
411
  */
376
412
  async acquire() {
377
- const names = scopeOutboxLockNames(connection, table);
378
413
  /** El nombre del cerrojo de PROCESO y del advisory de PG: el completo (allí no hay recorte). */
379
414
  const name = `authz_scope_outbox:${connection ?? 'default'}:${table}`;
380
415
  const client = lockClientOf(database, connection);
@@ -397,11 +432,17 @@ export function sqlScopeOutbox(options = {}, database = db) {
397
432
  return { release: async () => void (await trx.rollback()) };
398
433
  }
399
434
  if (dialect.startsWith('mysql')) {
400
- // **Los DOS locks** (alpha.5 · R6, transición): el nuevo —hash
401
- // estable, sin colisión— y el viejo —el recortado a 64 de alpha.4—,
402
- // para que un rolling deploy con las dos versiones siga siendo
403
- // escritor único. Si cualquiera de los dos falla, se sueltan los que
404
- // se tomaron y es `busy`. El viejo se retira en 2.5.
435
+ // **La base en el nombre** (rotación 2 · R2-01): la lee la MISMA sesión que va a sostener el cerrojo.
436
+ const database = firstValue(await query('acquire', () => trx.rawQuery('select database() as db')), 'db');
437
+ const names = scopeOutboxLockNames(connection, table, database === null || database === undefined ? null : String(database));
438
+ // **Transición** (2.5.x): el VIEJO (sin base) primero y el NUEVO después; si cualquiera falla se sueltan los
439
+ // ya tomados y es `busy`. Un proceso de alpha.4 solo toma el viejo: los dos siguen excluyéndose.
440
+ // (El recortado de ≤ alpha.4 sigue retirado: `SCOPE_OUTBOX_TAKES_LEGACY_LOCK` es `false` desde alpha.7.)
441
+ const order = [
442
+ ...(SCOPE_OUTBOX_TAKES_UNSCOPED_LOCK ? [names.unscoped] : []),
443
+ names.current,
444
+ ...(SCOPE_OUTBOX_TAKES_LEGACY_LOCK ? [names.legacy] : []),
445
+ ];
405
446
  const taken = [];
406
447
  const take = async (lockName) => {
407
448
  const result = await query('acquire', () => trx.rawQuery('select get_lock(?, 0) as taken', [lockName]));
@@ -410,18 +451,38 @@ export function sqlScopeOutbox(options = {}, database = db) {
410
451
  taken.push(lockName);
411
452
  return ok;
412
453
  };
454
+ // `GET_LOCK` es de SESIÓN, no de la transacción: revertir no lo suelta, y una conexión que vuelve al pool
455
+ // con el cerrojo puesto deja la cola `busy` para siempre. Se sueltan TODOS los tomados (en orden inverso)
456
+ // aunque uno falle, y la transacción se revierte al final pase lo que pase.
457
+ let released = false;
413
458
  const releaseAll = async () => {
414
- try {
415
- for (const lockName of taken.reverse())
459
+ if (released)
460
+ return;
461
+ released = true;
462
+ let failure;
463
+ for (const lockName of [...taken].reverse()) {
464
+ try {
416
465
  await trx.rawQuery('select release_lock(?)', [lockName]);
466
+ }
467
+ catch (error) {
468
+ failure ??= error;
469
+ }
417
470
  }
418
- finally {
419
- await trx.rollback();
420
- }
471
+ await trx.rollback();
472
+ if (failure !== undefined)
473
+ throw failure;
421
474
  };
422
- if (!(await take(names.current)) || (SCOPE_OUTBOX_TAKES_LEGACY_LOCK && !(await take(names.legacy)))) {
423
- await releaseAll();
424
- return null;
475
+ try {
476
+ for (const lockName of order) {
477
+ if (!(await take(lockName))) {
478
+ await releaseAll();
479
+ return null;
480
+ }
481
+ }
482
+ }
483
+ catch (error) {
484
+ await releaseAll().catch(() => { });
485
+ throw error;
425
486
  }
426
487
  return { release: releaseAll };
427
488
  }
@@ -532,4 +593,3 @@ export function sqlScopeOutbox(options = {}, database = db) {
532
593
  },
533
594
  };
534
595
  }
535
- //# sourceMappingURL=scope_outbox.js.map
@@ -66,6 +66,58 @@ export declare function isServerWriteLimit(error: unknown): boolean;
66
66
  * `cause`. Se mira la cadena entera de causas.
67
67
  */
68
68
  export declare function isTimeoutLike(error: unknown): boolean;
69
+ /**
70
+ * Las clases de la decisión del dueño 2026-10-17 (3) y (4) (lote «antes de RC» · R3), separadas SIN leer el mensaje
71
+ * (F0 · M-d: 41 formas literales del SDK 0.9.6 contra OpenFGA v1.19, en `tests/fixtures/fga_errors/`):
72
+ *
73
+ * | clase | al llamante | en el relay |
74
+ * |---|---|---|
75
+ * | `timeout` (deadline propio, `ECONNABORTED`/`ETIMEDOUT`, el `FgaError` sin respuesta) | 503 `E_AUTHZ_BACKEND_TIMEOUT` | transitorio |
76
+ * | `transient` (5xx —501 incluido—, 408, 429, `deadline_exceeded`, sin respuesta, ítem `internal_error`; en una ESCRITURA el 504 y `deadline_exceeded` son `timeout`) | 503 `E_AUTHZ_BACKEND_UNAVAILABLE` | transitorio |
77
+ * | `resolution-limit` (`authorization_model_resolution_too_complex`, excepción o ítem) | 503 `E_AUTHZ_RESOLUTION_LIMIT` | transitorio |
78
+ * | `deployment` (401/403/404, 400 con código de la LISTA BLANCA, validación del cliente) | 500 `E_AUTHZ_CONFIG` «no es transitorio» | transitorio (marca) |
79
+ * | `race` (409, 400 `write_failed_due_to_invalid_input`) | 409 `E_AUTHZ_WRITE_CONFLICT` si escapa | transitorio (`isTransientRelayFailure`, cierre de R3) |
80
+ * | `unknown-4xx` (cualquier otro 4xx) | 500 `E_AUTHZ_CONFIG` con `cause` | transitorio (marca) |
81
+ */
82
+ export type FgaFailureKind = 'timeout' | 'transient' | 'resolution-limit' | 'deployment' | 'race' | 'unknown-4xx';
83
+ export interface FgaFailure {
84
+ kind: FgaFailureKind;
85
+ /** El `statusCode` HTTP de la respuesta (ausente sin respuesta y en el error por ítem de `batchCheck`). */
86
+ status?: number;
87
+ /** `apiErrorCode` del SDK, o el valor de la clave del error por ítem (`input_error`/`internal_error`). */
88
+ apiErrorCode?: string;
89
+ }
90
+ /** El código de OpenFGA que dice «el grafo excede lo que resuelvo» (decisión 4: 503 con código propio). */
91
+ export declare const FGA_RESOLUTION_TOO_COMPLEX = "authorization_model_resolution_too_complex";
92
+ /**
93
+ * **La LISTA BLANCA de códigos de despliegue** (F0 · M-d): solo códigos MEDIDOS. Un 400 con un código fuera de ella (y todo 422)
94
+ * es `unknown-4xx` (la clase `FgaApiValidationError` no basta: el 400 de código desconocido es la misma clase). Dentro de
95
+ * la clase no se separan sin el mensaje —y no hace falta, la decisión 3 los junta—: relación/tipo ausente del modelo
96
+ * fijado ↔ contexto ausente en la tupla ↔ `current_time` ausente (los tres `validation_error`), y store ↔ modelo ausente
97
+ * con pin (los dos `authorization_model_not_found`).
98
+ */
99
+ export declare const FGA_DEPLOYMENT_CODES: ReadonlySet<string>;
100
+ /**
101
+ * **`classifyFgaFailure(error)`**: la clase de un fallo de OpenFGA, por `statusCode` + `apiErrorCode` (lista blanca) o,
102
+ * en el error POR ÍTEM de un `batchCheck` 200, por la CLAVE del objeto `error` (`input_error`/`internal_error`). **Jamás
103
+ * lee `message` ni `apiErrorMessage`** (lo vigila la propiedad `fga.errors.classify` de la guardia; las ÚNICAS exenciones,
104
+ * por nombre y firmadas —decisión del dueño 2026-10-18 (5-bis)—, son `isDuplicateWrite`/`isTreeWriteRace`, que leen el
105
+ * SUB-tipo de la carrera «already exists» / «does not exist» y viven ENCIMA de esta clase). PUNTO DE PASO ÚNICO de los
106
+ * TRES sitios: `#guard` del driver de relaciones, `guardBackendErrors` y `alignBatchResults` (los dos drivers).
107
+ */
108
+ export declare function classifyFgaFailure(error: unknown, context?: {
109
+ write?: boolean;
110
+ }): FgaFailure;
111
+ /**
112
+ * **El error PÚBLICO de un fallo de OpenFGA** (R3): `classifyFgaFailure` ⇒ la clase de la tabla de arriba. La causa
113
+ * original viaja SIEMPRE en `cause` (la carrera la leen `isDuplicateWrite`/`isTreeWriteRace` por la cadena). `timeoutMs`
114
+ * solo da la letra del timeout. `failure` deja pasar una clasificación ya hecha (el llamante que necesita mirarla antes).
115
+ */
116
+ export declare function fgaFailureError(driver: string, operation: string, error: unknown, options?: {
117
+ timeoutMs?: number;
118
+ failure?: FgaFailure;
119
+ write?: boolean;
120
+ }): Error;
69
121
  /**
70
122
  * Ejecuta una consulta SQL con deadline y traduce su fallo. El builder de
71
123
  * Lucid es perezoso (se ejecuta al hacer `await`), así que el `timeout` se
@@ -158,4 +210,3 @@ export declare function canonicalScope(resolver: ScopeChainResolver, scope: Scop
158
210
  export declare function canonicalScopeTargets(resolver: ScopeChainResolver, scope: ScopeRef, operation: string): Promise<ScopeRef[]>;
159
211
  /** La cadena canónica, o 422 `E_AUTHZ_UNKNOWN_SCOPE` si el scope no existe (para escrituras). */
160
212
  export declare function assertKnownScope(resolver: ScopeChainResolver, scope: ScopeRef, operation: string): Promise<ScopeRef[]>;
161
- //# sourceMappingURL=backend_guard.d.ts.map