@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,246 @@
1
+ import { AuthorizationBackendError, AuthorizationBackendTimeoutError, NoScopeResolverError, ScopeResolverError, UnknownScopeError, } from '../errors.js';
2
+ import { APP_SCOPE, APP_SCOPE_TYPE } from '../types.js';
3
+ import { assertScope, scopeSpellings } from '../identity.js';
4
+ /**
5
+ * Clasificación de fallos de dependencias, compartida por ambos drivers.
6
+ *
7
+ * Tres dependencias participan en una pregunta: el catálogo (SQL, en ambos
8
+ * drivers), el árbol del consumidor (`resolveChain`) y el backend de
9
+ * hechos (SQL o FGA). Las tres se clasifican igual: 503 con código propio,
10
+ * la causa original conservada, y NUNCA un `false` ni un error crudo — el
11
+ * invariante 5 tal como está publicado (L0.11, N3).
12
+ *
13
+ * Lo que ya es un error del paquete (422 de catálogo, 500 de programación)
14
+ * pasa intacto: un rol inexistente no es una caída.
15
+ */
16
+ /** ¿Es un error que este paquete ya clasificó (tiene `code` E_AUTHZ_*)? */
17
+ export function isAuthzError(error) {
18
+ return typeof error?.code === 'string' && error.code.startsWith('E_AUTHZ_');
19
+ }
20
+ /**
21
+ * ¿Es un error del CLIENTE SQL (pg, mysql2, better-sqlite3, knex) y no del
22
+ * código del consumidor? Por la forma que cada cliente da a sus errores:
23
+ * `pg` un `DatabaseError` con SQLSTATE de 5 caracteres (`25P02`…) y
24
+ * `severity`; `mysql2` un `errno` numérico con `sqlState`; SQLite un `code`
25
+ * `SQLITE_*`; knex un `KnexTimeoutError`. Sirve para clasificar (503) lo que
26
+ * un `fn` del consumidor deja escapar desde dentro de una transacción
27
+ * (2.5-B · K12): un error crudo del cliente nunca cruza la frontera.
28
+ */
29
+ export function isSqlDriverError(error) {
30
+ const e = error;
31
+ if (!e || typeof e !== 'object')
32
+ return false;
33
+ if (e.name === 'KnexTimeoutError')
34
+ return true;
35
+ if (typeof e.code === 'string' && /^SQLITE_/.test(e.code))
36
+ return true;
37
+ if (typeof e.errno === 'number' && typeof e.sqlState === 'string')
38
+ return true;
39
+ if (typeof e.code === 'string' && /^[0-9A-Z]{5}$/.test(e.code) && ('severity' in e || 'routine' in e))
40
+ return true;
41
+ return false;
42
+ }
43
+ /**
44
+ * ¿El error viene de un deadline vencido? knex lanza `KnexTimeoutError` (o, si
45
+ * además falló la cancelación, el error de cancelación con `timeout` puesto);
46
+ * axios usa `ECONNABORTED`/`ETIMEDOUT`, a veces envuelto por el SDK como
47
+ * `cause`. Se mira la cadena entera de causas.
48
+ */
49
+ export function isTimeoutLike(error) {
50
+ let current = error;
51
+ for (let depth = 0; current && depth < 6; depth++) {
52
+ if (current.name === 'KnexTimeoutError' || typeof current.timeout === 'number')
53
+ return true;
54
+ if (current.code === 'ECONNABORTED' || current.code === 'ETIMEDOUT')
55
+ return true;
56
+ current = current.cause;
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Ejecuta una consulta SQL con deadline y traduce su fallo. El builder de
62
+ * Lucid es perezoso (se ejecuta al hacer `await`), así que el `timeout` se
63
+ * fija aquí, en el ÚNICO punto por el que pasan todas las consultas: un
64
+ * call-site nuevo lo hereda sin saberlo. `cancel: true` pide a knex que
65
+ * cancele la consulta en el servidor cuando el dialecto lo permite.
66
+ */
67
+ export async function guardSql(driver, operation, timeoutMs, fn) {
68
+ try {
69
+ const query = fn();
70
+ if (typeof query?.timeout === 'function') {
71
+ try {
72
+ query.timeout(timeoutMs, { cancel: true });
73
+ }
74
+ catch {
75
+ // knex rechaza `cancel` al CONSTRUIR la consulta en los dialectos que
76
+ // no saben cancelar (SQLite). El deadline sigue valiendo: vencido, la
77
+ // promesa rechaza y la conexión se descarta; solo no se manda un
78
+ // "cancela" al servidor.
79
+ query.timeout(timeoutMs);
80
+ }
81
+ }
82
+ return await query;
83
+ }
84
+ catch (error) {
85
+ if (isAuthzError(error))
86
+ throw error;
87
+ if (isTimeoutLike(error)) {
88
+ throw new AuthorizationBackendTimeoutError(driver, operation, timeoutMs, error);
89
+ }
90
+ throw new AuthorizationBackendError(driver, operation, error);
91
+ }
92
+ }
93
+ /**
94
+ * Deadline TOTAL de una llamada al backend: vencido ⇒ rechaza con el error
95
+ * que fabrique `onTimeout`. El timer no retiene el event loop y se limpia al
96
+ * resolver. La promesa original sigue su curso —la petición en vuelo puede
97
+ * aterrizar después— pero el llamante ya fue liberado: por eso el manager
98
+ * notifica `indeterminate: true` en las escrituras que vencen (D2) y el
99
+ * driver openfga no deja al SDK reintentar por su cuenta (`maxRetry: 0`).
100
+ */
101
+ export function withDeadline(promise, ms, onTimeout) {
102
+ let timer;
103
+ const deadline = new Promise((_, reject) => {
104
+ timer = setTimeout(() => reject(onTimeout()), ms);
105
+ timer.unref?.();
106
+ });
107
+ return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
108
+ }
109
+ /**
110
+ * El resolutor que usa un driver construido SIN `resolveChain`: solo
111
+ * conoce la raíz. Sustituye al default plano de 1.x (`resto → [APP_SCOPE]`),
112
+ * que hacía descender de `app` cualquier scope inventado o borrado (L0.3).
113
+ * No se lanza al construir el driver, sino en la primera pregunta por un
114
+ * scope intermedio: un despliegue que solo usa `app` es legítimo sin árbol.
115
+ */
116
+ export const rootOnlyResolver = async (scope) => {
117
+ if (scope.type === APP_SCOPE_TYPE)
118
+ return [APP_SCOPE];
119
+ throw new NoScopeResolverError(`El driver no tiene 'resolveChain' y se preguntó por un scope de tipo '${scope.type}'. ` +
120
+ `Sin resolutor solo existe la raíz 'app': pasa 'scopes.resolveChain' en el config ` +
121
+ `(y 'resolveChain' al driver) para declarar tu jerarquía.`);
122
+ };
123
+ /**
124
+ * ¿Dos uuids nombran, con formas distintas, el mismo id? Un motor que
125
+ * canoniza (el tipo `uuid` de PostgreSQL acepta mayúsculas y guiones
126
+ * quitados; una collation `*_ci` funde mayúsculas) puede devolver la fila real
127
+ * para un alias: aquí se acepta ESA relación y nada más. Cualquier otra
128
+ * diferencia es otro scope, y un resolutor que lo devuelva miente.
129
+ */
130
+ function sameUuidLoosely(canonical, asked) {
131
+ if (canonical === null || asked === null)
132
+ return canonical === asked;
133
+ const fold = (value) => value.toLowerCase().replaceAll('-', '');
134
+ return fold(canonical) === fold(asked);
135
+ }
136
+ /**
137
+ * Cadena CANÓNICA `[scope canónico, ...ancestros]` con el resolutor del
138
+ * consumidor envuelto (2.5-B · K1): el elemento 0 es el scope tal como está
139
+ * en la tabla del consumidor —no tal como lo escribió el llamante—, y es la
140
+ * identidad con la que el paquete lee y escribe TODOS los hechos. Antes se
141
+ * devolvía `[scope del llamante, ...ancestros]`: un alias del uuid (que el
142
+ * árbol fundía con la fila real) resolvía la cadena entera y el grant del
143
+ * ancestro aplicaba, pero el deny —escrito con la forma canónica— no casaba.
144
+ *
145
+ * Si el resolutor lanza, sale como `ScopeResolverError` (503): el árbol es
146
+ * una dependencia más y su caída no es un bug del paquete ni un "sin
147
+ * permiso". `null` = el scope no existe para el consumidor; el llamante decide
148
+ * qué significa (denegar en lectura, 422 en escritura). La raíz no se
149
+ * pregunta: el motor la conoce y su cadena es `[APP_SCOPE]` por definición.
150
+ *
151
+ * La RESPUESTA se valida (D13 + K1): no-array, elemento mal formado
152
+ * (`{app, 'X'}`, `{organization, 'a|b'}`), cadena vacía, o un elemento 0 que
153
+ * no es el scope pedido (otro tipo; un uuid que no es el mismo id salvo
154
+ * mayúsculas/guiones) ⇒ 503 con el motivo como causa. Nunca se acepta en
155
+ * silencio una identidad que el llamante no pidió.
156
+ */
157
+ export async function resolveChain(resolver, scope, operation) {
158
+ if (scope.type === APP_SCOPE_TYPE)
159
+ return [APP_SCOPE];
160
+ let chain;
161
+ try {
162
+ chain = await resolver(scope);
163
+ }
164
+ catch (error) {
165
+ if (isAuthzError(error))
166
+ throw error;
167
+ throw new ScopeResolverError(operation, error);
168
+ }
169
+ if (chain === null || chain === undefined)
170
+ return null;
171
+ if (!Array.isArray(chain)) {
172
+ throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió ${typeof chain} en vez de ScopeRef[] | null`));
173
+ }
174
+ if (chain.length === 0) {
175
+ throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió una cadena vacía para ${scope.type}:${scope.uuid ?? ''}: la cadena empieza por el propio scope canónico`));
176
+ }
177
+ for (const element of chain) {
178
+ try {
179
+ assertScope(element);
180
+ }
181
+ catch (error) {
182
+ throw new ScopeResolverError(operation, error);
183
+ }
184
+ }
185
+ const self = chain[0];
186
+ if (self.type !== scope.type || !sameUuidLoosely(self.uuid, scope.uuid)) {
187
+ throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió ${self.type}:${self.uuid ?? ''} como elemento 0 de la cadena de ` +
188
+ `${scope.type}:${scope.uuid ?? ''}: el primer elemento tiene que ser el propio scope (canónico), no otro`));
189
+ }
190
+ return chain;
191
+ }
192
+ /**
193
+ * El scope CANÓNICO para una escritura que no exige que exista (`revoke`,
194
+ * `removeDeny`, `purgeScope`): la fila del árbol si lo conoce (un alias se
195
+ * canoniza y el hecho canónico se encuentra, K1); el scope tal cual si ya no
196
+ * lo conoce (un hecho de un scope borrado sin avisar, D8, sigue siendo
197
+ * alcanzable con su identidad exacta) o si el driver no tiene resolutor (solo
198
+ * existe la raíz: no hay nada con lo que canonizar). Un resolutor que LANZA
199
+ * sigue siendo 503: no se limpia a ciegas cuando el árbol está caído.
200
+ */
201
+ export async function canonicalScope(resolver, scope, operation) {
202
+ try {
203
+ const chain = await resolveChain(resolver, scope, operation);
204
+ return chain ? chain[0] : scope;
205
+ }
206
+ catch (error) {
207
+ if (error instanceof NoScopeResolverError)
208
+ return scope;
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * **Los scopes destino de un DELETE que no exige que el scope exista**
214
+ * (`revoke`/`removeDeny`; 3b-8 · A4, mismo cierre que 3b-2h · 🟠 3 dio a
215
+ * `scopes.detached`). Con la fila viva es exactamente UNO, el canónico
216
+ * (`chain[0]`). Sin cadena con la que canonizar —la fila ya borrada, o sin
217
+ * resolutor— la ortografía del llamante a secas hacía del delete un no-op
218
+ * SILENCIOSO cuando llegaba un alias del uuid (el mismo uuid sin guiones,
219
+ * que el tipo `uuid` de PG o una collation `*_ci` funden con la fila real):
220
+ * el hecho canónico seguía escrito y volvía a conceder si el scope se
221
+ * restauraba. Se reusa el fan-out de `scopeSpellings`: la ortografía del
222
+ * llamante Y la canónica de la que puede ser alias. Quitar nunca concede,
223
+ * así que borrar de más no existe aquí.
224
+ */
225
+ export async function canonicalScopeTargets(resolver, scope, operation) {
226
+ try {
227
+ const chain = await resolveChain(resolver, scope, operation);
228
+ if (chain)
229
+ return [chain[0]];
230
+ }
231
+ catch (error) {
232
+ if (!(error instanceof NoScopeResolverError))
233
+ throw error;
234
+ }
235
+ return scopeSpellings(scope);
236
+ }
237
+ /** La cadena canónica, o 422 `E_AUTHZ_UNKNOWN_SCOPE` si el scope no existe (para escrituras). */
238
+ export async function assertKnownScope(resolver, scope, operation) {
239
+ const chain = await resolveChain(resolver, scope, operation);
240
+ if (!chain) {
241
+ throw new UnknownScopeError(`El scope ${scope.type}:${scope.uuid} no existe para el resolutor de la cadena (${operation}). ` +
242
+ `No se escribe sobre un scope que el árbol no reconoce.`);
243
+ }
244
+ return chain;
245
+ }
246
+ //# sourceMappingURL=backend_guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend_guard.js","sourceRoot":"","sources":["../../../src/shared/backend_guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Cómo viaja `expires_at` entre el driver `database` y cada motor SQL
3
+ * (2.5-B · K2, auditor 🟠 2).
4
+ *
5
+ * El problema: en MySQL `expires_at` es `DATETIME(3)`, que NO guarda zona, y
6
+ * `mysql2` serializa y parsea los `Date` con el `TZ` del proceso (`timezone:
7
+ * 'local'`, su default). Un proceso en UTC escribía `12:00:00` para
8
+ * `12:00Z`; uno en Caracas leía ese `12:00:00` como `16:00Z`: la asignación
9
+ * caducaba 4 h tarde para él (y 9 h antes para uno en Tokio). Dentro de un
10
+ * solo proceso el ida y vuelta era exacto, que es lo único que el juez podía
11
+ * ver; el defecto solo aparece con dos procesos, que es el despliegue normal.
12
+ *
13
+ * La decisión: el driver NO depende de la config de la conexión. En MySQL
14
+ * escribe la caducidad como CADENA UTC explícita (`YYYY-MM-DD HH:mm:ss.SSS`),
15
+ * compara con `now` formateado igual y la LEE con `DATE_FORMAT(…)` (una
16
+ * cadena, que se parsea como UTC), así que `timezone`, `dateStrings` o el
17
+ * `TZ` del proceso no intervienen. En PostgreSQL la columna es
18
+ * `timestamptz(3)` (instante absoluto: el `Date` basta) y en SQLite un
19
+ * número; ahí el codec es la identidad. Lo fija `tests/expiry_timezone.spec`
20
+ * con procesos hijos reales en `TZ` distinta sobre la misma base.
21
+ */
22
+ export interface ExpiryCodec {
23
+ /** Valor con el que se ESCRIBE la columna (`insert`/`update`). */
24
+ toDb(expiresAt: Date | null): unknown;
25
+ /** Valor con el que se COMPARA (`expires_at > ?`) para un instante dado. */
26
+ bind(at: Date): unknown;
27
+ /** Expresión de SELECT que devuelve la columna de forma que `fromDb` la lea sin depender del cliente. */
28
+ select(column: string, alias?: string): unknown;
29
+ /** Lo que devuelve el SELECT de `select(...)` → `Date | null`. */
30
+ fromDb(value: unknown): Date | null;
31
+ }
32
+ /** Nombre del dialecto de una conexión de Lucid (`dialect.name`; fallback: el driver de knex). */
33
+ export declare function dialectOf(connection: any): string;
34
+ /**
35
+ * ¿Es SQLite? (3E · Q5) Lucid nombra su dialecto `better-sqlite3` —no
36
+ * empieza por `sqlite`—, así que un `startsWith('sqlite')` no reconocía el
37
+ * SQLite real de esta máquina ni el de CI. Donde eso decide una BARRERA (el
38
+ * cerrojo del catálogo, que SQLite no necesita porque ya serializa sus
39
+ * escrituras) el fallo es silencioso: solo era inocuo porque knex compila
40
+ * `forUpdate` a vacío en sqlite3.
41
+ */
42
+ export declare function isSqliteDialect(connection: any): boolean;
43
+ /** `2030-06-15T12:00:00.000Z` → `2030-06-15 12:00:00.000` (UTC, con milisegundos). */
44
+ export declare function formatUtcDatetime(at: Date): string;
45
+ /** `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`, o `null` si no es una fecha. */
46
+ export declare function parseUtcDatetime(value: string): Date | null;
47
+ /**
48
+ * El codec para una conexión de Lucid (default: la primaria). Se decide por
49
+ * dialecto una vez por driver; MySQL es el único que necesita algo distinto
50
+ * de la identidad.
51
+ */
52
+ export declare function sqlExpiryCodec(connection?: any): ExpiryCodec;
53
+ //# sourceMappingURL=sql_expiry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_expiry.d.ts","sourceRoot":"","sources":["../../../src/shared/sql_expiry.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAA;IACrC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAA;IACvB,yGAAyG;IACzG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/C,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;CACpC;AAID,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,UAAU,EAAE,GAAG,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAGxD;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAElD;AAED,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAM3D;AA6BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,GAAE,GAAqB,GAAG,WAAW,CAE7E"}
@@ -0,0 +1,66 @@
1
+ import db from '@adonisjs/lucid/services/db';
2
+ import { AuthorizationInternalError } from '../errors.js';
3
+ import { toExpiryDate } from '../expiry.js';
4
+ const MYSQL_DIALECTS = new Set(['mysql', 'mysql2']);
5
+ /** Nombre del dialecto de una conexión de Lucid (`dialect.name`; fallback: el driver de knex). */
6
+ export function dialectOf(connection) {
7
+ return connection?.dialect?.name ?? connection?.client?.driverName ?? 'desconocido';
8
+ }
9
+ /**
10
+ * ¿Es SQLite? (3E · Q5) Lucid nombra su dialecto `better-sqlite3` —no
11
+ * empieza por `sqlite`—, así que un `startsWith('sqlite')` no reconocía el
12
+ * SQLite real de esta máquina ni el de CI. Donde eso decide una BARRERA (el
13
+ * cerrojo del catálogo, que SQLite no necesita porque ya serializa sus
14
+ * escrituras) el fallo es silencioso: solo era inocuo porque knex compila
15
+ * `forUpdate` a vacío en sqlite3.
16
+ */
17
+ export function isSqliteDialect(connection) {
18
+ const name = dialectOf(connection);
19
+ return name.startsWith('sqlite') || name === 'better-sqlite3';
20
+ }
21
+ /** `2030-06-15T12:00:00.000Z` → `2030-06-15 12:00:00.000` (UTC, con milisegundos). */
22
+ export function formatUtcDatetime(at) {
23
+ return at.toISOString().slice(0, 23).replace('T', ' ');
24
+ }
25
+ /** `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`, o `null` si no es una fecha. */
26
+ export function parseUtcDatetime(value) {
27
+ const match = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?$/.exec(value.trim());
28
+ if (!match)
29
+ return null;
30
+ const millis = (match[3] ?? '').padEnd(3, '0').slice(0, 3);
31
+ const date = new Date(`${match[1]}T${match[2]}.${millis}Z`);
32
+ return Number.isNaN(date.getTime()) ? null : date;
33
+ }
34
+ const identityCodec = {
35
+ toDb: (expiresAt) => expiresAt,
36
+ bind: (at) => at,
37
+ select: (column, alias) => (alias && alias !== column ? `${column} as ${alias}` : column),
38
+ fromDb: toExpiryDate,
39
+ };
40
+ function mysqlCodec(connection) {
41
+ const raw = (sql, bindings) => typeof connection?.raw === 'function' ? connection.raw(sql, bindings) : db.raw(sql, bindings);
42
+ return {
43
+ toDb: (expiresAt) => (expiresAt ? formatUtcDatetime(expiresAt) : null),
44
+ bind: (at) => formatUtcDatetime(at),
45
+ select: (column, alias) => raw(`DATE_FORMAT(??, '%Y-%m-%d %H:%i:%s.%f') as ??`, [column, alias ?? column.split('.').pop()]),
46
+ fromDb: (value) => {
47
+ if (value === null || value === undefined || value === '')
48
+ return null;
49
+ if (typeof value === 'string')
50
+ return parseUtcDatetime(value);
51
+ // Un `Date` aquí es que la lectura no pasó por `select(...)` y el
52
+ // cliente ya la interpretó con SU zona: eso es exactamente lo que este
53
+ // codec existe para impedir. Ruidoso, nunca una caducidad corrida.
54
+ throw new AuthorizationInternalError(`expires_at llegó como ${value instanceof Date ? 'Date' : typeof value} en MySQL: la lectura tiene que pasar por ExpiryCodec.select(...) (DATE_FORMAT) para no depender de la zona del cliente`);
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * El codec para una conexión de Lucid (default: la primaria). Se decide por
60
+ * dialecto una vez por driver; MySQL es el único que necesita algo distinto
61
+ * de la identidad.
62
+ */
63
+ export function sqlExpiryCodec(connection = db.connection()) {
64
+ return MYSQL_DIALECTS.has(dialectOf(connection)) ? mysqlCodec(connection) : identityCodec;
65
+ }
66
+ //# sourceMappingURL=sql_expiry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_expiry.js","sourceRoot":"","sources":["../../../src/shared/sql_expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,6BAA6B,CAAA;AAC5C,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAkC3C,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAEnD,kGAAkG;AAClG,MAAM,UAAU,SAAS,CAAC,UAAe;IACvC,OAAO,UAAU,EAAE,OAAO,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,aAAa,CAAA;AACrF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAAe;IAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;IAClC,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,KAAK,gBAAgB,CAAA;AAC/D,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,EAAQ;IACxC,OAAO,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACxD,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,KAAK,GAAG,8DAA8D,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;IAC/F,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,GAAG,CAAC,CAAA;IAC3D,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,MAAM,aAAa,GAAgB;IACjC,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS;IAC9B,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACzF,MAAM,EAAE,YAAY;CACrB,CAAA;AAED,SAAS,UAAU,CAAC,UAAe;IACjC,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,QAAmB,EAAE,EAAE,CAC/C,OAAO,UAAU,EAAE,GAAG,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAC/F,OAAO;QACL,IAAI,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACnC,MAAM,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,+CAA+C,EAAE,CAAC,MAAM,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC;QAC5H,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAChB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;gBAAE,OAAO,IAAI,CAAA;YACtE,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAA;YAC7D,kEAAkE;YAClE,uEAAuE;YACvE,mEAAmE;YACnE,MAAM,IAAI,0BAA0B,CAClC,yBAAyB,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,yHAAyH,CAChM,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,aAAkB,EAAE,CAAC,UAAU,EAAE;IAC9D,OAAO,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,aAAa,CAAA;AAC3F,CAAC"}
@@ -0,0 +1,97 @@
1
+ import type { ScopeDescendantsResolver, ScopeEdgesEnumerator } from './types.js';
2
+ /**
3
+ * `descendantsOf` opt-in sobre una tabla del consumidor con columna padre
4
+ * (2.1, B2). Genera UNA CTE recursiva (`WITH RECURSIVE`), la misma para
5
+ * PostgreSQL, SQLite y MySQL 8 (2.5 · J3: solo cambia el carácter con el que
6
+ * se citan los identificadores); cualquier otro dialecto lanza 500
7
+ * `E_AUTHZ_UNSUPPORTED_DIALECT` en la primera llamada hasta que la suite lo
8
+ * observe. El paquete sigue sin conocer el árbol del consumidor: aquí solo
9
+ * se le dice qué tabla y qué columnas lo guardan.
10
+ *
11
+ * scopes: {
12
+ * resolveChain,
13
+ * descendantsOf: sqlDescendantsOf({
14
+ * table: 'organization_nodes', uuidColumn: 'uuid', parentColumn: 'parent_uuid', typeColumn: 'kind',
15
+ * }),
16
+ * }
17
+ *
18
+ * Cotas: la consulta lee como mucho `maxNodes + 1` filas (`LIMIT`) y no baja
19
+ * más de `maxNodes + 1` niveles, así que un ciclo en la tabla termina Y se
20
+ * nota: un ciclo alcanzable produce al menos una fila por nivel, es decir
21
+ * más de `maxNodes` ⇒ 422 `E_AUTHZ_TOO_MANY_SCOPES` con «posible ciclo» en
22
+ * el mensaje (con `depth < maxNodes` un ciclo de dos nodos devolvía
23
+ * `maxNodes - 1` filas duplicadas en silencio). No hay segunda barrera por
24
+ * uuid repetido: con esta profundidad un ciclo NUNCA cabe en `maxNodes`
25
+ * filas, así que era código muerto (G1, CR6). Nunca una lista parcial. Un
26
+ * scope que no está en la tabla ⇒ `null`. `scopeType` pasa por la gramática
27
+ * de identidad al construir (auditor 11).
28
+ */
29
+ export interface SqlDescendantsOptions {
30
+ table: string;
31
+ uuidColumn: string;
32
+ /** Columna con el uuid del padre; `NULL` = el nodo cuelga de `app`. */
33
+ parentColumn: string;
34
+ /** Columna con el `ScopeRef.type` de cada fila. Excluyente con `scopeType`. */
35
+ typeColumn?: string;
36
+ /** Tipo fijo de todos los nodos de la tabla (sin columna de tipo). Excluyente con `typeColumn`. */
37
+ scopeType?: string;
38
+ /** Cota propia del helper: se aplica `min(maxNodes del llamante, esta)`. */
39
+ maxNodes?: number;
40
+ /** Nombre de la conexión Lucid (default: la primaria). */
41
+ connection?: string;
42
+ /** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
43
+ timeoutMs?: number;
44
+ }
45
+ /** Lo que necesita del `db` de Lucid; inyectable para probar el dialecto sin servidor. */
46
+ interface DatabaseLike {
47
+ connection(name?: string): any;
48
+ }
49
+ export declare function sqlDescendantsOf(options: SqlDescendantsOptions, database?: DatabaseLike): ScopeDescendantsResolver;
50
+ /**
51
+ * `scopes.enumerateEdges` sobre la MISMA tabla que `sqlDescendantsOf`
52
+ * (3b-3a): una página de aristas `hijo → padre` por llamada, ordenadas por la
53
+ * clave primaria y continuadas con un cursor.
54
+ *
55
+ * scopes: {
56
+ * resolveChain,
57
+ * enumerateEdges: sqlScopeEdges({
58
+ * table: 'organization_nodes', uuidColumn: 'uuid',
59
+ * parentColumn: 'parent_uuid', typeColumn: 'kind',
60
+ * }),
61
+ * }
62
+ *
63
+ * Lo usa `authz:reconcile --to=openfga` para reconstruir el árbol del store
64
+ * (y para borrar las aristas que la tabla ya no respalda).
65
+ *
66
+ * Decisiones que hay que conocer:
67
+ * - **el cursor es el uuid de la última fila**, y el orden es `ORDER BY
68
+ * uuid` (clave primaria: orden total y estable). Una pasada reanudada no
69
+ * se salta nodos aunque el árbol cambie entre páginas; lo que cambie por
70
+ * debajo del cursor entra en la siguiente pasada, que es idempotente.
71
+ * - **una fila cuyo padre NO existe no produce arista** (LEFT JOIN sin
72
+ * pareja). Es coherente con `resolveChain`, que devuelve `null` para ese
73
+ * nodo: no está en el árbol. No es silencioso: el destino conserva la
74
+ * arista vieja, `authz:reconcile` la cuenta como sobrante y sus hechos
75
+ * salen en `skipped` con el motivo.
76
+ * - `parentColumn IS NULL` es la raíz (`APP_SCOPE`).
77
+ *
78
+ * Mismos dialectos que `sqlDescendantsOf` (PG, MySQL 8, SQLite) y el mismo
79
+ * 500 `E_AUTHZ_UNSUPPORTED_DIALECT` para cualquier otro.
80
+ */
81
+ export interface SqlScopeEdgesOptions {
82
+ table: string;
83
+ uuidColumn: string;
84
+ /** Columna con el uuid del padre; `NULL` = el nodo cuelga de `app`. */
85
+ parentColumn: string;
86
+ /** Columna con el `ScopeRef.type` de cada fila. Excluyente con `scopeType`. */
87
+ typeColumn?: string;
88
+ /** Tipo fijo de todos los nodos de la tabla (sin columna de tipo). Excluyente con `typeColumn`. */
89
+ scopeType?: string;
90
+ /** Nombre de la conexión Lucid (default: la primaria). */
91
+ connection?: string;
92
+ /** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
93
+ timeoutMs?: number;
94
+ }
95
+ export declare function sqlScopeEdges(options: SqlScopeEdgesOptions, database?: DatabaseLike): ScopeEdgesEnumerator;
96
+ export {};
97
+ //# sourceMappingURL=sql_descendants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_descendants.d.ts","sourceRoot":"","sources":["../../src/sql_descendants.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAa,oBAAoB,EAAY,MAAM,YAAY,CAAA;AAErG;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD,0FAA0F;AAC1F,UAAU,YAAY;IACpB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;CAC/B;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,GAAE,YAAiB,GAC1B,wBAAwB,CAiH1B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,GAAE,YAAiB,GAC1B,oBAAoB,CA+EtB"}