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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +414 -35
  2. package/build/commands/authz_catalog_diff.d.ts +0 -1
  3. package/build/commands/authz_catalog_diff.js +0 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +0 -1
  5. package/build/commands/authz_catalog_prune_orphans.js +0 -1
  6. package/build/commands/authz_catalog_sync.d.ts +0 -1
  7. package/build/commands/authz_catalog_sync.js +0 -1
  8. package/build/commands/authz_freeze.d.ts +0 -1
  9. package/build/commands/authz_freeze.js +0 -1
  10. package/build/commands/authz_reconcile.d.ts +0 -1
  11. package/build/commands/authz_reconcile.js +0 -1
  12. package/build/commands/authz_relations_reconcile.d.ts +0 -1
  13. package/build/commands/authz_relations_reconcile.js +56 -8
  14. package/build/commands/authz_scopes_relay.d.ts +0 -1
  15. package/build/commands/authz_scopes_relay.js +49 -5
  16. package/build/commands/authz_scopes_requeue.d.ts +0 -1
  17. package/build/commands/authz_scopes_requeue.js +14 -2
  18. package/build/commands/authz_unfreeze.d.ts +0 -1
  19. package/build/commands/authz_unfreeze.js +0 -1
  20. package/build/commands/main.d.ts +0 -1
  21. package/build/commands/main.js +0 -1
  22. package/build/commands/openfga_provision.d.ts +10 -1
  23. package/build/commands/openfga_provision.js +43 -3
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +7 -5
  27. package/build/index.js +10 -3
  28. package/build/providers/authz_provider.d.ts +0 -1
  29. package/build/providers/authz_provider.js +0 -1
  30. package/build/services/main.d.ts +0 -1
  31. package/build/services/main.js +0 -1
  32. package/build/services/relations.d.ts +0 -1
  33. package/build/services/relations.js +0 -1
  34. package/build/src/catalog/catalog.d.ts +0 -1
  35. package/build/src/catalog/catalog.js +0 -1
  36. package/build/src/catalog/catalog_cache.d.ts +13 -1
  37. package/build/src/catalog/catalog_cache.js +25 -1
  38. package/build/src/clock.d.ts +0 -1
  39. package/build/src/clock.js +0 -1
  40. package/build/src/define_config.d.ts +0 -1
  41. package/build/src/define_config.js +24 -1
  42. package/build/src/drivers/database_driver.d.ts +6 -1
  43. package/build/src/drivers/database_driver.js +22 -2
  44. package/build/src/drivers/database_relations_driver.d.ts +11 -5
  45. package/build/src/drivers/database_relations_driver.js +90 -18
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +882 -199
  48. package/build/src/drivers/openfga_facts.d.ts +91 -1
  49. package/build/src/drivers/openfga_facts.js +277 -3
  50. package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
  51. package/build/src/drivers/openfga_relations_driver.js +422 -228
  52. package/build/src/errors.d.ts +121 -1
  53. package/build/src/errors.js +153 -1
  54. package/build/src/expiry.d.ts +51 -2
  55. package/build/src/expiry.js +96 -2
  56. package/build/src/freeze.d.ts +0 -1
  57. package/build/src/freeze.js +0 -1
  58. package/build/src/hierarchical_resolver.d.ts +0 -1
  59. package/build/src/hierarchical_resolver.js +0 -1
  60. package/build/src/http/app_access_middleware.d.ts +0 -1
  61. package/build/src/http/app_access_middleware.js +0 -1
  62. package/build/src/http/resource_access_middleware.d.ts +0 -1
  63. package/build/src/http/resource_access_middleware.js +0 -1
  64. package/build/src/identity.d.ts +2 -1
  65. package/build/src/identity.js +4 -1
  66. package/build/src/manager.d.ts +0 -1
  67. package/build/src/manager.js +156 -28
  68. package/build/src/memoize_ancestors.d.ts +0 -1
  69. package/build/src/memoize_ancestors.js +0 -1
  70. package/build/src/models/authz_assignment.d.ts +5 -6
  71. package/build/src/models/authz_assignment.js +0 -1
  72. package/build/src/models/authz_deny.d.ts +5 -6
  73. package/build/src/models/authz_deny.js +0 -1
  74. package/build/src/models/authz_permission.d.ts +5 -6
  75. package/build/src/models/authz_permission.js +0 -1
  76. package/build/src/models/authz_role.d.ts +5 -6
  77. package/build/src/models/authz_role.js +0 -1
  78. package/build/src/models/authz_role_permission.d.ts +5 -6
  79. package/build/src/models/authz_role_permission.js +0 -1
  80. package/build/src/openfga.d.ts +2 -3
  81. package/build/src/openfga.js +4 -2
  82. package/build/src/reconcile.d.ts +0 -1
  83. package/build/src/reconcile.js +0 -1
  84. package/build/src/relation_partition_trigger.d.ts +0 -1
  85. package/build/src/relation_partition_trigger.js +0 -1
  86. package/build/src/relations/check_many.d.ts +0 -1
  87. package/build/src/relations/check_many.js +0 -1
  88. package/build/src/relations/define_relations_config.d.ts +25 -3
  89. package/build/src/relations/define_relations_config.js +8 -3
  90. package/build/src/relations/deny_reach.d.ts +102 -0
  91. package/build/src/relations/deny_reach.js +230 -0
  92. package/build/src/relations/group_depth.d.ts +215 -0
  93. package/build/src/relations/group_depth.js +453 -0
  94. package/build/src/relations/manager.d.ts +14 -7
  95. package/build/src/relations/manager.js +60 -15
  96. package/build/src/relations/page.d.ts +19 -0
  97. package/build/src/relations/page.js +74 -0
  98. package/build/src/relations/reconcile.d.ts +58 -4
  99. package/build/src/relations/reconcile.js +266 -12
  100. package/build/src/relations/tuple_key.d.ts +0 -1
  101. package/build/src/relations/tuple_key.js +0 -1
  102. package/build/src/relations/tuple_order.d.ts +5 -4
  103. package/build/src/relations/tuple_order.js +5 -4
  104. package/build/src/relations_config_store.d.ts +57 -2
  105. package/build/src/relations_config_store.js +155 -5
  106. package/build/src/relations_reconcile.d.ts +0 -1
  107. package/build/src/relations_reconcile.js +0 -1
  108. package/build/src/scope_outbox.d.ts +0 -1
  109. package/build/src/scope_outbox.js +0 -1
  110. package/build/src/shared/backend_guard.d.ts +52 -1
  111. package/build/src/shared/backend_guard.js +164 -2
  112. package/build/src/shared/sql_expiry.d.ts +0 -1
  113. package/build/src/shared/sql_expiry.js +0 -1
  114. package/build/src/shared/transaction_guard.d.ts +0 -1
  115. package/build/src/shared/transaction_guard.js +0 -1
  116. package/build/src/sql_descendants.d.ts +0 -1
  117. package/build/src/sql_descendants.js +0 -1
  118. package/build/src/testing/contract.d.ts +7 -1
  119. package/build/src/testing/contract.js +77 -1
  120. package/build/src/testing/main.d.ts +0 -1
  121. package/build/src/testing/main.js +0 -1
  122. package/build/src/testing/migration_contract.d.ts +0 -1
  123. package/build/src/testing/migration_contract.js +0 -1
  124. package/build/src/testing/relations_contract.d.ts +7 -1
  125. package/build/src/testing/relations_contract.js +174 -7
  126. package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
  127. package/build/src/testing/relations_reconcile_contract.js +0 -1
  128. package/build/src/testing/scope_outbox_contract.d.ts +0 -1
  129. package/build/src/testing/scope_outbox_contract.js +0 -1
  130. package/build/src/testing/scope_tree.d.ts +0 -1
  131. package/build/src/testing/scope_tree.js +0 -1
  132. package/build/src/testing/test_clock.d.ts +0 -1
  133. package/build/src/testing/test_clock.js +0 -1
  134. package/build/src/traits/authz_scopes.d.ts +0 -1
  135. package/build/src/traits/authz_scopes.js +0 -1
  136. package/build/src/traits/has_uuid.d.ts +5 -6
  137. package/build/src/traits/has_uuid.js +0 -1
  138. package/build/src/types.d.ts +89 -14
  139. package/build/src/types.js +4 -1
  140. package/build/stubs/config/authorization.stub +22 -6
  141. package/build/stubs/main.d.ts +0 -1
  142. package/build/stubs/main.js +0 -1
  143. package/package.json +4 -3
  144. package/build/commands/authz_catalog_diff.d.ts.map +0 -1
  145. package/build/commands/authz_catalog_diff.js.map +0 -1
  146. package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
  147. package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
  148. package/build/commands/authz_catalog_sync.d.ts.map +0 -1
  149. package/build/commands/authz_catalog_sync.js.map +0 -1
  150. package/build/commands/authz_freeze.d.ts.map +0 -1
  151. package/build/commands/authz_freeze.js.map +0 -1
  152. package/build/commands/authz_reconcile.d.ts.map +0 -1
  153. package/build/commands/authz_reconcile.js.map +0 -1
  154. package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
  155. package/build/commands/authz_relations_reconcile.js.map +0 -1
  156. package/build/commands/authz_scopes_relay.d.ts.map +0 -1
  157. package/build/commands/authz_scopes_relay.js.map +0 -1
  158. package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
  159. package/build/commands/authz_scopes_requeue.js.map +0 -1
  160. package/build/commands/authz_unfreeze.d.ts.map +0 -1
  161. package/build/commands/authz_unfreeze.js.map +0 -1
  162. package/build/commands/main.d.ts.map +0 -1
  163. package/build/commands/main.js.map +0 -1
  164. package/build/commands/openfga_provision.d.ts.map +0 -1
  165. package/build/commands/openfga_provision.js.map +0 -1
  166. package/build/configure.d.ts.map +0 -1
  167. package/build/configure.js.map +0 -1
  168. package/build/index.d.ts.map +0 -1
  169. package/build/index.js.map +0 -1
  170. package/build/providers/authz_provider.d.ts.map +0 -1
  171. package/build/providers/authz_provider.js.map +0 -1
  172. package/build/services/main.d.ts.map +0 -1
  173. package/build/services/main.js.map +0 -1
  174. package/build/services/relations.d.ts.map +0 -1
  175. package/build/services/relations.js.map +0 -1
  176. package/build/src/catalog/catalog.d.ts.map +0 -1
  177. package/build/src/catalog/catalog.js.map +0 -1
  178. package/build/src/catalog/catalog_cache.d.ts.map +0 -1
  179. package/build/src/catalog/catalog_cache.js.map +0 -1
  180. package/build/src/clock.d.ts.map +0 -1
  181. package/build/src/clock.js.map +0 -1
  182. package/build/src/define_config.d.ts.map +0 -1
  183. package/build/src/define_config.js.map +0 -1
  184. package/build/src/drivers/database_driver.d.ts.map +0 -1
  185. package/build/src/drivers/database_driver.js.map +0 -1
  186. package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
  187. package/build/src/drivers/database_relations_driver.js.map +0 -1
  188. package/build/src/drivers/openfga_driver.d.ts.map +0 -1
  189. package/build/src/drivers/openfga_driver.js.map +0 -1
  190. package/build/src/drivers/openfga_facts.d.ts.map +0 -1
  191. package/build/src/drivers/openfga_facts.js.map +0 -1
  192. package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
  193. package/build/src/drivers/openfga_relations_driver.js.map +0 -1
  194. package/build/src/errors.d.ts.map +0 -1
  195. package/build/src/errors.js.map +0 -1
  196. package/build/src/expiry.d.ts.map +0 -1
  197. package/build/src/expiry.js.map +0 -1
  198. package/build/src/freeze.d.ts.map +0 -1
  199. package/build/src/freeze.js.map +0 -1
  200. package/build/src/hierarchical_resolver.d.ts.map +0 -1
  201. package/build/src/hierarchical_resolver.js.map +0 -1
  202. package/build/src/http/app_access_middleware.d.ts.map +0 -1
  203. package/build/src/http/app_access_middleware.js.map +0 -1
  204. package/build/src/http/resource_access_middleware.d.ts.map +0 -1
  205. package/build/src/http/resource_access_middleware.js.map +0 -1
  206. package/build/src/identity.d.ts.map +0 -1
  207. package/build/src/identity.js.map +0 -1
  208. package/build/src/manager.d.ts.map +0 -1
  209. package/build/src/manager.js.map +0 -1
  210. package/build/src/memoize_ancestors.d.ts.map +0 -1
  211. package/build/src/memoize_ancestors.js.map +0 -1
  212. package/build/src/models/authz_assignment.d.ts.map +0 -1
  213. package/build/src/models/authz_assignment.js.map +0 -1
  214. package/build/src/models/authz_deny.d.ts.map +0 -1
  215. package/build/src/models/authz_deny.js.map +0 -1
  216. package/build/src/models/authz_permission.d.ts.map +0 -1
  217. package/build/src/models/authz_permission.js.map +0 -1
  218. package/build/src/models/authz_role.d.ts.map +0 -1
  219. package/build/src/models/authz_role.js.map +0 -1
  220. package/build/src/models/authz_role_permission.d.ts.map +0 -1
  221. package/build/src/models/authz_role_permission.js.map +0 -1
  222. package/build/src/openfga.d.ts.map +0 -1
  223. package/build/src/openfga.js.map +0 -1
  224. package/build/src/reconcile.d.ts.map +0 -1
  225. package/build/src/reconcile.js.map +0 -1
  226. package/build/src/relation_partition_trigger.d.ts.map +0 -1
  227. package/build/src/relation_partition_trigger.js.map +0 -1
  228. package/build/src/relations/check_many.d.ts.map +0 -1
  229. package/build/src/relations/check_many.js.map +0 -1
  230. package/build/src/relations/define_relations_config.d.ts.map +0 -1
  231. package/build/src/relations/define_relations_config.js.map +0 -1
  232. package/build/src/relations/manager.d.ts.map +0 -1
  233. package/build/src/relations/manager.js.map +0 -1
  234. package/build/src/relations/reconcile.d.ts.map +0 -1
  235. package/build/src/relations/reconcile.js.map +0 -1
  236. package/build/src/relations/tuple_key.d.ts.map +0 -1
  237. package/build/src/relations/tuple_key.js.map +0 -1
  238. package/build/src/relations/tuple_order.d.ts.map +0 -1
  239. package/build/src/relations/tuple_order.js.map +0 -1
  240. package/build/src/relations_config_store.d.ts.map +0 -1
  241. package/build/src/relations_config_store.js.map +0 -1
  242. package/build/src/relations_reconcile.d.ts.map +0 -1
  243. package/build/src/relations_reconcile.js.map +0 -1
  244. package/build/src/scope_outbox.d.ts.map +0 -1
  245. package/build/src/scope_outbox.js.map +0 -1
  246. package/build/src/shared/backend_guard.d.ts.map +0 -1
  247. package/build/src/shared/backend_guard.js.map +0 -1
  248. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  249. package/build/src/shared/sql_expiry.js.map +0 -1
  250. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  251. package/build/src/shared/transaction_guard.js.map +0 -1
  252. package/build/src/sql_descendants.d.ts.map +0 -1
  253. package/build/src/sql_descendants.js.map +0 -1
  254. package/build/src/testing/contract.d.ts.map +0 -1
  255. package/build/src/testing/contract.js.map +0 -1
  256. package/build/src/testing/main.d.ts.map +0 -1
  257. package/build/src/testing/main.js.map +0 -1
  258. package/build/src/testing/migration_contract.d.ts.map +0 -1
  259. package/build/src/testing/migration_contract.js.map +0 -1
  260. package/build/src/testing/relations_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_contract.js.map +0 -1
  262. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  263. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  264. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  265. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  266. package/build/src/testing/scope_tree.d.ts.map +0 -1
  267. package/build/src/testing/scope_tree.js.map +0 -1
  268. package/build/src/testing/test_clock.d.ts.map +0 -1
  269. package/build/src/testing/test_clock.js.map +0 -1
  270. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  271. package/build/src/traits/authz_scopes.js.map +0 -1
  272. package/build/src/traits/has_uuid.d.ts.map +0 -1
  273. package/build/src/traits/has_uuid.js.map +0 -1
  274. package/build/src/types.d.ts.map +0 -1
  275. package/build/src/types.js.map +0 -1
  276. package/build/stubs/main.d.ts.map +0 -1
  277. package/build/stubs/main.js.map +0 -1
@@ -420,6 +420,31 @@ export class CatalogCache {
420
420
  }
421
421
  return this.#reload();
422
422
  }
423
+ /**
424
+ * **La foto para una ESCRITURA** (lote C · carreras, resolución 2 del juez = K4): contrasta la versión con la fila
425
+ * SIEMPRE, ignore o no la ventana `{ everyMs }`. Con `'always'` es exactamente `view()`. La ventana es para DECIDIR
426
+ * (invariante 14: una pregunta puede responder con un catálogo de hasta `everyMs` de antigüedad); una escritura del driver
427
+ * deja ESTADO en el store que no caduca —el barrido de `moved`, el settle, la proyección—, y con un memo rancio un rol
428
+ * recién definido en otro proceso no se barre (fail-open persistente medido, K4). Coste: el SELECT por PK de `'always'`.
429
+ *
430
+ * **Nunca se suma a una comprobación ni a una carga EN VUELO** (asentar · cierre 2, ⚪ 6 / O-6 del auditor): la que ya
431
+ * estaba en marcha pudo leer la fila ANTES de la escritura que precede a esta llamada (la propia, o la de otro proceso
432
+ * que la escritura acaba de observar) y serviría esa foto. Se espera la carga en vuelo y se lee la versión otra vez;
433
+ * una carga que arranca DESPUÉS de esta llamada sí se comparte (ya es posterior).
434
+ */
435
+ async freshView() {
436
+ if (this.#loading)
437
+ await this.#loading.catch(() => undefined);
438
+ const current = this.#view;
439
+ if (!current || !this.#isFresh())
440
+ return this.#reload();
441
+ const dbVersion = await this.#readVersion();
442
+ if (this.#view === current && this.#isFresh() && dbVersion === current.version) {
443
+ this.#checkedAt = this.#now();
444
+ return current;
445
+ }
446
+ return this.#reload(dbVersion);
447
+ }
423
448
  /** Una carga compartida; `knownVersion` es la fila recién leída por una revalidación (se ahorra releerla). */
424
449
  #reload(knownVersion) {
425
450
  if (!this.#loading) {
@@ -663,4 +688,3 @@ function buildCatalogView(permissions, roles, links, loadedAt, version) {
663
688
  version,
664
689
  };
665
690
  }
666
- //# sourceMappingURL=catalog_cache.js.map
@@ -21,4 +21,3 @@ export type Clock = () => Date;
21
21
  export declare const systemClock: Clock;
22
22
  /** `now` tiene que ser una función; se valida al construir (500 `E_AUTHZ_CONFIG` es del llamante). */
23
23
  export declare function isClock(value: unknown): value is Clock;
24
- //# sourceMappingURL=clock.d.ts.map
@@ -4,4 +4,3 @@ export const systemClock = () => new Date();
4
4
  export function isClock(value) {
5
5
  return typeof value === 'function';
6
6
  }
7
- //# sourceMappingURL=clock.js.map
@@ -381,4 +381,3 @@ export declare function defineConfig<T extends AuthorizationConfig>(config: T):
381
381
  * barata (claves, no valores).
382
382
  */
383
383
  export declare function assertAuthorizationConfigShape(config: unknown): void;
384
- //# sourceMappingURL=define_config.d.ts.map
@@ -77,12 +77,36 @@ function assertConfigBlock(value, path) {
77
77
  `una clave conocida no es «nada»: se ignoraría en silencio (plan 2.5 · B2).`);
78
78
  }
79
79
  }
80
+ /**
81
+ * **Lápidas: claves que NO existen aquí pero que la letra de una decisión firmada nombra así** (cierre-2 de R1 · D1 (a),
82
+ * decisión del dueño 2026-10-20: «`relations.maxGroupDepth` ⇒ 500 con mensaje que apunta a `defineRelationsConfig`»).
83
+ * La decisión (5) escribió `relations.maxGroupDepth`; el mando vive en `defineRelationsConfig` (la config que capturan
84
+ * las factories de los drivers, `manager.driver()` y `runRelationsReconcile`) y se lee como `relations.config.maxGroupDepth`.
85
+ * Sin alias (una clave hermana se validaría y no gobernaría tres de las cuatro rutas): la ruta literal es el MISMO 500
86
+ * de clave desconocida, con la receta del sitio bueno en vez de la lista de claves.
87
+ */
88
+ const MISPLACED_CONFIG_KEYS = Object.freeze({
89
+ 'relations.maxGroupDepth': "defineConfig: la clave 'relations.maxGroupDepth' no existe. El techo de anidación de grupos va en la config de " +
90
+ 'RELACIONES: defineRelationsConfig({ objectTypes, maxGroupDepth, database: { enforceGroupDepth } }), y ese objeto en ' +
91
+ 'relations.config. Es la config que capturan los drivers (las factories de relations.drivers), manager.driver() y ' +
92
+ 'runRelationsReconcile (authz:relations:reconcile): una clave hermana de relations no llegaría a ninguno y el límite ' +
93
+ 'no se aplicaría.',
94
+ 'relations.enforceGroupDepth': "defineConfig: la clave 'relations.enforceGroupDepth' no existe. El opt-in del techo de anidación de grupos en " +
95
+ 'database va en la config de RELACIONES: defineRelationsConfig({ objectTypes, maxGroupDepth, database: { ' +
96
+ 'enforceGroupDepth: true } }), y ese objeto en relations.config (la que capturan los drivers).',
97
+ });
80
98
  function assertKnownConfigKeys(value, known, path) {
81
99
  if (!value || typeof value !== 'object' || Array.isArray(value))
82
100
  return;
83
101
  for (const key of Object.keys(value)) {
84
102
  if (known.includes(key))
85
103
  continue;
104
+ // Cierre-3 · R-6: `Object.hasOwn` — una clave `constructor`/`toString` (o un `__proto__` propio de un JSON) leía el
105
+ // prototipo de la tabla y el 500 llevaba de mensaje `function Object() { [native code] }`.
106
+ const dotted = path === '' ? key : `${path}.${key}`;
107
+ const tombstone = Object.hasOwn(MISPLACED_CONFIG_KEYS, dotted) ? MISPLACED_CONFIG_KEYS[dotted] : undefined;
108
+ if (tombstone !== undefined)
109
+ throw new AuthorizationConfigError(tombstone);
86
110
  const where = path === '' ? `'${key}' en la raíz` : `'${path}.${key}'`;
87
111
  throw new AuthorizationConfigError(`defineConfig: la clave ${where} no existe (claves válidas en ${path === '' ? 'la raíz' : path}: ${known.join(', ')}). ` +
88
112
  `Ninguna clave del config se descarta en silencio (plan 2.5 · B2): un typo ('requireWithing') sería una ` +
@@ -121,4 +145,3 @@ export function assertAuthorizationConfigShape(config) {
121
145
  assertKnownConfigKeys(c[block], AUTHORIZATION_CONFIG_KEYS[block], block);
122
146
  }
123
147
  }
124
- //# sourceMappingURL=define_config.js.map
@@ -204,6 +204,12 @@ export declare class DatabaseAuthorizationDriver implements AuthorizationDriver
204
204
  */
205
205
  readonly catalog: CatalogCache;
206
206
  constructor(options?: DatabaseDriverOptions);
207
+ /**
208
+ * **El catálogo de una ESCRITURA** (K4; asentar · cierre 2, D-1): con la versión contrastada en fresco SIEMPRE
209
+ * (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
210
+ * `view()` (el contrato del memo es suyo). Gemelo de `OpenFgaAuthorizationDriver.writeCatalog`.
211
+ */
212
+ private writeCatalog;
207
213
  /**
208
214
  * Vista de este driver con OTRO resolutor de la cadena y el mismo estado
209
215
  * (conexión, memo del catálogo, deadline). Es lo que usa
@@ -393,4 +399,3 @@ export declare class DatabaseAuthorizationDriver implements AuthorizationDriver
393
399
  */
394
400
  private eachReconcileRow;
395
401
  }
396
- //# sourceMappingURL=database_driver.d.ts.map
@@ -327,12 +327,24 @@ export class DatabaseAuthorizationDriver {
327
327
  // `resolveChain` y la barrera del freeze (autoridad). Un despliegue con
328
328
  // pool 1 lo declara `false` (ver `DatabaseDriverOptions`).
329
329
  transactionalWrites: this.transactionalWrites,
330
+ // Estrategia 2.5.0 · firma 4 (D15): `false` EXPLÍCITO — `database` es el driver ESTABLE de 2.5.0 (el experimental
331
+ // es `openfga`). Explícito para que la copia de `authorization.capabilities()` y este objeto digan lo mismo.
332
+ experimental: false,
330
333
  });
331
334
  assertCatalogOptions('DatabaseAuthorizationDriver', options);
332
335
  this.catalog =
333
336
  options.catalog ??
334
337
  new CatalogCache({ driver: 'database', timeoutMs: this.timeoutMs, revalidate: options.catalogRevalidate });
335
338
  }
339
+ /**
340
+ * **El catálogo de una ESCRITURA** (K4; asentar · cierre 2, D-1): con la versión contrastada en fresco SIEMPRE
341
+ * (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
342
+ * `view()` (el contrato del memo es suyo). Gemelo de `OpenFgaAuthorizationDriver.writeCatalog`.
343
+ */
344
+ writeCatalog() {
345
+ const catalog = this.catalog;
346
+ return typeof catalog.freshView === 'function' ? catalog.freshView() : catalog.view();
347
+ }
336
348
  /**
337
349
  * Vista de este driver con OTRO resolutor de la cadena y el mismo estado
338
350
  * (conexión, memo del catálogo, deadline). Es lo que usa
@@ -610,7 +622,11 @@ export class DatabaseAuthorizationDriver {
610
622
  // a lo sumo uno es visible ahí y los demás serían filas huérfanas; el
611
623
  // scope puede no existir ya para el árbol (D8), así que no se filtra por
612
624
  // visibilidad — quitar nunca concede.
613
- const named = rolesToRevoke(await this.catalog.view(), role, scope);
625
+ // Asentar · cierre 2 (D-1 del re-ataque; gemelo del 🟡 5 de `openfga`, K4): el catálogo de una ESCRITURA, con la
626
+ // versión en fresco. Con el memo de `{ everyMs }` un local recién definido en otro proceso que ensombrece al global
627
+ // no salía en `rolesToRevoke`: el DELETE no tocaba su fila y la asignación local seguía concediendo (un «revocado»
628
+ // falso, mudo y persistente).
629
+ const named = rolesToRevoke(await this.writeCatalog(), role, scope);
614
630
  // Sin cadena con la que canonizar, TODAS las ortografías de las que el
615
631
  // uuid puede ser alias (3b-8 · A4, mismo fan-out que `scopes.detached`):
616
632
  // con una sola, un alias hacía del DELETE un no-op silencioso y la fila
@@ -1141,6 +1157,11 @@ export class DatabaseAuthorizationDriver {
1141
1157
  scope_type: target.type,
1142
1158
  scope_uuid: toDbScopeUuid(target),
1143
1159
  };
1160
+ // Cierre-2 de L11 (C1): un deny que el origen guarda con una condición ajena se migra como deny (fail-closed:
1161
+ // perderlo concedería) pero se DECLARA: `invalid-condition` es deriva y el operador decide.
1162
+ if (fact.invalidCondition !== undefined) {
1163
+ note('deny', 'invalid-condition', `${fact.detail} (${fact.invalidCondition})`);
1164
+ }
1144
1165
  usableFacts += 1;
1145
1166
  const denyId = denyKey(row);
1146
1167
  if (wantedDenies.has(denyId))
@@ -1309,4 +1330,3 @@ export class DatabaseAuthorizationDriver {
1309
1330
  }
1310
1331
  }
1311
1332
  }
1312
- //# sourceMappingURL=database_driver.js.map
@@ -88,6 +88,13 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
88
88
  deny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
89
89
  /** `removeDeny`: borra la tupla `denied_<rel>`. Sobre CUALQUIER tipo declarado (D8/R2-4 b). No-op si no está. */
90
90
  removeDeny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
91
+ /**
92
+ * **El techo de anidación de grupos de ESTE driver** (lote «antes de RC» · R1, decisión del dueño 2026-10-17 (5)):
93
+ * `database` no tiene techo técnico (la CTE deduplica y termina con ciclos), así que es `null` SALVO el opt-in
94
+ * explícito `database.enforceGroupDepth: true`, que aplica el `maxGroupDepth` de la config (obligatorio y numérico
95
+ * con el opt-in: lo valida `defineRelationsConfig` al arrancar).
96
+ */
97
+ groupDepthLimit(): Promise<number | null>;
91
98
  unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
92
99
  /**
93
100
  * **F-05 en las LECTURAS y las PURGAS del driver** (2.4.0-alpha.3, cierre
@@ -117,14 +124,14 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
117
124
  * misma función que el doble y `openfga` cuando alinea su `batchCheck`). Tope, `[]` y F-05 por ítem como `checkMany`.
118
125
  */
119
126
  decideMany(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<RelationDecision[]>;
120
- listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, _page?: RelationPage): Promise<RelationObjectsPage>;
121
- listSubjects(relation: string, object: RelObject, partition: ScopeRef, _page?: RelationPage): Promise<RelationSubjectsPage>;
127
+ listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef): Promise<RelationObjectsPage>;
128
+ listSubjects(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
122
129
  /**
123
130
  * **`listDenies`** (lote D · D1): los sujetos con deny DIRECTO de `relation` sobre `object` —hechos, como
124
131
  * `listSubjects` sobre la relación almacenada `denied_<rel>`; no resta ni deriva—. Sobre CUALQUIER tipo declarado
125
132
  * (D8/R2-4 b: con el opt-in retirado, los denies vivos se siguen pudiendo listar y borrar).
126
133
  */
127
- listDenies(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
134
+ listDenies(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
128
135
  /**
129
136
  * **`denyPhase` — ¿admite este driver escrituras de deny en el tipo?** (lote D · D2, R2-5): la fase del opt-in EN
130
137
  * LA CONFIG DE ESTE DRIVER (`'write'`, `'read'` o `null`). F-05 estricto del par (422), cero sentencias: es
@@ -140,7 +147,7 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
140
147
  * directos): en un grupo con `member(g1#member, g2)`, `membersOf(g2)` trae a
141
148
  * los holders de g1 y `listSubjects(member, g2)` solo el userset `g1#member`.
142
149
  */
143
- membersOf(object: RelObject, relation: string, partition: ScopeRef, _page?: RelationPage): Promise<RelationSubjectsPage>;
150
+ membersOf(object: RelObject, relation: string, partition: ScopeRef): Promise<RelationSubjectsPage>;
144
151
  purgeObject(object: RelObject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
145
152
  purgeSubject(subject: RelSubject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
146
153
  /**
@@ -184,4 +191,3 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
184
191
  */
185
192
  enumerateRelationsMany(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
186
193
  }
187
- //# sourceMappingURL=database_relations_driver.d.ts.map
@@ -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();
@@ -844,9 +912,12 @@ export class DatabaseRelationsDriver {
844
912
  */
845
913
  async enumerateRelations(partition, page) {
846
914
  assertPartition(partition);
915
+ // 7.4 (injerto 4, H2/H3): limit entero ≥ 1 y after = un uuid canónico (el cursor que ESTE driver emite) o 422,
916
+ // con cero sentencias; un limit por encima del techo se recorta en #enumerateRows.
917
+ const checked = assertEnumeratePage('enumerateRelations', page, 'uuid', 'database');
847
918
  const wanted = new Map();
848
919
  wanted.set(partitionKey(partition), partition);
849
- return this.#enumerateRows('enumerateRelations', wanted, page);
920
+ return this.#enumerateRows('enumerateRelations', wanted, checked);
850
921
  }
851
922
  /**
852
923
  * **`enumerateRelationsMany` — K particiones en UNA consulta** (2.4.0-alpha.8
@@ -875,7 +946,8 @@ export class DatabaseRelationsDriver {
875
946
  if (!wanted.has(key))
876
947
  wanted.set(key, partition);
877
948
  }
878
- return this.#enumerateRows('enumerateRelationsMany', wanted, page);
949
+ const checked = assertEnumeratePage('enumerateRelationsMany', page, 'uuid', 'database');
950
+ return this.#enumerateRows('enumerateRelationsMany', wanted, checked);
879
951
  }
880
952
  /**
881
953
  * La consulta de `enumerateRelations`/`enumerateRelationsMany`: los hechos
@@ -887,7 +959,8 @@ export class DatabaseRelationsDriver {
887
959
  async #enumerateRows(operation, wanted, page) {
888
960
  const partitionKeys = [...wanted.keys()];
889
961
  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));
962
+ // 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».
963
+ const limit = enumerateLimit(page);
891
964
  const after = page?.after;
892
965
  const rows = await this.#sql(operation, () => {
893
966
  let q = this.#connection()
@@ -959,4 +1032,3 @@ function rowsOf(result) {
959
1032
  }
960
1033
  return result?.rows ?? [];
961
1034
  }
962
- //# sourceMappingURL=database_relations_driver.js.map