@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,144 @@
1
+ /**
2
+ * `defineRelationsConfig` — la API declarativa de ReBAC (Fase 4, lote 4-2).
3
+ *
4
+ * Valida la config de relaciones del consumidor y devuelve un objeto
5
+ * CONGELADO que el manager de relaciones consulta para F-05 (`isDeclared`).
6
+ * Es la puerta de entrada, AGNÓSTICA de driver: rige igual en `database` y en
7
+ * `openfga` (paridad ⚪4). El generador del modelo fusionado
8
+ * (`src/drivers/openfga_facts.ts`, lote 4-1) hace la MISMA comprobación de
9
+ * reservados por defensa en profundidad al emitir el modelo —y además la de
10
+ * F-04 (colisión con un permiso del catálogo), que necesita las tuplas del
11
+ * catálogo y por eso vive en la capa del modelo—; las dos comparten la fuente
12
+ * única de tipos reservados (`RESERVED_FACTS_TYPES` en `src/identity.ts`).
13
+ *
14
+ * Pureza (regla 2 de `check_purity.mjs`): este módulo solo depende del puerto
15
+ * (`../types.js`), de los errores y de la gramática compartida
16
+ * (`../identity.js`); JAMÁS de `../manager` ni de `../drivers/*`. Es lo que
17
+ * mantiene `relations/` desacoplado de `roles/`.
18
+ */
19
+ import { RelationConfigError } from '../errors.js';
20
+ import { RESERVED_FACTS_TYPES, RESERVED_SLUGS, RESERVED_SLUG_PREFIXES } from '../identity.js';
21
+ /** La cota de FGA para el nombre de una relación (50). Un tipo de objeto llega a 254. */
22
+ const RELATION_NAME_MAX = 50;
23
+ const OBJECT_TYPE_MAX = 254;
24
+ /** Nombres FGA: minúsculas, dígitos y `._-`; sin `:`/`#`/`@`/espacios ni `|`. */
25
+ const FGA_NAME_FORMAT = /^[a-z0-9][a-z0-9._-]*$/;
26
+ function reject(message) {
27
+ throw new RelationConfigError(message);
28
+ }
29
+ /**
30
+ * Valida y congela la config de relaciones. Config inválida ⇒ 422
31
+ * `E_AUTHZ_RELATION_CONFIG` (misma clase que el gate del modelo del lote 4-1).
32
+ */
33
+ export function defineRelationsConfig(spec) {
34
+ if (!spec || typeof spec !== 'object') {
35
+ reject(`Config de relaciones inválida: se esperaba un objeto y llegó ${JSON.stringify(spec)}.`);
36
+ }
37
+ // `membersOf` SOLO puede vivir en las opciones de `database` (⚪ del tester):
38
+ // ponerlo en la raíz o en un futuro bloque `openfga` es un error de
39
+ // configuración —`openfga` no puede resolver la transitiva sin truncar—.
40
+ if ('membersOf' in spec) {
41
+ reject(`Config de relaciones inválida: 'membersOf' es una opción del driver 'database' ` +
42
+ `(membresía transitiva por CTE recursiva) y va en 'database.membersOf', no en la raíz. ` +
43
+ `En 'openfga' la transitiva sería 'ListUsers', que trunca al tope del servidor.`);
44
+ }
45
+ if (!Array.isArray(spec.objectTypes) || spec.objectTypes.length === 0) {
46
+ reject(`Config de relaciones inválida: 'objectTypes' tiene que ser un array no vacío.`);
47
+ }
48
+ const holderTypes = spec.holderTypes ?? [];
49
+ for (const holder of holderTypes) {
50
+ if (typeof holder !== 'string') {
51
+ reject(`Config de relaciones inválida: un holder type no es una cadena (${JSON.stringify(holder)}).`);
52
+ }
53
+ }
54
+ const reservedTypes = new Set([...RESERVED_FACTS_TYPES, ...holderTypes]);
55
+ const seenTypes = new Set();
56
+ for (const objectType of spec.objectTypes) {
57
+ const type = objectType?.type;
58
+ if (typeof type !== 'string' || !FGA_NAME_FORMAT.test(type) || type.length > OBJECT_TYPE_MAX) {
59
+ reject(`Config de relaciones inválida: tipo de objeto ${JSON.stringify(type)} no es un tipo FGA válido ` +
60
+ `(minúsculas, dígitos y '._-'; sin ':'/'#'/'@'/'|'/espacios; 1-${OBJECT_TYPE_MAX} caracteres).`);
61
+ }
62
+ if (reservedTypes.has(type)) {
63
+ reject(`Config de relaciones inválida: el tipo de objeto '${type}' colisiona con un tipo reservado del ` +
64
+ `modelo compartido (${[...RESERVED_FACTS_TYPES].join(', ')}` +
65
+ `${holderTypes.length ? ', y los holder types ' + holderTypes.join(', ') : ''}). ` +
66
+ `En el store compartido un tipo de relaciones no puede duplicar uno de 'facts' (⚪4): ` +
67
+ `si pudiera declarar 'role_binding', 'relate' compondría el id de un binding real y escalaría a ` +
68
+ `roles.authorize. Renómbralo.`);
69
+ }
70
+ if (seenTypes.has(type)) {
71
+ reject(`Config de relaciones inválida: el tipo de objeto '${type}' está declarado dos veces.`);
72
+ }
73
+ seenTypes.add(type);
74
+ if (!Array.isArray(objectType.relations) || objectType.relations.length === 0) {
75
+ reject(`Config de relaciones inválida: el tipo '${type}' no declara ninguna relación.`);
76
+ }
77
+ const own = new Set();
78
+ for (const relation of objectType.relations) {
79
+ const name = relation?.name;
80
+ if (typeof name !== 'string' || !FGA_NAME_FORMAT.test(name)) {
81
+ reject(`Config de relaciones inválida: relación ${JSON.stringify(name)} del tipo '${type}' no es un ` +
82
+ `nombre de relación válido (minúsculas, dígitos y '._-').`);
83
+ }
84
+ if (name.length > RELATION_NAME_MAX) {
85
+ reject(`Config de relaciones inválida: la relación '${name}' del tipo '${type}' tiene ${name.length} ` +
86
+ `caracteres y FGA admite ${RELATION_NAME_MAX}.`);
87
+ }
88
+ if (RESERVED_SLUGS.has(name)) {
89
+ reject(`Config de relaciones inválida: la relación '${type}#${name}' usa un nombre reservado del modelo ` +
90
+ `facts (${[...RESERVED_SLUGS].join(', ')}): elegiría una relación propia de 'facts' (⚪4).`);
91
+ }
92
+ const family = RESERVED_SLUG_PREFIXES.find((prefix) => name.startsWith(prefix));
93
+ if (family) {
94
+ reject(`Config de relaciones inválida: la relación '${type}#${name}' empieza por '${family}', prefijo ` +
95
+ `reservado de las relaciones derivadas del modelo facts (${RESERVED_SLUG_PREFIXES.join(', ')}): ` +
96
+ `pisaría el nombre de un permiso proyectado (⚪4).`);
97
+ }
98
+ if (own.has(name)) {
99
+ reject(`Config de relaciones inválida: la relación '${type}#${name}' está declarada dos veces.`);
100
+ }
101
+ own.add(name);
102
+ }
103
+ // Los `includes` refieren relaciones del MISMO tipo (un nivel, SIN `from`).
104
+ for (const relation of objectType.relations) {
105
+ for (const included of relation.includes ?? []) {
106
+ if (typeof included !== 'string') {
107
+ reject(`Config de relaciones inválida: un 'includes' de '${type}#${relation.name}' no es una cadena.`);
108
+ }
109
+ // Un `from` (`viewer from parent`) se escribe con un separador; v1 no lo soporta.
110
+ if (/[:#@|. ]/.test(included) && !own.has(included)) {
111
+ reject(`Config de relaciones inválida: '${type}#${relation.name}' incluye '${included}', que parece una ` +
112
+ `herencia cross-objeto ('from'). v1 solo soporta includes de un nivel y del MISMO tipo.`);
113
+ }
114
+ if (!own.has(included)) {
115
+ reject(`Config de relaciones inválida: '${type}#${relation.name}' incluye '${included}', que no es una ` +
116
+ `relación de '${type}'. Los includes son de un nivel y del mismo tipo (v1 no soporta 'from').`);
117
+ }
118
+ if (included === relation.name) {
119
+ reject(`Config de relaciones inválida: '${type}#${relation.name}' se incluye a sí misma.`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ const declared = new Map();
125
+ // `group` es el tipo BUILT-IN de las relaciones (el portador de los
126
+ // usersets, `group:g#member`): lo emite SIEMPRE el generador del modelo, no
127
+ // el consumidor —por eso NO se declara en `objectTypes` (arriba se rechaza)
128
+ // pero SÍ es un tipo válido para F-05: `relate(group:g#member, viewer, doc)`
129
+ // y `relate(u, member, group:g)` son legales. `role_binding`/`scope`/`role`
130
+ // siguen fuera: la frontera del 🔴 se mantiene.
131
+ declared.set('group', new Set(['member']));
132
+ const frozenTypes = spec.objectTypes.map((objectType) => {
133
+ const relations = objectType.relations.map((relation) => Object.freeze({ name: relation.name, includes: relation.includes ? Object.freeze([...relation.includes]) : undefined }));
134
+ declared.set(objectType.type, new Set(relations.map((r) => r.name)));
135
+ return Object.freeze({ type: objectType.type, relations: Object.freeze(relations) });
136
+ });
137
+ return Object.freeze({
138
+ objectTypes: Object.freeze(frozenTypes),
139
+ membersOf: spec.database?.membersOf === true,
140
+ hasType: (objectType) => declared.has(objectType),
141
+ isDeclared: (objectType, relation) => declared.get(objectType)?.has(relation) ?? false,
142
+ });
143
+ }
144
+ //# sourceMappingURL=define_relations_config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define_relations_config.js","sourceRoot":"","sources":["../../../src/relations/define_relations_config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { RelObject, RelSubject, RelationRef, RelationWriteEvent, RelationWriteOptions, RelationsDriver, RelationObjectsPage, RelationSubjectsPage, RelationPage, RelationTuplePage, ScopeRef } from '../types.js';
2
+ import type { RelationsConfig } from './define_relations_config.js';
3
+ export interface RelationsManagerOptions {
4
+ /**
5
+ * Comprobación PURA de escritura (R-13): recibe la referencia completa de la
6
+ * tupla (sujeto, relación, objeto, partición, operación) y lanza para
7
+ * rechazarla. Corre DESPUÉS de F-05 y ANTES de tocar el driver.
8
+ */
9
+ assertWrite?(ref: RelationRef): void;
10
+ /** Auditoría: se notifica DESPUÉS de una escritura confirmada, con el `actor`. */
11
+ onRelationWrite?(event: RelationWriteEvent): void;
12
+ /** `true` exige `actor` en toda escritura (paridad con `requireActor` de roles). */
13
+ requireActor?: boolean;
14
+ }
15
+ export declare class RelationsManager {
16
+ #private;
17
+ constructor(driver: RelationsDriver, config: RelationsConfig, options?: RelationsManagerOptions);
18
+ /** El driver crudo — salida documentada de las barreras (plataforma y tests). */
19
+ driver(): RelationsDriver;
20
+ relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
21
+ unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
22
+ check(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef): Promise<boolean>;
23
+ listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, page?: RelationPage): Promise<RelationObjectsPage>;
24
+ listSubjects(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
25
+ purgeObject(object: RelObject, partition: ScopeRef): Promise<void>;
26
+ purgeSubject(subject: RelSubject, partition: ScopeRef): Promise<void>;
27
+ /**
28
+ * Membresía TRANSITIVA de un grupo (a través de usersets anidados) —
29
+ * DISTINTA de `listSubjects`, que da los hechos DIRECTOS. Solo la trae el
30
+ * driver `database` (`membersOfNative: true`); en `openfga` es 500
31
+ * `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
32
+ * trunca), jamás un `skip`.
33
+ */
34
+ membersOf(object: RelObject, relation: string, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
35
+ /** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
36
+ enumerateRelations(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
37
+ }
38
+ //# sourceMappingURL=manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/relations/manager.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACT,MAAM,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEnE,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,WAAW,CAAC,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,kFAAkF;IAClF,eAAe,CAAC,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACjD,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,qBAAa,gBAAgB;;gBAKf,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAE,uBAA4B;IAMnG,iFAAiF;IACjF,MAAM,IAAI,eAAe;IAmDnB,MAAM,CACV,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAYV,QAAQ,CACZ,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAYV,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAMtG,WAAW,CACf,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,QAAQ,EACnB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,mBAAmB,CAAC;IAKzB,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,QAAQ,EACnB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,oBAAoB,CAAC;IAM1B,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlE,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3E;;;;;;OAMG;IACG,SAAS,CACb,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,QAAQ,EACnB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,oBAAoB,CAAC;IAchC,oGAAoG;IAC9F,kBAAkB,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAuB/F"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `RelationsManager` — la fachada de ReBAC (Fase 4, lote 4-2).
3
+ *
4
+ * Es el análogo de `AuthorizationManager` para el puerto `RelationsDriver`:
5
+ * aplica la POLICY antes de tocar el driver y despacha la auditoría después.
6
+ * Lo que fija este lote:
7
+ *
8
+ * - **F-05 (cierre del 🔴 del auditor)**: `relate`/`unrelate` con un
9
+ * `object.type` o una `relation` que NO estén declarados en
10
+ * `defineRelationsConfig` ⇒ 422 ANTES de llamar al driver. Junto con ⚪4
11
+ * (un tipo reservado no se puede declarar) hace IMPOSIBLE que un `relate`
12
+ * componga el id de un `role_binding` real y escale a `roles.authorize`.
13
+ * - **R-13**: `assertWrite` PURO (obligatorio aquí, a diferencia de
14
+ * `roles/`): si rechaza, nada toca el driver.
15
+ * - `actor` en `onRelationWrite` (auditoría del consumidor sin
16
+ * `AsyncLocalStorage`).
17
+ * - `membersOf` como método OPCIONAL DISTINTO de `listSubjects`: si el driver
18
+ * no lo trae (`membersOfNative: false`) ⇒ 500 `E_AUTHZ_UNSUPPORTED`
19
+ * nombrándolo, jamás un `skip`.
20
+ *
21
+ * Pureza (regla 2): solo depende del puerto, los errores, la gramática
22
+ * compartida y `defineRelationsConfig`. Nada de `manager`/`drivers/*`.
23
+ */
24
+ import { ActorRequiredError, RelationTypeUnknownError, RelationUnknownError, UnsupportedOperationError, } from '../errors.js';
25
+ import { assertScope, assertSubject, assertRelationId } from '../identity.js';
26
+ import { isRelUserset } from '../types.js';
27
+ export class RelationsManager {
28
+ #driver;
29
+ #config;
30
+ #options;
31
+ constructor(driver, config, options = {}) {
32
+ this.#driver = driver;
33
+ this.#config = config;
34
+ this.#options = options;
35
+ }
36
+ /** El driver crudo — salida documentada de las barreras (plataforma y tests). */
37
+ driver() {
38
+ return this.#driver;
39
+ }
40
+ /**
41
+ * **F-05**: rechaza un tipo/relación no declarado ANTES de tocar el driver.
42
+ * La gramática de identidad no basta: `role_binding`/`assignee` son
43
+ * gramaticalmente válidos y en el store compartido componen el id de un
44
+ * binding real.
45
+ */
46
+ #assertDeclared(object, relation) {
47
+ if (!object || typeof object !== 'object' || typeof object.type !== 'string') {
48
+ throw new RelationTypeUnknownError(`Objeto de relación inválido: se esperaba { type, id } y llegó ${JSON.stringify(object)}.`);
49
+ }
50
+ if (!this.#config.hasType(object.type)) {
51
+ throw new RelationTypeUnknownError(`El tipo de objeto '${object.type}' no está declarado en defineRelationsConfig (F-05): ` +
52
+ `relate/unrelate solo aceptan tipos declarados. En el store compartido un tipo no declarado ` +
53
+ `podría componer el id de un 'role_binding' real y escalar a roles.authorize.`);
54
+ }
55
+ if (!this.#config.isDeclared(object.type, relation)) {
56
+ throw new RelationUnknownError(`La relación '${relation}' no está declarada para el tipo '${object.type}' en ` +
57
+ `defineRelationsConfig (F-05).`);
58
+ }
59
+ }
60
+ /**
61
+ * **R-16**: el `object.id` con la gramática estricta de relaciones ANTES del
62
+ * driver ⇒ 422 `E_AUTHZ_INVALID_IDENTITY`. `#assertDeclared` solo valida el
63
+ * TIPO (F-05): un `object.id` con un `|`/`#`/`:`/espacio se colaba y en
64
+ * `openfga` se ESCRIBÍA (invisibilidad de enumeración y pérdida en
65
+ * `reconcile`) o salía como 503 mal clasificado. Se valida aquí (una vez) y
66
+ * en cada driver (defensa en profundidad).
67
+ */
68
+ #assertObjectId(object) {
69
+ assertRelationId(`el id del objeto '${object?.type}'`, object?.id);
70
+ }
71
+ #assertActor(options) {
72
+ if (this.#options.requireActor && !options?.actor) {
73
+ throw new ActorRequiredError(`Escritura de relación sin 'actor' y la config exige uno (requireActor).`);
74
+ }
75
+ }
76
+ async relate(subject, relation, object, partition, options) {
77
+ this.#assertDeclared(object, relation);
78
+ this.#assertObjectId(object);
79
+ this.#assertActor(options);
80
+ assertScope(partition);
81
+ this.#assertSubject(subject, partition);
82
+ const ref = { operation: 'relate', subject, relation, object, partition };
83
+ this.#options.assertWrite?.(ref);
84
+ await this.#driver.relate(subject, relation, object, partition, options);
85
+ this.#options.onRelationWrite?.({ ...ref, actor: options?.actor });
86
+ }
87
+ async unrelate(subject, relation, object, partition, options) {
88
+ this.#assertDeclared(object, relation);
89
+ this.#assertObjectId(object);
90
+ this.#assertActor(options);
91
+ assertScope(partition);
92
+ this.#assertSubject(subject, partition);
93
+ const ref = { operation: 'unrelate', subject, relation, object, partition };
94
+ this.#options.assertWrite?.(ref);
95
+ await this.#driver.unrelate(subject, relation, object, partition, options);
96
+ this.#options.onRelationWrite?.({ ...ref, actor: options?.actor });
97
+ }
98
+ async check(subject, relation, object, partition) {
99
+ assertScope(partition);
100
+ this.#assertObjectId(object);
101
+ return this.#driver.check(subject, relation, object, partition);
102
+ }
103
+ async listObjects(subject, relation, objectType, partition, page) {
104
+ assertScope(partition);
105
+ return this.#driver.listObjects(subject, relation, objectType, partition, page);
106
+ }
107
+ async listSubjects(relation, object, partition, page) {
108
+ assertScope(partition);
109
+ this.#assertObjectId(object);
110
+ return this.#driver.listSubjects(relation, object, partition, page);
111
+ }
112
+ async purgeObject(object, partition) {
113
+ assertScope(partition);
114
+ this.#assertObjectId(object);
115
+ return this.#driver.purgeObject(object, partition);
116
+ }
117
+ async purgeSubject(subject, partition) {
118
+ assertScope(partition);
119
+ return this.#driver.purgeSubject(subject, partition);
120
+ }
121
+ /**
122
+ * Membresía TRANSITIVA de un grupo (a través de usersets anidados) —
123
+ * DISTINTA de `listSubjects`, que da los hechos DIRECTOS. Solo la trae el
124
+ * driver `database` (`membersOfNative: true`); en `openfga` es 500
125
+ * `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
126
+ * trunca), jamás un `skip`.
127
+ */
128
+ async membersOf(object, relation, partition, page) {
129
+ assertScope(partition);
130
+ if (!this.#driver.membersOf || !this.#driver.capabilities?.membersOfNative) {
131
+ throw new UnsupportedOperationError('membersOf', 'la membresía transitiva de un grupo', 'relations', `Solo el driver 'database' la resuelve (CTE recursiva); en 'openfga' la transitiva sería ` +
132
+ `'ListUsers', que trunca. Usa 'listSubjects' para los hechos directos.`);
133
+ }
134
+ return this.#driver.membersOf(object, relation, partition, page);
135
+ }
136
+ /** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
137
+ async enumerateRelations(partition, page) {
138
+ assertScope(partition);
139
+ if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
140
+ throw new UnsupportedOperationError('enumerateRelations', 'ser origen de authz:reconcile de relaciones', 'relations');
141
+ }
142
+ return this.#driver.enumerateRelations(partition, page);
143
+ }
144
+ #assertSubject(subject, partition) {
145
+ if (isRelUserset(subject)) {
146
+ // El userset (`group:g#member`) también se ancla a la partición; su
147
+ // objeto se valida como cualquier otro objeto declarado (tipo + id, R-16).
148
+ this.#assertDeclared(subject.object, subject.relation);
149
+ this.#assertObjectId(subject.object);
150
+ assertScope(partition);
151
+ return;
152
+ }
153
+ assertSubject(subject);
154
+ }
155
+ }
156
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/relations/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { RelationsDriver, ScopeRef } from '../types.js';
2
+ import type { RelationsConfig } from './define_relations_config.js';
3
+ export interface RelationsReconcileOptions {
4
+ /** El driver del que se LEEN las tuplas (origen). Necesita `enumerateRelations`. */
5
+ from: RelationsDriver;
6
+ /** El driver al que se ESCRIBEN (destino). Necesita `enumerateRelations` para ver lo que sobra. */
7
+ to: RelationsDriver;
8
+ /** La partición a migrar (mono-tenant: `APP_SCOPE`). El reconcile es POR partición. */
9
+ partition: ScopeRef;
10
+ /** Solo reporta lo que HARÍA; cero escrituras (read-only por contrato, como el de roles). */
11
+ dryRun?: boolean;
12
+ /** Borra del destino las tuplas que el origen ya no respalda. Sin él, solo añade. */
13
+ prune?: boolean;
14
+ /**
15
+ * **El seguro de borrado masivo** (R-17): con `prune` y un ORIGEN sin una
16
+ * sola tupla utilizable, borrar TODO el destino es 500
17
+ * `E_AUTHZ_MASS_RECONCILE_REFUSED` (la firma de un `--from` equivocado o de un
18
+ * store vacío). `allowMassDelete: true` (`--allow-mass-delete`) es la decisión
19
+ * humana de vaciarlo de verdad; `--dry-run` no lanza, lo marca en `massDelete`.
20
+ * Paridad con el seguro de `authz:reconcile` de roles.
21
+ */
22
+ allowMassDelete?: boolean;
23
+ /**
24
+ * La config del DESTINO, para vigilar la deriva del modelo fusionado en
25
+ * `--dry-run`: un tipo de relación presente en el origen que el destino NO
26
+ * declara no cabría en su modelo fusionado (se reporta en `modelDrift`).
27
+ */
28
+ toConfig?: RelationsConfig;
29
+ }
30
+ export interface RelationsReconcileReport {
31
+ /** ¿Fue `--dry-run` (sin escrituras)? */
32
+ dryRun: boolean;
33
+ /** Tuplas nuevas escritas en el destino (o que se escribirían, en `--dry-run`). */
34
+ written: number;
35
+ /** Tuplas que sobraban en el destino y se borraron (solo con `prune`; o que se borrarían). */
36
+ deleted: number;
37
+ /** Tuplas que ya estaban exactamente igual. */
38
+ unchanged: number;
39
+ /**
40
+ * Tuplas del destino que el origen ya no respalda y que NO se borraron por
41
+ * no pasar `--prune` (deriva recuperable; con `prune` pasan a `deleted`).
42
+ */
43
+ extra: number;
44
+ /**
45
+ * Deriva del modelo fusionado del destino (`--dry-run`): tipos de relación
46
+ * del origen que la config del destino no declara. Vacío si no aplica.
47
+ */
48
+ modelDrift: string[];
49
+ /**
50
+ * **El seguro de borrado masivo se disparó** (R-17): `prune` iba a borrar
51
+ * tuplas y el ORIGEN no aportó ni una utilizable. En una pasada real es 500
52
+ * (salvo `allowMassDelete`); en `--dry-run` NO lanza y esto lo marca.
53
+ */
54
+ massDelete: boolean;
55
+ }
56
+ /**
57
+ * Migra las tuplas de relación de `from` a `to` en `partition`. Idempotente
58
+ * (una segunda pasada no escribe nada) y bidireccional (funciona en cualquier
59
+ * sentido). Nunca silenciosa: devuelve `written`/`deleted`/`unchanged`/`extra`.
60
+ */
61
+ export declare function reconcileRelations(options: RelationsReconcileOptions): Promise<RelationsReconcileReport>;
62
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/relations/reconcile.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAA6B,eAAe,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAkBnE,MAAM,WAAW,yBAAyB;IACxC,oFAAoF;IACpF,IAAI,EAAE,eAAe,CAAA;IACrB,mGAAmG;IACnG,EAAE,EAAE,eAAe,CAAA;IACnB,uFAAuF;IACvF,SAAS,EAAE,QAAQ,CAAA;IACnB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,yCAAyC;IACzC,MAAM,EAAE,OAAO,CAAA;IACf,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAA;IACf,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAA;IACf,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;CACpB;AA8BD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAyE9G"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * **`authz:reconcile` de relaciones** (Fase 4, lote 4-5) — migra las tuplas de
3
+ * relación de un `RelationsDriver` a otro, idempotente y bidireccional, nunca
4
+ * silenciosa. Es el análogo de `manager.reconcile` de roles, pero para el
5
+ * puerto `RelationsDriver`: solo hay HECHOS de relación (no árbol ni catálogo),
6
+ * así que es más simple —lee el ORIGEN por `enumerateRelations`, lee el DESTINO
7
+ * ENTERO (la única forma de ver lo que SOBRA), y aplica writes y —con
8
+ * `prune`— deletes—.
9
+ *
10
+ * **La lección M1/M3/T4 de la 3b** (el agujero del 3b-4·C1): un contrato que
11
+ * solo cruza `report.skipped` se pasa PERDIENDO datos. El verificador de
12
+ * verdad es el CENSO (`src/testing/relations_reconcile_contract.ts`): busca
13
+ * cada hecho sembrado en el destino UNO A UNO por el camino DIRECTO
14
+ * (`listSubjects`), no le pregunta al origen lleno. Un tramo omisivo o una
15
+ * pérdida de `subject_relation` (un userset migrado como holder) o de
16
+ * partición se ve ROJA ahí, no en los contadores.
17
+ *
18
+ * Pureza (regla 2): este módulo vive en `src/relations/` y solo depende del
19
+ * puerto (`../types.js`), los errores y la config de relaciones. NO importa
20
+ * `manager` ni `drivers/*`: los drivers concretos los inyecta el llamante (la
21
+ * plataforma), como `manager.reconcile` recibe el destino por su clave.
22
+ */
23
+ import { MassReconcileRefusedError, UnsupportedOperationError } from '../errors.js';
24
+ import { isRelUserset } from '../types.js';
25
+ /** La clave estable de una tupla de relación (partición + objeto + relación + sujeto). */
26
+ function subjectKey(subject) {
27
+ if (isRelUserset(subject))
28
+ return `${subject.object.type}:${subject.object.id}#${subject.relation}`;
29
+ return `${subject.type}:${subject.uuid}`;
30
+ }
31
+ function scopeKeyOf(scope) {
32
+ return scope.type === 'app' ? 'app' : `${scope.type}|${scope.uuid}`;
33
+ }
34
+ function tupleKey(tuple) {
35
+ return `${scopeKeyOf(tuple.partition)}|${tuple.object.type}:${tuple.object.id}#${tuple.relation}@${subjectKey(tuple.subject)}`;
36
+ }
37
+ /** Enumera TODAS las tuplas de una partición de un driver (paginando el cursor). */
38
+ async function enumerateAll(driver, partition, role) {
39
+ if (!driver.enumerateRelations || !driver.capabilities?.enumerateRelations) {
40
+ throw new UnsupportedOperationError('enumerateRelations', `ser ${role} de authz:reconcile de relaciones`, 'relations', `El driver ${role} no sabe enumerar sus tuplas (capacidad 'enumerateRelations': false). ` +
41
+ `Sin poder leer ${role === 'destino' ? 'el destino no se ve qué sobra' : 'el origen no hay nada que migrar'}.`);
42
+ }
43
+ const out = [];
44
+ const seen = new Set();
45
+ let after;
46
+ let pages = 0;
47
+ do {
48
+ const page = await driver.enumerateRelations(partition, after ? { after } : undefined);
49
+ out.push(...page.tuples);
50
+ after = page.cursor || undefined;
51
+ if (after) {
52
+ if (seen.has(after))
53
+ throw new Error(`enumerateRelations (${role}): el cursor '${after}' se repite; no avanza`);
54
+ seen.add(after);
55
+ if (++pages > 100_000)
56
+ throw new Error(`enumerateRelations (${role}): más de 100000 páginas sin agotar`);
57
+ }
58
+ } while (after);
59
+ return out;
60
+ }
61
+ /**
62
+ * Migra las tuplas de relación de `from` a `to` en `partition`. Idempotente
63
+ * (una segunda pasada no escribe nada) y bidireccional (funciona en cualquier
64
+ * sentido). Nunca silenciosa: devuelve `written`/`deleted`/`unchanged`/`extra`.
65
+ */
66
+ export async function reconcileRelations(options) {
67
+ const { from, to, partition, dryRun = false, prune = false, allowMassDelete = false, toConfig } = options;
68
+ const sourceTuples = await enumerateAll(from, partition, 'origen');
69
+ const destTuples = await enumerateAll(to, partition, 'destino');
70
+ const sourceByKey = new Map();
71
+ for (const tuple of sourceTuples)
72
+ sourceByKey.set(tupleKey(tuple), tuple);
73
+ const destKeys = new Set(destTuples.map((tuple) => tupleKey(tuple)));
74
+ const toWrite = [];
75
+ let unchanged = 0;
76
+ for (const [key, tuple] of sourceByKey) {
77
+ if (destKeys.has(key))
78
+ unchanged += 1;
79
+ else
80
+ toWrite.push(tuple);
81
+ }
82
+ const toDelete = destTuples.filter((tuple) => !sourceByKey.has(tupleKey(tuple)));
83
+ // Deriva del modelo fusionado del destino (--dry-run): tipos del origen que
84
+ // el destino no declara. `group` es built-in (el generador lo emite siempre).
85
+ const modelDrift = [];
86
+ if (toConfig) {
87
+ const declared = new Set(['group', ...toConfig.objectTypes.map((t) => t.type)]);
88
+ const seenDrift = new Set();
89
+ for (const tuple of sourceTuples) {
90
+ if (!declared.has(tuple.object.type) && !seenDrift.has(tuple.object.type)) {
91
+ seenDrift.add(tuple.object.type);
92
+ modelDrift.push(tuple.object.type);
93
+ }
94
+ }
95
+ }
96
+ // **El seguro de borrado masivo** (R-17, paridad con `authz:reconcile` de
97
+ // roles / AA2). `prune` + un ORIGEN sin una sola tupla utilizable + un destino
98
+ // con tuplas que borrar es la firma de un `--from` equivocado o de un store
99
+ // vacío: vaciar el destino entero se rechaza con 500 antes de tocar nada,
100
+ // salvo `allowMassDelete`. En `--dry-run` no lanza, lo marca. «Utilizable» =
101
+ // una tupla del origen cuyo tipo cabe en el modelo del destino (los tipos que
102
+ // `modelDrift` recoge no migrarían, así que no cuentan como respaldo).
103
+ const usableSource = toConfig
104
+ ? sourceTuples.filter((tuple) => declaredIn(toConfig, tuple.object.type)).length
105
+ : sourceTuples.length;
106
+ const massDelete = prune && toDelete.length > 0 && usableSource === 0;
107
+ if (massDelete && !dryRun && !allowMassDelete) {
108
+ throw new MassReconcileRefusedError(`authz:relations:reconcile --prune borraría ${toDelete.length} tupla(s) de relación del destino y el ` +
109
+ `ORIGEN no ha aportado NI UNA utilizable (${sourceTuples.length} leídas` +
110
+ `${toConfig ? ', todas de tipos que el destino no declara' : ''}). Eso es la firma de un --from ` +
111
+ `equivocado o de un store vacío, no de un destino que sobra: esta pasada dejaría la partición sin ` +
112
+ `una sola relación. Comprueba el --from y el store; si de verdad quieres vaciarlo, --allow-mass-delete.`);
113
+ }
114
+ if (!dryRun) {
115
+ for (const tuple of toWrite) {
116
+ await to.relate(tuple.subject, tuple.relation, tuple.object, tuple.partition);
117
+ }
118
+ if (prune) {
119
+ for (const tuple of toDelete) {
120
+ await to.unrelate(tuple.subject, tuple.relation, tuple.object, tuple.partition);
121
+ }
122
+ }
123
+ }
124
+ return {
125
+ dryRun,
126
+ written: toWrite.length,
127
+ deleted: prune ? toDelete.length : 0,
128
+ unchanged,
129
+ extra: prune ? 0 : toDelete.length,
130
+ modelDrift,
131
+ massDelete,
132
+ };
133
+ }
134
+ /** ¿El tipo de objeto cabe en el modelo fusionado del destino? (`group` es built-in.) */
135
+ function declaredIn(config, objectType) {
136
+ return objectType === 'group' || config.objectTypes.some((t) => t.type === objectType);
137
+ }
138
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../../src/relations/reconcile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { RelationsConfig, RelationsConfigSpec } from './relations/define_relations_config.js';
2
+ export interface RelationsConfigStoreOptions {
3
+ connection?: string;
4
+ timeoutMs?: number;
5
+ }
6
+ /**
7
+ * **Guarda (persiste) la config de relaciones** bajo el gate de versión: la
8
+ * valida (⚪4/F-05 de config), la serializa a JSON y hace UPSERT de la fila
9
+ * `id=1` dentro de `withAuthzCatalogWrite` —que sube la versión compartida como
10
+ * última sentencia (invariante 14): un sync en otro proceso la ve en la
11
+ * siguiente pregunta—. Devuelve la config congelada.
12
+ */
13
+ export declare function saveRelationsConfig(spec: RelationsConfigSpec, options?: RelationsConfigStoreOptions): Promise<RelationsConfig>;
14
+ /** Lee el spec persistido (o `null` si no hay relaciones declaradas — opt-in). */
15
+ export declare function readRelationsConfigSpec(options?: RelationsConfigStoreOptions): Promise<RelationsConfigSpec | null>;
16
+ /**
17
+ * Lee la config persistida ya VALIDADA (o `null`). Es lo que hace 🟡3: un
18
+ * proceso SIN `defineRelationsConfig` en memoria recupera aquí los tipos de
19
+ * relación de la base, así que el modelo que republique los lleva.
20
+ */
21
+ export declare function readRelationsConfig(options?: RelationsConfigStoreOptions): Promise<RelationsConfig | null>;
22
+ //# sourceMappingURL=relations_config_store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations_config_store.d.ts","sourceRoot":"","sources":["../../src/relations_config_store.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAMlG,MAAM,WAAW,2BAA2B;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAMD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,mBAAmB,EACzB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,kFAAkF;AAClF,wBAAsB,uBAAuB,CAC3C,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAMrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAGjC"}