@jantstack/adonis-authz 2.0.0-alpha.1 → 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.
- package/README.md +462 -35
- package/build/commands/authz_catalog_diff.js +1 -1
- package/build/commands/authz_catalog_diff.js.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.js +136 -0
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
- package/build/commands/authz_catalog_sync.d.ts +17 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -1
- package/build/commands/authz_catalog_sync.js +27 -4
- package/build/commands/authz_catalog_sync.js.map +1 -1
- package/build/commands/authz_freeze.d.ts +44 -0
- package/build/commands/authz_freeze.d.ts.map +1 -0
- package/build/commands/authz_freeze.js +95 -0
- package/build/commands/authz_freeze.js.map +1 -0
- package/build/commands/authz_reconcile.d.ts +102 -0
- package/build/commands/authz_reconcile.d.ts.map +1 -0
- package/build/commands/authz_reconcile.js +294 -0
- package/build/commands/authz_reconcile.js.map +1 -0
- package/build/commands/authz_relations_reconcile.d.ts +73 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
- package/build/commands/authz_relations_reconcile.js +225 -0
- package/build/commands/authz_relations_reconcile.js.map +1 -0
- package/build/commands/authz_scopes_relay.d.ts +47 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -0
- package/build/commands/authz_scopes_relay.js +141 -0
- package/build/commands/authz_scopes_relay.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +37 -0
- package/build/commands/authz_unfreeze.d.ts.map +1 -0
- package/build/commands/authz_unfreeze.js +92 -0
- package/build/commands/authz_unfreeze.js.map +1 -0
- package/build/commands/main.d.ts +6 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +6 -1
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +46 -4
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +90 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/configure.d.ts +11 -0
- package/build/configure.d.ts.map +1 -1
- package/build/configure.js +37 -1
- package/build/configure.js.map +1 -1
- package/build/index.d.ts +39 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +35 -6
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +26 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +48 -2
- package/build/providers/authz_provider.js.map +1 -1
- package/build/services/relations.d.ts +14 -0
- package/build/services/relations.d.ts.map +1 -0
- package/build/services/relations.js +17 -0
- package/build/services/relations.js.map +1 -0
- package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
- package/build/src/catalog/catalog.d.ts.map +1 -0
- package/build/src/{catalog.js → catalog/catalog.js} +120 -14
- package/build/src/catalog/catalog.js.map +1 -0
- package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
- package/build/src/catalog/catalog_cache.d.ts.map +1 -0
- package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
- package/build/src/catalog/catalog_cache.js.map +1 -0
- package/build/src/define_config.d.ts +101 -3
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +86 -4
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +425 -11
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +75 -0
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/database_relations_driver.js +450 -0
- package/build/src/drivers/database_relations_driver.js.map +1 -0
- package/build/src/drivers/openfga_driver.d.ts +713 -119
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2048 -476
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +369 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -0
- package/build/src/drivers/openfga_facts.js +813 -0
- package/build/src/drivers/openfga_facts.js.map +1 -0
- package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/openfga_relations_driver.js +466 -0
- package/build/src/drivers/openfga_relations_driver.js.map +1 -0
- package/build/src/errors.d.ts +258 -5
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +238 -7
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +120 -0
- package/build/src/freeze.d.ts.map +1 -0
- package/build/src/freeze.js +172 -0
- package/build/src/freeze.js.map +1 -0
- package/build/src/http/app_access_middleware.d.ts.map +1 -0
- package/build/src/http/app_access_middleware.js.map +1 -0
- package/build/src/http/resource_access_middleware.d.ts +105 -0
- package/build/src/http/resource_access_middleware.d.ts.map +1 -0
- package/build/src/http/resource_access_middleware.js +81 -0
- package/build/src/http/resource_access_middleware.js.map +1 -0
- package/build/src/identity.d.ts +74 -1
- package/build/src/identity.d.ts.map +1 -1
- package/build/src/identity.js +100 -2
- package/build/src/identity.js.map +1 -1
- package/build/src/manager.d.ts +315 -4
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1187 -181
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +6 -6
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +6 -6
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +6 -6
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +6 -6
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +6 -6
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +18 -2
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +15 -1
- package/build/src/openfga.js.map +1 -1
- package/build/src/reconcile.d.ts +37 -0
- package/build/src/reconcile.d.ts.map +1 -0
- package/build/src/reconcile.js +69 -0
- package/build/src/reconcile.js.map +1 -0
- package/build/src/relation_partition_trigger.d.ts +8 -0
- package/build/src/relation_partition_trigger.d.ts.map +1 -0
- package/build/src/relation_partition_trigger.js +85 -0
- package/build/src/relation_partition_trigger.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +58 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -0
- package/build/src/relations/define_relations_config.js +144 -0
- package/build/src/relations/define_relations_config.js.map +1 -0
- package/build/src/relations/manager.d.ts +38 -0
- package/build/src/relations/manager.d.ts.map +1 -0
- package/build/src/relations/manager.js +156 -0
- package/build/src/relations/manager.js.map +1 -0
- package/build/src/relations/reconcile.d.ts +62 -0
- package/build/src/relations/reconcile.d.ts.map +1 -0
- package/build/src/relations/reconcile.js +138 -0
- package/build/src/relations/reconcile.js.map +1 -0
- package/build/src/relations_config_store.d.ts +22 -0
- package/build/src/relations_config_store.d.ts.map +1 -0
- package/build/src/relations_config_store.js +74 -0
- package/build/src/relations_config_store.js.map +1 -0
- package/build/src/scope_outbox.d.ts +69 -0
- package/build/src/scope_outbox.d.ts.map +1 -0
- package/build/src/scope_outbox.js +291 -0
- package/build/src/scope_outbox.js.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.d.ts +14 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.js +26 -1
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/sql_descendants.d.ts +47 -1
- package/build/src/sql_descendants.d.ts.map +1 -1
- package/build/src/sql_descendants.js +75 -1
- package/build/src/sql_descendants.js.map +1 -1
- package/build/src/testing/contract.d.ts +74 -0
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +672 -169
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +6 -0
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +3 -0
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +284 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -0
- package/build/src/testing/migration_contract.js +586 -0
- package/build/src/testing/migration_contract.js.map +1 -0
- package/build/src/testing/relations_contract.d.ts +51 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -0
- package/build/src/testing/relations_contract.js +654 -0
- package/build/src/testing/relations_contract.js.map +1 -0
- package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
- package/build/src/testing/relations_reconcile_contract.js +172 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -0
- package/build/src/traits/authz_scopes.js +1 -1
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +7 -7
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +865 -82
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +10 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +104 -4
- package/build/stubs/migration.stub +126 -0
- package/build/stubs/scopes_outbox_migration.stub +57 -0
- package/package.json +4 -2
- package/build/commands/openfga_import.d.ts +0 -34
- package/build/commands/openfga_import.d.ts.map +0 -1
- package/build/commands/openfga_import.js +0 -97
- package/build/commands/openfga_import.js.map +0 -1
- package/build/src/catalog.d.ts.map +0 -1
- package/build/src/catalog.js.map +0 -1
- package/build/src/catalog_cache.d.ts.map +0 -1
- package/build/src/catalog_cache.js.map +0 -1
- package/build/src/drivers/backend_guard.d.ts.map +0 -1
- package/build/src/drivers/backend_guard.js.map +0 -1
- package/build/src/drivers/sql_expiry.d.ts.map +0 -1
- package/build/src/drivers/sql_expiry.js.map +0 -1
- package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
- package/build/src/middleware/app_access_middleware.js.map +0 -1
- /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
- /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.js +0 -0
|
@@ -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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **La config de relaciones PERSISTIDA** (Fase 4, lote 4-5 · 🟡3) — vive en la
|
|
3
|
+
* tabla `authz_relations_config`, bajo el gate de versión (invariante 14), como
|
|
4
|
+
* el catálogo. Es la copia de la config de relaciones en la BASE, para que
|
|
5
|
+
* cualquier proceso —también uno SIN `defineRelationsConfig` en memoria— lea
|
|
6
|
+
* los tipos declarados. Hoy la CONSUME `authz:relations:reconcile` (la config
|
|
7
|
+
* del DESTINO, para vigilar la deriva del modelo fusionado en `--dry-run`).
|
|
8
|
+
*
|
|
9
|
+
* Este módulo vive en `src/` (NO en `src/relations/`, que la regla 2 de pureza
|
|
10
|
+
* mantiene aislado del `db`): es la ruta que TOCA la base, como `catalog.ts`.
|
|
11
|
+
* Importa el TIPO y el validador de `src/relations/` (la dirección permitida:
|
|
12
|
+
* `relations/` no importa hacia afuera; los demás sí importan de él).
|
|
13
|
+
*
|
|
14
|
+
* **Nota (Fase 4-8):** el modelo fusionado del store lo publica
|
|
15
|
+
* `openfga:provision` leyendo los tipos de relación del CONFIG estático
|
|
16
|
+
* (`relations.config`, como `permissions` sale de `catalogs`), no de esta
|
|
17
|
+
* tabla: el comando es `startApp: false` (bootstrap del appliance, sin base) y
|
|
18
|
+
* no puede leer la base. Por eso el antiguo `republishFusedModel` —el
|
|
19
|
+
* republicador basado en esta tabla, con CAS sobre `model_id`— se retiró en
|
|
20
|
+
* 4-8: no tenía llamador de producción (la carrera que protegía no ocurre;
|
|
21
|
+
* republicar el modelo es un paso manual de operador, escritor único). Ver el
|
|
22
|
+
* informe 4-8. La columna `model_id` queda sin uso (cleanup futuro).
|
|
23
|
+
*/
|
|
24
|
+
import { systemClock } from './clock.js';
|
|
25
|
+
import { guardSql } from './shared/backend_guard.js';
|
|
26
|
+
import db from '@adonisjs/lucid/services/db';
|
|
27
|
+
import { invalidateAuthzCatalog, withAuthzCatalogWrite } from './catalog/catalog_cache.js';
|
|
28
|
+
import { defineRelationsConfig } from './relations/define_relations_config.js';
|
|
29
|
+
const TABLE = 'authz_relations_config';
|
|
30
|
+
const ROW_ID = 1;
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
32
|
+
function connectionOf(options) {
|
|
33
|
+
return options?.connection ? db.connection(options.connection) : db;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* **Guarda (persiste) la config de relaciones** bajo el gate de versión: la
|
|
37
|
+
* valida (⚪4/F-05 de config), la serializa a JSON y hace UPSERT de la fila
|
|
38
|
+
* `id=1` dentro de `withAuthzCatalogWrite` —que sube la versión compartida como
|
|
39
|
+
* última sentencia (invariante 14): un sync en otro proceso la ve en la
|
|
40
|
+
* siguiente pregunta—. Devuelve la config congelada.
|
|
41
|
+
*/
|
|
42
|
+
export async function saveRelationsConfig(spec, options = {}) {
|
|
43
|
+
// Valida ANTES de escribir: una config inválida no ensucia la tabla.
|
|
44
|
+
const config = defineRelationsConfig(spec);
|
|
45
|
+
const json = JSON.stringify(spec);
|
|
46
|
+
await withAuthzCatalogWrite(async (trx) => {
|
|
47
|
+
const existing = await trx.from(TABLE).where('id', ROW_ID).first();
|
|
48
|
+
if (existing) {
|
|
49
|
+
await trx.from(TABLE).where('id', ROW_ID).update({ spec: json, updated_at: systemClock() });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await trx.table(TABLE).insert({ id: ROW_ID, spec: json, model_id: null, updated_at: systemClock() });
|
|
53
|
+
}
|
|
54
|
+
}, options);
|
|
55
|
+
invalidateAuthzCatalog();
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
/** Lee el spec persistido (o `null` si no hay relaciones declaradas — opt-in). */
|
|
59
|
+
export async function readRelationsConfigSpec(options = {}) {
|
|
60
|
+
const row = await guardSql('relations-config', 'read', options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => connectionOf(options).from(TABLE).where('id', ROW_ID).first());
|
|
61
|
+
if (!row?.spec)
|
|
62
|
+
return null;
|
|
63
|
+
return JSON.parse(String(row.spec));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Lee la config persistida ya VALIDADA (o `null`). Es lo que hace 🟡3: un
|
|
67
|
+
* proceso SIN `defineRelationsConfig` en memoria recupera aquí los tipos de
|
|
68
|
+
* relación de la base, así que el modelo que republique los lleva.
|
|
69
|
+
*/
|
|
70
|
+
export async function readRelationsConfig(options = {}) {
|
|
71
|
+
const spec = await readRelationsConfigSpec(options);
|
|
72
|
+
return spec ? defineRelationsConfig(spec) : null;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=relations_config_store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relations_config_store.js","sourceRoot":"","sources":["../../src/relations_config_store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AACpD,OAAO,EAAE,MAAM,6BAA6B,CAAA;AAC5C,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAG9E,MAAM,KAAK,GAAG,wBAAwB,CAAA;AACtC,MAAM,MAAM,GAAG,CAAC,CAAA;AAChB,MAAM,kBAAkB,GAAG,KAAK,CAAA;AAOhC,SAAS,YAAY,CAAC,OAAqC;IACzD,OAAO,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACrE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAyB,EACzB,UAAuC,EAAE;IAEzC,qEAAqE;IACrE,MAAM,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,qBAAqB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACxC,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAA;QAClE,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;QAC7F,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;QACtG,CAAC;IACH,CAAC,EAAE,OAAO,CAAC,CAAA;IACX,sBAAsB,EAAE,CAAA;IACxB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,UAAuC,EAAE;IAEzC,MAAM,GAAG,GAAQ,MAAM,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB,EAAE,GAAG,EAAE,CACxG,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAC9D,CAAA;IACD,IAAI,CAAC,GAAG,EAAE,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAwB,CAAA;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAAuC,EAAE;IAEzC,MAAM,IAAI,GAAG,MAAM,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAClD,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **La outbox del árbol sobre Lucid** (3b-2d; panel 2, cruce 4 · S5).
|
|
3
|
+
*
|
|
4
|
+
* El puerto es `ScopeOutbox` (`src/types.ts`) y el paquete NO impone tabla:
|
|
5
|
+
* cualquier implementación vale. Esta es la que se publica para quien no
|
|
6
|
+
* quiera escribir la suya, junto con su migración
|
|
7
|
+
* (`stubs/scopes_outbox_migration.stub`, que hay que COPIAR al proyecto:
|
|
8
|
+
* `configure` no la publica sola porque la outbox es opt-in).
|
|
9
|
+
*
|
|
10
|
+
* import { sqlScopeOutbox } from '@jantstack/adonis-authz'
|
|
11
|
+
*
|
|
12
|
+
* const outbox = sqlScopeOutbox()
|
|
13
|
+
* export default defineConfig({
|
|
14
|
+
* scopes: { resolveChain, outbox },
|
|
15
|
+
* ...
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* // y en el sitio donde mueves tu árbol:
|
|
19
|
+
* await db.transaction(async (trx) => {
|
|
20
|
+
* await authorization.scopes.moved(unit, nuevaOrg, { within, actor, transaction: trx })
|
|
21
|
+
* await unit.useTransaction(trx).merge({ organizationId: nuevaOrg.uuid }).save()
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* Lo único que tiene que cumplir una implementación del puerto es que
|
|
25
|
+
* `enqueue` escriba DENTRO de la transacción que le pasan: es lo que hace
|
|
26
|
+
* que el cambio del árbol y su propagación al backend confirmen —o se vayan—
|
|
27
|
+
* juntos. Todo lo demás (nombres de columna, si las filas aplicadas se
|
|
28
|
+
* borran o se marcan, si hay reintento con backoff) es del consumidor.
|
|
29
|
+
*
|
|
30
|
+
* Aquí las filas aplicadas se MARCAN (`applied_at`), no se borran: la cola
|
|
31
|
+
* es también el registro de lo que se propagó y cuándo, y `authz:scopes:relay`
|
|
32
|
+
* no puede ser silencioso.
|
|
33
|
+
*/
|
|
34
|
+
import type { Clock } from './clock.js';
|
|
35
|
+
import type { ScopeOutbox } from './types.js';
|
|
36
|
+
export interface SqlScopeOutboxOptions {
|
|
37
|
+
/** Tabla de la cola (default `authz_scope_outbox`, la del stub). */
|
|
38
|
+
table?: string;
|
|
39
|
+
/** Nombre de la conexión Lucid (default: la primaria). */
|
|
40
|
+
connection?: string;
|
|
41
|
+
/** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Reloj de pared con el que se sellan `created_at` y `applied_at`
|
|
45
|
+
* (2.5 · J1). Default `() => new Date()`; inyectable para fijar el
|
|
46
|
+
* instante en tests.
|
|
47
|
+
*/
|
|
48
|
+
now?: Clock;
|
|
49
|
+
/**
|
|
50
|
+
* **Cuántas veces se reintenta una entrada antes de APARCARLA** (3b-2h ·
|
|
51
|
+
* 🔴 2; default 5). Alcanzado el tope, `pending()` deja de ofrecerla y sale
|
|
52
|
+
* por `dead()`: hay entradas que no se pueden aplicar NUNCA —el scope padre
|
|
53
|
+
* del `attached` encolado lo borró el tenant antes del relevo y no va a
|
|
54
|
+
* volver— y sin tope esa fila se reintenta en todas las pasadas para
|
|
55
|
+
* siempre. Aparcar no es olvidar: el relay la reporta en cada pasada y el
|
|
56
|
+
* comando sale ≠ 0 mientras haya alguna, porque el árbol del backend queda
|
|
57
|
+
* permanentemente divergente en ese nodo (lo reconcilia `authz:reconcile`,
|
|
58
|
+
* 3b-3, o un `scopes.detached`/`attached` nuevo del consumidor).
|
|
59
|
+
*/
|
|
60
|
+
maxAttempts?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Lo que se necesita del `db` de Lucid; inyectable para probar sin servidor. */
|
|
63
|
+
interface DatabaseLike {
|
|
64
|
+
from(table: string): any;
|
|
65
|
+
table(table: string): any;
|
|
66
|
+
}
|
|
67
|
+
export declare function sqlScopeOutbox(options?: SqlScopeOutboxOptions, database?: DatabaseLike): ScopeOutbox;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=scope_outbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope_outbox.d.ts","sourceRoot":"","sources":["../../src/scope_outbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAKvC,OAAO,KAAK,EAEV,WAAW,EAKZ,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,KAAK,CAAA;IACX;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgBD,iFAAiF;AACjF,UAAU,YAAY;IACpB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;IACxB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;CAC1B;AAuFD,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,EACnC,QAAQ,GAAE,YAA4C,GACrD,WAAW,CAsLb"}
|