@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
@@ -29,13 +29,15 @@ import { AuthorizationBackendTimeoutError, AuthorizationConfigError, InvalidIden
29
29
  import { assertPartition, assertSubject, assertExpiresAt, partitionKey } from '../identity.js';
30
30
  import { relationPartitionTrigger, relationPartitionTriggerDrops } from '../relation_partition_trigger.js';
31
31
  import { isClock, systemClock } from '../clock.js';
32
- import { resolveGrantExpiry, sameInstant } from '../expiry.js';
32
+ import { isActiveExpiry, resolveGrantExpiry, sameInstant } from '../expiry.js';
33
+ import { assertGroupDepthSettings, groupDepthLimitError, groupNestingVerdict, isGroupNesting, plannedByReconcile } from '../relations/group_depth.js';
33
34
  import { sqlExpiryCodec } from '../shared/sql_expiry.js';
34
35
  import { guardSql, isDeadlock, isUniqueViolation, withDeadline } from '../shared/backend_guard.js';
35
36
  import { assertCallerTransaction } from '../shared/transaction_guard.js';
36
37
  import { isRelUserset } from '../types.js';
37
38
  import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject, assertRelationDenyable, assertDenySubject, assertDenyNoExpiry, cutPathAllows, decideFrom, deniableAbove, deniedRelationOf, storedRelationKind, denyPhaseOf, relationsDown, RELATION_DENY_PREFIX, } from '../relations/define_relations_config.js';
38
39
  import { assertCheckManyObjects, distinctObjects, expandByPosition } from '../relations/check_many.js';
40
+ import { assertEnumeratePage, assertNoListPage, enumerateLimit } from '../relations/page.js';
39
41
  const DEFAULT_TIMEOUT_MS = 5_000;
40
42
  /**
41
43
  * **Cota de candidatos de `membersOf` cuando el objeto tiene un deny vigente** (lote D · cierre 1 de D1, 🟡 4): con deny
@@ -58,9 +60,6 @@ const GROUP_MEMBER = 'member';
58
60
  * userset puede colisionar con ella.
59
61
  */
60
62
  const HOLDER_RELATION = '';
61
- /** Tamaño de página por defecto y tope de `enumerateRelations` (origen de reconcile). */
62
- const DEFAULT_ENUMERATE_LIMIT = 100;
63
- const MAX_ENUMERATE_LIMIT = 1_000;
64
63
  /** Formato de id de relación: minúsculas, dígitos y `._-`; sin separadores del store. */
65
64
  const RELATION_ID_FORMAT = /^[a-z0-9_.-]+$/;
66
65
  const DIALECTS = {
@@ -116,6 +115,9 @@ export class DatabaseRelationsDriver {
116
115
  #database;
117
116
  #expiryCodec;
118
117
  constructor(config, options = {}, database = db) {
118
+ // Cierre de R1 · 🟡 4 del auditor: el driver RE-VALIDA el techo de la config (una config extendida con spread no pasó
119
+ // por `defineRelationsConfig`; `NaN` o `enforceGroupDepth: true` sin número apagaban el gate en silencio). 500.
120
+ assertGroupDepthSettings(config.maxGroupDepth, config.enforceGroupDepth, 'DatabaseRelationsDriver');
119
121
  this.#config = config;
120
122
  this.#connectionName = options.connection;
121
123
  this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -137,6 +139,8 @@ export class DatabaseRelationsDriver {
137
139
  // Cierre 2 de D2 · 🟠 N1: renovar (otra caducidad) es delete + insert en UNA transacción (`#upsert`): ningún
138
140
  // instante sin la tupla, así que una membresía que transporta un deny se renueva sin ventana.
139
141
  atomicRenewal: true,
142
+ // Estrategia 2.5.0 · firma 4 (D15): `false` EXPLÍCITO — `database` es el driver ESTABLE de 2.5.0.
143
+ experimental: false,
140
144
  });
141
145
  }
142
146
  /**
@@ -367,6 +371,54 @@ export class DatabaseRelationsDriver {
367
371
  this.#assertObject(object);
368
372
  await this.#deleteTuple('removeDeny', subject, deniedRelationOf(relation), object, partition, options);
369
373
  }
374
+ /**
375
+ * **El techo de anidación de grupos de ESTE driver** (lote «antes de RC» · R1, decisión del dueño 2026-10-17 (5)):
376
+ * `database` no tiene techo técnico (la CTE deduplica y termina con ciclos), así que es `null` SALVO el opt-in
377
+ * explícito `database.enforceGroupDepth: true`, que aplica el `maxGroupDepth` de la config (obligatorio y numérico
378
+ * con el opt-in: lo valida `defineRelationsConfig` al arrancar).
379
+ */
380
+ async groupDepthLimit() {
381
+ return this.#groupDepthLimit();
382
+ }
383
+ #groupDepthLimit() {
384
+ const { maxGroupDepth, enforceGroupDepth } = assertGroupDepthSettings(this.#config.maxGroupDepth, this.#config.enforceGroupDepth, 'DatabaseRelationsDriver');
385
+ return enforceGroupDepth && typeof maxGroupDepth === 'number' ? maxGroupDepth : null;
386
+ }
387
+ /**
388
+ * **El gate de R1 en `database`** (opt-in): la arista `group:<parent>#member@group:<child>#member` que se va a escribir
389
+ * VIVA no puede cerrar un ciclo ni dejar un camino de grupos anidados más largo que el techo. El grafo se LEE POR EL
390
+ * MISMO CLIENTE QUE ESCRIBE (`trx`: la transacción del llamante con `{ transaction }`, o la interna de la escritura) —
391
+ * T-6 del tester: leído por la conexión del motor, un llamante que en UNA transacción escribe `A⊂B` y luego `B⊂C`
392
+ * pasaría las dos (la segunda no ve la primera, sin confirmar). Lo que esto NO cierra, declarado: dos transacciones
393
+ * CONCURRENTES que escriben cada una media cadena (cada una ve su foto, sin cerrojo) — la carrera entre procesos.
394
+ * Una sentencia por nivel del BFS (y por cada 500 grupos del nivel), con la caducidad ESTRICTA de la lectura.
395
+ */
396
+ async #assertGroupNesting(trx, operation, partitionKey, edge, now, limit) {
397
+ const neighbours = async (groups, column) => {
398
+ const out = [];
399
+ for (let i = 0; i < groups.length; i += 500) {
400
+ const chunk = groups.slice(i, i + 500);
401
+ const rows = await this.#sql(`${operation}.groupDepth`, () => this.#whereActive(trx
402
+ .from(RELATIONS_TABLE)
403
+ .where('partition_key', partitionKey)
404
+ .where('object_type', GROUP_TYPE)
405
+ .where('relation', GROUP_MEMBER)
406
+ .where('subject_type', GROUP_TYPE)
407
+ .where('subject_relation', GROUP_MEMBER)
408
+ .whereIn(column, chunk), now).select({ child: 'subject_uuid', parent: 'object_uuid' }));
409
+ for (const row of rows)
410
+ out.push({ child: row.child, parent: row.parent });
411
+ }
412
+ return out;
413
+ };
414
+ const verdict = await groupNestingVerdict(edge, limit, {
415
+ parentsOf: (groups) => neighbours(groups, 'subject_uuid'),
416
+ childrenOf: (groups) => neighbours(groups, 'object_uuid'),
417
+ });
418
+ if (verdict.kind !== 'ok') {
419
+ throw groupDepthLimitError(verdict, `group:${edge.parent}#member@group:${edge.child}#member`, limit);
420
+ }
421
+ }
370
422
  /** El check-then-delete-insert idempotente de `relate`/`deny` sobre la relación ALMACENADA. */
371
423
  async #upsert(operation, subject, relation, object, partition, requested, options) {
372
424
  const partitionKey = this.#keyOf(partition);
@@ -399,10 +451,21 @@ export class DatabaseRelationsDriver {
399
451
  // Los tres estados (invariante 10): omitido preserva la VIGENTE (una
400
452
  // caducada revive sin caducidad), null la quita, Date la fija.
401
453
  const expiresAt = resolveGrantExpiry(previous, requested, now);
454
+ // Idempotente (invariante 6): la misma caducidad no reescribe nada — y va ANTES del gate de R1: un `relate`
455
+ // repetido de una arista preexistente (sembrada antes de alpha.4 o con el gate apagado) sigue siendo un no-op.
456
+ if (current && sameInstant(previous, expiresAt))
457
+ return;
458
+ // R1 (opt-in en `database`): una arista de anidación que queda VIVA —nueva, renovada o revivida— pasa el gate,
459
+ // leyendo el grafo por ESTE `trx` (T-6). Una vencida no conecta nada (la misma regla que la lectura). Antes del
460
+ // DELETE: un 422 no deja la arista vieja borrada.
461
+ const limit = this.#groupDepthLimit();
462
+ if (limit !== null &&
463
+ isGroupNesting(subject, relation, object) &&
464
+ isActiveExpiry(expiresAt, now) &&
465
+ !plannedByReconcile(options, partition, { child: subject.object.id, parent: object.id })) {
466
+ await this.#assertGroupNesting(trx, operation, partitionKey, { child: subject.object.id, parent: object.id }, now, limit);
467
+ }
402
468
  if (current) {
403
- // Idempotente (invariante 6): la misma caducidad no reescribe nada.
404
- if (sameInstant(previous, expiresAt))
405
- return;
406
469
  // **INSERT/DELETE-ONLY (decisión (c))**: cambiar la caducidad es
407
470
  // BORRAR la fila e INSERTAR otra —nunca un UPDATE—; la fila nueva
408
471
  // tiene otro `uuid`, que es lo que lo hace observable. Dentro de la
@@ -592,10 +655,12 @@ export class DatabaseRelationsDriver {
592
655
  }
593
656
  return { distinct, index, facts };
594
657
  }
595
- async listObjects(subject, relation, objectType, partition, _page) {
658
+ async listObjects(subject, relation, objectType, partition) {
596
659
  assertRelationDeclared(this.#config, { type: objectType }, relation);
597
660
  assertPartition(partition);
598
661
  this.#assertId('el tipo de objeto', objectType, RELATION_ID_MAX);
662
+ // 7.4: sin página (un quinto argumento ≠ undefined ⇒ 500 con cero sentencias), después de F-05 y la partición.
663
+ assertNoListPage('listObjects', arguments[4], 'database');
599
664
  const partitionKey = this.#keyOf(partition);
600
665
  const s = this.#subjectColumns(subject, partitionKey);
601
666
  // Lote D · D-15: `listObjects` RESTA (la misma regla de corte que `check`, objeto a objeto).
@@ -630,14 +695,15 @@ export class DatabaseRelationsDriver {
630
695
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
631
696
  return { objects };
632
697
  }
633
- async listSubjects(relation, object, partition, _page) {
698
+ async listSubjects(relation, object, partition) {
634
699
  assertRelationDeclared(this.#config, object, relation);
635
700
  assertPartition(partition);
636
701
  this.#assertObject(object);
637
- return this.#directSubjects('listSubjects', relation, object, partition, _page);
702
+ assertNoListPage('listSubjects', arguments[3], 'database');
703
+ return this.#directSubjects('listSubjects', relation, object, partition);
638
704
  }
639
705
  /** Los sujetos DIRECTOS vigentes de UNA relación almacenada (`listSubjects`; `listDenies` con `denied_<rel>`). */
640
- async #directSubjects(operation, relation, object, partition, _page) {
706
+ async #directSubjects(operation, relation, object, partition) {
641
707
  const partitionKey = this.#keyOf(partition);
642
708
  // DIRECTOS del relation EXACTO (invariante 7): ni transitivo (eso es
643
709
  // `membersOf`) ni derivado por includes. `usersetSubjects: true` ⇒ los
@@ -657,11 +723,12 @@ export class DatabaseRelationsDriver {
657
723
  * `listSubjects` sobre la relación almacenada `denied_<rel>`; no resta ni deriva—. Sobre CUALQUIER tipo declarado
658
724
  * (D8/R2-4 b: con el opt-in retirado, los denies vivos se siguen pudiendo listar y borrar).
659
725
  */
660
- async listDenies(relation, object, partition, page) {
726
+ async listDenies(relation, object, partition) {
661
727
  assertRelationDeclared(this.#config, object, relation);
662
728
  assertPartition(partition);
663
729
  this.#assertObject(object);
664
- return this.#directSubjects('listDenies', deniedRelationOf(relation), object, partition, page);
730
+ assertNoListPage('listDenies', arguments[3], 'database');
731
+ return this.#directSubjects('listDenies', deniedRelationOf(relation), object, partition);
665
732
  }
666
733
  /**
667
734
  * **`denyPhase` — ¿admite este driver escrituras de deny en el tipo?** (lote D · D2, R2-5): la fase del opt-in EN
@@ -679,12 +746,13 @@ export class DatabaseRelationsDriver {
679
746
  * directos): en un grupo con `member(g1#member, g2)`, `membersOf(g2)` trae a
680
747
  * los holders de g1 y `listSubjects(member, g2)` solo el userset `g1#member`.
681
748
  */
682
- async membersOf(object, relation, partition, _page) {
749
+ async membersOf(object, relation, partition) {
683
750
  // Cierre-2 · 🟡 3: F-05 (par, ESTRICTA) también en `membersOf`, la octava
684
751
  // operación del puerto; antes solo `#assertObject` (gramática).
685
752
  assertRelationDeclared(this.#config, object, relation);
686
753
  assertPartition(partition);
687
754
  this.#assertObject(object);
755
+ assertNoListPage('membersOf', arguments[3], 'database');
688
756
  const partitionKey = this.#keyOf(partition);
689
757
  const relations = this.#expandDown(object.type, relation);
690
758
  const meta = this.#dialectMeta();
@@ -692,6 +760,11 @@ export class DatabaseRelationsDriver {
692
760
  const relPlaceholders = relations.map(() => '?').join(', ');
693
761
  const at = this.#expiry.bind(this.#now());
694
762
  const active = this.#activeSql(q, 'r');
763
+ // Rotación 2 · cierre 3b (L12, H1 del auditor): la PUERTA «¿hay algún `denied_*` vigente en el objeto?» va en la
764
+ // MISMA sentencia que los candidatos (una rama más del `UNION`, `foto_kind = 'd'`). Antes era una segunda sentencia y
765
+ // un offboarding suelto (unrelate y luego removeDeny) que caía entre las dos devolvía los candidatos de la primera
766
+ // SIN juzgarlos: listaba a un sujeto que no era miembro en NINGÚN estado real.
767
+ const deniedRelations = deniableAbove(this.#config, object.type, relation).map(deniedRelationOf);
695
768
  // `grp`: los grupos alcanzables por usersets desde (object, RS) —directos y
696
769
  // anidados—. Ancla de UN solo SELECT (MySQL no mezcla UNION ALL con la
697
770
  // recursión): los usersets directos del objeto para cualquier rel de RS.
@@ -709,16 +782,22 @@ export class DatabaseRelationsDriver {
709
782
  `AND r.${q('relation')} = '${GROUP_MEMBER}' AND r.${q('subject_type')} = '${GROUP_TYPE}' ` +
710
783
  `AND r.${q('subject_relation')} = '${GROUP_MEMBER}' AND ${active} ` +
711
784
  `) ` +
712
- // Holders directos del objeto para RS, UNION holders directos de cualquier grupo de `grp`.
713
- `SELECT ${meta.hint}${q('subject_type')} AS ${q('subject_type')}, ${q('subject_uuid')} AS ${q('subject_uuid')} ` +
785
+ // Holders directos del objeto para RS, UNION holders directos de cualquier grupo de `grp` (`foto_kind = 'm'`),
786
+ // UNION la puerta de denies (`foto_kind = 'd'`, una fila como mucho: el `UNION` funde los `NULL`).
787
+ `SELECT ${meta.hint}'m' AS ${q('foto_kind')}, ${q('subject_type')} AS ${q('subject_type')}, ${q('subject_uuid')} AS ${q('subject_uuid')} ` +
714
788
  `FROM ${q(RELATIONS_TABLE)} r ` +
715
789
  `WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = ? AND r.${q('object_uuid')} = ? ` +
716
790
  `AND r.${q('relation')} IN (${relPlaceholders}) AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active} ` +
717
791
  `UNION ` +
718
- `SELECT r.${q('subject_type')}, r.${q('subject_uuid')} FROM ${q(RELATIONS_TABLE)} r ` +
792
+ `SELECT 'm', r.${q('subject_type')}, r.${q('subject_uuid')} FROM ${q(RELATIONS_TABLE)} r ` +
719
793
  `JOIN grp ON r.${q('object_uuid')} = grp.g_uuid ` +
720
794
  `WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = '${GROUP_TYPE}' ` +
721
- `AND r.${q('relation')} = '${GROUP_MEMBER}' AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active}`;
795
+ `AND r.${q('relation')} = '${GROUP_MEMBER}' AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active}` +
796
+ (deniedRelations.length
797
+ ? ` UNION SELECT 'd', NULL, NULL FROM ${q(RELATIONS_TABLE)} r ` +
798
+ `WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = ? AND r.${q('object_uuid')} = ? ` +
799
+ `AND r.${q('relation')} IN (${deniedRelations.map(() => '?').join(', ')}) AND ${active}`
800
+ : '');
722
801
  const bindings = [
723
802
  // grp ancla
724
803
  partitionKey, object.type, object.id, ...relations, at,
@@ -728,22 +807,21 @@ export class DatabaseRelationsDriver {
728
807
  partitionKey, object.type, object.id, ...relations, at,
729
808
  // holders de los grupos
730
809
  partitionKey, at,
810
+ // la puerta de denies (misma sentencia)
811
+ ...(deniedRelations.length ? [partitionKey, object.type, object.id, ...deniedRelations, at] : []),
731
812
  ];
732
813
  const rows = rowsOf(await this.#raw('membersOf', sql, bindings));
733
- const candidates = rows.map((row) => ({ type: String(row.subject_type), uuid: String(row.subject_uuid) }));
814
+ const candidates = rows
815
+ .filter((row) => String(row.foto_kind) === 'm')
816
+ .map((row) => ({ type: String(row.subject_type), uuid: String(row.subject_uuid) }));
817
+ const denied = rows.some((row) => String(row.foto_kind) === 'd');
734
818
  // **Lote D · `membersOf` RESTA** (juez ronda 2: «listObjects y membersOf restan»; D8: siempre, con o sin opt-in).
735
819
  // Si el objeto tiene alguna fila `denied_*` vigente, cada candidato se juzga con `check` —el MISMO corte de camino
736
820
  // que el resto de lecturas, un `check` por candidato—; sin filas deny, cero consultas de más (la respuesta y el coste
737
- // de 2.4.0-alpha.10). Coste declarado: O(miembros) consultas cuando el objeto tiene denies.
738
- const denied = await this.#sql('membersOf.denies', () => this.#whereActive(this.#connection()
739
- .from(RELATIONS_TABLE)
740
- .where('partition_key', partitionKey)
741
- .where('object_type', object.type)
742
- .where('object_uuid', object.id)
743
- .whereIn('relation', deniableAbove(this.#config, object.type, relation).map(deniedRelationOf)), this.#now())
744
- .select('uuid')
745
- .limit(1));
746
- if (denied.length === 0)
821
+ // de 2.4.0-alpha.10). Coste declarado: O(miembros) consultas cuando el objeto tiene denies. La puerta sale de la
822
+ // MISMA sentencia que los candidatos (L12): sin deny en esa foto, todos los candidatos son miembros en ese estado.
823
+ // Con deny, cada candidato se juzga con su `check` (una foto por candidato, como cada posición de `authorizeMany`).
824
+ if (!denied)
747
825
  return { subjects: candidates };
748
826
  // **Cierre 1 de D1 · 🟡 4 del auditor**: con deny, N `check` en serie sin cota ni deadline total era una
749
827
  // amplificación barata para quien puede escribir un deny (disponibilidad, no concesión). Cota DECLARADA
@@ -844,9 +922,12 @@ export class DatabaseRelationsDriver {
844
922
  */
845
923
  async enumerateRelations(partition, page) {
846
924
  assertPartition(partition);
925
+ // 7.4 (injerto 4, H2/H3): limit entero ≥ 1 y after = un uuid canónico (el cursor que ESTE driver emite) o 422,
926
+ // con cero sentencias; un limit por encima del techo se recorta en #enumerateRows.
927
+ const checked = assertEnumeratePage('enumerateRelations', page, 'uuid', 'database');
847
928
  const wanted = new Map();
848
929
  wanted.set(partitionKey(partition), partition);
849
- return this.#enumerateRows('enumerateRelations', wanted, page);
930
+ return this.#enumerateRows('enumerateRelations', wanted, checked);
850
931
  }
851
932
  /**
852
933
  * **`enumerateRelationsMany` — K particiones en UNA consulta** (2.4.0-alpha.8
@@ -875,7 +956,8 @@ export class DatabaseRelationsDriver {
875
956
  if (!wanted.has(key))
876
957
  wanted.set(key, partition);
877
958
  }
878
- return this.#enumerateRows('enumerateRelationsMany', wanted, page);
959
+ const checked = assertEnumeratePage('enumerateRelationsMany', page, 'uuid', 'database');
960
+ return this.#enumerateRows('enumerateRelationsMany', wanted, checked);
879
961
  }
880
962
  /**
881
963
  * La consulta de `enumerateRelations`/`enumerateRelationsMany`: los hechos
@@ -887,7 +969,8 @@ export class DatabaseRelationsDriver {
887
969
  async #enumerateRows(operation, wanted, page) {
888
970
  const partitionKeys = [...wanted.keys()];
889
971
  const declaredTypes = [GROUP_TYPE, ...this.#config.objectTypes.map((t) => t.type)];
890
- const limit = Math.max(1, Math.min(page?.limit ?? DEFAULT_ENUMERATE_LIMIT, MAX_ENUMERATE_LIMIT));
972
+ // La COPIA validada en la entrada (assertEnumeratePage), no la página del llamante: aquí solo el default y el RECORTE al techo, declarado en «Limits».
973
+ const limit = enumerateLimit(page);
891
974
  const after = page?.after;
892
975
  const rows = await this.#sql(operation, () => {
893
976
  let q = this.#connection()
@@ -959,4 +1042,3 @@ function rowsOf(result) {
959
1042
  }
960
1043
  return result?.rows ?? [];
961
1044
  }
962
- //# sourceMappingURL=database_relations_driver.js.map
@@ -142,8 +142,12 @@ export declare function isDuplicateWrite(error: unknown): boolean;
142
142
  export interface OpenFgaDriverOptions {
143
143
  apiUrl: string;
144
144
  storeId: string;
145
- /** Pin del model; si se omite, FGA usa el último. */
146
- modelId?: string;
145
+ /**
146
+ * **Pin del model — OBLIGATORIO** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2)): el driver comprueba
147
+ * UNA vez por proceso que ese modelo es el que genera su config (la puerta L11) y el servidor evalúa con ÉL. Sin él,
148
+ * 500 `E_AUTHZ_CONFIG` al construir con la receta (`openfga:provision` imprime `OPENFGA_MODEL_ID`).
149
+ */
150
+ modelId: string;
147
151
  /** Jerarquía del consumidor (ver ScopeChainResolver). */
148
152
  resolveChain?: ScopeChainResolver;
149
153
  /**
@@ -225,6 +229,14 @@ export interface OpenFgaDriverOptions {
225
229
  * valor «truthy» no es una aceptación.
226
230
  */
227
231
  acceptScopeDriftRisk?: boolean;
232
+ /**
233
+ * **`openfga` es EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21): no apto para producción,
234
+ * con límites declarados en README «Limits» que se cierran en 2.6. Construirlo exige este LITERAL, versionado; sin él
235
+ * (o con `true`, `'acknowledged'`, otra versión…) el constructor lanza 500 `E_AUTHZ_CONFIG` como primera línea, sin
236
+ * crear el cliente. Opcional en el TIPO —como `acceptScopeDriftRisk`— para que un config que solo REGISTRA la factory
237
+ * `openfga` (el stub publicado) siga compilando; obligatorio al construir.
238
+ */
239
+ experimental?: 'openfga@2.5';
228
240
  }
229
241
  /**
230
242
  * **El gate de construcción de `facts`** (3b-2d, panel 2 cruce 4 · S5).
@@ -247,6 +259,13 @@ export declare function assertScopeDriftGuarded(options: {
247
259
  acceptScopeDriftRisk?: boolean;
248
260
  }): void;
249
261
  export declare const DEFAULT_TIMEOUT_MS = 5000;
262
+ /**
263
+ * **Vueltas del settle** (lote C · carreras, resolución 7 del juez; linaje L13): aplicaciones que
264
+ * `settleRoleVisibility` intenta antes de rendirse. Al agotarlas BORRA las aristas `scope#binding` de los pares (fail-CLOSED,
265
+ * intra-tenant: el churn exige `within` en los dos padres) y lanza 409 `E_AUTHZ_WRITE_CONFLICT` marcado PARCIAL
266
+ * (invariante 13: `indeterminate: true` en `onWrite`).
267
+ */
268
+ export declare const SETTLE_MAX_ROUNDS = 5;
250
269
  /**
251
270
  * Cota de páginas de una enumeración (1.000.000 de tuplas a 100 por página).
252
271
  * Un `continuation_token` que no avanza —un servidor roto, un proxy o una
@@ -284,6 +303,7 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
284
303
  canonicalScopeReads: false;
285
304
  enumerateFacts: true;
286
305
  transactionalWrites: false;
306
+ experimental: true;
287
307
  }>;
288
308
  private client;
289
309
  private chainResolver;
@@ -306,6 +326,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
306
326
  * con el que este driver escribe y responde, no el último del store.
307
327
  */
308
328
  private readonly modelId;
329
+ /**
330
+ * **La comparación modelo ↔ config de L11, UNA por proceso** (`modelGate`). Es un OBJETO que se MUTA y nunca se
331
+ * reasigna (H11 b): las vistas `withChainResolver`/`withClock` son `Object.create(this)` y lo heredan por prototipo,
332
+ * así que `forRequest()` no relee el modelo por petición.
333
+ */
334
+ private readonly modelMemo;
309
335
  private consistency;
310
336
  /** Reloj de pared del driver (J1): el ÚNICO `now` de checks, filtros y re-grant. */
311
337
  private now;
@@ -355,6 +381,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
355
381
  * `authorize` evaluaba el deny en un instante y el rol en otro.
356
382
  */
357
383
  private checkContext;
384
+ /**
385
+ * **La lectura del modelo, UNA por proceso** (L11): `readAuthorizationModel` del `modelId` FIJADO y
386
+ * `factsModelDifferences` con `slice: 'roles'` (los holders, `role`, `role_binding`, `scope` y las condiciones; la
387
+ * igualdad es «módulo el catálogo»: `P_M` sale del propio modelo). Se memoriza el ÉXITO —también con diferencias— y un
388
+ * generador que no puede generar (500); **un fallo al LEER el modelo NO se memoriza, sea 503 o 500** (cierre de L11).
389
+ */
390
+ private modelGate;
391
+ /**
392
+ * **La puerta L11 del driver de ROLES** (lote «antes de RC», decisión del dueño 2026-10-17 (1); hasta L11 este driver
393
+ * no comparaba NINGUNA regla —P7: solo el conjunto de `permits_*`, «no es una barrera de seguridad»—: un `can_<P>` sin
394
+ * `but not denied_<P>`, un `rooted` cambiado o un `assignee` sin `not_expired` concedían o negaban sin error). Toda
395
+ * operación que llega al store la pasa ENTERA, justo después de validar su entrada y antes de la primera llamada
396
+ * (catálogo, resolutor y cliente incluidos); `enumerateFacts` —el ORIGEN de una migración— solo la parte
397
+ * `conditions`. Distinta ⇒ 500 `E_AUTHZ_CONFIG` con las rutas y CERO llamadas que evalúen o escriban.
398
+ */
399
+ private assertModel;
358
400
  /**
359
401
  * Consulta al catálogo local clasificando su fallo. Con este driver el
360
402
  * catálogo SQL sigue siendo una dependencia dura de cada pregunta: su caída
@@ -368,6 +410,58 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
368
410
  * alineada por ese id: un resultado por check, ni uno más ni uno menos.
369
411
  */
370
412
  private batchCheckAll;
413
+ /**
414
+ * **El catálogo de una ESCRITURA** (K4, resolución 2 del juez): con la versión contrastada en fresco SIEMPRE
415
+ * (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
416
+ * `view()` (el contrato del memo es suyo).
417
+ */
418
+ private writeCatalog;
419
+ /**
420
+ * K4 (segunda mitad): un uuid de rol que el STORE tiene y el catálogo de la escritura no conoce fuerza UNA recarga del
421
+ * memo (`invalidate` + foto en fresco). Si tras ella sigue ausente, el rol se borró: quien llama BORRA su arista.
422
+ */
423
+ private writeCatalogKnowing;
424
+ /**
425
+ * Un `batchCheck` con `HIGHER_CONSISTENCY` para el settle, SIN colapsar los errores por ítem: el llamante decide qué
426
+ * hacer con un `E_AUTHZ_RESOLUTION_LIMIT` de `ancestor` (caer a `storeChain`, condición 2 de la resolución 9) y propaga
427
+ * el resto (clasificados por el MISMO `fgaFailureError` que `alignBatchResults`).
428
+ */
429
+ private checkForWrite;
430
+ /**
431
+ * **La decisión del settle**, con el STORE (todas las lecturas con HIGHER, en UN `batchCheck`): para cada par, si la
432
+ * arista TIENE que estar (`want`) y si ESTÁ (`has`). `want` exige, por este orden:
433
+ * - el rol en el catálogo de la escritura (K4: en fresco y, si falta, tras una recarga; si sigue faltando, `known: false`
434
+ * y la arista se borra — «rol ausente ⇒ borra»);
435
+ * - declarado para el NIVEL del scope (3b-2g · R1);
436
+ * - `role_binding#role` VIVO (auditor ⚪ 8: tras un `revoke`/`purgeScope`/`purgeRole` concurrente el settle no resucita
437
+ * la arista; `liveness: false` solo en la decisión PREVIA del `grant`, que escribe esa arista en el mismo `Write`);
438
+ * - y, si el rol es LOCAL, su owner ancestro-o-igual del scope EN EL STORE: atajo `scope == owner` (`ancestor` no es
439
+ * reflexivo, K6), `Check(scope:<owner> ancestor scope:<S>)` y, si ese `Check` excede la cota del servidor (medido en M2:
440
+ * solo cuando el owner SÍ es ancestro a > 26 saltos), la cadena del store salto a salto (`storeChain`) — nunca «borrar
441
+ * por si acaso» (fail-closed aleatorio cerca de la cota). La laxitud de `ancestor` ante DOS padres es deriva que ya
442
+ * denuncian `moved` y reconcile (riesgo declarado del juez).
443
+ */
444
+ private decideRoleVisibility;
445
+ /**
446
+ * **El ÚNICO sitio que da de ALTA una arista `scope#binding` de rol local** (y borra las que se le pidan), en lotes ≤ 100
447
+ * (`Write` con `Ignore` en las dos direcciones: dice el estado que DEBE quedar, no el delta — invariante 6).
448
+ */
449
+ private writeBindingEdges;
450
+ /**
451
+ * **`settleRoleVisibility`** — el punto de paso de L13. Decide con el store (`decideRoleVisibility`), y si alguna
452
+ * arista no está como tiene que estar, aplica la diferencia y VUELVE A LEER; termina cuando una lectura POSTERIOR a su
453
+ * última escritura ve el store coherente (punto fijo: «borro una vez» deja CLOSED persistente al entrar, R1-entrar).
454
+ *
455
+ * **Cota** (resolución 7): tras `SETTLE_MAX_ROUNDS` aplicaciones sin converger (un churn que mueve el árbol en cada
456
+ * relectura) se RINDE: borra las aristas de los pares que NO convergieron —y, en el mismo `Write`, `giveUpDeletes` (el `assignee` de un
457
+ * `grant` que CREÓ la asignación: vuelta a «no ocurrió»)— y lanza 409 `E_AUTHZ_WRITE_CONFLICT` marcado PARCIAL
458
+ * (`indeterminate: true`). Fail-CLOSED declarado e intra-tenant; en un re-grant el 409 NO significa «no asignado».
459
+ *
460
+ * Devuelve los pares cuyo rol el catálogo ya no conoce (`known: false`): el `grant` que los CREÓ deshace su asignación.
461
+ */
462
+ private settleRoleVisibility;
463
+ /** Las vueltas del settle (lo que hasta el cierre 1 era el cuerpo de `settleRoleVisibility`). */
464
+ private settleRounds;
371
465
  private findPermission;
372
466
  /**
373
467
  * Filtro por catálogo de las lecturas de membresía (`listRoles`,
@@ -438,6 +532,13 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
438
532
  */
439
533
  private rejectTransaction;
440
534
  grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
535
+ /**
536
+ * El cuerpo de ESCRITURA del `grant` (sin la decisión de visibilidad): lee la asignación, decide entre no-op, write o
537
+ * delete+write, y re-aplica ante una carrera. `structure` son las aristas que van con el `assignee` en el MISMO `Write`
538
+ * (las tres para un rol global; sin la `scope#binding` si el store dice que el rol local no es visible ahí). `progress`
539
+ * cuenta los `Write` INTENTADOS (el settle del `grant` local corre si hubo alguno, también si falló: invariante 13).
540
+ */
541
+ private writeGrant;
441
542
  /**
442
543
  * Write directo de la asignación CON su estructura (3b-2f · R3); si algo ya
443
544
  * estaba, camino largo. Devuelve si existía la ASIGNACIÓN —lo dice la
@@ -550,8 +651,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
550
651
  * borrar una tupla inexistente—, la cadena del padre NUEVO en el store
551
652
  * (3b-8 · A5: el anti-ciclos del consumidor no ve un store
552
653
  * desincronizado) y **UN solo `Write`** con el delete del padre viejo y el
553
- * write del nuevo, que es atómico dentro de la request. O(profundidad)
554
- * requests, UNA mutación.
654
+ * write del nuevo, que es atómico PARA EL ESCRITOR (el store nunca guarda
655
+ * los dos padres ni ninguno) pero NO para un lector concurrente: un `Check`
656
+ * que dura lo que el `Write` puede leer la arista vieja por una rama del
657
+ * modelo y la nueva por otra (lector rasgado, fila «torn read» de Limits del
658
+ * README; medido 1,2–7,1 %, decisión del dueño 6b). O(profundidad) requests;
659
+ * con roles locales, además el barrido F4 (ver `reparent`).
555
660
  */
556
661
  onScopeMoved(child: ScopeRef, newParent: ScopeRef): Promise<void>;
557
662
  /**
@@ -623,8 +728,8 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
623
728
  */
624
729
  private reparent;
625
730
  /**
626
- * **El barrido del rol local** (3b-2e · E1; decisión del dueño del
627
- * 2026-08-30, opción 1).
731
+ * **El barrido PREVIO del rol local** (3b-2e · E1; decisión del dueño del
732
+ * 2026-08-30, opción 1; F4: solo borra).
628
733
  *
629
734
  * En (c2) el modelo no tiene `owner`, así que `authorize` NO vuelve a
630
735
  * decidir con el árbol de hoy si un rol LOCAL sigue siendo visible: un
@@ -634,11 +739,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
634
739
  * un **fail-open** — y encima uno que solo se ve comparando drivers.
635
740
  *
636
741
  * Lo que se toca es la arista `scope#binding`, que es lo que hace
637
- * ALCANZABLE la asignación: se BORRA donde el owner del rol ya no está en
638
- * la cadena y se REESCRIBE donde vuelve a estarlo (invariante 18: volver la
639
- * unit a su sitio restaura). No se toca el `assignee` —el hecho de la
640
- * asignación no cambia porque el árbol se mueva, igual que en `database`—
641
- * ni nada de un rol GLOBAL, cuya visibilidad no depende del árbol.
742
+ * ALCANZABLE la asignación: el barrido previo la BORRA donde el owner del
743
+ * rol no estará en la cadena NUEVA; las ALTAS (volver a su sitio restaura)
744
+ * son del settle de después (`settleSubtree`), el punto de paso único de
745
+ * L13. No se toca el `assignee` —el hecho de la asignación no cambia porque
746
+ * el árbol se mueva, igual que en `database`— ni nada de un rol GLOBAL, cuya
747
+ * visibilidad no depende del árbol.
642
748
  *
643
749
  * **Por subárbol, no por nodo** (consecuencia 2): los descendientes del
644
750
  * nodo movido también cambian de cadena.
@@ -647,9 +753,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
647
753
  * que no usa delegación— son **cero** requests y `moved` sigue siendo el
648
754
  * `Read` + `Write` del cruce 8. Con roles locales: una lectura por rol local
649
755
  * (sus bindings, por `role_binding#role`), la bajada del subárbol y un
650
- * `Write` por lote.
756
+ * `Write` por lote (el coste O(roles locales) por `moved` es el 🟡 6, declarado hasta 2.6).
651
757
  */
652
758
  private sweepLocalRoleBindings;
759
+ /**
760
+ * Los bindings de roles LOCALES que cuelgan del subárbol de `movedKey` en el STORE (HIGHER), con la cadena de cada
761
+ * scope del subárbol. El catálogo es el de una ESCRITURA (K4: en fresco), y un uuid que el store tiene y el catálogo no
762
+ * conoce fuerza una recarga (`writeCatalogKnowing`); los roles se enumeran por el catálogo (el amarillo 6 —enumerar
763
+ * desde el store— es de 2.6). `chain` es la cadena que el nodo TENDRÁ (barrido previo) o, sin ella, la del store.
764
+ */
765
+ private localBindingsBelow;
766
+ /**
767
+ * **El settle del subárbol de un cambio de árbol** (lote C · L13): los bindings de roles locales del subárbol de
768
+ * `movedKey` (en el store, HIGHER, catálogo en fresco) pasan por `settleRoleVisibility`. Lo llaman `reparent` tras la
769
+ * arista, su atajo idempotente y su `finally`.
770
+ */
771
+ private settleSubtree;
653
772
  /**
654
773
  * **El barrido por NIVEL** (3b-2g · R1; decisión del dueño del 2026-08-30
655
774
  * (2), mismo mecanismo que E1).
@@ -660,18 +779,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
660
779
  * —donde `declaredRoleAt` se evalúa en cada pregunta— y **sigue
661
780
  * concediendo** en `facts`, que es la divergencia R1 del lote 2e.
662
781
  *
663
- * Se cierra igual que el owner: barriendo la arista `scope#binding` de los
664
- * bindings de ESE rol con la regla única de visibilidad. Lo llama
782
+ * Se cierra igual que el owner: dejando la arista `scope#binding` de los
783
+ * bindings de ESE rol como dice la regla única de visibilidad. Lo llama
665
784
  * `projectCatalogRole`, que es el hook de «una escritura de catálogo cambió
666
785
  * este rol»: el manager lo dispara tras `defineScopedRole`/`updateScopedRole`
667
786
  * y un escritor «a mano» de `authz_*` tiene el mismo deber que ya tenía con
668
787
  * el espejo de permisos (sin él, en `facts` un rol recién definido no
669
788
  * concedería nada y quitarle un permiso seguiría concediéndolo).
670
789
  *
671
- * Coste: una lectura (los bindings del rol) y, **solo si el rol es LOCAL y
672
- * tiene bindings**, la cadena del store de cada scope distinto donde cuelga
673
- * uno; un `Write` por lote si hay algo que barrer. Un rol sin bindings —el
674
- * caso de todo `defineScopedRole`— son 0 escrituras.
790
+ * **Desde el lote C pasa ENTERO por el settle** (L13; 🟠 1 de F4-carreras: este barrido leía `storeChain` y escribía
791
+ * DESPUÉS, y un `moved` que barriera entre medias quedaba pisado — fail-OPEN persistente entre tenants). El catálogo es
792
+ * el de una escritura (K4, hallazgo 4 del panel TREE): con un memo rancio un rol recién definido en otro proceso se
793
+ * ignoraba. Un rol que el catálogo ya no declara —tras la recarga— no tiene visibilidad que decidir: sus aristas se BORRAN
794
+ * («rol ausente ⇒ borra», K4); el resto de sus hechos son de `purgeRole`/`authz:reconcile`.
795
+ *
796
+ * Coste: una lectura (los bindings del rol) y un `batchCheck` (HIGHER) por vuelta del settle; un `Write` por lote solo
797
+ * si hay algo que corregir. Un rol sin bindings —todo `defineScopedRole`— son 0 escrituras.
675
798
  */
676
799
  private sweepRoleVisibility;
677
800
  /**
@@ -681,15 +804,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
681
804
  * NIVEL de ese scope (3b-2g · R1) y ser global o tener a su owner en la
682
805
  * cadena (3b-2e · E1). Visible ⇒ la arista se (re)escribe; no visible ⇒ se
683
806
  * borra. El `assignee` no se toca: la asignación existe igual, lo que
684
- * cambia es dónde se la ve.
807
+ * cambia es dónde se la ve. (Desde el lote C solo lo usa el barrido PREVIO, que solo borra.)
685
808
  */
686
809
  private classifyBindingEdge;
687
810
  /**
688
- * Aplica el barrido en lotes ≤ 100 (el límite del `Write`).
689
- *
690
- * `Ignore` en las dos direcciones: el barrido dice el estado que DEBE
691
- * quedar, no el delta — borrar lo que ya no está y reescribir lo que ya
692
- * estaba son no-ops, no errores (invariante 6).
811
+ * Aplica los BORRADOS de un barrido (lote C · L13: `applyBindingSweep` ya no da ALTAS; las altas de `scope#binding` de
812
+ * rol local solo salen del settle). Borrar nunca concede, así que va directo, en lotes ≤ 100 con `Ignore`.
693
813
  */
694
814
  private applyBindingSweep;
695
815
  /**
@@ -698,7 +818,8 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
698
818
  * tiene que gobernar el barrido; el resolutor del consumidor no participa.
699
819
  * Un nodo con más de un padre es deriva y se dice (`assertOneParent`), y el
700
820
  * recorrido está acotado por el mismo tope de páginas que las
701
- * enumeraciones: un ciclo escrito a mano no cuelga el proceso.
821
+ * enumeraciones: un ciclo escrito a mano no cuelga el proceso. Alimenta
822
+ * escrituras (anti-ciclos de `reparent`, barrido, caída del settle) ⇒ HIGHER (hallazgo 3 TREE).
702
823
  */
703
824
  private storeChain;
704
825
  /**
@@ -826,6 +947,13 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
826
947
  * divergencia R1 del lote 2e.
827
948
  */
828
949
  projectCatalogRole(roleUuid: string): Promise<void>;
950
+ /**
951
+ * La proyección `permits_<P>` de UN rol (la otra mitad de `projectCatalogRole`). **Hallazgo 4 del panel TREE (K4)**: el
952
+ * catálogo es el de una ESCRITURA (`writeCatalog`, versión en fresco) — con un memo `{ everyMs }` que todavía no conoce
953
+ * un rol recién definido en otro proceso, `rolePermissionsOf` devolvía `[]` y esto BORRABA sus `permits_*` (fail-closed
954
+ * medido: `dave@C`). Mutante `project.memo-catalog`.
955
+ */
956
+ private projectRolePermits;
829
957
  /**
830
958
  * **Reconstruye el store desde `authz_*` + el árbol del consumidor.**
831
959
  *
@@ -1085,6 +1213,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
1085
1213
  * default `?? []` sobre `tuples` vuelva a este fichero.
1086
1214
  */
1087
1215
  private tuplesOrThrow;
1216
+ /**
1217
+ * **`readAllTuples` para una lectura que ALIMENTA UNA ESCRITURA** (lote C · carreras, resolución 8; hallazgo 3 TREE):
1218
+ * con `HIGHER_CONSISTENCY` siempre (`WRITE_CONSISTENCY`). Es el helper ÚNICO: la guardia de fuente de
1219
+ * `tests/visibility_settle.spec.ts` exige que los métodos que escriben lean por aquí (o por `checkForWrite`).
1220
+ */
1221
+ private readForWrite;
1088
1222
  private readAllTuples;
1089
1223
  }
1090
1224
  /**
@@ -1120,4 +1254,3 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
1120
1254
  * en silencio los datos de quien ya usaba ese nombre).
1121
1255
  */
1122
1256
  export declare const OWN_FACTS_OBJECT_TYPES: ReadonlySet<string>;
1123
- //# sourceMappingURL=openfga_driver.d.ts.map