@jantstack/adonis-authz 2.0.0-alpha.1 → 2.4.0-alpha.10

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 (224) hide show
  1. package/README.md +934 -40
  2. package/build/commands/authz_catalog_diff.d.ts.map +1 -1
  3. package/build/commands/authz_catalog_diff.js +1 -2
  4. package/build/commands/authz_catalog_diff.js.map +1 -1
  5. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  6. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  7. package/build/commands/authz_catalog_prune_orphans.js +135 -0
  8. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  9. package/build/commands/authz_catalog_sync.d.ts +17 -0
  10. package/build/commands/authz_catalog_sync.d.ts.map +1 -1
  11. package/build/commands/authz_catalog_sync.js +27 -4
  12. package/build/commands/authz_catalog_sync.js.map +1 -1
  13. package/build/commands/authz_freeze.d.ts +44 -0
  14. package/build/commands/authz_freeze.d.ts.map +1 -0
  15. package/build/commands/authz_freeze.js +94 -0
  16. package/build/commands/authz_freeze.js.map +1 -0
  17. package/build/commands/authz_reconcile.d.ts +111 -0
  18. package/build/commands/authz_reconcile.d.ts.map +1 -0
  19. package/build/commands/authz_reconcile.js +417 -0
  20. package/build/commands/authz_reconcile.js.map +1 -0
  21. package/build/commands/authz_relations_reconcile.d.ts +144 -0
  22. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  23. package/build/commands/authz_relations_reconcile.js +438 -0
  24. package/build/commands/authz_relations_reconcile.js.map +1 -0
  25. package/build/commands/authz_scopes_relay.d.ts +97 -0
  26. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  27. package/build/commands/authz_scopes_relay.js +344 -0
  28. package/build/commands/authz_scopes_relay.js.map +1 -0
  29. package/build/commands/authz_scopes_requeue.d.ts +80 -0
  30. package/build/commands/authz_scopes_requeue.d.ts.map +1 -0
  31. package/build/commands/authz_scopes_requeue.js +230 -0
  32. package/build/commands/authz_scopes_requeue.js.map +1 -0
  33. package/build/commands/authz_unfreeze.d.ts +45 -0
  34. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  35. package/build/commands/authz_unfreeze.js +157 -0
  36. package/build/commands/authz_unfreeze.js.map +1 -0
  37. package/build/commands/main.d.ts +16 -8
  38. package/build/commands/main.d.ts.map +1 -1
  39. package/build/commands/main.js +59 -6
  40. package/build/commands/main.js.map +1 -1
  41. package/build/commands/openfga_provision.d.ts +92 -4
  42. package/build/commands/openfga_provision.d.ts.map +1 -1
  43. package/build/commands/openfga_provision.js +155 -7
  44. package/build/commands/openfga_provision.js.map +1 -1
  45. package/build/configure.d.ts +11 -0
  46. package/build/configure.d.ts.map +1 -1
  47. package/build/configure.js +37 -1
  48. package/build/configure.js.map +1 -1
  49. package/build/index.d.ts +50 -11
  50. package/build/index.d.ts.map +1 -1
  51. package/build/index.js +49 -7
  52. package/build/index.js.map +1 -1
  53. package/build/providers/authz_provider.d.ts +31 -2
  54. package/build/providers/authz_provider.d.ts.map +1 -1
  55. package/build/providers/authz_provider.js +65 -2
  56. package/build/providers/authz_provider.js.map +1 -1
  57. package/build/services/relations.d.ts +14 -0
  58. package/build/services/relations.d.ts.map +1 -0
  59. package/build/services/relations.js +17 -0
  60. package/build/services/relations.js.map +1 -0
  61. package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
  62. package/build/src/catalog/catalog.d.ts.map +1 -0
  63. package/build/src/{catalog.js → catalog/catalog.js} +120 -14
  64. package/build/src/catalog/catalog.js.map +1 -0
  65. package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
  66. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  67. package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
  68. package/build/src/catalog/catalog_cache.js.map +1 -0
  69. package/build/src/define_config.d.ts +206 -3
  70. package/build/src/define_config.d.ts.map +1 -1
  71. package/build/src/define_config.js +11 -0
  72. package/build/src/define_config.js.map +1 -1
  73. package/build/src/drivers/database_driver.d.ts +128 -7
  74. package/build/src/drivers/database_driver.d.ts.map +1 -1
  75. package/build/src/drivers/database_driver.js +510 -24
  76. package/build/src/drivers/database_driver.js.map +1 -1
  77. package/build/src/drivers/database_relations_driver.d.ts +140 -0
  78. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  79. package/build/src/drivers/database_relations_driver.js +748 -0
  80. package/build/src/drivers/database_relations_driver.js.map +1 -0
  81. package/build/src/drivers/openfga_driver.d.ts +777 -122
  82. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  83. package/build/src/drivers/openfga_driver.js +2228 -477
  84. package/build/src/drivers/openfga_driver.js.map +1 -1
  85. package/build/src/drivers/openfga_facts.d.ts +384 -0
  86. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  87. package/build/src/drivers/openfga_facts.js +836 -0
  88. package/build/src/drivers/openfga_facts.js.map +1 -0
  89. package/build/src/drivers/openfga_relations_driver.d.ts +186 -0
  90. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  91. package/build/src/drivers/openfga_relations_driver.js +724 -0
  92. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  93. package/build/src/errors.d.ts +430 -6
  94. package/build/src/errors.d.ts.map +1 -1
  95. package/build/src/errors.js +453 -6
  96. package/build/src/errors.js.map +1 -1
  97. package/build/src/freeze.d.ts +200 -0
  98. package/build/src/freeze.d.ts.map +1 -0
  99. package/build/src/freeze.js +306 -0
  100. package/build/src/freeze.js.map +1 -0
  101. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  102. package/build/src/http/app_access_middleware.js.map +1 -0
  103. package/build/src/http/resource_access_middleware.d.ts +105 -0
  104. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  105. package/build/src/http/resource_access_middleware.js +81 -0
  106. package/build/src/http/resource_access_middleware.js.map +1 -0
  107. package/build/src/identity.d.ts +74 -1
  108. package/build/src/identity.d.ts.map +1 -1
  109. package/build/src/identity.js +100 -2
  110. package/build/src/identity.js.map +1 -1
  111. package/build/src/manager.d.ts +549 -6
  112. package/build/src/manager.d.ts.map +1 -1
  113. package/build/src/manager.js +2243 -193
  114. package/build/src/manager.js.map +1 -1
  115. package/build/src/models/authz_assignment.d.ts +7 -7
  116. package/build/src/models/authz_assignment.d.ts.map +1 -1
  117. package/build/src/models/authz_deny.d.ts +7 -7
  118. package/build/src/models/authz_deny.d.ts.map +1 -1
  119. package/build/src/models/authz_permission.d.ts +7 -7
  120. package/build/src/models/authz_permission.d.ts.map +1 -1
  121. package/build/src/models/authz_role.d.ts +7 -7
  122. package/build/src/models/authz_role.d.ts.map +1 -1
  123. package/build/src/models/authz_role_permission.d.ts +7 -7
  124. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  125. package/build/src/openfga.d.ts +18 -2
  126. package/build/src/openfga.d.ts.map +1 -1
  127. package/build/src/openfga.js +15 -1
  128. package/build/src/openfga.js.map +1 -1
  129. package/build/src/reconcile.d.ts +37 -0
  130. package/build/src/reconcile.d.ts.map +1 -0
  131. package/build/src/reconcile.js +69 -0
  132. package/build/src/reconcile.js.map +1 -0
  133. package/build/src/relation_partition_trigger.d.ts +8 -0
  134. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  135. package/build/src/relation_partition_trigger.js +93 -0
  136. package/build/src/relation_partition_trigger.js.map +1 -0
  137. package/build/src/relations/define_relations_config.d.ts +109 -0
  138. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  139. package/build/src/relations/define_relations_config.js +211 -0
  140. package/build/src/relations/define_relations_config.js.map +1 -0
  141. package/build/src/relations/manager.d.ts +138 -0
  142. package/build/src/relations/manager.d.ts.map +1 -0
  143. package/build/src/relations/manager.js +494 -0
  144. package/build/src/relations/manager.js.map +1 -0
  145. package/build/src/relations/reconcile.d.ts +137 -0
  146. package/build/src/relations/reconcile.d.ts.map +1 -0
  147. package/build/src/relations/reconcile.js +294 -0
  148. package/build/src/relations/reconcile.js.map +1 -0
  149. package/build/src/relations_config_store.d.ts +22 -0
  150. package/build/src/relations_config_store.d.ts.map +1 -0
  151. package/build/src/relations_config_store.js +74 -0
  152. package/build/src/relations_config_store.js.map +1 -0
  153. package/build/src/scope_outbox.d.ts +123 -0
  154. package/build/src/scope_outbox.d.ts.map +1 -0
  155. package/build/src/scope_outbox.js +532 -0
  156. package/build/src/scope_outbox.js.map +1 -0
  157. package/build/src/{drivers → shared}/backend_guard.d.ts +42 -0
  158. package/build/src/shared/backend_guard.d.ts.map +1 -0
  159. package/build/src/{drivers → shared}/backend_guard.js +78 -1
  160. package/build/src/shared/backend_guard.js.map +1 -0
  161. package/build/src/{drivers → shared}/sql_expiry.d.ts +2 -0
  162. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  163. package/build/src/{drivers → shared}/sql_expiry.js +4 -0
  164. package/build/src/shared/sql_expiry.js.map +1 -0
  165. package/build/src/shared/transaction_guard.d.ts +53 -0
  166. package/build/src/shared/transaction_guard.d.ts.map +1 -0
  167. package/build/src/shared/transaction_guard.js +74 -0
  168. package/build/src/shared/transaction_guard.js.map +1 -0
  169. package/build/src/sql_descendants.d.ts +55 -1
  170. package/build/src/sql_descendants.d.ts.map +1 -1
  171. package/build/src/sql_descendants.js +75 -1
  172. package/build/src/sql_descendants.js.map +1 -1
  173. package/build/src/testing/contract.d.ts +129 -4
  174. package/build/src/testing/contract.d.ts.map +1 -1
  175. package/build/src/testing/contract.js +915 -177
  176. package/build/src/testing/contract.js.map +1 -1
  177. package/build/src/testing/main.d.ts +10 -2
  178. package/build/src/testing/main.d.ts.map +1 -1
  179. package/build/src/testing/main.js +5 -1
  180. package/build/src/testing/main.js.map +1 -1
  181. package/build/src/testing/migration_contract.d.ts +293 -0
  182. package/build/src/testing/migration_contract.d.ts.map +1 -0
  183. package/build/src/testing/migration_contract.js +589 -0
  184. package/build/src/testing/migration_contract.js.map +1 -0
  185. package/build/src/testing/relations_contract.d.ts +73 -0
  186. package/build/src/testing/relations_contract.d.ts.map +1 -0
  187. package/build/src/testing/relations_contract.js +1238 -0
  188. package/build/src/testing/relations_contract.js.map +1 -0
  189. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  190. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  191. package/build/src/testing/relations_reconcile_contract.js +220 -0
  192. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  193. package/build/src/testing/scope_outbox_contract.d.ts +98 -0
  194. package/build/src/testing/scope_outbox_contract.d.ts.map +1 -0
  195. package/build/src/testing/scope_outbox_contract.js +677 -0
  196. package/build/src/testing/scope_outbox_contract.js.map +1 -0
  197. package/build/src/traits/authz_scopes.js +1 -1
  198. package/build/src/traits/authz_scopes.js.map +1 -1
  199. package/build/src/traits/has_uuid.d.ts +8 -8
  200. package/build/src/traits/has_uuid.d.ts.map +1 -1
  201. package/build/src/types.d.ts +1537 -84
  202. package/build/src/types.d.ts.map +1 -1
  203. package/build/src/types.js +65 -0
  204. package/build/src/types.js.map +1 -1
  205. package/build/stubs/config/authorization.stub +149 -5
  206. package/build/stubs/migration.stub +136 -0
  207. package/build/stubs/scopes_outbox_migration.stub +57 -0
  208. package/package.json +4 -2
  209. package/build/commands/openfga_import.d.ts +0 -34
  210. package/build/commands/openfga_import.d.ts.map +0 -1
  211. package/build/commands/openfga_import.js +0 -97
  212. package/build/commands/openfga_import.js.map +0 -1
  213. package/build/src/catalog.d.ts.map +0 -1
  214. package/build/src/catalog.js.map +0 -1
  215. package/build/src/catalog_cache.d.ts.map +0 -1
  216. package/build/src/catalog_cache.js.map +0 -1
  217. package/build/src/drivers/backend_guard.d.ts.map +0 -1
  218. package/build/src/drivers/backend_guard.js.map +0 -1
  219. package/build/src/drivers/sql_expiry.d.ts.map +0 -1
  220. package/build/src/drivers/sql_expiry.js.map +0 -1
  221. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  222. package/build/src/middleware/app_access_middleware.js.map +0 -1
  223. /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
  224. /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
@@ -0,0 +1,200 @@
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 barrera se lee SIEMPRE por la conexión del motor, nunca por un
40
+ * cliente del llamante** (L-1 · 🟠 8, dictamen del juez del panel `{trx}`).
41
+ * Hasta L-1 `readFreezeRow` aceptaba `client` —la transacción del consumidor,
42
+ * «para no interbloquear un pool de 1»— y eso convertía la AUTORIDAD en una
43
+ * decisión del llamante: un cliente que responde «no congelado», o el
44
+ * snapshot REPEATABLE READ de una transacción abierta ANTES del freeze, dejaba
45
+ * entrar la escritura (medido: auditor C1 en SQLite; MySQL/PG en
46
+ * `freeze.spec.ts`). La regla: *la escritura va por tu transacción; la
47
+ * autoridad que decide si puedes escribir, nunca*. El precio, decidido por el
48
+ * dueño: `{ transaction }` exige pool ≥ 2 — con pool 1 la lectura de la
49
+ * barrera espera una conexión que el llamante sostiene y sale **503
50
+ * `E_AUTHZ_BACKEND_TIMEOUT` al vencer SU deadline** (`withDeadline`, porque
51
+ * el `timeout()` de knex NO cubre la espera del pool: medido, 60 s), jamás
52
+ * un cuelgue ni un bypass.
53
+ */
54
+ /** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
55
+ export declare const FREEZE_ROW_ID = 2;
56
+ /** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
57
+ export declare const DEFAULT_FREEZE_LEASE_MS = 15000;
58
+ /** Quién congela: el dueño va escrito en la fila (`<kind>:<pid>:<hex>`). */
59
+ export type FreezeKind = 'operator' | 'reconcile' | 'platform';
60
+ /** El token del dueño: lo devuelve `freeze()` y es lo ÚNICO que levanta o renueva ese freeze. */
61
+ export interface FreezeToken {
62
+ /** Número de generación: sube en cada `acquire`, nunca se reutiliza. */
63
+ fence: number;
64
+ /** `<kind>:<pid>:<hex>`: identifica al proceso que congeló. */
65
+ holder: string;
66
+ }
67
+ /** La fila del freeze, tal como está en la base. */
68
+ export interface FreezeRow {
69
+ reason: string | null;
70
+ holder: string | null;
71
+ untilMs: number | null;
72
+ fence: number;
73
+ }
74
+ export interface FreezeSqlOptions {
75
+ /** Etiqueta del driver en los errores 503 (default `freeze`). */
76
+ driver?: string;
77
+ /**
78
+ * Deadline TOTAL de cada consulta (default 5000), espera del pool incluida:
79
+ * vencido ⇒ 503 `E_AUTHZ_BACKEND_TIMEOUT`. No hay opción para leer por otro
80
+ * cliente: la barrera es autoridad y va por la conexión del motor (L-1).
81
+ */
82
+ timeoutMs?: number;
83
+ }
84
+ /**
85
+ * Lee la fila del freeze **por la conexión del motor** (503 clasificado si la
86
+ * base no responde o el pool no da una conexión en `timeoutMs`; 503 «migración
87
+ * no aplicada» si no existe). No acepta cliente: es la autoridad (L-1 · 🟠 8).
88
+ */
89
+ export declare function readFreezeRow(options?: FreezeSqlOptions): Promise<FreezeRow>;
90
+ /** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
91
+ export declare function freezeIsLive(row: FreezeRow, nowMs: number): boolean;
92
+ export interface FreezeBarrierOptions extends FreezeSqlOptions {
93
+ /** Milisegundos de PARED del reloj del config (el mismo que decide caducidades). */
94
+ nowMs: number;
95
+ }
96
+ /**
97
+ * **La barrera del freeze**, la MISMA para los dos motores (roles y
98
+ * relations, L-1 · J1): lee la fila por la conexión del motor y lanza 503
99
+ * `E_AUTHZ_FROZEN` (reintentable) si hay un freeze vivo en `nowMs`. Va
100
+ * delante de TODA escritura, antes de validar identidades o tocar el driver:
101
+ * durante una ventana una escritura no se valida a medias, se rechaza entera.
102
+ * Nunca recibe un cliente del llamante — ver el docblock del módulo.
103
+ */
104
+ export declare function assertNotFrozenRow(operation: string, options: FreezeBarrierOptions): Promise<void>;
105
+ /** El holder con el que se publica una fila con motivo y SIN holder (legada o a mano); también es su token. */
106
+ export declare const UNKNOWN_HOLDER = "?";
107
+ /** Prefijo del holder de la ventana del OPERADOR (`freeze()` con `kind: 'operator'`). */
108
+ export declare const OPERATOR_HOLDER_PREFIX = "operator:";
109
+ /**
110
+ * `isOperatorHolder` en SQL, por motor (cierre-2 de alpha.10 · C3): los
111
+ * `OPERATOR_HOLDER_PREFIX.length` primeros CARACTERES de `freeze_holder` igual a
112
+ * `operator:`, sensible a mayúsculas y acentos. SQLite: `substr()` no hereda
113
+ * collation ⇒ `BINARY`. PostgreSQL: `COLLATE "C"` (byte a byte aunque la base
114
+ * declare otra). MySQL: la collation de la columna es la de la tabla (`_ai_ci` por
115
+ * defecto: `OPERATOR:` y `opérator:` serían iguales), así que se compara como
116
+ * BINARIO. El prefijo es ASCII, así que «9 caracteres» y «9 unidades UTF-16» (JS)
117
+ * coinciden siempre que la comparación sea verdadera. Sin `LIKE`: ni comodines ni
118
+ * collation. Cualquier otro dialecto ⇒ 500 `E_AUTHZ_UNSUPPORTED_DIALECT` ANTES de
119
+ * tocar la fila (re-ataque del cierre-2 · ⚪ 3: el `substr` de SQLite en MSSQL
120
+ * sería un error del motor envuelto como 503, que invita a reintentar para siempre).
121
+ */
122
+ export declare function operatorHolderSql(connection?: any): {
123
+ sql: string;
124
+ bindings: unknown[];
125
+ };
126
+ /**
127
+ * Qué pasó al intentar tomar el freeze: `acquired` (con el token), `held` (hay
128
+ * un freeze VIVO de otro dueño) o `expired-operator` (la fila es una ventana de
129
+ * OPERADOR con el lease vencido, que no se pisa). `row` es la fila releída para
130
+ * REDACTAR el error; la decisión ya la tomó el UPDATE.
131
+ */
132
+ export type FreezeAcquireOutcome = {
133
+ status: 'acquired';
134
+ token: FreezeToken;
135
+ } | {
136
+ status: 'held';
137
+ row: FreezeRow;
138
+ } | {
139
+ status: 'expired-operator';
140
+ row: FreezeRow;
141
+ };
142
+ /**
143
+ * Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
144
+ * `acquire` a la vez acaban en exactamente uno). Una fila con el lease VENCIDO
145
+ * se toma encima (el dueño murió: la flota vuelve sola) **salvo que sea una
146
+ * ventana de OPERADOR** (cierre del auditor de alpha.10 · R2): esa exclusión va
147
+ * DENTRO del `WHERE` del mismo `UPDATE`, en los tres motores. Hasta el cierre
148
+ * vivía en `freeze()` como «leo la fila, compruebo, tomo», y con un reloj que
149
+ * cruzaba el vencimiento entre la comprobación y el UPDATE la pasada pisaba la
150
+ * ventana del operador sin 503 — el TOCTOU que el atómico existe para cerrar.
151
+ */
152
+ export declare function acquireFreezeOutcome(input: {
153
+ reason: string;
154
+ holder: string;
155
+ untilMs: number | null;
156
+ nowMs: number;
157
+ }, options?: FreezeSqlOptions, attempt?: number): Promise<FreezeAcquireOutcome>;
158
+ /** `acquireFreezeOutcome` reducido a «el token o `null`» (lo usan los casos que siembran una fila ajena). */
159
+ export declare function acquireFreeze(input: {
160
+ reason: string;
161
+ holder: string;
162
+ untilMs: number | null;
163
+ nowMs: number;
164
+ }, options?: FreezeSqlOptions): Promise<FreezeToken | null>;
165
+ /**
166
+ * Renueva el lease, CONDICIONAL por token y solo si aún no venció
167
+ * (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
168
+ * otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
169
+ * renovar — nunca «recupera» un freeze que ya no es suyo.
170
+ */
171
+ export declare function renewFreeze(token: FreezeToken, input: {
172
+ untilMs: number;
173
+ nowMs: number;
174
+ }, options?: FreezeSqlOptions): Promise<boolean>;
175
+ /**
176
+ * Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
177
+ * nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
178
+ * estaba vencido o el token ya no era el de la fila, hubo una ventana en la
179
+ * que otros procesos pudieron escribir — es lo que el reporte publica.
180
+ */
181
+ export declare function releaseFreeze(token: FreezeToken, input: {
182
+ nowMs: number;
183
+ }, options?: FreezeSqlOptions): Promise<{
184
+ released: boolean;
185
+ lapsed: boolean;
186
+ }>;
187
+ /**
188
+ * **La regla ÚNICA del prefijo de operador** (cierre-2 de alpha.10 · C3): los
189
+ * `OPERATOR_HOLDER_PREFIX.length` primeros caracteres del holder son EXACTAMENTE
190
+ * `operator:` — sensible a mayúsculas y a acentos, sin comodines. La MISMA
191
+ * comparación va en el `WHERE` de `acquireFreezeOutcome` (`operatorHolderSql`) y
192
+ * aquí; hasta el cierre-2 el SQL usaba `LIKE` (insensible a mayúsculas en SQLite y
193
+ * en MySQL `_ci`) y JS partía por `:`, así que un holder `OPERATOR:…` caducado era
194
+ * «de operador» para el UPDATE y `unknown` para JS ⇒ 423 «VIVO» sobre una fila
195
+ * caducada, distinto por motor.
196
+ */
197
+ export declare function isOperatorHolder(holder: string | null): boolean;
198
+ /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
199
+ export declare function freezeKindOf(holder: string | null): FreezeKind | 'unknown';
200
+ //# sourceMappingURL=freeze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,306 @@
1
+ import db from '@adonisjs/lucid/services/db';
2
+ import { guardSql, withDeadline } from './shared/backend_guard.js';
3
+ import { AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, UnsupportedDialectError, } from './errors.js';
4
+ import { CATALOG_VERSION_TABLE } from './catalog/catalog_cache.js';
5
+ import { dialectOf, isMysqlDialect, isSqliteDialect } from './shared/sql_expiry.js';
6
+ /**
7
+ * El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
8
+ * B + E-analista). Primitivas SQL puras; la policy (quién congela, el
9
+ * anidado, el contexto del operador) vive en el manager.
10
+ *
11
+ * El freeze cuelga de la fila `id = 2` de `authz_catalog_version` — la señal
12
+ * entre procesos que el paquete ya usa (invariante 14). La colocación está
13
+ * MEDIDA (panel 3, analista §4.1): en la fila `id = 1` la renovación del
14
+ * lease espera al `FOR UPDATE` de `withAuthzCatalogWrite` (816 ms en PG /
15
+ * 805 en MySQL con un sync de 800 ms); en su propia fila, 10-12 ms. Y la
16
+ * consulta de la barrera es PROPIA y sin memo (juez C2): colgarse del memo
17
+ * del catálogo haría del freeze «una ventana acotada de freeze que todavía
18
+ * no aplica» con `catalogRevalidate: { everyMs }`. El coste que se publica
19
+ * es el medido: +0,14 ms p50 por escritura (PG; +0,11 MySQL), 0 por
20
+ * `authorize`.
21
+ *
22
+ * Las tres reglas del protocolo:
23
+ * - **Token de dueño** (`fence` + `holder`): `acquire` sube el fence y solo
24
+ * hay UN freeze vivo a la vez; `release`/`renew` son condicionales por
25
+ * token, así que un `unfreeze` ajeno o rezagado es un no-op (auditor
26
+ * A1.3: el `finally` de una ventana interior ya no puede levantar la
27
+ * barrera de otra).
28
+ * - **Lease**: `untilMs` en ms de PARED (el reloj del config, el mismo que
29
+ * decide caducidades). Vencido ⇒ el freeze deja de estar vivo y la flota
30
+ * vuelve a escribir SOLA — tras un `SIGKILL` nadie limpia nada (medido:
31
+ * 2 527 ms con lease de 3 000). `untilMs: null` = sin caducidad (la
32
+ * ventana del operador).
33
+ * - **Fence publicado**: la renovación exige `freeze_until_ms > now`; una
34
+ * renovación que toca 0 filas significa que el lease se PERDIÓ a mitad
35
+ * (pausa de GC, base caída más tiempo que el lease) y quien lo sostiene
36
+ * lo marca `lapsed` — la pasada no se certifica (juez C4).
37
+ *
38
+ * La fila ausente (o las columnas: una base migrada con el stub < 2.3) es
39
+ * **503 «migración 2.0 no aplicada»** en toda escritura, jamás «no
40
+ * congelado»: el mismo patrón que la fila `id = 1` del memo. Un upsert
41
+ * perezoso desde la barrera sería una escritura en el camino de lectura,
42
+ * fuera de `withAuthzCatalogWrite`, y una carrera gratis.
43
+ *
44
+ * **La barrera se lee SIEMPRE por la conexión del motor, nunca por un
45
+ * cliente del llamante** (L-1 · 🟠 8, dictamen del juez del panel `{trx}`).
46
+ * Hasta L-1 `readFreezeRow` aceptaba `client` —la transacción del consumidor,
47
+ * «para no interbloquear un pool de 1»— y eso convertía la AUTORIDAD en una
48
+ * decisión del llamante: un cliente que responde «no congelado», o el
49
+ * snapshot REPEATABLE READ de una transacción abierta ANTES del freeze, dejaba
50
+ * entrar la escritura (medido: auditor C1 en SQLite; MySQL/PG en
51
+ * `freeze.spec.ts`). La regla: *la escritura va por tu transacción; la
52
+ * autoridad que decide si puedes escribir, nunca*. El precio, decidido por el
53
+ * dueño: `{ transaction }` exige pool ≥ 2 — con pool 1 la lectura de la
54
+ * barrera espera una conexión que el llamante sostiene y sale **503
55
+ * `E_AUTHZ_BACKEND_TIMEOUT` al vencer SU deadline** (`withDeadline`, porque
56
+ * el `timeout()` de knex NO cubre la espera del pool: medido, 60 s), jamás
57
+ * un cuelgue ni un bypass.
58
+ */
59
+ /** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
60
+ export const FREEZE_ROW_ID = 2;
61
+ /** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
62
+ export const DEFAULT_FREEZE_LEASE_MS = 15_000;
63
+ const DEFAULT_FREEZE_TIMEOUT_MS = 5_000;
64
+ function opts(options) {
65
+ return { driver: options.driver ?? 'freeze', timeoutMs: options.timeoutMs ?? DEFAULT_FREEZE_TIMEOUT_MS };
66
+ }
67
+ /**
68
+ * Una consulta del freeze con deadline TOTAL. `guardSql` fija el `timeout()`
69
+ * de knex, que solo cuenta desde que la consulta tiene conexión: si el pool
70
+ * está agotado (el llamante sostiene la única conexión de `:memory:`), la
71
+ * espera la gobierna `acquireConnectionTimeout` (60 s por defecto) — medido,
72
+ * L-1. `withDeadline` cubre también esa espera, así que con pool 1 la
73
+ * barrera responde 503 en `timeoutMs`, no en un minuto.
74
+ */
75
+ function freezeSql(driver, operation, timeoutMs, fn) {
76
+ return withDeadline(guardSql(driver, operation, timeoutMs, fn), timeoutMs, () => new AuthorizationBackendTimeoutError(driver, operation, timeoutMs));
77
+ }
78
+ function missingRow(driver) {
79
+ const error = new AuthorizationBackendError(driver, 'freeze.row', new Error(`no hay fila id = ${FREEZE_ROW_ID} en ${CATALOG_VERSION_TABLE}`));
80
+ error.message =
81
+ `${driver}: no se puede leer el freeze compartido (${CATALOG_VERSION_TABLE}, id = ${FREEZE_ROW_ID}). ` +
82
+ `Probablemente la migración 2.0 no está aplicada (la siembra el stub publicado; la receta está en el CHANGELOG). ` +
83
+ `Sin la fila las escrituras NO proceden: tratarla como «no congelado» sería fail-open.`;
84
+ return error;
85
+ }
86
+ function toMs(raw) {
87
+ if (raw === null || raw === undefined)
88
+ return null;
89
+ const value = typeof raw === 'number' ? raw : Number(raw);
90
+ return Number.isFinite(value) ? value : null;
91
+ }
92
+ /**
93
+ * Lee la fila del freeze **por la conexión del motor** (503 clasificado si la
94
+ * base no responde o el pool no da una conexión en `timeoutMs`; 503 «migración
95
+ * no aplicada» si no existe). No acepta cliente: es la autoridad (L-1 · 🟠 8).
96
+ */
97
+ export async function readFreezeRow(options = {}) {
98
+ const { driver, timeoutMs } = opts(options);
99
+ const rows = await freezeSql(driver, 'freeze.read', timeoutMs, () => db
100
+ .from(CATALOG_VERSION_TABLE)
101
+ .where('id', FREEZE_ROW_ID)
102
+ .select('freeze_reason', 'freeze_holder', 'freeze_until_ms', 'freeze_fence'));
103
+ if (rows.length === 0)
104
+ throw missingRow(driver);
105
+ const row = rows[0];
106
+ const fence = toMs(row.freeze_fence);
107
+ if (fence === null)
108
+ throw missingRow(driver);
109
+ return {
110
+ reason: row.freeze_reason === null || row.freeze_reason === undefined ? null : String(row.freeze_reason),
111
+ holder: row.freeze_holder === null || row.freeze_holder === undefined ? null : String(row.freeze_holder),
112
+ untilMs: toMs(row.freeze_until_ms),
113
+ fence,
114
+ };
115
+ }
116
+ /** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
117
+ export function freezeIsLive(row, nowMs) {
118
+ if (row.reason === null)
119
+ return false;
120
+ return row.untilMs === null || row.untilMs > nowMs;
121
+ }
122
+ /**
123
+ * **La barrera del freeze**, la MISMA para los dos motores (roles y
124
+ * relations, L-1 · J1): lee la fila por la conexión del motor y lanza 503
125
+ * `E_AUTHZ_FROZEN` (reintentable) si hay un freeze vivo en `nowMs`. Va
126
+ * delante de TODA escritura, antes de validar identidades o tocar el driver:
127
+ * durante una ventana una escritura no se valida a medias, se rechaza entera.
128
+ * Nunca recibe un cliente del llamante — ver el docblock del módulo.
129
+ */
130
+ export async function assertNotFrozenRow(operation, options) {
131
+ const row = await readFreezeRow(options);
132
+ if (!freezeIsLive(row, options.nowMs))
133
+ return;
134
+ const lift = freezeKindOf(row.holder) === 'operator' ? ' (la levanta authz:unfreeze)' : '';
135
+ throw new AuthorizationFrozenError(`${operation}: el motor de autorización está congelado (${row.reason})${lift} y no acepta escrituras. ` +
136
+ `Las lecturas siguen funcionando; reintenta esta escritura cuando la ventana termine.`);
137
+ }
138
+ /** El holder con el que se publica una fila con motivo y SIN holder (legada o a mano); también es su token. */
139
+ export const UNKNOWN_HOLDER = '?';
140
+ /** Prefijo del holder de la ventana del OPERADOR (`freeze()` con `kind: 'operator'`). */
141
+ export const OPERATOR_HOLDER_PREFIX = 'operator:';
142
+ /**
143
+ * `isOperatorHolder` en SQL, por motor (cierre-2 de alpha.10 · C3): los
144
+ * `OPERATOR_HOLDER_PREFIX.length` primeros CARACTERES de `freeze_holder` igual a
145
+ * `operator:`, sensible a mayúsculas y acentos. SQLite: `substr()` no hereda
146
+ * collation ⇒ `BINARY`. PostgreSQL: `COLLATE "C"` (byte a byte aunque la base
147
+ * declare otra). MySQL: la collation de la columna es la de la tabla (`_ai_ci` por
148
+ * defecto: `OPERATOR:` y `opérator:` serían iguales), así que se compara como
149
+ * BINARIO. El prefijo es ASCII, así que «9 caracteres» y «9 unidades UTF-16» (JS)
150
+ * coinciden siempre que la comparación sea verdadera. Sin `LIKE`: ni comodines ni
151
+ * collation. Cualquier otro dialecto ⇒ 500 `E_AUTHZ_UNSUPPORTED_DIALECT` ANTES de
152
+ * tocar la fila (re-ataque del cierre-2 · ⚪ 3: el `substr` de SQLite en MSSQL
153
+ * sería un error del motor envuelto como 503, que invita a reintentar para siempre).
154
+ */
155
+ export function operatorHolderSql(connection = db.connection()) {
156
+ const length = OPERATOR_HOLDER_PREFIX.length;
157
+ if (isMysqlDialect(connection)) {
158
+ return { sql: `CAST(SUBSTRING(freeze_holder, 1, ${length}) AS BINARY) = CAST(? AS BINARY)`, bindings: [OPERATOR_HOLDER_PREFIX] };
159
+ }
160
+ if (dialectOf(connection).startsWith('postgres')) {
161
+ return { sql: `substr(freeze_holder, 1, ${length}) COLLATE "C" = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
162
+ }
163
+ if (isSqliteDialect(connection)) {
164
+ return { sql: `substr(freeze_holder, 1, ${length}) = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
165
+ }
166
+ throw new UnsupportedDialectError(`freeze: dialecto '${dialectOf(connection)}' sin observación en la suite (hoy: PostgreSQL, MySQL 8 y SQLite)`);
167
+ }
168
+ /**
169
+ * Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
170
+ * `acquire` a la vez acaban en exactamente uno). Una fila con el lease VENCIDO
171
+ * se toma encima (el dueño murió: la flota vuelve sola) **salvo que sea una
172
+ * ventana de OPERADOR** (cierre del auditor de alpha.10 · R2): esa exclusión va
173
+ * DENTRO del `WHERE` del mismo `UPDATE`, en los tres motores. Hasta el cierre
174
+ * vivía en `freeze()` como «leo la fila, compruebo, tomo», y con un reloj que
175
+ * cruzaba el vencimiento entre la comprobación y el UPDATE la pasada pisaba la
176
+ * ventana del operador sin 503 — el TOCTOU que el atómico existe para cerrar.
177
+ */
178
+ export async function acquireFreezeOutcome(input, options = {}, attempt = 0) {
179
+ const { driver, timeoutMs } = opts(options);
180
+ // Fuera de `freezeSql`: un dialecto no soportado es config (500), no un backend caído (503).
181
+ const operator = operatorHolderSql();
182
+ const updated = await freezeSql(driver, 'freeze.acquire', timeoutMs, () => db
183
+ .from(CATALOG_VERSION_TABLE)
184
+ .where('id', FREEZE_ROW_ID)
185
+ .where((q) => {
186
+ // Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma)…
187
+ q.whereNull('freeze_reason').orWhere((expired) => {
188
+ expired
189
+ .whereNotNull('freeze_until_ms')
190
+ .andWhere('freeze_until_ms', '<=', input.nowMs)
191
+ // …salvo la ventana del OPERADOR vencida: es evidencia, no espacio libre (R2).
192
+ .andWhere((notOperator) => {
193
+ // La regla ÚNICA (`isOperatorHolder`), sin `LIKE`: comparación EXACTA de prefijo (cierre-2 · C3).
194
+ notOperator.whereNull('freeze_holder').orWhereRaw(`NOT (${operator.sql})`, operator.bindings);
195
+ });
196
+ });
197
+ })
198
+ .update({
199
+ freeze_reason: input.reason,
200
+ freeze_holder: input.holder,
201
+ freeze_until_ms: input.untilMs,
202
+ freeze_fence: db.raw('freeze_fence + 1'),
203
+ updated_at: new Date(input.nowMs),
204
+ }));
205
+ if (Number(updated) === 0) {
206
+ // O hay un freeze vivo, o una ventana de operador vencida, o la fila no existe: distinguirlo es leerla.
207
+ const row = await readFreezeRow(options);
208
+ if (freezeIsLive(row, input.nowMs))
209
+ return { status: 'held', row };
210
+ if (row.reason !== null && freezeKindOf(row.holder) === 'operator')
211
+ return { status: 'expired-operator', row };
212
+ // Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta UNA vez.
213
+ // Cierre-2 · C3: tras el reintento, una fila que NO está viva no es `held` (su
214
+ // letra dice «VIVO»). Con la regla única del prefijo esta rama solo la alcanza
215
+ // una fila que cambió dos veces entre UPDATE y SELECT: 503 reintentable que lo dice.
216
+ if (attempt >= 1) {
217
+ const error = new AuthorizationBackendError(driver, 'freeze.acquire', new Error('la fila del freeze cambió entre el UPDATE condicional y su relectura dos veces seguidas'));
218
+ error.message =
219
+ `${driver}: no se pudo tomar el freeze: la fila (${row.holder ?? 'sin holder'}, fence ${row.fence}) NO está viva ` +
220
+ `y aun así el UPDATE condicional no casó dos veces seguidas — otro proceso la está cambiando a la vez ` +
221
+ `(rama «contención tras reintento», no «freeze VIVO»). Reintenta.`;
222
+ throw error;
223
+ }
224
+ return acquireFreezeOutcome(input, options, attempt + 1);
225
+ }
226
+ const row = await readFreezeRow(options);
227
+ // Otro acquire ganó justo después (no debería: el nuestro dejó la fila viva).
228
+ if (row.holder !== input.holder)
229
+ return { status: 'held', row };
230
+ return { status: 'acquired', token: { fence: row.fence, holder: input.holder } };
231
+ }
232
+ /** `acquireFreezeOutcome` reducido a «el token o `null`» (lo usan los casos que siembran una fila ajena). */
233
+ export async function acquireFreeze(input, options = {}) {
234
+ const outcome = await acquireFreezeOutcome(input, options);
235
+ return outcome.status === 'acquired' ? outcome.token : null;
236
+ }
237
+ /**
238
+ * Renueva el lease, CONDICIONAL por token y solo si aún no venció
239
+ * (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
240
+ * otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
241
+ * renovar — nunca «recupera» un freeze que ya no es suyo.
242
+ */
243
+ export async function renewFreeze(token, input, options = {}) {
244
+ const { driver, timeoutMs } = opts(options);
245
+ const updated = await freezeSql(driver, 'freeze.renew', timeoutMs, () => db
246
+ .from(CATALOG_VERSION_TABLE)
247
+ .where('id', FREEZE_ROW_ID)
248
+ .where('freeze_fence', token.fence)
249
+ .where('freeze_holder', token.holder)
250
+ .where('freeze_until_ms', '>', input.nowMs)
251
+ .update({ freeze_until_ms: input.untilMs, updated_at: new Date(input.nowMs) }));
252
+ return Number(updated) > 0;
253
+ }
254
+ /**
255
+ * Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
256
+ * nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
257
+ * estaba vencido o el token ya no era el de la fila, hubo una ventana en la
258
+ * que otros procesos pudieron escribir — es lo que el reporte publica.
259
+ */
260
+ export async function releaseFreeze(token, input, options = {}) {
261
+ const { driver, timeoutMs } = opts(options);
262
+ const row = await readFreezeRow(options);
263
+ // La fila SIN holder (legada o escrita a mano) se publica como `UNKNOWN_HOLDER`
264
+ // en `freezeStatus()`/`lastFreezeStatus()`, así que ese es su token: sin esto
265
+ // `authz:unfreeze` no casaba nunca y repetía su aviso para siempre (cierre del
266
+ // auditor de alpha.10 · ⚪ 5). `freeze()` jamás escribe ese holder.
267
+ // Cierre-2 · C4: `'?'` publica DOS filas —holder NULL y holder LITERAL `'?'` (a
268
+ // mano)—, así que el token casa las dos (`IS NULL OR = '?'`); casar solo NULL
269
+ // dejaba la literal sin salida y `authz:unfreeze` en bucle con exit 1. Sigue
270
+ // atado al fence y nunca casa un holder real.
271
+ const unknownHolder = token.holder === UNKNOWN_HOLDER;
272
+ const mine = row.fence === token.fence && (unknownHolder ? row.holder === null || row.holder === UNKNOWN_HOLDER : row.holder === token.holder);
273
+ const lapsed = !mine || (row.untilMs !== null && row.untilMs <= input.nowMs);
274
+ if (!mine)
275
+ return { released: false, lapsed };
276
+ const updated = await freezeSql(driver, 'freeze.release', timeoutMs, () => db
277
+ .from(CATALOG_VERSION_TABLE)
278
+ .where('id', FREEZE_ROW_ID)
279
+ .where('freeze_fence', token.fence)
280
+ .where((q) => unknownHolder ? q.whereNull('freeze_holder').orWhere('freeze_holder', UNKNOWN_HOLDER) : q.where('freeze_holder', token.holder))
281
+ .update({ freeze_reason: null, freeze_holder: null, freeze_until_ms: null, updated_at: new Date(input.nowMs) }));
282
+ return { released: Number(updated) > 0, lapsed };
283
+ }
284
+ /**
285
+ * **La regla ÚNICA del prefijo de operador** (cierre-2 de alpha.10 · C3): los
286
+ * `OPERATOR_HOLDER_PREFIX.length` primeros caracteres del holder son EXACTAMENTE
287
+ * `operator:` — sensible a mayúsculas y a acentos, sin comodines. La MISMA
288
+ * comparación va en el `WHERE` de `acquireFreezeOutcome` (`operatorHolderSql`) y
289
+ * aquí; hasta el cierre-2 el SQL usaba `LIKE` (insensible a mayúsculas en SQLite y
290
+ * en MySQL `_ci`) y JS partía por `:`, así que un holder `OPERATOR:…` caducado era
291
+ * «de operador» para el UPDATE y `unknown` para JS ⇒ 423 «VIVO» sobre una fila
292
+ * caducada, distinto por motor.
293
+ */
294
+ export function isOperatorHolder(holder) {
295
+ return typeof holder === 'string' && holder.slice(0, OPERATOR_HOLDER_PREFIX.length) === OPERATOR_HOLDER_PREFIX;
296
+ }
297
+ /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
298
+ export function freezeKindOf(holder) {
299
+ if (isOperatorHolder(holder))
300
+ return 'operator';
301
+ if (typeof holder !== 'string' || !holder.includes(':'))
302
+ return 'unknown';
303
+ const prefix = holder.slice(0, holder.indexOf(':'));
304
+ return prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
305
+ }
306
+ //# 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 @@
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"}
@@ -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"}
@@ -0,0 +1,105 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ import type { ScopeRef } from '../types.js';
4
+ /**
5
+ * Enforcement de acceso A UN RECURSO concreto. COMPONE el `authorize` que ya
6
+ * existe: el consumidor dice CÓMO cargar el recurso (`load(ctx, id)`) y en qué
7
+ * `{ scope }` vive, y el middleware pregunta al motor sobre ese scope. No es un
8
+ * modelo ni un driver nuevo: es el borde HTTP del scope de un recurso.
9
+ *
10
+ * El ORDEN de las respuestas ES la propiedad de seguridad — un 403 donde
11
+ * debería ir un 404 filtra qué recursos existen (enumeración):
12
+ *
13
+ * 1. 401 si no hay usuario autenticado (antes de nada).
14
+ * 2. 403 si el `gate(ctx)` del consumidor (una ability previa, p. ej.
15
+ * «es admin del tenant») lo niega.
16
+ * 3. 404 si el CONTENEDOR no existe (`containerParam`, cuando se declara).
17
+ * 4. 404 si el RECURSO no existe (`load` ⇒ null) — MISMO cuerpo que el 404
18
+ * del contenedor, para no distinguir «no existe» de «no es tuyo».
19
+ * 5. authorize sobre el `{ scope }` que devolvió `load`, con `permission`
20
+ * (o `readPermission` en métodos seguros): un `false` aquí también
21
+ * es 404 con el MISMO cuerpo, NO un 403 — que no puedas verlo no
22
+ * revela que existe.
23
+ *
24
+ * Invariantes que NO se negocian:
25
+ * - `AuthorizationBackendError` (503) NUNCA se disfraza de 404/403: si el
26
+ * backend cae (`gate`/`load`/`authorize` lanzan), el error sube tal cual.
27
+ * Es lo que distingue «denegado» de «no pude comprobar». El middleware NO
28
+ * envuelve estas llamadas en try/catch a propósito.
29
+ * - Prohibido `role`: como `appAccess`, solo acepta `permission`/
30
+ * `readPermission`. `{ role }` es 500 con receta (membresía no es acceso, y
31
+ * el deny no la gobierna: toda decisión pasa por `authorize`).
32
+ * - Sin segundo `authorize`: se llama UNA sola vez. No re-autoriza.
33
+ *
34
+ * Limitación conocida — canal de TEMPORIZACIÓN (auditor Fase 5, ⚪): el status
35
+ * y el cuerpo de «no existe» y «existe pero no es tuyo» son idénticos, pero el
36
+ * TIEMPO no: un id inexistente responde 404 sin round-trip a `authorize`; uno
37
+ * ajeno responde el mismo 404 tras esa llamada. Es inherente a load→authorize
38
+ * (no se puede autorizar el scope de algo que no se ha cargado), no un defecto;
39
+ * quien necesite cerrar el canal iguala el tiempo en su capa (delay constante),
40
+ * no en el middleware.
41
+ *
42
+ * Pureza: el middleware NO importa aliases del consumidor; `load`/`gate` llegan
43
+ * INYECTADOS en las opciones de la ruta.
44
+ *
45
+ * Uso:
46
+ * router
47
+ * .get('/orgs/:orgId/documents/:id', [DocumentsController, 'show'])
48
+ * .use(middleware.resourceAccess({
49
+ * resource: 'document',
50
+ * param: 'id',
51
+ * containerParam: 'orgId',
52
+ * permission: 'documents:write',
53
+ * readPermission: 'documents:read',
54
+ * load: (ctx, id) => Document.query().where('id', id).first(),
55
+ * gate: (ctx) => ctx.auth.user!.isMemberOf(ctx.params.orgId),
56
+ * }))
57
+ */
58
+ export interface ResourceAccessOptions {
59
+ /**
60
+ * Nombre del recurso. Es la clave bajo la que el recurso cargado (con su
61
+ * scope) se PROPAGA al `ctx` para el controlador (`ctx[resource]`), así que
62
+ * dos `resourceAccess` anidados no se pisan.
63
+ */
64
+ resource: string;
65
+ /** Nombre del param de ruta que trae el id del recurso (`ctx.params[param]`). */
66
+ param: string;
67
+ /**
68
+ * Nombre del param de ruta del contenedor (p. ej. el tenant de una ruta
69
+ * anidada). Cuando se declara, un contenedor ausente en la ruta es 404 —el
70
+ * MISMO cuerpo que un recurso ausente— y se comprueba ANTES de cargar el
71
+ * recurso. La pertenencia real del recurso al contenedor la comprueba
72
+ * `load` (que ve `ctx.params`): devolver `null` allí es el mismo 404, así que
73
+ * «no existe» y «no está en este contenedor» son indistinguibles.
74
+ */
75
+ containerParam?: string;
76
+ /** Permiso requerido (métodos que mutan, y lectura si no hay `readPermission`). */
77
+ permission: string;
78
+ /**
79
+ * Permiso requerido en métodos SEGUROS (GET/HEAD). Si se omite, una lectura
80
+ * exige `permission` — nunca es gratis. Suele ser el permiso MENOS
81
+ * privilegiado (ver vs escribir).
82
+ */
83
+ readPermission?: string;
84
+ /**
85
+ * Carga el recurso por su id y devuelve dónde vive (`{ scope }`) o `null` si
86
+ * no existe / no es visible. Del consumidor: ve el `ctx` entero (params,
87
+ * usuario). Un throw es «no pude comprobar» (sube como 503), NO un 404.
88
+ */
89
+ load: (ctx: HttpContext, id: string) => Promise<{
90
+ scope: ScopeRef;
91
+ } | null> | {
92
+ scope: ScopeRef;
93
+ } | null;
94
+ /**
95
+ * Ability previa del consumidor (p. ej. «es admin del tenant»). Opcional; si
96
+ * se declara y devuelve `false`, es 403 —una restricción ADICIONAL, evaluada
97
+ * antes de tocar el recurso—. Si se omite, no hay pre-check: la seguridad la
98
+ * da igualmente `authorize`.
99
+ */
100
+ gate?: (ctx: HttpContext) => Promise<boolean> | boolean;
101
+ }
102
+ export default class ResourceAccessMiddleware {
103
+ handle(ctx: HttpContext, next: NextFn, options: ResourceAccessOptions): Promise<any>;
104
+ }
105
+ //# sourceMappingURL=resource_access_middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource_access_middleware.d.ts","sourceRoot":"","sources":["../../../src/http/resource_access_middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAGvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,IAAI,EAAE,CACJ,GAAG,EAAE,WAAW,EAChB,EAAE,EAAE,MAAM,KACP,OAAO,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC,GAAG;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAA;IACrE;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;CACxD;AAKD,MAAM,CAAC,OAAO,OAAO,wBAAwB;IACrC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;CA6F5E"}