@jantstack/adonis-authz 2.5.0-alpha.2 → 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 (275) hide show
  1. package/README.md +743 -28
  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 +6 -1
  13. package/build/commands/authz_relations_reconcile.js +163 -5
  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 +72 -1
  23. package/build/commands/openfga_provision.js +236 -4
  24. package/build/configure.d.ts +0 -1
  25. package/build/configure.js +0 -1
  26. package/build/index.d.ts +11 -9
  27. package/build/index.js +12 -5
  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 +46 -5
  45. package/build/src/drivers/database_relations_driver.js +297 -59
  46. package/build/src/drivers/openfga_driver.d.ts +159 -26
  47. package/build/src/drivers/openfga_driver.js +980 -231
  48. package/build/src/drivers/openfga_facts.d.ts +112 -1
  49. package/build/src/drivers/openfga_facts.js +351 -11
  50. package/build/src/drivers/openfga_relations_driver.d.ts +109 -8
  51. package/build/src/drivers/openfga_relations_driver.js +817 -53
  52. package/build/src/errors.d.ts +233 -1
  53. package/build/src/errors.js +283 -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 +164 -31
  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 +3 -4
  81. package/build/src/openfga.js +5 -3
  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 +1 -2
  87. package/build/src/relations/check_many.js +6 -5
  88. package/build/src/relations/define_relations_config.d.ts +140 -5
  89. package/build/src/relations/define_relations_config.js +205 -6
  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 +46 -8
  95. package/build/src/relations/manager.js +178 -16
  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 +274 -2
  99. package/build/src/relations/reconcile.js +1006 -162
  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 +127 -0
  103. package/build/src/relations/tuple_order.js +599 -0
  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 +60 -1
  111. package/build/src/shared/backend_guard.js +179 -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 +1 -2
  121. package/build/src/testing/main.js +1 -2
  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 +25 -1
  125. package/build/src/testing/relations_contract.js +996 -49
  126. package/build/src/testing/relations_reconcile_contract.d.ts +9 -1
  127. package/build/src/testing/relations_reconcile_contract.js +207 -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 +170 -15
  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_config_store.d.ts.map +0 -1
  239. package/build/src/relations_config_store.js.map +0 -1
  240. package/build/src/relations_reconcile.d.ts.map +0 -1
  241. package/build/src/relations_reconcile.js.map +0 -1
  242. package/build/src/scope_outbox.d.ts.map +0 -1
  243. package/build/src/scope_outbox.js.map +0 -1
  244. package/build/src/shared/backend_guard.d.ts.map +0 -1
  245. package/build/src/shared/backend_guard.js.map +0 -1
  246. package/build/src/shared/sql_expiry.d.ts.map +0 -1
  247. package/build/src/shared/sql_expiry.js.map +0 -1
  248. package/build/src/shared/transaction_guard.d.ts.map +0 -1
  249. package/build/src/shared/transaction_guard.js.map +0 -1
  250. package/build/src/sql_descendants.d.ts.map +0 -1
  251. package/build/src/sql_descendants.js.map +0 -1
  252. package/build/src/testing/contract.d.ts.map +0 -1
  253. package/build/src/testing/contract.js.map +0 -1
  254. package/build/src/testing/main.d.ts.map +0 -1
  255. package/build/src/testing/main.js.map +0 -1
  256. package/build/src/testing/migration_contract.d.ts.map +0 -1
  257. package/build/src/testing/migration_contract.js.map +0 -1
  258. package/build/src/testing/relations_contract.d.ts.map +0 -1
  259. package/build/src/testing/relations_contract.js.map +0 -1
  260. package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
  261. package/build/src/testing/relations_reconcile_contract.js.map +0 -1
  262. package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
  263. package/build/src/testing/scope_outbox_contract.js.map +0 -1
  264. package/build/src/testing/scope_tree.d.ts.map +0 -1
  265. package/build/src/testing/scope_tree.js.map +0 -1
  266. package/build/src/testing/test_clock.d.ts.map +0 -1
  267. package/build/src/testing/test_clock.js.map +0 -1
  268. package/build/src/traits/authz_scopes.d.ts.map +0 -1
  269. package/build/src/traits/authz_scopes.js.map +0 -1
  270. package/build/src/traits/has_uuid.d.ts.map +0 -1
  271. package/build/src/traits/has_uuid.js.map +0 -1
  272. package/build/src/types.d.ts.map +0 -1
  273. package/build/src/types.js.map +0 -1
  274. package/build/stubs/main.d.ts.map +0 -1
  275. package/build/stubs/main.js.map +0 -1
@@ -11,6 +11,15 @@ export interface RelationsReconcileHarness {
11
11
  makeB(config: RelationsConfig): RelationsDriver | Promise<RelationsDriver>;
12
12
  /** Limpia ambos backends antes de cada caso. */
13
13
  clean?(): Promise<void>;
14
+ /**
15
+ * **La cara del DENY** (plan 2.5 · lote D · D2; tester T3 / D-27): los dos drivers declaran `relationDenies: true` y
16
+ * `config` tiene al menos un tipo en `deny: 'write'`. Registra, por dirección, (a) «el deny viaja como deny» —con
17
+ * `kind`, ANTES que los allows, y el CENSO lo busca por `listDenies`; `check` del destino niega— y (b) «la poda de un
18
+ * deny sobrante es `removeDeny`, después de los allows», más un negativo global (S6: destino sin `relationDenies` ⇒
19
+ * 500 `E_AUTHZ_DENY_MIGRATION_REFUSED` con CERO escrituras). Un driver de terceros que declare `relationDenies` y no
20
+ * marque `kind` en `enumerateRelations` NO pasa (a): la pasada se niega con el motivo `unmarked`.
21
+ */
22
+ denies?: boolean;
14
23
  }
15
24
  export declare function runRelationsReconcileContract(harness: RelationsReconcileHarness): {
16
25
  registered: number;
@@ -21,4 +30,3 @@ export declare function registerRelationsReconcileContract(harness: RelationsRec
21
30
  }): {
22
31
  registered: number;
23
32
  };
24
- //# sourceMappingURL=relations_reconcile_contract.d.ts.map
@@ -190,6 +190,213 @@ export function registerRelationsReconcileContract(harness, api) {
190
190
  assert.lengthOf(first.subjects, 0, 'dry-run no tocó el destino');
191
191
  });
192
192
  }
193
+ if (harness.denies === true) {
194
+ const denyType = harness.config.objectTypes.find((t) => t.deny === 'write')?.type;
195
+ if (!denyType)
196
+ throw new Error(`runRelationsReconcileContract [${harness.name}]: denies: true exige un tipo con deny: 'write' en config`);
197
+ const writesOf = (driver) => {
198
+ const log = [];
199
+ const proxy = new Proxy(driver, {
200
+ get(target, prop) {
201
+ const value = Reflect.get(target, prop, target);
202
+ if (typeof prop === 'string' && ['relate', 'unrelate', 'deny', 'removeDeny'].includes(prop) && typeof value === 'function') {
203
+ return async (...args) => {
204
+ log.push(`${prop} ${args[1]}`);
205
+ return value.apply(target, args);
206
+ };
207
+ }
208
+ return typeof value === 'function' ? value.bind(target) : value;
209
+ },
210
+ });
211
+ return { driver: proxy, log };
212
+ };
213
+ /**
214
+ * Cierre 3 de D2 (🟠 C2-1 / 🟡 C2-2): un espía que pregunta `check(probe)` tras CADA escritura del destino — los
215
+ * prefijos de la pasada, tal como los ve el PDP del destino.
216
+ */
217
+ const prefixSpy = (driver, probe) => {
218
+ const prefixes = [];
219
+ const proxy = new Proxy(driver, {
220
+ get(target, prop) {
221
+ const value = Reflect.get(target, prop, target);
222
+ if (typeof prop === 'string' && ['relate', 'unrelate', 'deny', 'removeDeny'].includes(prop) && typeof value === 'function') {
223
+ return async (...args) => {
224
+ const out = await value.apply(target, args);
225
+ const s = args[0];
226
+ const who = isRelUserset(s) ? `${s.object.id.slice(-4)}#${s.relation}` : `${s.uuid.slice(-4)}`;
227
+ prefixes.push(`${prop}(${who} ${args[1]} ${args[2]?.type}:${String(args[2]?.id).slice(-4)}) ⇒ ${await probe(driver)}`);
228
+ return out;
229
+ };
230
+ }
231
+ return typeof value === 'function' ? value.bind(target) : value;
232
+ },
233
+ });
234
+ return { driver: proxy, prefixes };
235
+ };
236
+ const past = () => new Date(Date.now() - 3_600_000);
237
+ for (const dir of directions) {
238
+ const from = () => (dir.source === 'A' ? a : b);
239
+ const to = () => (dir.source === 'A' ? b : a);
240
+ test(`${dir.label} · vigencia · C2-1 · una membresía PORTADORA VENCIDA en el destino y viva en el origen es una escritura normal (jamás renewal-unsafe): ningún prefijo concede, el destino niega o concede según el ORIGEN y la 2.ª pasada tiene el plan vacío`, async ({ assert }) => {
241
+ const p = { type: 'unit', uuid: uuidv7() };
242
+ const u = { type: 'user', uuid: uuidv7() };
243
+ const h = { type: 'group', id: uuidv7() };
244
+ const x = { type: denyType, id: uuidv7() };
245
+ const y = { type: denyType, id: uuidv7() };
246
+ for (const side of [from(), to()]) {
247
+ await side.deny({ object: h, relation: 'member' }, 'viewer', x, p);
248
+ await side.relate({ object: h, relation: 'member' }, 'viewer', y, p);
249
+ }
250
+ await from().relate(u, 'member', h, p);
251
+ await from().relate(u, 'viewer', x, p);
252
+ await to().relate(u, 'member', h, p, { expiresAt: past() });
253
+ assert.isFalse(await from().check(u, 'viewer', x, p), 'el origen niega X');
254
+ assert.isTrue(await from().check(u, 'viewer', y, p), 'el origen concede Y');
255
+ assert.isFalse(await to().check(u, 'viewer', x, p), 'el destino niega X al empezar');
256
+ assert.isFalse(await to().check(u, 'viewer', y, p), 'el destino niega Y al empezar (la membresía venció)');
257
+ const dry = await reconcileRelations({ from: from(), to: to(), partition: p, toConfig: harness.config, dryRun: true });
258
+ const spy = prefixSpy(to(), (d) => d.check(u, 'viewer', x, p));
259
+ const report = await reconcileRelations({ from: from(), to: spy.driver, partition: p, toConfig: harness.config });
260
+ for (const r of [dry, report]) {
261
+ assert.deepEqual((r.details ?? []).map((d) => d.reason), [], 'ni renewal-unsafe ni residuo: la vencida es AUSENTE');
262
+ assert.deepEqual({ written: r.written, updated: r.updated }, { written: 2, updated: 0 }, 'la membresía y el allow son ESCRITURAS');
263
+ assert.deepEqual(r.orderResidue, []);
264
+ }
265
+ assert.notInclude(spy.prefixes.join(' | '), '⇒ true', `ningún prefijo concede X: ${spy.prefixes.join(' | ')}`);
266
+ assert.isFalse(await to().check(u, 'viewer', x, p), 'el destino niega X, como el origen');
267
+ assert.isTrue(await to().check(u, 'viewer', y, p), 'el destino concede Y, como el origen');
268
+ const again = await reconcileRelations({ from: from(), to: to(), partition: p, toConfig: harness.config });
269
+ assert.equal(again.written + again.updated + again.deleted, 0, 'converge: la 2.ª pasada no escribe nada');
270
+ assert.deepEqual(again.details ?? [], []);
271
+ });
272
+ test(`${dir.label} · vigencia · C2-2 · una arista de anidación VENCIDA solo en el destino no cuenta para el auto-corte del orden: la membresía que entra en el grupo que DENIEGA va antes que la del grupo que concede, y ningún prefijo concede (sin residuo)`, async ({ assert }) => {
273
+ const p = { type: 'unit', uuid: uuidv7() };
274
+ const u = { type: 'user', uuid: uuidv7() };
275
+ // Ids ordenados: sin la precedencia «h antes que g», el desempate estable pondría g primero.
276
+ const [gId, hId, kId] = [uuidv7(), uuidv7(), uuidv7()].sort();
277
+ const g = { type: 'group', id: gId };
278
+ const h = { type: 'group', id: hId };
279
+ const k = { type: 'group', id: kId };
280
+ const x = { type: denyType, id: uuidv7() };
281
+ for (const side of [from(), to()]) {
282
+ await side.relate({ object: g, relation: 'member' }, 'viewer', x, p);
283
+ await side.deny({ object: k, relation: 'member' }, 'viewer', x, p);
284
+ await side.deny({ object: h, relation: 'member' }, 'viewer', x, p);
285
+ }
286
+ await to().relate({ object: g, relation: 'member' }, 'member', k, p, { expiresAt: past() });
287
+ await from().relate(u, 'member', g, p);
288
+ await from().relate(u, 'member', h, p);
289
+ assert.isFalse(await to().check(u, 'viewer', x, p));
290
+ const spy = prefixSpy(to(), (d) => d.check(u, 'viewer', x, p));
291
+ const report = await reconcileRelations({ from: from(), to: spy.driver, partition: p, toConfig: harness.config });
292
+ assert.deepEqual(report.orderResidue, []);
293
+ assert.lengthOf(spy.prefixes, 2, spy.prefixes.join(' | '));
294
+ assert.notInclude(spy.prefixes.join(' | '), '⇒ true', `ningún prefijo concede: ${spy.prefixes.join(' | ')}`);
295
+ assert.isFalse(await to().check(u, 'viewer', x, p), 'final: niega, como el origen');
296
+ });
297
+ test(`${dir.label} · vigencia · C3-1 · una membresía PORTADORA VIVA en el destino con una caducidad MÁS CORTA que la del origen: en un destino sin atomicRenewal no se renueva (renewal-unsafe) y su PARTICIÓN se niega —cero escrituras, partitionsRefused— (cierre 5 de D2, opción A); con atomicRenewal se renueva; en los dos, el destino NIEGA en now y TRAS la caducidad vieja (reloj inyectado), como el origen`, async ({ assert }) => {
298
+ const p = { type: 'unit', uuid: uuidv7() };
299
+ const u = { type: 'user', uuid: uuidv7() };
300
+ const h = { type: 'group', id: uuidv7() };
301
+ const x = { type: denyType, id: uuidv7() };
302
+ const old = new Date(Date.now() + 3_600_000);
303
+ for (const side of [from(), to()])
304
+ await side.deny({ object: h, relation: 'member' }, 'viewer', x, p);
305
+ await from().relate(u, 'member', h, p, { expiresAt: new Date(Date.now() + 86_400_000) });
306
+ await from().relate(u, 'viewer', x, p);
307
+ await to().relate(u, 'member', h, p, { expiresAt: old });
308
+ assert.isFalse(await from().check(u, 'viewer', x, p), 'el origen niega X');
309
+ const atomic = to().capabilities?.atomicRenewal === true;
310
+ const dry = await reconcileRelations({ from: from(), to: to(), partition: p, toConfig: harness.config, dryRun: true });
311
+ const spy = writesOf(to());
312
+ const report = await reconcileRelations({ from: from(), to: spy.driver, partition: p, toConfig: harness.config });
313
+ for (const r of [dry, report]) {
314
+ assert.deepEqual({
315
+ written: r.written,
316
+ updated: r.updated,
317
+ details: (r.details ?? []).map((d) => `${d.side}:${d.reason}`),
318
+ refused: (r.partitionsRefused ?? []).map((x) => x.causes.map((c) => c.reason).join(',')),
319
+ }, atomic
320
+ ? { written: 1, updated: 1, details: [], refused: [] }
321
+ : { written: 0, updated: 0, details: ['destino:renewal-unsafe'], refused: ['renewal-unsafe'] }, `atomicRenewal=${atomic}`);
322
+ }
323
+ if (!atomic)
324
+ assert.deepEqual(spy.log, [], `ni la renovación ni el allow llegan al driver: ${spy.log.join(' | ')}`);
325
+ assert.isFalse(await to().check(u, 'viewer', x, p), 'el destino niega X en now');
326
+ // El HORIZONTE: tras la caducidad vieja del destino, con el reloj del destino adelantado (sin sleeps).
327
+ const later = typeof to().withClock === 'function' ? to().withClock(() => new Date(old.getTime() + 1_000)) : null;
328
+ if (later)
329
+ assert.isFalse(await later.check(u, 'viewer', x, p), `ROJO (C3-1): tras la caducidad vieja el destino concede lo que el origen niega`);
330
+ });
331
+ test(`${dir.label} · D2 · el deny viaja como DENY (kind), ANTES que los allows; el CENSO lo busca por listDenies y check niega en el destino`, async ({ assert }) => {
332
+ const seed = makeSeed();
333
+ await plant(from(), seed);
334
+ const object = { type: denyType, id: uuidv7() };
335
+ const viaGroup = { type: denyType, id: uuidv7() };
336
+ // Corte de camino (owner con deny en editor), y un deny transportado por g1#member (u3 es miembro).
337
+ await from().relate(seed.u1, 'owner', object, seed.partition);
338
+ await from().deny(seed.u1, 'editor', object, seed.partition);
339
+ await from().relate(seed.u3, 'viewer', viaGroup, seed.partition);
340
+ await from().deny({ object: seed.g1, relation: 'member' }, 'viewer', viaGroup, seed.partition);
341
+ const spy = writesOf(to());
342
+ const report = await reconcileRelations({ from: from(), to: spy.driver, partition: seed.partition });
343
+ assert.equal(report.denies?.written, 2, 'los dos denies, contados aparte');
344
+ assert.equal(report.written, seed.facts.length + 4);
345
+ assert.equal(report.skipped.undeclared, 0, 'un deny NUNCA es undeclared');
346
+ const firstAllow = spy.log.findIndex((l) => l.startsWith('relate'));
347
+ const lastDeny = spy.log.map((l) => l.startsWith('deny')).lastIndexOf(true);
348
+ assert.isBelow(lastDeny, firstAllow, `fase 1 (denies) antes que fase 2 (allows): ${spy.log.join(' | ')}`);
349
+ assert.isFalse(spy.log.some((l) => l.includes('denied_')), 'jamás relate/unrelate de una relación denied_*');
350
+ await census(assert, to(), seed);
351
+ const denied = await to().listDenies('editor', object, seed.partition);
352
+ assert.isTrue(denied.subjects.some((s) => subjectKey(s) === subjectKey(seed.u1)), 'CENSO del deny directo');
353
+ const deniedGroup = await to().listDenies('viewer', viaGroup, seed.partition);
354
+ assert.isTrue(deniedGroup.subjects.some((s) => subjectKey(s) === `${seed.g1.type}:${seed.g1.id}#member`), 'CENSO del deny a g1#member');
355
+ assert.isFalse(await to().check(seed.u1, 'owner', object, seed.partition), 'corte de camino en el destino');
356
+ assert.isFalse(await to().check(seed.u3, 'viewer', viaGroup, seed.partition), 'el deny del grupo llega a su miembro');
357
+ const again = await reconcileRelations({ from: from(), to: to(), partition: seed.partition });
358
+ assert.equal(again.written + again.updated + again.deleted, 0, 'idempotente con denies');
359
+ assert.equal(again.denies?.unchanged, 2);
360
+ });
361
+ test(`${dir.label} · D2 · --prune: el deny SOBRANTE se borra con removeDeny (jamás unrelate), DESPUÉS de los allows sobrantes`, async ({ assert }) => {
362
+ const seed = makeSeed();
363
+ await plant(from(), seed);
364
+ const gone = { type: denyType, id: uuidv7() };
365
+ await to().relate(seed.u2, 'viewer', gone, seed.partition);
366
+ await to().deny(seed.u2, 'viewer', gone, seed.partition);
367
+ const spy = writesOf(to());
368
+ const report = await reconcileRelations({ from: from(), to: spy.driver, partition: seed.partition, prune: true });
369
+ assert.equal(report.denies?.deleted, 1);
370
+ const prunes = spy.log.filter((l) => l.startsWith('unrelate') || l.startsWith('removeDeny'));
371
+ assert.deepEqual(prunes, ['unrelate viewer', 'removeDeny viewer'], 'allows antes que denies; el deny con removeDeny');
372
+ assert.lengthOf((await to().listDenies('viewer', gone, seed.partition)).subjects, 0);
373
+ await census(assert, to(), seed);
374
+ });
375
+ }
376
+ test('D2 · S6 · un destino SIN relationDenies con un deny en el origen ⇒ 500 E_AUTHZ_DENY_MIGRATION_REFUSED con CERO escrituras (jamás skipped)', async ({ assert }) => {
377
+ const seed = makeSeed();
378
+ await plant(a, seed);
379
+ await a.deny(seed.u1, 'viewer', { type: denyType, id: uuidv7() }, seed.partition);
380
+ const blind = writesOf(b);
381
+ const target = new Proxy(blind.driver, {
382
+ get(t, prop) {
383
+ if (prop === 'capabilities')
384
+ return { ...b.capabilities, relationDenies: false };
385
+ return Reflect.get(t, prop, t);
386
+ },
387
+ });
388
+ let caught = null;
389
+ try {
390
+ await reconcileRelations({ from: a, to: target, partition: seed.partition });
391
+ }
392
+ catch (error) {
393
+ caught = error;
394
+ }
395
+ assert.equal(caught?.status, 500, String(caught?.message));
396
+ assert.equal(caught?.code, 'E_AUTHZ_DENY_MIGRATION_REFUSED');
397
+ assert.deepEqual(blind.log, [], 'CERO escrituras: tampoco los allows');
398
+ });
399
+ }
193
400
  // El puerto exige `enumerateRelations` en ambos: un driver que no lo trae
194
401
  // no puede ser origen ni destino (500, jamás una migración a ciegas).
195
402
  test('un driver sin `enumerateRelations` ⇒ 500 E_AUTHZ_UNSUPPORTED como origen', async ({ assert }) => {
@@ -217,4 +424,3 @@ export function registerRelationsReconcileContract(harness, api) {
217
424
  });
218
425
  return { registered };
219
426
  }
220
- //# sourceMappingURL=relations_reconcile_contract.js.map
@@ -108,4 +108,3 @@ export declare function registerScopeOutboxContract(harness: ScopeOutboxContract
108
108
  registered: number;
109
109
  covered: Set<keyof ScopeOutboxCapabilities>;
110
110
  };
111
- //# sourceMappingURL=scope_outbox_contract.d.ts.map
@@ -752,4 +752,3 @@ export function registerScopeOutboxContract(harness, api) {
752
752
  });
753
753
  return { registered, covered };
754
754
  }
755
- //# sourceMappingURL=scope_outbox_contract.js.map
@@ -62,4 +62,3 @@ export declare function descendantsFrom(tree: ContractScopeTree): ScopeDescendan
62
62
  */
63
63
  export declare function memoryScopeTree(): ContractScopeTree;
64
64
  export { APP_SCOPE };
65
- //# sourceMappingURL=scope_tree.d.ts.map
@@ -142,4 +142,3 @@ export function memoryScopeTree() {
142
142
  };
143
143
  }
144
144
  export { APP_SCOPE };
145
- //# sourceMappingURL=scope_tree.js.map
@@ -12,4 +12,3 @@ export interface TestClock {
12
12
  }
13
13
  /** Un reloj de test parado en `start` (default: la hora de pared al crearlo), controlable con `set`/`advance`. */
14
14
  export declare function createTestClock(start?: Date | number | string): TestClock;
15
- //# sourceMappingURL=test_clock.d.ts.map
@@ -62,4 +62,3 @@ export function createTestClock(start = systemClock()) {
62
62
  clock.at = (offsetMs) => new Date(current + offsetMs);
63
63
  return clock;
64
64
  }
65
- //# sourceMappingURL=test_clock.js.map
@@ -42,4 +42,3 @@ export type WithAuthzScopes<T extends Constructor> = T & AuthzScopesStatics;
42
42
  */
43
43
  export declare function withAuthzScopes<T extends Constructor | AuthzScopesOptions>(arg: T): T extends Constructor ? WithAuthzScopes<T> : <U extends Constructor>(Base: U) => WithAuthzScopes<U>;
44
44
  export {};
45
- //# sourceMappingURL=authz_scopes.d.ts.map
@@ -54,4 +54,3 @@ export function withAuthzScopes(arg) {
54
54
  const clock = options.clock ?? systemClock;
55
55
  return (Base) => mixin(Base, clock);
56
56
  }
57
- //# sourceMappingURL=authz_scopes.js.map
@@ -81,6 +81,11 @@ export declare const hasUuid: <Model extends NormalizeConstructor<typeof BaseMod
81
81
  <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model_1, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelPaginatorContract<InstanceType<Model_1>>, "paginate">): void;
82
82
  <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Event extends import("@adonisjs/lucid/types/model").EventsList>(this: Model_1, event: Event, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model_1>, Event>): void;
83
83
  };
84
+ before: {
85
+ <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Event_1 extends "find" | "fetch">(this: Model_1, event: Event_1, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>, Event_1>): void;
86
+ <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model_1, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>], "paginate">): void;
87
+ <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Event_2 extends import("@adonisjs/lucid/types/model").EventsList>(this: Model_1, event: Event_2, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model_1>, Event_2>): void;
88
+ };
84
89
  readonly booted: boolean;
85
90
  $columnsDefinitions: Map<string, import("@adonisjs/lucid/types/model").ModelColumnOptions>;
86
91
  $relationsDefinitions: Map<string, import("@adonisjs/lucid/types/relations").RelationshipsContract>;
@@ -117,11 +122,6 @@ export declare const hasUuid: <Model extends NormalizeConstructor<typeof BaseMod
117
122
  };
118
123
  $defineProperty: <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Prop extends keyof Model_1>(this: Model_1, propertyName: Prop, defaultValue: Model_1[Prop], strategy: "inherit" | "define" | ((value: Model_1[Prop]) => Model_1[Prop])) => void;
119
124
  boot: () => void;
120
- before: {
121
- <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Event_1 extends "find" | "fetch">(this: Model_1, event: Event_1, handler: import("@adonisjs/lucid/types/model").HooksHandler<import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>, Event_1>): void;
122
- <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel>(this: Model_1, event: "paginate", handler: import("@adonisjs/lucid/types/model").HooksHandler<[import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>, import("@adonisjs/lucid/types/model").ModelQueryBuilderContract<Model_1>], "paginate">): void;
123
- <Model_1 extends import("@adonisjs/lucid/types/model").LucidModel, Event_2 extends import("@adonisjs/lucid/types/model").EventsList>(this: Model_1, event: Event_2, handler: import("@adonisjs/lucid/types/model").HooksHandler<InstanceType<Model_1>, Event_2>): void;
124
- };
125
125
  create: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
126
126
  createQuietly: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>, options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>>;
127
127
  createMany: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, values: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>[]>;
@@ -148,4 +148,3 @@ export declare const hasUuid: <Model extends NormalizeConstructor<typeof BaseMod
148
148
  fetchOrCreateMany: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, predicate: keyof import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>> | (keyof import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>)[], payload: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>[]>;
149
149
  updateOrCreateMany: <T extends import("@adonisjs/lucid/types/model").LucidModel>(this: T, predicate: keyof import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>> | (keyof import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>)[], payload: Partial<import("@adonisjs/lucid/types/model").ModelAttributes<InstanceType<T>>>[], options?: import("@adonisjs/lucid/types/model").ModelAssignOptions) => Promise<InstanceType<T>[]>;
150
150
  } & Model;
151
- //# sourceMappingURL=has_uuid.d.ts.map
@@ -35,4 +35,3 @@ export const hasUuid = (superclass) => {
35
35
  ], WithPrimaryUuidClass, "generateUuid", null);
36
36
  return WithPrimaryUuidClass;
37
37
  };
38
- //# sourceMappingURL=has_uuid.js.map
@@ -334,6 +334,15 @@ export interface AuthorizationDriverCapabilities {
334
334
  * `transactional: true`.
335
335
  */
336
336
  transactionalWrites: boolean;
337
+ /**
338
+ * **El driver es EXPERIMENTAL: fuera de la garantía de la versión** (estrategia 2.5.0, firma 4 del dueño 2026-10-21).
339
+ * **Opcional y normalizada a `false`** (regla D15). `openfga` = `true` en 2.5.x: no apto para producción, no se
340
+ * construye sin `experimental: 'openfga@2.5'` (500 `E_AUTHZ_CONFIG` sin él) y sus límites declarados —con cifra,
341
+ * receta, detector y versión de cierre (2.6)— están en README «Limits». `database` = `false` (no la declara). El juez
342
+ * corre ENTERO contra un driver experimental (no hay skip): la capacidad dice lo que el consumidor tiene que saber, no
343
+ * apaga casos. Par de caras en `runAuthorizationDriverContract`.
344
+ */
345
+ experimental?: boolean;
337
346
  }
338
347
  /**
339
348
  * Las claves de `AuthorizationDriverCapabilities`, LITERALES (2.4.0-alpha.4):
@@ -730,6 +739,14 @@ export interface ReconcileFact {
730
739
  * que es justo lo que la migración no puede hacer.
731
740
  */
732
741
  expiresAt?: Date | null;
742
+ /**
743
+ * `deny` (cierre-2 de L11 · C1): el origen guarda ese deny con una condición que el paquete no escribe ni sabe leer
744
+ * (escritor AJENO; el motivo, p. ej. `foreign: condición 'inverted'`). NO es un deny limpio: el destino lo migra como
745
+ * deny —fail-closed: ante la duda, niega— pero lo DECLARA (`invalid-condition`, deriva); y en la pasada de
746
+ * MANTENIMIENTO (origen = destino) no se escribe ni se borra, solo se reporta. Un destino que ignore la marca lo trata
747
+ * como deny permanente, que es la lectura conservadora para un deny.
748
+ */
749
+ invalidCondition?: string;
733
750
  /** Cómo lo nombra el origen (para `details`): un motivo sin la fila no se arregla. */
734
751
  detail: string;
735
752
  }
@@ -1151,6 +1168,18 @@ export interface ScopeRelayFailure {
1151
1168
  change: ScopeTreeChange;
1152
1169
  error: string;
1153
1170
  transient: boolean;
1171
+ /**
1172
+ * Cierre de R3 (⚪ R3-A7; punto 7): el `code` del error (`E_AUTHZ_WRITE_CONFLICT`, `E_AUTHZ_RESOLUTION_LIMIT`,
1173
+ * `E_AUTHZ_BACKEND_UNAVAILABLE`…), para que el comando diga QUÉ rama transitoria corrió: una carrera o un grafo
1174
+ * demasiado profundo no son «el backend no respondió». Ausente si el error no trae `code` (driver de terceros).
1175
+ */
1176
+ code?: string;
1177
+ /**
1178
+ * Cierre-2 de L11 (⚪ C2): `true` ⇒ el fallo es del DESPLIEGUE (el modelo fijado no es el que genera la config, o el
1179
+ * generador no puede generar con él; `isDeploymentFailure`), no del backend ni de la entrada. Es `transient` también
1180
+ * (no consume intento), pero el backend SÍ respondió: el comando lo dice en su propia línea. Ausente = no lo es.
1181
+ */
1182
+ deployment?: true;
1154
1183
  }
1155
1184
  /** Qué fue de una entrada en la pasada (2.4.0-alpha.5 · R1, `ScopeRelayReport.entries`). */
1156
1185
  export type ScopeRelayEntryStatus = 'applied' | 'deferred' | 'failed' | 'dead';
@@ -1179,6 +1208,13 @@ export interface ScopeRelayEntry {
1179
1208
  status: ScopeRelayEntryStatus;
1180
1209
  reason?: string;
1181
1210
  attempts?: number;
1211
+ /**
1212
+ * Solo en `applied` de un `attached`/`moved` relevado contra un driver con `hierarchyFacts` (asentar · cierre 1): el
1213
+ * padre que se APLICÓ de verdad —el FRESCO del consumidor, que puede no ser `change.parent` (`reason:
1214
+ * 'consumer-parent'`)— o `null` si no se escribió arista (`reason: 'superseded'`: el nodo ya no existe y un `detached`
1215
+ * suyo lo respalda). Ausente en `database`, que aplica el encolado tal cual.
1216
+ */
1217
+ appliedParent?: ScopeRef | null;
1182
1218
  /**
1183
1219
  * Solo en `deferred` (2.4.0-alpha.8): el ORIGEN —transitivo, la causa
1184
1220
  * raíz— del bloqueo que la aplazó. `parked` = una entrada APARCADA (la
@@ -1826,7 +1862,12 @@ export declare function isRelUserset(subject: RelSubject): subject is RelUserset
1826
1862
  * muta el store.
1827
1863
  */
1828
1864
  export interface RelationRef {
1829
- operation: 'relate' | 'unrelate';
1865
+ /**
1866
+ * La operación. **Ensanchada en 2.5.0-alpha.3 (lote D)** con `'deny'` y `'removeDeny'`: un `assertWrite`/
1867
+ * `onRelationWrite` escrito contra 2.4 con `if (op === 'unrelate') quitar; else añadir` trataría un DENY como un
1868
+ * alta y CONCEDERÍA de más en el índice del consumidor (auditor R2-14). Haz `switch` sobre la operación.
1869
+ */
1870
+ operation: 'relate' | 'unrelate' | 'deny' | 'removeDeny';
1830
1871
  subject: RelSubject;
1831
1872
  relation: string;
1832
1873
  object: RelObject;
@@ -1856,11 +1897,16 @@ export interface RelationRef {
1856
1897
  * terceros y dentro de una `{ transaction }` sería un número que un
1857
1898
  * rollback desmiente). Quien necesite el «cuántas» tiene
1858
1899
  * `enumerateRelations`/`listSubjects` ANTES de purgar. Las purgas NO pasan
1859
- * por `assertWrite` (su `RelationRef.operation` es `'relate' | 'unrelate'`,
1900
+ * por `assertWrite` (su `RelationRef.operation` es `'relate' | 'unrelate' | 'deny' | 'removeDeny'`,
1860
1901
  * tipo publicado del hook R-13).
1861
1902
  */
1862
1903
  export interface RelationWriteEvent {
1863
- operation: 'relate' | 'unrelate' | 'purgeObject' | 'purgeSubject';
1904
+ /**
1905
+ * La operación. Desde 2.5.0-alpha.3 también `'deny'`/`'removeDeny'` (con `subject`, `relation` —el nombre
1906
+ * PÚBLICO, nunca `denied_<rel>`—, `object`, `partition` y `actor`; sin `expiresAt`: el deny no caduca, D3). **Un
1907
+ * handler que trate todo lo que no sea `unrelate` como alta concede de más** (R2-14): ramifica por operación.
1908
+ */
1909
+ operation: 'relate' | 'unrelate' | 'deny' | 'removeDeny' | 'purgeObject' | 'purgeSubject';
1864
1910
  /** Ausente solo en `purgeObject` (afecta a todos los sujetos del objeto). */
1865
1911
  subject?: RelSubject;
1866
1912
  /** Ausente en `purgeObject`/`purgeSubject` (afectan a todas las relaciones). */
@@ -1943,7 +1989,22 @@ export interface RelationWriteOptions extends RelationTransactionOptions {
1943
1989
  */
1944
1990
  expiresAt?: Date | null;
1945
1991
  }
1946
- /** Una página de una enumeración de relaciones (cursor opaco que AVANZA, no filtra herencia). */
1992
+ /**
1993
+ * Opciones de `deny`/`removeDeny` (plan 2.5 · lote D): la transacción del consumidor y el `actor`. **Sin `expiresAt`**
1994
+ * (D3: el deny no caduca en v1): pasarlo —incluso `null`— es 422 `E_AUTHZ_DENY_EXPIRY_UNSUPPORTED` con la razón medida.
1995
+ */
1996
+ export interface RelationDenyOptions extends RelationTransactionOptions {
1997
+ /** Quién ordena el deny; validado (422) y viaja en `RelationWriteEvent.actor`. */
1998
+ actor?: SubjectRef;
1999
+ }
2000
+ /** Una decisión de `decideMany` (D9): `allow` (concede aquí), `deny` (no heredes más allá de este objeto), `none` (nada aquí). */
2001
+ export type RelationDecision = 'allow' | 'deny' | 'none';
2002
+ /**
2003
+ * Una página de `enumerateRelations`/`enumerateRelationsMany` (cursor opaco que AVANZA, no filtra herencia). **Solo
2004
+ * `enumerate*`** desde el lote «antes de RC» · 7.4: los `list*` no paginan en 2.5 (ver `RelationObjectsPage`).
2005
+ * `limit` entero ≥ 1 (si no, 422 `E_AUTHZ_INVALID_PAGE`; por encima de `RELATIONS_ENUMERATE_MAX_LIMIT` se recorta);
2006
+ * `after` = el `cursor` que devolvió la página anterior, sin tocar.
2007
+ */
1947
2008
  export interface RelationPage {
1948
2009
  limit?: number;
1949
2010
  after?: string;
@@ -2014,24 +2075,51 @@ export interface RelationsDriverCapabilities {
2014
2075
  * revierten juntos) y que una transacción ajena no recibe ni una sentencia.
2015
2076
  */
2016
2077
  transactionalWrites: boolean;
2078
+ /**
2079
+ * **El driver sabe denegar** (plan 2.5 · lote D, D1): trae `deny`/`removeDeny`/`listDenies`/`decideMany` y TODAS
2080
+ * sus lecturas honran las tuplas `denied_<rel>` con la semántica de CORTE DE CAMINO (D5: un deny en `Y` anula todo
2081
+ * grant de `T` con `Y ∈ up*(T)`). **Opcional y normalizada a `false`** (regla D15: toda clave nueva desde 2.5.0 es
2082
+ * `?:`); con `false` (o ausente) las cuatro operaciones del deny son 500 `E_AUTHZ_UNSUPPORTED` nombrándolas, con
2083
+ * CERO llamadas al driver. `database` y `openfga` = `true`. Par de caras en `runRelationsDriverContract`.
2084
+ */
2085
+ relationDenies?: boolean;
2086
+ /**
2087
+ * **`relate` sobre una tupla EXISTENTE con otra caducidad es UNA escritura atómica** (plan 2.5 · lote D · cierre 2 de
2088
+ * D2, 🟠 N1): ningún instante sin la tupla. `database` = `true` (delete + insert en UNA transacción). `openfga` =
2089
+ * `false`: FGA no reemplaza la misma clave en un `Write` (M5), así que renueva con DOS (delete, write) y entre ellos la
2090
+ * tupla no está — si es una membresía `group:*#member` que TRANSPORTA un deny (el grupo, o uno por encima de él, es
2091
+ * sujeto de un deny vivo de la partición), el sujeto quedaría sin el deny. Un driver con `false` (o ausente: regla D15,
2092
+ * opcional y normalizada a `false`) **tiene que NEGAR esa renovación** con 409 `E_AUTHZ_RENEWAL_UNSAFE` y CERO
2093
+ * escrituras, y marcar como escritura PARCIAL (`markPartialWrite`) la renovación permitida cuyo 2.º `Write` falla; el
2094
+ * reconcile no le pide renovar una membresía portadora (residuo `renewal-unsafe`). Par de caras en
2095
+ * `runRelationsDriverContract`.
2096
+ */
2097
+ atomicRenewal?: boolean;
2098
+ /**
2099
+ * **El driver es EXPERIMENTAL: fuera de la garantía de la versión** (estrategia 2.5.0, firma 4). Mismo nombre y misma
2100
+ * regla que en `AuthorizationDriverCapabilities` (D15: opcional, normalizada a `false`). `openfga` = `true` en 2.5.x
2101
+ * (500 `E_AUTHZ_CONFIG` al construir sin `experimental: 'openfga@2.5'`; límites en README «Limits», cierre en 2.6);
2102
+ * `database` = `false`. Par de caras en `runRelationsDriverContract`.
2103
+ */
2104
+ experimental?: boolean;
2017
2105
  }
2018
2106
  /** Las claves de `RelationsDriverCapabilities`, literales: lo que `relations.capabilities()` normaliza (2.4.0-alpha.4). */
2019
2107
  export declare const RELATIONS_DRIVER_CAPABILITY_KEYS: ReadonlyArray<keyof RelationsDriverCapabilities>;
2020
2108
  /** La copia congelada y normalizada de `relations.capabilities()`; misma regla que `normalizeAuthorizationCapabilities`. */
2021
2109
  export declare function normalizeRelationsCapabilities(declared: Partial<RelationsDriverCapabilities> | undefined): Readonly<RelationsDriverCapabilities>;
2022
2110
  /**
2023
- * Una página de `listObjects`/`listSubjects`/`enumerateRelations`. `truncated`
2024
- * dice si el backend cortó al tope (solo con `listObjectsTruncation`): un
2025
- * consumidor que lo ve sabe que hay MÁS y no toma la lista por completa.
2111
+ * La respuesta de `listObjects`. **Sin `cursor`** (lote «antes de RC» · 7.4, opción 4 del juez): los `list*` de
2112
+ * relaciones devuelven el conjunto COMPLETO o señalan `truncated`; nunca un cursor. `truncated` dice si el backend
2113
+ * cortó (tope o deadline; solo con `listObjectsTruncation`): un consumidor que lo ve sabe que hay MÁS y no toma la
2114
+ * lista por completa. La paginación de los listados llegará en 2.6 como métodos APARTE (`*Page`) con capacidad propia.
2026
2115
  */
2027
2116
  export interface RelationObjectsPage {
2028
2117
  objects: RelObject[];
2029
- cursor?: string;
2030
2118
  truncated?: boolean;
2031
2119
  }
2120
+ /** La respuesta de `listSubjects`/`listDenies`/`membersOf`: completa o `truncated`, **sin `cursor`** (7.4). */
2032
2121
  export interface RelationSubjectsPage {
2033
2122
  subjects: RelSubject[];
2034
- cursor?: string;
2035
2123
  truncated?: boolean;
2036
2124
  }
2037
2125
  /** Una tupla de relación tal como la enumera `enumerateRelations` (origen de reconcile). */
@@ -2047,6 +2135,28 @@ export interface RelationTuple {
2047
2135
  * (la lección de la 3b); filtrarla en el origen la haría desaparecer sin rastro.
2048
2136
  */
2049
2137
  expiresAt?: Date | null;
2138
+ /**
2139
+ * **`allow` o `deny`** (plan 2.5 · lote D · D2): un deny se enumera con su relación PÚBLICA (`viewer`, no la
2140
+ * almacenada `denied_viewer`) y `kind: 'deny'`. Opcional en el tipo exportado (un driver de terceros anterior
2141
+ * sigue compilando; ausente = `allow`), **obligatorio en la práctica para un driver con `relationDenies: true`**:
2142
+ * `reconcileRelations` se NIEGA (500 `E_AUTHZ_DENY_MIGRATION_REFUSED`, motivo `unmarked`) a migrar una tupla cuya
2143
+ * relación empieza por el prefijo reservado `denied_` y no viene marcada — un deny sin `kind` casaría con un allow
2144
+ * o se contaría como relación no declarada, y perder un deny no es una pérdida que se pueda declarar.
2145
+ */
2146
+ kind?: 'allow' | 'deny';
2147
+ /**
2148
+ * **La condición de la tupla escrita no es la `not_expired` del paquete, o no se puede leer** (lote «antes de RC» ·
2149
+ * L11): el porqué, legible. Solo lo pone un driver que lee condiciones (`openfga`, `fgaTupleExpiry`): `foreign` (otra
2150
+ * condición o un contexto que no es exactamente `{ valid_until }`) o `unparseable`. Con esto puesto `expiresAt` es la
2151
+ * ÉPOCA (`new Date(0)`) y no es una caducidad real. **La época NO es conservadora para todo** (cierre-2 de L11 · ⚪ C3):
2152
+ * un lector que ignore este campo la lee VENCIDA, que es fail-CLOSED para un ALLOW (deja de conceder) pero fail-OPEN
2153
+ * para un DENY (`kind: 'deny'`) y para una membresía de `group` que TRANSPORTA un deny (deja de negar). Ningún valor de
2154
+ * `expiresAt` es seguro para los dos tipos de tupla (con `null` era al revés): quien lea `enumerateRelations` TIENE que
2155
+ * mirar `invalidCondition` antes que la fecha. Dentro del paquete se hace: `reconcileRelations` la clasifica en el PLAN, antes de mirar la fecha, como `invalid-condition` (origen:
2156
+ * no se migra; destino: presente e intocable, nunca se poda) y es deriva. Opcional (un driver de terceros anterior
2157
+ * sigue compilando; ausente = la condición se entendió).
2158
+ */
2159
+ invalidCondition?: string;
2050
2160
  }
2051
2161
  export interface RelationTuplePage {
2052
2162
  tuples: RelationTuple[];
@@ -2111,10 +2221,14 @@ export interface RelationsDriver {
2111
2221
  * llamante (eso es `decideMany`, lote D).
2112
2222
  */
2113
2223
  checkMany?(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<boolean[]>;
2114
- /** Los objetos de tipo `objectType` sobre los que `subject` tiene `relation`. Directos + derivados, sin herencia abierta. */
2115
- listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, page?: RelationPage): Promise<RelationObjectsPage>;
2116
- /** Los sujetos DIRECTOS de `relation` sobre `object` (holders y usersets). Nunca la membresía transitiva (eso es `membersOf`). */
2117
- listSubjects(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
2224
+ /**
2225
+ * Los objetos de tipo `objectType` sobre los que `subject` tiene `relation`. Directos + derivados, sin herencia
2226
+ * abierta. **Sin página** (7.4): el conjunto completo o `truncated`; un quinto argumento distinto de `undefined` ⇒
2227
+ * 500 `E_AUTHZ_UNSUPPORTED` sin tocar el backend (lo mismo en `listSubjects`/`listDenies`/`membersOf`).
2228
+ */
2229
+ listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef): Promise<RelationObjectsPage>;
2230
+ /** Los sujetos DIRECTOS de `relation` sobre `object` (holders y usersets). Nunca la membresía transitiva (eso es `membersOf`). Sin página (7.4). */
2231
+ listSubjects(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
2118
2232
  /** Borra todas las tuplas cuyo OBJETO es `object` (F-05 sobre el TIPO, alpha.3) y demuestra cero, o lanza 500 `E_AUTHZ_PURGE_INCOMPLETE` (invariante 11). Si falla tras borrar parte, marcado con `markPartialWrite`. */
2119
2233
  purgeObject(object: RelObject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
2120
2234
  /** Borra todas las tuplas cuyo SUJETO es `subject` (F-05 sobre el userset, alpha.3) y demuestra cero, o lanza 500. Si falla tras borrar parte, marcado con `markPartialWrite`. */
@@ -2125,7 +2239,7 @@ export interface RelationsDriver {
2125
2239
  * `listSubjects(member, group)`, que devuelve solo los hechos DIRECTOS. Solo
2126
2240
  * lo trae el driver con `membersOfNative: true`.
2127
2241
  */
2128
- membersOf?(object: RelObject, relation: string, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
2242
+ membersOf?(object: RelObject, relation: string, partition: ScopeRef): Promise<RelationSubjectsPage>;
2129
2243
  /** ORIGEN de `authz:reconcile` de relaciones: las tuplas paginadas, sin filtrar (la caducada LLEGA con su `expiresAt`). Solo con `enumerateRelations: true`. */
2130
2244
  enumerateRelations?(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
2131
2245
  /**
@@ -2142,6 +2256,48 @@ export interface RelationsDriver {
2142
2256
  * jamás «vacío = éxito» para una partición que no pudo leer entera.
2143
2257
  */
2144
2258
  enumerateRelationsMany?(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
2259
+ /**
2260
+ * **Deny de relación** (plan 2.5 · lote D · D1; obligatorio con `relationDenies: true`): escribe el deny de
2261
+ * `relation` sobre `object` para `subject` (la tupla almacenada `denied_<relation>`). Idempotente, SIN caducidad
2262
+ * (D3). Solo sobre un tipo en fase `'write'` del opt-in (422 `E_AUTHZ_RELATION_NOT_DENYABLE` si no), nunca sobre
2263
+ * `group` ni con `*`. Corte de camino (D5): un deny en `editor` anula entero un grant de `owner`.
2264
+ */
2265
+ deny?(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
2266
+ /** Retira el deny. No-op seguro si no existe. Sobre CUALQUIER tipo declarado (D8: retirar el opt-in es reversible). */
2267
+ removeDeny?(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
2268
+ /** Los sujetos con deny DIRECTO de `relation` sobre `object` (hechos, como `listSubjects`; no resta nada). Sin página (7.4). */
2269
+ listDenies?(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
2270
+ /**
2271
+ * **La primitiva de CADENA** (D9): una decisión por objeto, POR POSICIÓN — `allow` si `check` concede; si no, `deny`
2272
+ * si el sujeto tiene un deny vivo (directo o por grupo) sobre alguna relación de `up*(down*(R))` en el objeto; si
2273
+ * no, `none`. Las MISMAS garantías que `checkMany`: 1..`RELATIONS_CHECK_MANY_MAX` objetos (422 fuera), un `now`
2274
+ * por lote, F-05 por ítem, y un `error` en UN ítem ⇒ 503 para todo el lote (nunca `none`: sería fail-open).
2275
+ */
2276
+ decideMany?(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<RelationDecision[]>;
2277
+ /**
2278
+ * **¿Admite ESTE driver escrituras de deny en `object.type`?** (plan 2.5 · lote D · D2; juez ronda 2 · R2-5: «la
2279
+ * pregunta la responde el driver DESTINO en la fase de plan, antes de escribir: su config de relaciones y, en
2280
+ * `openfga`, su modelo fijado»). Devuelve la fase del opt-in del tipo en la config DEL DRIVER (`'write'`, `'read'`)
2281
+ * o `null` sin opt-in. F-05 estricto del par (422 si el tipo o la relación no están declarados), cero escrituras;
2282
+ * en `openfga` con opt-in además comprueba el modelo FIJADO (D-31: 500 `E_AUTHZ_CONFIG` si no declara
2283
+ * `denied_<rel>`). Lo llama `reconcileRelations` ANTES de escribir nada cuando el origen trae denies: un destino
2284
+ * con `relationDenies: true` que no lo traiga no puede ser destino de denies (500, jamás `skipped`). Lectura de
2285
+ * configuración: no lee tuplas ni recibe partición.
2286
+ */
2287
+ denyPhase?(object: {
2288
+ type: string;
2289
+ }, relation: string): Promise<'read' | 'write' | null>;
2290
+ /**
2291
+ * **El techo de anidación de grupos que ESTE driver aplica** (lote «antes de RC» · R1, decisión del dueño 2026-10-17
2292
+ * (5): «cada driver declara su techo»): saltos `group⊂group` del camino más largo que admite al escribir una arista
2293
+ * `group#member`, o `null` si no aplica ninguno. `openfga`: el CALIBRADO contra el servidor (el mínimo de un miembro y
2294
+ * de un no-miembro con membresía; solo lectura, tuplas contextuales, memoriza solo el éxito) o el `maxGroupDepth` de
2295
+ * la config si es menor; `database`: `maxGroupDepth` con `database.enforceGroupDepth`, si no `null`. Lo llama
2296
+ * `reconcileRelations` sobre el DESTINO en la fase de PLAN (también en `--dry-run`: es de solo lectura) para NEGAR la
2297
+ * partición cuya cadena final lo excede, nunca saltar la arista. Opcional: un driver de terceros sin él no tiene techo
2298
+ * para el plan. Lectura de configuración: no recibe tupla ni partición.
2299
+ */
2300
+ groupDepthLimit?(): Promise<number | null>;
2145
2301
  /**
2146
2302
  * Vista de este driver con OTRO reloj de pared (R-15, paridad con
2147
2303
  * `AuthorizationDriver.withClock`, 2.5 · J1): mismo backend, solo cambia el
@@ -2162,4 +2318,3 @@ export interface RelationsDriver {
2162
2318
  * factory (como el de roles), así que el comando nunca toca el SDK (pureza).
2163
2319
  */
2164
2320
  export type RelationsDriverFactory = () => RelationsDriver | Promise<RelationsDriver>;
2165
- //# sourceMappingURL=types.d.ts.map