@jantstack/adonis-authz 2.4.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 +496 -29
- package/build/commands/authz_catalog_diff.d.ts.map +1 -1
- package/build/commands/authz_catalog_diff.js +0 -1
- package/build/commands/authz_catalog_diff.js.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.js +0 -1
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -1
- package/build/commands/authz_freeze.d.ts.map +1 -1
- package/build/commands/authz_freeze.js +1 -2
- package/build/commands/authz_freeze.js.map +1 -1
- package/build/commands/authz_reconcile.d.ts +15 -6
- package/build/commands/authz_reconcile.d.ts.map +1 -1
- package/build/commands/authz_reconcile.js +143 -20
- package/build/commands/authz_reconcile.js.map +1 -1
- package/build/commands/authz_relations_reconcile.d.ts +72 -1
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +234 -21
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/commands/authz_scopes_relay.d.ts +50 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -1
- package/build/commands/authz_scopes_relay.js +213 -10
- package/build/commands/authz_scopes_relay.js.map +1 -1
- 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 +12 -4
- package/build/commands/authz_unfreeze.d.ts.map +1 -1
- package/build/commands/authz_unfreeze.js +74 -9
- package/build/commands/authz_unfreeze.js.map +1 -1
- package/build/commands/main.d.ts +16 -13
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +59 -11
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +49 -3
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +72 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/index.d.ts +15 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -3
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +7 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +20 -3
- package/build/providers/authz_provider.js.map +1 -1
- package/build/src/define_config.d.ts +106 -1
- 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 +53 -14
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +104 -32
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +74 -9
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +391 -93
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +71 -10
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +195 -16
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +15 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -1
- package/build/src/drivers/openfga_facts.js +25 -2
- package/build/src/drivers/openfga_facts.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +72 -6
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +308 -50
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +175 -4
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +219 -3
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +100 -20
- package/build/src/freeze.d.ts.map +1 -1
- package/build/src/freeze.js +159 -25
- package/build/src/freeze.js.map +1 -1
- package/build/src/manager.d.ts +239 -7
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1192 -148
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +2 -2
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +2 -2
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +2 -2
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +2 -2
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +2 -2
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/relation_partition_trigger.d.ts.map +1 -1
- package/build/src/relation_partition_trigger.js +11 -3
- package/build/src/relation_partition_trigger.js.map +1 -1
- package/build/src/relations/define_relations_config.d.ts +51 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -1
- package/build/src/relations/define_relations_config.js +68 -1
- package/build/src/relations/define_relations_config.js.map +1 -1
- package/build/src/relations/manager.d.ts +106 -6
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +368 -30
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +81 -6
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +233 -77
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/scope_outbox.d.ts +54 -0
- package/build/src/scope_outbox.d.ts.map +1 -1
- package/build/src/scope_outbox.js +274 -33
- package/build/src/scope_outbox.js.map +1 -1
- package/build/src/shared/backend_guard.d.ts +28 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -1
- package/build/src/shared/backend_guard.js +52 -0
- package/build/src/shared/backend_guard.js.map +1 -1
- package/build/src/shared/sql_expiry.d.ts +2 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -1
- package/build/src/shared/sql_expiry.js +4 -0
- package/build/src/shared/sql_expiry.js.map +1 -1
- 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 +8 -0
- package/build/src/sql_descendants.d.ts.map +1 -1
- package/build/src/sql_descendants.js.map +1 -1
- package/build/src/testing/contract.d.ts +55 -4
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +244 -9
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +6 -4
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +3 -2
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +9 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -1
- package/build/src/testing/migration_contract.js +5 -2
- package/build/src/testing/migration_contract.js.map +1 -1
- package/build/src/testing/relations_contract.d.ts +23 -1
- package/build/src/testing/relations_contract.d.ts.map +1 -1
- package/build/src/testing/relations_contract.js +603 -19
- package/build/src/testing/relations_contract.js.map +1 -1
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -1
- package/build/src/testing/relations_reconcile_contract.js +49 -1
- package/build/src/testing/relations_reconcile_contract.js.map +1 -1
- 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/has_uuid.d.ts +2 -2
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +708 -38
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +55 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +45 -1
- package/build/stubs/migration.stub +20 -10
- package/package.json +1 -1
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`,
|
|
@@ -116,9 +151,24 @@ export interface ScopeTreeWriteOptions extends ScopedWriteOptions {
|
|
|
116
151
|
* La transacción ABIERTA del consumidor (`TransactionClientContract` de
|
|
117
152
|
* Lucid, o lo que use su outbox). El paquete no la interpreta: se la pasa
|
|
118
153
|
* tal cual a `scopes.outbox.enqueue` para que el INSERT del encolado caiga
|
|
119
|
-
* dentro de ella
|
|
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
|
|
120
156
|
* y sin transacción, el encolado se confirma solo y vuelve a haber dos
|
|
121
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.
|
|
122
172
|
*/
|
|
123
173
|
transaction?: unknown;
|
|
124
174
|
}
|
|
@@ -258,7 +308,51 @@ export interface AuthorizationDriverCapabilities {
|
|
|
258
308
|
* el destino los lee de ahí directamente (`openfga.reconcile`).
|
|
259
309
|
*/
|
|
260
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;
|
|
261
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>;
|
|
262
356
|
export interface AuthorizationDriver {
|
|
263
357
|
/** Lo que este driver declara poder hacer (3b-2e · E2). Ver `AuthorizationDriverCapabilities`. */
|
|
264
358
|
readonly capabilities?: AuthorizationDriverCapabilities;
|
|
@@ -280,7 +374,7 @@ export interface AuthorizationDriver {
|
|
|
280
374
|
* el catálogo para `scope.type` (422 si no, como `grant`); la asignación
|
|
281
375
|
* puede no existir (no-op).
|
|
282
376
|
*/
|
|
283
|
-
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<void>;
|
|
377
|
+
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
284
378
|
/**
|
|
285
379
|
* ¿El holder tiene el rol (vigente) en el scope o en un ancestro?
|
|
286
380
|
* Misma regla de herencia hacia abajo que `authorize`. Es MEMBRESÍA: el
|
|
@@ -291,12 +385,12 @@ export interface AuthorizationDriver {
|
|
|
291
385
|
* Deny explícito de UN permiso al holder en un scope (y sus descendientes).
|
|
292
386
|
* El permiso debe existir en el catálogo (throw si no). Idempotente.
|
|
293
387
|
*/
|
|
294
|
-
deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
388
|
+
deny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
295
389
|
/**
|
|
296
390
|
* Levanta el deny en ese scope exacto. El permiso debe existir en el
|
|
297
391
|
* catálogo (422 si no, como `deny`); el deny puede no existir (no-op).
|
|
298
392
|
*/
|
|
299
|
-
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
393
|
+
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
300
394
|
/** Holders con asignación VIGENTE del rol en ese scope exacto (sin herencia). */
|
|
301
395
|
listSubjects(role: RoleQuery, scope: ScopeRef): Promise<SubjectRef[]>;
|
|
302
396
|
/**
|
|
@@ -565,12 +659,36 @@ export interface ReconcileSource {
|
|
|
565
659
|
* `enumerateFacts`) del origen `name`, que puede ser **el propio
|
|
566
660
|
* destino** cuando el destino es el driver ACTIVO y sus hechos son
|
|
567
661
|
* suyos (la pasada de MANTENIMIENTO: rehace lo derivado —marcador,
|
|
568
|
-
* catálogo, árbol y visibilidad— y no inventa
|
|
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).
|
|
569
665
|
*
|
|
570
666
|
* Ausente = `{ name: 'authz_*', authzTables: true }`: el comportamiento de
|
|
571
667
|
* 3b-3a, que es el que vale cuando el origen es el esquema publicado.
|
|
572
668
|
*/
|
|
573
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[];
|
|
574
692
|
}
|
|
575
693
|
/** Ver `ReconcileSource.factsOrigin` (3b-5). */
|
|
576
694
|
export interface ReconcileFactsOrigin {
|
|
@@ -635,6 +753,14 @@ export interface ReconcileOptions {
|
|
|
635
753
|
prune?: boolean;
|
|
636
754
|
/** La salida humana de `E_AUTHZ_MASS_RECONCILE_REFUSED`. */
|
|
637
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;
|
|
638
764
|
/** Filas por lote en las lecturas del origen y por `Write` en el destino (default 100). */
|
|
639
765
|
batchSize?: number;
|
|
640
766
|
/**
|
|
@@ -700,15 +826,37 @@ export interface ReconcileReport extends ReconcileCounts {
|
|
|
700
826
|
* significa que el lease se perdió a mitad —una pausa más larga que el
|
|
701
827
|
* lease, la base caída, otro dueño— y hubo una ventana en la que otros
|
|
702
828
|
* procesos pudieron escribir: la pasada NO se certifica y el comando sale
|
|
703
|
-
* distinto de cero. `leaseMs: null` = ventana
|
|
704
|
-
* OPERADOR dentro del que corrió la pasada
|
|
705
|
-
*
|
|
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.
|
|
706
832
|
*/
|
|
707
833
|
frozen?: {
|
|
708
834
|
durable: boolean;
|
|
709
835
|
lapsed: boolean;
|
|
710
836
|
leaseMs: number | null;
|
|
711
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;
|
|
712
860
|
};
|
|
713
861
|
dryRun: boolean;
|
|
714
862
|
prune: boolean;
|
|
@@ -739,9 +887,48 @@ export interface ReconcileReport extends ReconcileCounts {
|
|
|
739
887
|
pendingRelay: number;
|
|
740
888
|
/** Entradas APARCADAS de la outbox: divergencia permanente, no una ventana. */
|
|
741
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;
|
|
742
901
|
};
|
|
743
902
|
/** La pasada tiene la firma de un origen ciego (ver `E_AUTHZ_MASS_RECONCILE_REFUSED`). */
|
|
744
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
|
+
};
|
|
745
932
|
}
|
|
746
933
|
export interface ExcludedSubtree {
|
|
747
934
|
scope: ScopeRef;
|
|
@@ -846,7 +1033,13 @@ export interface ScopeEdgePage {
|
|
|
846
1033
|
*
|
|
847
1034
|
* Sin él, `authz:reconcile --to=openfga` no puede migrar el árbol y lo dice
|
|
848
1035
|
* (500 `E_AUTHZ_CONFIG`): NO se inventa un árbol plano.
|
|
849
|
-
* `sqlScopeEdges(...)` lo implementa sobre
|
|
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`.
|
|
850
1043
|
*/
|
|
851
1044
|
export type ScopeEdgesEnumerator = (options: {
|
|
852
1045
|
limit: number;
|
|
@@ -886,6 +1079,20 @@ export interface PendingScopeTreeChange {
|
|
|
886
1079
|
* por una cola.
|
|
887
1080
|
*/
|
|
888
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;
|
|
889
1096
|
}
|
|
890
1097
|
/** Lo que el relay aplicó (o aplicaría), pieza a pieza. */
|
|
891
1098
|
export interface RelayedScopeChange {
|
|
@@ -895,6 +1102,83 @@ export interface RelayedScopeChange {
|
|
|
895
1102
|
/** La causa, en lo que FALLÓ, se APARCÓ o se APLAZÓ (nunca en lo aplicado). */
|
|
896
1103
|
error?: string;
|
|
897
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
|
+
}
|
|
898
1182
|
/**
|
|
899
1183
|
* Reporte de `authz:scopes:relay` (3b-2d; 3b-2h · 🔴 2). Dice QUÉ se aplicó,
|
|
900
1184
|
* no un contador: la pasada no es atómica y un número no permite retomar nada.
|
|
@@ -907,34 +1191,76 @@ export interface ScopeRelayReport {
|
|
|
907
1191
|
* porque es lo que mira un supervisor; la lista completa está en
|
|
908
1192
|
* `failures`.
|
|
909
1193
|
*/
|
|
910
|
-
failed:
|
|
911
|
-
id: string | number;
|
|
912
|
-
change: ScopeTreeChange;
|
|
913
|
-
error: string;
|
|
914
|
-
} | null;
|
|
1194
|
+
failed: ScopeRelayFailure | null;
|
|
915
1195
|
/**
|
|
916
1196
|
* TODO lo que falló en esta pasada (3b-2h · 🔴 2). Un fallo ya no para la
|
|
917
1197
|
* pasada entera: para lo que DEPENDE de él —los cambios que nombran alguno
|
|
918
|
-
* de sus scopes, que salen en `deferred`— y el resto sigue.
|
|
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í.
|
|
919
1201
|
*/
|
|
920
|
-
failures:
|
|
921
|
-
id: string | number;
|
|
922
|
-
change: ScopeTreeChange;
|
|
923
|
-
error: string;
|
|
924
|
-
}>;
|
|
1202
|
+
failures: ScopeRelayFailure[];
|
|
925
1203
|
/**
|
|
926
1204
|
* Lo que NO se intentó porque toca un scope contaminado por un fallo o por
|
|
927
1205
|
* otro aplazado de esta misma pasada. Es lo que mantiene el ORDEN del árbol
|
|
928
1206
|
* (aplicar un `moved` antes que el `attached` de su padre da un árbol que
|
|
929
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.
|
|
930
1215
|
*/
|
|
931
1216
|
deferred: RelayedScopeChange[];
|
|
932
1217
|
/**
|
|
933
1218
|
* Entradas APARCADAS por la outbox tras agotar sus intentos (`dead()`), si
|
|
934
1219
|
* la implementación lo soporta. No se van a aplicar solas: el árbol del
|
|
935
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).
|
|
936
1225
|
*/
|
|
937
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>;
|
|
938
1264
|
/**
|
|
939
1265
|
* Otra pasada tenía el lease de la cola y esta no ha hecho NADA (3b-2h ·
|
|
940
1266
|
* 🟠 4). No es un error: el relay es escritor ÚNICO.
|
|
@@ -958,11 +1284,75 @@ export interface ScopeOutboxContext {
|
|
|
958
1284
|
/**
|
|
959
1285
|
* Lo que el llamante pasó en `ScopeTreeWriteOptions.transaction`: para
|
|
960
1286
|
* Lucid, el `TransactionClientContract` de la transacción en curso. El
|
|
961
|
-
*
|
|
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.
|
|
962
1291
|
*/
|
|
963
1292
|
transaction?: unknown;
|
|
964
1293
|
actor?: SubjectRef;
|
|
965
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
|
+
}
|
|
1325
|
+
/**
|
|
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
|
+
}
|
|
966
1356
|
/**
|
|
967
1357
|
* **El puerto de la outbox del árbol** (3b-2d, panel 2 cruce 4 · S5).
|
|
968
1358
|
*
|
|
@@ -989,22 +1379,66 @@ export interface ScopeOutbox {
|
|
|
989
1379
|
* Encola el cambio en la transacción del consumidor. Debe escribir y
|
|
990
1380
|
* volver: nada de aplicarlo aquí. Si lanza, la escritura del manager falla
|
|
991
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`).
|
|
992
1388
|
*/
|
|
993
|
-
enqueue(change: ScopeTreeChange, context: ScopeOutboxContext): Promise<void
|
|
1389
|
+
enqueue(change: ScopeTreeChange, context: ScopeOutboxContext): Promise<void | {
|
|
1390
|
+
id: string | number;
|
|
1391
|
+
}>;
|
|
994
1392
|
/**
|
|
995
1393
|
* Los pendientes MÁS ANTIGUOS primero: el orden del árbol es el del
|
|
996
|
-
* encolado.
|
|
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
|
|
997
1403
|
* relay ya vio en ESTA pasada: como una entrada que falla ya no para la
|
|
998
1404
|
* pasada, se queda pendiente y volvería a salir la primera para siempre.
|
|
999
1405
|
* Una implementación que lo ignore sigue siendo válida —el relay detecta
|
|
1000
1406
|
* que no avanza y termina la pasada—, pero solo drenará hasta el primer
|
|
1001
1407
|
* lote atascado.
|
|
1002
|
-
|
|
1003
|
-
|
|
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[]>;
|
|
1004
1418
|
/** Aplicado en el backend: no se vuelve a relevar. */
|
|
1005
1419
|
markApplied(id: string | number): Promise<void>;
|
|
1006
|
-
/**
|
|
1007
|
-
|
|
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>;
|
|
1008
1442
|
/**
|
|
1009
1443
|
* **Las entradas APARCADAS** (3b-2h · 🔴 2), opcional. Una entrada que ya
|
|
1010
1444
|
* no se puede aplicar —su scope padre se borró antes de la pasada— no se
|
|
@@ -1034,6 +1468,35 @@ export interface ScopeOutbox {
|
|
|
1034
1468
|
* reabriría la ventana del doble escritor que este lease cierra.
|
|
1035
1469
|
*/
|
|
1036
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[]>;
|
|
1037
1500
|
}
|
|
1038
1501
|
/**
|
|
1039
1502
|
* El árbol del consumidor hacia ABAJO (2.1, B2): todos los descendientes de
|
|
@@ -1110,8 +1573,24 @@ export interface AuthzWriteEvent {
|
|
|
1110
1573
|
* propagar el 503 para que la auditoría registre un resultado desconocido
|
|
1111
1574
|
* en vez de un silencio (que se lee como "no pasó nada"). Un 503 que no es
|
|
1112
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.
|
|
1113
1582
|
*/
|
|
1114
1583
|
indeterminate?: boolean;
|
|
1584
|
+
/**
|
|
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;
|
|
1115
1594
|
}
|
|
1116
1595
|
/** Un rol del catálogo tal como lo ve el motor (3A · A2/A3, 3B · B2). */
|
|
1117
1596
|
export interface CatalogRole {
|
|
@@ -1317,16 +1796,116 @@ export interface RelationRef {
|
|
|
1317
1796
|
relation: string;
|
|
1318
1797
|
object: RelObject;
|
|
1319
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;
|
|
1320
1884
|
}
|
|
1321
|
-
/**
|
|
1322
|
-
|
|
1323
|
-
|
|
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`. */
|
|
1324
1893
|
actor?: SubjectRef;
|
|
1325
1894
|
}
|
|
1326
1895
|
/** Opciones comunes a `relate`/`unrelate`. */
|
|
1327
|
-
export interface RelationWriteOptions {
|
|
1328
|
-
/** Quién ordena la escritura; viaja en `RelationWriteEvent.actor`. */
|
|
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`. */
|
|
1329
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;
|
|
1330
1909
|
}
|
|
1331
1910
|
/** Una página de una enumeración de relaciones (cursor opaco que AVANZA, no filtra herencia). */
|
|
1332
1911
|
export interface RelationPage {
|
|
@@ -1366,7 +1945,40 @@ export interface RelationsDriverCapabilities {
|
|
|
1366
1945
|
* `truncationSignal` de los `list*` de roles.
|
|
1367
1946
|
*/
|
|
1368
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;
|
|
1369
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>;
|
|
1370
1982
|
/**
|
|
1371
1983
|
* Una página de `listObjects`/`listSubjects`/`enumerateRelations`. `truncated`
|
|
1372
1984
|
* dice si el backend cortó al tope (solo con `listObjectsTruncation`): un
|
|
@@ -1388,6 +2000,13 @@ export interface RelationTuple {
|
|
|
1388
2000
|
relation: string;
|
|
1389
2001
|
object: RelObject;
|
|
1390
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;
|
|
1391
2010
|
}
|
|
1392
2011
|
export interface RelationTuplePage {
|
|
1393
2012
|
tuples: RelationTuple[];
|
|
@@ -1398,7 +2017,34 @@ export interface RelationTuplePage {
|
|
|
1398
2017
|
* (`APP_SCOPE` es válida para mono-tenant): el aislamiento de tenant se corta
|
|
1399
2018
|
* por la partición, y el driver la serializa en el id del objeto y del
|
|
1400
2019
|
* userset. La whitelist de tipo/relación (F-05) la aplica el manager ANTES de
|
|
1401
|
-
* llamar al driver, pero el driver la re-valida por defensa en profundidad
|
|
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.
|
|
1402
2048
|
*/
|
|
1403
2049
|
export interface RelationsDriver {
|
|
1404
2050
|
readonly capabilities?: RelationsDriverCapabilities;
|
|
@@ -1412,10 +2058,10 @@ export interface RelationsDriver {
|
|
|
1412
2058
|
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef, page?: RelationPage): Promise<RelationObjectsPage>;
|
|
1413
2059
|
/** Los sujetos DIRECTOS de `relation` sobre `object` (holders y usersets). Nunca la membresía transitiva (eso es `membersOf`). */
|
|
1414
2060
|
listSubjects(relation: string, object: RelObject, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
|
|
1415
|
-
/** Borra todas las tuplas cuyo OBJETO es `object` y demuestra cero, o lanza 500 `E_AUTHZ_PURGE_INCOMPLETE` (invariante 11). */
|
|
1416
|
-
purgeObject(object: RelObject, partition: ScopeRef): Promise<void>;
|
|
1417
|
-
/** Borra todas las tuplas cuyo SUJETO es `subject` y demuestra cero, o lanza 500. */
|
|
1418
|
-
purgeSubject(subject: RelSubject, partition: ScopeRef): Promise<void>;
|
|
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>;
|
|
1419
2065
|
/**
|
|
1420
2066
|
* La membresía TRANSITIVA de un objeto-grupo: todos los holders que son
|
|
1421
2067
|
* `member` directa o a través de grupos anidados. DISTINTO de
|
|
@@ -1423,8 +2069,32 @@ export interface RelationsDriver {
|
|
|
1423
2069
|
* lo trae el driver con `membersOfNative: true`.
|
|
1424
2070
|
*/
|
|
1425
2071
|
membersOf?(object: RelObject, relation: string, partition: ScopeRef, page?: RelationPage): Promise<RelationSubjectsPage>;
|
|
1426
|
-
/** ORIGEN de `authz:reconcile` de relaciones: las tuplas paginadas, sin filtrar. Solo con `enumerateRelations: true`. */
|
|
2072
|
+
/** ORIGEN de `authz:reconcile` de relaciones: las tuplas paginadas, sin filtrar (la caducada LLEGA con su `expiresAt`). Solo con `enumerateRelations: true`. */
|
|
1427
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;
|
|
1428
2098
|
}
|
|
1429
2099
|
/**
|
|
1430
2100
|
* Factory de un `RelationsDriver` (Fase 4, lote 4-6) — el análogo de
|