@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,203 @@
1
+ import db from '@adonisjs/lucid/services/db';
2
+ import { AuthorizationConfigError, TooManyScopesError, UnsupportedDialectError } from './errors.js';
3
+ import { assertScope, assertScopeType } from './identity.js';
4
+ import { guardSql } from './shared/backend_guard.js';
5
+ import { APP_SCOPE_TYPE } from './types.js';
6
+ import { APP_SCOPE } from './types.js';
7
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
8
+ /** Dialecto → carácter con el que cita identificadores (MySQL: backtick; el resto: comilla doble ANSI). */
9
+ const QUOTE_BY_DIALECT = {
10
+ 'postgres': '"',
11
+ 'sqlite3': '"',
12
+ 'better-sqlite3': '"',
13
+ 'mysql': '`',
14
+ 'mysql2': '`',
15
+ };
16
+ const DEFAULT_TIMEOUT_MS = 5_000;
17
+ function identifier(kind, value) {
18
+ if (typeof value !== 'string' || !IDENTIFIER.test(value)) {
19
+ throw new AuthorizationConfigError(`sqlDescendantsOf: ${kind} '${String(value)}' no es un identificador SQL simple (letras, dígitos y _); ` +
20
+ `nada que no lo sea se interpola en la consulta.`);
21
+ }
22
+ return value;
23
+ }
24
+ export function sqlDescendantsOf(options, database = db) {
25
+ const table = identifier('table', options.table);
26
+ const uuidCol = identifier('uuidColumn', options.uuidColumn);
27
+ const parentCol = identifier('parentColumn', options.parentColumn);
28
+ if ((options.typeColumn === undefined) === (options.scopeType === undefined)) {
29
+ throw new AuthorizationConfigError('sqlDescendantsOf: declara exactamente uno de typeColumn (el tipo viene de la fila) o scopeType (tipo fijo).');
30
+ }
31
+ const typeCol = options.typeColumn === undefined ? null : identifier('typeColumn', options.typeColumn);
32
+ const fixedType = options.scopeType ?? null;
33
+ if (fixedType !== null) {
34
+ // Es identidad de scope (minúsculas, ≤ 20, sin separadores) y nunca la
35
+ // raíz: `app` no tiene fila ni uuid.
36
+ try {
37
+ assertScopeType(fixedType);
38
+ }
39
+ catch (error) {
40
+ throw new AuthorizationConfigError(`sqlDescendantsOf: scopeType inválido: ${error.message}`);
41
+ }
42
+ if (fixedType === APP_SCOPE_TYPE) {
43
+ throw new AuthorizationConfigError("sqlDescendantsOf: scopeType no puede ser 'app' (la raíz no tiene fila)");
44
+ }
45
+ }
46
+ if (options.maxNodes !== undefined && !(Number.isInteger(options.maxNodes) && options.maxNodes >= 1)) {
47
+ throw new AuthorizationConfigError(`sqlDescendantsOf: maxNodes debe ser un entero >= 1 (llegó ${String(options.maxNodes)})`);
48
+ }
49
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
50
+ const connectionName = options.connection;
51
+ /** El SQL por carácter de cita (se construye una vez por dialecto). */
52
+ const statements = new Map();
53
+ function statementsFor(quote) {
54
+ let built = statements.get(quote);
55
+ if (built)
56
+ return built;
57
+ const q = (name) => `${quote}${name}${quote}`;
58
+ const typeSelect = typeCol ? `t.${q(typeCol)}` : 'NULL';
59
+ // MySQL corta una CTE recursiva a `cte_max_recursion_depth` iteraciones
60
+ // (1000 por defecto) con el error 3636: con una cota mayor, un ciclo
61
+ // salía como 503 «backend caído» en vez del 422 «posible ciclo» del
62
+ // contrato (2.5 · J3, hallazgo). El hint `SET_VAR` sube el tope SOLO para
63
+ // esta sentencia, a la cota + 1 que la propia consulta ya impone con
64
+ // `depth < ?`; el placeholder `%DEPTH%` se rellena con ese entero (nunca
65
+ // con entrada del consumidor). Los demás dialectos no tienen tope.
66
+ const hint = quote === '`' ? '/*+ SET_VAR(cte_max_recursion_depth = %DEPTH%) */ ' : '';
67
+ built = {
68
+ exists: `SELECT 1 AS ${q('ok')} FROM ${q(table)} t WHERE %WHERE% LIMIT 1`,
69
+ walk: `WITH RECURSIVE ${q('authz_descendants')}(${q('uuid')}, ${q('type')}, ${q('depth')}) AS (` +
70
+ ` SELECT t.${q(uuidCol)}, ${typeSelect}, 1 FROM ${q(table)} t WHERE ` +
71
+ `%ANCHOR%` +
72
+ ` UNION ALL` +
73
+ ` SELECT t.${q(uuidCol)}, ${typeSelect}, d.${q('depth')} + 1 FROM ${q(table)} t` +
74
+ ` JOIN ${q('authz_descendants')} d ON t.${q(parentCol)} = d.${q('uuid')} WHERE d.${q('depth')} < ?` +
75
+ `) SELECT ${hint}${q('uuid')}, ${q('type')} FROM ${q('authz_descendants')} LIMIT ?`,
76
+ };
77
+ statements.set(quote, built);
78
+ return built;
79
+ }
80
+ return async (scope, { maxNodes }) => {
81
+ assertScope(scope);
82
+ const cap = Math.min(maxNodes, options.maxNodes ?? Infinity);
83
+ if (!(Number.isInteger(cap) && cap >= 1)) {
84
+ throw new AuthorizationConfigError(`sqlDescendantsOf: maxNodes debe ser un entero >= 1 (llegó ${String(maxNodes)})`);
85
+ }
86
+ const connection = database.connection(connectionName);
87
+ const dialect = connection?.dialect?.name ?? connection?.client?.driverName ?? 'desconocido';
88
+ const quote = QUOTE_BY_DIALECT[dialect];
89
+ if (!quote) {
90
+ throw new UnsupportedDialectError(`sqlDescendantsOf: dialecto '${dialect}' sin observación en la suite (hoy: PostgreSQL, MySQL 8 y SQLite). ` +
91
+ `Implementa scopes.descendantsOf a mano para este motor.`);
92
+ }
93
+ const q = (name) => `${quote}${name}${quote}`;
94
+ const sql = statementsFor(quote);
95
+ const query = (operation, text, bindings) => guardSql('sqlDescendantsOf', operation, timeoutMs, () => connection.rawQuery(text, bindings));
96
+ // Forma del resultado crudo por cliente: pg `{ rows }`, sqlite `rows[]`,
97
+ // mysql2 `[rows[], fields[]]` (2.5 · J3).
98
+ const rowsOf = (result) => {
99
+ if (Array.isArray(result)) {
100
+ return result.length === 2 && Array.isArray(result[0]) && Array.isArray(result[1]) ? result[0] : result;
101
+ }
102
+ return result?.rows ?? [];
103
+ };
104
+ let anchor;
105
+ let bindings;
106
+ if (scope.type === APP_SCOPE_TYPE) {
107
+ anchor = `t.${q(parentCol)} IS NULL`;
108
+ bindings = [];
109
+ }
110
+ else {
111
+ if (fixedType !== null && scope.type !== fixedType)
112
+ return null;
113
+ const where = `t.${q(uuidCol)} = ?` + (typeCol ? ` AND t.${q(typeCol)} = ?` : '');
114
+ const args = typeCol ? [scope.uuid, scope.type] : [scope.uuid];
115
+ const exists = rowsOf(await query('descendantsOf.exists', sql.exists.replace('%WHERE%', where), args));
116
+ if (exists.length === 0)
117
+ return null;
118
+ anchor = `t.${q(parentCol)} = ?`;
119
+ bindings = [scope.uuid];
120
+ }
121
+ const walk = sql.walk.replace('%ANCHOR%', anchor).replace('%DEPTH%', String(cap + 2));
122
+ const rows = rowsOf(await query('descendantsOf', walk, [...bindings, cap + 1, cap + 1]));
123
+ if (rows.length > cap) {
124
+ throw new TooManyScopesError(`sqlDescendantsOf: ${scope.type}:${scope.uuid ?? ''} tiene más de ${cap} descendientes — o la tabla tiene un ` +
125
+ `posible ciclo (un ciclo alcanzable produce una fila por nivel hasta la cota); no se devuelve una lista parcial.`);
126
+ }
127
+ return rows.map((row) => ({ type: typeCol ? row.type : fixedType, uuid: row.uuid }));
128
+ };
129
+ }
130
+ export function sqlScopeEdges(options, database = db) {
131
+ const table = identifier('table', options.table);
132
+ const uuidCol = identifier('uuidColumn', options.uuidColumn);
133
+ const parentCol = identifier('parentColumn', options.parentColumn);
134
+ if ((options.typeColumn === undefined) === (options.scopeType === undefined)) {
135
+ throw new AuthorizationConfigError('sqlScopeEdges: declara exactamente uno de typeColumn (el tipo viene de la fila) o scopeType (tipo fijo).');
136
+ }
137
+ const typeCol = options.typeColumn === undefined ? null : identifier('typeColumn', options.typeColumn);
138
+ const fixedType = options.scopeType ?? null;
139
+ if (fixedType !== null) {
140
+ try {
141
+ assertScopeType(fixedType);
142
+ }
143
+ catch (error) {
144
+ throw new AuthorizationConfigError(`sqlScopeEdges: scopeType inválido: ${error.message}`);
145
+ }
146
+ if (fixedType === APP_SCOPE_TYPE) {
147
+ throw new AuthorizationConfigError("sqlScopeEdges: scopeType no puede ser 'app' (la raíz no tiene fila)");
148
+ }
149
+ }
150
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
151
+ const connectionName = options.connection;
152
+ return async ({ limit, after }) => {
153
+ if (!Number.isInteger(limit) || limit < 1) {
154
+ throw new AuthorizationConfigError(`sqlScopeEdges: limit debe ser un entero >= 1 (llegó ${String(limit)})`);
155
+ }
156
+ const connection = database.connection(connectionName);
157
+ const dialect = connection?.dialect?.name ?? connection?.client?.driverName ?? 'desconocido';
158
+ const quote = QUOTE_BY_DIALECT[dialect];
159
+ if (!quote) {
160
+ throw new UnsupportedDialectError(`sqlScopeEdges: dialecto '${dialect}' sin observación en la suite (hoy: PostgreSQL, MySQL 8 y SQLite). ` +
161
+ `Implementa scopes.enumerateEdges a mano para este motor.`);
162
+ }
163
+ const q = (name) => `${quote}${name}${quote}`;
164
+ const childType = typeCol ? `t.${q(typeCol)}` : 'NULL';
165
+ const parentType = typeCol ? `p.${q(typeCol)}` : 'NULL';
166
+ const text = `SELECT t.${q(uuidCol)} AS ${q('child_uuid')}, ${childType} AS ${q('child_type')},` +
167
+ ` t.${q(parentCol)} AS ${q('parent_uuid')}, ${parentType} AS ${q('parent_type')},` +
168
+ ` p.${q(uuidCol)} AS ${q('parent_row')}` +
169
+ ` FROM ${q(table)} t LEFT JOIN ${q(table)} p ON t.${q(parentCol)} = p.${q(uuidCol)}` +
170
+ (after === undefined ? '' : ` WHERE t.${q(uuidCol)} > ?`) +
171
+ ` ORDER BY t.${q(uuidCol)} LIMIT ?`;
172
+ const bindings = after === undefined ? [limit] : [after, limit];
173
+ const result = await guardSql('sqlScopeEdges', 'enumerateEdges', timeoutMs, () => connection.rawQuery(text, bindings));
174
+ const rows = Array.isArray(result)
175
+ ? result.length === 2 && Array.isArray(result[0]) && Array.isArray(result[1])
176
+ ? result[0]
177
+ : result
178
+ : (result?.rows ?? []);
179
+ const edges = [];
180
+ for (const row of rows) {
181
+ const child = { type: typeCol ? row.child_type : fixedType, uuid: String(row.child_uuid) };
182
+ if (row.parent_uuid === null || row.parent_uuid === undefined) {
183
+ edges.push({ child, parent: APP_SCOPE });
184
+ continue;
185
+ }
186
+ // Padre declarado que no tiene fila: el nodo NO está en el árbol
187
+ // (`resolveChain` devuelve `null` ahí). No se inventa la arista.
188
+ if (row.parent_row === null || row.parent_row === undefined)
189
+ continue;
190
+ edges.push({
191
+ child,
192
+ parent: { type: typeCol ? row.parent_type : fixedType, uuid: String(row.parent_uuid) },
193
+ });
194
+ }
195
+ // El cursor es la ÚLTIMA FILA LEÍDA, no la última arista emitida: si no,
196
+ // una fila con el padre colgando pararía la paginación en seco.
197
+ const last = rows[rows.length - 1];
198
+ return rows.length < limit
199
+ ? { edges }
200
+ : { edges, cursor: String(last.child_uuid) };
201
+ };
202
+ }
203
+ //# sourceMappingURL=sql_descendants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_descendants.js","sourceRoot":"","sources":["../../src/sql_descendants.ts"],"names":[],"mappings":"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"}
@@ -1,27 +1,232 @@
1
1
  import '@japa/assert';
2
+ import type { Assert } from '@japa/assert';
2
3
  import type { AuthorizationDriver, CatalogSpec } from '../types.js';
4
+ import type { ContractScopeTree } from './scope_tree.js';
3
5
  /**
4
6
  * Suite de contrato del sistema de autorización — EL JUEZ.
5
7
  *
6
8
  * Cualquier driver (database, openfga, custom del consumidor) debe pasar
7
9
  * estos casos sin modificarlos: son la definición ejecutable de la semántica
8
- * documentada en `#services/authorization/types`. Para enchufar un driver:
10
+ * documentada en `../types.js`. Para enchufar un driver:
9
11
  *
10
12
  * runAuthorizationDriverContract({
11
13
  * name: 'mi-driver',
12
- * makeDriver: () => new MiDriver(),
13
- * seedCatalog: (catalog) => ..., // materializa roles/permisos
14
- * cleanup: () => ..., // borra hechos + catálogo entre tests
14
+ * level: '2.0', // omitido = 'core': solo los casos de 1.x
15
+ * capabilities: { hierarchyFacts: false, transactions: false, truncationSignal: false,
16
+ * singleCheckAuthorize: false, injectableClock: false },
17
+ * seedCatalog: (catalog) => ..., // materializa roles/permisos
18
+ * makeDriver: (tree) => new MiDriver({ resolveChain: ... }), // recibe el árbol
19
+ * cleanup: () => ..., // borra hechos + catálogo entre tests
15
20
  * })
16
21
  *
22
+ * Llámala en el TOP LEVEL del spec (no dentro de un `test` ni de un hook):
23
+ * Japa registra los grupos al importar el archivo y descarta los tardíos.
24
+ *
17
25
  * `seedCatalog`/`cleanup` son del harness (no del contrato) porque cada
18
26
  * backend materializa el catálogo a su manera (tablas propias, FGA model...).
27
+ *
28
+ * El ÁRBOL de scopes también es del harness (`makeTree`, por defecto
29
+ * `memoryScopeTree()`): la suite lo construye caso a caso con `tree.attach` y
30
+ * el driver lo ve a través de lo que el harness le inyecte (un
31
+ * `resolveChain` que lo camina, o hechos en el backend). Así el
32
+ * invariante 1 se prueba con cadenas reales y no con el resolutor plano.
33
+ */
34
+ /**
35
+ * Lo que un driver DECLARA poder hacer. Cada capacidad tiene en la suite un
36
+ * par de casos `{ whenTrue, whenFalse }` — nunca un `skip`. Hoy hay par para
37
+ * `truncationSignal: false` (con `exhaustiveLists`) y `hierarchyFacts: false`;
38
+ * los demás llegan con su fase, y mientras tanto declararlos `true` hace que
39
+ * la suite lance al registrarse: una promesa sin juez no pasa.
19
40
  */
41
+ export interface DriverCapabilities {
42
+ /** El driver materializa el árbol como hechos propios (Fase 3b). */
43
+ hierarchyFacts: boolean;
44
+ /** Acepta `{ trx }` externa en las escrituras (Fase 2.5). */
45
+ transactions: boolean;
46
+ /**
47
+ * Los `list*` lanzan si el backend trunca. Ningún driver del paquete trunca
48
+ * (L0.7 se cerró enumerando con `Read` paginado), así que no hay caso para
49
+ * `true`: es el par que un driver de terceros con backend con tope tendría
50
+ * que traer.
51
+ */
52
+ truncationSignal: boolean;
53
+ /** `authorize` = una sola llamada al backend (modo facts, Fase 3b). */
54
+ singleCheckAuthorize: boolean;
55
+ /**
56
+ * El driver acepta un reloj inyectado (2.5 · J1): `withClock(now)` en el
57
+ * puerto devuelve una vista que evalúa la caducidad con ESE `now` (SQL,
58
+ * `current_time` de FGA, filtros en cliente y los tres estados del
59
+ * re-grant). Con `true` el juez fija el instante y observa la caducidad
60
+ * exacta (T−1 ms concede, T no) sin dormir; con `false` solo puede
61
+ * observarla en tiempo real (el caso de los tres estados espera 1,5 s).
62
+ */
63
+ injectableClock: boolean;
64
+ /**
65
+ * Los `list*` devuelven TODO sin tope. Es la verdad de los dos drivers del
66
+ * paquete: `database` (SQL sin límite) y `openfga` (enumeración por `Read`
67
+ * paginado, L0.7). Con `false` el harness debe dar `limits.listMaxResults`
68
+ * y el juez prueba solo la frontera exacta: es la declaración honesta de
69
+ * un driver de terceros cuyo backend trunca y que no lo señala.
70
+ */
71
+ exhaustiveLists: boolean;
72
+ /**
73
+ * El driver implementa el método opcional `listDenies` (2.1). Solo tiene
74
+ * casos en `level: '2.1'` (2E · I5): con `true` se juzgan `listDenies`,
75
+ * `effectivePermissions`, `authorizedScopes` y la versión compartida del
76
+ * catálogo; con `false` se juzga que esas primitivas lo digan (500
77
+ * `E_AUTHZ_UNSUPPORTED` nombrándolo) en vez de simular «sin denies». En un
78
+ * harness `core`/`'2.0'` no hay caso que lo observe: declara `false`.
79
+ */
80
+ listDenies: boolean;
81
+ /**
82
+ * `purgeRole(roleUuid)` está implementado de verdad (3B · B4): revoca
83
+ * todas las asignaciones del rol en todos los scopes, borra sus vínculos
84
+ * y el rol, atómicamente, y recrear el slug no revive nada. Solo tiene
85
+ * casos en `level: '2.2'`: con `true` se juzga la purga; con `false` se
86
+ * juzga que el driver lo DIGA (500 `E_AUTHZ_UNSUPPORTED`) y que no deje
87
+ * nada a medias — el driver `openfga` del paquete hasta 3b (`facts` +
88
+ * `reconcile`): sus bindings no son enumerables por rol sin leer el
89
+ * store entero. En un harness por debajo de `'2.2'` declara `false`.
90
+ */
91
+ purgeRole: boolean;
92
+ /**
93
+ * `countRoleAssignments(uuids)` está implementado de verdad (3b-2j): el
94
+ * driver sabe decir cuántos hechos VIGENTES tiene cada rol, que es lo que
95
+ * hace verdadero el `stillGranting` de `pruneOrphanRoles` —el campo que se
96
+ * lee justo antes de un borrado destructivo—. Con `true` se juzga el
97
+ * conteo (caducidad estricta, por posición, 422 por uuid mal formado); con
98
+ * `false`, que el barrido diga `undefined` y **nunca** `false`: «no lo sé»
99
+ * no puede degradar a «no concede». Solo tiene casos en `level: '2.2'`.
100
+ *
101
+ * **Es la capacidad que este lote estrena y es breaking**: un driver de
102
+ * terceros de 2.2 la declara `false` hasta que traiga el método.
103
+ */
104
+ countRoleAssignments: boolean;
105
+ /**
106
+ * Las escrituras del CATÁLOGO se serializan de verdad en el motor (3E ·
107
+ * R2, tester): dos `defineScopedRole` del mismo `(slug, nivel)` en
108
+ * paralelo terminan con EXACTAMENTE uno confirmado y el otro con 422
109
+ * `E_AUTHZ_CATALOG_CONFLICT` — nunca un fallo de backend. Es lo que da el
110
+ * cerrojo sobre la fila de `authz_catalog_version` (3D · M2) en PostgreSQL
111
+ * y MySQL, y es la promesa que el README hace.
112
+ *
113
+ * Con `false` (SQLite, que serializa bloqueando la BASE ENTERA) el juez
114
+ * solo puede exigir lo innegociable: nunca dos ganadores y el perdedor no
115
+ * escribe — su transacción puede morir con `SQLITE_BUSY`, que es un 503
116
+ * legítimo. Declararlo `true` en un motor así convierte un flake en un
117
+ * fallo; declararlo `false` en PG/MySQL deja pasar un mutante que
118
+ * convierte la colisión en 503. Solo tiene casos en `level: '2.2'`.
119
+ */
120
+ serializedCatalogWrites: boolean;
121
+ /**
122
+ * El backend resuelve la MEMBRESÍA por sí mismo (3b-2e · E2). **`false` en
123
+ * los dos drivers del paquete, también en el modo `facts`** (panel 2, cruce
124
+ * 6): `hasRole`, `listRoles`, `listRoleScopes`, `listSubjects` y
125
+ * `listScopes` siguen usando el `resolveChain` del consumidor. Con `false`
126
+ * el juez lo DEMUESTRA con un espía sobre el árbol, que es lo que impide
127
+ * vender «sin SQL en el camino caliente» a secas: lo cierto es «sin SQL por
128
+ * request en `authorize`». La cara `true` no tiene caso todavía: no hay
129
+ * driver que lo cumpla.
130
+ */
131
+ roleInheritanceNative: boolean;
132
+ /**
133
+ * Los `list*` enumeran también lo HEREDADO (3b-2e · E2). **`false` siempre
134
+ * en este paquete** (invariante 7): enumerar descendientes sería abierto, y
135
+ * en `openfga` obligaría a `ListObjects`, que trunca al tope del servidor
136
+ * SIN señal (S16, y `Check` y `ListObjects` ni siquiera coinciden en
137
+ * profundidad). Con `false` el juez fija que un grant en el ANCESTRO no
138
+ * aparece en las listas del descendiente aunque `authorize` diga `true`.
139
+ */
140
+ listObjectsInherited: boolean;
141
+ /**
142
+ * **Las LECTURAS canonizan la ortografía del scope contra el árbol del
143
+ * consumidor antes de buscar los hechos** (3b-2k · K1 · R2 (c)). El árbol
144
+ * del consumidor puede fundir dos formas del mismo id —el tipo `uuid` de
145
+ * PostgreSQL y la collation `*_ci` de MySQL leen `bbbb…` con y sin guiones
146
+ * como la MISMA fila—, y `authz_*` compara por bytes.
147
+ *
148
+ * Con `true` (driver `database`) `authorize` resuelve la cadena y usa
149
+ * `chain[0]`, la identidad CANÓNICA (invariante 17): un alias que el árbol
150
+ * funde con la fila real encuentra exactamente los mismos hechos que la
151
+ * forma canónica.
152
+ *
153
+ * Con `false` (`openfga` en modo `facts`) la DECISIÓN no pasa por el árbol
154
+ * —es la contrapartida de `singleCheckAuthorize`— así que el objeto del
155
+ * store se compone con la ortografía del LLAMANTE y un alias no encuentra
156
+ * nada: `authorize` responde `false` donde la forma canónica concede. Es
157
+ * fail-**CLOSED** y no evade ningún deny, pero **no es la misma respuesta
158
+ * que `database`** y por eso se declara. Lo que NO cambia es la ESCRITURA:
159
+ * `grant`/`revoke`/`removeDeny`/`purgeScope` sí canonizan en los dos
160
+ * drivers (3b-2h · 🟠 3 cerró la cara fail-open), y el juez lo fija en las
161
+ * DOS caras del par para que nadie lea esta capacidad como «en `facts` la
162
+ * ortografía da igual».
163
+ */
164
+ canonicalScopeReads: boolean;
165
+ /**
166
+ * El driver sabe ser el **ORIGEN** de `authz:reconcile` (3b-3b):
167
+ * `enumerateFacts` entrega sus hechos por páginas, con su caducidad y **sin
168
+ * filtrar** (una asignación caducada tiene que LLEGAR para que el destino
169
+ * la cuente en `skipped`; filtrarla la haría desaparecer sin rastro).
170
+ *
171
+ * Con `true` el juez pasea las páginas, comprueba que el cursor avanza,
172
+ * que no se devuelven más de `limit` hechos y que la caducada sale con su
173
+ * instante. Con `false` juzga que el driver no puede ser origen y que
174
+ * `authz:reconcile` lo DICE —500 `E_AUTHZ_UNSUPPORTED` nombrando el
175
+ * método— en vez de leer cero hechos y vaciar el destino con `--prune`.
176
+ *
177
+ * `false` en el driver `database` a propósito: sus hechos son
178
+ * `authz_assignments`/`authz_denies`, el esquema publicado del paquete, y
179
+ * el destino los lee de ahí. Solo tiene casos en `level: '2.2'`.
180
+ */
181
+ enumerateFacts: boolean;
182
+ }
183
+ export type ContractLevel = 'core' | '2.0' | '2.1' | '2.2';
20
184
  export interface DriverContractHarness {
21
185
  name: string;
22
- makeDriver: () => AuthorizationDriver | Promise<AuthorizationDriver>;
23
- seedCatalog: (catalog: CatalogSpec) => Promise<void>;
24
- cleanup: () => Promise<void>;
186
+ /** `core` (default): los casos de 1.x. `'2.0'`: además los de la Fase 0/1. `'2.1'`: además las primitivas de la Fase 2. `'2.2'`: además los roles locales a un scope (Fase 3). */
187
+ level?: ContractLevel;
188
+ capabilities: DriverCapabilities;
189
+ /** Tope de resultados del backend de test. Obligatorio con `exhaustiveLists: false`. */
190
+ limits?: {
191
+ listMaxResults?: number;
192
+ };
193
+ /**
194
+ * Materializa el catálogo del juez. Devuelve lo que quiera (3E: el
195
+ * `syncAuthzCatalog` del paquete devuelve su reporte): el juez no lo mira.
196
+ */
197
+ seedCatalog(catalog: CatalogSpec): Promise<unknown>;
198
+ /** Árbol que usará la suite. Default: `memoryScopeTree()`. */
199
+ makeTree?(): Promise<ContractScopeTree>;
200
+ makeDriver(tree: ContractScopeTree): AuthorizationDriver | Promise<AuthorizationDriver>;
201
+ /**
202
+ * OTRA instancia del driver sobre el MISMO backend de hechos y con su
203
+ * propio memo del catálogo — lo que sería el mismo driver en otro proceso
204
+ * (2D · F1). Default: si el driver expone `catalog` (`CatalogCache`, los
205
+ * dos del paquete), una vista por prototipo con un memo nuevo; si no, el
206
+ * mismo driver (un driver que lea el catálogo en vivo pasa igual).
207
+ */
208
+ makeTwin?(driver: AuthorizationDriver, tree: ContractScopeTree): AuthorizationDriver | Promise<AuthorizationDriver>;
209
+ cleanup(): Promise<void>;
210
+ }
211
+ /**
212
+ * Lo mínimo que el juez necesita del runner. En producción es el `test` de
213
+ * Japa; en `tests/contract_harness.spec.ts` es una API falsa que solo anota
214
+ * títulos, porque Japa no permite registrar grupos desde un test en marcha y
215
+ * la regla de capacidades hay que poder probarla.
216
+ */
217
+ export interface ContractTestApi {
218
+ group(title: string, define: (group: {
219
+ each: {
220
+ setup(fn: () => Promise<void>): unknown;
221
+ };
222
+ teardown(fn: () => Promise<void>): unknown;
223
+ }) => void): unknown;
224
+ test(title: string, fn: (ctx: {
225
+ assert: Assert;
226
+ }) => Promise<void>): {
227
+ timeout(ms: number): unknown;
228
+ } | undefined | void;
25
229
  }
26
230
  export declare function runAuthorizationDriverContract(harness: DriverContractHarness): void;
231
+ export declare function registerAuthorizationDriverContract(harness: DriverContractHarness, api: ContractTestApi): void;
27
232
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAA;AAGrB,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EAGZ,MAAM,aAAa,CAAA;AAGpB;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACpE,WAAW,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAoBD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,qBAAqB,QAkP5E"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"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"}