@jantstack/adonis-authz 1.1.0 → 2.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +837 -51
  3. package/build/commands/authz_catalog_diff.d.ts +28 -0
  4. package/build/commands/authz_catalog_diff.d.ts.map +1 -0
  5. package/build/commands/authz_catalog_diff.js +67 -0
  6. package/build/commands/authz_catalog_diff.js.map +1 -0
  7. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  8. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  9. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  10. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  11. package/build/commands/authz_catalog_sync.d.ts +37 -0
  12. package/build/commands/authz_catalog_sync.d.ts.map +1 -0
  13. package/build/commands/authz_catalog_sync.js +81 -0
  14. package/build/commands/authz_catalog_sync.js.map +1 -0
  15. package/build/commands/authz_freeze.d.ts +44 -0
  16. package/build/commands/authz_freeze.d.ts.map +1 -0
  17. package/build/commands/authz_freeze.js +95 -0
  18. package/build/commands/authz_freeze.js.map +1 -0
  19. package/build/commands/authz_reconcile.d.ts +102 -0
  20. package/build/commands/authz_reconcile.d.ts.map +1 -0
  21. package/build/commands/authz_reconcile.js +294 -0
  22. package/build/commands/authz_reconcile.js.map +1 -0
  23. package/build/commands/authz_relations_reconcile.d.ts +73 -0
  24. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  25. package/build/commands/authz_relations_reconcile.js +225 -0
  26. package/build/commands/authz_relations_reconcile.js.map +1 -0
  27. package/build/commands/authz_scopes_relay.d.ts +47 -0
  28. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  29. package/build/commands/authz_scopes_relay.js +141 -0
  30. package/build/commands/authz_scopes_relay.js.map +1 -0
  31. package/build/commands/authz_unfreeze.d.ts +37 -0
  32. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  33. package/build/commands/authz_unfreeze.js +92 -0
  34. package/build/commands/authz_unfreeze.js.map +1 -0
  35. package/build/commands/main.d.ts +8 -1
  36. package/build/commands/main.d.ts.map +1 -1
  37. package/build/commands/main.js +8 -1
  38. package/build/commands/main.js.map +1 -1
  39. package/build/commands/openfga_provision.d.ts +46 -4
  40. package/build/commands/openfga_provision.d.ts.map +1 -1
  41. package/build/commands/openfga_provision.js +91 -7
  42. package/build/commands/openfga_provision.js.map +1 -1
  43. package/build/configure.d.ts +11 -0
  44. package/build/configure.d.ts.map +1 -1
  45. package/build/configure.js +37 -1
  46. package/build/configure.js.map +1 -1
  47. package/build/index.d.ts +75 -7
  48. package/build/index.d.ts.map +1 -1
  49. package/build/index.js +67 -5
  50. package/build/index.js.map +1 -1
  51. package/build/providers/authz_provider.d.ts +26 -2
  52. package/build/providers/authz_provider.d.ts.map +1 -1
  53. package/build/providers/authz_provider.js +48 -2
  54. package/build/providers/authz_provider.js.map +1 -1
  55. package/build/services/relations.d.ts +14 -0
  56. package/build/services/relations.d.ts.map +1 -0
  57. package/build/services/relations.js +17 -0
  58. package/build/services/relations.js.map +1 -0
  59. package/build/src/catalog/catalog.d.ts +289 -0
  60. package/build/src/catalog/catalog.d.ts.map +1 -0
  61. package/build/src/catalog/catalog.js +859 -0
  62. package/build/src/catalog/catalog.js.map +1 -0
  63. package/build/src/catalog/catalog_cache.d.ts +324 -0
  64. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  65. package/build/src/catalog/catalog_cache.js +666 -0
  66. package/build/src/catalog/catalog_cache.js.map +1 -0
  67. package/build/src/clock.d.ts +24 -0
  68. package/build/src/clock.d.ts.map +1 -0
  69. package/build/src/clock.js +7 -0
  70. package/build/src/clock.js.map +1 -0
  71. package/build/src/define_config.d.ts +201 -7
  72. package/build/src/define_config.d.ts.map +1 -1
  73. package/build/src/define_config.js.map +1 -1
  74. package/build/src/drivers/database_driver.d.ts +324 -15
  75. package/build/src/drivers/database_driver.d.ts.map +1 -1
  76. package/build/src/drivers/database_driver.js +1107 -128
  77. package/build/src/drivers/database_driver.js.map +1 -1
  78. package/build/src/drivers/database_relations_driver.d.ts +75 -0
  79. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  80. package/build/src/drivers/database_relations_driver.js +450 -0
  81. package/build/src/drivers/database_relations_driver.js.map +1 -0
  82. package/build/src/drivers/openfga_driver.d.ts +963 -55
  83. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  84. package/build/src/drivers/openfga_driver.js +2693 -372
  85. package/build/src/drivers/openfga_driver.js.map +1 -1
  86. package/build/src/drivers/openfga_facts.d.ts +369 -0
  87. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  88. package/build/src/drivers/openfga_facts.js +813 -0
  89. package/build/src/drivers/openfga_facts.js.map +1 -0
  90. package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
  91. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  92. package/build/src/drivers/openfga_relations_driver.js +466 -0
  93. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  94. package/build/src/errors.d.ts +586 -0
  95. package/build/src/errors.d.ts.map +1 -1
  96. package/build/src/errors.js +583 -0
  97. package/build/src/errors.js.map +1 -1
  98. package/build/src/expiry.d.ts +27 -0
  99. package/build/src/expiry.d.ts.map +1 -0
  100. package/build/src/expiry.js +50 -0
  101. package/build/src/expiry.js.map +1 -0
  102. package/build/src/freeze.d.ts +120 -0
  103. package/build/src/freeze.d.ts.map +1 -0
  104. package/build/src/freeze.js +172 -0
  105. package/build/src/freeze.js.map +1 -0
  106. package/build/src/hierarchical_resolver.d.ts +56 -0
  107. package/build/src/hierarchical_resolver.d.ts.map +1 -0
  108. package/build/src/hierarchical_resolver.js +87 -0
  109. package/build/src/hierarchical_resolver.js.map +1 -0
  110. package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
  111. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  112. package/build/src/{middleware → http}/app_access_middleware.js +10 -26
  113. package/build/src/http/app_access_middleware.js.map +1 -0
  114. package/build/src/http/resource_access_middleware.d.ts +105 -0
  115. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  116. package/build/src/http/resource_access_middleware.js +81 -0
  117. package/build/src/http/resource_access_middleware.js.map +1 -0
  118. package/build/src/identity.d.ts +228 -0
  119. package/build/src/identity.d.ts.map +1 -0
  120. package/build/src/identity.js +457 -0
  121. package/build/src/identity.js.map +1 -0
  122. package/build/src/manager.d.ts +536 -7
  123. package/build/src/manager.d.ts.map +1 -1
  124. package/build/src/manager.js +2676 -23
  125. package/build/src/manager.js.map +1 -1
  126. package/build/src/memoize_ancestors.d.ts +23 -0
  127. package/build/src/memoize_ancestors.d.ts.map +1 -0
  128. package/build/src/memoize_ancestors.js +42 -0
  129. package/build/src/memoize_ancestors.js.map +1 -0
  130. package/build/src/models/authz_assignment.d.ts +11 -11
  131. package/build/src/models/authz_assignment.d.ts.map +1 -1
  132. package/build/src/models/authz_deny.d.ts +11 -11
  133. package/build/src/models/authz_deny.d.ts.map +1 -1
  134. package/build/src/models/authz_permission.d.ts +17 -11
  135. package/build/src/models/authz_permission.d.ts.map +1 -1
  136. package/build/src/models/authz_permission.js +4 -0
  137. package/build/src/models/authz_permission.js.map +1 -1
  138. package/build/src/models/authz_role.d.ts +19 -12
  139. package/build/src/models/authz_role.d.ts.map +1 -1
  140. package/build/src/models/authz_role.js +6 -1
  141. package/build/src/models/authz_role.js.map +1 -1
  142. package/build/src/models/authz_role_permission.d.ts +11 -11
  143. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  144. package/build/src/openfga.d.ts +29 -0
  145. package/build/src/openfga.d.ts.map +1 -0
  146. package/build/src/openfga.js +26 -0
  147. package/build/src/openfga.js.map +1 -0
  148. package/build/src/reconcile.d.ts +37 -0
  149. package/build/src/reconcile.d.ts.map +1 -0
  150. package/build/src/reconcile.js +69 -0
  151. package/build/src/reconcile.js.map +1 -0
  152. package/build/src/relation_partition_trigger.d.ts +8 -0
  153. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  154. package/build/src/relation_partition_trigger.js +85 -0
  155. package/build/src/relation_partition_trigger.js.map +1 -0
  156. package/build/src/relations/define_relations_config.d.ts +58 -0
  157. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  158. package/build/src/relations/define_relations_config.js +144 -0
  159. package/build/src/relations/define_relations_config.js.map +1 -0
  160. package/build/src/relations/manager.d.ts +38 -0
  161. package/build/src/relations/manager.d.ts.map +1 -0
  162. package/build/src/relations/manager.js +156 -0
  163. package/build/src/relations/manager.js.map +1 -0
  164. package/build/src/relations/reconcile.d.ts +62 -0
  165. package/build/src/relations/reconcile.d.ts.map +1 -0
  166. package/build/src/relations/reconcile.js +138 -0
  167. package/build/src/relations/reconcile.js.map +1 -0
  168. package/build/src/relations_config_store.d.ts +22 -0
  169. package/build/src/relations_config_store.d.ts.map +1 -0
  170. package/build/src/relations_config_store.js +74 -0
  171. package/build/src/relations_config_store.js.map +1 -0
  172. package/build/src/scope_outbox.d.ts +69 -0
  173. package/build/src/scope_outbox.d.ts.map +1 -0
  174. package/build/src/scope_outbox.js +291 -0
  175. package/build/src/scope_outbox.js.map +1 -0
  176. package/build/src/shared/backend_guard.d.ts +106 -0
  177. package/build/src/shared/backend_guard.d.ts.map +1 -0
  178. package/build/src/shared/backend_guard.js +246 -0
  179. package/build/src/shared/backend_guard.js.map +1 -0
  180. package/build/src/shared/sql_expiry.d.ts +53 -0
  181. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  182. package/build/src/shared/sql_expiry.js +66 -0
  183. package/build/src/shared/sql_expiry.js.map +1 -0
  184. package/build/src/sql_descendants.d.ts +97 -0
  185. package/build/src/sql_descendants.d.ts.map +1 -0
  186. package/build/src/sql_descendants.js +203 -0
  187. package/build/src/sql_descendants.js.map +1 -0
  188. package/build/src/testing/contract.d.ts +212 -7
  189. package/build/src/testing/contract.d.ts.map +1 -1
  190. package/build/src/testing/contract.js +3449 -24
  191. package/build/src/testing/contract.js.map +1 -1
  192. package/build/src/testing/main.d.ts +10 -2
  193. package/build/src/testing/main.d.ts.map +1 -1
  194. package/build/src/testing/main.js +5 -1
  195. package/build/src/testing/main.js.map +1 -1
  196. package/build/src/testing/migration_contract.d.ts +284 -0
  197. package/build/src/testing/migration_contract.d.ts.map +1 -0
  198. package/build/src/testing/migration_contract.js +586 -0
  199. package/build/src/testing/migration_contract.js.map +1 -0
  200. package/build/src/testing/relations_contract.d.ts +51 -0
  201. package/build/src/testing/relations_contract.d.ts.map +1 -0
  202. package/build/src/testing/relations_contract.js +654 -0
  203. package/build/src/testing/relations_contract.js.map +1 -0
  204. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  205. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  206. package/build/src/testing/relations_reconcile_contract.js +172 -0
  207. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  208. package/build/src/testing/scope_tree.d.ts +65 -0
  209. package/build/src/testing/scope_tree.d.ts.map +1 -0
  210. package/build/src/testing/scope_tree.js +145 -0
  211. package/build/src/testing/scope_tree.js.map +1 -0
  212. package/build/src/traits/authz_scopes.d.ts +30 -6
  213. package/build/src/traits/authz_scopes.d.ts.map +1 -1
  214. package/build/src/traits/authz_scopes.js +30 -18
  215. package/build/src/traits/authz_scopes.js.map +1 -1
  216. package/build/src/traits/has_uuid.d.ts +12 -12
  217. package/build/src/traits/has_uuid.d.ts.map +1 -1
  218. package/build/src/types.d.ts +1313 -28
  219. package/build/src/types.d.ts.map +1 -1
  220. package/build/src/types.js +20 -4
  221. package/build/src/types.js.map +1 -1
  222. package/build/stubs/config/app_acl.stub +4 -2
  223. package/build/stubs/config/authorization.stub +168 -14
  224. package/build/stubs/migration.stub +183 -13
  225. package/build/stubs/scopes_outbox_migration.stub +57 -0
  226. package/package.json +14 -7
  227. package/build/commands/openfga_import.d.ts +0 -28
  228. package/build/commands/openfga_import.d.ts.map +0 -1
  229. package/build/commands/openfga_import.js +0 -74
  230. package/build/commands/openfga_import.js.map +0 -1
  231. package/build/src/catalog.d.ts +0 -3
  232. package/build/src/catalog.d.ts.map +0 -1
  233. package/build/src/catalog.js +0 -103
  234. package/build/src/catalog.js.map +0 -1
  235. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  236. package/build/src/middleware/app_access_middleware.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ import { systemClock } from './clock.js';
2
+ /**
3
+ * Semántica de `expiresAt` en un re-grant (L0.4), compartida por los drivers
4
+ * para que ambos respondan igual:
5
+ *
6
+ * omitido (`undefined`) → no tocar una caducidad VIGENTE; si la asignación
7
+ * ya expiró, revive sin caducidad (es un grant nuevo)
8
+ * `null` → quitar la caducidad, explícitamente
9
+ * `Date` → fijarla (también a un instante pasado: expira)
10
+ *
11
+ * El defecto que cierra: "asegúrate de que tiene el rol" (grant sin opciones,
12
+ * idempotente por naturaleza) convertía un acceso temporal en permanente.
13
+ */
14
+ export function resolveGrantExpiry(previous, requested, now = systemClock()) {
15
+ if (requested !== undefined)
16
+ return requested;
17
+ return isActiveExpiry(previous, now) ? previous : null;
18
+ }
19
+ /** Vigente: sin caducidad o con caducidad futura. */
20
+ export function isActiveExpiry(expiresAt, now = systemClock()) {
21
+ return expiresAt === null || expiresAt.getTime() > now.getTime();
22
+ }
23
+ /**
24
+ * ¿Mismo instante? Compara el tiempo, no la referencia ni la cadena:
25
+ * `2026-01-01T00:00:00Z` y `…00.000Z` son el mismo momento y no justifican
26
+ * una reescritura (que en openfga es delete+write con ventana de denegación).
27
+ */
28
+ export function sameInstant(a, b) {
29
+ if (!a && !b)
30
+ return true;
31
+ if (!a || !b)
32
+ return false;
33
+ return a.getTime() === b.getTime();
34
+ }
35
+ /** Lo que devuelve el backend como caducidad (número, ISO, Date) → Date | null. */
36
+ export function toExpiryDate(value) {
37
+ if (value === null || value === undefined || value === '')
38
+ return null;
39
+ if (value instanceof Date)
40
+ return value;
41
+ const date = new Date(value);
42
+ return Number.isNaN(date.getTime()) ? null : date;
43
+ }
44
+ /** ¿El outcome de un grant cambió la caducidad de una asignación que ya existía? */
45
+ export function expiryChanged(outcome) {
46
+ return (outcome.existed &&
47
+ outcome.previousExpiresAt !== undefined &&
48
+ !sameInstant(outcome.previousExpiresAt, outcome.expiresAt));
49
+ }
50
+ //# sourceMappingURL=expiry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.js","sourceRoot":"","sources":["../../src/expiry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAqB,EACrB,SAAkC,EAClC,MAAY,WAAW,EAAE;IAEzB,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AACxD,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,cAAc,CAAC,SAAsB,EAAE,MAAY,WAAW,EAAE;IAC9E,OAAO,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAA;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAA0B,EAAE,CAA0B;IAChF,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IACzB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1B,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAA;AACpC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACtE,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAA;IACvC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAY,CAAC,CAAA;IACnC,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAAC,OAAqB;IACjD,OAAO,CACL,OAAO,CAAC,OAAO;QACf,OAAO,CAAC,iBAAiB,KAAK,SAAS;QACvC,CAAC,WAAW,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,SAAS,CAAC,CAC3D,CAAA;AACH,CAAC"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
3
+ * B + E-analista). Primitivas SQL puras; la policy (quién congela, el
4
+ * anidado, el contexto del operador) vive en el manager.
5
+ *
6
+ * El freeze cuelga de la fila `id = 2` de `authz_catalog_version` — la señal
7
+ * entre procesos que el paquete ya usa (invariante 14). La colocación está
8
+ * MEDIDA (panel 3, analista §4.1): en la fila `id = 1` la renovación del
9
+ * lease espera al `FOR UPDATE` de `withAuthzCatalogWrite` (816 ms en PG /
10
+ * 805 en MySQL con un sync de 800 ms); en su propia fila, 10-12 ms. Y la
11
+ * consulta de la barrera es PROPIA y sin memo (juez C2): colgarse del memo
12
+ * del catálogo haría del freeze «una ventana acotada de freeze que todavía
13
+ * no aplica» con `catalogRevalidate: { everyMs }`. El coste que se publica
14
+ * es el medido: +0,14 ms p50 por escritura (PG; +0,11 MySQL), 0 por
15
+ * `authorize`.
16
+ *
17
+ * Las tres reglas del protocolo:
18
+ * - **Token de dueño** (`fence` + `holder`): `acquire` sube el fence y solo
19
+ * hay UN freeze vivo a la vez; `release`/`renew` son condicionales por
20
+ * token, así que un `unfreeze` ajeno o rezagado es un no-op (auditor
21
+ * A1.3: el `finally` de una ventana interior ya no puede levantar la
22
+ * barrera de otra).
23
+ * - **Lease**: `untilMs` en ms de PARED (el reloj del config, el mismo que
24
+ * decide caducidades). Vencido ⇒ el freeze deja de estar vivo y la flota
25
+ * vuelve a escribir SOLA — tras un `SIGKILL` nadie limpia nada (medido:
26
+ * 2 527 ms con lease de 3 000). `untilMs: null` = sin caducidad (la
27
+ * ventana del operador).
28
+ * - **Fence publicado**: la renovación exige `freeze_until_ms > now`; una
29
+ * renovación que toca 0 filas significa que el lease se PERDIÓ a mitad
30
+ * (pausa de GC, base caída más tiempo que el lease) y quien lo sostiene
31
+ * lo marca `lapsed` — la pasada no se certifica (juez C4).
32
+ *
33
+ * La fila ausente (o las columnas: una base migrada con el stub < 2.3) es
34
+ * **503 «migración 2.0 no aplicada»** en toda escritura, jamás «no
35
+ * congelado»: el mismo patrón que la fila `id = 1` del memo. Un upsert
36
+ * perezoso desde la barrera sería una escritura en el camino de lectura,
37
+ * fuera de `withAuthzCatalogWrite`, y una carrera gratis.
38
+ */
39
+ /** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
40
+ export declare const FREEZE_ROW_ID = 2;
41
+ /** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
42
+ export declare const DEFAULT_FREEZE_LEASE_MS = 15000;
43
+ /** Quién congela: el dueño va escrito en la fila (`<kind>:<pid>:<hex>`). */
44
+ export type FreezeKind = 'operator' | 'reconcile' | 'platform';
45
+ /** El token del dueño: lo devuelve `freeze()` y es lo ÚNICO que levanta o renueva ese freeze. */
46
+ export interface FreezeToken {
47
+ /** Número de generación: sube en cada `acquire`, nunca se reutiliza. */
48
+ fence: number;
49
+ /** `<kind>:<pid>:<hex>`: identifica al proceso que congeló. */
50
+ holder: string;
51
+ }
52
+ /** La fila del freeze, tal como está en la base. */
53
+ export interface FreezeRow {
54
+ reason: string | null;
55
+ holder: string | null;
56
+ untilMs: number | null;
57
+ fence: number;
58
+ }
59
+ export interface FreezeSqlOptions {
60
+ /** Etiqueta del driver en los errores 503 (default `freeze`). */
61
+ driver?: string;
62
+ /** Deadline de cada consulta (default 5000): vencido ⇒ 503. */
63
+ timeoutMs?: number;
64
+ /**
65
+ * Cliente sobre el que leer (la TRANSACCIÓN del llamante, si la escritura
66
+ * llegó con una — `ScopeTreeWriteOptions.transaction`). Sin esto, la
67
+ * barrera necesitaría una SEGUNDA conexión del pool mientras el consumidor
68
+ * sostiene la suya, y con un pool de 1 (SQLite `:memory:`) eso es un
69
+ * interbloqueo, no una lectura. El precio, declarado: en un motor cuyo
70
+ * aislamiento por defecto congela la foto de las lecturas (InnoDB
71
+ * REPEATABLE READ), un freeze puesto DESPUÉS de abrir esa transacción
72
+ * puede no verse desde dentro — la misma ventana que una escritura que ya
73
+ * pasó su barrera (tester A4), y por eso la promesa publicada es «otro
74
+ * proceso recibe 503», no «ninguna escritura entra».
75
+ */
76
+ client?: {
77
+ from: (table: string) => any;
78
+ };
79
+ }
80
+ /** Lee la fila del freeze (503 clasificado si la base no responde; 503 «migración no aplicada» si no existe). */
81
+ export declare function readFreezeRow(options?: FreezeSqlOptions): Promise<FreezeRow>;
82
+ /** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
83
+ export declare function freezeIsLive(row: FreezeRow, nowMs: number): boolean;
84
+ /**
85
+ * Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
86
+ * `acquire` a la vez acaban en exactamente uno). Devuelve el token, o `null`
87
+ * si hay un freeze vivo de otro dueño (el llamante decide si eso es 423 o un
88
+ * contexto que reconoce).
89
+ */
90
+ export declare function acquireFreeze(input: {
91
+ reason: string;
92
+ holder: string;
93
+ untilMs: number | null;
94
+ nowMs: number;
95
+ }, options?: FreezeSqlOptions): Promise<FreezeToken | null>;
96
+ /**
97
+ * Renueva el lease, CONDICIONAL por token y solo si aún no venció
98
+ * (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
99
+ * otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
100
+ * renovar — nunca «recupera» un freeze que ya no es suyo.
101
+ */
102
+ export declare function renewFreeze(token: FreezeToken, input: {
103
+ untilMs: number;
104
+ nowMs: number;
105
+ }, options?: FreezeSqlOptions): Promise<boolean>;
106
+ /**
107
+ * Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
108
+ * nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
109
+ * estaba vencido o el token ya no era el de la fila, hubo una ventana en la
110
+ * que otros procesos pudieron escribir — es lo que el reporte publica.
111
+ */
112
+ export declare function releaseFreeze(token: FreezeToken, input: {
113
+ nowMs: number;
114
+ }, options?: FreezeSqlOptions): Promise<{
115
+ released: boolean;
116
+ lapsed: boolean;
117
+ }>;
118
+ /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`). */
119
+ export declare function freezeKindOf(holder: string | null): FreezeKind | 'unknown';
120
+ //# sourceMappingURL=freeze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,wIAAwI;AACxI,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAI7C,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAA;AAE9D,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAA;CACf;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;KAAE,CAAA;CAC1C;AAyBD,iHAAiH;AACjH,wBAAsB,aAAa,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmBtF;AAED,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAChF,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA8B7B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACzC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAejD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,SAAS,CAG1E"}
@@ -0,0 +1,172 @@
1
+ import db from '@adonisjs/lucid/services/db';
2
+ import { guardSql } from './shared/backend_guard.js';
3
+ import { AuthorizationBackendError } from './errors.js';
4
+ import { CATALOG_VERSION_TABLE } from './catalog/catalog_cache.js';
5
+ /**
6
+ * El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
7
+ * B + E-analista). Primitivas SQL puras; la policy (quién congela, el
8
+ * anidado, el contexto del operador) vive en el manager.
9
+ *
10
+ * El freeze cuelga de la fila `id = 2` de `authz_catalog_version` — la señal
11
+ * entre procesos que el paquete ya usa (invariante 14). La colocación está
12
+ * MEDIDA (panel 3, analista §4.1): en la fila `id = 1` la renovación del
13
+ * lease espera al `FOR UPDATE` de `withAuthzCatalogWrite` (816 ms en PG /
14
+ * 805 en MySQL con un sync de 800 ms); en su propia fila, 10-12 ms. Y la
15
+ * consulta de la barrera es PROPIA y sin memo (juez C2): colgarse del memo
16
+ * del catálogo haría del freeze «una ventana acotada de freeze que todavía
17
+ * no aplica» con `catalogRevalidate: { everyMs }`. El coste que se publica
18
+ * es el medido: +0,14 ms p50 por escritura (PG; +0,11 MySQL), 0 por
19
+ * `authorize`.
20
+ *
21
+ * Las tres reglas del protocolo:
22
+ * - **Token de dueño** (`fence` + `holder`): `acquire` sube el fence y solo
23
+ * hay UN freeze vivo a la vez; `release`/`renew` son condicionales por
24
+ * token, así que un `unfreeze` ajeno o rezagado es un no-op (auditor
25
+ * A1.3: el `finally` de una ventana interior ya no puede levantar la
26
+ * barrera de otra).
27
+ * - **Lease**: `untilMs` en ms de PARED (el reloj del config, el mismo que
28
+ * decide caducidades). Vencido ⇒ el freeze deja de estar vivo y la flota
29
+ * vuelve a escribir SOLA — tras un `SIGKILL` nadie limpia nada (medido:
30
+ * 2 527 ms con lease de 3 000). `untilMs: null` = sin caducidad (la
31
+ * ventana del operador).
32
+ * - **Fence publicado**: la renovación exige `freeze_until_ms > now`; una
33
+ * renovación que toca 0 filas significa que el lease se PERDIÓ a mitad
34
+ * (pausa de GC, base caída más tiempo que el lease) y quien lo sostiene
35
+ * lo marca `lapsed` — la pasada no se certifica (juez C4).
36
+ *
37
+ * La fila ausente (o las columnas: una base migrada con el stub < 2.3) es
38
+ * **503 «migración 2.0 no aplicada»** en toda escritura, jamás «no
39
+ * congelado»: el mismo patrón que la fila `id = 1` del memo. Un upsert
40
+ * perezoso desde la barrera sería una escritura en el camino de lectura,
41
+ * fuera de `withAuthzCatalogWrite`, y una carrera gratis.
42
+ */
43
+ /** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
44
+ export const FREEZE_ROW_ID = 2;
45
+ /** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
46
+ export const DEFAULT_FREEZE_LEASE_MS = 15_000;
47
+ const DEFAULT_FREEZE_TIMEOUT_MS = 5_000;
48
+ function opts(options) {
49
+ return { driver: options.driver ?? 'freeze', timeoutMs: options.timeoutMs ?? DEFAULT_FREEZE_TIMEOUT_MS };
50
+ }
51
+ function missingRow(driver) {
52
+ const error = new AuthorizationBackendError(driver, 'freeze.row', new Error(`no hay fila id = ${FREEZE_ROW_ID} en ${CATALOG_VERSION_TABLE}`));
53
+ error.message =
54
+ `${driver}: no se puede leer el freeze compartido (${CATALOG_VERSION_TABLE}, id = ${FREEZE_ROW_ID}). ` +
55
+ `Probablemente la migración 2.0 no está aplicada (la siembra el stub publicado; la receta está en el CHANGELOG). ` +
56
+ `Sin la fila las escrituras NO proceden: tratarla como «no congelado» sería fail-open.`;
57
+ return error;
58
+ }
59
+ function toMs(raw) {
60
+ if (raw === null || raw === undefined)
61
+ return null;
62
+ const value = typeof raw === 'number' ? raw : Number(raw);
63
+ return Number.isFinite(value) ? value : null;
64
+ }
65
+ /** Lee la fila del freeze (503 clasificado si la base no responde; 503 «migración no aplicada» si no existe). */
66
+ export async function readFreezeRow(options = {}) {
67
+ const { driver, timeoutMs } = opts(options);
68
+ const client = options.client ?? db;
69
+ const rows = await guardSql(driver, 'freeze.read', timeoutMs, () => client
70
+ .from(CATALOG_VERSION_TABLE)
71
+ .where('id', FREEZE_ROW_ID)
72
+ .select('freeze_reason', 'freeze_holder', 'freeze_until_ms', 'freeze_fence'));
73
+ if (rows.length === 0)
74
+ throw missingRow(driver);
75
+ const row = rows[0];
76
+ const fence = toMs(row.freeze_fence);
77
+ if (fence === null)
78
+ throw missingRow(driver);
79
+ return {
80
+ reason: row.freeze_reason === null || row.freeze_reason === undefined ? null : String(row.freeze_reason),
81
+ holder: row.freeze_holder === null || row.freeze_holder === undefined ? null : String(row.freeze_holder),
82
+ untilMs: toMs(row.freeze_until_ms),
83
+ fence,
84
+ };
85
+ }
86
+ /** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
87
+ export function freezeIsLive(row, nowMs) {
88
+ if (row.reason === null)
89
+ return false;
90
+ return row.untilMs === null || row.untilMs > nowMs;
91
+ }
92
+ /**
93
+ * Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
94
+ * `acquire` a la vez acaban en exactamente uno). Devuelve el token, o `null`
95
+ * si hay un freeze vivo de otro dueño (el llamante decide si eso es 423 o un
96
+ * contexto que reconoce).
97
+ */
98
+ export async function acquireFreeze(input, options = {}) {
99
+ const { driver, timeoutMs } = opts(options);
100
+ const updated = await guardSql(driver, 'freeze.acquire', timeoutMs, () => db
101
+ .from(CATALOG_VERSION_TABLE)
102
+ .where('id', FREEZE_ROW_ID)
103
+ .where((q) => {
104
+ // Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma).
105
+ q.whereNull('freeze_reason').orWhere((expired) => {
106
+ expired.whereNotNull('freeze_until_ms').andWhere('freeze_until_ms', '<=', input.nowMs);
107
+ });
108
+ })
109
+ .update({
110
+ freeze_reason: input.reason,
111
+ freeze_holder: input.holder,
112
+ freeze_until_ms: input.untilMs,
113
+ freeze_fence: db.raw('freeze_fence + 1'),
114
+ updated_at: new Date(input.nowMs),
115
+ }));
116
+ if (Number(updated) === 0) {
117
+ // O hay un freeze vivo, o la fila no existe: distinguirlo es leerla.
118
+ const row = await readFreezeRow(options);
119
+ if (freezeIsLive(row, input.nowMs))
120
+ return null;
121
+ // Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta una vez.
122
+ return acquireFreeze(input, options);
123
+ }
124
+ const row = await readFreezeRow(options);
125
+ if (row.holder !== input.holder)
126
+ return null; // otro acquire ganó justo después (no debería: el nuestro dejó la fila viva)
127
+ return { fence: row.fence, holder: input.holder };
128
+ }
129
+ /**
130
+ * Renueva el lease, CONDICIONAL por token y solo si aún no venció
131
+ * (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
132
+ * otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
133
+ * renovar — nunca «recupera» un freeze que ya no es suyo.
134
+ */
135
+ export async function renewFreeze(token, input, options = {}) {
136
+ const { driver, timeoutMs } = opts(options);
137
+ const updated = await guardSql(driver, 'freeze.renew', timeoutMs, () => db
138
+ .from(CATALOG_VERSION_TABLE)
139
+ .where('id', FREEZE_ROW_ID)
140
+ .where('freeze_fence', token.fence)
141
+ .where('freeze_holder', token.holder)
142
+ .where('freeze_until_ms', '>', input.nowMs)
143
+ .update({ freeze_until_ms: input.untilMs, updated_at: new Date(input.nowMs) }));
144
+ return Number(updated) > 0;
145
+ }
146
+ /**
147
+ * Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
148
+ * nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
149
+ * estaba vencido o el token ya no era el de la fila, hubo una ventana en la
150
+ * que otros procesos pudieron escribir — es lo que el reporte publica.
151
+ */
152
+ export async function releaseFreeze(token, input, options = {}) {
153
+ const { driver, timeoutMs } = opts(options);
154
+ const row = await readFreezeRow(options);
155
+ const mine = row.fence === token.fence && row.holder === token.holder;
156
+ const lapsed = !mine || (row.untilMs !== null && row.untilMs <= input.nowMs);
157
+ if (!mine)
158
+ return { released: false, lapsed };
159
+ const updated = await guardSql(driver, 'freeze.release', timeoutMs, () => db
160
+ .from(CATALOG_VERSION_TABLE)
161
+ .where('id', FREEZE_ROW_ID)
162
+ .where('freeze_fence', token.fence)
163
+ .where('freeze_holder', token.holder)
164
+ .update({ freeze_reason: null, freeze_holder: null, freeze_until_ms: null, updated_at: new Date(input.nowMs) }));
165
+ return { released: Number(updated) > 0, lapsed };
166
+ }
167
+ /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`). */
168
+ export function freezeKindOf(holder) {
169
+ const prefix = holder?.split(':', 1)[0];
170
+ return prefix === 'operator' || prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
171
+ }
172
+ //# sourceMappingURL=freeze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { ScopeChainResolver, ScopeRef } from './types.js';
2
+ /**
3
+ * Un nodo del árbol del consumidor, tal como está en SU tabla (2.5-B · K1):
4
+ * - `self`: el propio scope CANÓNICO (la fila leída: su `type` y su `uuid`
5
+ * tal como están guardados, no tal como llegó la pregunta);
6
+ * - `parent`: el padre directo (`ScopeRef`; si es `app`, la cadena termina
7
+ * ahí), o `null` si el nodo es de primer nivel ⇒ cuelga de `app`.
8
+ */
9
+ export interface ScopeNode {
10
+ self: ScopeRef;
11
+ parent: ScopeRef | null;
12
+ }
13
+ /**
14
+ * `nodeOf` del consumidor: la fila de un scope.
15
+ * - `ScopeNode`: el nodo existe; `self` es su identidad canónica y `parent`
16
+ * su padre (`null` = primer nivel);
17
+ * - `undefined`: el scope NO existe ⇒ el resolutor devuelve `null` y el
18
+ * motor deniega/rechaza escribir (L0.3). Vale también a mitad de cadena:
19
+ * un hijo cuyo padre ya no existe es un árbol roto, no un nodo de `app`.
20
+ * Si lanza, el error se propaga tal cual y el driver lo clasifica como 503
21
+ * `E_AUTHZ_RESOLVER_FAILED` (nunca `[]`, nunca `false`). Un `self` o un
22
+ * padre mal formado (`{app, uuid}`, tipo en mayúsculas…) es 503
23
+ * `E_AUTHZ_RESOLVER_FAILED` del propio resolutor (F6): nunca se normaliza en
24
+ * silencio. Un `self` que no sea el scope preguntado (otro tipo, o un uuid
25
+ * que no es el mismo id salvo mayúsculas/guiones) también es 503: la fila
26
+ * que devuelves tiene que ser la del scope que se te pidió.
27
+ *
28
+ * Por qué devuelve el nodo entero y no solo el padre (`parentOf` de 2.1): con
29
+ * el tipo `uuid` de PostgreSQL o una collation `*_ci`, `SELECT … WHERE uuid =
30
+ * ?` encuentra la fila para un ALIAS del id (mayúsculas, sin guiones); si el
31
+ * resolutor solo devolvía el padre, la cadena se construía con el alias del
32
+ * llamante y los hechos —guardados canónicos— no casaban (el deny se evadía).
33
+ */
34
+ export type NodeOf = (scope: ScopeRef) => Promise<ScopeNode | undefined>;
35
+ export interface HierarchicalResolverOptions {
36
+ nodeOf: NodeOf;
37
+ /**
38
+ * Número máximo de ANCESTROS (`app` incluida; el propio scope no cuenta).
39
+ * Default 64. Superado ⇒ lanza 500 `E_AUTHZ_SCOPE_TOO_DEEP`: truncar
40
+ * devolvería una cadena sin la raíz y el deny de `app` dejaría de verse
41
+ * (fail-open).
42
+ */
43
+ maxDepth?: number;
44
+ }
45
+ /**
46
+ * Construye un `ScopeChainResolver` a partir de un `nodeOf` (2.1, B4; 2.5-B ·
47
+ * K1): lee el nodo (canónico), sube padre a padre hasta `app`, con conjunto
48
+ * de visitados (un ciclo lanza 422 `E_AUTHZ_SCOPE_CYCLE`: FGA evaluaría el
49
+ * ciclo y un grant en cualquier nodo concedería en la raíz, S2) y cota de
50
+ * profundidad que lanza. Cuesta una llamada a `nodeOf` por nivel: envuélvelo
51
+ * con `memoizeAncestors` (o usa `forRequest()`) para que una request no lo
52
+ * pague por cada pregunta. La cadena que devuelve es `[self canónico,
53
+ * ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
54
+ */
55
+ export declare function hierarchicalScopeResolver(options: HierarchicalResolverOptions): ScopeChainResolver;
56
+ //# sourceMappingURL=hierarchical_resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hierarchical_resolver.d.ts","sourceRoot":"","sources":["../../src/hierarchical_resolver.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAA;AAExE,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,2BAA2B,GAAG,kBAAkB,CA8ElG"}
@@ -0,0 +1,87 @@
1
+ import { ScopeCycleError, ScopeResolverError, ScopeTooDeepError } from './errors.js';
2
+ import { assertScope } from './identity.js';
3
+ import { APP_SCOPE, APP_SCOPE_TYPE } from './types.js';
4
+ /**
5
+ * Construye un `ScopeChainResolver` a partir de un `nodeOf` (2.1, B4; 2.5-B ·
6
+ * K1): lee el nodo (canónico), sube padre a padre hasta `app`, con conjunto
7
+ * de visitados (un ciclo lanza 422 `E_AUTHZ_SCOPE_CYCLE`: FGA evaluaría el
8
+ * ciclo y un grant en cualquier nodo concedería en la raíz, S2) y cota de
9
+ * profundidad que lanza. Cuesta una llamada a `nodeOf` por nivel: envuélvelo
10
+ * con `memoizeAncestors` (o usa `forRequest()`) para que una request no lo
11
+ * pague por cada pregunta. La cadena que devuelve es `[self canónico,
12
+ * ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
13
+ */
14
+ export function hierarchicalScopeResolver(options) {
15
+ const { nodeOf } = options;
16
+ const maxDepth = options.maxDepth ?? 64;
17
+ if (typeof nodeOf !== 'function') {
18
+ throw new TypeError('hierarchicalScopeResolver: nodeOf debe ser una función');
19
+ }
20
+ if (!Number.isInteger(maxDepth) || maxDepth < 1) {
21
+ throw new TypeError(`hierarchicalScopeResolver: maxDepth debe ser un entero >= 1 (llegó ${String(maxDepth)})`);
22
+ }
23
+ // Separador no imprimible, escrito como escape (nunca como carácter
24
+ // literal): sin él `org`+`a-1` y `o`+`rga-1` colisionaban y un árbol
25
+ // legítimo era un 422 falso de ciclo (F6, CR4).
26
+ const key = (s) => `${s.type}\u001f${s.uuid ?? ''}`;
27
+ const fold = (uuid) => (uuid ?? '').toLowerCase().replaceAll('-', '');
28
+ return async (scope) => {
29
+ if (scope.type === APP_SCOPE_TYPE)
30
+ return [APP_SCOPE];
31
+ const label = `hierarchicalScopeResolver(${scope.type}:${scope.uuid ?? ''})`;
32
+ const chain = [];
33
+ const visited = new Set();
34
+ let asked = scope;
35
+ for (;;) {
36
+ const node = await nodeOf(asked);
37
+ if (node === undefined)
38
+ return null;
39
+ // La RESPUESTA del árbol se valida (D13, F6, K1): un `self` o un padre
40
+ // mal formado no es un nodo, es un fallo de la dependencia ⇒ 503 con la
41
+ // causa; y `self` tiene que ser la fila del scope que se pidió.
42
+ if (!node || typeof node !== 'object' || !('self' in node) || !('parent' in node)) {
43
+ throw new ScopeResolverError(label, new TypeError(`nodeOf devolvió ${typeof node} en vez de { self, parent } | undefined`));
44
+ }
45
+ try {
46
+ assertScope(node.self);
47
+ }
48
+ catch (error) {
49
+ throw new ScopeResolverError(label, error);
50
+ }
51
+ if (node.self.type !== asked.type || fold(node.self.uuid) !== fold(asked.uuid)) {
52
+ throw new ScopeResolverError(label, new TypeError(`nodeOf(${asked.type}:${asked.uuid ?? ''}) devolvió self = ${node.self.type}:${node.self.uuid ?? ''}: no es la fila del scope pedido`));
53
+ }
54
+ const self = node.self;
55
+ const k = key(self);
56
+ if (visited.has(k)) {
57
+ throw new ScopeCycleError(`hierarchicalScopeResolver: ciclo al resolver ${scope.type}:${scope.uuid} — ` +
58
+ `${self.type}:${self.uuid} ya está en la cadena (${chain.map((s) => `${s.type}:${s.uuid}`).join(' → ') || 'vacía'})`);
59
+ }
60
+ visited.add(k);
61
+ chain.push(self);
62
+ // Los ancestros serán (nodos leídos − 1) + `app` = nodos leídos: si ya
63
+ // superan la cota, se lanza ahora — nunca se devuelve una cadena corta.
64
+ if (chain.length > maxDepth) {
65
+ throw new ScopeTooDeepError(`hierarchicalScopeResolver: la cadena de ${scope.type}:${scope.uuid} supera maxDepth=${maxDepth} ` +
66
+ `(truncarla dejaría fuera la raíz y sus denies)`);
67
+ }
68
+ const parent = node.parent;
69
+ if (parent === null) {
70
+ chain.push(APP_SCOPE);
71
+ return chain;
72
+ }
73
+ try {
74
+ assertScope(parent);
75
+ }
76
+ catch (error) {
77
+ throw new ScopeResolverError(label, error);
78
+ }
79
+ if (parent.type === APP_SCOPE_TYPE) {
80
+ chain.push(APP_SCOPE);
81
+ return chain;
82
+ }
83
+ asked = parent;
84
+ }
85
+ };
86
+ }
87
+ //# sourceMappingURL=hierarchical_resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hierarchical_resolver.js","sourceRoot":"","sources":["../../src/hierarchical_resolver.ts"],"names":[],"mappings":"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"}
@@ -7,17 +7,19 @@ import type { NextFn } from '@adonisjs/core/types/http';
7
7
  *
8
8
  * Reglas:
9
9
  * - No autenticado → 401.
10
- * - Holder sin el rol/permiso requerido a nivel app → 403.
10
+ * - Holder sin el permiso requerido a nivel app → 403.
11
11
  * - El ACL decide por IDENTIDAD; el scope del token lo acota
12
12
  * middleware.ability() aparte (el token nunca amplía, solo restringe).
13
+ * - Solo `permission`. `{ role }` se retiró (L0.6): `hasRole` es membresía
14
+ * y el deny no la gobierna, así que un PEP sobre un rol era indenegable.
15
+ * Toda decisión de acceso pasa por `authorize`.
13
16
  *
14
17
  * Uso: middleware.appAccess({ permission: 'audit:read' })
15
- * middleware.appAccess({ role: 'superadmin' })
16
18
  */
19
+ export interface AppAccessOptions {
20
+ permission: string;
21
+ }
17
22
  export default class AppAccessMiddleware {
18
- handle(ctx: HttpContext, next: NextFn, options?: {
19
- permission?: string;
20
- role?: string;
21
- }): Promise<any>;
23
+ handle(ctx: HttpContext, next: NextFn, options: AppAccessOptions): Promise<any>;
22
24
  }
23
25
  //# sourceMappingURL=app_access_middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app_access_middleware.d.ts","sourceRoot":"","sources":["../../../src/http/app_access_middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAMvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAChC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB;CAgDvE"}
@@ -1,24 +1,16 @@
1
1
  import { Exception } from '@adonisjs/core/exceptions';
2
2
  import authorization from '../../services/main.js';
3
3
  import { APP_SCOPE } from '../types.js';
4
- /**
5
- * Enforcement del ACL de NIVEL APP (gemelo de orgAccess para el scope
6
- * global). Debe ir DESPUÉS de `middleware.auth(...)` — típicamente con el
7
- * guard `admins`, aunque funciona con cualquier holder con morph name.
8
- *
9
- * Reglas:
10
- * - No autenticado → 401.
11
- * - Holder sin el rol/permiso requerido a nivel app → 403.
12
- * - El ACL decide por IDENTIDAD; el scope del token lo acota
13
- * middleware.ability() aparte (el token nunca amplía, solo restringe).
14
- *
15
- * Uso: middleware.appAccess({ permission: 'audit:read' })
16
- * middleware.appAccess({ role: 'superadmin' })
17
- */
4
+ import { AuthorizationConfigError, RoleIsNotAccessError } from '../errors.js';
18
5
  export default class AppAccessMiddleware {
19
- async handle(ctx, next, options = {}) {
20
- if (!options.permission && !options.role) {
21
- throw new Exception('middleware.appAccess requiere `permission` o `role`', { status: 500 });
6
+ async handle(ctx, next, options) {
7
+ // Antes de mirar si hay usuario: una ruta mal declarada tiene que saltar
8
+ // en el primer request de desarrollo, autenticado o no.
9
+ if (options && 'role' in options) {
10
+ throw new RoleIsNotAccessError(options.role);
11
+ }
12
+ if (!options?.permission) {
13
+ throw new AuthorizationConfigError("middleware.appAccess requiere `{ permission }` (p. ej. appAccess({ permission: 'audit:read' }))");
22
14
  }
23
15
  const holder = ctx.auth?.user;
24
16
  if (!holder) {
@@ -39,15 +31,7 @@ export default class AppAccessMiddleware {
39
31
  `no por la clave primaria del modelo.`, { status: 500 });
40
32
  }
41
33
  const subject = { type: morph, uuid: holder.uuid };
42
- if (options.role && !(await authorization.hasRole(subject, options.role, APP_SCOPE))) {
43
- return ctx.response.forbidden({
44
- message: 'Permiso insuficiente',
45
- statusCode: 403,
46
- errors: [],
47
- });
48
- }
49
- if (options.permission &&
50
- !(await authorization.authorize(subject, options.permission, APP_SCOPE))) {
34
+ if (!(await authorization.authorize(subject, options.permission, APP_SCOPE))) {
51
35
  return ctx.response.forbidden({
52
36
  message: 'Permiso insuficiente',
53
37
  statusCode: 403,
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app_access_middleware.js","sourceRoot":"","sources":["../../../src/http/app_access_middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,aAAa,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAsB7E,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACtC,KAAK,CAAC,MAAM,CAAC,GAAgB,EAAE,IAAY,EAAE,OAAyB;QACpE,yEAAyE;QACzE,wDAAwD;QACxD,IAAI,OAAO,IAAI,MAAM,IAAK,OAAkB,EAAE,CAAC;YAC7C,MAAM,IAAI,oBAAoB,CAAE,OAAe,CAAC,IAAI,CAAC,CAAA;QACvD,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,wBAAwB,CAChC,iGAAiG,CAClG,CAAA;QACH,CAAC;QAED,MAAM,MAAM,GAAI,GAAW,CAAC,IAAI,EAAE,IAA6D,CAAA;QAC/F,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QAC9F,CAAC;QAED,mEAAmE;QACnE,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAA;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,mEAAmE,EAAE;gBACvF,MAAM,EAAE,GAAG;aACZ,CAAC,CAAA;QACJ,CAAC;QAED,kEAAkE;QAClE,wEAAwE;QACxE,yEAAyE;QACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,SAAS,CACjB,cAAc,KAAK,mEAAmE;gBACpF,sCAAsC,EACxC,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAA;QACH,CAAC;QAED,MAAM,OAAO,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;QAElD,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;YAC7E,OAAO,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC5B,OAAO,EAAE,sBAAsB;gBAC/B,UAAU,EAAE,GAAG;gBACf,MAAM,EAAE,EAAE;aACX,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;CACF"}