@jantstack/adonis-authz 1.1.0 → 2.0.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 (132) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +417 -58
  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_sync.d.ts +20 -0
  8. package/build/commands/authz_catalog_sync.d.ts.map +1 -0
  9. package/build/commands/authz_catalog_sync.js +58 -0
  10. package/build/commands/authz_catalog_sync.js.map +1 -0
  11. package/build/commands/main.d.ts +2 -0
  12. package/build/commands/main.d.ts.map +1 -1
  13. package/build/commands/main.js +2 -0
  14. package/build/commands/main.js.map +1 -1
  15. package/build/commands/openfga_import.d.ts +8 -2
  16. package/build/commands/openfga_import.d.ts.map +1 -1
  17. package/build/commands/openfga_import.js +29 -6
  18. package/build/commands/openfga_import.js.map +1 -1
  19. package/build/commands/openfga_provision.d.ts.map +1 -1
  20. package/build/commands/openfga_provision.js +2 -1
  21. package/build/commands/openfga_provision.js.map +1 -1
  22. package/build/index.d.ts +45 -6
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +37 -4
  25. package/build/index.js.map +1 -1
  26. package/build/src/catalog.d.ts +249 -2
  27. package/build/src/catalog.d.ts.map +1 -1
  28. package/build/src/catalog.js +709 -59
  29. package/build/src/catalog.js.map +1 -1
  30. package/build/src/catalog_cache.d.ts +300 -0
  31. package/build/src/catalog_cache.d.ts.map +1 -0
  32. package/build/src/catalog_cache.js +656 -0
  33. package/build/src/catalog_cache.js.map +1 -0
  34. package/build/src/clock.d.ts +24 -0
  35. package/build/src/clock.d.ts.map +1 -0
  36. package/build/src/clock.js +7 -0
  37. package/build/src/clock.js.map +1 -0
  38. package/build/src/define_config.d.ts +102 -6
  39. package/build/src/define_config.d.ts.map +1 -1
  40. package/build/src/define_config.js.map +1 -1
  41. package/build/src/drivers/backend_guard.d.ts +92 -0
  42. package/build/src/drivers/backend_guard.d.ts.map +1 -0
  43. package/build/src/drivers/backend_guard.js +221 -0
  44. package/build/src/drivers/backend_guard.js.map +1 -0
  45. package/build/src/drivers/database_driver.d.ts +242 -15
  46. package/build/src/drivers/database_driver.d.ts.map +1 -1
  47. package/build/src/drivers/database_driver.js +692 -127
  48. package/build/src/drivers/database_driver.js.map +1 -1
  49. package/build/src/drivers/openfga_driver.d.ts +341 -27
  50. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  51. package/build/src/drivers/openfga_driver.js +1020 -271
  52. package/build/src/drivers/openfga_driver.js.map +1 -1
  53. package/build/src/drivers/sql_expiry.d.ts +53 -0
  54. package/build/src/drivers/sql_expiry.d.ts.map +1 -0
  55. package/build/src/drivers/sql_expiry.js +66 -0
  56. package/build/src/drivers/sql_expiry.js.map +1 -0
  57. package/build/src/errors.d.ts +333 -0
  58. package/build/src/errors.d.ts.map +1 -1
  59. package/build/src/errors.js +352 -0
  60. package/build/src/errors.js.map +1 -1
  61. package/build/src/expiry.d.ts +27 -0
  62. package/build/src/expiry.d.ts.map +1 -0
  63. package/build/src/expiry.js +50 -0
  64. package/build/src/expiry.js.map +1 -0
  65. package/build/src/hierarchical_resolver.d.ts +56 -0
  66. package/build/src/hierarchical_resolver.d.ts.map +1 -0
  67. package/build/src/hierarchical_resolver.js +87 -0
  68. package/build/src/hierarchical_resolver.js.map +1 -0
  69. package/build/src/identity.d.ts +155 -0
  70. package/build/src/identity.d.ts.map +1 -0
  71. package/build/src/identity.js +359 -0
  72. package/build/src/identity.js.map +1 -0
  73. package/build/src/manager.d.ts +225 -7
  74. package/build/src/manager.d.ts.map +1 -1
  75. package/build/src/manager.js +1670 -23
  76. package/build/src/manager.js.map +1 -1
  77. package/build/src/memoize_ancestors.d.ts +23 -0
  78. package/build/src/memoize_ancestors.d.ts.map +1 -0
  79. package/build/src/memoize_ancestors.js +42 -0
  80. package/build/src/memoize_ancestors.js.map +1 -0
  81. package/build/src/middleware/app_access_middleware.d.ts +8 -6
  82. package/build/src/middleware/app_access_middleware.d.ts.map +1 -1
  83. package/build/src/middleware/app_access_middleware.js +10 -26
  84. package/build/src/middleware/app_access_middleware.js.map +1 -1
  85. package/build/src/models/authz_assignment.d.ts +5 -5
  86. package/build/src/models/authz_assignment.d.ts.map +1 -1
  87. package/build/src/models/authz_deny.d.ts +5 -5
  88. package/build/src/models/authz_deny.d.ts.map +1 -1
  89. package/build/src/models/authz_permission.d.ts +11 -5
  90. package/build/src/models/authz_permission.d.ts.map +1 -1
  91. package/build/src/models/authz_permission.js +4 -0
  92. package/build/src/models/authz_permission.js.map +1 -1
  93. package/build/src/models/authz_role.d.ts +13 -6
  94. package/build/src/models/authz_role.d.ts.map +1 -1
  95. package/build/src/models/authz_role.js +6 -1
  96. package/build/src/models/authz_role.js.map +1 -1
  97. package/build/src/models/authz_role_permission.d.ts +5 -5
  98. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  99. package/build/src/openfga.d.ts +13 -0
  100. package/build/src/openfga.d.ts.map +1 -0
  101. package/build/src/openfga.js +12 -0
  102. package/build/src/openfga.js.map +1 -0
  103. package/build/src/sql_descendants.d.ts +51 -0
  104. package/build/src/sql_descendants.d.ts.map +1 -0
  105. package/build/src/sql_descendants.js +129 -0
  106. package/build/src/sql_descendants.js.map +1 -0
  107. package/build/src/testing/contract.d.ts +138 -7
  108. package/build/src/testing/contract.d.ts.map +1 -1
  109. package/build/src/testing/contract.js +2946 -24
  110. package/build/src/testing/contract.js.map +1 -1
  111. package/build/src/testing/main.d.ts +4 -2
  112. package/build/src/testing/main.d.ts.map +1 -1
  113. package/build/src/testing/main.js +2 -1
  114. package/build/src/testing/main.js.map +1 -1
  115. package/build/src/testing/scope_tree.d.ts +65 -0
  116. package/build/src/testing/scope_tree.d.ts.map +1 -0
  117. package/build/src/testing/scope_tree.js +145 -0
  118. package/build/src/testing/scope_tree.js.map +1 -0
  119. package/build/src/traits/authz_scopes.d.ts +30 -6
  120. package/build/src/traits/authz_scopes.d.ts.map +1 -1
  121. package/build/src/traits/authz_scopes.js +30 -18
  122. package/build/src/traits/authz_scopes.js.map +1 -1
  123. package/build/src/traits/has_uuid.d.ts +5 -5
  124. package/build/src/traits/has_uuid.d.ts.map +1 -1
  125. package/build/src/types.d.ts +530 -28
  126. package/build/src/types.d.ts.map +1 -1
  127. package/build/src/types.js +10 -4
  128. package/build/src/types.js.map +1 -1
  129. package/build/stubs/config/app_acl.stub +4 -2
  130. package/build/stubs/config/authorization.stub +66 -12
  131. package/build/stubs/migration.stub +57 -13
  132. package/package.json +11 -6
@@ -32,4 +32,356 @@ export class AuthorizationBackendError extends Exception {
32
32
  super(`El backend de autorización '${driver}' no respondió (${operation})`, { cause });
33
33
  }
34
34
  }
35
+ /**
36
+ * El backend respondió tarde: venció `timeoutMs`. Es un `AuthorizationBackendError`
37
+ * (503, se maneja igual) con código propio, porque para operaciones "no responde"
38
+ * y "responde lento" son incidentes distintos: el segundo suele ser saturación, no
39
+ * caída, y el remedio (subir el deadline, escalar el backend) es otro.
40
+ */
41
+ export class AuthorizationBackendTimeoutError extends AuthorizationBackendError {
42
+ static code = 'E_AUTHZ_BACKEND_TIMEOUT';
43
+ constructor(driver, operation, timeoutMs, cause) {
44
+ super(driver, operation, cause);
45
+ this.message = `El backend de autorización '${driver}' no respondió en ${timeoutMs} ms (${operation})`;
46
+ }
47
+ }
48
+ /**
49
+ * El resolutor de ancestros del consumidor lanzó. Es una dependencia más de
50
+ * cada pregunta (el árbol de scopes), así que su caída se clasifica como la
51
+ * del backend: 503, nunca `false`, nunca el error crudo del consumidor.
52
+ */
53
+ export class ScopeResolverError extends Exception {
54
+ static status = 503;
55
+ static code = 'E_AUTHZ_RESOLVER_FAILED';
56
+ constructor(operation, cause) {
57
+ super(`El resolutor de ancestros de scopes falló (${operation})`, { cause });
58
+ }
59
+ }
60
+ /**
61
+ * Un componente de identidad (holder, scope, rol, permiso) no cumple el formato
62
+ * del motor. Es una pregunta inválida (422), no un "sin permiso": aceptarla
63
+ * fundiría identidades distintas en una (`user:undefined`, `{app, uuid}` que
64
+ * colapsa a la raíz) o produciría ids que otro backend no puede representar.
65
+ */
66
+ export class InvalidIdentityError extends Exception {
67
+ static status = 422;
68
+ static code = 'E_AUTHZ_INVALID_IDENTITY';
69
+ }
70
+ /**
71
+ * Un slug de rol/permiso no cumple la gramática del catálogo: formato,
72
+ * longitud, nombre reservado o familia de prefijos reservada. 422 porque es la
73
+ * pregunta la que está mal formada; el catálogo no se consulta.
74
+ */
75
+ export class InvalidSlugError extends Exception {
76
+ static status = 422;
77
+ static code = 'E_AUTHZ_INVALID_SLUG';
78
+ }
79
+ /**
80
+ * La configuración del consumidor es contradictoria (p. ej. `holderTypes` no
81
+ * inyectivo). 500: la aplicación está mal construida, no hay pregunta que
82
+ * responder hasta que se arregle.
83
+ */
84
+ export class AuthorizationConfigError extends Exception {
85
+ static status = 500;
86
+ static code = 'E_AUTHZ_CONFIG';
87
+ }
88
+ /**
89
+ * Invariante interno del motor violado (una escritura sin scopes, un batchCheck
90
+ * incompleto). 500: es un bug del paquete o de quien lo extiende, no del
91
+ * llamante ni del backend.
92
+ */
93
+ export class AuthorizationInternalError extends Exception {
94
+ static status = 500;
95
+ static code = 'E_AUTHZ_INTERNAL';
96
+ }
97
+ /**
98
+ * El scope no existe para el consumidor (`resolveChain` devolvió `null`).
99
+ * En lectura el motor responde `false` sin más; en escritura es 422: conceder
100
+ * o denegar en un scope que nadie reconoce dejaría un hecho huérfano que
101
+ * ningún árbol volverá a alcanzar — o, peor, que resucitaría si el uuid se
102
+ * reutiliza (L0.3, N5).
103
+ */
104
+ export class UnknownScopeError extends Exception {
105
+ static status = 422;
106
+ static code = 'E_AUTHZ_UNKNOWN_SCOPE';
107
+ }
108
+ /**
109
+ * Se preguntó por un scope que no es la raíz y el driver no tiene resolutor
110
+ * de ancestros. Antes el paquete suplía un default plano ("todo cuelga de
111
+ * app") que convertía cualquier scope inventado en descendiente de la raíz.
112
+ * 422 y no 500: la raíz sigue funcionando; lo que no existe en este
113
+ * despliegue son los niveles intermedios.
114
+ */
115
+ export class NoScopeResolverError extends Exception {
116
+ static status = 422;
117
+ static code = 'E_AUTHZ_NO_SCOPE_RESOLVER';
118
+ }
119
+ /**
120
+ * Rol fuera del catálogo para ese nivel (pregunta inválida, 422). Con
121
+ * `scopeType` se buscó por `(slug, scopeType)`; sin él, por uuid (3B ·
122
+ * `purgeRole`, `updateScopedRole`, `deleteScopedRole`).
123
+ */
124
+ export class UnknownRoleError extends Exception {
125
+ static status = 422;
126
+ static code = 'E_AUTHZ_UNKNOWN_ROLE';
127
+ constructor(slugOrUuid, scopeType) {
128
+ super(scopeType === undefined
129
+ ? `Rol con uuid '${slugOrUuid}' no existe en el catálogo`
130
+ : `Rol '${slugOrUuid}' no existe en el catálogo para el nivel '${scopeType}'`);
131
+ }
132
+ }
133
+ /**
134
+ * El rol existe pero es LOCAL a un scope que no está en la cadena del scope
135
+ * de la escritura (3B · B2): un rol de la organization A no existe en B ni
136
+ * en la raíz. 422 y nada escrito — es la barrera que impide asignar el rol
137
+ * de un tenant fuera de su contenedor. En lectura no hay error: el rol
138
+ * simplemente no concede ni es membresía fuera de su owner.
139
+ */
140
+ export class RoleNotVisibleError extends Exception {
141
+ static status = 422;
142
+ static code = 'E_AUTHZ_ROLE_NOT_VISIBLE';
143
+ }
144
+ /**
145
+ * En la cadena del scope de la pregunta hay MÁS DE UN rol visible con ese
146
+ * `(slug, nivel)` (3D · M1): dos locales de owners distintos que un
147
+ * `scopes.moved` juntó, o un local que tapa a un global. El slug ya no
148
+ * identifica un rol y elegir uno («el owner más cercano gana») convertía un
149
+ * homónimo en una escalada de privilegios (auditor V1/V2/V3): la ambigüedad
150
+ * es un ERROR, no una regla de resolución. Toda ruta que direccione por slug
151
+ * —`grant`, `revoke` por uuid, `hasRole`, `listSubjects`— falla CERRADA con
152
+ * 422 nombrando los uuids y sus owners; la forma sin ambigüedad posible es
153
+ * `{ uuid }` (`RoleQuery`). `authorize` no pasa por aquí: no direcciona por
154
+ * slug (lo asignado concede lo que su rol vincula, invariante 1).
155
+ */
156
+ export class AmbiguousRoleError extends Exception {
157
+ static status = 422;
158
+ static code = 'E_AUTHZ_AMBIGUOUS_ROLE';
159
+ }
160
+ /**
161
+ * El `scopeType` de un rol local no es el nivel de su owner ni uno que
162
+ * cuelgue de él (3E · P1, auditor A1). Un rol de nivel SUPERIOR al owner no
163
+ * es visible en ninguna parte —no concede, no es membresía, nadie lo puede
164
+ * asignar— pero OCUPA ese `(slug, nivel)`: bloqueaba el `defineScopedRole`
165
+ * del dueño del árbol y, hasta 3E, el `syncAuthzCatalog` entero de la
166
+ * plataforma. Es squatting con forma de spec, como `permissions: []` (3D ·
167
+ * N3): 422. Los niveles que cuelgan del owner los declara el consumidor con
168
+ * `scopes.descendantsOf`; sin él solo se admite el nivel del owner.
169
+ */
170
+ export class RoleLevelAboveOwnerError extends Exception {
171
+ static status = 422;
172
+ static code = 'E_AUTHZ_ROLE_LEVEL_ABOVE_OWNER';
173
+ }
174
+ /**
175
+ * `updateScopedRole`/`deleteScopedRole` sobre un rol GLOBAL (3B · B3). Los
176
+ * roles del catálogo del config se cambian en el config y se sincronizan
177
+ * (`syncAuthzCatalog`); por la API de delegación son inmutables, o un
178
+ * administrador de tenant podría editar el catálogo de la plataforma.
179
+ */
180
+ export class RoleImmutableError extends Exception {
181
+ static status = 422;
182
+ static code = 'E_AUTHZ_ROLE_IMMUTABLE';
183
+ }
184
+ /**
185
+ * Un rol de nivel L no puede llevar (o ser asignado llevando) un permiso
186
+ * cuyo `assignableAt` no incluye L (3B · B5). Es un control de COMPOSICIÓN
187
+ * —`syncAuthzCatalog`, `defineScopedRole`, `updateScopedRole` y, por defensa
188
+ * en profundidad, `grant`—, nunca de evaluación: lo ya asignado sigue
189
+ * concediendo (invariante 1).
190
+ */
191
+ export class RoleNotAssignableAtError extends Exception {
192
+ static status = 422;
193
+ static code = 'E_AUTHZ_ROLE_NOT_ASSIGNABLE_AT';
194
+ }
195
+ /**
196
+ * `defineScopedRole`/`updateScopedRole` con un permiso que el actor no puede
197
+ * delegar (3B · B3): no está en `config.delegablePermissions` (lista blanca;
198
+ * vacía por defecto: nadie delega nada hasta declararla), no existe en el
199
+ * catálogo, o el actor no lo tiene EFECTIVO en el owner (no lo concede
200
+ * ningún rol suyo en la cadena, o lo tiene denegado — auditor C2: un deny
201
+ * no se lava componiendo un rol para un títere).
202
+ */
203
+ export class PermissionNotDelegableError extends Exception {
204
+ static status = 422;
205
+ static code = 'E_AUTHZ_PERMISSION_NOT_DELEGABLE';
206
+ }
207
+ /**
208
+ * El `rank` de un rol local no cumple `0 < rank < min(rank del actor, rank
209
+ * máximo global)` (3B · B3), o el actor intenta tocar un rol de rango ≥ al
210
+ * suyo. Es policy de ESCRITURA (composición y delegación); el motor sigue sin
211
+ * evaluar `rank` en `authorize` (invariante 8).
212
+ */
213
+ export class RankExceededError extends Exception {
214
+ static status = 422;
215
+ static code = 'E_AUTHZ_RANK_EXCEEDED';
216
+ }
217
+ /**
218
+ * Dos catálogos de `config.catalogs` declaran el mismo rol `(slug, scopeType)`
219
+ * o el mismo permiso. La identidad de un rol es ese par: compartirla es
220
+ * compartir el rol, y el prune del segundo sync borraría los vínculos del
221
+ * primero en silencio (D3). Un rol pertenece a exactamente un catálogo; se
222
+ * rechaza antes de tocar la base, en el sync y en el diff.
223
+ */
224
+ export class CatalogConflictError extends Exception {
225
+ static status = 422;
226
+ static code = 'E_AUTHZ_CATALOG_CONFLICT';
227
+ }
228
+ /** Permiso fuera del catálogo (pregunta inválida en `deny`/catálogo, 422). */
229
+ export class UnknownPermissionError extends Exception {
230
+ static status = 422;
231
+ static code = 'E_AUTHZ_UNKNOWN_PERMISSION';
232
+ constructor(slug) {
233
+ super(`Permiso '${slug}' no existe en el catálogo`);
234
+ }
235
+ }
236
+ /**
237
+ * `appAccess({ role })` se retiró (L0.6). `hasRole` es una consulta de
238
+ * MEMBRESÍA y el deny no la gobierna: un middleware sobre ella era un punto de
239
+ * decisión indenegable (un holder con todos sus permisos denegados seguía
240
+ * pasando). 500: es la ruta la que está mal declarada, y debe saltar en el
241
+ * primer request de desarrollo, no en producción.
242
+ */
243
+ export class RoleIsNotAccessError extends Exception {
244
+ static status = 500;
245
+ static code = 'E_AUTHZ_ROLE_IS_NOT_ACCESS';
246
+ constructor(role) {
247
+ super(`appAccess({ role: ${JSON.stringify(role)} }) ya no existe: un rol es membresía, no acceso, ` +
248
+ `y el deny no lo gobierna. Receta: crea un permiso que represente ese acceso ` +
249
+ `(p. ej. 'admin:access'), vincúlalo al rol en el catálogo y protege la ruta con ` +
250
+ `appAccess({ permission: 'admin:access' }).`);
251
+ }
252
+ }
253
+ /**
254
+ * Colgar `child` de `parent` cerraría un ciclo (el padre desciende del hijo,
255
+ * o son el mismo). FGA no detecta ciclos de `parent`: los evalúa, y un grant
256
+ * en cualquier nodo del ciclo concede en todos, raíz incluida (S2). La única
257
+ * barrera es el paquete, antes de escribir nada.
258
+ */
259
+ export class ScopeCycleError extends Exception {
260
+ static status = 422;
261
+ static code = 'E_AUTHZ_SCOPE_CYCLE';
262
+ }
263
+ /**
264
+ * `purgeScope` terminó y el backend todavía muestra hechos del scope. Un
265
+ * número parcial que no lanza deja grants huérfanos e indenegables (S6, B2);
266
+ * se lanza para que el consumidor NO confirme el borrado de su entidad.
267
+ */
268
+ export class PurgeIncompleteError extends Exception {
269
+ static status = 500;
270
+ static code = 'E_AUTHZ_PURGE_INCOMPLETE';
271
+ }
272
+ /**
273
+ * `openfga:import` sobre un store con tuplas y sin `--reconcile`. En FGA la
274
+ * condición no es parte de la clave: escribir "ignorando duplicados" sobre
275
+ * una tupla existente dejaba la caducidad vieja (o ninguna) y reportaba
276
+ * éxito (S7). 409: el estado del destino no es el que el comando espera.
277
+ */
278
+ export class StoreNotEmptyError extends Exception {
279
+ static status = 409;
280
+ static code = 'E_AUTHZ_STORE_NOT_EMPTY';
281
+ }
282
+ /**
283
+ * `config.requireActor: true` y una escritura llegó sin `actor` (2.1, B7).
284
+ * 422 antes de tocar el driver: una auditoría que exige saber quién ordenó
285
+ * cada cambio no puede tener huecos, y el hueco no se descubre después.
286
+ */
287
+ export class ActorRequiredError extends Exception {
288
+ static status = 422;
289
+ static code = 'E_AUTHZ_ACTOR_REQUIRED';
290
+ }
291
+ /**
292
+ * `within` no está en la cadena del scope de la escritura (2.1, B1): el
293
+ * call-site declaró "dentro de MI tenant" y el scope pertenece a otro. 422 y
294
+ * nada escrito: es la barrera contra el administrador de A que concede en
295
+ * una unit de B pasando un uuid ajeno. Vale para las SEIS escrituras del
296
+ * manager (2D · F2): quitar un deny ajeno es conceder, revocar un rol ajeno
297
+ * es sabotear, y purgar o recolgar un scope ajeno es lo mismo.
298
+ */
299
+ export class NotWithinError extends Exception {
300
+ static status = 422;
301
+ static code = 'E_AUTHZ_NOT_WITHIN';
302
+ }
303
+ /**
304
+ * `config.requireWithin` (`true` o `'non-root'`) y una escritura llegó sin
305
+ * `within` (2.1, B1; las seis desde 2D · F2). 422 antes del driver: la
306
+ * contención opt-in (auditor E2) deja de serlo cuando el consumidor lo declara.
307
+ */
308
+ export class WithinRequiredError extends Exception {
309
+ static status = 422;
310
+ static code = 'E_AUTHZ_WITHIN_REQUIRED';
311
+ }
312
+ /**
313
+ * `config.requireWithin: 'non-root'` y una escritura declaró `within: APP_SCOPE`
314
+ * (2.1, 2D · F2). La raíz contiene todo, así que como `within` no acota nada:
315
+ * era el comodín con el que un call-site de tenant satisfacía `requireWithin`
316
+ * sin decir de qué tenant es (auditor 9). 422 y nada escrito; la plataforma,
317
+ * que sí escribe en la raíz, usa `manager.driver()` o una config sin el flag.
318
+ */
319
+ export class WithinRootForbiddenError extends Exception {
320
+ static status = 422;
321
+ static code = 'E_AUTHZ_WITHIN_ROOT_FORBIDDEN';
322
+ }
323
+ /**
324
+ * Una enumeración de scopes superó su cota (2.1, B2/B3): `authorizedScopes`
325
+ * por encima de `maxScopes`, o un `descendantsOf` que devolvió más de
326
+ * `maxNodes`. 422 y NUNCA una lista parcial: un listado truncado en silencio
327
+ * es la forma más discreta de fail-open/fail-closed. El llamante acota la
328
+ * pregunta o sube la cota a sabiendas.
329
+ */
330
+ export class TooManyScopesError extends Exception {
331
+ static status = 422;
332
+ static code = 'E_AUTHZ_TOO_MANY_SCOPES';
333
+ }
334
+ /**
335
+ * `sqlDescendantsOf` sobre un dialecto sin observación (2.1, B2): hoy solo PG
336
+ * y SQLite corren la CTE en la suite; MySQL entra en 2.5 con harness propio.
337
+ * 500: config del consumidor, salta en la primera llamada.
338
+ */
339
+ export class UnsupportedDialectError extends Exception {
340
+ static status = 500;
341
+ static code = 'E_AUTHZ_UNSUPPORTED_DIALECT';
342
+ }
343
+ /**
344
+ * `hierarchicalScopeResolver` superó `maxDepth` (2.1, B4). Truncar la cadena
345
+ * perdería la raíz —y con ella cualquier deny en `app`—: fail-open. 500 y no
346
+ * 503: el árbol del consumidor es más profundo de lo declarado, no hay
347
+ * reintento que lo arregle.
348
+ */
349
+ export class ScopeTooDeepError extends Exception {
350
+ static status = 500;
351
+ static code = 'E_AUTHZ_SCOPE_TOO_DEEP';
352
+ }
353
+ /**
354
+ * Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
355
+ * activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
356
+ * es el despliegue el que está incompleto; nunca se simula la respuesta —un
357
+ * `[]` en `listDenies` sería "sin denies", fail-open.
358
+ */
359
+ export class UnsupportedOperationError extends Exception {
360
+ static status = 500;
361
+ static code = 'E_AUTHZ_UNSUPPORTED';
362
+ constructor(method, primitive, driver, hint) {
363
+ super(`${primitive} necesita '${method}' y el driver '${driver}' no lo implementa: es un método opcional del ` +
364
+ `puerto (2.1) que este driver tiene que añadir para usar esta primitiva.` + (hint ? ` ${hint}` : ''));
365
+ }
366
+ }
367
+ /**
368
+ * Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
369
+ * F9, auditor 5). El memo de ancestros de la vista es correcto durante un
370
+ * request y peligroso guardado en un módulo: tras un `scopes.moved` serviría
371
+ * la cadena vieja para siempre (cruce de tenant). 500 y ruidoso: es un bug
372
+ * del consumidor (la vista sobrevivió a su request), no un "sin permiso".
373
+ */
374
+ export class ViewExpiredError extends Exception {
375
+ static status = 500;
376
+ static code = 'E_AUTHZ_VIEW_EXPIRED';
377
+ }
378
+ /**
379
+ * `authorizedScopes` sin `scopes.descendantsOf` en el config (2.1, B3). 500
380
+ * y nunca `none`: un "ningún scope" sin haber mirado el árbol sería un
381
+ * fail-closed mentiroso que el consumidor tomaría por respuesta.
382
+ */
383
+ export class NoDescendantsResolverError extends Exception {
384
+ static status = 500;
385
+ static code = 'E_AUTHZ_NO_DESCENDANTS_RESOLVER';
386
+ }
35
387
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IACtD,MAAM,CAAC,MAAM,GAAG,GAAG,CAAA;IACnB,MAAM,CAAC,IAAI,GAAG,6BAA6B,CAAA;IAE3C,YAAY,MAAc,EAAE,SAAiB,EAAE,KAAc;QAC3D,KAAK,CAAC,+BAA+B,MAAM,mBAAmB,SAAS,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACxF,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { GrantOutcome } from './types.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 declare function resolveGrantExpiry(previous: Date | null, requested: Date | null | undefined, now?: Date): Date | null;
15
+ /** Vigente: sin caducidad o con caducidad futura. */
16
+ export declare function isActiveExpiry(expiresAt: Date | null, now?: Date): boolean;
17
+ /**
18
+ * ¿Mismo instante? Compara el tiempo, no la referencia ni la cadena:
19
+ * `2026-01-01T00:00:00Z` y `…00.000Z` son el mismo momento y no justifican
20
+ * una reescritura (que en openfga es delete+write con ventana de denegación).
21
+ */
22
+ export declare function sameInstant(a: Date | null | undefined, b: Date | null | undefined): boolean;
23
+ /** Lo que devuelve el backend como caducidad (número, ISO, Date) → Date | null. */
24
+ export declare function toExpiryDate(value: unknown): Date | null;
25
+ /** ¿El outcome de un grant cambió la caducidad de una asignación que ya existía? */
26
+ export declare function expiryChanged(outcome: GrantOutcome): boolean;
27
+ //# sourceMappingURL=expiry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.d.ts","sourceRoot":"","sources":["../../src/expiry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,GAAG,IAAI,EACrB,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EAClC,GAAG,GAAE,IAAoB,GACxB,IAAI,GAAG,IAAI,CAGb;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,GAAE,IAAoB,GAAG,OAAO,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI3F;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAKxD;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAM5D"}
@@ -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,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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"}