@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
@@ -105,7 +105,7 @@ export default class OpenFgaProvision extends BaseCommand {
105
105
  }
106
106
  const { OpenFgaClient } = await import('@openfga/sdk');
107
107
  // El subpath `/openfga` es la única puerta al SDK (D9).
108
- const { openFgaFactsModel, provisionOpenFgaStore, assertFactsModelPublishable } = await import('../src/openfga.js');
108
+ const { openFgaFactsModel, provisionOpenFgaStore, assertFactsModelPublishable, factsModelDifferences } = await import('../src/openfga.js');
109
109
  // Los holders del consumidor viven en SU config, no en el paquete.
110
110
  const holderTypes = config?.holderTypes ?? {};
111
111
  // Los PERMISOS del modelo (c2r) salen de los catálogos del config. Se
@@ -130,12 +130,26 @@ export default class OpenFgaProvision extends BaseCommand {
130
130
  // esto salía como error crudo del SDK con una request ya hecha (⚪ N1
131
131
  // del auditor de alpha.4). Cero requests si no es publicable.
132
132
  assertFactsModelPublishable(holderTypes, permissions, undefined, relations);
133
- const client = new OpenFgaClient({ apiUrl, storeId: this.storeId });
134
- const model = await client.writeAuthorizationModel(openFgaFactsModel(holderTypes, permissions, relations));
133
+ // Injerto 15 del juez (H12): sin reintentos a escondidas (desde el SDK 0.9.6 un cliente sin `retryParams` reintenta).
134
+ const client = new OpenFgaClient({ apiUrl, storeId: this.storeId, retryParams: { maxRetry: 0 } });
135
+ const next = openFgaFactsModel(holderTypes, permissions, relations);
136
+ // Lote D · D2: la guarda contra la MUTILACIÓN del modelo (R2-4 c / H3) — ANTES de escribir nada. Cierre 1 de D2:
137
+ // juzga el ESTADO del store (🟡 3) y devuelve las relaciones que hay que RE-CONTAR justo antes de publicar (🟠 2).
138
+ const guard = await this.guardDroppedDenies(client, next);
139
+ if (guard === null)
140
+ return;
141
+ // Cierre 1 de D2 · 🟠 2 (TOCTOU): el barrido es O(store) y la flota puede seguir escribiendo denies mientras dura.
142
+ // Se vuelve a contar JUSTO antes de `writeAuthorizationModel`; un deny nuevo ⇒ exit 1 y CERO modelos. Estrecha la
143
+ // ventana, no la cierra: la garantía es el paso 0 del procedimiento (la flota en deny: 'read' antes de contar).
144
+ if (guard.recount.length > 0 && !(await this.recountBeforePublish(client, guard.recount)))
145
+ return;
146
+ const model = await client.writeAuthorizationModel(next);
135
147
  this.logger.success('Nuevo authorization model escrito en el store existente.');
136
148
  if (relations)
137
149
  this.logger.info(`Tipos de relación fusionados: ${relations.objectTypes.map((t) => t.type).join(', ')}`);
138
150
  this.logger.log(`OPENFGA_STORE_ID=${this.storeId}`);
151
+ if (!(await this.reread(OpenFgaClient, factsModelDifferences, apiUrl, this.storeId, model.authorization_model_id, holderTypes, relations)))
152
+ return;
139
153
  this.logger.log(`OPENFGA_MODEL_ID=${model.authorization_model_id}`);
140
154
  return;
141
155
  }
@@ -147,8 +161,140 @@ export default class OpenFgaProvision extends BaseCommand {
147
161
  this.logger.log('Añade al .env (junto a AUTHZ_DRIVER=openfga):');
148
162
  this.logger.log(`OPENFGA_URL=${apiUrl}`);
149
163
  this.logger.log(`OPENFGA_STORE_ID=${storeId}`);
164
+ if (!(await this.reread(OpenFgaClient, factsModelDifferences, apiUrl, storeId, modelId, holderTypes, relations)))
165
+ return;
150
166
  this.logger.log(`OPENFGA_MODEL_ID=${modelId}`);
151
167
  }
168
+ /**
169
+ * **La RELECTURA de lo publicado** (lote «antes de RC» · L11, diseño 1.3 D del analista; condición de aceptación C12 del
170
+ * tester): tras `writeAuthorizationModel` se LEE el modelo por su id y se compara ENTERO (`slice: 'all'`) con el que
171
+ * genera la config. Ancla el canonicalizador de L11 contra el servidor REAL en cada publicación: un servidor que
172
+ * serialice distinto (otra versión, un campo nuevo) se descubre en el despliegue, no con un 500 en la primera petición
173
+ * de producción. Si la relectura falla o difiere: exit 1, prefijo ESTABLE «provision RELECTURA FALLIDA:» (punto 7) y la
174
+ * letra que no miente (H11 c): el modelo YA está publicado en el store, y NO hay que fijarlo — por eso la línea
175
+ * `OPENFGA_MODEL_ID=` NO se imprime. Devuelve si se puede fijar.
176
+ */
177
+ async reread(OpenFgaClient, factsModelDifferences, apiUrl, storeId, modelId, holderTypes, relations) {
178
+ let differences;
179
+ try {
180
+ const client = new OpenFgaClient({ apiUrl, storeId, authorizationModelId: modelId, retryParams: { maxRetry: 0 } });
181
+ const response = await client.readAuthorizationModel();
182
+ differences = factsModelDifferences(response?.authorization_model, { holderTypes, relations, slice: 'all' }).differences.map((d) => d.path);
183
+ }
184
+ catch (error) {
185
+ this.logger.error(`provision RELECTURA FALLIDA: el modelo ${modelId} SE PUBLICÓ en el store ${storeId}, pero no se pudo releer y ` +
186
+ `comparar con el que genera la config (${String(error?.message ?? error)}). NO lo fijes (OPENFGA_MODEL_ID): los ` +
187
+ 'drivers lo comparan al arrancar y, sin esta comprobación, podrían negarse a servir. Repite el comando cuando el ' +
188
+ 'servidor responda.');
189
+ this.exitCode = 1;
190
+ return false;
191
+ }
192
+ if (differences.length === 0) {
193
+ // Punto 7 de CLAUDE.md (cierre de L11, 🟡-1 del tester): la rama de éxito también dice que corrió, con prefijo ESTABLE.
194
+ this.logger.success(`provision RELECTURA OK: el modelo ${modelId} releído del store ${storeId} es IGUAL (entero) al que genera la config; ya se puede fijar.`);
195
+ return true;
196
+ }
197
+ this.logger.error(`provision RELECTURA FALLIDA: el modelo ${modelId} SE PUBLICÓ en el store ${storeId}, pero lo que el servidor devuelve ` +
198
+ `NO es igual al que genera la config (L11): ${differences.slice(0, 20).join('; ')}${differences.length > 20 ? ' …' : ''}. ` +
199
+ 'NO lo fijes (OPENFGA_MODEL_ID): los drivers openfga lo compararían igual al arrancar y responderían 500 E_AUTHZ_CONFIG. ' +
200
+ 'Suele ser un servidor de otra versión que serializa el modelo distinto: compruébalo (CI prueba OpenFGA v1.19.0) y repórtalo.');
201
+ this.exitCode = 1;
202
+ return false;
203
+ }
204
+ /**
205
+ * **La guarda contra la MUTILACIÓN del modelo** (plan 2.5 · lote D · D2; analista §4.6, juez ronda 2 · R2-4 c y H3;
206
+ * cierre 1 de D2 · 🟡 3 y 🟠 2). `openfga:provision --store-id` publica desde la config del PROCESO: si ese proceso ya
207
+ * no declara el opt-in de un tipo (un refactor, otra rama del config, un binario viejo), el modelo nuevo no trae
208
+ * `denied_<rel>` y TODOS los denies de ese tipo dejan de bloquear en cuanto un pod lo resuelve — fail-open en silencio.
209
+ *
210
+ * - **El ESTADO del store manda** (🟡 3): se leen TODOS los modelos del store (`readAuthorizationModels`, paginado y
211
+ * acotado por `PROVISION_MODEL_HISTORY_MAX`) y se barre por las `<tipo>#denied_<rel>` que ALGÚN modelo declaró y el
212
+ * nuevo no (una tupla `denied_*` solo puede existir si un modelo la declaró al escribirla). Un deny VIVO de esas
213
+ * relaciones ⇒ exit 1 con o sin la bandera. Hasta el cierre se comparaba solo con el ÚLTIMO modelo: un modelo
214
+ * intermedio sin `denied_*` (un ensayo de rollback con alpha.10) desactivaba la guarda.
215
+ * - **La INTENCIÓN la dice la bandera**: si el nuevo quita respecto del ÚLTIMO modelo, sin `--allow-drop-deny` es exit
216
+ * 1 aunque no quede ningún deny (retirar un opt-in se pide en voz alta).
217
+ * - Devuelve las relaciones a RE-CONTAR antes de publicar (🟠 2), o `null` si negó. Todas las ramas fallidas empiezan
218
+ * por el prefijo ESTABLE «provision NEGADO:» (punto 7), también un barrido que falla a medias (⚪ 10 c).
219
+ */
220
+ async guardDroppedDenies(client, next) {
221
+ let models;
222
+ try {
223
+ models = await readModelHistory(client);
224
+ }
225
+ catch (error) {
226
+ this.logger.error(`provision NEGADO: no se pudo leer el modelo ACTUAL del store ${this.storeId} ni su historia de modelos ` +
227
+ `(${String(error?.message ?? error)}). Sin ella no se puede comprobar si el modelo nuevo apaga denies vivos; no ` +
228
+ 'se publica a ciegas. No se ha escrito nada.');
229
+ this.exitCode = 1;
230
+ return null;
231
+ }
232
+ const droppedLatest = droppedDenyRelations(models[0], next);
233
+ const droppedEver = droppedDenyRelations({ type_definitions: models.flatMap((m) => m?.type_definitions ?? []) }, next);
234
+ if (droppedLatest.length > 0 && this.allowDropDeny !== true) {
235
+ this.logger.error(`provision NEGADO: el modelo nuevo QUITA ${droppedLatest.join(', ')}, que el modelo actual del store declara. Publicarlo apagaría ` +
236
+ 'todos los denies de esas relaciones en cuanto un pod lo resuelva (fail-open en silencio). Suele ser un proceso ' +
237
+ "con la config sin el opt-in (deny: 'read'/'write') o con un paquete < 2.5.0-alpha.3: publica desde la versión y " +
238
+ "la config actuales. Si de verdad quieres retirar el opt-in: pasa ANTES la flota entera a deny: 'read', retira esos " +
239
+ 'denies (removeDeny) y repite con --allow-drop-deny (solo publica si el store no tiene NI UN deny vivo de ellas). ' +
240
+ 'No se ha escrito nada.');
241
+ this.exitCode = 1;
242
+ return null;
243
+ }
244
+ if (droppedEver.length === 0)
245
+ return { recount: [] };
246
+ const list = droppedEver.join(', ');
247
+ const sweep = await this.sweep(client, droppedEver);
248
+ if (sweep === null)
249
+ return null;
250
+ if (sweep.count > 0) {
251
+ this.logger.error((droppedLatest.length > 0
252
+ ? `provision NEGADO: --allow-drop-deny no apaga denies VIVOS: el store tiene ${sweep.count} deny(s) de ${list} `
253
+ : `provision NEGADO: el store tiene ${sweep.count} deny(s) VIVOS de ${list} (declaradas por un modelo ` +
254
+ `ANTERIOR del store) que el modelo nuevo no declara `) +
255
+ `(p. ej. ${sweep.live.slice(0, 3).join(', ')}). Publicarlo los apagaría. Retíralos con removeDeny (o purga) y ` +
256
+ 'repite. No se ha escrito nada.');
257
+ this.exitCode = 1;
258
+ return null;
259
+ }
260
+ this.logger.warning(`${droppedLatest.length > 0 ? '--allow-drop-deny: el modelo nuevo quita' : 'el modelo nuevo no declara (algún modelo anterior sí)'} ${list}; ` +
261
+ `barrido del store: 0 denies vivos de esas relaciones (${sweep.read} tuplas leídas). Se publica.`);
262
+ return { recount: droppedEver };
263
+ }
264
+ /**
265
+ * El barrido; `null` (y exit 1 con el prefijo estable) si falla a medias (⚪ 10 c). Cierre 2 de D2 · 🟡 N4: en STREAMING
266
+ * —página a página, sin cargar el store en memoria y SIN cota de tuplas—: hasta el cierre 2, un store de más de 1 M
267
+ * tuplas dejaba `provision --store-id` en exit 1 PARA SIEMPRE tras retirar un opt-in (la historia de modelos no se
268
+ * borra en FGA), sin bandera que lo salvara. Lo que sigue siendo ruidoso: un servidor que no avanza (token repetido).
269
+ */
270
+ async sweep(client, dropped) {
271
+ try {
272
+ return await countLiveDenies(client, dropped);
273
+ }
274
+ catch (error) {
275
+ this.logger.error(`provision NEGADO: el barrido del store falló a medias (${String(error?.message ?? error)}): no se puede demostrar ` +
276
+ `que no queda ningún deny vivo de ${dropped.join(', ')}. No se ha escrito nada.`);
277
+ this.exitCode = 1;
278
+ return null;
279
+ }
280
+ }
281
+ /**
282
+ * **El RECUENTO justo antes de publicar** (cierre 1 de D2 · 🟠 2, TOCTOU): otro barrido de las mismas relaciones; un
283
+ * deny que llegó DURANTE o DESPUÉS del primero ⇒ exit 1 y CERO modelos. Es la señal de que la flota sigue en
284
+ * `deny: 'write'` (el paso 0 del procedimiento no se hizo).
285
+ */
286
+ async recountBeforePublish(client, dropped) {
287
+ const sweep = await this.sweep(client, dropped);
288
+ if (sweep === null)
289
+ return false;
290
+ if (sweep.count === 0)
291
+ return true;
292
+ this.logger.error(`provision NEGADO: el RECUENTO justo antes de publicar encontró ${sweep.count} deny(s) de ${dropped.join(', ')} ` +
293
+ `que llegaron DURANTE el barrido (p. ej. ${sweep.live.slice(0, 3).join(', ')}): la flota sigue escribiendo denies. ` +
294
+ "Pasa ANTES toda la flota a deny: 'read' (deny ⇒ 422 en todos los pods), retíralos y repite. No se ha escrito nada.");
295
+ this.exitCode = 1;
296
+ return false;
297
+ }
152
298
  /**
153
299
  * Los permisos del modelo, o `null` (y exit 1) si no hay ninguno: un modelo
154
300
  * `facts` sin permisos no puede responder a nada, y descubrirlo aquí es
@@ -178,6 +324,10 @@ __decorate([
178
324
  flags.string({ description: 'Store name when creating one (default: APP_NAME del entorno)' }),
179
325
  __metadata("design:type", Object)
180
326
  ], OpenFgaProvision.prototype, "name", void 0);
327
+ __decorate([
328
+ flags.boolean({ description: 'Allow a new model that drops denied_<rel> relations (only if the store has no live deny of them)' }),
329
+ __metadata("design:type", Object)
330
+ ], OpenFgaProvision.prototype, "allowDropDeny", void 0);
181
331
  /**
182
332
  * Los slugs de permiso de TODOS los catálogos del config, sin duplicados y en
183
333
  * orden estable (3b-2k · K2). Se exporta —y no es un método— para que se
@@ -219,4 +369,86 @@ export function relationsConfigOf(config) {
219
369
  return undefined;
220
370
  return { objectTypes };
221
371
  }
222
- //# sourceMappingURL=openfga_provision.js.map
372
+ /**
373
+ * Cierre 1 de D2 · 🟡 3: tope de la HISTORIA de modelos que `provision --store-id` lee para juzgar el estado del store
374
+ * (cada modelo publicado es una versión): pasarse es exit 1 (`provision NEGADO:`), nunca juzgar con una historia cortada.
375
+ */
376
+ export const PROVISION_MODEL_HISTORY_MAX = 10_000;
377
+ /** Todos los modelos del store, del más RECIENTE al más antiguo (`readAuthorizationModels` paginado, acotado). */
378
+ async function readModelHistory(client) {
379
+ const models = [];
380
+ let continuationToken;
381
+ do {
382
+ const page = await client.readAuthorizationModels({ pageSize: 50, continuationToken });
383
+ models.push(...(page?.authorization_models ?? []));
384
+ if (models.length > PROVISION_MODEL_HISTORY_MAX) {
385
+ throw new Error(`el store tiene más de ${PROVISION_MODEL_HISTORY_MAX} modelos (PROVISION_MODEL_HISTORY_MAX)`);
386
+ }
387
+ continuationToken = page?.continuation_token || undefined;
388
+ } while (continuationToken);
389
+ return models;
390
+ }
391
+ /** Los tipos que gobierna el modelo de ROLES (`facts`): sus `denied_<P>` son de permisos, no del deny de relaciones. */
392
+ const ROLE_MODEL_TYPES = new Set(['scope', 'role', 'role_binding', 'deny_binding']);
393
+ /**
394
+ * **Las `<tipo>#denied_<rel>` de TIPOS DE RELACIÓN que el modelo actual declara y el nuevo no** (plan 2.5 · lote D ·
395
+ * D2): la mutilación que la guarda de `openfga:provision` niega. Función PURA (dos modelos JSON en la forma de
396
+ * `readLatestAuthorizationModel` / `openFgaFactsModel`). Los `denied_<P>` de `scope` (roles) quedan fuera: retirar un
397
+ * permiso del catálogo no es retirar el opt-in de un tipo.
398
+ */
399
+ export function droppedDenyRelations(current, next) {
400
+ const nextRelations = new Set();
401
+ for (const t of next?.type_definitions ?? []) {
402
+ for (const relation of Object.keys(t?.relations ?? {}))
403
+ nextRelations.add(`${t.type}#${relation}`);
404
+ }
405
+ const dropped = [];
406
+ for (const t of current?.type_definitions ?? []) {
407
+ if (ROLE_MODEL_TYPES.has(t?.type))
408
+ continue;
409
+ for (const relation of Object.keys(t?.relations ?? {})) {
410
+ if (relation.startsWith('denied_') && !nextRelations.has(`${t.type}#${relation}`))
411
+ dropped.push(`${t.type}#${relation}`);
412
+ }
413
+ }
414
+ return dropped.sort();
415
+ }
416
+ /**
417
+ * **El barrido de `--allow-drop-deny`, en STREAMING** (cierre 2 de D2 · 🟡 N4): cuenta las tuplas vivas cuyo
418
+ * `<tipo>#<relación>` está en `dropped` leyendo el store ENTERO por páginas (`Read({})`: un `Read` por prefijo de objeto
419
+ * es 200 con cero tuplas) sin acumularlo — guarda el recuento y TRES ejemplos para la letra, nada más — y SIN cota de
420
+ * tuplas (hasta el cierre 2, 1 M ⇒ exit 1 para siempre). Exhaustivo o ruidoso: una página sin `tuples` o un
421
+ * `continuation_token` que se repite (el servidor no avanza) LANZA (el comando lo dice como «el barrido falló a medias»).
422
+ * Exportada para el caso que la juzga sin servidor.
423
+ */
424
+ export async function countLiveDenies(client, dropped) {
425
+ const wanted = new Set(dropped);
426
+ const live = [];
427
+ let count = 0;
428
+ let read = 0;
429
+ let continuationToken;
430
+ const seen = new Set();
431
+ do {
432
+ const page = await client.read({}, { pageSize: 100, continuationToken });
433
+ if (!Array.isArray(page?.tuples))
434
+ throw new Error("una página del Read no trae 'tuples': no se toma por vacía");
435
+ for (const tuple of page.tuples) {
436
+ read++;
437
+ const type = String(tuple?.key?.object ?? '').split(':')[0];
438
+ if (!wanted.has(`${type}#${tuple?.key?.relation}`))
439
+ continue;
440
+ count++;
441
+ if (live.length < 3)
442
+ live.push(`${tuple.key.object}#${tuple.key.relation}@${tuple.key.user}`);
443
+ }
444
+ const next = page?.continuation_token || undefined;
445
+ // Un token YA visto (no solo el inmediato: un ciclo a → b → a tampoco avanza) lanza; sin la cota de tuplas, esta
446
+ // guarda es lo único que impide un barrido infinito. Coste: un token por página en memoria.
447
+ if (next !== undefined && seen.has(next))
448
+ throw new Error(`el continuation_token se repite tras ${read} tuplas: el servidor no avanza`);
449
+ if (next !== undefined)
450
+ seen.add(next);
451
+ continuationToken = next;
452
+ } while (continuationToken);
453
+ return { live, count, read };
454
+ }
@@ -18,4 +18,3 @@ import type Configure from '@adonisjs/core/commands/configure';
18
18
  * nada distinto.
19
19
  */
20
20
  export declare function configure(command: Configure): Promise<void>;
21
- //# sourceMappingURL=configure.d.ts.map
@@ -76,4 +76,3 @@ export async function configure(command) {
76
76
  }
77
77
  command.logger.info('Siguiente paso: node ace migration:run');
78
78
  }
79
- //# sourceMappingURL=configure.js.map
package/build/index.d.ts CHANGED
@@ -50,7 +50,7 @@ export type { SqlScopeOutboxOptions } from './src/scope_outbox.js';
50
50
  * obligaría a importar ese SDK — acoplando el call-site al backend que este
51
51
  * paquete existe para abstraer.
52
52
  */
53
- export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, WriteConflictError, ActorRequiredError, CheckManyBatchError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, TooManyLocalRolesError, MassPurgeRefusedError, PruneInterruptedError, UnsupportedDialectError, ScopeTooDeepError, ScopeDepthLimitError, ScopeRequeueRefusedError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, ModelTooLargeError, RelationConfigError, RelationConfigUnknownKeyError, RelationTypeUnknownError, RelationUnknownError, RelationUsersetUnsupportedError, ScopeTreeDriftError, ScopeDriftUnguardedError, AuthorizationFrozenError, FreezeHeldError, MassReconcileRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, ReconcileTooLargeError, markPartialWrite, isPartialWrite, } from './src/errors.js';
53
+ export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ResolutionLimitError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, WriteConflictError, RenewalUnsafeError, RenewalRacedError, ActorRequiredError, CheckManyBatchError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, TooManyLocalRolesError, MassPurgeRefusedError, PruneInterruptedError, UnsupportedDialectError, ScopeTooDeepError, ScopeDepthLimitError, GroupDepthLimitError, ScopeRequeueRefusedError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, ModelTooLargeError, RelationConfigError, RelationConfigUnknownKeyError, RelationTypeUnknownError, RelationUnknownError, RelationUsersetUnsupportedError, RelationNotDenyableError, RelationDenyExpiryError, MembersOfDenyLimitError, InvalidRelationPageError, ScopeTreeDriftError, ScopeDriftUnguardedError, AuthorizationFrozenError, FreezeHeldError, MassReconcileRefusedError, DenyMigrationRefusedError, DenyReachRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, ReconcileTooLargeError, markPartialWrite, isPartialWrite, } from './src/errors.js';
54
54
  /**
55
55
  * Validación de identidad y de slugs (422). La aplica el manager en cada
56
56
  * llamada y los drivers por defensa en profundidad; se exporta para que un
@@ -65,17 +65,20 @@ export type { IdentityParts, SlugKind } from './src/identity.js';
65
65
  * `defineRelationsConfig`, la fachada `RelationsManager` (F-05 + `assertWrite`
66
66
  * + `actor`) y los tipos de la config.
67
67
  */
68
- export { defineRelationsConfig } from './src/relations/define_relations_config.js';
69
- export type { RelationsConfig, RelationsConfigSpec, RelationsDatabaseOptions, RelationObjectTypeSpec, RelationObjectRelationSpec, } from './src/relations/define_relations_config.js';
68
+ export { defineRelationsConfig, DENIABLE_RELATION_NAME_MAX, RELATION_DENY_PREFIX, } from './src/relations/define_relations_config.js';
69
+ export type { RelationDenyPhase, RelationsConfig, RelationsConfigSpec, RelationsDatabaseOptions, RelationObjectTypeSpec, RelationObjectRelationSpec, } from './src/relations/define_relations_config.js';
70
70
  export { RelationsManager } from './src/relations/manager.js';
71
71
  export type { RelationsManagerOptions } from './src/relations/manager.js';
72
72
  export { relationTupleKey, relationSubjectKey } from './src/relations/tuple_key.js';
73
73
  export type { RelationTupleKind, RelationTupleIdentity } from './src/relations/tuple_key.js';
74
74
  export { RELATIONS_CHECK_MANY_MAX } from './src/relations/check_many.js';
75
- export type { RelationsReconcileReport, RelationsReconcileDetail } from './src/relations/reconcile.js';
76
- export { saveRelationsConfig, readRelationsConfig, readRelationsConfigSpec, } from './src/relations_config_store.js';
77
- export type { RelationsConfigStoreOptions } from './src/relations_config_store.js';
78
- export { DatabaseRelationsDriver } from './src/drivers/database_relations_driver.js';
75
+ export { RELATIONS_ENUMERATE_DEFAULT_LIMIT, RELATIONS_ENUMERATE_MAX_LIMIT } from './src/relations/page.js';
76
+ export { RELATIONS_GROUP_DEPTH_MEASURED, GROUP_DEPTH_CONFIG_MAX, GROUP_DEPTH_MAX_NODES, GROUP_DEPTH_MAX_READS, GROUP_DEPTH_MAX_TUPLES, GROUP_DEPTH_DEADLINE_MS, GROUP_DEPTH_CALIBRATION_BACKOFF_MS, GROUP_DEPTH_RECALIBRATION_MIN_INTERVAL_MS, } from './src/relations/group_depth.js';
77
+ export type { RelationsReconcileReport, RelationsReconcileDetail, RelationsPartitionRefusal, RelationsPartitionRefusalReason } from './src/relations/reconcile.js';
78
+ export { saveRelationsConfig, readRelationsConfig, readRelationsConfigSpec, checkRelationsConfigDenyReach, } from './src/relations_config_store.js';
79
+ export type { RelationsConfigStoreOptions, SaveRelationsConfigOptions, RelationsConfigDenyReachCheck } from './src/relations_config_store.js';
80
+ export type { DenyReachFinding, DenyReachReason } from './src/relations/deny_reach.js';
81
+ export { DatabaseRelationsDriver, RELATIONS_MEMBERS_DENY_CHECK_MAX } from './src/drivers/database_relations_driver.js';
79
82
  export type { DatabaseRelationsDriverOptions } from './src/drivers/database_relations_driver.js';
80
83
  export { resolveGrantExpiry, isActiveExpiry, sameInstant } from './src/expiry.js';
81
84
  export { DatabaseAuthorizationDriver, APP_SCOPE_DB_UUID } from './src/drivers/database_driver.js';
@@ -86,7 +89,7 @@ export type { DatabaseDriverOptions } from './src/drivers/database_driver.js';
86
89
  * opcional `@openfga/sdk`. Un consumidor solo-database arranca sin él (D9).
87
90
  */
88
91
  export { APP_SCOPE, APP_SCOPE_TYPE, DEFAULT_RECONCILE_MAX_TUPLES } from './src/types.js';
89
- export type { AuthorizationDriver, AuthorizationDriverFactory, AuthorizedScopes, AuthzWriteEvent, AuthzCatalogWriteEvent, CatalogPermissionSpec, CatalogRole, CatalogRoleSpec, CatalogSpec, ScopedRoleChanges, ScopedRoleSpec, DenyOptions, DenyRef, ExcludedSubtree, GrantOptions, GrantOutcome, HolderTypeMap, ReconcileCounts, ReconcileFact, ReconcileFactPage, ReconcileFactsEnumerator, ReconcileOptions, ReconcileReport, ReconcileSkip, ReconcileSource, RoleQuery, ScopeChainResolver, ScopeDescendantsResolver, ScopeEdge, ScopeEdgePage, ScopeEdgesEnumerator, ScopedWriteOptions, ScopeRef, ScopeType, SubjectRef, WriteOptions, AuthorizationDriverCapabilities, RelationsDriver, RelationsDriverCapabilities, RelObject, RelSubject, RelUserset, RelationRef, RelationWriteEvent, RelationWriteOptions, RelationTransactionOptions, RelationPurgeOptions, RelationPage, RelationObjectsPage, RelationSubjectsPage, RelationTuple, RelationTuplePage, ScopeOutbox, ScopeOutboxContext, ScopeOutboxLease, ScopeOutboxPendingFilter, ScopeOutboxFailure, ScopeRequeueConflict, ScopeRequeueReport, ScopeTreeChange, PendingScopeTreeChange, RelayedScopeChange, ScopeRelayReport, ScopeRelayFailure, ScopeRelayEntry, ScopeRelayEntryStatus, ScopeEnqueueReceipt, } from './src/types.js';
92
+ export type { AuthorizationDriver, AuthorizationDriverFactory, AuthorizedScopes, AuthzWriteEvent, AuthzCatalogWriteEvent, CatalogPermissionSpec, CatalogRole, CatalogRoleSpec, CatalogSpec, ScopedRoleChanges, ScopedRoleSpec, DenyOptions, DenyRef, ExcludedSubtree, GrantOptions, GrantOutcome, HolderTypeMap, ReconcileCounts, ReconcileFact, ReconcileFactPage, ReconcileFactsEnumerator, ReconcileOptions, ReconcileReport, ReconcileSkip, ReconcileSource, RoleQuery, ScopeChainResolver, ScopeDescendantsResolver, ScopeEdge, ScopeEdgePage, ScopeEdgesEnumerator, ScopedWriteOptions, ScopeRef, ScopeType, SubjectRef, WriteOptions, AuthorizationDriverCapabilities, RelationsDriver, RelationsDriverCapabilities, RelObject, RelSubject, RelUserset, RelationRef, RelationWriteEvent, RelationWriteOptions, RelationDenyOptions, RelationDecision, RelationTransactionOptions, RelationPurgeOptions, RelationPage, RelationObjectsPage, RelationSubjectsPage, RelationTuple, RelationTuplePage, ScopeOutbox, ScopeOutboxContext, ScopeOutboxLease, ScopeOutboxPendingFilter, ScopeOutboxFailure, ScopeRequeueConflict, ScopeRequeueReport, ScopeTreeChange, PendingScopeTreeChange, RelayedScopeChange, ScopeRelayReport, ScopeRelayFailure, ScopeRelayEntry, ScopeRelayEntryStatus, ScopeEnqueueReceipt, } from './src/types.js';
90
93
  export { isRelUserset } from './src/types.js';
91
94
  /**
92
95
  * **¿De quién es esta transacción?** (L-1 · 🟠 9, regla del puerto `{trx}`):
@@ -106,4 +109,3 @@ export { default as AuthzAssignment } from './src/models/authz_assignment.js';
106
109
  export { default as AuthzDeny } from './src/models/authz_deny.js';
107
110
  export { withAuthzScopes } from './src/traits/authz_scopes.js';
108
111
  export { hasUuid } from './src/traits/has_uuid.js';
109
- //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -43,7 +43,7 @@ export { sqlScopeOutbox } from './src/scope_outbox.js';
43
43
  * obligaría a importar ese SDK — acoplando el call-site al backend que este
44
44
  * paquete existe para abstraer.
45
45
  */
46
- export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, WriteConflictError, ActorRequiredError, CheckManyBatchError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, TooManyLocalRolesError, MassPurgeRefusedError, PruneInterruptedError, UnsupportedDialectError, ScopeTooDeepError, ScopeDepthLimitError, ScopeRequeueRefusedError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, ModelTooLargeError, RelationConfigError, RelationConfigUnknownKeyError, RelationTypeUnknownError, RelationUnknownError, RelationUsersetUnsupportedError, ScopeTreeDriftError, ScopeDriftUnguardedError, AuthorizationFrozenError, FreezeHeldError, MassReconcileRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, ReconcileTooLargeError,
46
+ export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ResolutionLimitError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, WriteConflictError, RenewalUnsafeError, RenewalRacedError, ActorRequiredError, CheckManyBatchError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, TooManyLocalRolesError, MassPurgeRefusedError, PruneInterruptedError, UnsupportedDialectError, ScopeTooDeepError, ScopeDepthLimitError, GroupDepthLimitError, ScopeRequeueRefusedError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, ModelTooLargeError, RelationConfigError, RelationConfigUnknownKeyError, RelationTypeUnknownError, RelationUnknownError, RelationUsersetUnsupportedError, RelationNotDenyableError, RelationDenyExpiryError, MembersOfDenyLimitError, InvalidRelationPageError, ScopeTreeDriftError, ScopeDriftUnguardedError, AuthorizationFrozenError, FreezeHeldError, MassReconcileRefusedError, DenyMigrationRefusedError, DenyReachRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, ReconcileTooLargeError,
47
47
  // Cierre de alpha.3 (invariante 13 en purgas multi-request): un driver de
48
48
  // relaciones de terceros marca con `markPartialWrite` el fallo que llega
49
49
  // DESPUÉS de haber borrado parte; el manager notifica `indeterminate: true`.
@@ -61,19 +61,27 @@ export { assertIdentity, assertSubject, assertScope, assertPartition, assertVali
61
61
  * `defineRelationsConfig`, la fachada `RelationsManager` (F-05 + `assertWrite`
62
62
  * + `actor`) y los tipos de la config.
63
63
  */
64
- export { defineRelationsConfig } from './src/relations/define_relations_config.js';
64
+ export { defineRelationsConfig, DENIABLE_RELATION_NAME_MAX, RELATION_DENY_PREFIX, } from './src/relations/define_relations_config.js';
65
65
  export { RelationsManager } from './src/relations/manager.js';
66
66
  // Plan 2.5 · lote A: la clave PURA de una tupla (P3, con `kind` desde el primer día) y el tope de `checkMany` (P4).
67
67
  export { relationTupleKey, relationSubjectKey } from './src/relations/tuple_key.js';
68
68
  export { RELATIONS_CHECK_MANY_MAX } from './src/relations/check_many.js';
69
+ // Lote «antes de RC» · 7.4: la página de `enumerate*` (default y techo; por encima se recorta con cursor).
70
+ export { RELATIONS_ENUMERATE_DEFAULT_LIMIT, RELATIONS_ENUMERATE_MAX_LIMIT } from './src/relations/page.js';
71
+ // Lote «antes de RC» · R1: las cotas de la anidación de grupos (la fila de «Limits»); la regla vive en los drivers.
72
+ export { RELATIONS_GROUP_DEPTH_MEASURED, GROUP_DEPTH_CONFIG_MAX, GROUP_DEPTH_MAX_NODES,
73
+ // Cierre de R1 (🟠 1 del auditor): el presupuesto de la comprobación de una arista en openfga.
74
+ GROUP_DEPTH_MAX_READS, GROUP_DEPTH_MAX_TUPLES, GROUP_DEPTH_DEADLINE_MS,
75
+ // Cierre-2 de R1 (D5): la espera tras una calibración fallida y la cota de la re-calibración tras un 503 de profundidad.
76
+ GROUP_DEPTH_CALIBRATION_BACKOFF_MS, GROUP_DEPTH_RECALIBRATION_MIN_INTERVAL_MS, } from './src/relations/group_depth.js';
69
77
  // La config de relaciones PERSISTIDA (🟡3): API de PLATAFORMA (bajo el gate de
70
78
  // versión, invariante 14). La CONSUME `authz:relations:reconcile` (config del
71
79
  // destino, para la deriva del modelo fusionado en --dry-run).
72
- export { saveRelationsConfig, readRelationsConfig, readRelationsConfigSpec, } from './src/relations_config_store.js';
80
+ export { saveRelationsConfig, readRelationsConfig, readRelationsConfigSpec, checkRelationsConfigDenyReach, } from './src/relations_config_store.js';
73
81
  // El driver `database` de relaciones (el consumidor lo cablea en
74
82
  // `relations.drivers` de config/authorization.ts). El driver `openfga` de
75
83
  // relaciones NO sale de aquí: vive en el subpath `/openfga` (peer opcional).
76
- export { DatabaseRelationsDriver } from './src/drivers/database_relations_driver.js';
84
+ export { DatabaseRelationsDriver, RELATIONS_MEMBERS_DENY_CHECK_MAX } from './src/drivers/database_relations_driver.js';
77
85
  export { resolveGrantExpiry, isActiveExpiry, sameInstant } from './src/expiry.js';
78
86
  export { DatabaseAuthorizationDriver, APP_SCOPE_DB_UUID } from './src/drivers/database_driver.js';
79
87
  /**
@@ -100,4 +108,3 @@ export { default as AuthzAssignment } from './src/models/authz_assignment.js';
100
108
  export { default as AuthzDeny } from './src/models/authz_deny.js';
101
109
  export { withAuthzScopes } from './src/traits/authz_scopes.js';
102
110
  export { hasUuid } from './src/traits/has_uuid.js';
103
- //# sourceMappingURL=index.js.map
@@ -43,4 +43,3 @@ export default class AuthzProvider {
43
43
  constructor(app: ApplicationService);
44
44
  register(): void;
45
45
  }
46
- //# sourceMappingURL=authz_provider.d.ts.map
@@ -84,4 +84,3 @@ export default class AuthzProvider {
84
84
  });
85
85
  }
86
86
  }
87
- //# sourceMappingURL=authz_provider.js.map
@@ -7,4 +7,3 @@ import type { AuthorizationManager } from '../src/manager.js';
7
7
  */
8
8
  declare let authorization: AuthorizationManager;
9
9
  export { authorization as default };
10
- //# sourceMappingURL=main.d.ts.map
@@ -10,4 +10,3 @@ await app.booted(async () => {
10
10
  authorization = await app.container.make('authz.manager');
11
11
  });
12
12
  export { authorization as default };
13
- //# sourceMappingURL=main.js.map
@@ -11,4 +11,3 @@ import type { RelationsManager } from '../src/relations/manager.js';
11
11
  */
12
12
  declare let relations: RelationsManager;
13
13
  export { relations as default };
14
- //# sourceMappingURL=relations.d.ts.map
@@ -14,4 +14,3 @@ await app.booted(async () => {
14
14
  relations = await app.container.make('authz.relations');
15
15
  });
16
16
  export { relations as default };
17
- //# sourceMappingURL=relations.js.map
@@ -286,4 +286,3 @@ export declare function runCatalogDiff(catalogs: CatalogSource[], options?: {
286
286
  export declare function syncCatalogs(catalogs: CatalogSource[], options?: SyncCatalogOptions): Promise<{
287
287
  count: number;
288
288
  } & CatalogSyncReport>;
289
- //# sourceMappingURL=catalog.d.ts.map
@@ -856,4 +856,3 @@ export async function syncCatalogs(catalogs, options = {}) {
856
856
  }
857
857
  return { count, shadowedByGlobal, assignableAtViolations };
858
858
  }
859
- //# sourceMappingURL=catalog.js.map
@@ -304,6 +304,19 @@ export declare class CatalogCache {
304
304
  * vuelve a intentarlo.
305
305
  */
306
306
  view(): Promise<CatalogView>;
307
+ /**
308
+ * **La foto para una ESCRITURA** (lote C · carreras, resolución 2 del juez = K4): contrasta la versión con la fila
309
+ * SIEMPRE, ignore o no la ventana `{ everyMs }`. Con `'always'` es exactamente `view()`. La ventana es para DECIDIR
310
+ * (invariante 14: una pregunta puede responder con un catálogo de hasta `everyMs` de antigüedad); una escritura del driver
311
+ * deja ESTADO en el store que no caduca —el barrido de `moved`, el settle, la proyección—, y con un memo rancio un rol
312
+ * recién definido en otro proceso no se barre (fail-open persistente medido, K4). Coste: el SELECT por PK de `'always'`.
313
+ *
314
+ * **Nunca se suma a una comprobación ni a una carga EN VUELO** (asentar · cierre 2, ⚪ 6 / O-6 del auditor): la que ya
315
+ * estaba en marcha pudo leer la fila ANTES de la escritura que precede a esta llamada (la propia, o la de otro proceso
316
+ * que la escritura acaba de observar) y serviría esa foto. Se espera la carga en vuelo y se lee la versión otra vez;
317
+ * una carga que arranca DESPUÉS de esta llamada sí se comparte (ya es posterior).
318
+ */
319
+ freshView(): Promise<CatalogView>;
307
320
  /**
308
321
  * Olvida la foto de ESTE memo. La siguiente pregunta recarga. Es un bump
309
322
  * de generación, no un `#view = null`: una carga en vuelo capturó la
@@ -321,4 +334,3 @@ export declare class CatalogCache {
321
334
  * disfrazado de dato.
322
335
  */
323
336
  export declare function parseAssignableAt(slug: string, raw: unknown): readonly ScopeType[] | null;
324
- //# sourceMappingURL=catalog_cache.d.ts.map
@@ -420,6 +420,31 @@ export class CatalogCache {
420
420
  }
421
421
  return this.#reload();
422
422
  }
423
+ /**
424
+ * **La foto para una ESCRITURA** (lote C · carreras, resolución 2 del juez = K4): contrasta la versión con la fila
425
+ * SIEMPRE, ignore o no la ventana `{ everyMs }`. Con `'always'` es exactamente `view()`. La ventana es para DECIDIR
426
+ * (invariante 14: una pregunta puede responder con un catálogo de hasta `everyMs` de antigüedad); una escritura del driver
427
+ * deja ESTADO en el store que no caduca —el barrido de `moved`, el settle, la proyección—, y con un memo rancio un rol
428
+ * recién definido en otro proceso no se barre (fail-open persistente medido, K4). Coste: el SELECT por PK de `'always'`.
429
+ *
430
+ * **Nunca se suma a una comprobación ni a una carga EN VUELO** (asentar · cierre 2, ⚪ 6 / O-6 del auditor): la que ya
431
+ * estaba en marcha pudo leer la fila ANTES de la escritura que precede a esta llamada (la propia, o la de otro proceso
432
+ * que la escritura acaba de observar) y serviría esa foto. Se espera la carga en vuelo y se lee la versión otra vez;
433
+ * una carga que arranca DESPUÉS de esta llamada sí se comparte (ya es posterior).
434
+ */
435
+ async freshView() {
436
+ if (this.#loading)
437
+ await this.#loading.catch(() => undefined);
438
+ const current = this.#view;
439
+ if (!current || !this.#isFresh())
440
+ return this.#reload();
441
+ const dbVersion = await this.#readVersion();
442
+ if (this.#view === current && this.#isFresh() && dbVersion === current.version) {
443
+ this.#checkedAt = this.#now();
444
+ return current;
445
+ }
446
+ return this.#reload(dbVersion);
447
+ }
423
448
  /** Una carga compartida; `knownVersion` es la fila recién leída por una revalidación (se ahorra releerla). */
424
449
  #reload(knownVersion) {
425
450
  if (!this.#loading) {
@@ -663,4 +688,3 @@ function buildCatalogView(permissions, roles, links, loadedAt, version) {
663
688
  version,
664
689
  };
665
690
  }
666
- //# sourceMappingURL=catalog_cache.js.map
@@ -21,4 +21,3 @@ export type Clock = () => Date;
21
21
  export declare const systemClock: Clock;
22
22
  /** `now` tiene que ser una función; se valida al construir (500 `E_AUTHZ_CONFIG` es del llamante). */
23
23
  export declare function isClock(value: unknown): value is Clock;
24
- //# sourceMappingURL=clock.d.ts.map
@@ -4,4 +4,3 @@ export const systemClock = () => new Date();
4
4
  export function isClock(value) {
5
5
  return typeof value === 'function';
6
6
  }
7
- //# sourceMappingURL=clock.js.map
@@ -381,4 +381,3 @@ export declare function defineConfig<T extends AuthorizationConfig>(config: T):
381
381
  * barata (claves, no valores).
382
382
  */
383
383
  export declare function assertAuthorizationConfigShape(config: unknown): void;
384
- //# sourceMappingURL=define_config.d.ts.map
@@ -77,12 +77,36 @@ function assertConfigBlock(value, path) {
77
77
  `una clave conocida no es «nada»: se ignoraría en silencio (plan 2.5 · B2).`);
78
78
  }
79
79
  }
80
+ /**
81
+ * **Lápidas: claves que NO existen aquí pero que la letra de una decisión firmada nombra así** (cierre-2 de R1 · D1 (a),
82
+ * decisión del dueño 2026-10-20: «`relations.maxGroupDepth` ⇒ 500 con mensaje que apunta a `defineRelationsConfig`»).
83
+ * La decisión (5) escribió `relations.maxGroupDepth`; el mando vive en `defineRelationsConfig` (la config que capturan
84
+ * las factories de los drivers, `manager.driver()` y `runRelationsReconcile`) y se lee como `relations.config.maxGroupDepth`.
85
+ * Sin alias (una clave hermana se validaría y no gobernaría tres de las cuatro rutas): la ruta literal es el MISMO 500
86
+ * de clave desconocida, con la receta del sitio bueno en vez de la lista de claves.
87
+ */
88
+ const MISPLACED_CONFIG_KEYS = Object.freeze({
89
+ 'relations.maxGroupDepth': "defineConfig: la clave 'relations.maxGroupDepth' no existe. El techo de anidación de grupos va en la config de " +
90
+ 'RELACIONES: defineRelationsConfig({ objectTypes, maxGroupDepth, database: { enforceGroupDepth } }), y ese objeto en ' +
91
+ 'relations.config. Es la config que capturan los drivers (las factories de relations.drivers), manager.driver() y ' +
92
+ 'runRelationsReconcile (authz:relations:reconcile): una clave hermana de relations no llegaría a ninguno y el límite ' +
93
+ 'no se aplicaría.',
94
+ 'relations.enforceGroupDepth': "defineConfig: la clave 'relations.enforceGroupDepth' no existe. El opt-in del techo de anidación de grupos en " +
95
+ 'database va en la config de RELACIONES: defineRelationsConfig({ objectTypes, maxGroupDepth, database: { ' +
96
+ 'enforceGroupDepth: true } }), y ese objeto en relations.config (la que capturan los drivers).',
97
+ });
80
98
  function assertKnownConfigKeys(value, known, path) {
81
99
  if (!value || typeof value !== 'object' || Array.isArray(value))
82
100
  return;
83
101
  for (const key of Object.keys(value)) {
84
102
  if (known.includes(key))
85
103
  continue;
104
+ // Cierre-3 · R-6: `Object.hasOwn` — una clave `constructor`/`toString` (o un `__proto__` propio de un JSON) leía el
105
+ // prototipo de la tabla y el 500 llevaba de mensaje `function Object() { [native code] }`.
106
+ const dotted = path === '' ? key : `${path}.${key}`;
107
+ const tombstone = Object.hasOwn(MISPLACED_CONFIG_KEYS, dotted) ? MISPLACED_CONFIG_KEYS[dotted] : undefined;
108
+ if (tombstone !== undefined)
109
+ throw new AuthorizationConfigError(tombstone);
86
110
  const where = path === '' ? `'${key}' en la raíz` : `'${path}.${key}'`;
87
111
  throw new AuthorizationConfigError(`defineConfig: la clave ${where} no existe (claves válidas en ${path === '' ? 'la raíz' : path}: ${known.join(', ')}). ` +
88
112
  `Ninguna clave del config se descarta en silencio (plan 2.5 · B2): un typo ('requireWithing') sería una ` +
@@ -121,4 +145,3 @@ export function assertAuthorizationConfigShape(config) {
121
145
  assertKnownConfigKeys(c[block], AUTHORIZATION_CONFIG_KEYS[block], block);
122
146
  }
123
147
  }
124
- //# sourceMappingURL=define_config.js.map
@@ -204,6 +204,12 @@ export declare class DatabaseAuthorizationDriver implements AuthorizationDriver
204
204
  */
205
205
  readonly catalog: CatalogCache;
206
206
  constructor(options?: DatabaseDriverOptions);
207
+ /**
208
+ * **El catálogo de una ESCRITURA** (K4; asentar · cierre 2, D-1): con la versión contrastada en fresco SIEMPRE
209
+ * (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
210
+ * `view()` (el contrato del memo es suyo). Gemelo de `OpenFgaAuthorizationDriver.writeCatalog`.
211
+ */
212
+ private writeCatalog;
207
213
  /**
208
214
  * Vista de este driver con OTRO resolutor de la cadena y el mismo estado
209
215
  * (conexión, memo del catálogo, deadline). Es lo que usa
@@ -393,4 +399,3 @@ export declare class DatabaseAuthorizationDriver implements AuthorizationDriver
393
399
  */
394
400
  private eachReconcileRow;
395
401
  }
396
- //# sourceMappingURL=database_driver.d.ts.map