@jantstack/adonis-authz 2.0.0-alpha.1 → 2.4.0-alpha.10
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 +934 -40
- package/build/commands/authz_catalog_diff.d.ts.map +1 -1
- package/build/commands/authz_catalog_diff.js +1 -2
- 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 +135 -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 +94 -0
- package/build/commands/authz_freeze.js.map +1 -0
- package/build/commands/authz_reconcile.d.ts +111 -0
- package/build/commands/authz_reconcile.d.ts.map +1 -0
- package/build/commands/authz_reconcile.js +417 -0
- package/build/commands/authz_reconcile.js.map +1 -0
- package/build/commands/authz_relations_reconcile.d.ts +144 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
- package/build/commands/authz_relations_reconcile.js +438 -0
- package/build/commands/authz_relations_reconcile.js.map +1 -0
- package/build/commands/authz_scopes_relay.d.ts +97 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -0
- package/build/commands/authz_scopes_relay.js +344 -0
- package/build/commands/authz_scopes_relay.js.map +1 -0
- package/build/commands/authz_scopes_requeue.d.ts +80 -0
- package/build/commands/authz_scopes_requeue.d.ts.map +1 -0
- package/build/commands/authz_scopes_requeue.js +230 -0
- package/build/commands/authz_scopes_requeue.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +45 -0
- package/build/commands/authz_unfreeze.d.ts.map +1 -0
- package/build/commands/authz_unfreeze.js +157 -0
- package/build/commands/authz_unfreeze.js.map +1 -0
- package/build/commands/main.d.ts +16 -8
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +59 -6
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +92 -4
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +155 -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 +50 -11
- package/build/index.d.ts.map +1 -1
- package/build/index.js +49 -7
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +31 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +65 -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 +206 -3
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js +11 -0
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +128 -7
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +510 -24
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +140 -0
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/database_relations_driver.js +748 -0
- package/build/src/drivers/database_relations_driver.js.map +1 -0
- package/build/src/drivers/openfga_driver.d.ts +777 -122
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2228 -477
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +384 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -0
- package/build/src/drivers/openfga_facts.js +836 -0
- package/build/src/drivers/openfga_facts.js.map +1 -0
- package/build/src/drivers/openfga_relations_driver.d.ts +186 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/openfga_relations_driver.js +724 -0
- package/build/src/drivers/openfga_relations_driver.js.map +1 -0
- package/build/src/errors.d.ts +430 -6
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +453 -6
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +200 -0
- package/build/src/freeze.d.ts.map +1 -0
- package/build/src/freeze.js +306 -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 +549 -6
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +2243 -193
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +7 -7
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +7 -7
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +7 -7
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +7 -7
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +7 -7
- 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 +93 -0
- package/build/src/relation_partition_trigger.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +109 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -0
- package/build/src/relations/define_relations_config.js +211 -0
- package/build/src/relations/define_relations_config.js.map +1 -0
- package/build/src/relations/manager.d.ts +138 -0
- package/build/src/relations/manager.d.ts.map +1 -0
- package/build/src/relations/manager.js +494 -0
- package/build/src/relations/manager.js.map +1 -0
- package/build/src/relations/reconcile.d.ts +137 -0
- package/build/src/relations/reconcile.d.ts.map +1 -0
- package/build/src/relations/reconcile.js +294 -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 +123 -0
- package/build/src/scope_outbox.d.ts.map +1 -0
- package/build/src/scope_outbox.js +532 -0
- package/build/src/scope_outbox.js.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.d.ts +42 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.js +78 -1
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/{drivers → shared}/sql_expiry.d.ts +2 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/{drivers → shared}/sql_expiry.js +4 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/shared/transaction_guard.d.ts +53 -0
- package/build/src/shared/transaction_guard.d.ts.map +1 -0
- package/build/src/shared/transaction_guard.js +74 -0
- package/build/src/shared/transaction_guard.js.map +1 -0
- package/build/src/sql_descendants.d.ts +55 -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 +129 -4
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +915 -177
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +10 -2
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +5 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +293 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -0
- package/build/src/testing/migration_contract.js +589 -0
- package/build/src/testing/migration_contract.js.map +1 -0
- package/build/src/testing/relations_contract.d.ts +73 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -0
- package/build/src/testing/relations_contract.js +1238 -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 +220 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -0
- package/build/src/testing/scope_outbox_contract.d.ts +98 -0
- package/build/src/testing/scope_outbox_contract.d.ts.map +1 -0
- package/build/src/testing/scope_outbox_contract.js +677 -0
- package/build/src/testing/scope_outbox_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 +8 -8
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +1537 -84
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +65 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +149 -5
- package/build/stubs/migration.stub +136 -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/types.d.ts
CHANGED
|
@@ -76,6 +76,41 @@ export interface WriteOptions {
|
|
|
76
76
|
* quién puede conceder qué es policy del consumidor (invariante 8).
|
|
77
77
|
*/
|
|
78
78
|
actor?: SubjectRef;
|
|
79
|
+
/**
|
|
80
|
+
* **La transacción ABIERTA del consumidor** (`TransactionClientContract` de
|
|
81
|
+
* Lucid: el `trx` de `db.transaction()`), para que la escritura del paquete
|
|
82
|
+
* confirme o revierta CON la tuya — L-2, panel `{trx}` (C).
|
|
83
|
+
*
|
|
84
|
+
* **Encolar ≠ escribir.** Son dos promesas distintas y este campo hace UNA
|
|
85
|
+
* u OTRA según la operación:
|
|
86
|
+
*
|
|
87
|
+
* - En `grant`/`revoke`/`deny`/`removeDeny` (los HECHOS) significa
|
|
88
|
+
* **ESCRIBIR en tu transacción**: «los dos o ninguno» entre el hecho y
|
|
89
|
+
* tus filas, en el mismo motor transaccional. Solo lo cumple un driver
|
|
90
|
+
* que declare `capabilities.transactionalWrites: true` (`database`, con el
|
|
91
|
+
* `trx` de SU conexión: otra conexión, un `QueryClient` o el `db` entero
|
|
92
|
+
* son 500 `E_AUTHZ_CONFIG`, `assertCallerTransaction`). Con un driver
|
|
93
|
+
* que declare `false` —`openfga`: una tupla no entra en una transacción
|
|
94
|
+
* SQL, no hay 2PC— la llamada es **500 `E_AUTHZ_UNSUPPORTED`** nombrando
|
|
95
|
+
* driver y operación, **antes de tocar el driver** (cero llamadas):
|
|
96
|
+
* nunca se ignora, nunca un aviso. Quien quiera fallar al ARRANCAR en
|
|
97
|
+
* vez de en una ruta poco transitada declara `requireTransactionalWrites:
|
|
98
|
+
* true` en el config (500 `E_AUTHZ_CONFIG` al resolver el driver).
|
|
99
|
+
* - En `scopes.attached/moved/detached` significa **ENCOLAR en tu
|
|
100
|
+
* transacción** (3b-2d, `ScopeTreeWriteOptions.transaction`): el INSERT
|
|
101
|
+
* de la outbox cae dentro de ella; el backend NO se toca dentro de tu
|
|
102
|
+
* transacción y no pasa por la puerta de la capacidad.
|
|
103
|
+
* - En la API de delegación (`defineScopedRole`/`updateScopedRole`/
|
|
104
|
+
* `deleteScopedRole`) **no se admite** (500 `E_AUTHZ_UNSUPPORTED`): esas
|
|
105
|
+
* escriben el catálogo por `withAuthzCatalogWrite`, que ES el
|
|
106
|
+
* serializador entre procesos (cerrojo + bump como última sentencia,
|
|
107
|
+
* invariante 14); moverlas al commit del consumidor lo anularía.
|
|
108
|
+
*
|
|
109
|
+
* Lo que NUNCA viaja por ella, en ninguna de las tres: **la autoridad**
|
|
110
|
+
* (L-1 · 🟠 8) — la barrera del freeze, el catálogo y `resolveChain` se leen
|
|
111
|
+
* por la conexión del motor, así que `{ transaction }` exige **pool ≥ 2**.
|
|
112
|
+
*/
|
|
113
|
+
transaction?: unknown;
|
|
79
114
|
}
|
|
80
115
|
/**
|
|
81
116
|
* Opciones de las NUEVE escrituras del manager (`grant`, `revoke`, `deny`,
|
|
@@ -105,6 +140,38 @@ export interface ScopedWriteOptions extends WriteOptions {
|
|
|
105
140
|
*/
|
|
106
141
|
within?: ScopeRef;
|
|
107
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Opciones de las TRES notificaciones del árbol (`scopes.attached/moved/
|
|
145
|
+
* detached`) — 3b-2d. Añaden la transacción del consumidor, que solo se usa
|
|
146
|
+
* cuando hay `scopes.outbox` declarada: es lo que hace que el cambio del
|
|
147
|
+
* árbol y su encolado confirmen (o se vayan) juntos.
|
|
148
|
+
*/
|
|
149
|
+
export interface ScopeTreeWriteOptions extends ScopedWriteOptions {
|
|
150
|
+
/**
|
|
151
|
+
* La transacción ABIERTA del consumidor (`TransactionClientContract` de
|
|
152
|
+
* Lucid, o lo que use su outbox). El paquete no la interpreta: se la pasa
|
|
153
|
+
* tal cual a `scopes.outbox.enqueue` para que el INSERT del encolado caiga
|
|
154
|
+
* dentro de ella — **ENCOLAR, no escribir**: el backend no se toca dentro
|
|
155
|
+
* de tu transacción. Sin outbox declarada no hace nada; con outbox declarada
|
|
156
|
+
* y sin transacción, el encolado se confirma solo y vuelve a haber dos
|
|
157
|
+
* confirmaciones distintas (la outbox lo avisa si puede).
|
|
158
|
+
*
|
|
159
|
+
* **Lo que NUNCA viaja por ella: la autoridad** (L-1 · 🟠 8). La barrera
|
|
160
|
+
* del freeze se lee por la conexión del motor, jamás por esta transacción
|
|
161
|
+
* (su snapshot puede ser anterior al freeze). Por eso exige **pool ≥ 2**:
|
|
162
|
+
* con pool 1 (SQLite `:memory:`) la barrera no consigue conexión mientras
|
|
163
|
+
* tú sostienes la única y la notificación sale 503 `E_AUTHZ_BACKEND_TIMEOUT`
|
|
164
|
+
* (`freezeTimeoutMs`) — fail-closed, nunca un bypass. Y `sqlScopeOutbox`
|
|
165
|
+
* exige que sea una transacción ABIERTA de SU conexión: otra conexión, un
|
|
166
|
+
* `QueryClient` o el `db` entero son 500 `E_AUTHZ_CONFIG` (🟠 9).
|
|
167
|
+
*
|
|
168
|
+
* **Encolar ≠ escribir** (L-2): aquí la transacción ENCOLA; en
|
|
169
|
+
* `grant`/`revoke`/`deny`/`removeDeny` (`WriteOptions.transaction`) ESCRIBE
|
|
170
|
+
* el hecho dentro de ella y pasa por la puerta de `transactionalWrites`.
|
|
171
|
+
* Esta notificación no pasa por esa puerta: un driver `openfga` la acepta.
|
|
172
|
+
*/
|
|
173
|
+
transaction?: unknown;
|
|
174
|
+
}
|
|
108
175
|
export interface GrantOptions extends ScopedWriteOptions {
|
|
109
176
|
/**
|
|
110
177
|
* Caducidad de la asignación, en TRES estados (L0.4):
|
|
@@ -162,7 +229,133 @@ export type NormalizedRoleQuery = {
|
|
|
162
229
|
slug?: undefined;
|
|
163
230
|
scopeType?: undefined;
|
|
164
231
|
};
|
|
232
|
+
/**
|
|
233
|
+
* **Lo que un driver DECLARA de sí mismo** (3b-2e · E2). No es documentación:
|
|
234
|
+
* el manager lo LEE (el gate de deriva del árbol, E3) y la suite de contrato
|
|
235
|
+
* exige a cada capacidad su caso —el del valor declarado, nunca un `skip`—.
|
|
236
|
+
*
|
|
237
|
+
* Todas son opcionales de declarar (un driver de 2.x que no traiga
|
|
238
|
+
* `capabilities` se trata como todo `false`), pero declarar `true` lo que no
|
|
239
|
+
* se cumple es una promesa sin juez: el contrato lanza al registrarse.
|
|
240
|
+
*/
|
|
241
|
+
export interface AuthorizationDriverCapabilities {
|
|
242
|
+
/**
|
|
243
|
+
* El ÁRBOL de scopes vive como hechos del backend y el backend es el PDP
|
|
244
|
+
* (`openfga` con `hierarchy: 'facts'`). Con `true` el manager exige la
|
|
245
|
+
* mitigación de la deriva (`scopes.outbox` o la firma explícita): el árbol
|
|
246
|
+
* está en dos sitios y un `rollback` del consumidor deja al backend
|
|
247
|
+
* adelantado (cruce 4 · S5).
|
|
248
|
+
*/
|
|
249
|
+
hierarchyFacts: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* `authorize` es UNA sola llamada al backend: no consulta el árbol del
|
|
252
|
+
* consumidor (`resolveChain`) y el catálogo solo a través del memo.
|
|
253
|
+
*/
|
|
254
|
+
singleCheckAuthorize: boolean;
|
|
255
|
+
/**
|
|
256
|
+
* El backend resuelve la MEMBRESÍA por sí mismo. **`false` en los dos
|
|
257
|
+
* drivers del paquete, también en `facts`** (panel 2, cruce 6): `hasRole`,
|
|
258
|
+
* `listRoles`, `listRoleScopes`, `listSubjects` y `listScopes` siguen
|
|
259
|
+
* usando `resolveChain`. Por eso el titular «sin SQL en el camino caliente»
|
|
260
|
+
* está PROHIBIDO a secas: lo cierto es «sin SQL por request en `authorize`».
|
|
261
|
+
*/
|
|
262
|
+
roleInheritanceNative: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Los `list*` enumeran también lo HEREDADO. **`false` siempre en este
|
|
265
|
+
* paquete** (invariante 7): enumerar descendientes sería abierto, y en
|
|
266
|
+
* `openfga` además obligaría a `ListObjects`, que trunca al tope del
|
|
267
|
+
* servidor sin ninguna señal (S16). Los `list*` devuelven hechos DIRECTOS.
|
|
268
|
+
*/
|
|
269
|
+
listObjectsInherited: boolean;
|
|
270
|
+
/** El driver implementa `purgeRole` de verdad (sin él no hay roles locales). */
|
|
271
|
+
purgeRole: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* El driver sabe CONTAR los hechos vigentes de un rol
|
|
274
|
+
* (`countRoleAssignments`, 3b-2j). Es lo que hace verdadero el
|
|
275
|
+
* `stillGranting` de `pruneOrphanRoles`, que se lee justo antes de un
|
|
276
|
+
* borrado destructivo. Con `false` el barrido no lo sabe y lo dice
|
|
277
|
+
* (`undefined`), nunca `false`: «no lo sé» no puede degradar a «no
|
|
278
|
+
* concede».
|
|
279
|
+
*/
|
|
280
|
+
countRoleAssignments: boolean;
|
|
281
|
+
/**
|
|
282
|
+
* Las LECTURAS canonizan la ortografía del scope contra el árbol del
|
|
283
|
+
* consumidor antes de buscar los hechos (3b-2k · K1 · R2 (c)). Con `true`
|
|
284
|
+
* (driver `database`) `authorize` resuelve la cadena y usa `chain[0]`, la
|
|
285
|
+
* identidad canónica (invariante 17), así que un alias del uuid que TU
|
|
286
|
+
* tabla funde con la fila real —una columna `uuid` de PostgreSQL, una
|
|
287
|
+
* collation `*_ci` de MySQL— encuentra los mismos hechos. Con `false`
|
|
288
|
+
* (`openfga` en modo `facts`) la decisión no pasa por el árbol —es la
|
|
289
|
+
* contrapartida de `singleCheckAuthorize`— y el objeto del store se compone
|
|
290
|
+
* con la ortografía del LLAMANTE: un alias responde `false` donde la forma
|
|
291
|
+
* canónica concede. Es fail-CLOSED y no evade ningún deny, pero no es la
|
|
292
|
+
* misma respuesta: **pasa los uuids exactamente como los guarda tu tabla**.
|
|
293
|
+
* La ESCRITURA canoniza en los dos (3b-2h · 🟠 3).
|
|
294
|
+
*/
|
|
295
|
+
canonicalScopeReads: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* El driver sabe ser el **ORIGEN** de una migración: implementa
|
|
298
|
+
* `enumerateFacts` y entrega sus hechos paginados, sin filtrar y con su
|
|
299
|
+
* caducidad (3b-3b). Con `true` es lo que `authz:reconcile --to=<otro>`
|
|
300
|
+
* pasa como `source.facts`. Con `false` el driver no puede ser origen por
|
|
301
|
+
* el puerto y `authz:reconcile` lo DICE (500 `E_AUTHZ_UNSUPPORTED`
|
|
302
|
+
* nombrando `enumerateFacts`), nunca una migración vacía en silencio — que
|
|
303
|
+
* es exactamente el fail-dangerous que se evita: un origen que devuelve
|
|
304
|
+
* cero hechos y un `--prune` detrás borran el destino entero.
|
|
305
|
+
*
|
|
306
|
+
* **`false` en el driver `database` a propósito**: sus hechos son
|
|
307
|
+
* `authz_assignments`/`authz_denies`, el esquema publicado del paquete, y
|
|
308
|
+
* el destino los lee de ahí directamente (`openfga.reconcile`).
|
|
309
|
+
*/
|
|
310
|
+
enumerateFacts: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* El driver puede inscribir sus escrituras en la transacción del consumidor
|
|
313
|
+
* (`{ transaction }` en `grant`/`revoke`/`deny`/`removeDeny`). `true`
|
|
314
|
+
* significa EXACTAMENTE «los dos o ninguno con TU transacción», nunca «no
|
|
315
|
+
* se pierde». `database` = true (con el `trx` de SU conexión; L-3).
|
|
316
|
+
* `openfga` = false, y no puede ser otra cosa: una tupla no entra en una
|
|
317
|
+
* transacción SQL — el store es otro servicio y no hay 2PC. No hay valor
|
|
318
|
+
* intermedio y no se publica ninguno (panel `{trx}`, veredicto (C)).
|
|
319
|
+
*
|
|
320
|
+
* Dos puertas la hacen verdad: con `false`, `{ transaction }` es 500
|
|
321
|
+
* `E_AUTHZ_UNSUPPORTED` por llamada, con cero llamadas al driver; y con
|
|
322
|
+
* `requireTransactionalWrites: true` en el config un driver `false` es 500
|
|
323
|
+
* `E_AUTHZ_CONFIG` al RESOLVER (el despliegue no arranca). **Mismo nombre
|
|
324
|
+
* en `RelationsDriverCapabilities`**: un driver de terceros no aprende dos.
|
|
325
|
+
*
|
|
326
|
+
* `database` la cumple desde L-3: la ESCRITURA (y la lectura «¿ya existe?»
|
|
327
|
+
* que forma parte de ella) va por la transacción ABIERTA del llamante
|
|
328
|
+
* (`assertCallerTransaction` contra la conexión primaria de Lucid); la
|
|
329
|
+
* AUTORIDAD (barrera del freeze, catálogo, `resolveChain`) nunca — por eso
|
|
330
|
+
* exige pool ≥ 2, y un despliegue con pool 1 declara `false` en las opciones
|
|
331
|
+
* del driver. Un choque del UNIQUE dentro de la transacción del llamante es
|
|
332
|
+
* 409 `E_AUTHZ_WRITE_CONFLICT` («envenena tu transacción»); un deadline
|
|
333
|
+
* vencido ahí sigue siendo `indeterminate: true` y el evento lleva
|
|
334
|
+
* `transactional: true`.
|
|
335
|
+
*/
|
|
336
|
+
transactionalWrites: boolean;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Las claves de `AuthorizationDriverCapabilities`, LITERALES (2.4.0-alpha.4):
|
|
340
|
+
* lo que `authorization.capabilities()` normaliza. Una capacidad nueva que no
|
|
341
|
+
* se añada aquí saldría como `undefined` de la lectura publicada, que es
|
|
342
|
+
* justo lo que la API promete no devolver — el caso D-3 de la suite lo ve.
|
|
343
|
+
*/
|
|
344
|
+
export declare const AUTHORIZATION_DRIVER_CAPABILITY_KEYS: ReadonlyArray<keyof AuthorizationDriverCapabilities>;
|
|
345
|
+
/**
|
|
346
|
+
* **La copia CONGELADA y NORMALIZADA que publica `authorization.capabilities()`**
|
|
347
|
+
* (2.4.0-alpha.4; 🟠 3 y 🟡 5 del auditor). Todas las claves conocidas
|
|
348
|
+
* presentes; solo `true` es `true` — la MISMA lectura que aplican las puertas
|
|
349
|
+
* de `{ transaction }` (`=== true`)—, así que `undefined`, un valor a medias
|
|
350
|
+
* de un driver de terceros o un driver de 2.x sin `capabilities` salen como
|
|
351
|
+
* `false`, nunca como `undefined` (repetiría el bug de `stillGranting`: «no lo
|
|
352
|
+
* sé» degradando a «no»). Y es una copia: mutarla no toca el objeto que leen
|
|
353
|
+
* las puertas del manager.
|
|
354
|
+
*/
|
|
355
|
+
export declare function normalizeAuthorizationCapabilities(declared: Partial<AuthorizationDriverCapabilities> | undefined): Readonly<AuthorizationDriverCapabilities>;
|
|
165
356
|
export interface AuthorizationDriver {
|
|
357
|
+
/** Lo que este driver declara poder hacer (3b-2e · E2). Ver `AuthorizationDriverCapabilities`. */
|
|
358
|
+
readonly capabilities?: AuthorizationDriverCapabilities;
|
|
166
359
|
/**
|
|
167
360
|
* ¿El holder tiene el permiso en el scope? Evalúa la cadena completa:
|
|
168
361
|
* sin deny en la cadena Y alguna asignación vigente cuyo rol concede el
|
|
@@ -181,7 +374,7 @@ export interface AuthorizationDriver {
|
|
|
181
374
|
* el catálogo para `scope.type` (422 si no, como `grant`); la asignación
|
|
182
375
|
* puede no existir (no-op).
|
|
183
376
|
*/
|
|
184
|
-
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<void>;
|
|
377
|
+
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
185
378
|
/**
|
|
186
379
|
* ¿El holder tiene el rol (vigente) en el scope o en un ancestro?
|
|
187
380
|
* Misma regla de herencia hacia abajo que `authorize`. Es MEMBRESÍA: el
|
|
@@ -192,12 +385,12 @@ export interface AuthorizationDriver {
|
|
|
192
385
|
* Deny explícito de UN permiso al holder en un scope (y sus descendientes).
|
|
193
386
|
* El permiso debe existir en el catálogo (throw si no). Idempotente.
|
|
194
387
|
*/
|
|
195
|
-
deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
388
|
+
deny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
196
389
|
/**
|
|
197
390
|
* Levanta el deny en ese scope exacto. El permiso debe existir en el
|
|
198
391
|
* catálogo (422 si no, como `deny`); el deny puede no existir (no-op).
|
|
199
392
|
*/
|
|
200
|
-
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
393
|
+
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
201
394
|
/** Holders con asignación VIGENTE del rol en ese scope exacto (sin herencia). */
|
|
202
395
|
listSubjects(role: RoleQuery, scope: ScopeRef): Promise<SubjectRef[]>;
|
|
203
396
|
/**
|
|
@@ -303,6 +496,102 @@ export interface AuthorizationDriver {
|
|
|
303
496
|
* `defineScopedRole` lo dice antes de escribir nada (3E · P4).
|
|
304
497
|
*/
|
|
305
498
|
purgeRole?(roleUuid: string): Promise<void>;
|
|
499
|
+
/**
|
|
500
|
+
* Cuántos hechos VIGENTES tiene cada rol, en TODOS los scopes (3b-2j,
|
|
501
|
+
* decisión del dueño del 2026-08-31 (3)). Un hecho es una asignación del
|
|
502
|
+
* rol a un holder que no ha caducado (`expiresAt` nulo o futuro, con el
|
|
503
|
+
* reloj del driver); el rol se identifica por su uuid y la respuesta va
|
|
504
|
+
* POR POSICIÓN, como `authorizeMany`. Un rol sin hechos —o que el backend
|
|
505
|
+
* no conoce— es `0`; `uuid` mal formado ⇒ 422 `E_AUTHZ_INVALID_IDENTITY`.
|
|
506
|
+
*
|
|
507
|
+
* Es lo que `pruneOrphanRoles` (`authz:catalog:prune-orphans`) necesita
|
|
508
|
+
* para decir si un rol huérfano TODAVÍA CONCEDE, y es una pregunta del
|
|
509
|
+
* PUERTO porque los hechos son del driver: hasta 3b-2j el barrido contaba
|
|
510
|
+
* filas de `authz_assignments` —la tabla del driver `database`— y con
|
|
511
|
+
* `openfga` en modo `facts`, donde viven en el store, decía siempre que
|
|
512
|
+
* no. El campo se lee justo antes de un borrado destructivo y su contrato
|
|
513
|
+
* publicado es «falso ⇒ este rol seguro que no concede», así que ese
|
|
514
|
+
* `false` era fail-dangerous.
|
|
515
|
+
*
|
|
516
|
+
* Es CONSERVADOR a propósito: cuenta hechos, no comprueba si el scope de
|
|
517
|
+
* cada uno sigue resolviendo. Cero ⇒ no concede seguro; más de cero ⇒
|
|
518
|
+
* míralo antes de purgar.
|
|
519
|
+
*
|
|
520
|
+
* OPCIONAL en el puerto (**breaking para un driver de 2.2 que no lo
|
|
521
|
+
* traiga**, y por eso opcional y no obligatorio): sin él
|
|
522
|
+
* `pruneOrphanRoles` deja `assignments` y `stillGranting` en `undefined`
|
|
523
|
+
* —jamás en `false`— y el comando lista esos roles APARTE, como los que sí
|
|
524
|
+
* conceden. Capacidad `countRoleAssignments`.
|
|
525
|
+
*/
|
|
526
|
+
countRoleAssignments?(roleUuids: string[]): Promise<number[]>;
|
|
527
|
+
/**
|
|
528
|
+
* Rehace la **proyección derivada** del catálogo para UN rol (3b-2e · E4).
|
|
529
|
+
* Opcional: solo la implementa un driver que mantenga esa proyección (el
|
|
530
|
+
* `openfga` en modo `facts`, donde lo que un rol concede son tuplas y no
|
|
531
|
+
* el catálogo local). El manager la llama después de `defineScopedRole` y
|
|
532
|
+
* `updateScopedRole` —las dos escrituras de catálogo que cambian los
|
|
533
|
+
* vínculos de un rol fuera de `syncAuthzCatalog`—, porque si no un rol
|
|
534
|
+
* recién definido no concedería NADA y un rol al que se le quita un permiso
|
|
535
|
+
* lo seguiría concediendo (fail-open). En `database` no existe: el catálogo
|
|
536
|
+
* es la fuente y no hay espejo que rehacer.
|
|
537
|
+
*/
|
|
538
|
+
projectCatalogRole?(roleUuid: string): Promise<void>;
|
|
539
|
+
/**
|
|
540
|
+
* La **proyección derivada del catálogo entero** de este driver (3b-2a ·
|
|
541
|
+
* A5), para inyectarla en `syncAuthzCatalog`/`syncCatalogs`. Opcional por
|
|
542
|
+
* el mismo motivo que `projectCatalogRole`: solo la trae un driver que
|
|
543
|
+
* mantenga un espejo del catálogo en su backend (el `openfga` en modo
|
|
544
|
+
* `facts`).
|
|
545
|
+
*
|
|
546
|
+
* Está en el PUERTO porque el camino de recuperación documentado —«un
|
|
547
|
+
* `authz:catalog:sync` reescribe la proyección»— lo ejecuta un comando que
|
|
548
|
+
* solo ve `AuthorizationDriver` (3b-8 · A1): sin esto, el CLI sincronizaba
|
|
549
|
+
* `authz_*` y dejaba el espejo del store SIN TOCAR, o sea que en `facts`
|
|
550
|
+
* un permiso quitado del catálogo seguía concediendo y un rol nuevo no
|
|
551
|
+
* concedía nada.
|
|
552
|
+
*/
|
|
553
|
+
catalogProjection?(): CatalogProjection;
|
|
554
|
+
/**
|
|
555
|
+
* **Reconstruye el estado de ESTE driver desde `authz_*` + el árbol del
|
|
556
|
+
* consumidor** (3b-3a). Es lo que hay detrás de `authz:reconcile --to=<este
|
|
557
|
+
* driver>`: hechos, árbol y proyección del catálogo, idempotente
|
|
558
|
+
* (la segunda pasada escribe cero), reanudable por lotes con cursor y
|
|
559
|
+
* **nunca silenciosa** (el reporte cuenta lo escrito, lo actualizado, lo
|
|
560
|
+
* igual, lo que sobra, lo borrado y lo que NO se migró con su motivo).
|
|
561
|
+
*
|
|
562
|
+
* `dryRun` es el VERIFICADOR: mismo recorrido, cero escrituras. Es
|
|
563
|
+
* **read-only por contrato** (panel 2, cruce 4 · S18) — un `--fix` sería un
|
|
564
|
+
* mecanismo de concesión y queda PROHIBIDO.
|
|
565
|
+
*
|
|
566
|
+
* Opcional en el puerto: un driver que no lo trae dice «no sé
|
|
567
|
+
* reconstruirme» y el manager responde 500 `E_AUTHZ_UNSUPPORTED` nombrando
|
|
568
|
+
* el método, nunca una migración a medias en silencio. El driver
|
|
569
|
+
* `database` NO lo implementa: sus tablas SON el origen, y llenarlas desde
|
|
570
|
+
* un store es la otra dirección (3b-3b).
|
|
571
|
+
*/
|
|
572
|
+
reconcile?(source: ReconcileSource, options: ReconcileOptions): Promise<ReconcileReport>;
|
|
573
|
+
/**
|
|
574
|
+
* **Los hechos de ESTE driver, paginados, para que otro se reconstruya
|
|
575
|
+
* desde ellos** (3b-3b). Es la otra mitad de `reconcile`: `reconcile` es
|
|
576
|
+
* ser el DESTINO de `authz:reconcile`, `enumerateFacts` es ser el ORIGEN.
|
|
577
|
+
*
|
|
578
|
+
* Contrato: como mucho `limit` hechos por página (más ⇒ 500), orden total
|
|
579
|
+
* y estable, cursor opaco que tiene que AVANZAR (repetirlo ⇒ 500, jamás un
|
|
580
|
+
* bucle), y **nada se filtra**: una asignación caducada sale con su
|
|
581
|
+
* `expiresAt` para que el destino la cuente en `skipped` con su motivo. Lo
|
|
582
|
+
* que el origen no sabe expresar como hecho del puerto sale en `skipped`
|
|
583
|
+
* de la página, nunca descartado en silencio.
|
|
584
|
+
*
|
|
585
|
+
* Opcional: capacidad `enumerateFacts`. El driver `database` **no lo
|
|
586
|
+
* trae** a propósito — sus hechos son `authz_assignments`/`authz_denies`,
|
|
587
|
+
* el esquema publicado del paquete, y el destino los lee de ahí (es lo que
|
|
588
|
+
* hace `openfga.reconcile`). Un driver de terceros que quiera migrar
|
|
589
|
+
* DESDE otro sitio sí lo necesita.
|
|
590
|
+
*/
|
|
591
|
+
enumerateFacts?(page: {
|
|
592
|
+
limit: number;
|
|
593
|
+
after?: string;
|
|
594
|
+
}): Promise<ReconcileFactPage>;
|
|
306
595
|
/**
|
|
307
596
|
* Roles DIRECTOS vigentes del holder en cada scope de `chain` (2D · G5),
|
|
308
597
|
* como pares `{ scope, role }`; solo roles que EXISTEN en ese scope (D5 +
|
|
@@ -330,6 +619,317 @@ export interface AuthorizationDriver {
|
|
|
330
619
|
* `authorization.expandExcludedSubtrees(excluded)` (usa tu `descendantsOf`)
|
|
331
620
|
* o resta el subárbol en tu propia consulta (CTE recursiva, `path LIKE`…).
|
|
332
621
|
*/
|
|
622
|
+
/**
|
|
623
|
+
* Lo que el manager le presta al driver para reconciliar: el árbol del
|
|
624
|
+
* consumidor (entero y paginado) y su resolutor. El driver pone lo suyo —qué
|
|
625
|
+
* hechos guarda y cómo—; el paquete no le dice cómo migrar, le da la FUENTE.
|
|
626
|
+
*/
|
|
627
|
+
export interface ReconcileSource {
|
|
628
|
+
enumerateEdges: ScopeEdgesEnumerator;
|
|
629
|
+
resolveChain: ScopeChainResolver;
|
|
630
|
+
/**
|
|
631
|
+
* Los HECHOS del origen, paginados (3b-3b). Solo hace falta en la
|
|
632
|
+
* dirección en la que el origen NO es `authz_*`: `--to=database` los lee
|
|
633
|
+
* del store con este enumerador, mientras que `--to=openfga` lee las
|
|
634
|
+
* tablas del paquete directamente (son su propio esquema publicado, no el
|
|
635
|
+
* secreto de un driver).
|
|
636
|
+
*
|
|
637
|
+
* Es **perezoso a propósito**: el manager solo resuelve el driver de
|
|
638
|
+
* ORIGEN cuando el destino lo pide, así que una migración que no necesita
|
|
639
|
+
* hechos del puerto no construye nada. Sin origen que lo implemente, la
|
|
640
|
+
* primera llamada es 500 `E_AUTHZ_UNSUPPORTED` nombrando `enumerateFacts`.
|
|
641
|
+
*/
|
|
642
|
+
facts?: ReconcileFactsEnumerator;
|
|
643
|
+
/**
|
|
644
|
+
* **Quién es la FUENTE DE VERDAD de los hechos en esta pasada** (3b-5, los
|
|
645
|
+
* dos 🔴 del auditor final). Lo decide el MANAGER, que es el único que sabe
|
|
646
|
+
* qué driver está sirviendo (`config.default`) y qué declara cada uno
|
|
647
|
+
* (`capabilities.hierarchyFacts`), y el destino lo OBEDECE.
|
|
648
|
+
*
|
|
649
|
+
* Sin esto, `--to=openfga` leía siempre `authz_assignments`/`authz_denies`,
|
|
650
|
+
* y en un despliegue `hierarchy: 'facts'` esas tablas **no son** la fuente
|
|
651
|
+
* de verdad de los hechos —lo son las tuplas del store—: la pasada
|
|
652
|
+
* reescribía lo revocado después del cutover, `--prune` borraba los denies
|
|
653
|
+
* vivos y el barrido de visibilidad del invariante 18 no se aplicaba nunca
|
|
654
|
+
* (`forbidden` salía vacío porque `wanted.facts` salía vacío).
|
|
655
|
+
*
|
|
656
|
+
* - `authzTables: true` ⇒ los hechos son las tablas del paquete y el
|
|
657
|
+
* destino las lee él mismo (es la MIGRACIÓN `database` → `openfga`);
|
|
658
|
+
* - `authzTables: false` ⇒ los hechos llegan por el PUERTO (`facts`,
|
|
659
|
+
* `enumerateFacts`) del origen `name`, que puede ser **el propio
|
|
660
|
+
* destino** cuando el destino es el driver ACTIVO y sus hechos son
|
|
661
|
+
* suyos (la pasada de MANTENIMIENTO: rehace lo derivado —marcador,
|
|
662
|
+
* catálogo, árbol y visibilidad— y no inventa hechos; con `--prune` borra
|
|
663
|
+
* los que ya no respaldan nada: scopes que ya no resuelven, caducados,
|
|
664
|
+
* roles/permisos retirados y los que sobran).
|
|
665
|
+
*
|
|
666
|
+
* Ausente = `{ name: 'authz_*', authzTables: true }`: el comportamiento de
|
|
667
|
+
* 3b-3a, que es el que vale cuando el origen es el esquema publicado.
|
|
668
|
+
*/
|
|
669
|
+
factsOrigin?: ReconcileFactsOrigin;
|
|
670
|
+
/**
|
|
671
|
+
* **El techo de profundidad con el que se juzga el árbol del origen**
|
|
672
|
+
* (2.4.0-alpha.5 · R0): `scopes.maxDepth` del config si el consumidor lo
|
|
673
|
+
* declaró; sin él, el destino usa su propia cota (`openfga`:
|
|
674
|
+
* `FACTS_MAX_RESOLVE_DEPTH`, 22 saltos). Las cadenas que lo superan salen
|
|
675
|
+
* en `drift.tooDeep`. Un solo mando: quien subió
|
|
676
|
+
* `OPENFGA_RESOLVE_NODE_LIMIT` en su servidor sube esto y el verificador
|
|
677
|
+
* lo sigue.
|
|
678
|
+
*/
|
|
679
|
+
maxDepth?: number;
|
|
680
|
+
/**
|
|
681
|
+
* **Los tipos de objeto que OTRO subsistema gobierna en el mismo backend**
|
|
682
|
+
* (2.4.0-alpha.9 · B · S4): los declarados en `relations.config`, que pone
|
|
683
|
+
* el manager. Con `openfga` los dos subsistemas comparten store, así que la
|
|
684
|
+
* pasada de hechos ve tuplas que no son suyas; esto es **solo para el
|
|
685
|
+
* REPORTE** —parte `foreign` en `relations` (declaradas) y `unknown` (no
|
|
686
|
+
* las reclama nadie)— y **jamás decide un borrado**: eso lo decide la lista
|
|
687
|
+
* blanca congelada del driver de destino, que no lee nada en runtime. Un
|
|
688
|
+
* proceso sin `relations.config` —el caso de un cutover— tiene que
|
|
689
|
+
* respetar lo ajeno igual, y lo hace.
|
|
690
|
+
*/
|
|
691
|
+
relationTypes?: readonly string[];
|
|
692
|
+
}
|
|
693
|
+
/** Ver `ReconcileSource.factsOrigin` (3b-5). */
|
|
694
|
+
export interface ReconcileFactsOrigin {
|
|
695
|
+
/** Cómo se NOMBRA el origen en el reporte (clave de `drivers`, o `authz_*`). */
|
|
696
|
+
name: string;
|
|
697
|
+
/** `true` ⇒ los hechos son `authz_assignments`/`authz_denies` y los lee el destino. */
|
|
698
|
+
authzTables: boolean;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Un hecho del ORIGEN en el vocabulario del PUERTO, no en el del backend
|
|
702
|
+
* (3b-3b). Es lo que un driver entrega cuando le toca ser el origen de una
|
|
703
|
+
* migración: el destino no sabe si detrás hay tuplas, filas o un fichero.
|
|
704
|
+
*
|
|
705
|
+
* La identidad del rol es el **uuid** (3D · M1), nunca el slug: dos owners
|
|
706
|
+
* definen `lead@unit` y el slug no identifica nada. La del permiso es el
|
|
707
|
+
* **slug**, que es lo que el catálogo local sabe traducir a uuid.
|
|
708
|
+
*/
|
|
709
|
+
export interface ReconcileFact {
|
|
710
|
+
kind: 'assignment' | 'deny';
|
|
711
|
+
holder: SubjectRef;
|
|
712
|
+
/** El scope tal como lo guarda el ORIGEN; el destino lo canoniza con SU árbol. */
|
|
713
|
+
scope: ScopeRef;
|
|
714
|
+
/** `assignment`: uuid del rol. */
|
|
715
|
+
roleUuid?: string;
|
|
716
|
+
/** `deny`: slug del permiso. */
|
|
717
|
+
permission?: string;
|
|
718
|
+
/**
|
|
719
|
+
* `assignment`: la caducidad tal como está guardada, **sin filtrar**. Una
|
|
720
|
+
* caducada tiene que LLEGAR para poder contarse en `skipped` con su motivo;
|
|
721
|
+
* un origen que la filtre por su cuenta la haría desaparecer en silencio,
|
|
722
|
+
* que es justo lo que la migración no puede hacer.
|
|
723
|
+
*/
|
|
724
|
+
expiresAt?: Date | null;
|
|
725
|
+
/** Cómo lo nombra el origen (para `details`): un motivo sin la fila no se arregla. */
|
|
726
|
+
detail: string;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Una página de hechos del origen. `cursor` es opaco y tiene que AVANZAR
|
|
730
|
+
* (repetirlo ⇒ 500, nunca un bucle); `skipped` es lo que el ORIGEN no supo
|
|
731
|
+
* expresar como hecho del puerto (basura de otra versión, un holder type que
|
|
732
|
+
* el config no declara…) y que el destino suma a su reporte.
|
|
733
|
+
*/
|
|
734
|
+
export interface ReconcileFactPage {
|
|
735
|
+
facts: ReconcileFact[];
|
|
736
|
+
skipped?: ReconcileSkip[];
|
|
737
|
+
cursor?: string;
|
|
738
|
+
}
|
|
739
|
+
export type ReconcileFactsEnumerator = (page: {
|
|
740
|
+
limit: number;
|
|
741
|
+
after?: string;
|
|
742
|
+
}) => Promise<ReconcileFactPage>;
|
|
743
|
+
export interface ReconcileOptions {
|
|
744
|
+
/** Mismo recorrido, CERO escrituras. Es el verificador (read-only por contrato). */
|
|
745
|
+
dryRun?: boolean;
|
|
746
|
+
/**
|
|
747
|
+
* Borra del destino los HECHOS que el origen no respalda: los de un scope
|
|
748
|
+
* que ya no resuelve (3b-0b · AA4, «resurrección») y los que sobran (un
|
|
749
|
+
* store escrito por una versión anterior). Sin él se REPORTAN y no se
|
|
750
|
+
* borran. Lo derivado —marcador de raíz, proyección del catálogo y árbol—
|
|
751
|
+
* se rehace siempre: es un espejo de datos locales que nadie más escribe.
|
|
752
|
+
*/
|
|
753
|
+
prune?: boolean;
|
|
754
|
+
/** La salida humana de `E_AUTHZ_MASS_RECONCILE_REFUSED`. */
|
|
755
|
+
allowMassDelete?: boolean;
|
|
756
|
+
/**
|
|
757
|
+
* La salida humana de `E_AUTHZ_STALE_SOURCE_REFUSED` (2.4.0-alpha.10): deja
|
|
758
|
+
* correr `--to=<driver ACTIVO> --from=<otro>`, la dirección que re-escribe en
|
|
759
|
+
* el driver que sirve hechos que el origen ya no refleja (resucita las
|
|
760
|
+
* revocaciones hechas desde el cutover). Lo lee el MANAGER; el driver lo
|
|
761
|
+
* ignora.
|
|
762
|
+
*/
|
|
763
|
+
allowStaleSource?: boolean;
|
|
764
|
+
/** Filas por lote en las lecturas del origen y por `Write` en el destino (default 100). */
|
|
765
|
+
batchSize?: number;
|
|
766
|
+
/**
|
|
767
|
+
* **La cota del volcado del destino** (3b-3b · B5). Reconciliar exige
|
|
768
|
+
* comparar contra el estado ENTERO del destino, y ese volcado entra en
|
|
769
|
+
* memoria: el ORIGEN se lee por lotes con cursor, el destino no. En vez de
|
|
770
|
+
* dejarlo como una sorpresa (un OOM en producción), se declara: pasar de
|
|
771
|
+
* `maxTuples` es 500 `E_AUTHZ_RECONCILE_TOO_LARGE` **antes de escribir
|
|
772
|
+
* nada**, nombrando la cota y cómo subirla. Default
|
|
773
|
+
* `DEFAULT_RECONCILE_MAX_TUPLES`.
|
|
774
|
+
*/
|
|
775
|
+
maxTuples?: number;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Cuántas tuplas/filas del destino caben en una pasada de `authz:reconcile`
|
|
779
|
+
* (3b-3b · B5). No es una garantía de memoria: es la cota DECLARADA por
|
|
780
|
+
* encima de la cual la pasada se niega en vez de intentarlo.
|
|
781
|
+
*/
|
|
782
|
+
export declare const DEFAULT_RECONCILE_MAX_TUPLES = 1000000;
|
|
783
|
+
/**
|
|
784
|
+
* Algo que la pasada NO migró (una fila del origen) o NO tocó (una tupla del
|
|
785
|
+
* destino), con su motivo. Nunca un contador a secas: un motivo sin la fila
|
|
786
|
+
* no se puede arreglar.
|
|
787
|
+
*/
|
|
788
|
+
export interface ReconcileSkip {
|
|
789
|
+
kind: 'assignment' | 'deny' | 'edge' | 'tuple';
|
|
790
|
+
reason: string;
|
|
791
|
+
detail: string;
|
|
792
|
+
}
|
|
793
|
+
/** Los cinco números de una fase (o del total). */
|
|
794
|
+
export interface ReconcileCounts {
|
|
795
|
+
/** Tuplas nuevas en el destino. */
|
|
796
|
+
written: number;
|
|
797
|
+
/** Tuplas que estaban con OTRA caducidad y se han rehecho (delete + write). */
|
|
798
|
+
updated: number;
|
|
799
|
+
/** Tuplas que ya estaban exactamente igual. */
|
|
800
|
+
unchanged: number;
|
|
801
|
+
/** Tuplas del destino que el origen NO respalda. */
|
|
802
|
+
extra: number;
|
|
803
|
+
/** De las anteriores, las que la pasada borra (las que sobran de lo derivado, y con `prune` también los hechos). */
|
|
804
|
+
deleted: number;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Lo que movió una pasada de `authz:reconcile`. Los contadores describen el
|
|
808
|
+
* PLAN: con `dryRun` son exactamente los mismos números y no se escribe nada
|
|
809
|
+
* (lo dice `dryRun: true`), que es lo que hace del verificador un simulacro
|
|
810
|
+
* fiel y no una segunda implementación.
|
|
811
|
+
*/
|
|
812
|
+
export interface ReconcileReport extends ReconcileCounts {
|
|
813
|
+
/** El driver de destino (`--to`). */
|
|
814
|
+
to: string;
|
|
815
|
+
/**
|
|
816
|
+
* **De dónde salieron los HECHOS de esta pasada** (3b-5): el nombre del
|
|
817
|
+
* driver ORIGEN, o `authz_*` si fueron las tablas del paquete. No es
|
|
818
|
+
* decoración: es la diferencia entre una migración y una pasada de
|
|
819
|
+
* mantenimiento contra el driver activo, y el comando la imprime — una
|
|
820
|
+
* pasada que lee los hechos del sitio equivocado no puede ser silenciosa.
|
|
821
|
+
*/
|
|
822
|
+
factsFrom?: string;
|
|
823
|
+
/**
|
|
824
|
+
* **La garantía del freeze, publicada en vez de supuesta** (3b-7, juez C4).
|
|
825
|
+
* Solo en la pasada que ESCRIBE (el `--dry-run` no congela). `lapsed: true`
|
|
826
|
+
* significa que el lease se perdió a mitad —una pausa más larga que el
|
|
827
|
+
* lease, la base caída, otro dueño— y hubo una ventana en la que otros
|
|
828
|
+
* procesos pudieron escribir: la pasada NO se certifica y el comando sale
|
|
829
|
+
* distinto de cero. `leaseMs: null` = ventana que no caduca (el freeze de
|
|
830
|
+
* OPERADOR sin `--lease-ms` dentro del que corrió la pasada). Lo pone el
|
|
831
|
+
* MANAGER: el driver no sabe de ventanas.
|
|
832
|
+
*/
|
|
833
|
+
frozen?: {
|
|
834
|
+
durable: boolean;
|
|
835
|
+
lapsed: boolean;
|
|
836
|
+
leaseMs: number | null;
|
|
837
|
+
fence: number;
|
|
838
|
+
/**
|
|
839
|
+
* Solo si la pasada corrió dentro de la ventana del OPERADOR (2.4.0-alpha.10):
|
|
840
|
+
* cuándo caduca esa ventana (ms de pared, de la fila), o `null` si no caduca.
|
|
841
|
+
* Entonces `leaseMs` es lo que le quedaba al adoptarla, y es `null` solo si
|
|
842
|
+
* `untilMs` es `null`.
|
|
843
|
+
*/
|
|
844
|
+
untilMs?: number | null;
|
|
845
|
+
/** En QUÉ ventana corrió (alpha.10, S2): la del operador, una propia, o la que ya sostenía el manager. */
|
|
846
|
+
window?: 'operator' | 'own' | 'enclosing';
|
|
847
|
+
};
|
|
848
|
+
/**
|
|
849
|
+
* **La dirección RANCIA** (2.4.0-alpha.10): esta pasada tenía como destino
|
|
850
|
+
* el driver ACTIVO (`config.default`) y como origen OTRO driver — la que
|
|
851
|
+
* resucita las revocaciones hechas desde el cutover. Sin `allowStaleSource`
|
|
852
|
+
* solo llega aquí un `--dry-run` (la pasada que escribe es 500
|
|
853
|
+
* `E_AUTHZ_STALE_SOURCE_REFUSED`), y es deriva: exit ≠ 0. `allowed: true` =
|
|
854
|
+
* el operador la levantó a sabiendas. Lo pone el MANAGER.
|
|
855
|
+
*/
|
|
856
|
+
staleSource?: {
|
|
857
|
+
to: string;
|
|
858
|
+
from: string;
|
|
859
|
+
allowed: boolean;
|
|
860
|
+
};
|
|
861
|
+
dryRun: boolean;
|
|
862
|
+
prune: boolean;
|
|
863
|
+
/** Los mismos números por fase: qué es catálogo, qué es árbol y qué son hechos. */
|
|
864
|
+
phases: Record<'root' | 'catalog' | 'tree' | 'facts', ReconcileCounts>;
|
|
865
|
+
/**
|
|
866
|
+
* Motivo → cuántas cosas se quedaron fuera: filas del origen que no se
|
|
867
|
+
* migraron y tuplas del destino que esta pasada no tocó (`extra-fact`, las
|
|
868
|
+
* que solo se van con `--prune`).
|
|
869
|
+
*/
|
|
870
|
+
skipped: Record<string, number>;
|
|
871
|
+
/** Y cuáles (acotado por `maxSkipDetails`): un contador no permite arreglar nada. */
|
|
872
|
+
details: ReconcileSkip[];
|
|
873
|
+
/** Ciclos del árbol del ORIGEN: sus aristas NO se escriben (FGA los evalúa y son fail-open). */
|
|
874
|
+
cycles: string[][];
|
|
875
|
+
drift: {
|
|
876
|
+
/** Faltaba el marcador de raíz: sin él el store entero DENIEGA (3b-2i). */
|
|
877
|
+
rootMarker: boolean;
|
|
878
|
+
/** Scopes con más de un padre en el destino (3b-2h · 🟠 4): cruce de tenants. */
|
|
879
|
+
multiParent: string[];
|
|
880
|
+
/**
|
|
881
|
+
* Aristas `scope#binding` que el destino tenía mal (invariante 18): la
|
|
882
|
+
* escritura de visibilidad que `scopes.moved`/`projectCatalogRole`
|
|
883
|
+
* pudieron perder si el relay no pasó.
|
|
884
|
+
*/
|
|
885
|
+
roleVisibility: number;
|
|
886
|
+
/** Cambios del árbol encolados y sin relevar: la VENTANA del relay, medida. */
|
|
887
|
+
pendingRelay: number;
|
|
888
|
+
/** Entradas APARCADAS de la outbox: divergencia permanente, no una ventana. */
|
|
889
|
+
deadRelay: number;
|
|
890
|
+
/**
|
|
891
|
+
* Scopes del ORIGEN cuya cadena supera el techo de profundidad
|
|
892
|
+
* (2.4.0-alpha.5 · R0): `ReconcileSource.maxDepth`, o la cota del destino
|
|
893
|
+
* (`FACTS_MAX_RESOLVE_DEPTH` en `openfga`). Legales en `database`, un 503
|
|
894
|
+
* en `facts` para todo el que pregunte por ellos: el verificador los
|
|
895
|
+
* cuenta como deriva (exit ≠ 0). Opcional: un driver de terceros que no
|
|
896
|
+
* lo calcula no lo reporta, y `reconcileLines` lo trata como vacío.
|
|
897
|
+
*/
|
|
898
|
+
tooDeep?: string[];
|
|
899
|
+
/** El techo con el que se calculó `tooDeep`. */
|
|
900
|
+
tooDeepLimit?: number;
|
|
901
|
+
};
|
|
902
|
+
/** La pasada tiene la firma de un origen ciego (ver `E_AUTHZ_MASS_RECONCILE_REFUSED`). */
|
|
903
|
+
massDelete: boolean;
|
|
904
|
+
/**
|
|
905
|
+
* **Lo que esta pasada vio en el destino y NO gobierna** (2.4.0-alpha.9 · B):
|
|
906
|
+
* el store es COMPARTIDO con otro subsistema (con `openfga`, las tuplas de
|
|
907
|
+
* `relations/`) y sus tipos de objeto no son los de este reconcile. Ni se
|
|
908
|
+
* borran —tampoco con `--prune`—, ni cuentan como deriva, ni entran en
|
|
909
|
+
* `phases`: se cuentan aquí, con el desglose por TIPO que es lo único que
|
|
910
|
+
* permite reconocer un tipo que ya no se declara.
|
|
911
|
+
*
|
|
912
|
+
* Va FUERA de `skipped` a propósito: ahí dentro `clean` era `false` para
|
|
913
|
+
* siempre en cualquier despliegue con relaciones (el corolario del 🔴 3 del
|
|
914
|
+
* quinto informe de COGNITIV), y fuera de `details`, cuyo tope de 200
|
|
915
|
+
* entradas expulsaría los `extra-fact` REALES, que son los que conceden.
|
|
916
|
+
*
|
|
917
|
+
* `relations`/`unknown` solo aparecen si el manager pasó los tipos
|
|
918
|
+
* declarados (`ReconcileSource.relationTypes`); sin ellos todo es
|
|
919
|
+
* `unknown`, que es la verdad. Opcional: un driver de terceros que no lo
|
|
920
|
+
* calcule simplemente no lo trae y `reconcileLines` lo trata como cero.
|
|
921
|
+
*/
|
|
922
|
+
foreign?: {
|
|
923
|
+
/** Cuántas tuplas del destino son de otro subsistema. */
|
|
924
|
+
tuples: number;
|
|
925
|
+
/** Cuántas por TIPO de objeto, en orden estable. */
|
|
926
|
+
types: Record<string, number>;
|
|
927
|
+
/** De ésas, las de un tipo declarado en `relations.config` (más `group`). */
|
|
928
|
+
relations?: number;
|
|
929
|
+
/** Las que no reclama nadie: otro modelo anterior, o un tipo ya retirado. */
|
|
930
|
+
unknown?: number;
|
|
931
|
+
};
|
|
932
|
+
}
|
|
333
933
|
export interface ExcludedSubtree {
|
|
334
934
|
scope: ScopeRef;
|
|
335
935
|
/** Siempre `true`: recuerda que lo excluido es el subárbol entero. */
|
|
@@ -395,35 +995,529 @@ export type AuthorizationDriverFactory = () => AuthorizationDriver | Promise<Aut
|
|
|
395
995
|
*/
|
|
396
996
|
export type ScopeChainResolver = (scope: ScopeRef) => Promise<ScopeRef[] | null>;
|
|
397
997
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
998
|
+
* Una arista del árbol del consumidor: «`child` cuelga de `parent`» (3b-3a).
|
|
999
|
+
* `parent` puede ser `APP_SCOPE`; `child` nunca es la raíz.
|
|
1000
|
+
*/
|
|
1001
|
+
export interface ScopeEdge {
|
|
1002
|
+
child: ScopeRef;
|
|
1003
|
+
parent: ScopeRef;
|
|
1004
|
+
}
|
|
1005
|
+
/** Una página de `scopes.enumerateEdges`. Sin `cursor` = no queda nada más. */
|
|
1006
|
+
export interface ScopeEdgePage {
|
|
1007
|
+
edges: ScopeEdge[];
|
|
1008
|
+
/**
|
|
1009
|
+
* Continuación OPACA para la siguiente llamada (`after`). Ausente o
|
|
1010
|
+
* `undefined` significa «se acabó»: devolver siempre un cursor es un bucle
|
|
1011
|
+
* infinito, y el llamante lo denuncia (500) si el cursor no avanza.
|
|
1012
|
+
*/
|
|
1013
|
+
cursor?: string;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* **El árbol ENTERO, paginado** (3b-3a). Es la otra mitad de
|
|
1017
|
+
* `resolveChain`: aquel responde «¿de qué cuelga ESTE scope?» y este
|
|
1018
|
+
* «¿cuáles son todas las aristas?», que es lo que hace falta para
|
|
1019
|
+
* reconstruir el árbol de un backend que lo guarda como hechos propios
|
|
1020
|
+
* (`authz:reconcile --to=openfga`) y para ver las que sobran (las que el
|
|
1021
|
+
* consumidor ya no respalda).
|
|
1022
|
+
*
|
|
1023
|
+
* Contrato:
|
|
1024
|
+
* - devuelve **como mucho `limit`** aristas por página (más ⇒ 500: el
|
|
1025
|
+
* llamante no puede paginar lo que no cabe en su lote);
|
|
1026
|
+
* - el orden tiene que ser TOTAL y ESTABLE entre llamadas (la clave
|
|
1027
|
+
* primaria vale): si no, una pasada reanudada se salta nodos;
|
|
1028
|
+
* - `cursor` es opaco para el paquete y vuelve tal cual en `after`; que no
|
|
1029
|
+
* avance es 500, nunca un bucle;
|
|
1030
|
+
* - una arista cuyo padre no existe en la tabla NO se emite (es un nodo que
|
|
1031
|
+
* `resolveChain` tampoco resuelve): el destino la ve como sobrante y
|
|
1032
|
+
* `authz:reconcile` la cuenta y la reporta.
|
|
1033
|
+
*
|
|
1034
|
+
* Sin él, `authz:reconcile --to=openfga` no puede migrar el árbol y lo dice
|
|
1035
|
+
* (500 `E_AUTHZ_CONFIG`): NO se inventa un árbol plano.
|
|
1036
|
+
* `sqlScopeEdges(...)` lo implementa sobre UNA tabla con columna padre — y
|
|
1037
|
+
* **concatenar varios `sqlScopeEdges` NO cubre un árbol repartido en varias
|
|
1038
|
+
* tablas** (2.4.0-alpha.4): cada uno hace su `LEFT JOIN` contra su propia
|
|
1039
|
+
* tabla y descarta EN SILENCIO la fila cuyo padre vive en otra, con lo que
|
|
1040
|
+
* `--prune` borraría esas aristas del store y el subárbol entero denegaría.
|
|
1041
|
+
* Ahí se escribe un `enumerateEdges` propio (UNION ALL, cursor por uuid,
|
|
1042
|
+
* `parent IS NULL` = `app`); ver el README de `authz:reconcile`.
|
|
1043
|
+
*/
|
|
1044
|
+
export type ScopeEdgesEnumerator = (options: {
|
|
1045
|
+
limit: number;
|
|
1046
|
+
after?: string;
|
|
1047
|
+
}) => Promise<ScopeEdgePage>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Un cambio del ÁRBOL, tal como lo encola la outbox (3b-2d). Es exactamente
|
|
1050
|
+
* lo que el consumidor notifica por `manager.scopes.*`, con la identidad ya
|
|
1051
|
+
* CANÓNICA (invariante 17): se resuelve al encolar, mientras la fila del
|
|
1052
|
+
* consumidor todavía existe, no al relevarla.
|
|
1053
|
+
*/
|
|
1054
|
+
export type ScopeTreeChange = {
|
|
1055
|
+
op: 'attached';
|
|
1056
|
+
child: ScopeRef;
|
|
1057
|
+
parent: ScopeRef;
|
|
1058
|
+
} | {
|
|
1059
|
+
op: 'moved';
|
|
1060
|
+
child: ScopeRef;
|
|
1061
|
+
parent: ScopeRef;
|
|
1062
|
+
} | {
|
|
1063
|
+
op: 'detached';
|
|
1064
|
+
child: ScopeRef;
|
|
1065
|
+
};
|
|
1066
|
+
/** Un cambio pendiente en la outbox, con la identidad de su registro. */
|
|
1067
|
+
export interface PendingScopeTreeChange {
|
|
1068
|
+
/** Identificador estable del registro; el relay lo devuelve al marcarlo. */
|
|
1069
|
+
id: string | number;
|
|
1070
|
+
change: ScopeTreeChange;
|
|
1071
|
+
/** Intentos fallidos previos, si la outbox los lleva (el reporte los muestra). */
|
|
1072
|
+
attempts?: number;
|
|
1073
|
+
/** La última causa de fallo, si la outbox la guarda (`dead()` la enseña). */
|
|
1074
|
+
lastError?: string;
|
|
1075
|
+
/**
|
|
1076
|
+
* Quién ordenó el cambio, si el call-site lo declaró y la outbox lo
|
|
1077
|
+
* guarda. El relay lo pone en el `AuthzWriteEvent` del `scope_purged` que
|
|
1078
|
+
* dispara un `detached`: la auditoría no debe perder al autor por pasar
|
|
1079
|
+
* por una cola.
|
|
1080
|
+
*/
|
|
1081
|
+
actor?: SubjectRef;
|
|
1082
|
+
/**
|
|
1083
|
+
* **Cuándo se encoló** (2.4.0-alpha.8; panel relay 2, cuestión 5). Es el
|
|
1084
|
+
* sello de `created_at` con el reloj de QUIEN ENCOLÓ (`sqlScopeOutbox` lo
|
|
1085
|
+
* devuelve tal cual: la columna ya existía). Con él el relay publica la
|
|
1086
|
+
* EDAD de la cabeza global de la cola (`ScopeRelayReport.queue`), que es la
|
|
1087
|
+
* alarma que faltaba cuando el store está caído (un 503 no consume intento,
|
|
1088
|
+
* así que `attempts` y `dead` se quedan a cero durante horas). OPCIONAL: una
|
|
1089
|
+
* outbox de terceros que no lo traiga deja el campo AUSENTE —jamás `0` ni
|
|
1090
|
+
* «ahora»— y el reporte dice `null` (patrón `stillGranting`/
|
|
1091
|
+
* `countRoleAssignments`: «no lo sé» no degrada a un número). Es un sello,
|
|
1092
|
+
* no una decisión: nada del paquete decide con él (README, «audit stamps are
|
|
1093
|
+
* not decisions»).
|
|
1094
|
+
*/
|
|
1095
|
+
createdAt?: Date;
|
|
1096
|
+
}
|
|
1097
|
+
/** Lo que el relay aplicó (o aplicaría), pieza a pieza. */
|
|
1098
|
+
export interface RelayedScopeChange {
|
|
1099
|
+
id: string | number;
|
|
1100
|
+
change: ScopeTreeChange;
|
|
1101
|
+
attempts?: number;
|
|
1102
|
+
/** La causa, en lo que FALLÓ, se APARCÓ o se APLAZÓ (nunca en lo aplicado). */
|
|
1103
|
+
error?: string;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Un fallo de la pasada (2.4.0-alpha.5 · R2a). `transient` dice si la causa
|
|
1107
|
+
* fue el BACKEND (503: no respondió, venció el deadline, está congelado) y
|
|
1108
|
+
* no la entrada: entonces `markFailed` se llamó con `{ transient: true }` y
|
|
1109
|
+
* `sqlScopeOutbox` NO le contó el intento — una caída del store es común a
|
|
1110
|
+
* todas las entradas y no dice nada de ninguna; `maxAttempts` existe para
|
|
1111
|
+
* converger sobre lo IRREPARABLE (422, 500, el padre borrado). Un
|
|
1112
|
+
* `ScopeOutbox` de terceros que ignore el tercer parámetro cuenta igual.
|
|
1113
|
+
*/
|
|
1114
|
+
export interface ScopeRelayFailure {
|
|
1115
|
+
id: string | number;
|
|
1116
|
+
change: ScopeTreeChange;
|
|
1117
|
+
error: string;
|
|
1118
|
+
transient: boolean;
|
|
1119
|
+
}
|
|
1120
|
+
/** Qué fue de una entrada en la pasada (2.4.0-alpha.5 · R1, `ScopeRelayReport.entries`). */
|
|
1121
|
+
export type ScopeRelayEntryStatus = 'applied' | 'deferred' | 'failed' | 'dead';
|
|
1122
|
+
/**
|
|
1123
|
+
* **El resultado por ENTRADA** (2.4.0-alpha.5 · R1; COGNITIV comparaba
|
|
1124
|
+
* claves de `applied[].change` a mano, y dos cambios iguales encolados dos
|
|
1125
|
+
* veces no se distinguen así). Una fila por cada entrada que la pasada
|
|
1126
|
+
* miró, en orden de proceso, con las aparcadas al final:
|
|
1127
|
+
*
|
|
1128
|
+
* - `applied`: aterrizó en el driver y está marcada;
|
|
1129
|
+
* - `deferred`: NO se intentó, y `reason` dice por qué, en una de dos formas
|
|
1130
|
+
* —`depends-on:<id>`: una entrada ANTERIOR sin aplicar comparte clave de
|
|
1131
|
+
* scope con ella (falló en esta pasada, quedó aplazada, o está pendiente y
|
|
1132
|
+
* no entraba en `only`); `parked:<id>`: la entrada aparcada `<id>` nombra
|
|
1133
|
+
* uno de sus scopes y esta concede o recuelga (`attached`/`moved`)—;
|
|
1134
|
+
* - `failed`: se intentó y el driver lanzó; `reason` es el mensaje;
|
|
1135
|
+
* - `dead`: aparcada por la outbox; `reason` es su última causa.
|
|
1136
|
+
*
|
|
1137
|
+
* Con `only`, las entradas son las pedidas (y las que de ellas estén
|
|
1138
|
+
* aparcadas); las que no estaban ni pendientes ni aparcadas salen en
|
|
1139
|
+
* `missing`, nunca desaparecen en silencio.
|
|
1140
|
+
*/
|
|
1141
|
+
export interface ScopeRelayEntry {
|
|
1142
|
+
id: string | number;
|
|
1143
|
+
change: ScopeTreeChange;
|
|
1144
|
+
status: ScopeRelayEntryStatus;
|
|
1145
|
+
reason?: string;
|
|
1146
|
+
attempts?: number;
|
|
1147
|
+
/**
|
|
1148
|
+
* Solo en `deferred` (2.4.0-alpha.8): el ORIGEN —transitivo, la causa
|
|
1149
|
+
* raíz— del bloqueo que la aplazó. `parked` = una entrada APARCADA (la
|
|
1150
|
+
* exención de la purga, invariante 11, mira SOLO este valor); `failed` =
|
|
1151
|
+
* un fallo real de esta pasada; `pending` = bajo `only`, una anterior
|
|
1152
|
+
* pendiente de verdad que no se pidió (se aplicaría en la pasada
|
|
1153
|
+
* completa). Hasta alpha.8 esto solo estaba en la prosa de
|
|
1154
|
+
* `deferred[].error`, y `depends-on:` no distinguía `pending` de
|
|
1155
|
+
* `failed`. Se deriva del bloqueo, nunca del prefijo de `reason`.
|
|
1156
|
+
*/
|
|
1157
|
+
origin?: ScopeRelayBlockOrigin;
|
|
1158
|
+
}
|
|
1159
|
+
/** El origen de un bloqueo por clave (2.4.0-alpha.8). Ver `ScopeRelayEntry.origin`. */
|
|
1160
|
+
export type ScopeRelayBlockOrigin = 'parked' | 'pending' | 'failed';
|
|
1161
|
+
/**
|
|
1162
|
+
* **La edad de la cola** (2.4.0-alpha.8; panel relay 2, cuestión 5; el B2 del
|
|
1163
|
+
* auditor de COGNITIV: diez pasadas con el store caído decían `attempts = 0`,
|
|
1164
|
+
* `dead = 0` y nada de tiempo). Es la CABEZA GLOBAL de la cola —el
|
|
1165
|
+
* `pending(1)` que la pasada ya hacía al final para `remaining`, cero
|
|
1166
|
+
* consultas nuevas— y su edad medida con el reloj INYECTABLE del manager
|
|
1167
|
+
* (`config.clock`, jamás `Date.now()` en `src/`): `Math.max(0, ahora −
|
|
1168
|
+
* createdAt)`, nunca negativa (el `created_at` lo selló el reloj de OTRA
|
|
1169
|
+
* réplica, con su skew). **Siempre global**, también bajo `only` (como
|
|
1170
|
+
* `remaining` y `dead`): no filtra identidad de nadie, como mucho dice «hay
|
|
1171
|
+
* backlog». `null` en los dos campos si no hay nada PENDIENTE (cola vacía, o
|
|
1172
|
+
* solo aparcadas: lo aparcado tiene su propia alarma, `deadCount`) o si la
|
|
1173
|
+
* outbox no trae `createdAt`. El comando la imprime y `--max-age=<ms>` sale
|
|
1174
|
+
* ≠ 0 SOLO con umbral y por encima de él.
|
|
1175
|
+
*/
|
|
1176
|
+
export interface ScopeRelayQueue {
|
|
1177
|
+
/** El `createdAt` de la pendiente más antigua de TODA la cola, o `null`. */
|
|
1178
|
+
oldestPendingAt: Date | null;
|
|
1179
|
+
/** `ahora − oldestPendingAt` con el reloj del manager, ≥ 0; `null` si no hay cabeza con fecha. */
|
|
1180
|
+
pendingAgeMs: number | null;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Reporte de `authz:scopes:relay` (3b-2d; 3b-2h · 🔴 2). Dice QUÉ se aplicó,
|
|
1184
|
+
* no un contador: la pasada no es atómica y un número no permite retomar nada.
|
|
1185
|
+
*/
|
|
1186
|
+
export interface ScopeRelayReport {
|
|
1187
|
+
/** Aplicados en esta pasada, en orden. Vacío en `dryRun`. */
|
|
1188
|
+
applied: RelayedScopeChange[];
|
|
1189
|
+
/**
|
|
1190
|
+
* El PRIMER cambio que falló, con la causa (`failures[0]`). Se conserva
|
|
1191
|
+
* porque es lo que mira un supervisor; la lista completa está en
|
|
1192
|
+
* `failures`.
|
|
1193
|
+
*/
|
|
1194
|
+
failed: ScopeRelayFailure | null;
|
|
1195
|
+
/**
|
|
1196
|
+
* TODO lo que falló en esta pasada (3b-2h · 🔴 2). Un fallo ya no para la
|
|
1197
|
+
* pasada entera: para lo que DEPENDE de él —los cambios que nombran alguno
|
|
1198
|
+
* de sus scopes, que salen en `deferred`— y el resto sigue. `transient`
|
|
1199
|
+
* (alpha.5 · R2a) distingue la caída del backend, que no consume intento,
|
|
1200
|
+
* del fallo de la entrada, que sí.
|
|
1201
|
+
*/
|
|
1202
|
+
failures: ScopeRelayFailure[];
|
|
1203
|
+
/**
|
|
1204
|
+
* Lo que NO se intentó porque toca un scope contaminado por un fallo o por
|
|
1205
|
+
* otro aplazado de esta misma pasada. Es lo que mantiene el ORDEN del árbol
|
|
1206
|
+
* (aplicar un `moved` antes que el `attached` de su padre da un árbol que
|
|
1207
|
+
* nunca existió) sin dejar que una fila envenenada bloquee a los demás.
|
|
1208
|
+
*
|
|
1209
|
+
* Desde 2.4.0-alpha.5 también contamina lo APARCADO (R2a): las claves de
|
|
1210
|
+
* `dead()` siembran el bloqueo al inicio de la pasada, pero solo para lo
|
|
1211
|
+
* que concede o recuelga (`attached`/`moved`) — un `detached` de esa clave
|
|
1212
|
+
* se aplica igual (invariante 11: una purga nunca queda bloqueada). Y con
|
|
1213
|
+
* `only` (R1) también lo que depende de una pendiente ANTERIOR que no se
|
|
1214
|
+
* pidió: `deferred`, nunca aplicado fuera de orden.
|
|
1215
|
+
*/
|
|
1216
|
+
deferred: RelayedScopeChange[];
|
|
1217
|
+
/**
|
|
1218
|
+
* Entradas APARCADAS por la outbox tras agotar sus intentos (`dead()`), si
|
|
1219
|
+
* la implementación lo soporta. No se van a aplicar solas: el árbol del
|
|
1220
|
+
* backend está permanentemente divergente en esos nodos y hay que mirarlas.
|
|
1221
|
+
* **Siempre GLOBAL**, también con `only` (como `remaining`): un reporte
|
|
1222
|
+
* filtrado que callara lo aparcado sería un reporte en verde sobre una cola
|
|
1223
|
+
* que no converge. Por lo mismo, este reporte enumera actividad de otros
|
|
1224
|
+
* tenants: no lo devuelvas por HTTP (README).
|
|
1225
|
+
*/
|
|
1226
|
+
dead: RelayedScopeChange[];
|
|
1227
|
+
/**
|
|
1228
|
+
* **Cuántas entradas APARCADAS hay en TODA la cola** (2.4.0-alpha.8),
|
|
1229
|
+
* siempre y global. `dead` está acotado a `batchSize` (100), así que
|
|
1230
|
+
* `dead.length` MIENTE cuando hay más; la alarma «la cola no converge» se
|
|
1231
|
+
* sostiene con este número, no con la lista. Sale del censo que la pasada
|
|
1232
|
+
* ya lee para el tapón por clave (cero consultas nuevas); `0` con una outbox
|
|
1233
|
+
* sin `dead()`.
|
|
1234
|
+
*/
|
|
1235
|
+
deadCount: number;
|
|
1236
|
+
/** La edad de la cabeza GLOBAL de la cola (2.4.0-alpha.8). Ver `ScopeRelayQueue`. */
|
|
1237
|
+
queue: ScopeRelayQueue;
|
|
1238
|
+
/**
|
|
1239
|
+
* El resultado por ENTRADA (2.4.0-alpha.5 · R1): qué fue de cada una, con
|
|
1240
|
+
* su motivo. Ver `ScopeRelayEntry`. Con o sin `only`.
|
|
1241
|
+
*/
|
|
1242
|
+
entries: ScopeRelayEntry[];
|
|
1243
|
+
/**
|
|
1244
|
+
* Con `only`: los ids pedidos que NO están en la cola —ni pendientes ni
|
|
1245
|
+
* aparcados— ya aplicados por una pasada anterior (la receta inline con un
|
|
1246
|
+
* cron detrás), o inexistentes; el relay no puede distinguirlo. Vacío sin
|
|
1247
|
+
* `only`. Nunca un reporte vacío en verde por un id que no era.
|
|
1248
|
+
*
|
|
1249
|
+
* **Desde el cierre-2 de 2.4.0-alpha.10 · C2** un id PENDIENTE que la pasada no
|
|
1250
|
+
* llegó a evaluar NO va aquí sino en `notEvaluated`: hasta entonces
|
|
1251
|
+
* `only: [a, b], limit: 1` metía `b` en `missing` —«ya lo aplicó otra pasada, o no
|
|
1252
|
+
* existe»— con `b` pendiente.
|
|
1253
|
+
*/
|
|
1254
|
+
missing: Array<string | number>;
|
|
1255
|
+
/**
|
|
1256
|
+
* **Con `only`: los ids pedidos que SIGUEN PENDIENTES y esta pasada no evaluó**
|
|
1257
|
+
* (cierre-2 de 2.4.0-alpha.10 · C2, campo aditivo): la pasada alcanzó `limit`
|
|
1258
|
+
* antes de llegar a ellos (en `dryRun`, `wouldApply` se cortó en `limit`). No se
|
|
1259
|
+
* aplicaron ni se aplazaron: no se miraron. Se distingue de `missing` con UNA
|
|
1260
|
+
* lectura más de la cola filtrada por esos ids, solo si hay ids pedidos sin ver.
|
|
1261
|
+
* Vacío sin `only` y en un reporte `busy` (ahí no se evaluó nada y lo dice `busy`).
|
|
1262
|
+
*/
|
|
1263
|
+
notEvaluated: Array<string | number>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Otra pasada tenía el lease de la cola y esta no ha hecho NADA (3b-2h ·
|
|
1266
|
+
* 🟠 4). No es un error: el relay es escritor ÚNICO.
|
|
1267
|
+
*/
|
|
1268
|
+
busy: boolean;
|
|
1269
|
+
/** Quedan cambios sin aplicar tras la pasada (vuelve a ejecutar). */
|
|
1270
|
+
remaining: boolean;
|
|
1271
|
+
dryRun: boolean;
|
|
1272
|
+
/** Solo con `dryRun`: lo que se aplicaría, en orden. */
|
|
1273
|
+
wouldApply: RelayedScopeChange[];
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* El lease de una pasada del relay (3b-2h · 🟠 4). Lo devuelve
|
|
1277
|
+
* `ScopeOutbox.acquire()` y lo suelta el manager en un `finally`.
|
|
406
1278
|
*/
|
|
1279
|
+
export interface ScopeOutboxLease {
|
|
1280
|
+
release(): Promise<void>;
|
|
1281
|
+
}
|
|
1282
|
+
/** Contexto del encolado: la transacción del consumidor y quién lo ordena. */
|
|
1283
|
+
export interface ScopeOutboxContext {
|
|
1284
|
+
/**
|
|
1285
|
+
* Lo que el llamante pasó en `ScopeTreeWriteOptions.transaction`: para
|
|
1286
|
+
* Lucid, el `TransactionClientContract` de la transacción en curso. El
|
|
1287
|
+
* manager no lo interpreta —no conoce la BD del consumidor—: lo pasea.
|
|
1288
|
+
* `sqlScopeOutbox` SÍ lo juzga (L-1 · 🟠 9, `assertCallerTransaction`):
|
|
1289
|
+
* tiene que ser una transacción ABIERTA de la conexión de la cola, o 500
|
|
1290
|
+
* `E_AUTHZ_CONFIG` antes del INSERT. Una outbox propia hereda el deber.
|
|
1291
|
+
*/
|
|
1292
|
+
transaction?: unknown;
|
|
1293
|
+
actor?: SubjectRef;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* **El recibo del encolado** (2.4.0-alpha.5 · R1). Lo devuelven
|
|
1297
|
+
* `manager.scopes.attached/moved/detached`: los ids que la outbox asignó a
|
|
1298
|
+
* lo que acaba de encolar — en PLURAL, porque `scopes.detached` encola una
|
|
1299
|
+
* fila por ortografía del uuid cuando la fila del consumidor ya no existe
|
|
1300
|
+
* (invariante 17). Es lo que se le pasa a `relayScopeChanges({ only })` para
|
|
1301
|
+
* relevar «lo mío» en la misma petición sin pagar la cola de los demás.
|
|
1302
|
+
*
|
|
1303
|
+
* **Vacío = no hay nada que relevar**: sin outbox el manager aplicó el cambio
|
|
1304
|
+
* en línea en el driver; con una outbox de terceros cuyo `enqueue` devuelve
|
|
1305
|
+
* `void` tampoco hay id, y entonces `only` no se puede usar con esa outbox
|
|
1306
|
+
* (el manager lo dice con 500 al recibir `only: []`, jamás relevando la cola
|
|
1307
|
+
* entera en silencio).
|
|
1308
|
+
*/
|
|
1309
|
+
export interface ScopeEnqueueReceipt {
|
|
1310
|
+
queued: Array<string | number>;
|
|
1311
|
+
}
|
|
1312
|
+
/** El filtro de `ScopeOutbox.pending` (2.4.0-alpha.5 · R1): SOLO estos ids. */
|
|
1313
|
+
export interface ScopeOutboxPendingFilter {
|
|
1314
|
+
ids: Array<string | number>;
|
|
1315
|
+
}
|
|
1316
|
+
/** El tercer parámetro de `ScopeOutbox.markFailed` (2.4.0-alpha.5 · R2a). */
|
|
1317
|
+
export interface ScopeOutboxFailure {
|
|
1318
|
+
/**
|
|
1319
|
+
* `true` = la causa fue el BACKEND (503), no la entrada: la implementación
|
|
1320
|
+
* escribe la causa y NO cuenta el intento. Una implementación que lo
|
|
1321
|
+
* ignore cuenta siempre (lo de hasta alpha.4: degrada hacia lo seguro).
|
|
1322
|
+
*/
|
|
1323
|
+
transient?: boolean;
|
|
1324
|
+
}
|
|
407
1325
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
1326
|
+
* Un conflicto de `requeue` (2.4.0-alpha.5 · R2b): la entrada APARCADA que se
|
|
1327
|
+
* quiere devolver a la cola y las entradas APLICADAS posteriores (id mayor)
|
|
1328
|
+
* que comparten clave de scope con ella — lo que reaplicarla REVERTIRÍA.
|
|
1329
|
+
* Es el diff que `authz:scopes:requeue` imprime y que `--force` acepta.
|
|
1330
|
+
*/
|
|
1331
|
+
export interface ScopeRequeueConflict {
|
|
1332
|
+
parked: PendingScopeTreeChange;
|
|
1333
|
+
/** Las primeras `DEFAULT_RELAY_BATCH` (100), en orden de id. */
|
|
1334
|
+
applied: PendingScopeTreeChange[];
|
|
1335
|
+
/** `true` = había MÁS de las listadas: el diff que lee el operador está cortado, y se dice (cierre alpha.5, ⚪ 6). */
|
|
1336
|
+
truncated?: boolean;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Reporte de `manager.requeueScopeChanges` (2.4.0-alpha.5 · R2b). `overrides`
|
|
1340
|
+
* solo viene poblado con `force: true`: sin él, un conflicto es 500
|
|
1341
|
+
* `E_AUTHZ_REQUEUE_REFUSED` y nada se reencola.
|
|
1342
|
+
*/
|
|
1343
|
+
export interface ScopeRequeueReport {
|
|
1344
|
+
/** Lo que volvió a `pending()`, con su id de siempre. Con `apply` y el lease ocupado: vacío (cero filas tocadas). */
|
|
1345
|
+
requeued: PendingScopeTreeChange[];
|
|
1346
|
+
/** Los conflictos que `force` saltó (vacío sin conflictos). */
|
|
1347
|
+
overrides: ScopeRequeueConflict[];
|
|
1348
|
+
forced: boolean;
|
|
1349
|
+
/**
|
|
1350
|
+
* Solo con `apply: true` (2.4.0-alpha.8): el reporte de la pasada que relevó
|
|
1351
|
+
* SOLO los ids reencolados, bajo el mismo lease. `relay.busy === true` =
|
|
1352
|
+
* otra pasada tenía el lease y NO se reencoló nada (todo o nada).
|
|
1353
|
+
*/
|
|
1354
|
+
relay?: ScopeRelayReport;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* **El puerto de la outbox del árbol** (3b-2d, panel 2 cruce 4 · S5).
|
|
1358
|
+
*
|
|
1359
|
+
* Sin él, `manager.scopes.attached/moved/detached` escribe en el backend
|
|
1360
|
+
* DENTRO de la transacción del consumidor y un `rollback` posterior deja el
|
|
1361
|
+
* árbol de FGA diciendo una cosa y la BD del consumidor otra —una escalada
|
|
1362
|
+
* persistente e invisible, porque la aplicación lista y audita contra SQL—.
|
|
1363
|
+
* Con él, el manager no toca el driver: ENCOLA el cambio con la transacción
|
|
1364
|
+
* del consumidor, así que el cambio del árbol y su intención de propagación
|
|
1365
|
+
* confirman o se van juntos. Lo aplica después `authz:scopes:relay`.
|
|
1366
|
+
*
|
|
1367
|
+
* El paquete no impone tabla: define este puerto y publica un stub de
|
|
1368
|
+
* migración (`stubs/scopes_outbox_migration.stub`) y una implementación
|
|
1369
|
+
* sobre Lucid (`sqlScopeOutbox`) para quien no quiera escribir la suya.
|
|
411
1370
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1371
|
+
* Lo que NO arregla, y hay que leerlo así: durante el lag del relay
|
|
1372
|
+
* (segundos) FGA decide con el árbol VIEJO. Es un fail-open temporal — el
|
|
1373
|
+
* tenant antiguo conserva acceso tras un `moved`, y los denies heredados no
|
|
1374
|
+
* aplican tras un `attached`—. No hay 2PC; es el precio de tener el árbol en
|
|
1375
|
+
* dos sitios.
|
|
1376
|
+
*/
|
|
1377
|
+
export interface ScopeOutbox {
|
|
1378
|
+
/**
|
|
1379
|
+
* Encola el cambio en la transacción del consumidor. Debe escribir y
|
|
1380
|
+
* volver: nada de aplicarlo aquí. Si lanza, la escritura del manager falla
|
|
1381
|
+
* (y la transacción del consumidor se lleva las dos cosas).
|
|
1382
|
+
*
|
|
1383
|
+
* **Recibo** (2.4.0-alpha.5 · R1): devuelve `{ id }` con el identificador
|
|
1384
|
+
* del registro —el MISMO que `pending()` devolverá en `id`— y el manager lo
|
|
1385
|
+
* entrega al llamante en `ScopeEnqueueReceipt.queued`. Es covariante: una
|
|
1386
|
+
* implementación que siga devolviendo `void` compila y funciona, y su
|
|
1387
|
+
* consumidor recibe `queued: []` (sin recibo no hay `only`).
|
|
1388
|
+
*/
|
|
1389
|
+
enqueue(change: ScopeTreeChange, context: ScopeOutboxContext): Promise<void | {
|
|
1390
|
+
id: string | number;
|
|
1391
|
+
}>;
|
|
1392
|
+
/**
|
|
1393
|
+
* Los pendientes MÁS ANTIGUOS primero: el orden del árbol es el del
|
|
1394
|
+
* encolado. **Y ENTEROS** (cierre alpha.5, auditor 🟡 3): `pending()` no
|
|
1395
|
+
* puede ESCONDER una entrada sin aplicar que el relay no haya filtrado con
|
|
1396
|
+
* `filter.ids` —ni por tenant, ni por scope, ni por nada—. La garantía de
|
|
1397
|
+
* orden por clave del relay (que un `attached` no aterrice antes que el
|
|
1398
|
+
* `moved` del que depende) se apoya en que la cola que ve es la cola
|
|
1399
|
+
* entera; una implementación que filtre por su cuenta reabre la escalada
|
|
1400
|
+
* medida en `relay-auditor-2.ts` y ningún guardián del manager la ve. Es
|
|
1401
|
+
* un deber del puerto y `runScopeOutboxContract` lo juzga (O-08).
|
|
1402
|
+
* `after` (3b-2h · 🔴 2) es el id del último registro que el
|
|
1403
|
+
* relay ya vio en ESTA pasada: como una entrada que falla ya no para la
|
|
1404
|
+
* pasada, se queda pendiente y volvería a salir la primera para siempre.
|
|
1405
|
+
* Una implementación que lo ignore sigue siendo válida —el relay detecta
|
|
1406
|
+
* que no avanza y termina la pasada—, pero solo drenará hasta el primer
|
|
1407
|
+
* lote atascado.
|
|
1408
|
+
*
|
|
1409
|
+
* `filter` (2.4.0-alpha.5 · R1, opcional): SOLO los pendientes cuyo id esté
|
|
1410
|
+
* en `filter.ids`, mismo orden. Es lo que hace que `relayScopeChanges({
|
|
1411
|
+
* only })` no lea la cola de los demás (medido: 4 507 ms → 19 ms con 500
|
|
1412
|
+
* entradas ajenas por delante). Una implementación que lo IGNORE devuelve
|
|
1413
|
+
* lo que no se pidió, y el manager lo detecta y para (500 `E_AUTHZ_CONFIG`
|
|
1414
|
+
* nombrando el puerto) en vez de aplicar el backlog ajeno dentro de tu
|
|
1415
|
+
* petición: el filtro tiene que estar AQUÍ, no en memoria.
|
|
1416
|
+
*/
|
|
1417
|
+
pending(limit: number, after?: string | number, filter?: ScopeOutboxPendingFilter): Promise<PendingScopeTreeChange[]>;
|
|
1418
|
+
/** Aplicado en el backend: no se vuelve a relevar. */
|
|
1419
|
+
markApplied(id: string | number): Promise<void>;
|
|
1420
|
+
/**
|
|
1421
|
+
* Falló al aplicarse: se queda pendiente, con la causa a la vista.
|
|
1422
|
+
* `options.transient` (2.4.0-alpha.5 · R2a): la causa fue el backend, no la
|
|
1423
|
+
* entrada — escribe la causa y NO cuentes el intento. Ignorarlo es válido
|
|
1424
|
+
* (cuenta siempre, lo de hasta alpha.4).
|
|
1425
|
+
*/
|
|
1426
|
+
markFailed(id: string | number, error: string, options?: ScopeOutboxFailure): Promise<void>;
|
|
1427
|
+
/**
|
|
1428
|
+
* **La sonda de dependencia de `only`** (2.4.0-alpha.5 · R1), opcional:
|
|
1429
|
+
* la entrada PENDIENTE más antigua anterior a `id` (menor id, sin aplicar,
|
|
1430
|
+
* NO aparcada) que nombre —como hijo o como padre— alguno de `scopes`, o
|
|
1431
|
+
* `null`. Con ella, `relayScopeChanges({ only })` decide si una entrada
|
|
1432
|
+
* pedida depende de una que no se pidió sin leer la cola entera: una sonda
|
|
1433
|
+
* de existencia (0,69 ms medidos sobre 501 pendientes con el índice
|
|
1434
|
+
* `(applied_at, id)`). Un nivel basta: si una cadena de pendientes llega
|
|
1435
|
+
* hasta `id`, su último eslabón comparte clave con ella por definición de
|
|
1436
|
+
* la contaminación por clave.
|
|
1437
|
+
*
|
|
1438
|
+
* Sin ella el manager hace lo mismo paginando `pending()` desde el
|
|
1439
|
+
* principio hasta `id` — correcto, pero O(cola).
|
|
1440
|
+
*/
|
|
1441
|
+
earlierPending?(id: string | number, scopes: ScopeRef[]): Promise<PendingScopeTreeChange | null>;
|
|
1442
|
+
/**
|
|
1443
|
+
* **Las entradas APARCADAS** (3b-2h · 🔴 2), opcional. Una entrada que ya
|
|
1444
|
+
* no se puede aplicar —su scope padre se borró antes de la pasada— no se
|
|
1445
|
+
* arregla sola: la outbox puede dejar de ofrecerla en `pending()` tras N
|
|
1446
|
+
* intentos y enseñarla aquí. El relay las REPORTA en cada pasada y el
|
|
1447
|
+
* comando sale ≠ 0 mientras haya alguna: un aparcado es una divergencia
|
|
1448
|
+
* permanente del árbol del backend, no un incidente resuelto.
|
|
1449
|
+
*/
|
|
1450
|
+
dead?(limit: number): Promise<PendingScopeTreeChange[]>;
|
|
1451
|
+
/**
|
|
1452
|
+
* **El lease del escritor ÚNICO** (3b-2h · 🟠 4), opcional. `pending()` no
|
|
1453
|
+
* reserva nada, así que dos pasadas a la vez (un `CronJob` con
|
|
1454
|
+
* `concurrencyPolicy: Allow`, dos réplicas, una pasada más larga que su
|
|
1455
|
+
* intervalo) trabajan sobre el MISMO lote: la rezagada re-aplica un
|
|
1456
|
+
* `attached` viejo después de que la otra aplicara el `moved` nuevo y deja
|
|
1457
|
+
* el árbol del store REVERTIDO —con un solo padre, así que nada lo
|
|
1458
|
+
* delata— (medido). Con `acquire`, la segunda pasada no hace nada y lo
|
|
1459
|
+
* dice (`busy`). `null` = otra pasada lo tiene.
|
|
1460
|
+
*
|
|
1461
|
+
* CONTRATO: el lease se toma UNA vez al inicio de la pasada y se sostiene
|
|
1462
|
+
* hasta el `finally`; el relay NO lo re-verifica ni lo renueva dentro del
|
|
1463
|
+
* bucle (a diferencia del freeze durable, que sí se re-afirma por lote).
|
|
1464
|
+
* Por eso la implementación DEBE ser un cerrojo SOSTENIDO mientras dura la
|
|
1465
|
+
* pasada, no un TTL que pueda vencer a mitad: los que trae el paquete lo
|
|
1466
|
+
* cumplen (`pg_try_advisory_xact_lock` vive con la transacción; `get_lock`
|
|
1467
|
+
* de MySQL con la sesión; SQLite en proceso). Un `acquire` con TTL
|
|
1468
|
+
* reabriría la ventana del doble escritor que este lease cierra.
|
|
1469
|
+
*/
|
|
1470
|
+
acquire?(): Promise<ScopeOutboxLease | null>;
|
|
1471
|
+
/**
|
|
1472
|
+
* **Reencola entradas APARCADAS** (2.4.0-alpha.5 · R2b), opcional y
|
|
1473
|
+
* MECÁNICO: la política —la sonda anti-reversión— es del manager
|
|
1474
|
+
* (`requeueScopeChanges`), porque necesita las claves de scope del cambio y
|
|
1475
|
+
* una implementación de terceros no las tiene. Todo o nada: cada id tiene
|
|
1476
|
+
* que existir y estar aparcado (sin aplicar, intentos agotados); si alguno
|
|
1477
|
+
* no lo está —no existe, sigue pendiente, ya se aplicó— se lanza
|
|
1478
|
+
* nombrándolo y NO se toca ninguna fila (una entrada aplicada no revive
|
|
1479
|
+
* jamás: reencolar un `detached` aplicado volvería a purgar). La entrada
|
|
1480
|
+
* vuelve a `pending()` con su id de siempre y **`attempts = maxAttempts −
|
|
1481
|
+
* 1`** (decisión del dueño del 2026-09-09 (2)): si la causa sigue rota
|
|
1482
|
+
* vuelve a aparcarse en UNA pasada —y contamina su clave solo esa pasada—,
|
|
1483
|
+
* y si el operador la arregló entra a la primera. Sin columna nueva.
|
|
1484
|
+
* `sqlScopeOutbox` lo implementa; el manager lo exige junto a
|
|
1485
|
+
* `laterApplied` (sin sonda no hay requeue: 500 `E_AUTHZ_UNSUPPORTED`).
|
|
1486
|
+
*/
|
|
1487
|
+
requeue?(ids: Array<string | number>): Promise<void>;
|
|
1488
|
+
/**
|
|
1489
|
+
* **La sonda anti-reversión de `requeue`** (2.4.0-alpha.5 · R2b), opcional
|
|
1490
|
+
* en el puerto pero exigida por `requeueScopeChanges`: las entradas
|
|
1491
|
+
* APLICADAS con id MAYOR que `id` que nombren —como hijo o como padre—
|
|
1492
|
+
* alguno de `scopes`, en orden de id y hasta `limit`. Espejo de
|
|
1493
|
+
* `earlierPending`. Es lo que el manager mira antes de devolver una
|
|
1494
|
+
* aparcada a la cola: reaplicar un `moved` viejo después de otro `moved`
|
|
1495
|
+
* ya aplicado deja el store con el padre VIEJO y un solo padre (medido:
|
|
1496
|
+
* `assertOneParent` no lo delata). Las filas vuelven con la forma de
|
|
1497
|
+
* `pending()`; `attempts`/`lastError` son los que tenían al aplicarse.
|
|
1498
|
+
*/
|
|
1499
|
+
laterApplied?(id: string | number, scopes: ScopeRef[], limit: number): Promise<PendingScopeTreeChange[]>;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* El árbol del consumidor hacia ABAJO (2.1, B2): todos los descendientes de
|
|
1503
|
+
* `scope` (cualquier tipo, cualquier profundidad), en cualquier orden y sin
|
|
1504
|
+
* incluirlo. Lo implementa el consumidor (o `sqlDescendantsOf`, el helper
|
|
1505
|
+
* opt-in del paquete): el paquete NO lo suple con N+1 llamadas a
|
|
1506
|
+
* `resolveChain`. `null` = «este árbol no conoce ese scope».
|
|
422
1507
|
*
|
|
423
1508
|
* Más de `maxNodes` nodos ⇒ el resolutor puede devolver la lista larga (el
|
|
424
1509
|
* paquete la caza con 422 `E_AUTHZ_TOO_MANY_SCOPES`) o lanzar; en
|
|
425
|
-
* `authorizedScopes` eso es un 422 y en `
|
|
426
|
-
* DEGRADA (3F · S2, y ver el aviso de
|
|
1510
|
+
* `authorizedScopes` eso es un 422 y en `defineScopedRole`/`updateScopedRole`
|
|
1511
|
+
* DEGRADA a la regla de nivel mínima (3F · S2, y ver el aviso de
|
|
1512
|
+
* `#assertLevelUnderOwner`).
|
|
1513
|
+
*
|
|
1514
|
+
* Solo se llama desde `authorizedScopes`/`expandExcludedSubtrees` y desde la
|
|
1515
|
+
* regla de nivel de la delegación; NUNCA desde `authorize`/`hasRole`/`list*`
|
|
1516
|
+
* (test de arquitectura) ni desde `scopes.detached`, que purga hechos del
|
|
1517
|
+
* scope EXACTO y no baja por el árbol (invariante 11; 3b-0 · Z1).
|
|
1518
|
+
*
|
|
1519
|
+
* (D7: hasta 3G había DOS docblocks seguidos aquí y el viejo contradecía al
|
|
1520
|
+
* nuevo sobre qué se espera al pasarse de `maxNodes`. Queda uno.)
|
|
427
1521
|
*/
|
|
428
1522
|
export type ScopeDescendantsResolver = (scope: ScopeRef, options: {
|
|
429
1523
|
maxNodes: number;
|
|
@@ -479,63 +1573,24 @@ export interface AuthzWriteEvent {
|
|
|
479
1573
|
* propagar el 503 para que la auditoría registre un resultado desconocido
|
|
480
1574
|
* en vez de un silencio (que se lee como "no pasó nada"). Un 503 que no es
|
|
481
1575
|
* timeout (conexión rechazada) no lo lleva: esa escritura no ocurrió.
|
|
1576
|
+
*
|
|
1577
|
+
* **Con `{ transaction }` (L-3) sigue siendo `true` en el deadline**, junto
|
|
1578
|
+
* a `transactional: true`: la sentencia puede haber aterrizado DENTRO de la
|
|
1579
|
+
* transacción del llamante (SQLite no cancela; MySQL la mata y la
|
|
1580
|
+
* transacción sigue; PostgreSQL deja la transacción abortada) y confirmar
|
|
1581
|
+
* o no es del llamante, que el paquete no ve. Invariante 13 intacto.
|
|
482
1582
|
*/
|
|
483
1583
|
indeterminate?: boolean;
|
|
484
1584
|
/**
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
* rango se mide en la cadena del OWNER de cada rol (3G · W1), así que un
|
|
495
|
-
* `detached` de un ancestro desconocido ya NO destruye los roles de
|
|
496
|
-
* descendientes vivos. Sale también con `purgedRoles: 0`.
|
|
497
|
-
*/
|
|
498
|
-
reason?: 'owner-detached-unknown';
|
|
499
|
-
/**
|
|
500
|
-
* Solo en `scope_purged`: la purga de roles se acotó al scope EXACTO
|
|
501
|
-
* porque el subárbol no se pudo enumerar (3F · S2). Ver
|
|
502
|
-
* `ScopeDetachOutcome.truncated`.
|
|
503
|
-
*/
|
|
504
|
-
truncated?: true;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Lo que devuelve `scopes.detached` (3F · S1/S2). Hasta 3E era `void` y no
|
|
508
|
-
* había forma de saber si la purga alcanzó a todo el subárbol ni si la
|
|
509
|
-
* policy de rango se llegó a evaluar.
|
|
510
|
-
*/
|
|
511
|
-
export interface ScopeDetachOutcome {
|
|
512
|
-
/** Roles LOCALES purgados (los del scope y, con `descendantsOf`, los del subárbol). */
|
|
513
|
-
purgedRoles: number;
|
|
514
|
-
/**
|
|
515
|
-
* `true` cuando el subárbol NO se pudo enumerar (más de `maxDescendants`,
|
|
516
|
-
* o un `descendantsOf` que falló) y la purga se acotó al scope EXACTO
|
|
517
|
-
* (3F · S2). Degradar en vez de tumbar la operación es la regla: declarar
|
|
518
|
-
* `scopes.descendantsOf` nunca puede dejarte peor que no declararlo, y
|
|
519
|
-
* hasta 3E un subárbol grande dejaba el `detached` en 503 sin purgar ni
|
|
520
|
-
* los roles ni los hechos (auditor N3). Los roles que quedan abajo no son
|
|
521
|
-
* visibles en ninguna parte —su owner ya no cuelga del árbol—, pero siguen
|
|
522
|
-
* ocupando su `(slug, nivel)`: hay que volver a notificar nodo a nodo o
|
|
523
|
-
* subir la cota.
|
|
524
|
-
*
|
|
525
|
-
* También es `true` cuando el árbol ya NO conoce el scope y `descendantsOf`
|
|
526
|
-
* no devolvió nada debajo (3G · W2, auditor P2): el puerto no le exige
|
|
527
|
-
* responder por un scope que `resolveChain` desconoce —puede devolver sus
|
|
528
|
-
* hijos o `null`, ver `ScopeDescendantsResolver`—, así que un vacío ahí no
|
|
529
|
-
* demuestra que debajo no quedara nada. Decir `truncated: false` era
|
|
530
|
-
* afirmar «purga completa» con el rol de la unit hija vivo y concediendo.
|
|
531
|
-
*/
|
|
532
|
-
truncated: boolean;
|
|
533
|
-
/**
|
|
534
|
-
* Igual que en `AuthzWriteEvent`: el scope notificado (o el owner de algún
|
|
535
|
-
* rol purgado) ya no está en el árbol, así que para esos roles la policy
|
|
536
|
-
* de rango no se pudo evaluar. Presente aunque `purgedRoles` sea 0.
|
|
537
|
-
*/
|
|
538
|
-
reason?: 'owner-detached-unknown';
|
|
1585
|
+
* `true` cuando la escritura se inscribió en la transacción del llamante
|
|
1586
|
+
* (`{ transaction }`, L-3): en el momento del evento la fila existe SOLO
|
|
1587
|
+
* dentro de esa transacción, y es un hecho si y solo si el llamante
|
|
1588
|
+
* confirma — cosa que el paquete no ve. Un sink que registre esto como
|
|
1589
|
+
* firme registra algo que un rollback deshace; si necesita la última
|
|
1590
|
+
* palabra, que se cuelgue del commit (`trx.after('commit', …)` en Lucid).
|
|
1591
|
+
* Ausente en el resto (encolar en `scopes.*` no es escribir y no lo lleva).
|
|
1592
|
+
*/
|
|
1593
|
+
transactional?: boolean;
|
|
539
1594
|
}
|
|
540
1595
|
/** Un rol del catálogo tal como lo ve el motor (3A · A2/A3, 3B · B2). */
|
|
541
1596
|
export interface CatalogRole {
|
|
@@ -628,10 +1683,9 @@ export interface ScopedRoleChanges {
|
|
|
628
1683
|
export interface AuthzCatalogWriteEvent {
|
|
629
1684
|
action: 'role_defined' | 'role_updated' | 'role_purged';
|
|
630
1685
|
/**
|
|
631
|
-
* Quién lo ordenó. La API de delegación lo exige siempre; ausente
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* no venir.
|
|
1686
|
+
* Quién lo ordenó. La API de delegación lo exige siempre; ausente en los
|
|
1687
|
+
* `role_purged` de `authz:catalog:prune-orphans` (3b-0 · Z2), que es una
|
|
1688
|
+
* operación de PLATAFORMA y no de un actor del árbol.
|
|
635
1689
|
*/
|
|
636
1690
|
actor?: SubjectRef;
|
|
637
1691
|
role: CatalogRole;
|
|
@@ -652,4 +1706,403 @@ export interface AuthzCatalogWriteEvent {
|
|
|
652
1706
|
*/
|
|
653
1707
|
shadowedByAncestor?: CatalogRoleRef[];
|
|
654
1708
|
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Un rol del catálogo tal como lo ve la PROYECCIÓN: su uuid (la identidad, 3A
|
|
1711
|
+
* · A1) y los slugs de los permisos que vincula.
|
|
1712
|
+
*/
|
|
1713
|
+
export interface CatalogProjectionRole {
|
|
1714
|
+
uuid: string;
|
|
1715
|
+
permissions: string[];
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Foto del catálogo confirmado que un driver puede materializar en su
|
|
1719
|
+
* backend. Se lee de `authz_*` dentro de la transacción del sync: es
|
|
1720
|
+
* DERIVADA, y por eso se puede reconstruir entera (`authz:reconcile`).
|
|
1721
|
+
*/
|
|
1722
|
+
export interface CatalogProjectionSnapshot {
|
|
1723
|
+
/** Todos los slugs de permiso del catálogo (no solo los del spec que se sincroniza). */
|
|
1724
|
+
permissions: string[];
|
|
1725
|
+
/** Todos los roles con sus vínculos rol→permiso. */
|
|
1726
|
+
roles: CatalogProjectionRole[];
|
|
1727
|
+
}
|
|
1728
|
+
/** Lo que una pasada de proyección movió. Nunca un booleano: una proyección silenciosa no se vigila. */
|
|
1729
|
+
export interface CatalogProjectionReport {
|
|
1730
|
+
/** Tuplas nuevas escritas. */
|
|
1731
|
+
written: number;
|
|
1732
|
+
/** Tuplas que sobraban (el catálogo ya no las respalda) y se han borrado. */
|
|
1733
|
+
deleted: number;
|
|
1734
|
+
/** Tuplas que ya estaban exactamente igual. */
|
|
1735
|
+
unchanged: number;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* **Proyección derivada del catálogo en el backend de un driver** (regla del
|
|
1739
|
+
* catálogo reescrita — panel 2, cruce 7; decisión del dueño 2026-08-28).
|
|
1740
|
+
*
|
|
1741
|
+
* El catálogo es propiedad LOCAL siempre: roles y permisos viven en `authz_*`
|
|
1742
|
+
* y ningún driver es su fuente de verdad. Un driver PUEDE mantener una
|
|
1743
|
+
* proyección (el modo `facts` de openfga: permisos como relaciones del modelo
|
|
1744
|
+
* + vínculos rol→permiso como tuplas `role:<uuid>#permits_<P>@<holder>:*`) si
|
|
1745
|
+
* y solo si: (a) es reconstruible desde `authz_*`, (b) `authz:reconcile` la
|
|
1746
|
+
* vigila y (c) NUNCA se lee como catálogo.
|
|
1747
|
+
*
|
|
1748
|
+
* Se inyecta en `syncAuthzCatalog` en vez de importarse: `src/catalog.ts` es
|
|
1749
|
+
* la ruta de un consumidor solo-database y no puede tirar del SDK de OpenFGA
|
|
1750
|
+
* (regla 3 de `check_purity.mjs`).
|
|
1751
|
+
*/
|
|
1752
|
+
export interface CatalogProjection {
|
|
1753
|
+
/**
|
|
1754
|
+
* ¿El catálogo que va a quedar es publicable en este backend? Se llama
|
|
1755
|
+
* ANTES de escribir nada (cotas de nombre y techo del modelo, A3/A4): un
|
|
1756
|
+
* catálogo que no se puede proyectar no se escribe a medias.
|
|
1757
|
+
*/
|
|
1758
|
+
assertPublishable(permissions: readonly string[]): void;
|
|
1759
|
+
/** Rehace la proyección del catálogo ya confirmado: escribe lo que falta y BORRA lo que sobra. */
|
|
1760
|
+
project(snapshot: CatalogProjectionSnapshot): Promise<CatalogProjectionReport>;
|
|
1761
|
+
}
|
|
1762
|
+
/** Un objeto de relaciones: `document:<id>`, `folder:<id>`, `group:<id>`… */
|
|
1763
|
+
export interface RelObject {
|
|
1764
|
+
/** El tipo FGA del objeto (`document`, `folder`, `space`…), declarado en `defineRelationsConfig`. */
|
|
1765
|
+
type: string;
|
|
1766
|
+
/** El id del objeto dentro de su partición. Sin `|`/`#`/`:` (los pone el driver al componer). */
|
|
1767
|
+
id: string;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Un userset como sujeto: `group:eng#member` (todos los miembros del grupo).
|
|
1771
|
+
* Es lo que hace que un `relate(group:g#member, viewer, doc)` conceda `viewer`
|
|
1772
|
+
* a todo el que sea `member` de `g` (`usersetsOf`, un nivel).
|
|
1773
|
+
*/
|
|
1774
|
+
export interface RelUserset {
|
|
1775
|
+
object: RelObject;
|
|
1776
|
+
relation: string;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* El sujeto de una relación: un HOLDER (`{type,uuid}`, como `SubjectRef`) o un
|
|
1780
|
+
* USERSET (`{object, relation}`). El puerto lo tipa explícito porque los dos
|
|
1781
|
+
* viajan por `relate`/`listSubjects`/`check`; un userset de OTRA partición se
|
|
1782
|
+
* corta por comparación de string en el driver (nunca cruza).
|
|
1783
|
+
*/
|
|
1784
|
+
export type RelSubject = SubjectRef | RelUserset;
|
|
1785
|
+
/** Discriminador: ¿este sujeto es un userset (`group:g#member`) y no un holder? */
|
|
1786
|
+
export declare function isRelUserset(subject: RelSubject): subject is RelUserset;
|
|
1787
|
+
/**
|
|
1788
|
+
* La referencia COMPLETA a una tupla de relación, tal como la ve `assertWrite`
|
|
1789
|
+
* (R-13) y `onRelationWrite`: sujeto + relación + objeto + partición, más la
|
|
1790
|
+
* operación. Es puro dato: quien lo recibe decide (auditar, rechazar), nunca
|
|
1791
|
+
* muta el store.
|
|
1792
|
+
*/
|
|
1793
|
+
export interface RelationRef {
|
|
1794
|
+
operation: 'relate' | 'unrelate';
|
|
1795
|
+
subject: RelSubject;
|
|
1796
|
+
relation: string;
|
|
1797
|
+
object: RelObject;
|
|
1798
|
+
partition: ScopeRef;
|
|
1799
|
+
/**
|
|
1800
|
+
* La caducidad pedida en `relate` (R-15), en sus tres estados: omitida
|
|
1801
|
+
* (preserva la vigente), `null` (la quita) o `Date` (la fija). Solo viaja en
|
|
1802
|
+
* `relate`; `assertWrite` puede rechazar una compartición sin plazo.
|
|
1803
|
+
*/
|
|
1804
|
+
expiresAt?: Date | null;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* El evento de escritura de relaciones (auditoría del consumidor, sin
|
|
1808
|
+
* `AsyncLocalStorage`), notificado a `onRelationWrite` DESPUÉS de que el
|
|
1809
|
+
* driver vuelva. **Espeja la forma de `AuthzWriteEvent`** (2.4.0-alpha.3,
|
|
1810
|
+
* paridad con roles): un solo tipo con campos opcionales, no una unión.
|
|
1811
|
+
*
|
|
1812
|
+
* - `relate`/`unrelate`: `subject`, `relation`, `object` y `partition` (y
|
|
1813
|
+
* `expiresAt` en `relate` si se pasó) — la `RelationRef` que vio
|
|
1814
|
+
* `assertWrite`, más `actor`.
|
|
1815
|
+
* - `purgeObject`: `object` y `partition`; **sin `subject` ni `relation`**.
|
|
1816
|
+
* - `purgeSubject`: `subject` y `partition`; **sin `object` ni `relation`**.
|
|
1817
|
+
* Una purga notifica **UN evento por llamada**, aunque el driver barra las
|
|
1818
|
+
* dos ortografías del uuid de partición, y **sin conteo**: el puerto
|
|
1819
|
+
* devuelve `void` (un conteo obligaría a romperlo para los drivers de
|
|
1820
|
+
* terceros y dentro de una `{ transaction }` sería un número que un
|
|
1821
|
+
* rollback desmiente). Quien necesite el «cuántas» tiene
|
|
1822
|
+
* `enumerateRelations`/`listSubjects` ANTES de purgar. Las purgas NO pasan
|
|
1823
|
+
* por `assertWrite` (su `RelationRef.operation` es `'relate' | 'unrelate'`,
|
|
1824
|
+
* tipo publicado del hook R-13).
|
|
1825
|
+
*/
|
|
1826
|
+
export interface RelationWriteEvent {
|
|
1827
|
+
operation: 'relate' | 'unrelate' | 'purgeObject' | 'purgeSubject';
|
|
1828
|
+
/** Ausente solo en `purgeObject` (afecta a todos los sujetos del objeto). */
|
|
1829
|
+
subject?: RelSubject;
|
|
1830
|
+
/** Ausente en `purgeObject`/`purgeSubject` (afectan a todas las relaciones). */
|
|
1831
|
+
relation?: string;
|
|
1832
|
+
/** Ausente solo en `purgeSubject` (afecta a todos los objetos del sujeto). */
|
|
1833
|
+
object?: RelObject;
|
|
1834
|
+
partition: ScopeRef;
|
|
1835
|
+
/** La caducidad pedida en `relate` (R-15), si se pasó. Solo en `relate`. */
|
|
1836
|
+
expiresAt?: Date | null;
|
|
1837
|
+
/** Quién ordenó la escritura (`actor` de las opciones), ya validado (422 si mal formado). Ausente si no lo pasó. */
|
|
1838
|
+
actor?: SubjectRef;
|
|
1839
|
+
/**
|
|
1840
|
+
* `true` cuando la escritura se inscribió en la transacción del llamante
|
|
1841
|
+
* (`{ transaction }`, L-4): en el momento del evento la tupla existe SOLO
|
|
1842
|
+
* dentro de esa transacción y es un hecho si y solo si el llamante
|
|
1843
|
+
* confirma — cosa que el paquete no ve. Un sink que quiera la última
|
|
1844
|
+
* palabra se cuelga de `trx.after('commit', …)`. **Ausente** sin
|
|
1845
|
+
* `{ transaction }` (nunca `false`).
|
|
1846
|
+
*/
|
|
1847
|
+
transactional?: true;
|
|
1848
|
+
/**
|
|
1849
|
+
* `true` cuando el paquete NO sabe si el backend aplicó la escritura
|
|
1850
|
+
* (invariante 13, el MISMO de roles): venció el deadline (503
|
|
1851
|
+
* `E_AUTHZ_BACKEND_TIMEOUT`) **o la escritura era multi-request y falló
|
|
1852
|
+
* DESPUÉS de haber aplicado parte** (la purga de `openfga`: ortografías ×
|
|
1853
|
+
* tipos, cada una `Read`+`deleteTuples`+`Read`; el driver marca el error
|
|
1854
|
+
* con `markPartialWrite`, cierre de alpha.3). Se notifica ANTES de
|
|
1855
|
+
* propagar el error. Un 422, el freeze o un `assertWrite` que lanza no lo
|
|
1856
|
+
* llevan: esa escritura no ocurrió y no hay evento. Un 503 que no es
|
|
1857
|
+
* timeout tampoco **cuando llega antes de la primera sentencia/`Write`**
|
|
1858
|
+
* — en `database` una purga es UN `DELETE`, así que siempre; en `openfga`
|
|
1859
|
+
* solo si aún no se había borrado nada. Con `{ transaction }` sigue siendo
|
|
1860
|
+
* `true` junto a `transactional: true`. **Ausente** en el resto.
|
|
1861
|
+
*/
|
|
1862
|
+
indeterminate?: true;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Opciones de `purgeObject`/`purgeSubject` (L-2): solo la transacción del
|
|
1866
|
+
* consumidor. Ver `RelationTransactionOptions.transaction`.
|
|
1867
|
+
*/
|
|
1868
|
+
export interface RelationTransactionOptions {
|
|
1869
|
+
/**
|
|
1870
|
+
* **La transacción ABIERTA del consumidor** (`TransactionClientContract` de
|
|
1871
|
+
* Lucid), para que la escritura de la tupla confirme o revierta CON la tuya
|
|
1872
|
+
* — L-2, panel `{trx}` (C). Aquí significa **ESCRIBIR en tu transacción**
|
|
1873
|
+
* («los dos o ninguno» en el mismo motor transaccional) — **encolar ≠
|
|
1874
|
+
* escribir**: no es la outbox de `scopes.*`, que solo ENCOLA. Solo lo
|
|
1875
|
+
* cumple un driver con `capabilities.transactionalWrites: true` (`database`,
|
|
1876
|
+
* con el `trx` de SU conexión, L-4); con `false` (`openfga`: una tupla no
|
|
1877
|
+
* entra en una transacción SQL) la llamada es **500 `E_AUTHZ_UNSUPPORTED`**
|
|
1878
|
+
* nombrando driver y operación, antes de tocar el driver. Con
|
|
1879
|
+
* `requireTransactionalWrites: true` (`config.relations`, o heredado del
|
|
1880
|
+
* raíz) un driver `false` es 500 `E_AUTHZ_CONFIG` al resolver. La
|
|
1881
|
+
* AUTORIDAD (barrera del freeze) nunca viaja por ella: pool ≥ 2.
|
|
1882
|
+
*/
|
|
1883
|
+
transaction?: unknown;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Opciones de `purgeObject`/`purgeSubject` del `RelationsManager`
|
|
1887
|
+
* (2.4.0-alpha.3): la transacción del consumidor Y el `actor`. Una purga es
|
|
1888
|
+
* la escritura que MÁS actor necesita (un borrado masivo de llaves), así que
|
|
1889
|
+
* está sujeta a `requireActor` como el `scopes.detached` de roles.
|
|
1890
|
+
*/
|
|
1891
|
+
export interface RelationPurgeOptions extends RelationTransactionOptions {
|
|
1892
|
+
/** Quién ordena la purga; validado (422) y viaja en `RelationWriteEvent.actor`. */
|
|
1893
|
+
actor?: SubjectRef;
|
|
1894
|
+
}
|
|
1895
|
+
/** Opciones comunes a `relate`/`unrelate`. */
|
|
1896
|
+
export interface RelationWriteOptions extends RelationTransactionOptions {
|
|
1897
|
+
/** Quién ordena la escritura; validado (422 `E_AUTHZ_INVALID_IDENTITY` si mal formado) y viaja en `RelationWriteEvent.actor`. */
|
|
1898
|
+
actor?: SubjectRef;
|
|
1899
|
+
/**
|
|
1900
|
+
* **Caducidad de la tupla de relación** (R-15, 2.4.0-alpha.2) — los MISMOS
|
|
1901
|
+
* tres estados que `grant` (invariante 10): omitida ⇒ preserva una caducidad
|
|
1902
|
+
* VIGENTE (una ya caducada revive sin caducidad: es una relación nueva);
|
|
1903
|
+
* `null` ⇒ la quita; `Date` ⇒ la fija (también a un instante pasado: caduca).
|
|
1904
|
+
* Caducidad ESTRICTA: lo que vence AHORA ya no cuenta (`expires_at > now`;
|
|
1905
|
+
* `current_time < valid_until`). Solo la lee `relate`; `unrelate` la ignora.
|
|
1906
|
+
* Cualquier otro valor ⇒ 422 `E_AUTHZ_INVALID_IDENTITY` antes del driver.
|
|
1907
|
+
*/
|
|
1908
|
+
expiresAt?: Date | null;
|
|
1909
|
+
}
|
|
1910
|
+
/** Una página de una enumeración de relaciones (cursor opaco que AVANZA, no filtra herencia). */
|
|
1911
|
+
export interface RelationPage {
|
|
1912
|
+
limit?: number;
|
|
1913
|
+
after?: string;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Lo que un `RelationsDriver` DECLARA que puede hacer. Cada valor lleva su par
|
|
1917
|
+
* de casos `{ whenTrue, whenFalse }` en `runRelationsDriverContract` — nunca
|
|
1918
|
+
* un `skip` (3b-2e · E2). El runner FALLA si una capacidad declarada no tiene
|
|
1919
|
+
* poblada la cara que corresponde a su valor.
|
|
1920
|
+
*/
|
|
1921
|
+
export interface RelationsDriverCapabilities {
|
|
1922
|
+
/** `check` es UNA sola llamada al backend (`openfga`: un `Check`). */
|
|
1923
|
+
singleCheckRelations: boolean;
|
|
1924
|
+
/**
|
|
1925
|
+
* Los `listObjects` enumeran también lo HEREDADO. **`false` siempre** en
|
|
1926
|
+
* este paquete (invariante 7): en `openfga` obligaría a `ListObjects`, que
|
|
1927
|
+
* trunca al tope del servidor. Devuelven hechos DIRECTOS + lo derivado.
|
|
1928
|
+
*/
|
|
1929
|
+
listObjectsInherited: boolean;
|
|
1930
|
+
/** `listSubjects` devuelve también sujetos USERSET (`group:g#member`), no solo holders. */
|
|
1931
|
+
usersetSubjects: boolean;
|
|
1932
|
+
/**
|
|
1933
|
+
* El driver implementa `membersOf` (membresía TRANSITIVA a través de
|
|
1934
|
+
* usersets). Solo `database` (CTE recursiva); `openfga` es `false` (la
|
|
1935
|
+
* transitiva sería `ListUsers`, que trunca) ⇒ `membersOf` es 500
|
|
1936
|
+
* `E_AUTHZ_UNSUPPORTED`.
|
|
1937
|
+
*/
|
|
1938
|
+
membersOfNative: boolean;
|
|
1939
|
+
/** El driver sabe ser ORIGEN de `authz:reconcile` de relaciones (`enumerateRelations`). */
|
|
1940
|
+
enumerateRelations: boolean;
|
|
1941
|
+
/**
|
|
1942
|
+
* `listObjects` SEÑALA el truncamiento cuando el backend corta al tope
|
|
1943
|
+
* (`openfga` con `ListObjects`): la página devuelve `truncated: true`, nunca
|
|
1944
|
+
* una lista parcial muda (S16). Capacidad NUEVA, distinta del
|
|
1945
|
+
* `truncationSignal` de los `list*` de roles.
|
|
1946
|
+
*/
|
|
1947
|
+
listObjectsTruncation: boolean;
|
|
1948
|
+
/**
|
|
1949
|
+
* El driver acepta un reloj inyectado (R-15, paridad con el par
|
|
1950
|
+
* `injectableClock` de roles, 2.5 · J1): `withClock(now)` devuelve una
|
|
1951
|
+
* vista del driver cuyo `now()` decide la caducidad de las tuplas. Con
|
|
1952
|
+
* `true` el juez observa la caducidad EXACTA (T−1 ms concede, T no) y los
|
|
1953
|
+
* tres estados de `expiresAt` sin dormir; con `false` solo puede observarlos
|
|
1954
|
+
* en tiempo real (y el driver NO debe traer `withClock`: declara lo que se
|
|
1955
|
+
* observa).
|
|
1956
|
+
*/
|
|
1957
|
+
injectableClock: boolean;
|
|
1958
|
+
/**
|
|
1959
|
+
* El driver puede inscribir sus escrituras en la transacción del consumidor
|
|
1960
|
+
* (`{ transaction }` en `relate`/`unrelate`/`purgeObject`/`purgeSubject`).
|
|
1961
|
+
* `true` significa EXACTAMENTE «los dos o ninguno con TU transacción»,
|
|
1962
|
+
* nunca «no se pierde». `database` = **`true`** desde L-4 (la escritura va
|
|
1963
|
+
* por el `trx` ABIERTO de SU conexión, `assertCallerTransaction`; la
|
|
1964
|
+
* AUTORIDAD —barrera del freeze, F-05— nunca; exige pool ≥ 2, y un
|
|
1965
|
+
* despliegue con pool 1 declara `false` con la opción
|
|
1966
|
+
* `DatabaseRelationsDriverOptions.transactionalWrites`). `openfga` = false,
|
|
1967
|
+
* y no puede ser otra cosa: una tupla no entra en una transacción SQL — el
|
|
1968
|
+
* store es otro servicio y no hay 2PC. **Mismo nombre que en
|
|
1969
|
+
* `AuthorizationDriverCapabilities`**. Con `false`, `{ transaction }` es 500
|
|
1970
|
+
* `E_AUTHZ_UNSUPPORTED` por llamada (cero llamadas al driver); con
|
|
1971
|
+
* `requireTransactionalWrites: true` un driver `false` es 500
|
|
1972
|
+
* `E_AUTHZ_CONFIG` al resolver. La cara `true` del runner juzga el rollback
|
|
1973
|
+
* POR CENSO (cero tuplas nuevas para las cuatro escrituras; `purge*`
|
|
1974
|
+
* revierten juntos) y que una transacción ajena no recibe ni una sentencia.
|
|
1975
|
+
*/
|
|
1976
|
+
transactionalWrites: boolean;
|
|
1977
|
+
}
|
|
1978
|
+
/** Las claves de `RelationsDriverCapabilities`, literales: lo que `relations.capabilities()` normaliza (2.4.0-alpha.4). */
|
|
1979
|
+
export declare const RELATIONS_DRIVER_CAPABILITY_KEYS: ReadonlyArray<keyof RelationsDriverCapabilities>;
|
|
1980
|
+
/** La copia congelada y normalizada de `relations.capabilities()`; misma regla que `normalizeAuthorizationCapabilities`. */
|
|
1981
|
+
export declare function normalizeRelationsCapabilities(declared: Partial<RelationsDriverCapabilities> | undefined): Readonly<RelationsDriverCapabilities>;
|
|
1982
|
+
/**
|
|
1983
|
+
* Una página de `listObjects`/`listSubjects`/`enumerateRelations`. `truncated`
|
|
1984
|
+
* dice si el backend cortó al tope (solo con `listObjectsTruncation`): un
|
|
1985
|
+
* consumidor que lo ve sabe que hay MÁS y no toma la lista por completa.
|
|
1986
|
+
*/
|
|
1987
|
+
export interface RelationObjectsPage {
|
|
1988
|
+
objects: RelObject[];
|
|
1989
|
+
cursor?: string;
|
|
1990
|
+
truncated?: boolean;
|
|
1991
|
+
}
|
|
1992
|
+
export interface RelationSubjectsPage {
|
|
1993
|
+
subjects: RelSubject[];
|
|
1994
|
+
cursor?: string;
|
|
1995
|
+
truncated?: boolean;
|
|
1996
|
+
}
|
|
1997
|
+
/** Una tupla de relación tal como la enumera `enumerateRelations` (origen de reconcile). */
|
|
1998
|
+
export interface RelationTuple {
|
|
1999
|
+
subject: RelSubject;
|
|
2000
|
+
relation: string;
|
|
2001
|
+
object: RelObject;
|
|
2002
|
+
partition: ScopeRef;
|
|
2003
|
+
/**
|
|
2004
|
+
* La caducidad de la tupla tal como está ESCRITA (R-15): `null`/ausente = no
|
|
2005
|
+
* caduca. `enumerateRelations` NO filtra la caducada: tiene que LLEGAR al
|
|
2006
|
+
* destino de `reconcile` con su `expiresAt` para contarse en `skipped`
|
|
2007
|
+
* (la lección de la 3b); filtrarla en el origen la haría desaparecer sin rastro.
|
|
2008
|
+
*/
|
|
2009
|
+
expiresAt?: Date | null;
|
|
2010
|
+
}
|
|
2011
|
+
export interface RelationTuplePage {
|
|
2012
|
+
tuples: RelationTuple[];
|
|
2013
|
+
cursor?: string;
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* El puerto de ReBAC. `partition: ScopeRef` es OBLIGATORIA en TODA operación
|
|
2017
|
+
* (`APP_SCOPE` es válida para mono-tenant): el aislamiento de tenant se corta
|
|
2018
|
+
* por la partición, y el driver la serializa en el id del objeto y del
|
|
2019
|
+
* userset. La whitelist de tipo/relación (F-05) la aplica el manager ANTES de
|
|
2020
|
+
* llamar al driver, pero el driver la re-valida por defensa en profundidad:
|
|
2021
|
+
* `relate`/`unrelate` de `database` y de `openfga` rechazan 422
|
|
2022
|
+
* (`E_AUTHZ_RELATION_TYPE_UNKNOWN` / `E_AUTHZ_RELATION_UNKNOWN`, la MISMA
|
|
2023
|
+
* función y el mismo `code` que el manager, `assertRelationDeclared`) un
|
|
2024
|
+
* `object.type` no declarado o una `relation` no declarada para ese tipo,
|
|
2025
|
+
* ANTES de tocar el backend (cero `Write`, cero INSERT). Es la red para quien
|
|
2026
|
+
* entra por `manager.driver()` o por `reconcileRelations` (L-0): hasta
|
|
2027
|
+
* entonces esta frase era falsa en los dos drivers y, en el store compartido,
|
|
2028
|
+
* `driver.relate(evil, 'assignee', {type:'role_binding', id:<roleUuid>}, S)`
|
|
2029
|
+
* escalaba a `roles.authorize` (medido). **Y desde `2.4.0-alpha.3` en las
|
|
2030
|
+
* OCHO operaciones** (cierre del 🔴 1 / 🟠 2 del auditor; cierre-2 · 🟡 3):
|
|
2031
|
+
* `purgeObject` (el tipo, `assertObjectTypeDeclared`), `purgeSubject` (el
|
|
2032
|
+
* userset del sujeto), `check`/`listSubjects`/`membersOf` (el par) y
|
|
2033
|
+
* `listObjects` (tipo y relación) también la aplican, en el manager y en los
|
|
2034
|
+
* dos drivers, con cero llamadas al backend — sin ella
|
|
2035
|
+
* `purgeObject({type:'role_binding', id:R}, S)` BORRABA el binding real del
|
|
2036
|
+
* store compartido y `listSubjects('assignee', role_binding)` enumeraba sus
|
|
2037
|
+
* asignados (medido contra el `:8101`). **La relación es OBLIGATORIA** donde
|
|
2038
|
+
* se nombra (cierre-2 · 🟠 1: `undefined` ⇒ 422 `E_AUTHZ_RELATION_UNKNOWN`,
|
|
2039
|
+
* nunca una tupla escrita, un 503 del servidor ni la unión de `listSubjects`).
|
|
2040
|
+
* El runner publicado lo exige a un driver de terceros en las seis.
|
|
2041
|
+
*
|
|
2042
|
+
* **Invariante 13 y las purgas multi-request**: un driver cuya purga NO sea
|
|
2043
|
+
* una sola sentencia/`Write` y falle DESPUÉS de haber borrado parte debe
|
|
2044
|
+
* propagar el error marcado con `markPartialWrite` (exportado): el
|
|
2045
|
+
* `RelationsManager` notifica entonces `onRelationWrite` con
|
|
2046
|
+
* `indeterminate: true` antes de propagar. `openfga` lo hace; `database`
|
|
2047
|
+
* (un `DELETE`) no lo necesita.
|
|
2048
|
+
*/
|
|
2049
|
+
export interface RelationsDriver {
|
|
2050
|
+
readonly capabilities?: RelationsDriverCapabilities;
|
|
2051
|
+
/** Crea la relación `subject —relation→ object` en `partition`. Idempotente. */
|
|
2052
|
+
relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
|
|
2053
|
+
/** Retira la relación. No-op seguro si no existe (invariante 6). */
|
|
2054
|
+
unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
|
|
2055
|
+
/** ¿`subject` tiene `relation` sobre `object` en `partition` (directo o derivado por includes/userset)? */
|
|
2056
|
+
check(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef): Promise<boolean>;
|
|
2057
|
+
/** Los objetos de tipo `objectType` sobre los que `subject` tiene `relation`. Directos + derivados, sin herencia abierta. */
|
|
2058
|
+
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, page?: RelationPage): Promise<RelationObjectsPage>;
|
|
2059
|
+
/** Los sujetos DIRECTOS de `relation` sobre `object` (holders y usersets). Nunca la membresía transitiva (eso es `membersOf`). */
|
|
2060
|
+
listSubjects(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
|
|
2061
|
+
/** Borra todas las tuplas cuyo OBJETO es `object` (F-05 sobre el TIPO, alpha.3) y demuestra cero, o lanza 500 `E_AUTHZ_PURGE_INCOMPLETE` (invariante 11). Si falla tras borrar parte, marcado con `markPartialWrite`. */
|
|
2062
|
+
purgeObject(object: RelObject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
|
|
2063
|
+
/** Borra todas las tuplas cuyo SUJETO es `subject` (F-05 sobre el userset, alpha.3) y demuestra cero, o lanza 500. Si falla tras borrar parte, marcado con `markPartialWrite`. */
|
|
2064
|
+
purgeSubject(subject: RelSubject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
|
|
2065
|
+
/**
|
|
2066
|
+
* La membresía TRANSITIVA de un objeto-grupo: todos los holders que son
|
|
2067
|
+
* `member` directa o a través de grupos anidados. DISTINTO de
|
|
2068
|
+
* `listSubjects(member, group)`, que devuelve solo los hechos DIRECTOS. Solo
|
|
2069
|
+
* lo trae el driver con `membersOfNative: true`.
|
|
2070
|
+
*/
|
|
2071
|
+
membersOf?(object: RelObject, relation: string, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
|
|
2072
|
+
/** ORIGEN de `authz:reconcile` de relaciones: las tuplas paginadas, sin filtrar (la caducada LLEGA con su `expiresAt`). Solo con `enumerateRelations: true`. */
|
|
2073
|
+
enumerateRelations?(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
|
|
2074
|
+
/**
|
|
2075
|
+
* **K particiones en UNA pasada** (2.4.0-alpha.6, opcional). La MISMA
|
|
2076
|
+
* semántica que `enumerateRelations` por cada partición pedida —exhaustiva,
|
|
2077
|
+
* la caducada llega, el cursor avanza— y cada tupla trae SU `partition`
|
|
2078
|
+
* (la referencia pedida, casada por clave ENTERA): el llamante reparte por
|
|
2079
|
+
* ella. Existe porque en `openfga` `enumerateRelations` es un barrido del
|
|
2080
|
+
* store entero por llamada (no hay `Read` por prefijo: responde 200 con cero
|
|
2081
|
+
* tuplas) y K particiones costaban K barridos; con esto cuestan uno.
|
|
2082
|
+
* `reconcileRelations({ partitions })` lo usa si el driver lo trae y si no
|
|
2083
|
+
* cae a K llamadas de `enumerateRelations` (correcto, más caro). Un driver
|
|
2084
|
+
* que lo implemente tiene que enumerar EXHAUSTIVAMENTE o fallar ruidoso:
|
|
2085
|
+
* jamás «vacío = éxito» para una partición que no pudo leer entera.
|
|
2086
|
+
*/
|
|
2087
|
+
enumerateRelationsMany?(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
|
|
2088
|
+
/**
|
|
2089
|
+
* Vista de este driver con OTRO reloj de pared (R-15, paridad con
|
|
2090
|
+
* `AuthorizationDriver.withClock`, 2.5 · J1): mismo backend, solo cambia el
|
|
2091
|
+
* `now()` que decide la caducidad (`expires_at > now` en SQL; el
|
|
2092
|
+
* `current_time` del `Check` en FGA; el filtro en cliente de `listSubjects`).
|
|
2093
|
+
* Opcional: el `RelationsManager` lo aplica si recibe `clock` (500
|
|
2094
|
+
* `E_AUTHZ_CONFIG` si el driver no lo trae) y el juez lo usa con
|
|
2095
|
+
* `injectableClock: true`.
|
|
2096
|
+
*/
|
|
2097
|
+
withClock?(now: () => Date): RelationsDriver;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Factory de un `RelationsDriver` (Fase 4, lote 4-6) — el análogo de
|
|
2101
|
+
* `AuthorizationDriverFactory` para el puerto de relaciones. El consumidor la
|
|
2102
|
+
* declara en `config.relations.drivers`, y `authz:relations:reconcile` la
|
|
2103
|
+
* invoca para construir el ORIGEN y el DESTINO de una migración de tuplas. El
|
|
2104
|
+
* driver `openfga` de relaciones entra por el subpath `/openfga` DENTRO de la
|
|
2105
|
+
* factory (como el de roles), así que el comando nunca toca el SDK (pureza).
|
|
2106
|
+
*/
|
|
2107
|
+
export type RelationsDriverFactory = () => RelationsDriver | Promise<RelationsDriver>;
|
|
655
2108
|
//# sourceMappingURL=types.d.ts.map
|