@jantstack/adonis-authz 2.5.0-alpha.2 → 2.5.0-alpha.4
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 +743 -28
- package/build/commands/authz_catalog_diff.d.ts +0 -1
- package/build/commands/authz_catalog_diff.js +0 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts +0 -1
- package/build/commands/authz_catalog_prune_orphans.js +0 -1
- package/build/commands/authz_catalog_sync.d.ts +0 -1
- package/build/commands/authz_catalog_sync.js +0 -1
- package/build/commands/authz_freeze.d.ts +0 -1
- package/build/commands/authz_freeze.js +0 -1
- package/build/commands/authz_reconcile.d.ts +0 -1
- package/build/commands/authz_reconcile.js +0 -1
- package/build/commands/authz_relations_reconcile.d.ts +6 -1
- package/build/commands/authz_relations_reconcile.js +163 -5
- package/build/commands/authz_scopes_relay.d.ts +0 -1
- package/build/commands/authz_scopes_relay.js +49 -5
- package/build/commands/authz_scopes_requeue.d.ts +0 -1
- package/build/commands/authz_scopes_requeue.js +14 -2
- package/build/commands/authz_unfreeze.d.ts +0 -1
- package/build/commands/authz_unfreeze.js +0 -1
- package/build/commands/main.d.ts +0 -1
- package/build/commands/main.js +0 -1
- package/build/commands/openfga_provision.d.ts +72 -1
- package/build/commands/openfga_provision.js +236 -4
- package/build/configure.d.ts +0 -1
- package/build/configure.js +0 -1
- package/build/index.d.ts +11 -9
- package/build/index.js +12 -5
- package/build/providers/authz_provider.d.ts +0 -1
- package/build/providers/authz_provider.js +0 -1
- package/build/services/main.d.ts +0 -1
- package/build/services/main.js +0 -1
- package/build/services/relations.d.ts +0 -1
- package/build/services/relations.js +0 -1
- package/build/src/catalog/catalog.d.ts +0 -1
- package/build/src/catalog/catalog.js +0 -1
- package/build/src/catalog/catalog_cache.d.ts +13 -1
- package/build/src/catalog/catalog_cache.js +25 -1
- package/build/src/clock.d.ts +0 -1
- package/build/src/clock.js +0 -1
- package/build/src/define_config.d.ts +0 -1
- package/build/src/define_config.js +24 -1
- package/build/src/drivers/database_driver.d.ts +6 -1
- package/build/src/drivers/database_driver.js +22 -2
- package/build/src/drivers/database_relations_driver.d.ts +46 -5
- package/build/src/drivers/database_relations_driver.js +297 -59
- package/build/src/drivers/openfga_driver.d.ts +159 -26
- package/build/src/drivers/openfga_driver.js +980 -231
- package/build/src/drivers/openfga_facts.d.ts +112 -1
- package/build/src/drivers/openfga_facts.js +351 -11
- package/build/src/drivers/openfga_relations_driver.d.ts +109 -8
- package/build/src/drivers/openfga_relations_driver.js +817 -53
- package/build/src/errors.d.ts +233 -1
- package/build/src/errors.js +283 -1
- package/build/src/expiry.d.ts +51 -2
- package/build/src/expiry.js +96 -2
- package/build/src/freeze.d.ts +0 -1
- package/build/src/freeze.js +0 -1
- package/build/src/hierarchical_resolver.d.ts +0 -1
- package/build/src/hierarchical_resolver.js +0 -1
- package/build/src/http/app_access_middleware.d.ts +0 -1
- package/build/src/http/app_access_middleware.js +0 -1
- package/build/src/http/resource_access_middleware.d.ts +0 -1
- package/build/src/http/resource_access_middleware.js +0 -1
- package/build/src/identity.d.ts +2 -1
- package/build/src/identity.js +4 -1
- package/build/src/manager.d.ts +0 -1
- package/build/src/manager.js +164 -31
- package/build/src/memoize_ancestors.d.ts +0 -1
- package/build/src/memoize_ancestors.js +0 -1
- package/build/src/models/authz_assignment.d.ts +5 -6
- package/build/src/models/authz_assignment.js +0 -1
- package/build/src/models/authz_deny.d.ts +5 -6
- package/build/src/models/authz_deny.js +0 -1
- package/build/src/models/authz_permission.d.ts +5 -6
- package/build/src/models/authz_permission.js +0 -1
- package/build/src/models/authz_role.d.ts +5 -6
- package/build/src/models/authz_role.js +0 -1
- package/build/src/models/authz_role_permission.d.ts +5 -6
- package/build/src/models/authz_role_permission.js +0 -1
- package/build/src/openfga.d.ts +3 -4
- package/build/src/openfga.js +5 -3
- package/build/src/reconcile.d.ts +0 -1
- package/build/src/reconcile.js +0 -1
- package/build/src/relation_partition_trigger.d.ts +0 -1
- package/build/src/relation_partition_trigger.js +0 -1
- package/build/src/relations/check_many.d.ts +1 -2
- package/build/src/relations/check_many.js +6 -5
- package/build/src/relations/define_relations_config.d.ts +140 -5
- package/build/src/relations/define_relations_config.js +205 -6
- package/build/src/relations/deny_reach.d.ts +102 -0
- package/build/src/relations/deny_reach.js +230 -0
- package/build/src/relations/group_depth.d.ts +215 -0
- package/build/src/relations/group_depth.js +453 -0
- package/build/src/relations/manager.d.ts +46 -8
- package/build/src/relations/manager.js +178 -16
- package/build/src/relations/page.d.ts +19 -0
- package/build/src/relations/page.js +74 -0
- package/build/src/relations/reconcile.d.ts +274 -2
- package/build/src/relations/reconcile.js +1006 -162
- package/build/src/relations/tuple_key.d.ts +0 -1
- package/build/src/relations/tuple_key.js +0 -1
- package/build/src/relations/tuple_order.d.ts +127 -0
- package/build/src/relations/tuple_order.js +599 -0
- package/build/src/relations_config_store.d.ts +57 -2
- package/build/src/relations_config_store.js +155 -5
- package/build/src/relations_reconcile.d.ts +0 -1
- package/build/src/relations_reconcile.js +0 -1
- package/build/src/scope_outbox.d.ts +0 -1
- package/build/src/scope_outbox.js +0 -1
- package/build/src/shared/backend_guard.d.ts +60 -1
- package/build/src/shared/backend_guard.js +179 -2
- package/build/src/shared/sql_expiry.d.ts +0 -1
- package/build/src/shared/sql_expiry.js +0 -1
- package/build/src/shared/transaction_guard.d.ts +0 -1
- package/build/src/shared/transaction_guard.js +0 -1
- package/build/src/sql_descendants.d.ts +0 -1
- package/build/src/sql_descendants.js +0 -1
- package/build/src/testing/contract.d.ts +7 -1
- package/build/src/testing/contract.js +77 -1
- package/build/src/testing/main.d.ts +1 -2
- package/build/src/testing/main.js +1 -2
- package/build/src/testing/migration_contract.d.ts +0 -1
- package/build/src/testing/migration_contract.js +0 -1
- package/build/src/testing/relations_contract.d.ts +25 -1
- package/build/src/testing/relations_contract.js +996 -49
- package/build/src/testing/relations_reconcile_contract.d.ts +9 -1
- package/build/src/testing/relations_reconcile_contract.js +207 -1
- package/build/src/testing/scope_outbox_contract.d.ts +0 -1
- package/build/src/testing/scope_outbox_contract.js +0 -1
- package/build/src/testing/scope_tree.d.ts +0 -1
- package/build/src/testing/scope_tree.js +0 -1
- package/build/src/testing/test_clock.d.ts +0 -1
- package/build/src/testing/test_clock.js +0 -1
- package/build/src/traits/authz_scopes.d.ts +0 -1
- package/build/src/traits/authz_scopes.js +0 -1
- package/build/src/traits/has_uuid.d.ts +5 -6
- package/build/src/traits/has_uuid.js +0 -1
- package/build/src/types.d.ts +170 -15
- package/build/src/types.js +6 -1
- package/build/stubs/config/authorization.stub +22 -6
- package/build/stubs/main.d.ts +0 -1
- package/build/stubs/main.js +0 -1
- package/package.json +4 -3
- package/build/commands/authz_catalog_diff.d.ts.map +0 -1
- package/build/commands/authz_catalog_diff.js.map +0 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
- package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
- package/build/commands/authz_catalog_sync.d.ts.map +0 -1
- package/build/commands/authz_catalog_sync.js.map +0 -1
- package/build/commands/authz_freeze.d.ts.map +0 -1
- package/build/commands/authz_freeze.js.map +0 -1
- package/build/commands/authz_reconcile.d.ts.map +0 -1
- package/build/commands/authz_reconcile.js.map +0 -1
- package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
- package/build/commands/authz_relations_reconcile.js.map +0 -1
- package/build/commands/authz_scopes_relay.d.ts.map +0 -1
- package/build/commands/authz_scopes_relay.js.map +0 -1
- package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
- package/build/commands/authz_scopes_requeue.js.map +0 -1
- package/build/commands/authz_unfreeze.d.ts.map +0 -1
- package/build/commands/authz_unfreeze.js.map +0 -1
- package/build/commands/main.d.ts.map +0 -1
- package/build/commands/main.js.map +0 -1
- package/build/commands/openfga_provision.d.ts.map +0 -1
- package/build/commands/openfga_provision.js.map +0 -1
- package/build/configure.d.ts.map +0 -1
- package/build/configure.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/providers/authz_provider.d.ts.map +0 -1
- package/build/providers/authz_provider.js.map +0 -1
- package/build/services/main.d.ts.map +0 -1
- package/build/services/main.js.map +0 -1
- package/build/services/relations.d.ts.map +0 -1
- package/build/services/relations.js.map +0 -1
- package/build/src/catalog/catalog.d.ts.map +0 -1
- package/build/src/catalog/catalog.js.map +0 -1
- package/build/src/catalog/catalog_cache.d.ts.map +0 -1
- package/build/src/catalog/catalog_cache.js.map +0 -1
- package/build/src/clock.d.ts.map +0 -1
- package/build/src/clock.js.map +0 -1
- package/build/src/define_config.d.ts.map +0 -1
- package/build/src/define_config.js.map +0 -1
- package/build/src/drivers/database_driver.d.ts.map +0 -1
- package/build/src/drivers/database_driver.js.map +0 -1
- package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
- package/build/src/drivers/database_relations_driver.js.map +0 -1
- package/build/src/drivers/openfga_driver.d.ts.map +0 -1
- package/build/src/drivers/openfga_driver.js.map +0 -1
- package/build/src/drivers/openfga_facts.d.ts.map +0 -1
- package/build/src/drivers/openfga_facts.js.map +0 -1
- package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
- package/build/src/drivers/openfga_relations_driver.js.map +0 -1
- package/build/src/errors.d.ts.map +0 -1
- package/build/src/errors.js.map +0 -1
- package/build/src/expiry.d.ts.map +0 -1
- package/build/src/expiry.js.map +0 -1
- package/build/src/freeze.d.ts.map +0 -1
- package/build/src/freeze.js.map +0 -1
- package/build/src/hierarchical_resolver.d.ts.map +0 -1
- package/build/src/hierarchical_resolver.js.map +0 -1
- package/build/src/http/app_access_middleware.d.ts.map +0 -1
- package/build/src/http/app_access_middleware.js.map +0 -1
- package/build/src/http/resource_access_middleware.d.ts.map +0 -1
- package/build/src/http/resource_access_middleware.js.map +0 -1
- package/build/src/identity.d.ts.map +0 -1
- package/build/src/identity.js.map +0 -1
- package/build/src/manager.d.ts.map +0 -1
- package/build/src/manager.js.map +0 -1
- package/build/src/memoize_ancestors.d.ts.map +0 -1
- package/build/src/memoize_ancestors.js.map +0 -1
- package/build/src/models/authz_assignment.d.ts.map +0 -1
- package/build/src/models/authz_assignment.js.map +0 -1
- package/build/src/models/authz_deny.d.ts.map +0 -1
- package/build/src/models/authz_deny.js.map +0 -1
- package/build/src/models/authz_permission.d.ts.map +0 -1
- package/build/src/models/authz_permission.js.map +0 -1
- package/build/src/models/authz_role.d.ts.map +0 -1
- package/build/src/models/authz_role.js.map +0 -1
- package/build/src/models/authz_role_permission.d.ts.map +0 -1
- package/build/src/models/authz_role_permission.js.map +0 -1
- package/build/src/openfga.d.ts.map +0 -1
- package/build/src/openfga.js.map +0 -1
- package/build/src/reconcile.d.ts.map +0 -1
- package/build/src/reconcile.js.map +0 -1
- package/build/src/relation_partition_trigger.d.ts.map +0 -1
- package/build/src/relation_partition_trigger.js.map +0 -1
- package/build/src/relations/check_many.d.ts.map +0 -1
- package/build/src/relations/check_many.js.map +0 -1
- package/build/src/relations/define_relations_config.d.ts.map +0 -1
- package/build/src/relations/define_relations_config.js.map +0 -1
- package/build/src/relations/manager.d.ts.map +0 -1
- package/build/src/relations/manager.js.map +0 -1
- package/build/src/relations/reconcile.d.ts.map +0 -1
- package/build/src/relations/reconcile.js.map +0 -1
- package/build/src/relations/tuple_key.d.ts.map +0 -1
- package/build/src/relations/tuple_key.js.map +0 -1
- package/build/src/relations_config_store.d.ts.map +0 -1
- package/build/src/relations_config_store.js.map +0 -1
- package/build/src/relations_reconcile.d.ts.map +0 -1
- package/build/src/relations_reconcile.js.map +0 -1
- package/build/src/scope_outbox.d.ts.map +0 -1
- package/build/src/scope_outbox.js.map +0 -1
- package/build/src/shared/backend_guard.d.ts.map +0 -1
- package/build/src/shared/backend_guard.js.map +0 -1
- package/build/src/shared/sql_expiry.d.ts.map +0 -1
- package/build/src/shared/sql_expiry.js.map +0 -1
- package/build/src/shared/transaction_guard.d.ts.map +0 -1
- package/build/src/shared/transaction_guard.js.map +0 -1
- package/build/src/sql_descendants.d.ts.map +0 -1
- package/build/src/sql_descendants.js.map +0 -1
- package/build/src/testing/contract.d.ts.map +0 -1
- package/build/src/testing/contract.js.map +0 -1
- package/build/src/testing/main.d.ts.map +0 -1
- package/build/src/testing/main.js.map +0 -1
- package/build/src/testing/migration_contract.d.ts.map +0 -1
- package/build/src/testing/migration_contract.js.map +0 -1
- package/build/src/testing/relations_contract.d.ts.map +0 -1
- package/build/src/testing/relations_contract.js.map +0 -1
- package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
- package/build/src/testing/relations_reconcile_contract.js.map +0 -1
- package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
- package/build/src/testing/scope_outbox_contract.js.map +0 -1
- package/build/src/testing/scope_tree.d.ts.map +0 -1
- package/build/src/testing/scope_tree.js.map +0 -1
- package/build/src/testing/test_clock.d.ts.map +0 -1
- package/build/src/testing/test_clock.js.map +0 -1
- package/build/src/traits/authz_scopes.d.ts.map +0 -1
- package/build/src/traits/authz_scopes.js.map +0 -1
- package/build/src/traits/has_uuid.d.ts.map +0 -1
- package/build/src/traits/has_uuid.js.map +0 -1
- package/build/src/types.d.ts.map +0 -1
- package/build/src/types.js.map +0 -1
- package/build/stubs/main.d.ts.map +0 -1
- package/build/stubs/main.js.map +0 -1
package/build/src/errors.d.ts
CHANGED
|
@@ -41,6 +41,18 @@ export declare class AuthorizationBackendTimeoutError extends AuthorizationBacke
|
|
|
41
41
|
static code: string;
|
|
42
42
|
constructor(driver: string, operation: string, timeoutMs: number, cause?: unknown);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* **El grafo excede lo que el motor resuelve** (lote «antes de RC» · R3, decisión del dueño 2026-10-17 (4)): OpenFGA
|
|
46
|
+
* respondió `authorization_model_resolution_too_complex` —una cadena de scopes o una anidación de grupos más profunda que
|
|
47
|
+
* su `--resolve-node-limit`—. Es un `AuthorizationBackendError` (503, se maneja igual y el relay lo trata como
|
|
48
|
+
* transitorio) con código propio, porque NO es una caída: en el BORDE es probabilístico (a 23 saltos falla entre un 4 % y
|
|
49
|
+
* un 26 % según la carga, medido en 3b-2e) y un reintento puede acertar, pero por ENCIMA de la cota falla siempre. Las
|
|
50
|
+
* cotas están en la tabla «Limits» del README (`FACTS_MAX_RESOLVE_DEPTH`). Nunca un `false`: deniega, pero ruidoso.
|
|
51
|
+
*/
|
|
52
|
+
export declare class ResolutionLimitError extends AuthorizationBackendError {
|
|
53
|
+
static code: string;
|
|
54
|
+
constructor(driver: string, operation: string, cause?: unknown);
|
|
55
|
+
}
|
|
44
56
|
/**
|
|
45
57
|
* El resolutor de ancestros del consumidor lanzó. Es una dependencia más de
|
|
46
58
|
* cada pregunta (el árbol de scopes), así que su caída se clasifica como la
|
|
@@ -78,6 +90,17 @@ export declare class InvalidSlugError extends Exception {
|
|
|
78
90
|
export declare class AuthorizationConfigError extends Exception {
|
|
79
91
|
static status: number;
|
|
80
92
|
static code: string;
|
|
93
|
+
/**
|
|
94
|
+
* **Un deny CONDICIONAL ya en el store que `openfga` no puede reemplazar** (plan 2.5 · lote D · cierre 1 de D2, 🟡 4).
|
|
95
|
+
* El deny del paquete no caduca (D3): una tupla `denied_<rel>` CON condición solo puede venir de fuera (escrita a
|
|
96
|
+
* mano). Re-escribirla sin condición es 409 «already exists with a different condition» (medido en v1.19.0; M5: FGA
|
|
97
|
+
* no reemplaza la misma clave en un `Write`), que hasta el cierre salía como 503 «transitorio» y se repetía para
|
|
98
|
+
* siempre — y el deny CADUCABA. Ahora es 500 con la receta; `denyConditional: true` deja que el reconcile lo
|
|
99
|
+
* clasifique como residuo `deny-conditional` del destino (nunca 503 repetido).
|
|
100
|
+
*/
|
|
101
|
+
static conditionalDeny(operation: string, tuple: string): AuthorizationConfigError & {
|
|
102
|
+
denyConditional: true;
|
|
103
|
+
};
|
|
81
104
|
/**
|
|
82
105
|
* **`assertWrite` devolvió una promesa** (2.4.0-alpha.3 · D3, hallazgo H2).
|
|
83
106
|
* `assertWrite(ref): void` es SÍNCRONO y puro a propósito (R-13), pero
|
|
@@ -124,6 +147,33 @@ export declare function isThenable(value: unknown): boolean;
|
|
|
124
147
|
export declare function markPartialWrite<E extends object>(error: E): E & {
|
|
125
148
|
partialWrite: true;
|
|
126
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* **Marca de fallo de DESPLIEGUE** (lote «antes de RC» · cierre de L11, 🟠 A1 del auditor = H1; adelanta SOLO esta pieza
|
|
152
|
+
* de la decisión del dueño 2026-10-17 (3), que R3 generaliza con `classifyFgaFailure`): el error dice algo del
|
|
153
|
+
* DESPLIEGUE —el modelo fijado no es el que genera la config (la puerta L11), o el generador no puede generar con él—,
|
|
154
|
+
* no de la entrada que se estaba aplicando. Para el relay es TRANSITORIO (`markFailed(…, { transient: true })`: no
|
|
155
|
+
* consume intento ni aparca): aparcar un `moved`/`detached` por un despliegue a medias era fail-open PERSISTENTE (los
|
|
156
|
+
* admins de la org vieja conservaban el acceso hasta un `requeue` manual). Para el llamante sigue siendo lo que es
|
|
157
|
+
* (500 `E_AUTHZ_CONFIG`, «no es transitorio»): la marca es una propiedad, no una clase, y no cambia ni `status` ni
|
|
158
|
+
* `code`. Interna: no se exporta por `index.ts` (R3 decide la forma pública).
|
|
159
|
+
*/
|
|
160
|
+
export declare function markDeploymentFailure<E extends object>(error: E): E & {
|
|
161
|
+
deploymentFailure: true;
|
|
162
|
+
};
|
|
163
|
+
/** ¿El error viene marcado como fallo de despliegue (`markDeploymentFailure`)? */
|
|
164
|
+
export declare function isDeploymentFailure(error: unknown): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* **¿Es un fallo TRANSITORIO para el relay?** (no consume intento ni aparca; `markFailed(…, { transient: true })`).
|
|
167
|
+
* PUNTO DE PASO ÚNICO de la regla (el manager la llama en cada fallo de la pasada; la guardia `fga.errors.classify` la
|
|
168
|
+
* juzga sobre las formas medidas). Transitorio: un 503 del backend (`AuthorizationBackendError` y subclases —timeout,
|
|
169
|
+
* `E_AUTHZ_RESOLUTION_LIMIT`—, o cualquier `status` 503), un fallo marcado de DESPLIEGUE (`markDeploymentFailure`) y
|
|
170
|
+
* **una CARRERA que escapa** (409 `E_AUTHZ_WRITE_CONFLICT`: otro escritor —un `grant` concurrente sobre la misma
|
|
171
|
+
* arista `scope#binding` del barrido, o la contención del árbol de `reparent`— llegó antes). Cierre de R3 (🟠 R3-A1 del
|
|
172
|
+
* auditor, decisión (3) firmada «carrera ⇒ como hoy»: antes de R3 la carrera que escapaba del guard era un 503
|
|
173
|
+
* transitorio): para el LLAMANTE sigue siendo 409; para el relay, aparcar por contención era H1 (un `moved` aparcado
|
|
174
|
+
* deja el padre viejo concediendo y tapona su clave). Lo demás —422, 500 sin marca, error sin `status`— cuenta intento.
|
|
175
|
+
*/
|
|
176
|
+
export declare function isTransientRelayFailure(error: unknown): boolean;
|
|
127
177
|
/** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
|
|
128
178
|
export declare function isPartialWrite(error: unknown): boolean;
|
|
129
179
|
/**
|
|
@@ -331,6 +381,54 @@ export declare class WriteConflictError extends Exception {
|
|
|
331
381
|
static status: number;
|
|
332
382
|
static code: string;
|
|
333
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* **Renovar esta membresía abriría un hueco en un deny** (plan 2.5 · lote D · cierre 2 de D2, 🟠 N1; decisión del dueño
|
|
386
|
+
* 2026-10-15 (9)): `relate` sobre una membresía `group:*#member` EXISTENTE con otra caducidad, en un driver que renueva
|
|
387
|
+
* con delete + write (`atomicRenewal: false`, `openfga`: FGA no reemplaza la misma clave en un `Write`, M5), cuando ese
|
|
388
|
+
* grupo —o uno por encima de él— es sujeto de un deny vivo de la partición. Entre los dos `Write` el sujeto no tendría el
|
|
389
|
+
* deny y CONCEDERÍA; si el 2.º fallara, para siempre. 409 y CERO escrituras: el estado del store no admite esta
|
|
390
|
+
* escritura tal cual. **Siempre cero escrituras** (cierre 4 · ⚪ C3-3): la renovación que SÍ se aplicó y en cuya
|
|
391
|
+
* ventana cayó un deny concurrente es `RenewalRacedError` (`E_AUTHZ_RENEWAL_RACED`), con otra receta.
|
|
392
|
+
*
|
|
393
|
+
* **La receta, honesta** (cierre 5 de D2, decisión del dueño §2026-10-16 (10), opción A): en `openfga` NO hay forma sin
|
|
394
|
+
* ventana de cambiar esa caducidad —FGA no tiene upsert, openfga/openfga#2151—. (1) Patrón recomendado: membresías SIN
|
|
395
|
+
* caducidad en los grupos con deny, que se terminan con `unrelate`. (2) Si hay que cambiarla: `unrelate` + `relate`
|
|
396
|
+
* ACEPTANDO la ventana, dicho como tal. Hasta el cierre 5 el mensaje ofrecía además «retira antes el deny, renueva y
|
|
397
|
+
* vuelve a escribirlo» como si fuera la salida segura: también abre la ventana (sin el deny, el sujeto concede). Cuando
|
|
398
|
+
* OpenFGA ofrezca upsert, la renovación será UNA escritura y `atomicRenewal` pasará a `true`.
|
|
399
|
+
*/
|
|
400
|
+
export declare class RenewalUnsafeError extends Exception {
|
|
401
|
+
static status: number;
|
|
402
|
+
static code: string;
|
|
403
|
+
static carrier(membership: string, denied: string): RenewalUnsafeError;
|
|
404
|
+
/**
|
|
405
|
+
* **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
|
|
406
|
+
* del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
|
|
407
|
+
* (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
|
|
408
|
+
* (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
|
|
409
|
+
* con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
|
|
410
|
+
*/
|
|
411
|
+
static foreignCondition(operation: string, tuple: string, detail: string): RenewalUnsafeError;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
|
|
415
|
+
* cierre 4 · ⚪ C3-3): la renovación (dos `Write`) de una membresía cuyo grupo NO transportaba ningún deny al
|
|
416
|
+
* comprobarlo, y la RE-COMPROBACIÓN tras los dos `Write` encuentra uno —un `deny` concurrente entró entre la comprobación
|
|
417
|
+
* y la renovación—. Estado final correcto (la membresía está y el deny vuelve a alcanzar al sujeto), pero durante la
|
|
418
|
+
* ventana entre los dos `Write` el deny ya confirmado NO alcanzó al sujeto. Va marcado parcial (`markPartialWrite`: el
|
|
419
|
+
* manager notifica `indeterminate: true`) y lleva `carriedDeny` y `renewalApplied: true`.
|
|
420
|
+
*
|
|
421
|
+
* Hasta el cierre 4 era el MISMO 409 `E_AUTHZ_RENEWAL_UNSAFE` («cero escrituras, aplica la receta»): un consumidor que
|
|
422
|
+
* automatizara esa receta (`unrelate` + `relate`) ante este caso reabría la ventana con el deny YA dentro del grupo. Por
|
|
423
|
+
* eso tiene su código y su receta, que NO es esa.
|
|
424
|
+
*/
|
|
425
|
+
export declare class RenewalRacedError extends Exception {
|
|
426
|
+
static status: number;
|
|
427
|
+
static code: string;
|
|
428
|
+
carriedDeny: string;
|
|
429
|
+
renewalApplied: true;
|
|
430
|
+
static raced(membership: string, denied: string): RenewalRacedError;
|
|
431
|
+
}
|
|
334
432
|
/**
|
|
335
433
|
* **El lote de `checkMany` no tiene la forma admitida** (plan 2.5 · lote A,
|
|
336
434
|
* P4): vacío, o por encima de `RELATIONS_CHECK_MANY_MAX` (50). Es 422 con
|
|
@@ -444,6 +542,31 @@ export declare class ScopeDepthLimitError extends Exception {
|
|
|
444
542
|
server: string;
|
|
445
543
|
});
|
|
446
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
|
|
547
|
+
* 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
|
|
548
|
+
* LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
|
|
549
|
+
* un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
|
|
550
|
+
* es el grafo el que no cabe. Gemelo de `E_AUTHZ_SCOPE_DEPTH_LIMIT` y con su misma regla de letra (alpha.8, 7.2): el
|
|
551
|
+
* `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
|
|
552
|
+
* backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
|
|
553
|
+
* en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
|
|
554
|
+
*/
|
|
555
|
+
export declare class GroupDepthLimitError extends Exception {
|
|
556
|
+
static status: number;
|
|
557
|
+
static code: string;
|
|
558
|
+
readonly details: {
|
|
559
|
+
reason: 'too-deep' | 'cycle' | 'too-many-groups';
|
|
560
|
+
edge: string;
|
|
561
|
+
depth: number | null;
|
|
562
|
+
maxGroupDepth: number;
|
|
563
|
+
/** Solo con `too-many-groups`: qué cota se pasó (grupos, requests, tuplas leídas o el deadline total). */
|
|
564
|
+
budget?: 'nodes' | 'reads' | 'tuples' | 'deadline';
|
|
565
|
+
why: string;
|
|
566
|
+
server: string;
|
|
567
|
+
};
|
|
568
|
+
constructor(message: string, details: GroupDepthLimitError['details']);
|
|
569
|
+
}
|
|
447
570
|
/**
|
|
448
571
|
* Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
|
|
449
572
|
* activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
|
|
@@ -480,6 +603,33 @@ export declare class UnsupportedOperationError extends Exception {
|
|
|
480
603
|
* entre procesos (invariante 14); moverla al commit del consumidor lo anula.
|
|
481
604
|
*/
|
|
482
605
|
static transactionalCatalog(operation: string): UnsupportedOperationError;
|
|
606
|
+
/**
|
|
607
|
+
* **Los `list*` de relaciones no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez, decisión del dueño
|
|
608
|
+
* 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
|
|
609
|
+
* hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
|
|
610
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
|
|
611
|
+
* una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
|
|
612
|
+
* como métodos aparte (`*Page`, 2.6) con capacidad propia.
|
|
613
|
+
*/
|
|
614
|
+
static listPage(operation: string, driver: string): UnsupportedOperationError;
|
|
615
|
+
/**
|
|
616
|
+
* **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
|
|
617
|
+
* devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
|
|
618
|
+
* llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
|
|
619
|
+
*/
|
|
620
|
+
static enumerateCursor(operation: string, driver: string): UnsupportedOperationError;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
|
|
624
|
+
* `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
|
|
625
|
+
* `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
|
|
626
|
+
* pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
|
|
627
|
+
* `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
|
|
628
|
+
* encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
|
|
629
|
+
*/
|
|
630
|
+
export declare class InvalidRelationPageError extends Exception {
|
|
631
|
+
static status: number;
|
|
632
|
+
static code: string;
|
|
483
633
|
}
|
|
484
634
|
/**
|
|
485
635
|
* Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
|
|
@@ -832,4 +982,86 @@ export declare class RelationUsersetUnsupportedError extends Exception {
|
|
|
832
982
|
static status: number;
|
|
833
983
|
static code: string;
|
|
834
984
|
}
|
|
835
|
-
|
|
985
|
+
/**
|
|
986
|
+
* **Deny sobre algo que no se puede denegar** (plan 2.5 · lote D · D1; panel
|
|
987
|
+
* tenencia ronda 2, D10/D-17). `deny(…)` sobre un tipo cuya spec no declara
|
|
988
|
+
* `deny: 'write'` (fase 2 del opt-in: sin opt-in, o en `deny: 'read'`, la fase
|
|
989
|
+
* en la que el modelo y las lecturas ya honran el deny pero aún no se aceptan
|
|
990
|
+
* escrituras), sobre el tipo built-in `group`, o con el comodín `*` como
|
|
991
|
+
* sujeto. 422 ANTES de tocar el driver (cero llamadas, cero eventos): una
|
|
992
|
+
* pregunta inválida, no una caída.
|
|
993
|
+
*/
|
|
994
|
+
export declare class RelationNotDenyableError extends Exception {
|
|
995
|
+
static status: number;
|
|
996
|
+
static code: string;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* **`expiresAt` en un deny** (plan 2.5 · lote D · D3, decisión del dueño
|
|
1000
|
+
* 2026-10-14): el deny de relaciones NO caduca en v1. Medido (M5, OpenFGA
|
|
1001
|
+
* v1.19.0): FGA rechaza delete+write de la misma clave en un `Write` (400
|
|
1002
|
+
* `cannot_allow_duplicate_tuples_in_one_request`) y `on_duplicate: ignore` con
|
|
1003
|
+
* otra condición es 409, así que cambiar la caducidad de un deny VIVO exige dos
|
|
1004
|
+
* `Write` con una ventana en la que deja de bloquear — y si el segundo cae, el
|
|
1005
|
+
* deny DESAPARECE. 422 ANTES de tocar el driver, con la razón medida.
|
|
1006
|
+
*/
|
|
1007
|
+
export declare class RelationDenyExpiryError extends Exception {
|
|
1008
|
+
static status: number;
|
|
1009
|
+
static code: string;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* **`membersOf` de `database` sobre un objeto con deny y más candidatos que la cota** (lote D · cierre 1 de D1, 🟡 4 del
|
|
1013
|
+
* auditor). Con una fila `denied_*` vigente en el objeto, cada candidato se juzga con un `check` (el corte de camino);
|
|
1014
|
+
* sin cota eran N consultas en serie —una amplificación barata para quien puede escribir un deny—. Pasada
|
|
1015
|
+
* `RELATIONS_MEMBERS_DENY_CHECK_MAX` (1 000) ⇒ 500, NUNCA una lista parcial; el deadline TOTAL del bucle es 503
|
|
1016
|
+
* `E_AUTHZ_BACKEND_TIMEOUT`. **500 y no 422** (decisión 1 del cierre 1, ratificada en el cierre 2): no es una pregunta
|
|
1017
|
+
* inválida del llamante —la misma pregunta es válida con menos candidatos— sino una cota del DESPLIEGUE (precedente
|
|
1018
|
+
* `E_AUTHZ_TOO_MANY_LOCAL_ROLES`); y no 503 porque no es reintentable. `authorizedScopes` (`E_AUTHZ_TOO_MANY_SCOPES`,
|
|
1019
|
+
* 422) acota una enumeración que el llamante pide con su `maxScopes`; aquí no hay cota del llamante.
|
|
1020
|
+
*/
|
|
1021
|
+
export declare class MembersOfDenyLimitError extends Exception {
|
|
1022
|
+
static status: number;
|
|
1023
|
+
static code: string;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* **`authz:relations:reconcile` se NIEGA a migrar denies** (plan 2.5 · lote D · D2; auditor R2-5, tester S6, juez ronda 2
|
|
1027
|
+
* fila D). El origen trae al menos un deny vivo y el destino no puede escribirlo: no declara `relationDenies` (o no trae
|
|
1028
|
+
* `deny`/`removeDeny`/`denyPhase`), el tipo no está en la fase `'write'` del opt-in EN EL DESTINO (su config, no la spec
|
|
1029
|
+
* persistida), el par tipo/relación no está declarado allí, su modelo fijado no declara `denied_<rel>` (D-31), o el
|
|
1030
|
+
* origen enumera un deny SIN marcar `kind` (`unmarked`). Se decide en la FASE DE PLAN, antes de la primera escritura en
|
|
1031
|
+
* cualquier partición: **CERO escrituras**, y **jamás `skipped`** — perder un deny no es una pérdida que se pueda declarar
|
|
1032
|
+
* (escribir los allows sin él concede lo que el origen niega). `--dry-run` no lanza: lo marca en `report.denyRefused` y es
|
|
1033
|
+
* deriva. Lleva `refusals` (una entrada por tipo/relación y motivo). 500 y no 422: no es una pregunta del llamante sino
|
|
1034
|
+
* el estado del despliegue (precedente `E_AUTHZ_MASS_RECONCILE_REFUSED`).
|
|
1035
|
+
*/
|
|
1036
|
+
export declare class DenyMigrationRefusedError extends Exception {
|
|
1037
|
+
static status: number;
|
|
1038
|
+
static code: string;
|
|
1039
|
+
refusals: ReadonlyArray<{
|
|
1040
|
+
objectType: string;
|
|
1041
|
+
relation: string;
|
|
1042
|
+
reason: string;
|
|
1043
|
+
message: string;
|
|
1044
|
+
}>;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
|
|
1048
|
+
* decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
|
|
1049
|
+
* vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
|
|
1050
|
+
* ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
|
|
1051
|
+
* línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
|
|
1052
|
+
* levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
|
|
1053
|
+
* precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
|
|
1054
|
+
* estado del despliegue.
|
|
1055
|
+
*/
|
|
1056
|
+
export declare class DenyReachRefusedError extends Exception {
|
|
1057
|
+
static status: number;
|
|
1058
|
+
static code: string;
|
|
1059
|
+
findings: ReadonlyArray<{
|
|
1060
|
+
objectType: string;
|
|
1061
|
+
relation: string;
|
|
1062
|
+
reason: string;
|
|
1063
|
+
count: number;
|
|
1064
|
+
lost: string[];
|
|
1065
|
+
recipe: string;
|
|
1066
|
+
}>;
|
|
1067
|
+
}
|
package/build/src/errors.js
CHANGED
|
@@ -45,6 +45,27 @@ export class AuthorizationBackendTimeoutError extends AuthorizationBackendError
|
|
|
45
45
|
this.message = `El backend de autorización '${driver}' no respondió en ${timeoutMs} ms (${operation})`;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* **El grafo excede lo que el motor resuelve** (lote «antes de RC» · R3, decisión del dueño 2026-10-17 (4)): OpenFGA
|
|
50
|
+
* respondió `authorization_model_resolution_too_complex` —una cadena de scopes o una anidación de grupos más profunda que
|
|
51
|
+
* su `--resolve-node-limit`—. Es un `AuthorizationBackendError` (503, se maneja igual y el relay lo trata como
|
|
52
|
+
* transitorio) con código propio, porque NO es una caída: en el BORDE es probabilístico (a 23 saltos falla entre un 4 % y
|
|
53
|
+
* un 26 % según la carga, medido en 3b-2e) y un reintento puede acertar, pero por ENCIMA de la cota falla siempre. Las
|
|
54
|
+
* cotas están en la tabla «Limits» del README (`FACTS_MAX_RESOLVE_DEPTH`). Nunca un `false`: deniega, pero ruidoso.
|
|
55
|
+
*/
|
|
56
|
+
export class ResolutionLimitError extends AuthorizationBackendError {
|
|
57
|
+
static code = 'E_AUTHZ_RESOLUTION_LIMIT';
|
|
58
|
+
constructor(driver, operation, cause) {
|
|
59
|
+
super(driver, operation, cause);
|
|
60
|
+
this.message =
|
|
61
|
+
`El backend de autorización '${driver}' no pudo resolver ${operation}: el grafo (la cadena de scopes o la ` +
|
|
62
|
+
'anidación de grupos) excede lo que OpenFGA resuelve (resolution_too_complex). En el borde es probabilístico y un ' +
|
|
63
|
+
'reintento puede acertar; por encima de la cota falla siempre: ver «Limits» en el README (FACTS_MAX_RESOLVE_DEPTH ' +
|
|
64
|
+
'para la cadena de scopes; maxGroupDepth (defineRelationsConfig) para la anidación de grupos —el driver ' +
|
|
65
|
+
'openfga lo calibra contra el servidor, 22 en v1.19 por defecto—; el mando del servidor es OPENFGA_RESOLVE_NODE_LIMIT / --resolve-node-limit). ' +
|
|
66
|
+
'Nunca es un «sin permiso».';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
48
69
|
/**
|
|
49
70
|
* El resolutor de ancestros del consumidor lanzó. Es una dependencia más de
|
|
50
71
|
* cada pregunta (el árbol de scopes), así que su caída se clasifica como la
|
|
@@ -84,6 +105,24 @@ export class InvalidSlugError extends Exception {
|
|
|
84
105
|
export class AuthorizationConfigError extends Exception {
|
|
85
106
|
static status = 500;
|
|
86
107
|
static code = 'E_AUTHZ_CONFIG';
|
|
108
|
+
/**
|
|
109
|
+
* **Un deny CONDICIONAL ya en el store que `openfga` no puede reemplazar** (plan 2.5 · lote D · cierre 1 de D2, 🟡 4).
|
|
110
|
+
* El deny del paquete no caduca (D3): una tupla `denied_<rel>` CON condición solo puede venir de fuera (escrita a
|
|
111
|
+
* mano). Re-escribirla sin condición es 409 «already exists with a different condition» (medido en v1.19.0; M5: FGA
|
|
112
|
+
* no reemplaza la misma clave en un `Write`), que hasta el cierre salía como 503 «transitorio» y se repetía para
|
|
113
|
+
* siempre — y el deny CADUCABA. Ahora es 500 con la receta; `denyConditional: true` deja que el reconcile lo
|
|
114
|
+
* clasifique como residuo `deny-conditional` del destino (nunca 503 repetido).
|
|
115
|
+
*/
|
|
116
|
+
static conditionalDeny(operation, tuple) {
|
|
117
|
+
const error = new AuthorizationConfigError(`${operation}: en el store ya hay un deny CONDICIONAL (con caducidad) sobre ${tuple}. El deny del paquete no caduca ` +
|
|
118
|
+
'(D3), así que esa tupla se escribió desde fuera del paquete; OpenFGA no puede REEMPLAZAR la misma clave en un ' +
|
|
119
|
+
'Write (409 «already exists with a different condition», M5), así que este driver no puede rehacerlo permanente ' +
|
|
120
|
+
'y repetir no sirve: no es una caída. Mientras viva niega; cuando caduque concederá lo que cortaba. Receta: ' +
|
|
121
|
+
'removeDeny + deny (entre los dos hay una VENTANA en la que el deny no está y lo que cortaba se concede: hazlo ' +
|
|
122
|
+
'con la flota sin tráfico sobre ese objeto o asúmela). No se ha escrito nada.');
|
|
123
|
+
error.denyConditional = true;
|
|
124
|
+
return error;
|
|
125
|
+
}
|
|
87
126
|
/**
|
|
88
127
|
* **`assertWrite` devolvió una promesa** (2.4.0-alpha.3 · D3, hallazgo H2).
|
|
89
128
|
* `assertWrite(ref): void` es SÍNCRONO y puro a propósito (R-13), pero
|
|
@@ -159,6 +198,52 @@ export function markPartialWrite(error) {
|
|
|
159
198
|
}
|
|
160
199
|
return error;
|
|
161
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* **Marca de fallo de DESPLIEGUE** (lote «antes de RC» · cierre de L11, 🟠 A1 del auditor = H1; adelanta SOLO esta pieza
|
|
203
|
+
* de la decisión del dueño 2026-10-17 (3), que R3 generaliza con `classifyFgaFailure`): el error dice algo del
|
|
204
|
+
* DESPLIEGUE —el modelo fijado no es el que genera la config (la puerta L11), o el generador no puede generar con él—,
|
|
205
|
+
* no de la entrada que se estaba aplicando. Para el relay es TRANSITORIO (`markFailed(…, { transient: true })`: no
|
|
206
|
+
* consume intento ni aparca): aparcar un `moved`/`detached` por un despliegue a medias era fail-open PERSISTENTE (los
|
|
207
|
+
* admins de la org vieja conservaban el acceso hasta un `requeue` manual). Para el llamante sigue siendo lo que es
|
|
208
|
+
* (500 `E_AUTHZ_CONFIG`, «no es transitorio»): la marca es una propiedad, no una clase, y no cambia ni `status` ni
|
|
209
|
+
* `code`. Interna: no se exporta por `index.ts` (R3 decide la forma pública).
|
|
210
|
+
*/
|
|
211
|
+
export function markDeploymentFailure(error) {
|
|
212
|
+
try {
|
|
213
|
+
Object.defineProperty(error, 'deploymentFailure', { value: true, enumerable: false, configurable: true });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Un error congelado no admite la marca: se propaga sin ella (cuenta intento, como antes de la marca).
|
|
217
|
+
}
|
|
218
|
+
return error;
|
|
219
|
+
}
|
|
220
|
+
/** ¿El error viene marcado como fallo de despliegue (`markDeploymentFailure`)? */
|
|
221
|
+
export function isDeploymentFailure(error) {
|
|
222
|
+
return error !== null && typeof error === 'object' && error.deploymentFailure === true;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* **¿Es un fallo TRANSITORIO para el relay?** (no consume intento ni aparca; `markFailed(…, { transient: true })`).
|
|
226
|
+
* PUNTO DE PASO ÚNICO de la regla (el manager la llama en cada fallo de la pasada; la guardia `fga.errors.classify` la
|
|
227
|
+
* juzga sobre las formas medidas). Transitorio: un 503 del backend (`AuthorizationBackendError` y subclases —timeout,
|
|
228
|
+
* `E_AUTHZ_RESOLUTION_LIMIT`—, o cualquier `status` 503), un fallo marcado de DESPLIEGUE (`markDeploymentFailure`) y
|
|
229
|
+
* **una CARRERA que escapa** (409 `E_AUTHZ_WRITE_CONFLICT`: otro escritor —un `grant` concurrente sobre la misma
|
|
230
|
+
* arista `scope#binding` del barrido, o la contención del árbol de `reparent`— llegó antes). Cierre de R3 (🟠 R3-A1 del
|
|
231
|
+
* auditor, decisión (3) firmada «carrera ⇒ como hoy»: antes de R3 la carrera que escapaba del guard era un 503
|
|
232
|
+
* transitorio): para el LLAMANTE sigue siendo 409; para el relay, aparcar por contención era H1 (un `moved` aparcado
|
|
233
|
+
* deja el padre viejo concediendo y tapona su clave). Lo demás —422, 500 sin marca, error sin `status`— cuenta intento.
|
|
234
|
+
*/
|
|
235
|
+
export function isTransientRelayFailure(error) {
|
|
236
|
+
if (error instanceof AuthorizationBackendError)
|
|
237
|
+
return true;
|
|
238
|
+
if (isDeploymentFailure(error))
|
|
239
|
+
return true;
|
|
240
|
+
if (typeof error !== 'object' || error === null)
|
|
241
|
+
return false;
|
|
242
|
+
const { status, code } = error;
|
|
243
|
+
if (code === WriteConflictError.code)
|
|
244
|
+
return true;
|
|
245
|
+
return status === 503;
|
|
246
|
+
}
|
|
162
247
|
/** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
|
|
163
248
|
export function isPartialWrite(error) {
|
|
164
249
|
return error !== null && typeof error === 'object' && error.partialWrite === true;
|
|
@@ -382,6 +467,75 @@ export class WriteConflictError extends Exception {
|
|
|
382
467
|
static status = 409;
|
|
383
468
|
static code = 'E_AUTHZ_WRITE_CONFLICT';
|
|
384
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* **Renovar esta membresía abriría un hueco en un deny** (plan 2.5 · lote D · cierre 2 de D2, 🟠 N1; decisión del dueño
|
|
472
|
+
* 2026-10-15 (9)): `relate` sobre una membresía `group:*#member` EXISTENTE con otra caducidad, en un driver que renueva
|
|
473
|
+
* con delete + write (`atomicRenewal: false`, `openfga`: FGA no reemplaza la misma clave en un `Write`, M5), cuando ese
|
|
474
|
+
* grupo —o uno por encima de él— es sujeto de un deny vivo de la partición. Entre los dos `Write` el sujeto no tendría el
|
|
475
|
+
* deny y CONCEDERÍA; si el 2.º fallara, para siempre. 409 y CERO escrituras: el estado del store no admite esta
|
|
476
|
+
* escritura tal cual. **Siempre cero escrituras** (cierre 4 · ⚪ C3-3): la renovación que SÍ se aplicó y en cuya
|
|
477
|
+
* ventana cayó un deny concurrente es `RenewalRacedError` (`E_AUTHZ_RENEWAL_RACED`), con otra receta.
|
|
478
|
+
*
|
|
479
|
+
* **La receta, honesta** (cierre 5 de D2, decisión del dueño §2026-10-16 (10), opción A): en `openfga` NO hay forma sin
|
|
480
|
+
* ventana de cambiar esa caducidad —FGA no tiene upsert, openfga/openfga#2151—. (1) Patrón recomendado: membresías SIN
|
|
481
|
+
* caducidad en los grupos con deny, que se terminan con `unrelate`. (2) Si hay que cambiarla: `unrelate` + `relate`
|
|
482
|
+
* ACEPTANDO la ventana, dicho como tal. Hasta el cierre 5 el mensaje ofrecía además «retira antes el deny, renueva y
|
|
483
|
+
* vuelve a escribirlo» como si fuera la salida segura: también abre la ventana (sin el deny, el sujeto concede). Cuando
|
|
484
|
+
* OpenFGA ofrezca upsert, la renovación será UNA escritura y `atomicRenewal` pasará a `true`.
|
|
485
|
+
*/
|
|
486
|
+
export class RenewalUnsafeError extends Exception {
|
|
487
|
+
static status = 409;
|
|
488
|
+
static code = 'E_AUTHZ_RENEWAL_UNSAFE';
|
|
489
|
+
static carrier(membership, denied) {
|
|
490
|
+
return new RenewalUnsafeError(`relate: no se renueva ${membership}: el grupo TRANSPORTA un deny vivo (${denied}) y este driver renueva con DOS ` +
|
|
491
|
+
'Write —delete y write: OpenFGA no reemplaza la misma clave en un Write (M5; sin upsert, openfga/openfga#2151)—; ' +
|
|
492
|
+
'entre los dos el deny no alcanzaría al sujeto y concedería (y si el segundo fallara, para siempre). No se ha escrito ' +
|
|
493
|
+
'nada. En este driver NO hay forma sin ventana de cambiar esta caducidad. (1) Patrón recomendado: membresías SIN ' +
|
|
494
|
+
'caducidad en los grupos con deny, que se terminan con unrelate. (2) Si hay que cambiarla: unrelate + relate de esa ' +
|
|
495
|
+
'membresía ACEPTANDO la ventana — mientras dura, el sujeto no recibe el deny del grupo y concede lo que otro camino le ' +
|
|
496
|
+
'dé y ese deny corte.');
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
|
|
500
|
+
* del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
|
|
501
|
+
* (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
|
|
502
|
+
* (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
|
|
503
|
+
* con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
|
|
504
|
+
*/
|
|
505
|
+
static foreignCondition(operation, tuple, detail) {
|
|
506
|
+
return new RenewalUnsafeError(`${operation}: la tupla ${tuple} ya existe en el store con una condición que este paquete no escribe ni sabe leer ` +
|
|
507
|
+
`(${detail}). No se reescribe a ciegas: el paquete no sabe qué concede (la puede haber escrito otra herramienta, o ` +
|
|
508
|
+
'un modelo anterior). No se ha escrito nada. Receta: revísala y bórrala a mano (SDK/CLI de OpenFGA, o unrelate/' +
|
|
509
|
+
'revoke por el paquete) y repite.');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
|
|
514
|
+
* cierre 4 · ⚪ C3-3): la renovación (dos `Write`) de una membresía cuyo grupo NO transportaba ningún deny al
|
|
515
|
+
* comprobarlo, y la RE-COMPROBACIÓN tras los dos `Write` encuentra uno —un `deny` concurrente entró entre la comprobación
|
|
516
|
+
* y la renovación—. Estado final correcto (la membresía está y el deny vuelve a alcanzar al sujeto), pero durante la
|
|
517
|
+
* ventana entre los dos `Write` el deny ya confirmado NO alcanzó al sujeto. Va marcado parcial (`markPartialWrite`: el
|
|
518
|
+
* manager notifica `indeterminate: true`) y lleva `carriedDeny` y `renewalApplied: true`.
|
|
519
|
+
*
|
|
520
|
+
* Hasta el cierre 4 era el MISMO 409 `E_AUTHZ_RENEWAL_UNSAFE` («cero escrituras, aplica la receta»): un consumidor que
|
|
521
|
+
* automatizara esa receta (`unrelate` + `relate`) ante este caso reabría la ventana con el deny YA dentro del grupo. Por
|
|
522
|
+
* eso tiene su código y su receta, que NO es esa.
|
|
523
|
+
*/
|
|
524
|
+
export class RenewalRacedError extends Exception {
|
|
525
|
+
static status = 409;
|
|
526
|
+
static code = 'E_AUTHZ_RENEWAL_RACED';
|
|
527
|
+
static raced(membership, denied) {
|
|
528
|
+
const error = new RenewalRacedError(`relate: la renovación de ${membership} SE APLICÓ, pero un deny concurrente (${denied}) entró entre la comprobación ` +
|
|
529
|
+
'de portadora y los dos Write: durante la ventana entre el delete y el write ese deny, ya confirmado, NO alcanzó al ' +
|
|
530
|
+
'sujeto. El estado final es correcto (la membresía está y el deny la alcanza): NO hay nada que reescribir, y la receta ' +
|
|
531
|
+
'de E_AUTHZ_RENEWAL_UNSAFE (unrelate + relate) reabriría la ventana con el deny ya dentro. Receta: revisa las ' +
|
|
532
|
+
'decisiones de acceso de ese sujeto en esa ventana; para no repetirla, serializa los deny y las renovaciones de ese ' +
|
|
533
|
+
'grupo, o renueva con removeDeny antes y deny después.');
|
|
534
|
+
error.carriedDeny = denied;
|
|
535
|
+
error.renewalApplied = true;
|
|
536
|
+
return error;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
385
539
|
/**
|
|
386
540
|
* **El lote de `checkMany` no tiene la forma admitida** (plan 2.5 · lote A,
|
|
387
541
|
* P4): vacío, o por encima de `RELATIONS_CHECK_MANY_MAX` (50). Es 422 con
|
|
@@ -481,6 +635,24 @@ export class ScopeDepthLimitError extends Exception {
|
|
|
481
635
|
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
482
636
|
}
|
|
483
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
|
|
640
|
+
* 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
|
|
641
|
+
* LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
|
|
642
|
+
* un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
|
|
643
|
+
* es el grafo el que no cabe. Gemelo de `E_AUTHZ_SCOPE_DEPTH_LIMIT` y con su misma regla de letra (alpha.8, 7.2): el
|
|
644
|
+
* `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
|
|
645
|
+
* backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
|
|
646
|
+
* en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
|
|
647
|
+
*/
|
|
648
|
+
export class GroupDepthLimitError extends Exception {
|
|
649
|
+
static status = 422;
|
|
650
|
+
static code = 'E_AUTHZ_GROUP_DEPTH_LIMIT';
|
|
651
|
+
constructor(message, details) {
|
|
652
|
+
super(message);
|
|
653
|
+
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
484
656
|
/**
|
|
485
657
|
* Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
|
|
486
658
|
* activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
|
|
@@ -546,6 +718,46 @@ export class UnsupportedOperationError extends Exception {
|
|
|
546
718
|
`removeDeny (hechos) y relate/unrelate/purge* (relaciones) admiten { transaction }.`;
|
|
547
719
|
return error;
|
|
548
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* **Los `list*` de relaciones no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez, decisión del dueño
|
|
723
|
+
* 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
|
|
724
|
+
* hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
|
|
725
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
|
|
726
|
+
* una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
|
|
727
|
+
* como métodos aparte (`*Page`, 2.6) con capacidad propia.
|
|
728
|
+
*/
|
|
729
|
+
static listPage(operation, driver) {
|
|
730
|
+
const error = new UnsupportedOperationError('page', operation, driver);
|
|
731
|
+
error.message =
|
|
732
|
+
`${operation}: se pasó una página (page) y ${operation} no pagina en 2.5 — devuelve el conjunto COMPLETO (o ` +
|
|
733
|
+
`señala truncated), nunca un cursor. No se ha llamado al backend ('${driver}'). Quita el argumento page; la ` +
|
|
734
|
+
`paginación de los listados llegará como métodos aparte (*Page) con capacidad propia.`;
|
|
735
|
+
return error;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
|
|
739
|
+
* devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
|
|
740
|
+
* llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
|
|
741
|
+
*/
|
|
742
|
+
static enumerateCursor(operation, driver) {
|
|
743
|
+
const error = new UnsupportedOperationError('page.after', operation, driver);
|
|
744
|
+
error.message =
|
|
745
|
+
`${operation}: el driver '${driver}' no emite cursor (devuelve todas las tuplas en una página), así que un ` +
|
|
746
|
+
`page.after no salió de él y no se acepta. No se ha llamado al backend. Llama sin after.`;
|
|
747
|
+
return error;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
|
|
752
|
+
* `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
|
|
753
|
+
* `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
|
|
754
|
+
* pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
|
|
755
|
+
* `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
|
|
756
|
+
* encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
|
|
757
|
+
*/
|
|
758
|
+
export class InvalidRelationPageError extends Exception {
|
|
759
|
+
static status = 422;
|
|
760
|
+
static code = 'E_AUTHZ_INVALID_PAGE';
|
|
549
761
|
}
|
|
550
762
|
/**
|
|
551
763
|
* Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
|
|
@@ -876,4 +1088,74 @@ export class RelationUsersetUnsupportedError extends Exception {
|
|
|
876
1088
|
static status = 422;
|
|
877
1089
|
static code = 'E_AUTHZ_RELATION_USERSET_UNSUPPORTED';
|
|
878
1090
|
}
|
|
879
|
-
|
|
1091
|
+
/**
|
|
1092
|
+
* **Deny sobre algo que no se puede denegar** (plan 2.5 · lote D · D1; panel
|
|
1093
|
+
* tenencia ronda 2, D10/D-17). `deny(…)` sobre un tipo cuya spec no declara
|
|
1094
|
+
* `deny: 'write'` (fase 2 del opt-in: sin opt-in, o en `deny: 'read'`, la fase
|
|
1095
|
+
* en la que el modelo y las lecturas ya honran el deny pero aún no se aceptan
|
|
1096
|
+
* escrituras), sobre el tipo built-in `group`, o con el comodín `*` como
|
|
1097
|
+
* sujeto. 422 ANTES de tocar el driver (cero llamadas, cero eventos): una
|
|
1098
|
+
* pregunta inválida, no una caída.
|
|
1099
|
+
*/
|
|
1100
|
+
export class RelationNotDenyableError extends Exception {
|
|
1101
|
+
static status = 422;
|
|
1102
|
+
static code = 'E_AUTHZ_RELATION_NOT_DENYABLE';
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* **`expiresAt` en un deny** (plan 2.5 · lote D · D3, decisión del dueño
|
|
1106
|
+
* 2026-10-14): el deny de relaciones NO caduca en v1. Medido (M5, OpenFGA
|
|
1107
|
+
* v1.19.0): FGA rechaza delete+write de la misma clave en un `Write` (400
|
|
1108
|
+
* `cannot_allow_duplicate_tuples_in_one_request`) y `on_duplicate: ignore` con
|
|
1109
|
+
* otra condición es 409, así que cambiar la caducidad de un deny VIVO exige dos
|
|
1110
|
+
* `Write` con una ventana en la que deja de bloquear — y si el segundo cae, el
|
|
1111
|
+
* deny DESAPARECE. 422 ANTES de tocar el driver, con la razón medida.
|
|
1112
|
+
*/
|
|
1113
|
+
export class RelationDenyExpiryError extends Exception {
|
|
1114
|
+
static status = 422;
|
|
1115
|
+
static code = 'E_AUTHZ_DENY_EXPIRY_UNSUPPORTED';
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* **`membersOf` de `database` sobre un objeto con deny y más candidatos que la cota** (lote D · cierre 1 de D1, 🟡 4 del
|
|
1119
|
+
* auditor). Con una fila `denied_*` vigente en el objeto, cada candidato se juzga con un `check` (el corte de camino);
|
|
1120
|
+
* sin cota eran N consultas en serie —una amplificación barata para quien puede escribir un deny—. Pasada
|
|
1121
|
+
* `RELATIONS_MEMBERS_DENY_CHECK_MAX` (1 000) ⇒ 500, NUNCA una lista parcial; el deadline TOTAL del bucle es 503
|
|
1122
|
+
* `E_AUTHZ_BACKEND_TIMEOUT`. **500 y no 422** (decisión 1 del cierre 1, ratificada en el cierre 2): no es una pregunta
|
|
1123
|
+
* inválida del llamante —la misma pregunta es válida con menos candidatos— sino una cota del DESPLIEGUE (precedente
|
|
1124
|
+
* `E_AUTHZ_TOO_MANY_LOCAL_ROLES`); y no 503 porque no es reintentable. `authorizedScopes` (`E_AUTHZ_TOO_MANY_SCOPES`,
|
|
1125
|
+
* 422) acota una enumeración que el llamante pide con su `maxScopes`; aquí no hay cota del llamante.
|
|
1126
|
+
*/
|
|
1127
|
+
export class MembersOfDenyLimitError extends Exception {
|
|
1128
|
+
static status = 500;
|
|
1129
|
+
static code = 'E_AUTHZ_MEMBERS_DENY_LIMIT';
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* **`authz:relations:reconcile` se NIEGA a migrar denies** (plan 2.5 · lote D · D2; auditor R2-5, tester S6, juez ronda 2
|
|
1133
|
+
* fila D). El origen trae al menos un deny vivo y el destino no puede escribirlo: no declara `relationDenies` (o no trae
|
|
1134
|
+
* `deny`/`removeDeny`/`denyPhase`), el tipo no está en la fase `'write'` del opt-in EN EL DESTINO (su config, no la spec
|
|
1135
|
+
* persistida), el par tipo/relación no está declarado allí, su modelo fijado no declara `denied_<rel>` (D-31), o el
|
|
1136
|
+
* origen enumera un deny SIN marcar `kind` (`unmarked`). Se decide en la FASE DE PLAN, antes de la primera escritura en
|
|
1137
|
+
* cualquier partición: **CERO escrituras**, y **jamás `skipped`** — perder un deny no es una pérdida que se pueda declarar
|
|
1138
|
+
* (escribir los allows sin él concede lo que el origen niega). `--dry-run` no lanza: lo marca en `report.denyRefused` y es
|
|
1139
|
+
* deriva. Lleva `refusals` (una entrada por tipo/relación y motivo). 500 y no 422: no es una pregunta del llamante sino
|
|
1140
|
+
* el estado del despliegue (precedente `E_AUTHZ_MASS_RECONCILE_REFUSED`).
|
|
1141
|
+
*/
|
|
1142
|
+
export class DenyMigrationRefusedError extends Exception {
|
|
1143
|
+
static status = 500;
|
|
1144
|
+
static code = 'E_AUTHZ_DENY_MIGRATION_REFUSED';
|
|
1145
|
+
refusals = [];
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
|
|
1149
|
+
* decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
|
|
1150
|
+
* vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
|
|
1151
|
+
* ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
|
|
1152
|
+
* línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
|
|
1153
|
+
* levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
|
|
1154
|
+
* precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
|
|
1155
|
+
* estado del despliegue.
|
|
1156
|
+
*/
|
|
1157
|
+
export class DenyReachRefusedError extends Exception {
|
|
1158
|
+
static status = 500;
|
|
1159
|
+
static code = 'E_AUTHZ_DENY_REACH_REFUSED';
|
|
1160
|
+
findings = [];
|
|
1161
|
+
}
|