@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
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { RelationConfigError, RelationConfigUnknownKeyError, RelationNotDenyableError, RelationDenyExpiryError, RelationTypeUnknownError, RelationUnknownError, RelationUsersetUnsupportedError, } from '../errors.js';
20
20
  import { RESERVED_FACTS_TYPES, RESERVED_SLUGS, RESERVED_SLUG_PREFIXES } from '../identity.js';
21
+ import { assertGroupDepthSettings } from './group_depth.js';
21
22
  /**
22
23
  * **El ÚNICO userset que el modelo admite como sujeto** (plan 2.5 · lote B · B3):
23
24
  * `group#member`. El generador del modelo fusionado emite, para CADA relación de
@@ -58,10 +59,10 @@ const FGA_NAME_FORMAT = /^[a-z0-9][a-z0-9._-]*$/;
58
59
  * Las claves del lote D entran AQUÍ, en el mismo commit que su efecto.
59
60
  */
60
61
  export const RELATIONS_SPEC_KEYS = Object.freeze({
61
- root: Object.freeze(['objectTypes', 'holderTypes', 'database']),
62
+ root: Object.freeze(['objectTypes', 'holderTypes', 'database', 'maxGroupDepth']),
62
63
  objectType: Object.freeze(['type', 'relations', 'deny']),
63
64
  relation: Object.freeze(['name', 'includes']),
64
- database: Object.freeze(['membersOf']),
65
+ database: Object.freeze(['membersOf', 'enforceGroupDepth']),
65
66
  });
66
67
  const _exhaustiveRelationsRoot = {};
67
68
  const _exhaustiveRelationsObjectType = {};
@@ -431,9 +432,14 @@ export function defineRelationsConfig(spec) {
431
432
  ? Object.freeze({ type: objectType.type, relations: Object.freeze(relations) })
432
433
  : Object.freeze({ type: objectType.type, relations: Object.freeze(relations), deny: objectType.deny });
433
434
  });
435
+ // R1 (decisión del dueño 2026-10-17 (5); V2-1 del auditor): el techo se valida AL ARRANCAR, 500 y no 422 — un valor
436
+ // así no es una pregunta mal hecha, es un despliegue que apagaría el límite en silencio.
437
+ const { maxGroupDepth, enforceGroupDepth } = assertGroupDepthSettings(spec.maxGroupDepth, spec.database?.enforceGroupDepth);
434
438
  return Object.freeze({
435
439
  objectTypes: Object.freeze(frozenTypes),
436
440
  membersOf: spec.database?.membersOf === true,
441
+ ...(maxGroupDepth === undefined ? {} : { maxGroupDepth }),
442
+ ...(enforceGroupDepth === true ? { enforceGroupDepth: true } : {}),
437
443
  hasType: (objectType) => declared.has(objectType),
438
444
  isDeclared: (objectType, relation) => declared.get(objectType)?.has(relation) ?? false,
439
445
  });
@@ -596,4 +602,3 @@ export function assertDenyNoExpiry(options) {
596
602
  `caduca el GRANT (relate con expiresAt), no el deny. No se ha escrito nada.`);
597
603
  }
598
604
  }
599
- //# sourceMappingURL=define_relations_config.js.map
@@ -0,0 +1,102 @@
1
+ import type { RelationsConfig } from './define_relations_config.js';
2
+ type ConfigLike = Pick<RelationsConfig, 'objectTypes'>;
3
+ /** Un deny vivo, agregado por tipo y relación PÚBLICA (`editor`, no `denied_editor`). */
4
+ export interface LiveDenyGroup {
5
+ objectType: string;
6
+ relation: string;
7
+ /**
8
+ * Cuántos denies vivos hay sobre `objectType#relation`: filas de `authz_relations` en la guarda; en el detector de
9
+ * reconcile, CLAVES DE TUPLA DISTINTAS (firma (8 · `count`): un deny espejado en origen y destino cuenta una vez).
10
+ */
11
+ count: number;
12
+ }
13
+ /**
14
+ * Por qué un grupo de denies vivos es un hallazgo: `shrunk`/`orphan` (la config nueva encoge su alcance), `no-baseline`
15
+ * (sin línea base utilizable no se puede juzgar) o `stale` (la línea base es más estrecha que lo servido; solo el detector).
16
+ */
17
+ export type DenyReachReason = 'shrunk' | 'orphan' | 'no-baseline' | 'stale';
18
+ /** Un grupo de denies vivos cuyo alcance encoge con la config nueva, con su receta. */
19
+ export interface DenyReachFinding {
20
+ objectType: string;
21
+ relation: string;
22
+ reason: DenyReachReason;
23
+ count: number;
24
+ /**
25
+ * El alcance con la config VIEJA (`down*` en la línea base); `null` = sin línea base o ya huérfano allí. Con `stale`, el
26
+ * alcance que CREE la línea base (`null` si no declara la relación).
27
+ */
28
+ before: string[] | null;
29
+ /** El alcance con la config NUEVA; `[]` si quedó huérfano. Con `stale`, el de la config SERVIDA. */
30
+ after: string[];
31
+ /**
32
+ * Lo que deja de cortar (`before \ after`); con `orphan` sin línea base, `[relation]`; con `no-baseline`, `[]` (no se
33
+ * sabe); con `stale`, lo que el deny corta con lo servido y la línea base no sabe (`after \ before`).
34
+ */
35
+ lost: string[];
36
+ /** La receta (español, como el resto de la letra del paquete). */
37
+ recipe: string;
38
+ }
39
+ /**
40
+ * **El alcance de un deny sobre `objectType#relation`** con `config`: `down*(relation)` ordenado, o `null` si la config no
41
+ * declara ese tipo o esa relación (huérfano).
42
+ */
43
+ export declare function denyReachOf(config: ConfigLike, objectType: string, relation: string): string[] | null;
44
+ /** La relación ALMACENADA `denied_<rel>` ⇒ `<rel>`; cualquier otra ⇒ `null` (no es un deny). */
45
+ export declare function deniedPublicRelation(stored: string): string | null;
46
+ /** La clave `tipo#relación` de un grupo o hallazgo: la que nombra la salida forzada acotada (`allowDenyReachShrink`). */
47
+ export declare function denyReachKey(group: {
48
+ objectType: string;
49
+ relation: string;
50
+ }): string;
51
+ /** La forma de una clave `tipo#relación` que acepta la salida forzada acotada (dos nombres no vacíos, sin `#` ni blancos). */
52
+ export declare const DENY_REACH_KEY_PATTERN: RegExp;
53
+ /**
54
+ * **La lectura de la LÍNEA BASE, TODO O NADA y tipada** (firma (8 · línea base); conflicto 6 del juez del panel): de la spec
55
+ * persistida (el JSON ya parseado) lee SOLO lo que decide un alcance —`objectTypes[].{type, relations[].{name, includes}}`—,
56
+ * sin pasar por `defineRelationsConfig` (una fila de 2.4/alpha.3 con una clave que 2.5 no conoce sigue siendo una línea
57
+ * base: la receta de B5, re-guardar la misma spec corregida, entra sin forzar). Cualquier forma rota EN LO QUE LEE —un
58
+ * objeto que no lo es, `objectTypes`/`relations` que no son arrays, un `type`/`name` que no es string, un `includes`
59
+ * presente que no es un array de strings— deja la línea base como NO UTILIZABLE (`null`): **nunca se filtra** (filtrar
60
+ * subestima el alcance viejo y deja pasar el estrechamiento: S-D3 del tester del panel). Un `includes` ausente es `[]`
61
+ * (la spec lo permite).
62
+ */
63
+ export declare function denyReachBaselineOf(parsed: unknown): ConfigLike | null;
64
+ /**
65
+ * **La propiedad de (8)**: para cada grupo de denies vivos, ¿`alcance(after) ⊇ alcance(before)`? Devuelve los que la
66
+ * violan (`shrunk` | `orphan`, orden estable por tipo y relación), vacío si ninguno. `before = null` = sin línea base: solo
67
+ * `orphan` (el motivo `no-baseline` lo añade `judgeDenyReach`).
68
+ */
69
+ export declare function denyReachFindings(before: ConfigLike | null, after: ConfigLike, denies: Iterable<LiveDenyGroup>): DenyReachFinding[];
70
+ /**
71
+ * **El juicio de la guarda (y la mitad «persistida → servida» de los detectores)**: `denyReachFindings` más, SIN línea base
72
+ * utilizable (`baseline === null`), un hallazgo `no-baseline` por cada grupo de denies vivos que no sea ya `orphan` (de un
73
+ * huérfano sí se sabe que encoge: su alcance es ∅). Sin denies vivos no hay hallazgo alguno: la primera fila se crea sin
74
+ * forzar. Orden estable por tipo, relación y motivo.
75
+ */
76
+ export declare function judgeDenyReach(baseline: ConfigLike | null, next: ConfigLike, denies: Iterable<LiveDenyGroup>): DenyReachFinding[];
77
+ /**
78
+ * **La línea base RANCIA** (firma (8 · rancia); conflicto 3 del juez): la dirección inversa, `denyReachFindings(servida,
79
+ * persistida)`. Un grupo de denies vivos cuyo alcance con lo SERVIDO no cabe en el de la línea base (la fila no declara la
80
+ * relación que se sirve, o la declara más estrecha) es un hallazgo `stale`: la flota sirvió (o sirve) algo que la fila no
81
+ * registra, y desde esa fila un estrechamiento de vuelta a ella parecería «sin cambio» (A1/S-B). Un grupo que lo servido
82
+ * tampoco declara no es rancio (es `orphan` en la otra dirección).
83
+ */
84
+ export declare function staleDenyReach(baseline: ConfigLike, served: ConfigLike, denies: Iterable<LiveDenyGroup>): DenyReachFinding[];
85
+ /** Lo que devuelven los dos detectores (el de un driver y el de `--dry-run`) para un estado: las dos direcciones. */
86
+ export interface DenyReachCheck {
87
+ /** `'persisted'`: hay línea base utilizable; `'none'`: no la hay (sin fila, o fila que la lectura TODO O NADA no tipa). */
88
+ baseline: 'persisted' | 'none';
89
+ /** Persistida → servida: `shrunk`, `orphan` y, sin línea base, `no-baseline` (el juicio de la guarda, sin salidas). */
90
+ findings: DenyReachFinding[];
91
+ /** Servida → persistida: `stale` (solo con línea base). */
92
+ staleBaseline: DenyReachFinding[];
93
+ }
94
+ /** **Los dos sentidos sobre el mismo estado** (firma (8 · detector de un driver)): el punto de paso de los detectores. */
95
+ export declare function denyReachCheck(baseline: ConfigLike | null, served: ConfigLike, denies: Iterable<LiveDenyGroup>): DenyReachCheck;
96
+ /**
97
+ * Una línea por hallazgo, con el prefijo ESTABLE de su rama (CLAUDE.md punto 7): la usan el error de la guarda, el detector
98
+ * de un driver y el comando. Prefijos: «deny con alcance ENCOGIDO», «deny HUÉRFANO», «deny SIN línea base», «línea base
99
+ * RANCIA».
100
+ */
101
+ export declare function denyReachLine(finding: DenyReachFinding): string;
102
+ export {};
@@ -0,0 +1,230 @@
1
+ /**
2
+ * **El ALCANCE de un deny vivo, y cuándo un cambio de config lo ENCOGE** (lote RC · sublote (8); decisión §2026-10-15 (8)
3
+ * del dueño y firmas «(8 · panel)» 2026-10-22). Módulo PURO (sin `db`, sin drivers: regla 2 de pureza) y PUNTO DE PASO
4
+ * ÚNICO de las tres piezas de (8) —los gemelos guarda ↔ detector de un driver ↔ `--dry-run`—:
5
+ *
6
+ * - la **guarda de `database`** (`saveRelationsConfig`, `src/relations_config_store.ts`): `judgeDenyReach` sobre la línea
7
+ * base persistida (leída TODO O NADA por `denyReachBaselineOf`), la config nueva y los denies vivos de `authz_relations`;
8
+ * niega (500 `E_AUTHZ_DENY_REACH_REFUSED`, cero escrituras) salvo las dos salidas con nombre: `establishBaseline: true`
9
+ * (solo el motivo `no-baseline`) y `allowDenyReachShrink: ['tipo#relación', …]` (solo esos hallazgos);
10
+ * - el **detector de un solo driver** (`checkRelationsConfigDenyReach`, mismo módulo de la guarda): `denyReachCheck`, la
11
+ * misma línea base y los mismos denies, en las DOS direcciones (persistida → servida, y servida → persistida: la línea
12
+ * base RANCIA, A1/S-B del panel);
13
+ * - el **detector de `authz:relations:reconcile --dry-run`** (`reconcileRelations({ denyReach })`): `denyReachCheck` sobre
14
+ * los denies que la pasada YA enumeró, en origen y destino. En `openfga` es solo detección (regla (ritmo): cero
15
+ * arreglos hasta 2.6; la guarda de `openfga:provision` sigue siendo el límite declarado `openfga.provision.deny-reach`).
16
+ *
17
+ * **El alcance** de un deny sobre `T#X` es el conjunto de relaciones cuyos grants CORTA (D5, corte de camino:
18
+ * `cutPathAllows` corta un grant de `g` sii algún deny vivo está en `up*(g)`, o sea sii `g ∈ down*(X)`):
19
+ * `alcance(T#X) = down*(X)`. Con `editor: { includes: ['owner'] }` un deny en `editor` corta `{editor, owner}`; estrechado
20
+ * a `includes: []` corta solo `{editor}` y el owner directo vuelve a conceder (el escenario firmado).
21
+ *
22
+ * **La propiedad firmada es «para todo deny vivo, alcance_nuevo ⊇ alcance_viejo»**, NO «la relación sigue existiendo».
23
+ * Motivos de un hallazgo:
24
+ * - `shrunk`: `T#X` sigue declarada pero `down*_nuevo(X) ⊉ down*_viejo(X)` —un `includes` estrechado, directo o
25
+ * TRANSITIVO, o una relación de `down*_viejo(X)` retirada del tipo—;
26
+ * - `orphan`: `T#X` ya no está declarada (relación o tipo retirados): el deny no lo lee nadie (alcance ∅) y ningún
27
+ * `removeDeny` lo alcanza (F-05); si la relación se vuelve a declarar, el deny resucita. Se juzga SIEMPRE contra la
28
+ * config nueva, con o sin línea base, y aunque ya fuera huérfano en la línea base (firma (8 · salida forzada): P2 «juzgar
29
+ * por delta» NO entra — una AUSENCIA en una fila que puede estar rancia no prueba que el deny ya fuera huérfano en la flota);
30
+ * - `no-baseline`: no hay línea base UTILIZABLE (sin fila, o una fila que la lectura TODO O NADA no puede tipar) y el deny
31
+ * vive: no se puede juzgar si encoge (firma (8 · línea base));
32
+ * - `stale` (solo el detector): la línea base no sabe que el deny corta algo que la config SERVIDA sí corta — la fila es
33
+ * más estrecha que lo servido, y desde ella un estrechamiento de vuelta parecería «sin cambio» (firma (8 · rancia)).
34
+ *
35
+ * Decisiones que siguen (informe `loteRC-8-informe.md`, sin cambio del dueño): una relación de `down*_viejo(X)` RETIRADA del
36
+ * tipo cuenta como encogimiento aunque sus grants ya no concedan (la propiedad, literal); retirar el opt-in `deny` de un tipo
37
+ * NO cambia el alcance en `database` (las lecturas honran la fila igual) y no se juzga aquí — en `openfga` lo juzga la guarda
38
+ * de `provision` (`--allow-drop-deny`).
39
+ */
40
+ import { RELATION_DENY_PREFIX, relationsDown } from './define_relations_config.js';
41
+ /**
42
+ * **El alcance de un deny sobre `objectType#relation`** con `config`: `down*(relation)` ordenado, o `null` si la config no
43
+ * declara ese tipo o esa relación (huérfano).
44
+ */
45
+ export function denyReachOf(config, objectType, relation) {
46
+ const type = config.objectTypes.find((t) => t.type === objectType);
47
+ if (!type || !type.relations.some((r) => r.name === relation))
48
+ return null;
49
+ return relationsDown(config, objectType, relation).sort();
50
+ }
51
+ /** La relación ALMACENADA `denied_<rel>` ⇒ `<rel>`; cualquier otra ⇒ `null` (no es un deny). */
52
+ export function deniedPublicRelation(stored) {
53
+ return typeof stored === 'string' && stored.startsWith(RELATION_DENY_PREFIX) && stored.length > RELATION_DENY_PREFIX.length
54
+ ? stored.slice(RELATION_DENY_PREFIX.length)
55
+ : null;
56
+ }
57
+ /** La clave `tipo#relación` de un grupo o hallazgo: la que nombra la salida forzada acotada (`allowDenyReachShrink`). */
58
+ export function denyReachKey(group) {
59
+ return `${group.objectType}#${group.relation}`;
60
+ }
61
+ /** La forma de una clave `tipo#relación` que acepta la salida forzada acotada (dos nombres no vacíos, sin `#` ni blancos). */
62
+ export const DENY_REACH_KEY_PATTERN = /^[^#\s]+#[^#\s]+$/;
63
+ /**
64
+ * **La lectura de la LÍNEA BASE, TODO O NADA y tipada** (firma (8 · línea base); conflicto 6 del juez del panel): de la spec
65
+ * persistida (el JSON ya parseado) lee SOLO lo que decide un alcance —`objectTypes[].{type, relations[].{name, includes}}`—,
66
+ * sin pasar por `defineRelationsConfig` (una fila de 2.4/alpha.3 con una clave que 2.5 no conoce sigue siendo una línea
67
+ * base: la receta de B5, re-guardar la misma spec corregida, entra sin forzar). Cualquier forma rota EN LO QUE LEE —un
68
+ * objeto que no lo es, `objectTypes`/`relations` que no son arrays, un `type`/`name` que no es string, un `includes`
69
+ * presente que no es un array de strings— deja la línea base como NO UTILIZABLE (`null`): **nunca se filtra** (filtrar
70
+ * subestima el alcance viejo y deja pasar el estrechamiento: S-D3 del tester del panel). Un `includes` ausente es `[]`
71
+ * (la spec lo permite).
72
+ */
73
+ export function denyReachBaselineOf(parsed) {
74
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
75
+ return null;
76
+ const objectTypes = parsed.objectTypes;
77
+ if (!Array.isArray(objectTypes))
78
+ return null;
79
+ const types = [];
80
+ for (const t of objectTypes) {
81
+ if (t === null || typeof t !== 'object' || Array.isArray(t))
82
+ return null;
83
+ if (typeof t.type !== 'string' || !Array.isArray(t.relations))
84
+ return null;
85
+ const relations = [];
86
+ for (const r of t.relations) {
87
+ if (r === null || typeof r !== 'object' || Array.isArray(r) || typeof r.name !== 'string')
88
+ return null;
89
+ if (r.includes !== undefined && !(Array.isArray(r.includes) && r.includes.every((i) => typeof i === 'string')))
90
+ return null;
91
+ relations.push({ name: r.name, includes: Array.isArray(r.includes) ? r.includes.filter((i) => typeof i === 'string') : [] });
92
+ }
93
+ types.push({ type: t.type, relations });
94
+ }
95
+ return { objectTypes: types };
96
+ }
97
+ function mergeGroups(denies) {
98
+ const groups = new Map();
99
+ for (const deny of denies) {
100
+ const key = denyReachKey(deny);
101
+ const seen = groups.get(key);
102
+ groups.set(key, seen ? { ...seen, count: seen.count + deny.count } : { ...deny });
103
+ }
104
+ return [...groups.values()];
105
+ }
106
+ const byKey = (a, b) => denyReachKey(a).localeCompare(denyReachKey(b)) || String(a.reason ?? '').localeCompare(String(b.reason ?? ''));
107
+ /**
108
+ * **La propiedad de (8)**: para cada grupo de denies vivos, ¿`alcance(after) ⊇ alcance(before)`? Devuelve los que la
109
+ * violan (`shrunk` | `orphan`, orden estable por tipo y relación), vacío si ninguno. `before = null` = sin línea base: solo
110
+ * `orphan` (el motivo `no-baseline` lo añade `judgeDenyReach`).
111
+ */
112
+ export function denyReachFindings(before, after, denies) {
113
+ const findings = [];
114
+ for (const group of mergeGroups(denies).sort(byKey)) {
115
+ const old = before ? denyReachOf(before, group.objectType, group.relation) : null;
116
+ const now = denyReachOf(after, group.objectType, group.relation);
117
+ if (now === null) {
118
+ findings.push({
119
+ ...group,
120
+ reason: 'orphan',
121
+ before: old,
122
+ after: [],
123
+ lost: old ?? [group.relation],
124
+ recipe: orphanRecipe(group),
125
+ });
126
+ continue;
127
+ }
128
+ if (old === null)
129
+ continue;
130
+ const lost = old.filter((relation) => !now.includes(relation));
131
+ if (lost.length === 0)
132
+ continue;
133
+ findings.push({ ...group, reason: 'shrunk', before: old, after: now, lost, recipe: shrunkRecipe(group, lost) });
134
+ }
135
+ return findings;
136
+ }
137
+ /**
138
+ * **El juicio de la guarda (y la mitad «persistida → servida» de los detectores)**: `denyReachFindings` más, SIN línea base
139
+ * utilizable (`baseline === null`), un hallazgo `no-baseline` por cada grupo de denies vivos que no sea ya `orphan` (de un
140
+ * huérfano sí se sabe que encoge: su alcance es ∅). Sin denies vivos no hay hallazgo alguno: la primera fila se crea sin
141
+ * forzar. Orden estable por tipo, relación y motivo.
142
+ */
143
+ export function judgeDenyReach(baseline, next, denies) {
144
+ const groups = mergeGroups(denies);
145
+ const findings = denyReachFindings(baseline, next, groups);
146
+ if (baseline !== null)
147
+ return findings;
148
+ const judged = new Set(findings.map(denyReachKey));
149
+ for (const group of groups) {
150
+ if (judged.has(denyReachKey(group)))
151
+ continue;
152
+ findings.push({ ...group, reason: 'no-baseline', before: null, after: denyReachOf(next, group.objectType, group.relation) ?? [], lost: [], recipe: noBaselineRecipe(group) });
153
+ }
154
+ return findings.sort(byKey);
155
+ }
156
+ /**
157
+ * **La línea base RANCIA** (firma (8 · rancia); conflicto 3 del juez): la dirección inversa, `denyReachFindings(servida,
158
+ * persistida)`. Un grupo de denies vivos cuyo alcance con lo SERVIDO no cabe en el de la línea base (la fila no declara la
159
+ * relación que se sirve, o la declara más estrecha) es un hallazgo `stale`: la flota sirvió (o sirve) algo que la fila no
160
+ * registra, y desde esa fila un estrechamiento de vuelta a ella parecería «sin cambio» (A1/S-B). Un grupo que lo servido
161
+ * tampoco declara no es rancio (es `orphan` en la otra dirección).
162
+ */
163
+ export function staleDenyReach(baseline, served, denies) {
164
+ return denyReachFindings(served, baseline, denies)
165
+ .filter((reverse) => reverse.before !== null)
166
+ .map((reverse) => ({
167
+ objectType: reverse.objectType,
168
+ relation: reverse.relation,
169
+ count: reverse.count,
170
+ reason: 'stale',
171
+ before: reverse.reason === 'orphan' ? null : reverse.after,
172
+ after: reverse.before,
173
+ lost: reverse.lost,
174
+ recipe: staleRecipe(reverse),
175
+ }));
176
+ }
177
+ /** **Los dos sentidos sobre el mismo estado** (firma (8 · detector de un driver)): el punto de paso de los detectores. */
178
+ export function denyReachCheck(baseline, served, denies) {
179
+ const groups = mergeGroups(denies);
180
+ return {
181
+ baseline: baseline === null ? 'none' : 'persisted',
182
+ findings: judgeDenyReach(baseline, served, groups),
183
+ staleBaseline: baseline === null ? [] : staleDenyReach(baseline, served, groups),
184
+ };
185
+ }
186
+ function shrunkRecipe(group, lost) {
187
+ return (`antes de estrechar, vuelve a emitir cada uno de esos ${group.count} deny(s) sobre una relación de '${group.objectType}' que ` +
188
+ `con la config nueva siga cortando ${lost.map((r) => `'${r}'`).join(', ')} (deny(<sujeto>, '<esa relación>', <objeto>); ` +
189
+ `listDenies(<objeto>, '${group.relation}') los enumera), o deja el includes como estaba; si de verdad deben dejar de ` +
190
+ `cortarlo, nombra este hallazgo en la salida forzada: allowDenyReachShrink: ['${denyReachKey(group)}']`);
191
+ }
192
+ function orphanRecipe(group) {
193
+ return (`antes de retirar '${denyReachKey(group)}', quita esos ${group.count} deny(s) con removeDeny (o re-emítelos sobre ` +
194
+ `una relación que se quede) y retírala después; si ya la retiraste, vuelve a declararla, haz removeDeny y retírala — ` +
195
+ `un deny huérfano no lo lee nadie, ningún removeDeny lo alcanza (F-05) y resucita si la relación se vuelve a declarar ` +
196
+ `(y re-declararla tras forzar la retirada resucita también los grants que cortaba)`);
197
+ }
198
+ function noBaselineRecipe(group) {
199
+ return (`guarda UNA vez la spec que la flota sirve HOY, sin cambios, con saveRelationsConfig(spec, { establishBaseline: true }), ` +
200
+ `en una release que NO cambie relations.config (adoptar en una release que estrecha certifica el estrechamiento); ` +
201
+ `desde esa fila la guarda juzga cada cambio de '${denyReachKey(group)}'`);
202
+ }
203
+ function staleRecipe(reverse) {
204
+ return (`la línea base (authz_relations_config) es más estrecha que la config que se sirve: guarda con saveRelationsConfig la ` +
205
+ `config que sirve la flota —persistir la config es paso obligatorio de release si usas deny— y vuelve a correr la ` +
206
+ `comprobación; mientras la fila no lo sepa, un estrechamiento de vuelta a ella no lo niega la guarda y los ` +
207
+ `${reverse.count} deny(s) de '${denyReachKey(reverse)}' dejarían de cortar ${reverse.lost.map((r) => `'${r}'`).join(', ')}`);
208
+ }
209
+ /**
210
+ * Una línea por hallazgo, con el prefijo ESTABLE de su rama (CLAUDE.md punto 7): la usan el error de la guarda, el detector
211
+ * de un driver y el comando. Prefijos: «deny con alcance ENCOGIDO», «deny HUÉRFANO», «deny SIN línea base», «línea base
212
+ * RANCIA».
213
+ */
214
+ export function denyReachLine(finding) {
215
+ const where = `${denyReachKey(finding)} (${finding.count} deny(s))`;
216
+ switch (finding.reason) {
217
+ case 'orphan':
218
+ return `deny HUÉRFANO ${where}: la config nueva no declara la relación (cortaba ${finding.lost.join(', ')}). Receta: ${finding.recipe}.`;
219
+ case 'no-baseline':
220
+ return (`deny SIN línea base ${where}: no hay config persistida utilizable en authz_relations_config con la que comparar, así que ` +
221
+ `no se puede saber si la config nueva encoge su alcance (hoy corta ${finding.after.join(', ')}). Receta: ${finding.recipe}.`);
222
+ case 'stale':
223
+ return (`línea base RANCIA ${where}: con la config servida el deny corta ${finding.after.join(', ')}, y la config persistida ` +
224
+ `${finding.before === null ? 'no declara la relación' : `solo sabe de ${finding.before.join(', ')}`} (no sabe de ` +
225
+ `${finding.lost.join(', ')}). Receta: ${finding.recipe}.`);
226
+ default:
227
+ return (`deny con alcance ENCOGIDO ${where}: cortaba ${finding.before.join(', ')} y con la config nueva solo ${finding.after.join(', ')} ` +
228
+ `(deja de cortar ${finding.lost.join(', ')}: lo que negaba vuelve a conceder). Receta: ${finding.recipe}.`);
229
+ }
230
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * **El límite de anidación de grupos** (lote «antes de RC» · R1; decisiones del dueño 2026-10-17 (5) «límite SEGÚN EL
3
+ * DRIVER» y (5-bis) opción (a) tras F0 · M-c). Piezas PURAS (sin backend), compartidas por los dos drivers de relaciones
4
+ * y por el plan de `reconcileRelations`, para que la regla sea UNA:
5
+ *
6
+ * - **qué es una arista de anidación**: `group:<Gp>#member@group:<Gc>#member` (los miembros de Gc son miembros de Gp);
7
+ * - **la profundidad es el camino MÁS LARGO** de aristas grupo⊂grupo (T-3 del tester: en un DAG con un camino corto y
8
+ * otro largo hacia la misma cima, OpenFGA da 503 a los NO-miembros por el largo; un BFS por niveles admitiría ese
9
+ * grafo). Un ciclo PREEXISTENTE se cuenta de forma CONSERVADORA: sus k nodos pesan k − 1 saltos (cota superior del
10
+ * camino simple más largo que lo atraviesa) — más negativas, nunca menos;
11
+ * - **la validación de `maxGroupDepth`** (V2-1 del auditor): entero 1..`GROUP_DEPTH_CONFIG_MAX` o `false` (el ÚNICO
12
+ * «apagado»), y nada más — `''`, `NaN`, `0`, `-1`, `Infinity`, `24.5`, `'24'`, `null`, `true` ⇒ 500 al ARRANCAR
13
+ * (`defineRelationsConfig`), jamás un gate apagado en silencio;
14
+ * - **la calibración** (decisión 5 + 5-bis): el techo de `openfga` se MIDE contra el servidor con tuplas contextuales
15
+ * (solo lectura) y es el MÍNIMO de la sonda POSITIVA (un miembro: T) y la NEGATIVA (un no-miembro CON alguna
16
+ * membresía: F; una sonda sin membresías es CIEGA, F a cualquier profundidad, F0 · M-c). La búsqueda es pura sobre
17
+ * una sonda inyectada.
18
+ *
19
+ * Pureza (regla 2): solo depende de los errores. Nada de `manager`/`drivers/*`.
20
+ */
21
+ import { AuthorizationBackendError, GroupDepthLimitError } from '../errors.js';
22
+ import type { RelObject, RelSubject } from '../types.js';
23
+ /**
24
+ * **El techo medido** en OpenFGA v1.19.0 con `--resolve-node-limit` por defecto (25): un miembro resuelve 24 saltos
25
+ * grupo⊂grupo y un no-miembro CON membresías 22 (F0 · M-c, 200/200 en los dos bordes, independiente de `includes` 0..3 y
26
+ * del deny) ⇒ el MÍNIMO, 22. Es la cifra de la fila de «Limits»; la VERDAD de cada despliegue es la calibración.
27
+ */
28
+ export declare const RELATIONS_GROUP_DEPTH_MEASURED = 22;
29
+ /**
30
+ * **El mayor `maxGroupDepth` que se acepta** (auditor M4): la calibración usa una cadena de tuplas CONTEXTUALES y el
31
+ * servidor admite 100 por `Check`; la forma de dominio cuesta K + 2. Por encima no hay nada que medir.
32
+ */
33
+ export declare const GROUP_DEPTH_CONFIG_MAX = 97;
34
+ /**
35
+ * **El mayor K que la calibración prueba**: la sonda NEGATIVA lleva dos tuplas más (la membresía del no-miembro y la
36
+ * suya propia, M-c) ⇒ K + 4 ≤ 100. Un `maxGroupDepth: 97` es válido en la config y, contra un servidor que resolviera
37
+ * todo, la calibración lo deja en 96 ⇒ 500 (el techo pedido no se puede DEMOSTRAR).
38
+ */
39
+ export declare const GROUP_DEPTH_CALIBRATION_MAX = 96;
40
+ /**
41
+ * **Cuántos grupos recorre como mucho la comprobación de UNA arista** (hacia arriba desde Gp más hacia abajo desde Gc).
42
+ * Pasarla ⇒ 422 `E_AUTHZ_GROUP_DEPTH_LIMIT` con motivo `too-many-groups`, nunca «pasa». El recorrido además se corta en
43
+ * cuanto un nivel del BFS supera el techo (el camino más corto ya excede: el largo también).
44
+ */
45
+ export declare const GROUP_DEPTH_MAX_NODES = 10000;
46
+ /**
47
+ * **El presupuesto de la comprobación de UNA arista en `openfga`** (cierre de R1 · 🟠 1 del auditor: amplificación de
48
+ * lecturas contra el store COMPARTIDO). En `openfga` el gate hace un `Read` por grupo de la frontera, y el `Read` hacia
49
+ * ABAJO (objeto + `member`) trae también los HOLDERS —el servidor no admite filtrar por tipo de `user` (`user: 'group:'`
50
+ * ⇒ 400 `validation_error`, medido en :8090 v1.19)—: sin presupuesto, quien controla la anidación hacía pagar O(grupos ×
51
+ * miembros/100) requests a cada `relate`. Pasar cualquiera de los tres ⇒ 422 `E_AUTHZ_GROUP_DEPTH_LIMIT`, motivo
52
+ * `too-many-groups` (fail-closed: no se escribe). Requests (páginas de `Read`), tuplas leídas y un deadline TOTAL del
53
+ * veredicto (comprobado antes de cada request; una request en vuelo la acota el `timeoutMs` del cliente).
54
+ */
55
+ export declare const GROUP_DEPTH_MAX_READS = 2000;
56
+ export declare const GROUP_DEPTH_MAX_TUPLES = 50000;
57
+ export declare const GROUP_DEPTH_DEADLINE_MS = 10000;
58
+ /**
59
+ * **La espera tras una calibración FALLIDA** (cierre-2 de R1 · D5 (a), injerto del auditor; decisión del dueño 2026-10-20):
60
+ * la calibración de `openfga` son ~14 `Check` (bisección 0..96, dos sondas). Si cae (servidor caído, 503, deadline, el
61
+ * 500 de una sonda ciega), durante esta ventana la siguiente escritura de anidación recibe un error de la MISMA clase —una
62
+ * instancia nueva con el memorizado de `cause`, y un timeout como 503 no-timeout (cierre-3 · R-5)— sin volver a
63
+ * sondear —una flota de `relate` contra un servidor que se cae no lo remata con 14 `Check` cada uno—; pasada, la
64
+ * siguiente vuelve a calibrar (el fallo nunca se memoriza para siempre, C13a). Reloj MONÓTONO del driver.
65
+ */
66
+ export declare const GROUP_DEPTH_CALIBRATION_BACKOFF_MS = 2000;
67
+ /**
68
+ * **La cota de frecuencia de la RE-calibración** (cierre-2 de R1 · D5 (a) + P2 del juez): una lectura de `openfga` que
69
+ * recibe 503 `E_AUTHZ_RESOLUTION_LIMIT` (el servidor ya no resuelve lo que el techo memorizado prometía: el operador BAJÓ
70
+ * `OPENFGA_RESOLVE_NODE_LIMIT` sin reiniciar la app) invalida la calibración memorizada, como mucho UNA vez por este
71
+ * intervalo y por proceso; la siguiente escritura de anidación vuelve a medir. Sin la cota, una forma que dé 503 en cada
72
+ * lectura dispararía una calibración por lectura. SUBIR el límite del servidor no produce ningún 503 que lo delate: eso
73
+ * sigue pidiendo reiniciar la app.
74
+ */
75
+ export declare const GROUP_DEPTH_RECALIBRATION_MIN_INTERVAL_MS = 60000;
76
+ /** Qué cota del presupuesto se pasó (va en `details.budget` del 422). */
77
+ export type GroupDepthBudgetKind = 'nodes' | 'reads' | 'tuples' | 'deadline';
78
+ /** Señal INTERNA: el lector la lanza al pasar el presupuesto y `groupNestingVerdict` la convierte en `too-many-groups`. */
79
+ export declare class GroupDepthBudgetExceeded extends Error {
80
+ readonly budget: Exclude<GroupDepthBudgetKind, 'nodes'>;
81
+ constructor(budget: Exclude<GroupDepthBudgetKind, 'nodes'>);
82
+ }
83
+ /** El contador de UN veredicto: `beforeRead()` antes de cada request, `charge(n)` con las tuplas de cada página. */
84
+ export declare class GroupDepthBudget {
85
+ #private;
86
+ readonly limits: {
87
+ reads: number;
88
+ tuples: number;
89
+ deadlineMs: number;
90
+ };
91
+ readonly clock: () => number;
92
+ reads: number;
93
+ tuples: number;
94
+ constructor(limits?: {
95
+ reads: number;
96
+ tuples: number;
97
+ deadlineMs: number;
98
+ }, clock?: () => number);
99
+ beforeRead(): void;
100
+ charge(tuples: number): void;
101
+ }
102
+ /** Las opciones de `relate` de UNA arista planificada por el reconcile (solo el reconcile del paquete las crea). */
103
+ export declare function plannedRelateOptions<T extends object>(options: T, tuple: {
104
+ subject: RelSubject;
105
+ relation: string;
106
+ object: RelObject;
107
+ partition: {
108
+ type: string;
109
+ uuid: string | null;
110
+ };
111
+ }): T;
112
+ /** ¿Llega ESTA arista (`child ⊂ parent` en `partition`) con las opciones que el reconcile creó para ella? */
113
+ export declare function plannedByReconcile(options: unknown, partition: {
114
+ type: string;
115
+ uuid: string | null;
116
+ }, edge: GroupEdge): boolean;
117
+ /** `group:<Gp>#member@group:<Gc>#member`: la ÚNICA forma de anidar (B3: el único userset admitido es `group#member`). */
118
+ export declare function isGroupNesting(subject: RelSubject, relation: string, object: RelObject): subject is Extract<RelSubject, {
119
+ relation: string;
120
+ }>;
121
+ /**
122
+ * **Valida `maxGroupDepth`** (V2-1; decisión 5: «entero 1..97 o `false`»): `undefined` ⇒ sin valor (el driver usa su
123
+ * techo), `false` ⇒ apagado EXPLÍCITO, entero en [1, 97] ⇒ ese techo. Cualquier otra cosa ⇒ 500 `E_AUTHZ_CONFIG`: el
124
+ * paquete no lee variables de entorno, y la línea típica `Number(env.get('X') ?? 24)` produce `0` (`''`) y `NaN` (`'abc'`),
125
+ * que con `if (depth > max)` o `if (!max)` APAGARÍAN el gate en silencio.
126
+ */
127
+ export declare function assertMaxGroupDepth(value: unknown, where?: string): number | false | undefined;
128
+ /**
129
+ * **Valida el PAR (`maxGroupDepth`, `enforceGroupDepth`)** — en `defineRelationsConfig` al arrancar y, desde el cierre de
130
+ * R1 (🟡 4 del auditor), OTRA VEZ en los dos drivers: una config extendida con spread
131
+ * (`{ ...defineRelationsConfig(spec), maxGroupDepth: Number(env.get('X')) }`) llegaba al driver sin pasar por la
132
+ * validación, y con `NaN` toda comparación `> limit` es `false`: el gate quedaba APAGADO en silencio. `enforceGroupDepth`
133
+ * solo admite `true`/`false`/ausente y `true` exige un número. Todo lo demás ⇒ 500 `E_AUTHZ_CONFIG`.
134
+ */
135
+ export declare function assertGroupDepthSettings(maxGroupDepth: unknown, enforceGroupDepth: unknown, where?: string): {
136
+ maxGroupDepth: number | false | undefined;
137
+ enforceGroupDepth: boolean;
138
+ };
139
+ /** Una arista grupo⊂grupo: `child` es miembro de `parent` (ids de objeto del grupo, sin partición). */
140
+ export interface GroupEdge {
141
+ child: string;
142
+ parent: string;
143
+ }
144
+ /**
145
+ * **La cadena más larga** (en aristas grupo⊂grupo) de un conjunto de aristas, con los ciclos colapsados de forma
146
+ * CONSERVADORA (cada componente fuertemente conexa de k nodos pesa k − 1). Devuelve la profundidad y una cadena
147
+ * representativa (los grupos, de abajo arriba) para la letra. Lineal en nodos + aristas (Tarjan iterativo + DP).
148
+ */
149
+ export declare function groupChainDepth(edges: Iterable<GroupEdge>): {
150
+ depth: number;
151
+ chain: string[];
152
+ };
153
+ /** El veredicto de UNA arista nueva sobre la región recorrida. */
154
+ export type GroupNestingVerdict = {
155
+ kind: 'ok';
156
+ depth: number;
157
+ } | {
158
+ kind: 'cycle';
159
+ depth: null;
160
+ } | {
161
+ kind: 'too-deep';
162
+ depth: number;
163
+ chain: string[];
164
+ } | {
165
+ kind: 'too-many-groups';
166
+ depth: null;
167
+ budget: GroupDepthBudgetKind;
168
+ };
169
+ /** Cómo leer los vecinos de un conjunto de grupos (UNA llamada por nivel del BFS en SQL; una `Read` por grupo en FGA). */
170
+ export interface GroupNeighbourReader {
171
+ /** Las aristas VIVAS cuyo `child` está en `groups` (los grupos que los contienen). */
172
+ parentsOf(groups: string[]): Promise<GroupEdge[]>;
173
+ /** Las aristas VIVAS cuyo `parent` está en `groups` (los grupos que contienen). */
174
+ childrenOf(groups: string[]): Promise<GroupEdge[]>;
175
+ }
176
+ /**
177
+ * **El veredicto de la arista `child ⊂ parent`** (gate de escritura de R1): recorre hacia ARRIBA desde `parent` y hacia
178
+ * ABAJO desde `child` (BFS por niveles, una lectura por nivel), y decide:
179
+ *
180
+ * - `cycle` si `child` es `parent` o aparece por encima de `parent` (la arista lo cerraría);
181
+ * - `too-deep` si el camino MÁS LARGO que atraviesa la arista (`alto(parent) + 1 + bajo(child)`) supera `limit` — y
182
+ * también, sin terminar el recorrido, en cuanto los niveles del BFS ya lo superan (el camino corto es una cota
183
+ * inferior del largo);
184
+ * - `too-many-groups` si la región pasa de `maxNodes`, o si el LECTOR agota su presupuesto (`GroupDepthBudgetExceeded`:
185
+ * requests, tuplas o deadline — `openfga`, cierre de R1 · 🟠 1 del auditor);
186
+ * - `ok` en otro caso.
187
+ *
188
+ * La arista misma (si ya existe: renovación) no se cuenta dos veces.
189
+ */
190
+ export declare function groupNestingVerdict(edge: GroupEdge, limit: number, reader: GroupNeighbourReader, maxNodes?: number): Promise<GroupNestingVerdict>;
191
+ /**
192
+ * **El error de un veredicto que no es `ok`** (cierre-3 de R1 · R-2 del re-ataque; decisión firmada (3): lo transitorio es
193
+ * 503): el agotamiento del DEADLINE del presupuesto (`budget: 'deadline'`) no dice nada de los datos —el mismo grafo da
194
+ * `ok` con el servidor descargado— sino que el backend va LENTO (o lo satura otro tenant del store compartido), así que es
195
+ * 503 `AuthorizationBackendError` transitorio —NO la subclase timeout: la escritura no ocurrió y el manager no notifica
196
+ * `indeterminate` (invariante 13)— y nunca el 422 «aplana la anidación», que un cliente no reintenta. Las otras cotas
197
+ * (`nodes`, `reads`, `tuples`) son deterministas sobre los datos y siguen siendo 422 `too-many-groups`.
198
+ */
199
+ export declare function groupNestingError(driver: string, verdict: Exclude<GroupNestingVerdict, {
200
+ kind: 'ok';
201
+ }>, edge: string, limit: number): GroupDepthLimitError | AuthorizationBackendError;
202
+ /** El 422 de un veredicto que no es `ok` (letra de `E_AUTHZ_SCOPE_DEPTH_LIMIT`: el `message` no nombra el backend). */
203
+ export declare function groupDepthLimitError(verdict: Exclude<GroupNestingVerdict, {
204
+ kind: 'ok';
205
+ }>, edge: string, limit: number): GroupDepthLimitError;
206
+ /** Resultado de una sonda de calibración: el `Check` resolvió con la respuesta esperada, excedió, o dio la OTRA respuesta. */
207
+ export type CalibrationProbe = 'ok' | 'too-complex' | 'blind';
208
+ /**
209
+ * **La calibración del techo** (decisión 5 + 5-bis, opción (a)): el mayor K en [0, `max`] para el que la sonda
210
+ * POSITIVA (un miembro, T) Y la NEGATIVA (un no-miembro con membresía, F) resuelven — el MÍNIMO de las dos —, por
211
+ * bisección (el borde es determinista en relaciones, M-c: 200/200). Una sonda que da la OTRA respuesta (`blind`: el
212
+ * miembro F, el no-miembro T) es un servidor o un modelo que no evalúa como el medido ⇒ 500, jamás «pasa». Cualquier
213
+ * otro fallo de la sonda (503, deadline) se PROPAGA: quien llama no lo memoriza.
214
+ */
215
+ export declare function calibrateGroupDepth(probe: (depth: number, side: 'positive' | 'negative') => Promise<CalibrationProbe>, max?: number): Promise<number>;