@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
@@ -1,3 +1,4 @@
1
+ import { AuthorizationConfigError } from '../errors.js';
1
2
  import type { CatalogProjectionRole, HolderTypeMap } from '../types.js';
2
3
  /**
3
4
  * `holderTypes` tiene que ser INYECTIVO. Si dos morph names caen en el mismo
@@ -333,6 +334,12 @@ export interface RelationObjectType {
333
334
  type: string;
334
335
  /** Sus relaciones, en orden de declaración. */
335
336
  relations: readonly RelationObjectRelation[];
337
+ /**
338
+ * Opt-in del deny (plan 2.5 · lote D, D10): con `'read'` o `'write'` el generador emite, por cada relación `X`,
339
+ * `X = <la de hoy> but not denied_X` y `denied_X = this ∨ denied_P` (por cada `P` que incluye a `X`). Ausente ⇒
340
+ * el literal de 2.4.0-alpha.10, byte a byte (D-29).
341
+ */
342
+ deny?: 'read' | 'write';
336
343
  }
337
344
  /**
338
345
  * La config de relaciones que el generador FUSIONA en el modelo `facts`
@@ -381,4 +388,108 @@ export declare function assertRelationsConfigPublishable(permissionRelations: re
381
388
  * `(holders + 1) × (tipo + 'not_expired')` bytes por relación declarada.
382
389
  */
383
390
  export declare function factsRelationTypeDefinitions(permissionRelations: readonly FactsRelations[], config: FactsRelationsConfig, holderTypes: readonly string[]): any[];
384
- //# sourceMappingURL=openfga_facts.d.ts.map
391
+ /**
392
+ * **Las reglas de reescritura (`relations`) de UN tipo de objeto, tal como las genera el paquete** — la fuente ÚNICA
393
+ * del generador (`factsRelationTypeDefinitions`) y de la guarda D-31 del driver (cierre 2 de D1, ⚪ N4: la guarda
394
+ * compara el modelo FIJADO con ESTO, por igualdad estructural, no solo la forma). Pura y sin holders: el `metadata`
395
+ * (qué sujetos admite cada relación) no decide qué concede.
396
+ */
397
+ export declare function factsRelationRewrites(objectType: RelationObjectType): Record<string, unknown>;
398
+ /**
399
+ * **Forma canónica de una regla de reescritura** para compararla con la que devuelve el servidor (cierre 2 de D1,
400
+ * ⚪ N4). MEDIDO en v1.19.0: `ReadAuthorizationModel` devuelve las relaciones en otro orden de claves y cada
401
+ * `computedUserset` con `object: ""`; ninguna de las dos cosas cambia qué concede. Se ordenan las claves, se quita el
402
+ * `object` vacío y se ordenan los hijos de `union`/`intersection` (conmutativos; `difference` tiene `base`/`subtract`
403
+ * con nombre y no se toca). Cualquier otra diferencia es una regla DISTINTA.
404
+ */
405
+ export declare function canonicalRewrite(node: unknown): unknown;
406
+ /**
407
+ * Qué tipos compara cada puerta (L11): `roles` (los holders, `role`, `role_binding`, `scope`), `relations` (los holders,
408
+ * `group` y los tipos de la config) y `all` (el modelo ENTERO que generaría la config, más todo tipo que el servido
409
+ * traiga de más: la relectura de `openfga:provision`). Las condiciones y `schema_version` se comparan SIEMPRE.
410
+ */
411
+ export type FactsModelSlice = 'roles' | 'relations' | 'all';
412
+ /** Una diferencia del modelo servido respecto del generado, por RUTA legible. `type: null` = global (condiciones…). */
413
+ export interface FactsModelDifference {
414
+ type: string | null;
415
+ path: string;
416
+ }
417
+ export interface FactsModelComparison {
418
+ equal: boolean;
419
+ differences: FactsModelDifference[];
420
+ /**
421
+ * La forma con la que el modelo servido trae cada tipo de relaciones de la config: `deny` (con `denied_<rel>`) o
422
+ * `plain`. Con opt-in siempre `deny`; sin él, **D8 por TIPO ENTERO** (cierra P2: un modelo MEZCLADO, unas relaciones
423
+ * con la forma con deny y otras sin ella, no lo genera ninguna config y es diferencia). Lo usa `decideMany`.
424
+ */
425
+ denyForm: ReadonlyMap<string, 'plain' | 'deny'>;
426
+ /** P_M: los permisos que declara el propio modelo servido (las bases de `role#permits_*`), ordenados. */
427
+ permissions: string[];
428
+ }
429
+ /**
430
+ * **Forma canónica de un modelo (o de cualquier trozo) de OpenFGA** para compararlo con el que genera el paquete
431
+ * (L11; extiende `canonicalRewrite`). MEDIDO contra v1.19.0 (`:8090`): el servidor devuelve el ruido de proto3
432
+ * —`module: ""`, `source_info: null`, `condition: ""` en cada restricción, `metadata: null`, `generic_types: []`,
433
+ * `object: ""` en cada `computedUserset`— y otro orden de claves. Se quitan los valores por defecto de proto3 (`""`,
434
+ * `null`, `[]`), **NUNCA un `{}`** (`this: {}` y `wildcard: {}` significan algo), se ordenan las claves y los hijos
435
+ * CONMUTATIVOS (`child` de `union`/`intersection` y `directly_related_user_types`; `difference` tiene `base`/
436
+ * `subtract` con nombre y no se toca). Una restricción DUPLICADA no se funde: es una diferencia.
437
+ */
438
+ export declare function canonicalModel(node: unknown): unknown;
439
+ /**
440
+ * **El punto de paso ÚNICO de L11** (lote «antes de RC», decisión del dueño 2026-10-17 (1); linaje L11): compara el
441
+ * modelo que SIRVE el servidor con el que genera la config de este proceso, POR TIPO, en reglas, restricciones de
442
+ * tipo (con su condición) y condiciones. Pura y sin SDK. Determinista: no busca entre combinaciones.
443
+ *
444
+ * 1. `P_M` = los permisos que declara el propio servido (`role#permits_*`): la igualdad es «módulo el catálogo», que
445
+ * cambia en runtime (el desfase catálogo ↔ modelo lo cuenta el reconcile, `permission-not-in-model`).
446
+ * 2. `D8_M` = los tipos de la config SIN opt-in que el servido trae con la forma con deny (algún `denied_*`): se
447
+ * generan con `deny` para comparar — por TIPO ENTERO —.
448
+ * 3. Se genera `openFgaFactsModel(holderTypes, P_M, config con D8_M)`; si el generador lanza (un `permits_*` del
449
+ * servido que no es un nombre publicable), es 500 `E_AUTHZ_CONFIG`, nunca «igual» (H11 d).
450
+ * 4. Diferencias por ruta: `<tipo> (tipo ausente)`, `<tipo>#<rel> (falta)`, `(relación de más)` —una relación EXTRA
451
+ * es deriva, D-L11-3—, `(regla)`, `(restricciones de tipo)`, `<tipo>.<campo>`, `conditions.<nombre> (…)`,
452
+ * `schema_version`. Un tipo AJENO al `slice` no se mira (límite declarado y razonado, caso C7); con `all`, sí.
453
+ */
454
+ export declare function factsModelDifferences(served: unknown, inputs: {
455
+ holderTypes: HolderTypeMap;
456
+ relations?: FactsRelationsConfig;
457
+ slice: FactsModelSlice;
458
+ }): FactsModelComparison;
459
+ /** Cuántas rutas nombra como mucho la letra del 500 de L11 (el resto se cuenta). */
460
+ export declare const FACTS_MODEL_DRIFT_LISTED = 20;
461
+ /**
462
+ * **La LETRA del 500 de la puerta L11**, la MISMA en los dos drivers `openfga`: qué driver, qué operación, qué
463
+ * `modelId`, las diferencias por ruta y la receta. Cero llamadas que evalúen o escriban.
464
+ */
465
+ export declare function factsModelDriftError(driver: string, operation: string, modelId: string | undefined, differences: readonly FactsModelDifference[]): AuthorizationConfigError & {
466
+ modelDifferences: string[];
467
+ };
468
+ /**
469
+ * **El token del opt-in de `openfga` EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21, «(b) con
470
+ * candado»). Lleva la VERSIÓN a propósito (auditor B1/B7, tester E1): con `true` nada obligaría a releer los límites NUEVOS
471
+ * cuando cambien; con `'openfga@2.5'` la 2.6 cambiará el token y un `^2.5.0` que se actualice hace ruido (500) en vez de
472
+ * colarse. NO se exporta por `/openfga`: el consumidor escribe el LITERAL, que es lo que demuestra que leyó «Limits».
473
+ */
474
+ export declare const OPENFGA_EXPERIMENTAL_TOKEN = "openfga@2.5";
475
+ /**
476
+ * **La puerta del opt-in experimental, en los DOS constructores `openfga`** (firma 4; mismo patrón que
477
+ * `E_AUTHZ_SCOPE_DRIFT_UNGUARDED`: «una recomendación no es un mecanismo»). En 2.5.x `openfga` tiene límites declarados
478
+ * en README «Limits» (carreras entre escritores, árbol con outbox sin medir, el gemelo `permits_*`…) que se cierran en
479
+ * 2.6, así que construirlo exige escribir `experimental: 'openfga@2.5'`. Cualquier otra cosa —ausente, `true`,
480
+ * `'acknowledged'`, otra versión, el token con espacios— es 500 `E_AUTHZ_CONFIG` al CONSTRUIR, como PRIMERA línea del
481
+ * constructor: ni se valida lo demás ni se crea el cliente, así que hay CERO llamadas al servidor. Va en el constructor
482
+ * y no en el stub ni en el registro de la factory: `manager.driver()` (y con él `authz:catalog:sync`),
483
+ * `authz:reconcile --to=openfga`, `authz:relations:reconcile`, el provider y la construcción directa pasan todos por
484
+ * aquí, y un config solo-`database` que REGISTRA la factory `openfga` sin usarla arranca (las factories son perezosas).
485
+ * `openfga:provision` NO construye un driver (crea el store y publica el modelo con el SDK): corre sin la marca, y el
486
+ * primer driver que sirva con ese store, no.
487
+ */
488
+ export declare function assertExperimentalOptIn(driver: string, value: unknown): void;
489
+ /**
490
+ * **`modelId` OBLIGATORIO en los dos drivers `openfga`** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2);
491
+ * D14 ampliado: siempre, con o sin deny). Sin el pin la puerta L11 compara el modelo que el driver lee al arrancar y el
492
+ * servidor evalúa cada petición con el ÚLTIMO del store: un `openfga:provision` desde otro proceso cambiaría lo que se
493
+ * concede sin que ninguna comparación lo vea. 500 al CONSTRUIR, con la receta.
494
+ */
495
+ export declare function assertPinnedModel(driver: string, modelId: unknown): void;
@@ -1,6 +1,7 @@
1
- import { InvalidIdentityError, InvalidSlugError, ModelTooLargeError, AuthorizationConfigError, RelationConfigError, } from '../errors.js';
1
+ import { InvalidIdentityError, InvalidSlugError, ModelTooLargeError, AuthorizationConfigError, RelationConfigError, markDeploymentFailure, } from '../errors.js';
2
2
  import { slugAsRelation, RESERVED_SLUG_PREFIXES, RESERVED_FACTS_TYPES } from '../identity.js';
3
3
  import { APP_SCOPE_TYPE } from '../types.js';
4
+ import { FGA_EXPIRY_CONDITION } from '../expiry.js';
4
5
  /**
5
6
  * Modo `facts` — el MODELO (c2) y la proyección del catálogo, sin `@openfga/sdk`.
6
7
  *
@@ -202,7 +203,7 @@ export const FACTS_ROOTED_RELATION = 'rooted';
202
203
  * `role_binding#assignee` desde el modo `facts`, y desde R-15 también la de
203
204
  * cada sujeto de una relación ReBAC (`document#viewer`, `group#member`…).
204
205
  */
205
- export const FACTS_EXPIRY_CONDITION = 'not_expired';
206
+ export const FACTS_EXPIRY_CONDITION = FGA_EXPIRY_CONDITION;
206
207
  /* ── Relaciones derivadas de un permiso ─────────────────────────────────── */
207
208
  /**
208
209
  * Relaciones propias del modelo (no derivadas de ningún permiso). Un permiso
@@ -751,6 +752,16 @@ export function assertRelationsConfigPublishable(permissionRelations, config, ho
751
752
  own.add(name);
752
753
  relationNames.add(name);
753
754
  }
755
+ // D1 (lote D): en un tipo con deny, `denied_<X>` tiene que caber en los 50 de FGA (defensa en profundidad de la
756
+ // cota de 43 que ya aplica `defineRelationsConfig`).
757
+ if (objectType.deny !== undefined) {
758
+ for (const name of own) {
759
+ if (FACTS_DENIED_PREFIX.length + name.length > FGA_MAX_RELATION_NAME) {
760
+ throw new RelationConfigError(`La relación '${type}#${name}' tiene ${name.length} caracteres y el tipo declara deny: '${FACTS_DENIED_PREFIX}${name}' ` +
761
+ `no cabe en los ${FGA_MAX_RELATION_NAME} de FGA (una relación denegable mide como mucho ${FGA_MAX_RELATION_NAME - FACTS_DENIED_PREFIX.length}).`);
762
+ }
763
+ }
764
+ }
754
765
  // Los `includes` refieren relaciones del MISMO tipo (un nivel, sin `from`).
755
766
  for (const relation of objectType.relations ?? []) {
756
767
  for (const included of relation.includes ?? []) {
@@ -820,17 +831,346 @@ export function factsRelationTypeDefinitions(permissionRelations, config, holder
820
831
  },
821
832
  ];
822
833
  for (const objectType of config.objectTypes) {
823
- const relations = {};
834
+ const relations = factsRelationRewrites(objectType);
835
+ // `<rel>` y `denied_<rel>` admiten los MISMOS sujetos (holders y `group#member`, con y sin `not_expired`).
824
836
  const metadata = {};
825
- for (const relation of objectType.relations) {
826
- const includes = relation.includes ?? [];
827
- relations[relation.name] = includes.length
828
- ? { union: { child: [{ this: {} }, ...includes.map((name) => computed(name))] } }
829
- : { this: {} };
830
- metadata[relation.name] = { directly_related_user_types: holdersOrGroup };
831
- }
837
+ for (const name of Object.keys(relations))
838
+ metadata[name] = { directly_related_user_types: holdersOrGroup };
832
839
  definitions.push({ type: objectType.type, relations, metadata: { relations: metadata } });
833
840
  }
834
841
  return definitions;
835
842
  }
836
- //# sourceMappingURL=openfga_facts.js.map
843
+ /**
844
+ * **Las reglas de reescritura (`relations`) de UN tipo de objeto, tal como las genera el paquete** — la fuente ÚNICA
845
+ * del generador (`factsRelationTypeDefinitions`) y de la guarda D-31 del driver (cierre 2 de D1, ⚪ N4: la guarda
846
+ * compara el modelo FIJADO con ESTO, por igualdad estructural, no solo la forma). Pura y sin holders: el `metadata`
847
+ * (qué sujetos admite cada relación) no decide qué concede.
848
+ */
849
+ export function factsRelationRewrites(objectType) {
850
+ const relations = {};
851
+ for (const relation of objectType.relations) {
852
+ const includes = relation.includes ?? [];
853
+ const base = includes.length
854
+ ? { union: { child: [{ this: {} }, ...includes.map((name) => computed(name))] } }
855
+ : { this: {} };
856
+ if (objectType.deny === undefined) {
857
+ // D-29: sin opt-in, el literal de 2.4.0-alpha.10 byte a byte.
858
+ relations[relation.name] = base;
859
+ continue;
860
+ }
861
+ // **D5 · corte de camino en el MODELO** (lote D, medido en ronda 1 y 2 contra v1.19.0): `X = base but not
862
+ // denied_X` y `denied_X = this ∨ denied_P` por cada `P` que INCLUYE a `X` (el cierre ASCENDENTE): un deny en
863
+ // `editor` alcanza a `denied_owner`, así que el grant de `owner` deja de valer entero —y con él lo que deriva—.
864
+ // `denied_X` admite los MISMOS sujetos que `X` (holders y `group#member`, con y sin `not_expired`): el deny de
865
+ // relaciones no caduca (D3), pero un deny puede viajar por una membresía de grupo que sí caduca (M6, caso C).
866
+ const denied = `${FACTS_DENIED_PREFIX}${relation.name}`;
867
+ const includers = objectType.relations.filter((other) => (other.includes ?? []).includes(relation.name));
868
+ relations[relation.name] = { difference: { base, subtract: computed(denied) } };
869
+ relations[denied] = includers.length
870
+ ? { union: { child: [{ this: {} }, ...includers.map((other) => computed(`${FACTS_DENIED_PREFIX}${other.name}`))] } }
871
+ : { this: {} };
872
+ }
873
+ return relations;
874
+ }
875
+ /**
876
+ * **Forma canónica de una regla de reescritura** para compararla con la que devuelve el servidor (cierre 2 de D1,
877
+ * ⚪ N4). MEDIDO en v1.19.0: `ReadAuthorizationModel` devuelve las relaciones en otro orden de claves y cada
878
+ * `computedUserset` con `object: ""`; ninguna de las dos cosas cambia qué concede. Se ordenan las claves, se quita el
879
+ * `object` vacío y se ordenan los hijos de `union`/`intersection` (conmutativos; `difference` tiene `base`/`subtract`
880
+ * con nombre y no se toca). Cualquier otra diferencia es una regla DISTINTA.
881
+ */
882
+ export function canonicalRewrite(node) {
883
+ if (Array.isArray(node))
884
+ return node.map(canonicalRewrite);
885
+ if (node === null || typeof node !== 'object')
886
+ return node;
887
+ const out = {};
888
+ for (const key of Object.keys(node).sort()) {
889
+ const value = node[key];
890
+ if (value === undefined || (key === 'object' && value === ''))
891
+ continue;
892
+ out[key] = canonicalRewrite(value);
893
+ }
894
+ if (Array.isArray(out.child)) {
895
+ out.child = [...out.child].sort((a, b) => {
896
+ const x = JSON.stringify(a);
897
+ const y = JSON.stringify(b);
898
+ return x < y ? -1 : x > y ? 1 : 0;
899
+ });
900
+ }
901
+ return out;
902
+ }
903
+ /**
904
+ * **Forma canónica de un modelo (o de cualquier trozo) de OpenFGA** para compararlo con el que genera el paquete
905
+ * (L11; extiende `canonicalRewrite`). MEDIDO contra v1.19.0 (`:8090`): el servidor devuelve el ruido de proto3
906
+ * —`module: ""`, `source_info: null`, `condition: ""` en cada restricción, `metadata: null`, `generic_types: []`,
907
+ * `object: ""` en cada `computedUserset`— y otro orden de claves. Se quitan los valores por defecto de proto3 (`""`,
908
+ * `null`, `[]`), **NUNCA un `{}`** (`this: {}` y `wildcard: {}` significan algo), se ordenan las claves y los hijos
909
+ * CONMUTATIVOS (`child` de `union`/`intersection` y `directly_related_user_types`; `difference` tiene `base`/
910
+ * `subtract` con nombre y no se toca). Una restricción DUPLICADA no se funde: es una diferencia.
911
+ */
912
+ export function canonicalModel(node) {
913
+ if (Array.isArray(node))
914
+ return node.map(canonicalModel);
915
+ if (node === null || typeof node !== 'object')
916
+ return node;
917
+ // Cierre de L11 (⚪ A3.2 del auditor): SIN prototipo — una relación o condición de más llamada `__proto__` (el servidor
918
+ // v1.19 la acepta, medido) cambiaba el prototipo de un objeto literal en vez de ser una clave, y el comparador decía
919
+ // «igual» (D-L11-3: relación de más = deriva). Con `Object.create(null)` es una clave propia más y se ve.
920
+ const out = Object.create(null);
921
+ for (const key of Object.keys(node).sort()) {
922
+ const value = node[key];
923
+ if (value === undefined || value === null || value === '')
924
+ continue;
925
+ if (Array.isArray(value) && value.length === 0)
926
+ continue;
927
+ out[key] = canonicalModel(value);
928
+ }
929
+ for (const key of ['child', 'directly_related_user_types']) {
930
+ if (Array.isArray(out[key]))
931
+ out[key] = sortByJson(out[key]);
932
+ }
933
+ return out;
934
+ }
935
+ function sortByJson(items) {
936
+ return [...items].sort((a, b) => {
937
+ const x = JSON.stringify(a);
938
+ const y = JSON.stringify(b);
939
+ return x < y ? -1 : x > y ? 1 : 0;
940
+ });
941
+ }
942
+ const sameCanon = (a, b) => JSON.stringify(a) === JSON.stringify(b);
943
+ /**
944
+ * **El punto de paso ÚNICO de L11** (lote «antes de RC», decisión del dueño 2026-10-17 (1); linaje L11): compara el
945
+ * modelo que SIRVE el servidor con el que genera la config de este proceso, POR TIPO, en reglas, restricciones de
946
+ * tipo (con su condición) y condiciones. Pura y sin SDK. Determinista: no busca entre combinaciones.
947
+ *
948
+ * 1. `P_M` = los permisos que declara el propio servido (`role#permits_*`): la igualdad es «módulo el catálogo», que
949
+ * cambia en runtime (el desfase catálogo ↔ modelo lo cuenta el reconcile, `permission-not-in-model`).
950
+ * 2. `D8_M` = los tipos de la config SIN opt-in que el servido trae con la forma con deny (algún `denied_*`): se
951
+ * generan con `deny` para comparar — por TIPO ENTERO —.
952
+ * 3. Se genera `openFgaFactsModel(holderTypes, P_M, config con D8_M)`; si el generador lanza (un `permits_*` del
953
+ * servido que no es un nombre publicable), es 500 `E_AUTHZ_CONFIG`, nunca «igual» (H11 d).
954
+ * 4. Diferencias por ruta: `<tipo> (tipo ausente)`, `<tipo>#<rel> (falta)`, `(relación de más)` —una relación EXTRA
955
+ * es deriva, D-L11-3—, `(regla)`, `(restricciones de tipo)`, `<tipo>.<campo>`, `conditions.<nombre> (…)`,
956
+ * `schema_version`. Un tipo AJENO al `slice` no se mira (límite declarado y razonado, caso C7); con `all`, sí.
957
+ */
958
+ export function factsModelDifferences(served, inputs) {
959
+ const model = served;
960
+ if (!model || typeof model !== 'object' || !Array.isArray(model.type_definitions)) {
961
+ throw new AuthorizationConfigError('L11: el modelo servido no trae type_definitions; no se puede comparar con el que genera la config (no se toma por igual).');
962
+ }
963
+ const differences = [];
964
+ const servedTypes = new Map();
965
+ for (const definition of model.type_definitions) {
966
+ const type = definition?.type;
967
+ if (typeof type !== 'string' || type === '') {
968
+ differences.push({ type: null, path: 'type_definitions (un tipo sin nombre)' });
969
+ continue;
970
+ }
971
+ if (servedTypes.has(type))
972
+ differences.push({ type, path: `${type} (tipo duplicado)` });
973
+ servedTypes.set(type, definition);
974
+ }
975
+ const permissions = inputs.slice === 'relations'
976
+ ? []
977
+ : Object.keys(servedTypes.get(FACTS_ROLE_TYPE)?.relations ?? {})
978
+ .filter((name) => name.startsWith(FACTS_PERMITS_PREFIX))
979
+ .map((name) => name.slice(FACTS_PERMITS_PREFIX.length))
980
+ .sort();
981
+ const denyForm = new Map();
982
+ let relationsConfig;
983
+ if (inputs.relations && inputs.slice !== 'roles') {
984
+ relationsConfig = {
985
+ objectTypes: inputs.relations.objectTypes.map((spec) => {
986
+ if (spec.deny !== undefined) {
987
+ denyForm.set(spec.type, 'deny');
988
+ return spec;
989
+ }
990
+ const d8 = Object.keys(servedTypes.get(spec.type)?.relations ?? {}).some((name) => name.startsWith(FACTS_DENIED_PREFIX));
991
+ denyForm.set(spec.type, d8 ? 'deny' : 'plain');
992
+ return d8 ? { ...spec, deny: 'read' } : spec;
993
+ }),
994
+ };
995
+ }
996
+ let generated;
997
+ try {
998
+ generated = openFgaFactsModel(inputs.holderTypes, permissions, relationsConfig);
999
+ }
1000
+ catch (error) {
1001
+ throw markDeploymentFailure(new AuthorizationConfigError(`L11: no se puede generar el modelo de la config con los permisos que declara el modelo servido ` +
1002
+ `(${error?.message ?? String(error)}); no se toma por igual.`, { cause: error }));
1003
+ }
1004
+ const generatedTypes = new Map(generated.type_definitions.map((definition) => [definition.type, definition]));
1005
+ const holders = Object.values(inputs.holderTypes);
1006
+ const sliceTypes = inputs.slice === 'all'
1007
+ ? [...generatedTypes.keys()]
1008
+ : inputs.slice === 'roles'
1009
+ ? [...holders, FACTS_ROLE_TYPE, FACTS_BINDING_TYPE, FACTS_SCOPE_TYPE]
1010
+ : [...holders, FACTS_GROUP_TYPE, ...(inputs.relations?.objectTypes ?? []).map((t) => t.type)];
1011
+ for (const type of sliceTypes)
1012
+ compareTypeDefinition(type, servedTypes.get(type), generatedTypes.get(type), differences);
1013
+ if (inputs.slice === 'all') {
1014
+ for (const type of servedTypes.keys()) {
1015
+ if (!generatedTypes.has(type))
1016
+ differences.push({ type, path: `${type} (tipo de más)` });
1017
+ }
1018
+ }
1019
+ const servedConditions = (canonicalModel(model.conditions ?? {}) ?? {});
1020
+ const generatedConditions = canonicalModel(generated.conditions);
1021
+ for (const name of [...new Set([...Object.keys(servedConditions), ...Object.keys(generatedConditions)])].sort()) {
1022
+ if (!(name in servedConditions))
1023
+ differences.push({ type: null, path: `conditions.${name} (falta)` });
1024
+ else if (!(name in generatedConditions))
1025
+ differences.push({ type: null, path: `conditions.${name} (condición de más)` });
1026
+ else if (!sameCanon(servedConditions[name], generatedConditions[name]))
1027
+ differences.push({ type: null, path: `conditions.${name} (distinta)` });
1028
+ }
1029
+ if (!sameCanon(canonicalModel(model.schema_version), generated.schema_version)) {
1030
+ differences.push({ type: null, path: `schema_version (${JSON.stringify(model.schema_version ?? null)})` });
1031
+ }
1032
+ for (const key of Object.keys(model).sort()) {
1033
+ if (['id', 'schema_version', 'type_definitions', 'conditions'].includes(key))
1034
+ continue;
1035
+ const value = model[key];
1036
+ if (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0))
1037
+ continue;
1038
+ differences.push({ type: null, path: `modelo.${key}` });
1039
+ }
1040
+ return { equal: differences.length === 0, differences, denyForm, permissions };
1041
+ }
1042
+ function compareTypeDefinition(type, served, generated, out) {
1043
+ if (generated === undefined)
1044
+ return;
1045
+ if (served === undefined) {
1046
+ out.push({ type, path: `${type} (tipo ausente del modelo)` });
1047
+ return;
1048
+ }
1049
+ const s = canonicalModel(served);
1050
+ const g = canonicalModel(generated);
1051
+ const sRelations = s.relations ?? {};
1052
+ const gRelations = g.relations ?? {};
1053
+ const reported = new Set();
1054
+ for (const name of [...new Set([...Object.keys(sRelations), ...Object.keys(gRelations)])].sort()) {
1055
+ if (!(name in sRelations))
1056
+ out.push({ type, path: `${type}#${name} (falta)` });
1057
+ else if (!(name in gRelations))
1058
+ out.push({ type, path: `${type}#${name} (relación de más)` });
1059
+ else if (!sameCanon(sRelations[name], gRelations[name]))
1060
+ out.push({ type, path: `${type}#${name} (regla)` });
1061
+ else
1062
+ continue;
1063
+ reported.add(name);
1064
+ }
1065
+ const sMeta = s.metadata?.relations ?? {};
1066
+ const gMeta = g.metadata?.relations ?? {};
1067
+ for (const name of [...new Set([...Object.keys(sMeta), ...Object.keys(gMeta)])].sort()) {
1068
+ if (reported.has(name))
1069
+ continue;
1070
+ if (!sameCanon(sMeta[name], gMeta[name]))
1071
+ out.push({ type, path: `${type}#${name} (restricciones de tipo)` });
1072
+ }
1073
+ const rest = (definition) => {
1074
+ const { type: _t, relations: _r, metadata, ...others } = definition;
1075
+ const { relations: _mr, ...metaOthers } = (metadata ?? {});
1076
+ return { others, metaOthers };
1077
+ };
1078
+ const sr = rest(s);
1079
+ const gr = rest(g);
1080
+ for (const key of [...new Set([...Object.keys(sr.others), ...Object.keys(gr.others)])].sort()) {
1081
+ if (!sameCanon(sr.others[key], gr.others[key]))
1082
+ out.push({ type, path: `${type}.${key}` });
1083
+ }
1084
+ for (const key of [...new Set([...Object.keys(sr.metaOthers), ...Object.keys(gr.metaOthers)])].sort()) {
1085
+ if (!sameCanon(sr.metaOthers[key], gr.metaOthers[key]))
1086
+ out.push({ type, path: `${type}.metadata.${key}` });
1087
+ }
1088
+ }
1089
+ /** Cuántas rutas nombra como mucho la letra del 500 de L11 (el resto se cuenta). */
1090
+ export const FACTS_MODEL_DRIFT_LISTED = 20;
1091
+ /**
1092
+ * **La LETRA del 500 de la puerta L11**, la MISMA en los dos drivers `openfga`: qué driver, qué operación, qué
1093
+ * `modelId`, las diferencias por ruta y la receta. Cero llamadas que evalúen o escriban.
1094
+ */
1095
+ export function factsModelDriftError(driver, operation, modelId, differences) {
1096
+ const paths = differences.map((d) => d.path);
1097
+ const listed = paths.slice(0, FACTS_MODEL_DRIFT_LISTED).join('; ');
1098
+ const more = paths.length > FACTS_MODEL_DRIFT_LISTED ? ` … y ${paths.length - FACTS_MODEL_DRIFT_LISTED} más` : '';
1099
+ const error = new AuthorizationConfigError(`${driver}.${operation}: el modelo que sirve este driver (modelId ${modelId ?? '(ninguno)'}) NO es el que genera la ` +
1100
+ `config de este proceso (L11: reglas, restricciones de tipo y condiciones, por tipo) — ${listed}${more}. Con ese ` +
1101
+ 'modelo el servidor decide con reglas o condiciones que la config no declara: puede conceder lo que la config —y ' +
1102
+ 'database con la misma config— niega, o leer caducidades con otra condición. Republica el modelo desde la config ' +
1103
+ 'actual (node ace openfga:provision --store-id=…) y fija el modelId nuevo (OPENFGA_MODEL_ID) en TODA la flota antes ' +
1104
+ 'de desplegar. No se ha llamado al servidor para evaluar ni escribir nada.');
1105
+ error.modelDifferences = paths;
1106
+ // Cierre de L11 (🟠 A1): fallo del DESPLIEGUE ⇒ transitorio para el relay (no consume intento ni aparca).
1107
+ return markDeploymentFailure(error);
1108
+ }
1109
+ /**
1110
+ * **El token del opt-in de `openfga` EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21, «(b) con
1111
+ * candado»). Lleva la VERSIÓN a propósito (auditor B1/B7, tester E1): con `true` nada obligaría a releer los límites NUEVOS
1112
+ * cuando cambien; con `'openfga@2.5'` la 2.6 cambiará el token y un `^2.5.0` que se actualice hace ruido (500) en vez de
1113
+ * colarse. NO se exporta por `/openfga`: el consumidor escribe el LITERAL, que es lo que demuestra que leyó «Limits».
1114
+ */
1115
+ export const OPENFGA_EXPERIMENTAL_TOKEN = 'openfga@2.5';
1116
+ /**
1117
+ * **La puerta del opt-in experimental, en los DOS constructores `openfga`** (firma 4; mismo patrón que
1118
+ * `E_AUTHZ_SCOPE_DRIFT_UNGUARDED`: «una recomendación no es un mecanismo»). En 2.5.x `openfga` tiene límites declarados
1119
+ * en README «Limits» (carreras entre escritores, árbol con outbox sin medir, el gemelo `permits_*`…) que se cierran en
1120
+ * 2.6, así que construirlo exige escribir `experimental: 'openfga@2.5'`. Cualquier otra cosa —ausente, `true`,
1121
+ * `'acknowledged'`, otra versión, el token con espacios— es 500 `E_AUTHZ_CONFIG` al CONSTRUIR, como PRIMERA línea del
1122
+ * constructor: ni se valida lo demás ni se crea el cliente, así que hay CERO llamadas al servidor. Va en el constructor
1123
+ * y no en el stub ni en el registro de la factory: `manager.driver()` (y con él `authz:catalog:sync`),
1124
+ * `authz:reconcile --to=openfga`, `authz:relations:reconcile`, el provider y la construcción directa pasan todos por
1125
+ * aquí, y un config solo-`database` que REGISTRA la factory `openfga` sin usarla arranca (las factories son perezosas).
1126
+ * `openfga:provision` NO construye un driver (crea el store y publica el modelo con el SDK): corre sin la marca, y el
1127
+ * primer driver que sirva con ese store, no.
1128
+ */
1129
+ export function assertExperimentalOptIn(driver, value) {
1130
+ if (typeof value === 'string' && value === OPENFGA_EXPERIMENTAL_TOKEN)
1131
+ return;
1132
+ throw new AuthorizationConfigError(experimentalOptInMessage(driver, describeOptInValue(value)));
1133
+ }
1134
+ /**
1135
+ * Qué llegó, SIN tocar el valor (cierre 1 del candado, ⚪ 5 del auditor): `String(value)` de un `Object.create(null)` o de
1136
+ * un objeto cuyo `toString` lanza sustituía el 500 por un `TypeError` o por el error del consumidor. Solo `typeof` y, para
1137
+ * un string, `JSON.stringify` (que no invoca nada del valor).
1138
+ */
1139
+ function describeOptInValue(value) {
1140
+ if (value === undefined)
1141
+ return 'nada';
1142
+ if (value === null)
1143
+ return 'null';
1144
+ if (typeof value === 'string')
1145
+ return JSON.stringify(value);
1146
+ if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'bigint')
1147
+ return `${typeof value} ${value}`;
1148
+ return `un ${Array.isArray(value) ? 'array' : typeof value}`;
1149
+ }
1150
+ function experimentalOptInMessage(driver, got) {
1151
+ return (`${driver}: el driver openfga es EXPERIMENTAL en 2.5.x y NO es apto para producción: tiene límites declarados ` +
1152
+ '(README «Limits»: carreras entre escritores del driver de roles, el árbol con outbox sin medir, el gemelo ' +
1153
+ 'permits_*…) que se cierran en 2.6. Para usarlo igualmente (CI, ensayo, desarrollo) opta de forma EXPLÍCITA con ' +
1154
+ `experimental: '${OPENFGA_EXPERIMENTAL_TOKEN}' en las opciones del driver (llegó ${got}). El token lleva la versión ` +
1155
+ 'a propósito: true u otra versión no valen; el token y el comportamiento experimental de openfga pueden cambiar en ' +
1156
+ '2.6 (una minor) sin major. ' +
1157
+ 'En producción usa database; para MIGRAR a database desde openfga (authz:reconcile --to=database, con openfga como ' +
1158
+ 'ORIGEN) la factory openfga también necesita la marca: leer el store para salir de él es construir este driver. ' +
1159
+ 'No se ha creado el cliente ni se ha llamado al servidor.');
1160
+ }
1161
+ /**
1162
+ * **`modelId` OBLIGATORIO en los dos drivers `openfga`** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2);
1163
+ * D14 ampliado: siempre, con o sin deny). Sin el pin la puerta L11 compara el modelo que el driver lee al arrancar y el
1164
+ * servidor evalúa cada petición con el ÚLTIMO del store: un `openfga:provision` desde otro proceso cambiaría lo que se
1165
+ * concede sin que ninguna comparación lo vea. 500 al CONSTRUIR, con la receta.
1166
+ */
1167
+ export function assertPinnedModel(driver, modelId) {
1168
+ if (typeof modelId === 'string' && modelId.trim() !== '')
1169
+ return;
1170
+ throw new AuthorizationConfigError(`${driver}: falta modelId (llegó ${JSON.stringify(modelId ?? null)}). Desde 2.5.0-alpha.4 el modelo FIJADO es ` +
1171
+ 'obligatorio en los dos drivers openfga, con o sin deny (L11): el driver comprueba UNA vez que ese modelo es el que ' +
1172
+ 'genera tu config y el servidor evalúa con ESE modelo; sin el pin evaluaría con el último del store. Receta: ' +
1173
+ '`node ace openfga:provision` (o `--store-id=$OPENFGA_STORE_ID` sobre un store existente) imprime OPENFGA_MODEL_ID; ' +
1174
+ 'ponlo en el .env de TODA la flota y pásalo como modelId (el stub de config/authorization.ts lo hace con ' +
1175
+ "env.get('OPENFGA_MODEL_ID')).");
1176
+ }