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

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 (214) hide show
  1. package/README.md +693 -36
  2. package/build/commands/authz_catalog_diff.js +1 -1
  3. package/build/commands/authz_catalog_diff.js.map +1 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  5. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  6. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  7. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  8. package/build/commands/authz_catalog_sync.d.ts +17 -0
  9. package/build/commands/authz_catalog_sync.d.ts.map +1 -1
  10. package/build/commands/authz_catalog_sync.js +27 -4
  11. package/build/commands/authz_catalog_sync.js.map +1 -1
  12. package/build/commands/authz_freeze.d.ts +44 -0
  13. package/build/commands/authz_freeze.d.ts.map +1 -0
  14. package/build/commands/authz_freeze.js +95 -0
  15. package/build/commands/authz_freeze.js.map +1 -0
  16. package/build/commands/authz_reconcile.d.ts +102 -0
  17. package/build/commands/authz_reconcile.d.ts.map +1 -0
  18. package/build/commands/authz_reconcile.js +294 -0
  19. package/build/commands/authz_reconcile.js.map +1 -0
  20. package/build/commands/authz_relations_reconcile.d.ts +97 -0
  21. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  22. package/build/commands/authz_relations_reconcile.js +313 -0
  23. package/build/commands/authz_relations_reconcile.js.map +1 -0
  24. package/build/commands/authz_scopes_relay.d.ts +47 -0
  25. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  26. package/build/commands/authz_scopes_relay.js +141 -0
  27. package/build/commands/authz_scopes_relay.js.map +1 -0
  28. package/build/commands/authz_unfreeze.d.ts +37 -0
  29. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  30. package/build/commands/authz_unfreeze.js +92 -0
  31. package/build/commands/authz_unfreeze.js.map +1 -0
  32. package/build/commands/main.d.ts +6 -1
  33. package/build/commands/main.d.ts.map +1 -1
  34. package/build/commands/main.js +6 -1
  35. package/build/commands/main.js.map +1 -1
  36. package/build/commands/openfga_provision.d.ts +46 -4
  37. package/build/commands/openfga_provision.d.ts.map +1 -1
  38. package/build/commands/openfga_provision.js +90 -7
  39. package/build/commands/openfga_provision.js.map +1 -1
  40. package/build/configure.d.ts +11 -0
  41. package/build/configure.d.ts.map +1 -1
  42. package/build/configure.js +37 -1
  43. package/build/configure.js.map +1 -1
  44. package/build/index.d.ts +50 -10
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +45 -6
  47. package/build/index.js.map +1 -1
  48. package/build/providers/authz_provider.d.ts +26 -2
  49. package/build/providers/authz_provider.d.ts.map +1 -1
  50. package/build/providers/authz_provider.js +55 -2
  51. package/build/providers/authz_provider.js.map +1 -1
  52. package/build/services/relations.d.ts +14 -0
  53. package/build/services/relations.d.ts.map +1 -0
  54. package/build/services/relations.js +17 -0
  55. package/build/services/relations.js.map +1 -0
  56. package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
  57. package/build/src/catalog/catalog.d.ts.map +1 -0
  58. package/build/src/{catalog.js → catalog/catalog.js} +120 -14
  59. package/build/src/catalog/catalog.js.map +1 -0
  60. package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
  61. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  62. package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
  63. package/build/src/catalog/catalog_cache.js.map +1 -0
  64. package/build/src/define_config.d.ts +137 -3
  65. package/build/src/define_config.d.ts.map +1 -1
  66. package/build/src/define_config.js.map +1 -1
  67. package/build/src/drivers/database_driver.d.ts +128 -7
  68. package/build/src/drivers/database_driver.d.ts.map +1 -1
  69. package/build/src/drivers/database_driver.js +510 -24
  70. package/build/src/drivers/database_driver.js.map +1 -1
  71. package/build/src/drivers/database_relations_driver.d.ts +113 -0
  72. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  73. package/build/src/drivers/database_relations_driver.js +679 -0
  74. package/build/src/drivers/database_relations_driver.js.map +1 -0
  75. package/build/src/drivers/openfga_driver.d.ts +729 -122
  76. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  77. package/build/src/drivers/openfga_driver.js +2083 -476
  78. package/build/src/drivers/openfga_driver.js.map +1 -1
  79. package/build/src/drivers/openfga_facts.d.ts +384 -0
  80. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  81. package/build/src/drivers/openfga_facts.js +836 -0
  82. package/build/src/drivers/openfga_facts.js.map +1 -0
  83. package/build/src/drivers/openfga_relations_driver.d.ts +141 -0
  84. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  85. package/build/src/drivers/openfga_relations_driver.js +590 -0
  86. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  87. package/build/src/errors.d.ts +290 -5
  88. package/build/src/errors.d.ts.map +1 -1
  89. package/build/src/errors.js +296 -7
  90. package/build/src/errors.js.map +1 -1
  91. package/build/src/freeze.d.ts +141 -0
  92. package/build/src/freeze.d.ts.map +1 -0
  93. package/build/src/freeze.js +217 -0
  94. package/build/src/freeze.js.map +1 -0
  95. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  96. package/build/src/http/app_access_middleware.js.map +1 -0
  97. package/build/src/http/resource_access_middleware.d.ts +105 -0
  98. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  99. package/build/src/http/resource_access_middleware.js +81 -0
  100. package/build/src/http/resource_access_middleware.js.map +1 -0
  101. package/build/src/identity.d.ts +74 -1
  102. package/build/src/identity.d.ts.map +1 -1
  103. package/build/src/identity.js +100 -2
  104. package/build/src/identity.js.map +1 -1
  105. package/build/src/manager.d.ts +330 -4
  106. package/build/src/manager.d.ts.map +1 -1
  107. package/build/src/manager.js +1285 -188
  108. package/build/src/manager.js.map +1 -1
  109. package/build/src/models/authz_assignment.d.ts +7 -7
  110. package/build/src/models/authz_assignment.d.ts.map +1 -1
  111. package/build/src/models/authz_deny.d.ts +7 -7
  112. package/build/src/models/authz_deny.d.ts.map +1 -1
  113. package/build/src/models/authz_permission.d.ts +7 -7
  114. package/build/src/models/authz_permission.d.ts.map +1 -1
  115. package/build/src/models/authz_role.d.ts +7 -7
  116. package/build/src/models/authz_role.d.ts.map +1 -1
  117. package/build/src/models/authz_role_permission.d.ts +7 -7
  118. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  119. package/build/src/openfga.d.ts +18 -2
  120. package/build/src/openfga.d.ts.map +1 -1
  121. package/build/src/openfga.js +15 -1
  122. package/build/src/openfga.js.map +1 -1
  123. package/build/src/reconcile.d.ts +37 -0
  124. package/build/src/reconcile.d.ts.map +1 -0
  125. package/build/src/reconcile.js +69 -0
  126. package/build/src/reconcile.js.map +1 -0
  127. package/build/src/relation_partition_trigger.d.ts +8 -0
  128. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  129. package/build/src/relation_partition_trigger.js +93 -0
  130. package/build/src/relation_partition_trigger.js.map +1 -0
  131. package/build/src/relations/define_relations_config.d.ts +75 -0
  132. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  133. package/build/src/relations/define_relations_config.js +173 -0
  134. package/build/src/relations/define_relations_config.js.map +1 -0
  135. package/build/src/relations/manager.d.ts +60 -0
  136. package/build/src/relations/manager.d.ts.map +1 -0
  137. package/build/src/relations/manager.js +213 -0
  138. package/build/src/relations/manager.js.map +1 -0
  139. package/build/src/relations/reconcile.d.ts +111 -0
  140. package/build/src/relations/reconcile.d.ts.map +1 -0
  141. package/build/src/relations/reconcile.js +200 -0
  142. package/build/src/relations/reconcile.js.map +1 -0
  143. package/build/src/relations_config_store.d.ts +22 -0
  144. package/build/src/relations_config_store.d.ts.map +1 -0
  145. package/build/src/relations_config_store.js +74 -0
  146. package/build/src/relations_config_store.js.map +1 -0
  147. package/build/src/scope_outbox.d.ts +69 -0
  148. package/build/src/scope_outbox.d.ts.map +1 -0
  149. package/build/src/scope_outbox.js +298 -0
  150. package/build/src/scope_outbox.js.map +1 -0
  151. package/build/src/{drivers → shared}/backend_guard.d.ts +42 -0
  152. package/build/src/shared/backend_guard.d.ts.map +1 -0
  153. package/build/src/{drivers → shared}/backend_guard.js +78 -1
  154. package/build/src/shared/backend_guard.js.map +1 -0
  155. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  156. package/build/src/shared/sql_expiry.js.map +1 -0
  157. package/build/src/shared/transaction_guard.d.ts +50 -0
  158. package/build/src/shared/transaction_guard.d.ts.map +1 -0
  159. package/build/src/shared/transaction_guard.js +60 -0
  160. package/build/src/shared/transaction_guard.js.map +1 -0
  161. package/build/src/sql_descendants.d.ts +47 -1
  162. package/build/src/sql_descendants.d.ts.map +1 -1
  163. package/build/src/sql_descendants.js +75 -1
  164. package/build/src/sql_descendants.js.map +1 -1
  165. package/build/src/testing/contract.d.ts +129 -4
  166. package/build/src/testing/contract.d.ts.map +1 -1
  167. package/build/src/testing/contract.js +912 -177
  168. package/build/src/testing/contract.js.map +1 -1
  169. package/build/src/testing/main.d.ts +8 -2
  170. package/build/src/testing/main.d.ts.map +1 -1
  171. package/build/src/testing/main.js +4 -1
  172. package/build/src/testing/main.js.map +1 -1
  173. package/build/src/testing/migration_contract.d.ts +284 -0
  174. package/build/src/testing/migration_contract.d.ts.map +1 -0
  175. package/build/src/testing/migration_contract.js +586 -0
  176. package/build/src/testing/migration_contract.js.map +1 -0
  177. package/build/src/testing/relations_contract.d.ts +73 -0
  178. package/build/src/testing/relations_contract.d.ts.map +1 -0
  179. package/build/src/testing/relations_contract.js +1069 -0
  180. package/build/src/testing/relations_contract.js.map +1 -0
  181. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  182. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  183. package/build/src/testing/relations_reconcile_contract.js +220 -0
  184. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  185. package/build/src/traits/authz_scopes.js +1 -1
  186. package/build/src/traits/authz_scopes.js.map +1 -1
  187. package/build/src/traits/has_uuid.d.ts +8 -8
  188. package/build/src/traits/has_uuid.d.ts.map +1 -1
  189. package/build/src/types.d.ts +1053 -83
  190. package/build/src/types.d.ts.map +1 -1
  191. package/build/src/types.js +10 -0
  192. package/build/src/types.js.map +1 -1
  193. package/build/stubs/config/authorization.stub +104 -4
  194. package/build/stubs/migration.stub +136 -0
  195. package/build/stubs/scopes_outbox_migration.stub +57 -0
  196. package/package.json +4 -2
  197. package/build/commands/openfga_import.d.ts +0 -34
  198. package/build/commands/openfga_import.d.ts.map +0 -1
  199. package/build/commands/openfga_import.js +0 -97
  200. package/build/commands/openfga_import.js.map +0 -1
  201. package/build/src/catalog.d.ts.map +0 -1
  202. package/build/src/catalog.js.map +0 -1
  203. package/build/src/catalog_cache.d.ts.map +0 -1
  204. package/build/src/catalog_cache.js.map +0 -1
  205. package/build/src/drivers/backend_guard.d.ts.map +0 -1
  206. package/build/src/drivers/backend_guard.js.map +0 -1
  207. package/build/src/drivers/sql_expiry.d.ts.map +0 -1
  208. package/build/src/drivers/sql_expiry.js.map +0 -1
  209. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  210. package/build/src/middleware/app_access_middleware.js.map +0 -1
  211. /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
  212. /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
  213. /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
  214. /package/build/src/{drivers → shared}/sql_expiry.js +0 -0
package/build/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export { AuthorizationManager, DEFAULT_MAX_SCOPES, DEFAULT_MAX_DESCENDANTS, MAX_SCOPE_BOUND, DEFAULT_VIEW_MAX_AGE_MS, expandExcludedSubtrees } from './src/manager.js';
6
6
  export { defineConfig } from './src/define_config.js';
7
- export { syncAuthzCatalog, diffAuthzCatalog, catalogInSync, formatCatalogDiff, formatScopedRoles, formatShadowedRoles, runCatalogDiff, syncCatalogs, } from './src/catalog.js';
7
+ export { syncAuthzCatalog, diffAuthzCatalog, catalogInSync, formatCatalogDiff, formatScopedRoles, formatShadowedRoles, runCatalogDiff, syncCatalogs, } from './src/catalog/catalog.js';
8
8
  /**
9
9
  * Memo del catálogo (2.1): se revalida contra la versión compartida
10
10
  * `authz_catalog_version`, que `syncAuthzCatalog` sube como última sentencia
@@ -13,7 +13,7 @@ export { syncAuthzCatalog, diffAuthzCatalog, catalogInSync, formatCatalogDiff, f
13
13
  * final: todos los procesos lo ven) — `bumpAuthzCatalogVersion(trx)` exige
14
14
  * ese trx; `invalidateAuthzCatalog()` solo alcanza a este proceso.
15
15
  */
16
- export { CatalogCache, invalidateAuthzCatalog, bumpAuthzCatalogVersion, withAuthzCatalogWrite, readAuthzCatalogVersion, CATALOG_VERSION_TABLE, GLOBAL_OWNER_KEY, } from './src/catalog_cache.js';
16
+ export { CatalogCache, invalidateAuthzCatalog, bumpAuthzCatalogVersion, withAuthzCatalogWrite, readAuthzCatalogVersion, CATALOG_VERSION_TABLE, GLOBAL_OWNER_KEY, } from './src/catalog/catalog_cache.js';
17
17
  /**
18
18
  * Memo de ancestros de una instancia (2.1), solo para el camino de lectura.
19
19
  * `authorization.forRequest()` ya lo aplica por ti.
@@ -25,21 +25,49 @@ export { memoizeAncestors } from './src/memoize_ancestors.js';
25
25
  * `descendantsOf` opt-in (CTE recursiva, PG y SQLite) para `authorizedScopes`.
26
26
  */
27
27
  export { hierarchicalScopeResolver } from './src/hierarchical_resolver.js';
28
- export { sqlDescendantsOf } from './src/sql_descendants.js';
28
+ export { sqlDescendantsOf, sqlScopeEdges } from './src/sql_descendants.js';
29
+ /**
30
+ * **La outbox del árbol** (3b-2d): `sqlScopeOutbox` implementa el puerto
31
+ * `ScopeOutbox` sobre Lucid, con la tabla de
32
+ * `stubs/scopes_outbox_migration.stub` (cópiala a tus migraciones:
33
+ * `configure` no la publica, porque la outbox es opt-in). Con
34
+ * `scopes.outbox` declarada, `authorization.scopes.attached/moved/detached`
35
+ * ENCOLAN el cambio en tu transacción en vez de escribir en el backend, y lo
36
+ * aplica `node ace authz:scopes:relay`. Cualquier implementación del puerto
37
+ * vale: el paquete no impone tabla.
38
+ */
39
+ export { sqlScopeOutbox } from './src/scope_outbox.js';
29
40
  /**
30
41
  * El backend de autorización no respondió (503). Tipo PROPIO a propósito: sin
31
42
  * él escaparía el error del SDK del driver, y distinguir "backend caído"
32
43
  * obligaría a importar ese SDK — acoplando el call-site al backend que este
33
44
  * paquete existe para abstraer.
34
45
  */
35
- export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, StoreNotEmptyError, ActorRequiredError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, UnsupportedDialectError, ScopeTooDeepError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, } from './src/errors.js';
46
+ export { AuthorizationBackendError, AuthorizationBackendTimeoutError, ScopeResolverError, InvalidIdentityError, InvalidSlugError, AuthorizationConfigError, AuthorizationInternalError, UnknownScopeError, NoScopeResolverError, UnknownRoleError, UnknownPermissionError, CatalogConflictError, RoleIsNotAccessError, ScopeCycleError, PurgeIncompleteError, WriteConflictError, ActorRequiredError, NotWithinError, WithinRequiredError, WithinRootForbiddenError, TooManyScopesError, TooManyLocalRolesError, MassPurgeRefusedError, PruneInterruptedError, UnsupportedDialectError, ScopeTooDeepError, UnsupportedOperationError, NoDescendantsResolverError, ViewExpiredError, RoleNotVisibleError, AmbiguousRoleError, RoleImmutableError, RoleLevelAboveOwnerError, RoleNotAssignableAtError, PermissionNotDelegableError, RankExceededError, ModelTooLargeError, RelationConfigError, RelationTypeUnknownError, RelationUnknownError, ScopeTreeDriftError, ScopeDriftUnguardedError, AuthorizationFrozenError, FreezeHeldError, MassReconcileRefusedError, ReconcileTooLargeError, } from './src/errors.js';
36
47
  /**
37
48
  * Validación de identidad y de slugs (422). La aplica el manager en cada
38
49
  * llamada y los drivers por defensa en profundidad; se exporta para que un
39
50
  * consumidor valide en su borde (un formulario, un importador) con la MISMA
40
51
  * regla y no con una copia.
41
52
  */
42
- export { assertIdentity, assertSubject, assertScope, assertValidSlug, assertExpiresAt, assertNoSlugCollisions, normalizeRoleQuery, scopeKey, scopeFromKey, IDENTITY_LIMITS, MAX_SLUG_LENGTH, RESERVED_SLUGS, RESERVED_SLUG_PREFIXES, } from './src/identity.js';
53
+ export { assertIdentity, assertSubject, assertScope, assertValidSlug, assertExpiresAt, assertNoSlugCollisions, normalizeRoleQuery, scopeKey, scopeFromKey, IDENTITY_LIMITS, MAX_SLUG_LENGTH, RESERVED_SLUGS, RESERVED_SLUG_PREFIXES, RESERVED_FACTS_TYPES, } from './src/identity.js';
54
+ /**
55
+ * ReBAC genérico (Fase 4, `relations/`). El puerto `RelationsDriver` vive en
56
+ * `./src/types.js` (el contrato); aquí salen la API declarativa
57
+ * `defineRelationsConfig`, la fachada `RelationsManager` (F-05 + `assertWrite`
58
+ * + `actor`) y los tipos de la config.
59
+ */
60
+ export { defineRelationsConfig } from './src/relations/define_relations_config.js';
61
+ export { RelationsManager } from './src/relations/manager.js';
62
+ export { reconcileRelations } from './src/relations/reconcile.js';
63
+ // La config de relaciones PERSISTIDA (🟡3): API de PLATAFORMA (bajo el gate de
64
+ // versión, invariante 14). La CONSUME `authz:relations:reconcile` (config del
65
+ // destino, para la deriva del modelo fusionado en --dry-run).
66
+ export { saveRelationsConfig, readRelationsConfig, readRelationsConfigSpec, } from './src/relations_config_store.js';
67
+ // El driver `database` de relaciones (el consumidor lo cablea en
68
+ // `relations.drivers` de config/authorization.ts). El driver `openfga` de
69
+ // relaciones NO sale de aquí: vive en el subpath `/openfga` (peer opcional).
70
+ export { DatabaseRelationsDriver } from './src/drivers/database_relations_driver.js';
43
71
  export { resolveGrantExpiry, isActiveExpiry, sameInstant } from './src/expiry.js';
44
72
  export { DatabaseAuthorizationDriver, APP_SCOPE_DB_UUID } from './src/drivers/database_driver.js';
45
73
  /**
@@ -47,7 +75,18 @@ export { DatabaseAuthorizationDriver, APP_SCOPE_DB_UUID } from './src/drivers/da
47
75
  * `@jantstack/adonis-authz/openfga`, que es lo único que importa el peer
48
76
  * opcional `@openfga/sdk`. Un consumidor solo-database arranca sin él (D9).
49
77
  */
50
- export { APP_SCOPE, APP_SCOPE_TYPE } from './src/types.js';
78
+ export { APP_SCOPE, APP_SCOPE_TYPE, DEFAULT_RECONCILE_MAX_TUPLES } from './src/types.js';
79
+ export { isRelUserset } from './src/types.js';
80
+ /**
81
+ * **¿De quién es esta transacción?** (L-1 · 🟠 9, regla del puerto `{trx}`):
82
+ * la ÚNICA comprobación con la que una escritura del paquete —la outbox de
83
+ * `scopes.*` hoy; `grant`/`relate` de un driver con `transactionalWrites:
84
+ * true`— se inscribe en `{ transaction }`: tiene que ser una transacción
85
+ * ABIERTA de Lucid de la conexión de quien escribe (otra conexión, un
86
+ * `QueryClient` o el `db` entero son 500 `E_AUTHZ_CONFIG`). Un driver de
87
+ * terceros que declare `transactionalWrites: true` la llama con SU conexión.
88
+ */
89
+ export { assertCallerTransaction } from './src/shared/transaction_guard.js';
51
90
  export { default as AuthzRole } from './src/models/authz_role.js';
52
91
  export { default as AuthzPermission } from './src/models/authz_permission.js';
53
92
  export { default as AuthzRolePermission } from './src/models/authz_role_permission.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,eAAe,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAEtK,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAErD,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,YAAY,GACb,MAAM,kBAAkB,CAAA;AAGzB;;;;;;;GAOG;AACH,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,wBAAwB,CAAA;AAY/B;;;GAGG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE7D;;;;GAIG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAA;AAE1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAG3D;;;;;GAKG;AACH,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,wBAAwB,EACxB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,yBAAyB,EACzB,0BAA0B,EAC1B,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,iBAAiB,GAClB,MAAM,iBAAiB,CAAA;AAExB;;;;;GAKG;AACH,OAAO,EACL,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,GACvB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAEjF,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAGjG;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AA8B1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,uCAAuC,CAAA;AACtF,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,37 @@
1
1
  import type { ApplicationService } from '@adonisjs/core/types';
2
2
  import { AuthorizationManager } from '../src/manager.js';
3
+ import { RelationsManager } from '../src/relations/manager.js';
4
+ import type { AuthorizationConfig } from '../src/define_config.js';
3
5
  declare module '@adonisjs/core/types' {
4
6
  interface ContainerBindings {
5
7
  'authz.manager': AuthorizationManager;
8
+ 'authz.relations': RelationsManager;
6
9
  }
7
10
  }
8
11
  /**
9
- * Registra el manager de autorización como singleton del contenedor,
10
- * construido desde `config/authorization.ts` de la aplicación.
12
+ * **Construye el `RelationsManager` de servicio desde el config** (Fase 4-8) —
13
+ * el análogo de resolver el `AuthorizationManager` del provider, pero para el
14
+ * puerto de relaciones. Función PURA (no toca el contenedor) para tener su caso
15
+ * sin montar la app: recibe el config y devuelve el manager cableado.
16
+ *
17
+ * El driver ACTIVO sale de `relations.drivers[relations.default ?? default]`
18
+ * (los drivers de relaciones se nombran igual que los de roles); la config, de
19
+ * `relations.config` (la MISMA que capturan las factories de `drivers`, así que
20
+ * la frontera F-05 del manager y la del driver son la misma). `requireActor` se
21
+ * hereda del config de roles: la política de auditoría es una sola.
22
+ *
23
+ * Sin `relations.config` o sin el driver activo lanza con la receta: las
24
+ * relaciones son OPT-IN y este servicio solo existe si el consumidor las
25
+ * declaró.
26
+ */
27
+ export declare function buildRelationsManager(config: AuthorizationConfig): Promise<RelationsManager>;
28
+ /**
29
+ * Registra los managers de autorización como singletons del contenedor,
30
+ * construidos desde `config/authorization.ts` de la aplicación:
31
+ *
32
+ * - `authz.manager`: el motor de roles (siempre).
33
+ * - `authz.relations`: el motor de ReBAC (solo si el consumidor declaró
34
+ * `relations.config`; si no, resolverlo lanza con la receta).
11
35
  */
12
36
  export default class AuthzProvider {
13
37
  protected app: ApplicationService;
@@ -1 +1 @@
1
- {"version":3,"file":"authz_provider.d.ts","sourceRoot":"","sources":["../../providers/authz_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAiB,iBAAiB;QAChC,eAAe,EAAE,oBAAoB,CAAA;KACtC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IACpB,SAAS,CAAC,GAAG,EAAE,kBAAkB;gBAAvB,GAAG,EAAE,kBAAkB;IAE7C,QAAQ;CAWT"}
1
+ {"version":3,"file":"authz_provider.d.ts","sourceRoot":"","sources":["../../providers/authz_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAElE,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAiB,iBAAiB;QAChC,eAAe,EAAE,oBAAoB,CAAA;QACrC,iBAAiB,EAAE,gBAAgB,CAAA;KACpC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4BlG;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IACpB,SAAS,CAAC,GAAG,EAAE,kBAAkB;gBAAvB,GAAG,EAAE,kBAAkB;IAE7C,QAAQ;CAqBT"}
@@ -1,7 +1,53 @@
1
1
  import { AuthorizationManager } from '../src/manager.js';
2
+ import { RelationsManager } from '../src/relations/manager.js';
2
3
  /**
3
- * Registra el manager de autorización como singleton del contenedor,
4
- * construido desde `config/authorization.ts` de la aplicación.
4
+ * **Construye el `RelationsManager` de servicio desde el config** (Fase 4-8) —
5
+ * el análogo de resolver el `AuthorizationManager` del provider, pero para el
6
+ * puerto de relaciones. Función PURA (no toca el contenedor) para tener su caso
7
+ * sin montar la app: recibe el config y devuelve el manager cableado.
8
+ *
9
+ * El driver ACTIVO sale de `relations.drivers[relations.default ?? default]`
10
+ * (los drivers de relaciones se nombran igual que los de roles); la config, de
11
+ * `relations.config` (la MISMA que capturan las factories de `drivers`, así que
12
+ * la frontera F-05 del manager y la del driver son la misma). `requireActor` se
13
+ * hereda del config de roles: la política de auditoría es una sola.
14
+ *
15
+ * Sin `relations.config` o sin el driver activo lanza con la receta: las
16
+ * relaciones son OPT-IN y este servicio solo existe si el consumidor las
17
+ * declaró.
18
+ */
19
+ export async function buildRelationsManager(config) {
20
+ const relations = config.relations;
21
+ if (!relations?.config) {
22
+ throw new Error('No hay `relations.config` en config/authorization.ts: declara `relations: { config: ' +
23
+ 'defineRelationsConfig({...}), drivers: {...} }` para usar el servicio de relaciones ' +
24
+ '(@jantstack/adonis-authz/services/relations).');
25
+ }
26
+ const driverKey = relations.default ?? config.default;
27
+ const factory = relations.drivers?.[driverKey];
28
+ if (!factory) {
29
+ throw new Error(`No hay driver de relaciones '${driverKey}' en \`relations.drivers\` de config/authorization.ts ` +
30
+ `(declara la factory bajo esa clave, o fija \`relations.default\`).`);
31
+ }
32
+ const driver = await factory();
33
+ return new RelationsManager(driver, relations.config, {
34
+ requireActor: config.requireActor,
35
+ // R-15: el MISMO reloj que el motor de roles (`config.clock`, 2.5 · J1).
36
+ clock: config.clock,
37
+ // L-1 · J1: la MISMA barrera del freeze (y su deadline) que el motor de roles.
38
+ freezeTimeoutMs: config.freezeTimeoutMs,
39
+ // L-2 · puerta 2: el flag de relaciones manda; si no está, el del raíz.
40
+ requireTransactionalWrites: relations.requireTransactionalWrites ?? config.requireTransactionalWrites,
41
+ driverName: driverKey,
42
+ });
43
+ }
44
+ /**
45
+ * Registra los managers de autorización como singletons del contenedor,
46
+ * construidos desde `config/authorization.ts` de la aplicación:
47
+ *
48
+ * - `authz.manager`: el motor de roles (siempre).
49
+ * - `authz.relations`: el motor de ReBAC (solo si el consumidor declaró
50
+ * `relations.config`; si no, resolverlo lanza con la receta).
5
51
  */
6
52
  export default class AuthzProvider {
7
53
  app;
@@ -16,6 +62,13 @@ export default class AuthzProvider {
16
62
  }
17
63
  return new AuthorizationManager(config);
18
64
  });
65
+ this.app.container.singleton('authz.relations', async () => {
66
+ const config = this.app.config.get('authorization');
67
+ if (!config) {
68
+ throw new Error('Falta config/authorization.ts — ejecuta: node ace configure @jantstack/adonis-authz');
69
+ }
70
+ return buildRelationsManager(config);
71
+ });
19
72
  }
20
73
  }
21
74
  //# sourceMappingURL=authz_provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"authz_provider.js","sourceRoot":"","sources":["../../providers/authz_provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAQxD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IACV;IAAtB,YAAsB,GAAuB;QAAvB,QAAG,GAAH,GAAG,CAAoB;IAAG,CAAC;IAEjD,QAAQ;QACN,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,eAAe,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAQ,CAAA;YAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,qFAAqF,CACtF,CAAA;YACH,CAAC;YACD,OAAO,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAAA;QACzC,CAAC,CAAC,CAAA;IACJ,CAAC;CACF"}
1
+ {"version":3,"file":"authz_provider.js","sourceRoot":"","sources":["../../providers/authz_provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAU9D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,MAA2B;IACrE,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;IAClC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,sFAAsF;YACpF,sFAAsF;YACtF,+CAA+C,CAClD,CAAA;IACH,CAAC;IACD,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAA;IACrD,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAA;IAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,gCAAgC,SAAS,wDAAwD;YAC/F,oEAAoE,CACvE,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;IAC9B,OAAO,IAAI,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;QACpD,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,yEAAyE;QACzE,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,+EAA+E;QAC/E,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,wEAAwE;QACxE,0BAA0B,EAAE,SAAS,CAAC,0BAA0B,IAAI,MAAM,CAAC,0BAA0B;QACrG,UAAU,EAAE,SAAS;KACtB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IACV;IAAtB,YAAsB,GAAuB;QAAvB,QAAG,GAAH,GAAG,CAAoB;IAAG,CAAC;IAEjD,QAAQ;QACN,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,eAAe,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAQ,CAAA;YAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,qFAAqF,CACtF,CAAA;YACH,CAAC;YACD,OAAO,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAAA;QACzC,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAQ,CAAA;YAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,qFAAqF,CACtF,CAAA;YACH,CAAC;YACD,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;IACJ,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import type { RelationsManager } from '../src/relations/manager.js';
2
+ /**
3
+ * Singleton del motor de relaciones (ReBAC), resuelto del contenedor cuando la
4
+ * app ha booteado — el análogo de `services/main` para el puerto
5
+ * `RelationsDriver`. Solo existe si el consumidor declaró `relations.config` en
6
+ * `config/authorization.ts`; si no, resolverlo lanza con la receta.
7
+ *
8
+ * import relations from '@jantstack/adonis-authz/services/relations'
9
+ * await relations.relate(user, 'viewer', { type: 'document', id }, APP_SCOPE)
10
+ * await relations.check(user, 'viewer', { type: 'document', id }, APP_SCOPE)
11
+ */
12
+ declare let relations: RelationsManager;
13
+ export { relations as default };
14
+ //# sourceMappingURL=relations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../../services/relations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAEnE;;;;;;;;;GASG;AACH,QAAA,IAAI,SAAS,EAAE,gBAAgB,CAAA;AAM/B,OAAO,EAAE,SAAS,IAAI,OAAO,EAAE,CAAA"}
@@ -0,0 +1,17 @@
1
+ import app from '@adonisjs/core/services/app';
2
+ /**
3
+ * Singleton del motor de relaciones (ReBAC), resuelto del contenedor cuando la
4
+ * app ha booteado — el análogo de `services/main` para el puerto
5
+ * `RelationsDriver`. Solo existe si el consumidor declaró `relations.config` en
6
+ * `config/authorization.ts`; si no, resolverlo lanza con la receta.
7
+ *
8
+ * import relations from '@jantstack/adonis-authz/services/relations'
9
+ * await relations.relate(user, 'viewer', { type: 'document', id }, APP_SCOPE)
10
+ * await relations.check(user, 'viewer', { type: 'document', id }, APP_SCOPE)
11
+ */
12
+ let relations;
13
+ await app.booted(async () => {
14
+ relations = await app.container.make('authz.relations');
15
+ });
16
+ export { relations as default };
17
+ //# sourceMappingURL=relations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.js","sourceRoot":"","sources":["../../services/relations.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,6BAA6B,CAAA;AAG7C;;;;;;;;;GASG;AACH,IAAI,SAA2B,CAAA;AAE/B,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;IAC1B,SAAS,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;AACzD,CAAC,CAAC,CAAA;AAEF,OAAO,EAAE,SAAS,IAAI,OAAO,EAAE,CAAA"}
@@ -1,4 +1,4 @@
1
- import type { CatalogRoleRef, CatalogSpec, ScopeChainResolver, ScopeType } from './types.js';
1
+ import type { CatalogProjection, CatalogProjectionReport, CatalogProjectionSnapshot, CatalogRoleRef, CatalogSpec, ScopeChainResolver, ScopeType } from '../types.js';
2
2
  /**
3
3
  * `assignableAt` de un permiso tal como se GUARDA (`authz_permissions.assignable_at`):
4
4
  * JSON ordenado y sin duplicados, o `null` = cualquier nivel. Ordenado para
@@ -60,6 +60,12 @@ export interface CatalogSyncReport {
60
60
  role: CatalogRoleRef;
61
61
  permission: string;
62
62
  }>;
63
+ /**
64
+ * Lo que movió la proyección del driver, si se pasó una (3b-2a · A5).
65
+ * Ausente sin `projection`. Nunca un booleano: una proyección que no dice
66
+ * cuánto escribió y cuánto borró no se vigila.
67
+ */
68
+ projection?: CatalogProjectionReport;
63
69
  }
64
70
  export interface SyncCatalogOptions {
65
71
  /**
@@ -76,6 +82,22 @@ export interface SyncCatalogOptions {
76
82
  * cliente SQL: el sync corre en el arranque de un despliegue (D15).
77
83
  */
78
84
  timeoutMs?: number;
85
+ /**
86
+ * Proyección DERIVADA del catálogo en el backend de un driver (3b-2a · A5;
87
+ * regla del catálogo reescrita, panel 2 cruce 7). Se INYECTA en vez de
88
+ * importarse: este módulo es la ruta de un consumidor solo-database y no
89
+ * puede tirar del SDK de OpenFGA (regla 3 de `check_purity.mjs`).
90
+ *
91
+ * Con ella, el sync (a) comprueba ANTES de escribir que el catálogo que va
92
+ * a quedar es publicable en ese backend —cotas de nombre y techo del
93
+ * modelo, A3/A4—, y (b) rehace la proyección con el catálogo ya
94
+ * CONFIRMADO. En ese orden: la fuente de verdad son las tablas `authz_*`,
95
+ * y una proyección que se adelantara a la transacción concedería lo que la
96
+ * base no respalda. Si la proyección falla, el catálogo ya está escrito y
97
+ * lo que queda es deriva reconstruible (`authz:reconcile`), no una
98
+ * decisión inventada.
99
+ */
100
+ projection?: CatalogProjection;
79
101
  }
80
102
  /**
81
103
  * Sincroniza un catálogo (roles + permisos + vínculos) a las tablas `authz_*`.
@@ -97,6 +119,23 @@ export interface SyncCatalogOptions {
97
119
  * driver externo (openfga) materializa aparte solo los hechos.
98
120
  */
99
121
  export declare function syncAuthzCatalog(catalog: CatalogSpec, options?: SyncCatalogOptions): Promise<CatalogSyncReport>;
122
+ /**
123
+ * **La foto del catálogo que se PROYECTA** (3b-2a · A5), leída de `authz_*`.
124
+ *
125
+ * Es el catálogo ENTERO, no el spec que se está sincronizando: la proyección
126
+ * es un espejo y lo que sobra en el backend solo se sabe comparando contra
127
+ * todo. `syncAuthzCatalog` la lee DENTRO de su transacción (coherente con lo
128
+ * que acaba de escribir) y `authz:reconcile` fuera, contra la base: las dos
129
+ * tienen que construir exactamente la misma foto, o reconcile "arreglaría" en
130
+ * cada pasada lo que el sync acaba de dejar bien. Por eso está aquí y no
131
+ * duplicada en el driver.
132
+ *
133
+ * `source` es cualquier cosa con `from(tabla)` (el `db` de Lucid o una
134
+ * transacción) y `sql` envuelve cada consulta con el deadline de quien llama.
135
+ */
136
+ export declare function readCatalogProjectionSnapshot(source: {
137
+ from(table: string): any;
138
+ }, sql?: <T>(operation: string, fn: () => Promise<T>) => Promise<T>): Promise<CatalogProjectionSnapshot>;
100
139
  export interface CatalogLinkRef {
101
140
  role: string;
102
141
  scopeType: ScopeType;
@@ -223,7 +262,7 @@ export declare function formatScopedRoles(diff: CatalogDiff): string[];
223
262
  * mientras duren, ese slug es 422 `E_AUTHZ_AMBIGUOUS_ROLE` en la cadena de
224
263
  * su owner y hay que operar por `{ uuid }` (o purgar uno de los dos).
225
264
  */
226
- export declare function formatShadowedRoles(diff: CatalogDiff): string[];
265
+ export declare function formatShadowedRoles(diff: Pick<CatalogDiff, 'shadowedByGlobal' | 'shadowedByAncestor'>): string[];
227
266
  /** Fábricas de catálogo tal como se declaran en `config.catalogs`. */
228
267
  export type CatalogSource = () => Promise<CatalogSpec> | CatalogSpec;
229
268
  /**
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../src/catalog/catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,SAAS,EACV,MAAM,aAAa,CAAA;AAOpB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAO5E;AAED,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,eAAmB,CAAA;AAkBrD,sIAAsI;AACtI,wBAAgB,kBAAkB,CAChC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,EAC5C,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,GAClC,IAAI,CAON;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAA;AAEpC,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,OAAQ,CAAA;AAE/C;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,gBAAgB,EAAE,cAAc,EAAE,CAAA;IAClC;;;;;;;;OAQG;IACH,sBAAsB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3E;;;;OAIG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAA;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAA;CAC/B;AAgED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAyC5B;AA6PD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE;IAAE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,EACpC,GAAG,GAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAA4B,GACzF,OAAO,CAAC,yBAAyB,CAAC,CAmBpC;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,SAAS,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,kBAAkB,EAAE,MAAM,EAAE,CAAA;IAC5B,gDAAgD;IAChD,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC3D,uDAAuD;IACvD,YAAY,EAAE,cAAc,EAAE,CAAA;IAC9B,+FAA+F;IAC/F,UAAU,EAAE,cAAc,EAAE,CAAA;IAC5B,mDAAmD;IACnD,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC/F;;;;;OAKG;IACH,sBAAsB,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAClI;;;;OAIG;IACH,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzE;;;;;;;;;;;;;OAaG;IACH,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;IAC/E;;;;;;;;;OASG;IACH,gBAAgB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9E;;;;;OAKG;IACH,kBAAkB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACpG;;;;OAIG;IACH,sBAAsB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzG;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,WAAW,EACpB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAAO,GACtE,OAAO,CAAC,WAAW,CAAC,CAoItB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAWxD;AAqID,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAmC7D;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,kBAAkB,GAAG,oBAAoB,CAAC,GAAG,MAAM,EAAE,CAiBhH;AAED,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAA;AAyCpE;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,aAAa,EAAE,EACzB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuE/C;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CAAC,CAWhD"}
@@ -1,10 +1,10 @@
1
1
  import db from '@adonisjs/lucid/services/db';
2
2
  import { v7 as uuidv7 } from 'uuid';
3
- import { assertCatalogUuid, assertNoSlugCollisions, assertScopeType, assertValidSlug, scopeFromKey, scopeKey } from './identity.js';
4
- import { CatalogConflictError, InvalidIdentityError, RoleNotAssignableAtError, UnknownPermissionError } from './errors.js';
5
- import { guardSql } from './drivers/backend_guard.js';
3
+ import { assertCatalogUuid, assertNoSlugCollisions, assertScopeType, assertValidSlug, scopeFromKey, scopeKey } from '../identity.js';
4
+ import { CatalogConflictError, InvalidIdentityError, RoleNotAssignableAtError, UnknownPermissionError } from '../errors.js';
5
+ import { guardSql } from '../shared/backend_guard.js';
6
6
  import { GLOBAL_OWNER_KEY, invalidateAuthzCatalog, parseAssignableAt, withAuthzCatalogWrite } from './catalog_cache.js';
7
- import { systemClock } from './clock.js';
7
+ import { systemClock } from '../clock.js';
8
8
  /**
9
9
  * `assignableAt` de un permiso tal como se GUARDA (`authz_permissions.assignable_at`):
10
10
  * JSON ordenado y sin duplicados, o `null` = cualquier nivel. Ordenado para
@@ -158,15 +158,29 @@ export async function syncAuthzCatalog(catalog, options = {}) {
158
158
  // que confirmó tiene que verse en la siguiente pregunta también con
159
159
  // `everyMs`, y uno que falló al confirmar no se sabe si confirmó. Recargar
160
160
  // de más es gratis; servir un catálogo viejo no.
161
+ let report;
162
+ let snapshot = null;
161
163
  try {
162
- return await syncInTransaction(catalog, prune, sql, one, timeoutMs);
164
+ const done = await syncInTransaction(catalog, prune, sql, one, timeoutMs, options.projection);
165
+ report = done.report;
166
+ snapshot = done.snapshot;
163
167
  }
164
168
  finally {
165
169
  invalidateAuthzCatalog();
166
170
  }
171
+ // La proyección se rehace con el catálogo YA CONFIRMADO y fuera de la
172
+ // transacción: el store no participa del commit de SQL (no hay 2PC), así
173
+ // que la fuente de verdad se escribe primero y lo derivado después. Un
174
+ // fallo aquí deja deriva —reconstruible con `authz:reconcile`—, nunca una
175
+ // concesión que las tablas no respalden.
176
+ if (options.projection && snapshot) {
177
+ report.projection = await options.projection.project(snapshot);
178
+ }
179
+ return report;
167
180
  }
168
- async function syncInTransaction(catalog, prune, sql, one, timeoutMs) {
181
+ async function syncInTransaction(catalog, prune, sql, one, timeoutMs, projection) {
169
182
  const report = { shadowedByGlobal: [], assignableAtViolations: [] };
183
+ let snapshot = null;
170
184
  await sql('sync', () => withAuthzCatalogWrite(async (trx) => {
171
185
  // 0. Colisión tras codificar también contra lo que YA hay en la base:
172
186
  // `docs:write` de otro catálogo y `docs_write` de este serían UNA
@@ -176,6 +190,19 @@ async function syncInTransaction(catalog, prune, sql, one, timeoutMs) {
176
190
  ...catalog.permissions.map((p) => p.slug),
177
191
  ...stored.map((p) => p.slug),
178
192
  ]);
193
+ // 0.5 ¿El catálogo que va a quedar es PUBLICABLE en el backend del
194
+ // driver? (3b-2a · A3/A4). ANTES de escribir nada: si se escribiera
195
+ // primero, un catálogo que rebasa el techo del modelo quedaría en la
196
+ // base sin poder proyectarse nunca y con el store sin poder
197
+ // regenerarse. Los permisos que van a existir son los de la base más
198
+ // los del spec (el sync no borra permisos).
199
+ if (projection) {
200
+ const willExist = new Set([
201
+ ...stored.map((p) => String(p.slug)),
202
+ ...catalog.permissions.map((p) => p.slug),
203
+ ]);
204
+ projection.assertPublishable([...willExist].sort());
205
+ }
179
206
  // 1. Permisos: upsert por slug; `assignable_at` manda el config (B5).
180
207
  for (const perm of catalog.permissions) {
181
208
  const existing = await one('sync.permission', () => trx.from('authz_permissions').where('slug', perm.slug).select('uuid', 'assignable_at'));
@@ -331,11 +358,54 @@ async function syncInTransaction(catalog, prune, sql, one, timeoutMs) {
331
358
  });
332
359
  }
333
360
  }
361
+ // 3.9 Foto del catálogo para la proyección del driver (3b-2a · A5).
362
+ // Se lee DENTRO de la transacción para que sea coherente, y se
363
+ // proyecta fuera, con el commit hecho. Es el catálogo ENTERO, no el
364
+ // spec: la proyección es un espejo, y lo que sobra en el store solo
365
+ // se sabe comparando contra todo.
366
+ if (projection) {
367
+ snapshot = await readCatalogProjectionSnapshot(trx, (operation, fn) => sql(`sync.projection.${operation}`, fn));
368
+ }
334
369
  // 4. La versión compartida la sube `withAuthzCatalogWrite` al salir de
335
370
  // aquí, como última sentencia: o se confirma todo (catálogo nuevo +
336
371
  // versión nueva) o nada.
337
372
  }, { driver: 'catalog', timeoutMs }));
338
- return report;
373
+ return { report, snapshot };
374
+ }
375
+ /**
376
+ * **La foto del catálogo que se PROYECTA** (3b-2a · A5), leída de `authz_*`.
377
+ *
378
+ * Es el catálogo ENTERO, no el spec que se está sincronizando: la proyección
379
+ * es un espejo y lo que sobra en el backend solo se sabe comparando contra
380
+ * todo. `syncAuthzCatalog` la lee DENTRO de su transacción (coherente con lo
381
+ * que acaba de escribir) y `authz:reconcile` fuera, contra la base: las dos
382
+ * tienen que construir exactamente la misma foto, o reconcile "arreglaría" en
383
+ * cada pasada lo que el sync acaba de dejar bien. Por eso está aquí y no
384
+ * duplicada en el driver.
385
+ *
386
+ * `source` es cualquier cosa con `from(tabla)` (el `db` de Lucid o una
387
+ * transacción) y `sql` envuelve cada consulta con el deadline de quien llama.
388
+ */
389
+ export async function readCatalogProjectionSnapshot(source, sql = (_operation, fn) => fn()) {
390
+ const permissions = await sql('permissions', () => source.from('authz_permissions').select('uuid', 'slug'));
391
+ const roles = await sql('roles', () => source.from('authz_roles').select('uuid'));
392
+ const links = await sql('links', () => source.from('authz_role_permissions').select('role_uuid', 'permission_uuid'));
393
+ const slugByUuid = new Map(permissions.map((p) => [String(p.uuid), String(p.slug)]));
394
+ const permissionsByRole = new Map(roles.map((r) => [String(r.uuid), []]));
395
+ for (const link of links) {
396
+ const slug = slugByUuid.get(String(link.permission_uuid));
397
+ const list = permissionsByRole.get(String(link.role_uuid));
398
+ // Un vínculo sin rol o sin permiso no existe (FK): si apareciera,
399
+ // proyectarlo sería inventar catálogo.
400
+ if (slug && list)
401
+ list.push(slug);
402
+ }
403
+ return {
404
+ permissions: [...slugByUuid.values()].sort(),
405
+ roles: [...permissionsByRole.entries()]
406
+ .map(([uuid, perms]) => ({ uuid, permissions: perms.sort() }))
407
+ .sort((a, b) => (a.uuid < b.uuid ? -1 : a.uuid > b.uuid ? 1 : 0)),
408
+ };
339
409
  }
340
410
  /**
341
411
  * Compara un spec con lo que hay en `authz_*`, sin escribir. Igual que el
@@ -555,12 +625,22 @@ async function classifyHomonyms(sql, resolveChain) {
555
625
  // eso dejaba de detectarse una pareja de locales CONTRADICTORIA (dos
556
626
  // owners que se declaran ancestro el uno del otro), que es la única
557
627
  // deriva de verdad de esta clasificación: un caso ciego nuevo.
558
- for (const a of locals) {
559
- for (const b of locals) {
628
+ // 3b-1 · T-3b 3 (tester 3F · §6.3): UNA línea por rol ENSOMBRECIDO, no
629
+ // una por pareja. Con owners anidados a > b > c salían tres (a→b, a→c,
630
+ // b→c) para tres roles, y la tercera no añadía nada: lo que el operador
631
+ // necesita saber es qué `(slug, nivel)` está muerto y quién manda ahí.
632
+ // El ensombrecedor que se nombra es el MÁS AUTORIZADO —el ancestro más
633
+ // alto de la cadena del ensombrecido—, que es el orden que 3F · S3 fijó.
634
+ for (const b of locals) {
635
+ const chainB = await chainOf(b);
636
+ if (!chainB)
637
+ continue;
638
+ const shadowers = [];
639
+ for (const a of locals) {
560
640
  if (a === b)
561
641
  continue;
562
642
  // `a` está en la cadena de `b`: `a` es su ancestro y lo ensombrece.
563
- if (!(await chainOf(b))?.includes(a))
643
+ if (!chainB.includes(a))
564
644
  continue;
565
645
  if ((await chainOf(a))?.includes(b)) {
566
646
  // Y `b` en la de `a`: el árbol se contradice y nadie manda.
@@ -570,8 +650,13 @@ async function classifyHomonyms(sql, resolveChain) {
570
650
  }
571
651
  continue;
572
652
  }
573
- result.shadowedByAncestor.push({ slug, scopeType, owner: b, shadowedBy: a });
653
+ shadowers.push(a);
574
654
  }
655
+ if (!shadowers.length)
656
+ continue;
657
+ // Más lejos en la cadena de `b` = más arriba en el árbol = más autoridad.
658
+ shadowers.sort((x, y) => chainB.indexOf(y) - chainB.indexOf(x));
659
+ result.shadowedByAncestor.push({ slug, scopeType, owner: b, shadowedBy: shadowers[0] });
575
660
  }
576
661
  }
577
662
  return result;
@@ -692,11 +777,31 @@ export async function runCatalogDiff(catalogs, options = {}) {
692
777
  // extraerlos, y sus líneas salían indentadas DENTRO del bloque de
693
778
  // diferencias del último catálogo, como si fueran suyas).
694
779
  let scoped = [];
695
- let shadowed = [];
780
+ // 3b-1 · T-3b 1 (tester 3F · §6.1): las sombras se ACUMULAN sobre todos los
781
+ // catálogos, con deduplicación. `diff.shadowedByGlobal` tiene una fuente
782
+ // DEPENDIENTE del spec (un rol del spec homónimo de un local), así que
783
+ // tomarlas del índice 0 perdía por completo las que causaba un rol del
784
+ // catálogo #2: no salían como línea de sombras ni como diferencia de ese
785
+ // catálogo. `scopedRoles` sí sale del índice 0: lee la BASE y no depende
786
+ // del spec, así que repetir el diff entero solo para él sería gratuito
787
+ // (3E · Q6).
788
+ const sombras = {
789
+ shadowedByGlobal: [],
790
+ shadowedByAncestor: [],
791
+ };
696
792
  for (const [index, spec] of specs.entries()) {
697
793
  const diff = await diffAuthzCatalog(spec, options);
698
- if (index === 0)
699
- shadowed = formatShadowedRoles(diff);
794
+ for (const s of diff.shadowedByGlobal) {
795
+ if (sombras.shadowedByGlobal.some((x) => x.slug === s.slug && x.scopeType === s.scopeType && x.owner === s.owner))
796
+ continue;
797
+ sombras.shadowedByGlobal.push(s);
798
+ }
799
+ for (const s of diff.shadowedByAncestor) {
800
+ if (sombras.shadowedByAncestor.some((x) => x.slug === s.slug && x.scopeType === s.scopeType && x.owner === s.owner && x.shadowedBy === s.shadowedBy)) {
801
+ continue;
802
+ }
803
+ sombras.shadowedByAncestor.push(s);
804
+ }
700
805
  if (index === 0)
701
806
  scoped = formatScopedRoles(diff);
702
807
  if (catalogInSync(diff)) {
@@ -708,6 +813,7 @@ export async function runCatalogDiff(catalogs, options = {}) {
708
813
  for (const line of formatCatalogDiff(diff))
709
814
  lines.push(` ${line}`);
710
815
  }
816
+ const shadowed = formatShadowedRoles(sombras);
711
817
  if (shadowed.length) {
712
818
  lines.push('roles locales ENSOMBRECIDOS por una definición más autorizada (no son deriva: 3F · S3):');
713
819
  for (const line of shadowed)