@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
@@ -28,6 +28,7 @@ import { assertNotFrozenRow } from '../freeze.js';
28
28
  import { isRelUserset, normalizeRelationsCapabilities } from '../types.js';
29
29
  import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject, assertRelationDenyable, assertDenySubject, assertDenyNoExpiry, } from './define_relations_config.js';
30
30
  import { assertCheckManyObjects } from './check_many.js';
31
+ import { assertEnumeratePage, assertNoListPage } from './page.js';
31
32
  /**
32
33
  * ¿Cada POSICIÓN del array es un boolean? Por índice, nunca con `some`/`every`
33
34
  * (que saltan los huecos de un array disperso: 🟡 A3 del auditor del lote A).
@@ -327,7 +328,8 @@ export class RelationsManager {
327
328
  timeoutMs: this.#options.freezeTimeoutMs,
328
329
  });
329
330
  }
330
- async relate(subject, relation, object, partition, options) {
331
+ async relate(subject, relation, object, partition, rawOptions) {
332
+ const options = knownWriteOptions(rawOptions);
331
333
  this.#assertTransactional(options, 'relate');
332
334
  await this.#assertNotFrozen('relate');
333
335
  this.#assertDeclared(object, relation);
@@ -346,7 +348,8 @@ export class RelationsManager {
346
348
  await this.#write(event, () => this.#driver.relate(subject, relation, object, partition, options));
347
349
  await this.#notify(event);
348
350
  }
349
- async unrelate(subject, relation, object, partition, options) {
351
+ async unrelate(subject, relation, object, partition, rawOptions) {
352
+ const options = knownWriteOptions(rawOptions);
350
353
  this.#assertTransactional(options, 'unrelate');
351
354
  await this.#assertNotFrozen('unrelate');
352
355
  this.#assertDeclared(object, relation);
@@ -474,12 +477,13 @@ export class RelationsManager {
474
477
  await this.#notify(event);
475
478
  }
476
479
  /** Los sujetos con deny DIRECTO de `relation` sobre `object` (hechos: no resta ni deriva). Cualquier tipo declarado (D8). */
477
- async listDenies(relation, object, partition, page) {
480
+ async listDenies(relation, object, partition) {
478
481
  this.#assertDeclared(object, relation);
479
482
  this.#assertDenies('listDenies');
480
483
  assertPartition(partition);
481
484
  this.#assertObjectId(object);
482
- return this.#driver.listDenies(relation, object, partition, page);
485
+ assertNoListPage('listDenies', arguments[3], this.#driverLabel());
486
+ return this.#driver.listDenies(relation, object, partition);
483
487
  }
484
488
  /**
485
489
  * **La primitiva de CADENA** (lote D · D9, decisión del dueño 2026-10-14): una decisión por objeto, POR POSICIÓN —
@@ -509,6 +513,10 @@ export class RelationsManager {
509
513
  }
510
514
  return answers;
511
515
  }
516
+ /** El nombre del driver en los mensajes (la clave en `relations.drivers`, como en las puertas de `{ transaction }`). */
517
+ #driverLabel() {
518
+ return this.#options.driverName ?? 'relations';
519
+ }
512
520
  /**
513
521
  * **D-28 · la capacidad `relationDenies`** (regla D15: opcional, normalizada a `false`): sin ella, o sin el método
514
522
  * del puerto, las cuatro operaciones del deny son 500 `E_AUTHZ_UNSUPPORTED` nombrándose, con CERO llamadas al
@@ -520,17 +528,26 @@ export class RelationsManager {
520
528
  throw new UnsupportedOperationError(operation, 'el deny de relaciones', this.#options.driverName ?? 'relations', `El driver no declara relationDenies: true (o no trae '${operation}'): no sabe escribir ni honrar un deny. ` +
521
529
  `Los dos drivers del paquete (database, openfga) lo traen desde 2.5.0-alpha.3.`);
522
530
  }
523
- async listObjects(subject, relation, objectType, partition, page) {
531
+ /**
532
+ * **Los cuatro listados no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez): `page` salió de la firma
533
+ * de `listObjects`/`listSubjects`/`listDenies`/`membersOf` (nunca hizo nada) y un valor distinto de `undefined` que
534
+ * llegue por JS o `as any` es 500 `E_AUTHZ_UNSUPPORTED` con CERO llamadas al driver (`assertNoListPage`), DESPUÉS de
535
+ * F-05 y de la partición (RC74-5: primero la identidad de la pregunta). Sin página, la lista es completa o lleva
536
+ * `truncated`; nunca un cursor.
537
+ */
538
+ async listObjects(subject, relation, objectType, partition) {
524
539
  this.#assertDeclared({ type: objectType }, relation);
525
540
  assertPartition(partition);
526
541
  this.#assertSubject(subject, partition);
527
- return this.#driver.listObjects(subject, relation, objectType, partition, page);
542
+ assertNoListPage('listObjects', arguments[4], this.#driverLabel());
543
+ return this.#driver.listObjects(subject, relation, objectType, partition);
528
544
  }
529
- async listSubjects(relation, object, partition, page) {
545
+ async listSubjects(relation, object, partition) {
530
546
  this.#assertDeclared(object, relation);
531
547
  assertPartition(partition);
532
548
  this.#assertObjectId(object);
533
- return this.#driver.listSubjects(relation, object, partition, page);
549
+ assertNoListPage('listSubjects', arguments[3], this.#driverLabel());
550
+ return this.#driver.listSubjects(relation, object, partition);
534
551
  }
535
552
  /**
536
553
  * **Las purgas notifican** (2.4.0-alpha.3 · E, hallazgo #4; D-1/D-3): UN
@@ -592,7 +609,7 @@ export class RelationsManager {
592
609
  * `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
593
610
  * trunca), jamás un `skip`.
594
611
  */
595
- async membersOf(object, relation, partition, page) {
612
+ async membersOf(object, relation, partition) {
596
613
  // Cierre-2 · 🟡 3: `membersOf` toma `(object, relation)` —la forma exacta
597
614
  // que F-05 guarda— y era la octava operación fuera de «las siete»; un
598
615
  // driver de terceros con `membersOfNative: true` sobre un store compartido
@@ -605,7 +622,8 @@ export class RelationsManager {
605
622
  throw new UnsupportedOperationError('membersOf', 'la membresía transitiva de un grupo', 'relations', `Solo el driver 'database' la resuelve (CTE recursiva); en 'openfga' la transitiva sería ` +
606
623
  `'ListUsers', que trunca. Usa 'listSubjects' para los hechos directos.`);
607
624
  }
608
- return this.#driver.membersOf(object, relation, partition, page);
625
+ assertNoListPage('membersOf', arguments[3], this.#driverLabel());
626
+ return this.#driver.membersOf(object, relation, partition);
609
627
  }
610
628
  /** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
611
629
  async enumerateRelations(partition, page) {
@@ -613,7 +631,10 @@ export class RelationsManager {
613
631
  if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
614
632
  throw new UnsupportedOperationError('enumerateRelations', 'ser origen de authz:reconcile de relaciones', 'relations');
615
633
  }
616
- return this.#driver.enumerateRelations(partition, page);
634
+ // 7.4 (injerto 4): la FORMA de la página (limit entero ≥ 1, after cadena no vacía) ⇒ 422 sin tocar el driver; qué
635
+ // cursor es legítimo lo decide el driver (el manager no sabe qué emite).
636
+ const checked = assertEnumeratePage('enumerateRelations', page, 'opaque', this.#driverLabel());
637
+ return this.#driver.enumerateRelations(partition, checked);
617
638
  }
618
639
  /**
619
640
  * **K particiones en UN barrido** (2.4.0-alpha.6) si el driver trae
@@ -646,9 +667,10 @@ export class RelationsManager {
646
667
  if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
647
668
  throw new UnsupportedOperationError('enumerateRelationsMany', 'ser origen de authz:reconcile de relaciones', 'relations');
648
669
  }
670
+ const checked = assertEnumeratePage('enumerateRelationsMany', page, 'opaque', this.#driverLabel());
649
671
  if (typeof this.#driver.enumerateRelationsMany === 'function')
650
- return this.#driver.enumerateRelationsMany(partitions, page);
651
- if (page?.after !== undefined) {
672
+ return this.#driver.enumerateRelationsMany(partitions, checked);
673
+ if (checked?.after !== undefined) {
652
674
  throw new AuthorizationConfigError('enumerateRelationsMany: el driver de relaciones no implementa enumerateRelationsMany y el respaldo del manager (K llamadas ' +
653
675
  'a enumerateRelations) no sabe reanudar con un cursor: `after` no se acepta aquí (reanudar aplicaría el cursor de UNA ' +
654
676
  'partición a todas y perdería tuplas en silencio). Implementa enumerateRelationsMany en el driver, o pagina partición a ' +
@@ -656,7 +678,7 @@ export class RelationsManager {
656
678
  }
657
679
  const tuples = [];
658
680
  for (const partition of partitions) {
659
- const one = await this.#driver.enumerateRelations(partition, page);
681
+ const one = await this.#driver.enumerateRelations(partition, checked);
660
682
  if (one.cursor !== undefined) {
661
683
  throw new AuthorizationConfigError(`enumerateRelationsMany: la partición ${scopeKey(partition)} tiene más tuplas de las que caben en una página y el driver ` +
662
684
  'de relaciones no implementa enumerateRelationsMany: el respaldo del manager (K llamadas a enumerateRelations) no ' +
@@ -681,4 +703,27 @@ export class RelationsManager {
681
703
  assertSubject(subject);
682
704
  }
683
705
  }
684
- //# sourceMappingURL=manager.js.map
706
+ /**
707
+ * **Las opciones de `relate`/`unrelate` que cruzan al driver** (cierre de R1 · 🟡 2 del auditor + 🟠 C del tester): un
708
+ * objeto NUEVO con SOLO las claves conocidas (`transaction`, `actor`, `expiresAt`, con su presencia: «omitido» ≠
709
+ * `undefined` explícito no importa al driver, pero `'expiresAt' in options` sí al evento), leídas UNA vez — un `Proxy`
710
+ * de la app no llega al driver, ni sus símbolos, ni respuestas distintas entre la validación y la escritura. Lo que no
711
+ * es un objeto pasa tal cual (las validaciones de siempre lo juzgan).
712
+ */
713
+ function knownWriteOptions(options) {
714
+ if (options === undefined || options === null || typeof options !== 'object')
715
+ return options;
716
+ const out = {};
717
+ for (const key of KNOWN_WRITE_OPTION_KEYS)
718
+ if (key in options)
719
+ out[key] = options[key];
720
+ return out;
721
+ }
722
+ /**
723
+ * **La lista de claves que cruzan, EXHAUSTIVA por tipo** (cierre-2 de R1 · D3 (b), injerto del analista y del auditor):
724
+ * el `satisfies` exige UNA entrada por cada clave de `RelationWriteOptions` (y ninguna más). Una clave nueva del tipo —una
725
+ * de seguridad, por ejemplo— que no se decida aquí NO COMPILA: hasta el cierre-2 se perdía en silencio en el manager
726
+ * (fallo abierto si la clave era una guarda). Decidir = `true` (cruza al driver) o sacarla del tipo.
727
+ */
728
+ const KNOWN_WRITE_OPTIONS = { transaction: true, actor: true, expiresAt: true };
729
+ export const KNOWN_WRITE_OPTION_KEYS = Object.freeze(Object.keys(KNOWN_WRITE_OPTIONS));
@@ -0,0 +1,19 @@
1
+ import type { RelationPage } from '../types.js';
2
+ /** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
3
+ export declare const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
4
+ /** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
5
+ export declare const RELATIONS_ENUMERATE_MAX_LIMIT = 1000;
6
+ /** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
7
+ export declare function assertNoListPage(operation: string, page: unknown, driver: string): void;
8
+ /** Qué cursor emite quien valida: `uuid` (database), `none` (openfga, el doble), `opaque` (el manager). */
9
+ export type EnumerateCursorKind = 'uuid' | 'none' | 'opaque';
10
+ /**
11
+ * Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
12
+ * se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
13
+ * quien consulta usa la copia, jamás la página original.
14
+ */
15
+ export declare function assertEnumeratePage(operation: string, page: unknown, cursor: EnumerateCursorKind, driver: string): RelationPage | undefined;
16
+ /** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
17
+ export declare function enumerateLimit(page: {
18
+ limit?: number;
19
+ } | undefined): number;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * **La página de relaciones, en UN sitio** (lote «antes de RC» · 7.4, opción 4 del juez —`loteRC-74-paginacion-juez.md`,
3
+ * 88,5/100—, decisión del dueño 2026-10-17 (7.4)). Dos reglas, compartidas por el manager, los dos drivers y el doble:
4
+ *
5
+ * 1. **Los `list*` no paginan en 2.5.** `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en
6
+ * la firma. `undefined` (ausente o explícito) es «sin página»; CUALQUIER otro valor —`null`, `{}`, `{ after: '' }`,
7
+ * `{ limit: 0 }`…— que llegue por JS o `as any` ⇒ 500 `E_AUTHZ_UNSUPPORTED` nombrando la operación, con CERO
8
+ * llamadas al backend (`assertNoListPage`). Se mira `!== undefined`, jamás `arguments.length` (RC74-3: quien reenvía
9
+ * un opcional vacío no se rompe). Orden de las guardas (RC74-5): F-05 y partición ANTES; la página, la última.
10
+ * 2. **`enumerate*` valida su página** (`assertEnumeratePage`): `limit` entero ≥ 1 o 422 `E_AUTHZ_INVALID_PAGE`
11
+ * (`NaN` generaba una consulta SIN `LIMIT`, tester H2), por encima de `RELATIONS_ENUMERATE_MAX_LIMIT` se RECORTA
12
+ * (`enumerateLimit`: con cursor, sin pérdida; declarado en «Limits»); `after` cadena no vacía o 422, y según el
13
+ * cursor que el driver emite: `uuid` (`database`: el uuid canónico de la fila; un cursor ajeno era un 503 en PG y
14
+ * un fin silencioso en MySQL/SQLite, H3), `none` (`openfga` y el doble no emiten cursor ⇒ 500) u `opaque` (el
15
+ * manager, que no sabe qué emite el driver: solo la forma).
16
+ *
17
+ * Lo que NO hace (H3 a medias, a propósito): atar el cursor a su partición —un cursor de A reutilizado en B salta
18
+ * filas— exige un sobre de cursor; es del lote C / 2.6.
19
+ */
20
+ import { InvalidRelationPageError, UnsupportedOperationError } from '../errors.js';
21
+ import { isCanonicalUuid } from '../identity.js';
22
+ /** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
23
+ export const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
24
+ /** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
25
+ export const RELATIONS_ENUMERATE_MAX_LIMIT = 1_000;
26
+ /** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
27
+ export function assertNoListPage(operation, page, driver) {
28
+ if (page !== undefined)
29
+ throw UnsupportedOperationError.listPage(operation, driver);
30
+ }
31
+ /**
32
+ * Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
33
+ * se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
34
+ * quien consulta usa la copia, jamás la página original.
35
+ */
36
+ export function assertEnumeratePage(operation, page, cursor, driver) {
37
+ if (page === undefined)
38
+ return undefined;
39
+ if (page === null || typeof page !== 'object' || Array.isArray(page)) {
40
+ throw new InvalidRelationPageError(`${operation}: la página tiene que ser un objeto { limit?, after? } (llegó ${describe(page)}).`);
41
+ }
42
+ const { limit, after } = page;
43
+ if (limit !== undefined && !(typeof limit === 'number' && Number.isInteger(limit) && limit >= 1)) {
44
+ throw new InvalidRelationPageError(`${operation}: page.limit tiene que ser un entero ≥ 1 (llegó ${describe(limit)}); por encima de ` +
45
+ `${RELATIONS_ENUMERATE_MAX_LIMIT} se recorta, no es un error.`);
46
+ }
47
+ if (after === undefined)
48
+ return limit === undefined ? {} : { limit };
49
+ if (typeof after !== 'string' || after.length === 0) {
50
+ throw new InvalidRelationPageError(`${operation}: page.after tiene que ser el cursor que devolvió la página anterior (llegó ${describe(after)}).`);
51
+ }
52
+ if (cursor === 'none')
53
+ throw UnsupportedOperationError.enumerateCursor(operation, driver);
54
+ if (cursor === 'uuid' && !isCanonicalUuid(after)) {
55
+ throw new InvalidRelationPageError(`${operation}: page.after no es un cursor de este driver ('${driver}' emite el uuid canónico de la última fila leída); ` +
56
+ `pasa el cursor que devolvió la página anterior, sin tocarlo.`);
57
+ }
58
+ return limit === undefined ? { after } : { limit, after };
59
+ }
60
+ /** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
61
+ export function enumerateLimit(page) {
62
+ return Math.min(page?.limit ?? RELATIONS_ENUMERATE_DEFAULT_LIMIT, RELATIONS_ENUMERATE_MAX_LIMIT);
63
+ }
64
+ function describe(value) {
65
+ if (typeof value === 'string')
66
+ return `'${value.slice(0, 40)}'`;
67
+ if (typeof value === 'number')
68
+ return String(value);
69
+ if (value === null)
70
+ return 'null';
71
+ if (Array.isArray(value))
72
+ return 'un array';
73
+ return typeof value;
74
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Clock } from '../clock.js';
2
2
  import type { RelationsDriver, ScopeRef } from '../types.js';
3
+ import type { DenyReachFinding } from './deny_reach.js';
3
4
  import type { RelationsConfig } from './define_relations_config.js';
4
5
  export interface RelationsReconcileOptions {
5
6
  /** El driver del que se LEEN las tuplas (origen). Necesita `enumerateRelations`. */
@@ -50,6 +51,16 @@ export interface RelationsReconcileOptions {
50
51
  * sigue — nunca un throw a mitad de pasada.
51
52
  */
52
53
  toConfig?: RelationsConfig;
54
+ /**
55
+ * **El detector del alcance de los denies (8)**: con él, la pasada lista en `report.denyReach` los denies vivos de sus
56
+ * particiones (origen y destino) cuyo alcance encoge de `baseline` (la config persistida; `null` = sin línea base, solo
57
+ * huérfanos) a `current` (la config que sirve el proceso). Solo LEE lo que la pasada ya enumeró: cero lecturas y cero
58
+ * escrituras de más. Lo pide `authz:relations:reconcile --dry-run`.
59
+ */
60
+ denyReach?: {
61
+ baseline: RelationsConfig | null;
62
+ current: RelationsConfig;
63
+ };
53
64
  /**
54
65
  * El reloj que decide qué tupla del ORIGEN está CADUCADA (R-15): una caducada
55
66
  * no se migra —se cuenta en `skipped.expired`— y no respalda nada en el
@@ -80,7 +91,7 @@ export interface RelationsReconcileOptions {
80
91
  * cierre 5 de D2 (opción A) NIEGA SU PARTICIÓN (`partitionsRefused`: cero escrituras y cero borrados en ella, el resto
81
92
  * migra); la línea la nombra con su receta y es deriva. `withheld-by-unsafe-deny` (cierre 4) se fue con la retención.
82
93
  */
83
- export declare const RELATIONS_RECONCILE_DETAIL_REASONS: readonly ["invalid-identity", "userset-unsupported", "undeclared", "deny-conditional", "renewal-unsafe"];
94
+ export declare const RELATIONS_RECONCILE_DETAIL_REASONS: readonly ["invalid-identity", "userset-unsupported", "undeclared", "deny-conditional", "renewal-unsafe", "invalid-condition"];
84
95
  export type RelationsReconcileDetailReason = (typeof RELATIONS_RECONCILE_DETAIL_REASONS)[number];
85
96
  export interface RelationsReconcileDetail {
86
97
  /** De qué store llegó enumerada. */
@@ -315,13 +326,57 @@ export interface RelationsReconcileReport {
315
326
  * la pasada real dan los mismos números. Es deriva (exit ≠ 0) mientras exista. Opcional en el tipo; ausente = `[]`.
316
327
  */
317
328
  partitionsRefused?: RelationsPartitionRefusal[];
329
+ /**
330
+ * **Las particiones cuya cadena de grupos FINAL excede el techo del destino** (lote «antes de RC» · R1, decisión del
331
+ * dueño 2026-10-17 (5); 🔴 C-1 del auditor): por cada una, la profundidad (saltos `group⊂group` del camino más largo,
332
+ * con los ciclos contados de forma conservadora), el techo (`to.groupDepthLimit()`, el CALIBRADO del destino) y la
333
+ * cadena. Esa partición está además en `partitionsRefused` (causa `too-deep`): cero escrituras en ella — saltar la
334
+ * arista que excede quitaría los denies que transporta y fabricaría una concesión —. Lo decide el PLAN: mismos números
335
+ * en `--dry-run` y en la real. Opcional en el tipo; ausente = `[]`.
336
+ */
337
+ tooDeep?: Array<{
338
+ partition: string;
339
+ depth: number;
340
+ limit: number;
341
+ chain: string[];
342
+ }>;
343
+ /**
344
+ * **El detector de (8)** (lote RC · sublote (8)): solo si se pidió (`denyReach` en las opciones; el comando lo pide en
345
+ * `--dry-run`). Los denies VIVOS de las particiones de la pasada —en el ORIGEN y en el DESTINO, enumerados por la misma
346
+ * pasada— juzgados con `denyReachCheck` (`src/relations/deny_reach.ts`, el mismo punto de paso que la guarda de
347
+ * `database` y que `checkRelationsConfigDenyReach`), en las dos direcciones entre la línea base (`baseline`: la config
348
+ * persistida) y la config que SIRVE el proceso (`current`). Sirve a los dos drivers; `findings` y `stale` son deriva
349
+ * (exit ≠ 0); `unjudged` (sin línea base) NO lo es (firma (8 · `--dry-run`): exit 0 con su línea, el runbook no cambia).
350
+ */
351
+ denyReach?: {
352
+ /** `'persisted'`: se comparó contra la config persistida; `'none'`: sin línea base, solo se buscan huérfanos. */
353
+ baseline: 'persisted' | 'none';
354
+ /** Persistida → servida: `shrunk` y `orphan` (también sin línea base). `count` = claves de tupla DISTINTAS (firma (8 · `count`)). */
355
+ findings: Array<DenyReachFinding & {
356
+ sides: Array<'origen' | 'destino'>;
357
+ }>;
358
+ /** Servida → persistida: la línea base RANCIA (`stale`, solo con línea base; firma (8 · rancia)). */
359
+ stale: Array<DenyReachFinding & {
360
+ sides: Array<'origen' | 'destino'>;
361
+ }>;
362
+ /** Sin línea base: cuántos denies vivos (claves distintas, no huérfanos) quedan SIN juzgar. `0` con línea base. */
363
+ unjudged: number;
364
+ };
318
365
  }
366
+ /**
367
+ * Por qué se NIEGA una partición: `renewal-unsafe` (membresía portadora que cambia de caducidad), `deny-conditional` (deny
368
+ * con caducidad que el destino no puede rehacer) o, desde el cierre de L11 (🟡 A2 del auditor), `invalid-condition`: una
369
+ * tupla de `group` (una membresía, portadora posible) con una condición que el paquete no escribe ni sabe leer
370
+ * (`foreign`/`unparseable`) en una partición con ALGÚN deny — el servidor puede estar evaluándola VIVA (un `not_expired`
371
+ * con `current_time` congelado lo está, medido) y transportando ese deny; saltarla como allow fabricaba una concesión.
372
+ */
373
+ export type RelationsPartitionRefusalReason = 'renewal-unsafe' | 'deny-conditional' | 'invalid-condition' | 'too-deep';
319
374
  /** Una partición NEGADA (cierre 5 de D2): por qué, qué receta, y lo que la pasada habría hecho en ella y NO hizo. */
320
375
  export interface RelationsPartitionRefusal {
321
376
  partition: string;
322
377
  /** Las causas, una por tupla: `renewal-unsafe` (membresía portadora) o `deny-conditional` (deny con caducidad). */
323
378
  causes: Array<{
324
- reason: 'renewal-unsafe' | 'deny-conditional';
379
+ reason: RelationsPartitionRefusalReason;
325
380
  tuple: string;
326
381
  }>;
327
382
  /** La deriva que queda a la vista: lo que la pasada escribiría, renovaría y (con `--prune`) borraría en ella. */
@@ -397,7 +452,7 @@ export declare function denyRefusalRecipe(reason: DenyRefusal['reason'], objectT
397
452
  * openfga/openfga#2151—) NO existe forma sin ventana de cambiar la caducidad de una membresía que transporta un deny, ni
398
453
  * de rehacer permanente un deny con caducidad. Ninguna de las salidas de abajo se presenta como segura.
399
454
  */
400
- export declare function partitionRefusalRecipe(reasons: Iterable<'renewal-unsafe' | 'deny-conditional'>): string;
455
+ export declare function partitionRefusalRecipe(reasons: Iterable<RelationsPartitionRefusalReason>): string;
401
456
  /**
402
457
  * Migra las tuplas de relación de `from` a `to` en `partition`. Idempotente
403
458
  * (una segunda pasada no escribe nada) y bidireccional (funciona en cualquier
@@ -469,4 +524,3 @@ export declare const RELATION_RESIDUES: Readonly<Record<RelationsReconcileDetail
469
524
  how: string;
470
525
  why: string;
471
526
  }>>;
472
- //# sourceMappingURL=reconcile.d.ts.map