@jantstack/adonis-authz 2.5.0-alpha.3 → 2.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +437 -48
- 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 +17 -3
- package/build/commands/authz_catalog_prune_orphans.js +28 -6
- 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 +0 -1
- package/build/commands/authz_relations_reconcile.js +56 -8
- package/build/commands/authz_scopes_relay.d.ts +0 -1
- package/build/commands/authz_scopes_relay.js +54 -6
- package/build/commands/authz_scopes_requeue.d.ts +0 -1
- package/build/commands/authz_scopes_requeue.js +16 -3
- 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 +10 -1
- package/build/commands/openfga_provision.js +43 -3
- package/build/configure.d.ts +0 -1
- package/build/configure.js +0 -1
- package/build/index.d.ts +9 -7
- package/build/index.js +10 -3
- 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 +27 -2
- 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 +60 -3
- package/build/src/drivers/database_driver.js +160 -25
- package/build/src/drivers/database_relations_driver.d.ts +11 -5
- package/build/src/drivers/database_relations_driver.js +115 -33
- package/build/src/drivers/openfga_driver.d.ts +159 -26
- package/build/src/drivers/openfga_driver.js +882 -199
- package/build/src/drivers/openfga_facts.d.ts +91 -1
- package/build/src/drivers/openfga_facts.js +277 -3
- package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
- package/build/src/drivers/openfga_relations_driver.js +422 -228
- package/build/src/errors.d.ts +160 -1
- package/build/src/errors.js +161 -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 +9 -2
- package/build/src/identity.js +33 -1
- package/build/src/manager.d.ts +24 -1
- package/build/src/manager.js +381 -85
- 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 +2 -3
- package/build/src/openfga.js +4 -2
- 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 +0 -1
- package/build/src/relations/check_many.js +0 -1
- package/build/src/relations/define_relations_config.d.ts +25 -3
- package/build/src/relations/define_relations_config.js +8 -3
- 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 +14 -7
- package/build/src/relations/manager.js +60 -15
- package/build/src/relations/page.d.ts +19 -0
- package/build/src/relations/page.js +74 -0
- package/build/src/relations/reconcile.d.ts +58 -4
- package/build/src/relations/reconcile.js +266 -12
- 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 +5 -4
- package/build/src/relations/tuple_order.js +5 -4
- 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 +36 -3
- package/build/src/scope_outbox.js +78 -18
- package/build/src/shared/backend_guard.d.ts +52 -1
- package/build/src/shared/backend_guard.js +164 -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 +26 -3
- package/build/src/testing/contract.js +258 -20
- package/build/src/testing/main.d.ts +0 -1
- package/build/src/testing/main.js +0 -1
- 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 +7 -1
- package/build/src/testing/relations_contract.js +174 -7
- package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
- package/build/src/testing/relations_reconcile_contract.js +0 -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 +138 -14
- 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/tuple_order.d.ts.map +0 -1
- package/build/src/relations/tuple_order.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
|
|
@@ -135,6 +147,33 @@ export declare function isThenable(value: unknown): boolean;
|
|
|
135
147
|
export declare function markPartialWrite<E extends object>(error: E): E & {
|
|
136
148
|
partialWrite: true;
|
|
137
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;
|
|
138
177
|
/** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
|
|
139
178
|
export declare function isPartialWrite(error: unknown): boolean;
|
|
140
179
|
/**
|
|
@@ -156,6 +195,24 @@ export declare class AuthorizationInternalError extends Exception {
|
|
|
156
195
|
export declare class UnknownScopeError extends Exception {
|
|
157
196
|
static status: number;
|
|
158
197
|
static code: string;
|
|
198
|
+
/**
|
|
199
|
+
* **Para el OPERADOR, no para el tenant** (rotación 2 · cierre 2 · RC2-A): en `updateScopedRole`/`deleteScopedRole` el
|
|
200
|
+
* scope que no resuelve es el OWNER del rol (dormido), que el llamante no pasó y puede ser de otro tenant: el `message`
|
|
201
|
+
* nombra el rol y el owner va aquí, NO enumerable (mismo patrón que `NotWithinError.details`). Ausente en el resto de
|
|
202
|
+
* rutas, donde el scope desconocido es el que pasó el llamante y el `message` lo nombra.
|
|
203
|
+
*/
|
|
204
|
+
readonly details: {
|
|
205
|
+
owner: {
|
|
206
|
+
type: string;
|
|
207
|
+
uuid: string | null;
|
|
208
|
+
};
|
|
209
|
+
} | undefined;
|
|
210
|
+
constructor(message?: string, details?: {
|
|
211
|
+
owner: {
|
|
212
|
+
type: string;
|
|
213
|
+
uuid: string | null;
|
|
214
|
+
};
|
|
215
|
+
});
|
|
159
216
|
}
|
|
160
217
|
/**
|
|
161
218
|
* Se preguntó por un scope que no es la raíz y el driver no tiene resolutor
|
|
@@ -362,6 +419,14 @@ export declare class RenewalUnsafeError extends Exception {
|
|
|
362
419
|
static status: number;
|
|
363
420
|
static code: string;
|
|
364
421
|
static carrier(membership: string, denied: string): RenewalUnsafeError;
|
|
422
|
+
/**
|
|
423
|
+
* **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
|
|
424
|
+
* del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
|
|
425
|
+
* (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
|
|
426
|
+
* (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
|
|
427
|
+
* con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
|
|
428
|
+
*/
|
|
429
|
+
static foreignCondition(operation: string, tuple: string, detail: string): RenewalUnsafeError;
|
|
365
430
|
}
|
|
366
431
|
/**
|
|
367
432
|
* **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
|
|
@@ -411,6 +476,27 @@ export declare class ActorRequiredError extends Exception {
|
|
|
411
476
|
export declare class NotWithinError extends Exception {
|
|
412
477
|
static status: number;
|
|
413
478
|
static code: string;
|
|
479
|
+
/**
|
|
480
|
+
* **Para el OPERADOR, no para el tenant** (rotación 2 de M4 · R2-03, decisión del dueño 2026-10-22 (3)). Hasta
|
|
481
|
+
* 2.5.0-alpha.4 el `message` imprimía la cadena ENTERA del scope (`unit:<B1x> → unit:<B1> → organization:<org de B>
|
|
482
|
+
* → app`): el admin de A que conoce un uuid de B aprendía a qué tenant pertenece y su ruta, y un 422 es lo que un
|
|
483
|
+
* framework devuelve al cliente tal cual (la misma clase de fuga que 3E · Q2 cerró en `ROLE_NOT_VISIBLE`). Ahora el
|
|
484
|
+
* `message` nombra solo lo que el llamante ya tiene —el scope que pasó y su `within`— y la cadena va aquí, NO
|
|
485
|
+
* enumerable como `ScopeDepthLimitError.details` (alpha.8): `JSON.stringify(error)` —lo que un handler genérico
|
|
486
|
+
* manda— no la lleva, y el log del servidor la lee por `error.details.chain`. El CÓDIGO no cambia.
|
|
487
|
+
*/
|
|
488
|
+
readonly details: {
|
|
489
|
+
chain: ReadonlyArray<{
|
|
490
|
+
type: string;
|
|
491
|
+
uuid: string | null;
|
|
492
|
+
}>;
|
|
493
|
+
} | undefined;
|
|
494
|
+
constructor(message: string, details?: {
|
|
495
|
+
chain: ReadonlyArray<{
|
|
496
|
+
type: string;
|
|
497
|
+
uuid: string | null;
|
|
498
|
+
}>;
|
|
499
|
+
});
|
|
414
500
|
}
|
|
415
501
|
/**
|
|
416
502
|
* `config.requireWithin` (`true` o `'non-root'`) y una escritura llegó sin
|
|
@@ -495,6 +581,31 @@ export declare class ScopeDepthLimitError extends Exception {
|
|
|
495
581
|
server: string;
|
|
496
582
|
});
|
|
497
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
|
|
586
|
+
* 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
|
|
587
|
+
* LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
|
|
588
|
+
* un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
|
|
589
|
+
* 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
|
|
590
|
+
* `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
|
|
591
|
+
* backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
|
|
592
|
+
* en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
|
|
593
|
+
*/
|
|
594
|
+
export declare class GroupDepthLimitError extends Exception {
|
|
595
|
+
static status: number;
|
|
596
|
+
static code: string;
|
|
597
|
+
readonly details: {
|
|
598
|
+
reason: 'too-deep' | 'cycle' | 'too-many-groups';
|
|
599
|
+
edge: string;
|
|
600
|
+
depth: number | null;
|
|
601
|
+
maxGroupDepth: number;
|
|
602
|
+
/** Solo con `too-many-groups`: qué cota se pasó (grupos, requests, tuplas leídas o el deadline total). */
|
|
603
|
+
budget?: 'nodes' | 'reads' | 'tuples' | 'deadline';
|
|
604
|
+
why: string;
|
|
605
|
+
server: string;
|
|
606
|
+
};
|
|
607
|
+
constructor(message: string, details: GroupDepthLimitError['details']);
|
|
608
|
+
}
|
|
498
609
|
/**
|
|
499
610
|
* Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
|
|
500
611
|
* activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
|
|
@@ -531,6 +642,33 @@ export declare class UnsupportedOperationError extends Exception {
|
|
|
531
642
|
* entre procesos (invariante 14); moverla al commit del consumidor lo anula.
|
|
532
643
|
*/
|
|
533
644
|
static transactionalCatalog(operation: string): UnsupportedOperationError;
|
|
645
|
+
/**
|
|
646
|
+
* **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
|
|
647
|
+
* 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
|
|
648
|
+
* hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
|
|
649
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
|
|
650
|
+
* una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
|
|
651
|
+
* como métodos aparte (`*Page`, 2.6) con capacidad propia.
|
|
652
|
+
*/
|
|
653
|
+
static listPage(operation: string, driver: string): UnsupportedOperationError;
|
|
654
|
+
/**
|
|
655
|
+
* **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
|
|
656
|
+
* devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
|
|
657
|
+
* llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
|
|
658
|
+
*/
|
|
659
|
+
static enumerateCursor(operation: string, driver: string): UnsupportedOperationError;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
|
|
663
|
+
* `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
|
|
664
|
+
* `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
|
|
665
|
+
* pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
|
|
666
|
+
* `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
|
|
667
|
+
* encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
|
|
668
|
+
*/
|
|
669
|
+
export declare class InvalidRelationPageError extends Exception {
|
|
670
|
+
static status: number;
|
|
671
|
+
static code: string;
|
|
534
672
|
}
|
|
535
673
|
/**
|
|
536
674
|
* Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
|
|
@@ -944,4 +1082,25 @@ export declare class DenyMigrationRefusedError extends Exception {
|
|
|
944
1082
|
message: string;
|
|
945
1083
|
}>;
|
|
946
1084
|
}
|
|
947
|
-
|
|
1085
|
+
/**
|
|
1086
|
+
* **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
|
|
1087
|
+
* decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
|
|
1088
|
+
* vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
|
|
1089
|
+
* ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
|
|
1090
|
+
* línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
|
|
1091
|
+
* levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
|
|
1092
|
+
* precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
|
|
1093
|
+
* estado del despliegue.
|
|
1094
|
+
*/
|
|
1095
|
+
export declare class DenyReachRefusedError extends Exception {
|
|
1096
|
+
static status: number;
|
|
1097
|
+
static code: string;
|
|
1098
|
+
findings: ReadonlyArray<{
|
|
1099
|
+
objectType: string;
|
|
1100
|
+
relation: string;
|
|
1101
|
+
reason: string;
|
|
1102
|
+
count: number;
|
|
1103
|
+
lost: string[];
|
|
1104
|
+
recipe: string;
|
|
1105
|
+
}>;
|
|
1106
|
+
}
|
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
|
|
@@ -177,6 +198,52 @@ export function markPartialWrite(error) {
|
|
|
177
198
|
}
|
|
178
199
|
return error;
|
|
179
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
|
+
}
|
|
180
247
|
/** ¿El error viene marcado como escritura parcial (`markPartialWrite`)? */
|
|
181
248
|
export function isPartialWrite(error) {
|
|
182
249
|
return error !== null && typeof error === 'object' && error.partialWrite === true;
|
|
@@ -200,6 +267,10 @@ export class AuthorizationInternalError extends Exception {
|
|
|
200
267
|
export class UnknownScopeError extends Exception {
|
|
201
268
|
static status = 422;
|
|
202
269
|
static code = 'E_AUTHZ_UNKNOWN_SCOPE';
|
|
270
|
+
constructor(message, details) {
|
|
271
|
+
super(message);
|
|
272
|
+
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
273
|
+
}
|
|
203
274
|
}
|
|
204
275
|
/**
|
|
205
276
|
* Se preguntó por un scope que no es la raíz y el driver no tiene resolutor
|
|
@@ -428,6 +499,19 @@ export class RenewalUnsafeError extends Exception {
|
|
|
428
499
|
'membresía ACEPTANDO la ventana — mientras dura, el sujeto no recibe el deny del grupo y concede lo que otro camino le ' +
|
|
429
500
|
'dé y ese deny corte.');
|
|
430
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* **La clave YA existe con una condición que el paquete no escribe** (lote «antes de RC» · L11, injerto 3 del juez —H8
|
|
504
|
+
* del auditor—): `relate` (relaciones) o `grant` (roles) de `openfga` sobre una tupla cuya condición es `foreign`
|
|
505
|
+
* (otra condición, o `not_expired` con un contexto que no es exactamente `{ valid_until }`) o `unparseable`
|
|
506
|
+
* (`fgaTupleExpiry`). Tratarla como «sin caducidad» era un no-op mudo sobre una tupla que el servidor ignora o evalúa
|
|
507
|
+
* con otra semántica; como «vencida», una reescritura en dos `Write` sin mirar nada. 409 y CERO escrituras.
|
|
508
|
+
*/
|
|
509
|
+
static foreignCondition(operation, tuple, detail) {
|
|
510
|
+
return new RenewalUnsafeError(`${operation}: la tupla ${tuple} ya existe en el store con una condición que este paquete no escribe ni sabe leer ` +
|
|
511
|
+
`(${detail}). No se reescribe a ciegas: el paquete no sabe qué concede (la puede haber escrito otra herramienta, o ` +
|
|
512
|
+
'un modelo anterior). No se ha escrito nada. Receta: revísala y bórrala a mano (SDK/CLI de OpenFGA, o unrelate/' +
|
|
513
|
+
'revoke por el paquete) y repite.');
|
|
514
|
+
}
|
|
431
515
|
}
|
|
432
516
|
/**
|
|
433
517
|
* **La renovación SE APLICÓ y un deny concurrente cayó en su ventana** (cierre 3 de D2 · 🟡 C2-3; código PROPIO desde el
|
|
@@ -485,6 +569,10 @@ export class ActorRequiredError extends Exception {
|
|
|
485
569
|
export class NotWithinError extends Exception {
|
|
486
570
|
static status = 422;
|
|
487
571
|
static code = 'E_AUTHZ_NOT_WITHIN';
|
|
572
|
+
constructor(message, details) {
|
|
573
|
+
super(message);
|
|
574
|
+
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
575
|
+
}
|
|
488
576
|
}
|
|
489
577
|
/**
|
|
490
578
|
* `config.requireWithin` (`true` o `'non-root'`) y una escritura llegó sin
|
|
@@ -555,6 +643,24 @@ export class ScopeDepthLimitError extends Exception {
|
|
|
555
643
|
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
556
644
|
}
|
|
557
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* **La arista de anidación de grupos que el techo del driver no admite** (lote «antes de RC» · R1, decisiones del dueño
|
|
648
|
+
* 2026-10-17 (5) y (5-bis)): `relate(group:Gc#member, 'member', group:Gp)` dejaría una cadena de grupos anidados MÁS
|
|
649
|
+
* LARGA que el techo del driver —el camino más largo que atraviesa la arista nueva, hacia arriba y hacia abajo—, cerraría
|
|
650
|
+
* un CICLO, o el grafo a recorrer pasa de `GROUP_DEPTH_MAX_NODES`. 422 y CERO escrituras: la pregunta está bien formada,
|
|
651
|
+
* 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
|
|
652
|
+
* `message` es para el TENANT (la cadena, el techo, `maxGroupDepth`, «no se ha escrito nada») y no nombra el
|
|
653
|
+
* backend; el porqué del techo —OpenFGA no resuelve más allá, el mando del servidor es `OPENFGA_RESOLVE_NODE_LIMIT`— va
|
|
654
|
+
* en `details`, NO enumerable (un handler genérico que serializa el error no se lo enseña al tenant).
|
|
655
|
+
*/
|
|
656
|
+
export class GroupDepthLimitError extends Exception {
|
|
657
|
+
static status = 422;
|
|
658
|
+
static code = 'E_AUTHZ_GROUP_DEPTH_LIMIT';
|
|
659
|
+
constructor(message, details) {
|
|
660
|
+
super(message);
|
|
661
|
+
Object.defineProperty(this, 'details', { value: details, enumerable: false, configurable: true, writable: false });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
558
664
|
/**
|
|
559
665
|
* Una primitiva de 2.1 necesita un método OPCIONAL del puerto que el driver
|
|
560
666
|
* activo no implementa (2.1, B5): se nombra el método y se lanza. 500 porque
|
|
@@ -620,6 +726,46 @@ export class UnsupportedOperationError extends Exception {
|
|
|
620
726
|
`removeDeny (hechos) y relate/unrelate/purge* (relaciones) admiten { transaction }.`;
|
|
621
727
|
return error;
|
|
622
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* **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
|
|
731
|
+
* 2026-10-17 (7.4)): `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en la firma (nunca
|
|
732
|
+
* hizo nada: se diseñó en la Fase 4 y no llegó a programarse). Un valor DISTINTO de `undefined` —`null`, `{}`,
|
|
733
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` es 500 con cero llamadas al backend, en el manager y en los drivers:
|
|
734
|
+
* una lista entera que PARECE una página es la ruptura muda que el linaje L8 prohíbe. La paginación de verdad llegará
|
|
735
|
+
* como métodos aparte (`*Page`, 2.6) con capacidad propia.
|
|
736
|
+
*/
|
|
737
|
+
static listPage(operation, driver) {
|
|
738
|
+
const error = new UnsupportedOperationError('page', operation, driver);
|
|
739
|
+
error.message =
|
|
740
|
+
`${operation}: se pasó una página (page) y ${operation} no pagina en 2.5 — devuelve el conjunto COMPLETO (o ` +
|
|
741
|
+
`señala truncated), nunca un cursor. No se ha llamado al backend ('${driver}'). Quita el argumento page; la ` +
|
|
742
|
+
`paginación de los listados llegará como métodos aparte (*Page) con capacidad propia.`;
|
|
743
|
+
return error;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* **Un cursor que este driver nunca emitió** (7.4, injerto 4 del juez; `openfga` y el doble): sus `enumerate*`
|
|
747
|
+
* devuelven todo en una página y JAMÁS un cursor, así que un `after` no puede haber salido de ellos. 500 con cero
|
|
748
|
+
* llamadas: aceptarlo en silencio sería empezar «desde algún sitio» o ignorarlo, las dos cosas mudas.
|
|
749
|
+
*/
|
|
750
|
+
static enumerateCursor(operation, driver) {
|
|
751
|
+
const error = new UnsupportedOperationError('page.after', operation, driver);
|
|
752
|
+
error.message =
|
|
753
|
+
`${operation}: el driver '${driver}' no emite cursor (devuelve todas las tuplas en una página), así que un ` +
|
|
754
|
+
`page.after no salió de él y no se acepta. No se ha llamado al backend. Llama sin after.`;
|
|
755
|
+
return error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* **Una página de `enumerate*` mal formada** (lote «antes de RC» · 7.4, injerto 4 del juez; tester H2/H3, auditor T-1):
|
|
760
|
+
* `limit` que no es un entero ≥ 1 (`NaN`, `0`, negativo, decimal, cadena) o `after` que no es una cadena no vacía (en
|
|
761
|
+
* `database`, además, con forma de uuid canónico en minúsculas: el cursor que ESE driver emite). 422 porque es la
|
|
762
|
+
* pregunta la que está mal formada (invariante 5); hasta 7.4 `limit: NaN` generaba una consulta sin `LIMIT` y un
|
|
763
|
+
* `after` ajeno era un 503 en PostgreSQL (la columna es `uuid`) y un fin silencioso en MySQL/SQLite. Un `limit` por
|
|
764
|
+
* encima de `RELATIONS_ENUMERATE_MAX_LIMIT` NO es este error: se recorta (con cursor, sin pérdida).
|
|
765
|
+
*/
|
|
766
|
+
export class InvalidRelationPageError extends Exception {
|
|
767
|
+
static status = 422;
|
|
768
|
+
static code = 'E_AUTHZ_INVALID_PAGE';
|
|
623
769
|
}
|
|
624
770
|
/**
|
|
625
771
|
* Una vista de `forRequest()` se usó para LEER después de `maxAgeMs` (2D ·
|
|
@@ -1006,4 +1152,18 @@ export class DenyMigrationRefusedError extends Exception {
|
|
|
1006
1152
|
static code = 'E_AUTHZ_DENY_MIGRATION_REFUSED';
|
|
1007
1153
|
refusals = [];
|
|
1008
1154
|
}
|
|
1009
|
-
|
|
1155
|
+
/**
|
|
1156
|
+
* **`saveRelationsConfig` se NIEGA a guardar una config que encoge el alcance de un deny vivo** (lote RC · sublote (8);
|
|
1157
|
+
* decisión §2026-10-15 (8) y firmas «(8 · panel)» 2026-10-22): la guarda de `database` con la propiedad «para todo deny
|
|
1158
|
+
* vivo de `authz_relations`, alcance nuevo ⊇ alcance viejo» (`src/relations/deny_reach.ts`). Cero escrituras (ni la fila
|
|
1159
|
+
* ni la versión del catálogo). Lleva `findings` (un hallazgo por tipo/relación: `shrunk`, `orphan` o `no-baseline` —sin
|
|
1160
|
+
* línea base utilizable no se puede juzgar—, con su receta). Salidas con nombre, separadas: `{ establishBaseline: true }`
|
|
1161
|
+
* levanta solo `no-baseline`; `{ allowDenyReachShrink: ['tipo#relación'] }` exime solo esos hallazgos. 500 y no 422 por
|
|
1162
|
+
* precedente (`E_AUTHZ_DENY_MIGRATION_REFUSED`, `E_AUTHZ_MASS_RECONCILE_REFUSED`): no es una pregunta mal formada, es el
|
|
1163
|
+
* estado del despliegue.
|
|
1164
|
+
*/
|
|
1165
|
+
export class DenyReachRefusedError extends Exception {
|
|
1166
|
+
static status = 500;
|
|
1167
|
+
static code = 'E_AUTHZ_DENY_REACH_REFUSED';
|
|
1168
|
+
findings = [];
|
|
1169
|
+
}
|
package/build/src/expiry.d.ts
CHANGED
|
@@ -20,8 +20,57 @@ export declare function isActiveExpiry(expiresAt: Date | null, now?: Date): bool
|
|
|
20
20
|
* una reescritura (que en openfga es delete+write con ventana de denegación).
|
|
21
21
|
*/
|
|
22
22
|
export declare function sameInstant(a: Date | null | undefined, b: Date | null | undefined): boolean;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Lo que devuelve el backend SQL como caducidad (número, ISO, Date) → Date | null. **Solo SQL** (`sql_expiry.ts`): una
|
|
25
|
+
* tupla de OpenFGA se lee con `fgaTupleExpiry` (L11), nunca con esto — aquí una cadena ilegible es `null` =
|
|
26
|
+
* PERMANENTE, que para una tupla del store era fail-open (C6-1/C6-2).
|
|
27
|
+
*/
|
|
24
28
|
export declare function toExpiryDate(value: unknown): Date | null;
|
|
25
29
|
/** ¿El outcome de un grant cambió la caducidad de una asignación que ya existía? */
|
|
26
30
|
export declare function expiryChanged(outcome: GrantOutcome): boolean;
|
|
27
|
-
|
|
31
|
+
/**
|
|
32
|
+
* El nombre de la condición de caducidad que escribe el paquete (`current_time < valid_until`). Es la MISMA cadena que
|
|
33
|
+
* `FACTS_EXPIRY_CONDITION` del generador del modelo (`openfga_facts.ts` la re-exporta desde aquí: una sola fuente).
|
|
34
|
+
*/
|
|
35
|
+
export declare const FGA_EXPIRY_CONDITION = "not_expired";
|
|
36
|
+
/**
|
|
37
|
+
* **Lo que dice la condición de una tupla LEÍDA de OpenFGA** (lote «antes de RC» · L11, decisión del dueño 2026-10-17
|
|
38
|
+
* (1); injerto 2 del juez; F0 · M-a(2)/M-b). Cuatro estados, y ninguno es «permanente» por defecto:
|
|
39
|
+
*
|
|
40
|
+
* - `none`: la tupla no lleva condición. Con el modelo GENERADO toda relación la admite así y el servidor la evalúa
|
|
41
|
+
* PERMANENTE (medido, F0 · M-a(2)); la puerta L11 garantiza que el modelo que sirve el driver es ése.
|
|
42
|
+
* - `expiry`: `not_expired` con el contexto EXACTAMENTE `{ valid_until }` y una marca RFC3339 de la gramática que
|
|
43
|
+
* acepta el servidor. `at` es el milisegundo SIGUIENTE a la fracción (techo al ms: el servidor compara en ns y el
|
|
44
|
+
* paquete manda `current_time` en ms, así que `now < at` ⇔ `ns(now) < trunc9(ns(valid_until))`; medido 58/58, el
|
|
45
|
+
* `floor` de `new Date()` fallaba 30/60 — F0 · M-b).
|
|
46
|
+
* - `foreign`: otra condición (`inverted`…), o `not_expired` con un contexto que trae MÁS que `valid_until` (un
|
|
47
|
+
* `current_time` congelado en la tupla gana al de la petición: medido por el auditor, H2). El paquete no la
|
|
48
|
+
* escribe y no sabe qué concede.
|
|
49
|
+
* - `unparseable`: `not_expired` sin contexto o sin `valid_until` (el servidor responde 400 al evaluarla), o una marca
|
|
50
|
+
* que el servidor acepta y este parser no reproduce. Hasta L11 eso era `null` = PERMANENTE (`toExpiryDate`).
|
|
51
|
+
*
|
|
52
|
+
* Qué hace cada consumidor con `foreign`/`unparseable` lo decide él, siempre fail-closed (tabla en el README): VIVA
|
|
53
|
+
* donde la tupla NIEGA o transporta un deny, AUSENTE donde CONCEDE, intocable en el destino de un reconcile, 409 con
|
|
54
|
+
* receta si una escritura del paquete tendría que reemplazarla, borrada igual en una purga.
|
|
55
|
+
*/
|
|
56
|
+
export type FgaTupleExpiry = {
|
|
57
|
+
kind: 'none';
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'expiry';
|
|
60
|
+
at: Date;
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'foreign';
|
|
63
|
+
detail: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'unparseable';
|
|
66
|
+
detail: string;
|
|
67
|
+
};
|
|
68
|
+
export declare function fgaTupleExpiry(condition: unknown): FgaTupleExpiry;
|
|
69
|
+
/**
|
|
70
|
+
* **La gramática de marca de tiempo que acepta OpenFGA v1.19 en `valid_until`, en nanosegundos desde la época**
|
|
71
|
+
* (F0 · M-b, medido): `YYYY-MM-DDTHH:MM:SS[(.|,)d+](Z|±hh:mm)`, fracción de cualquier longitud TRUNCADA a 9 dígitos
|
|
72
|
+
* (sin redondeo), coma decimal admitida, desfase con `hh` de 0 a 24 y `mm` de 0 a 59 (`+24:00`/`-24:00`/`+24:01` se
|
|
73
|
+
* aceptan; `+99:00` no; 25–98 sin medir ⇒ aquí `null`, fail-closed), fecha y hora válidas del calendario gregoriano
|
|
74
|
+
* proléptico (año 0000–9999; `02-30`, `T24:00:00`, minuto/segundo 60 ⇒ `null`). Todo lo demás ⇒ `null`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function fgaTimestampNs(value: string): bigint | null;
|
package/build/src/expiry.js
CHANGED
|
@@ -32,7 +32,11 @@ export function sameInstant(a, b) {
|
|
|
32
32
|
return false;
|
|
33
33
|
return a.getTime() === b.getTime();
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Lo que devuelve el backend SQL como caducidad (número, ISO, Date) → Date | null. **Solo SQL** (`sql_expiry.ts`): una
|
|
37
|
+
* tupla de OpenFGA se lee con `fgaTupleExpiry` (L11), nunca con esto — aquí una cadena ilegible es `null` =
|
|
38
|
+
* PERMANENTE, que para una tupla del store era fail-open (C6-1/C6-2).
|
|
39
|
+
*/
|
|
36
40
|
export function toExpiryDate(value) {
|
|
37
41
|
if (value === null || value === undefined || value === '')
|
|
38
42
|
return null;
|
|
@@ -47,4 +51,94 @@ export function expiryChanged(outcome) {
|
|
|
47
51
|
outcome.previousExpiresAt !== undefined &&
|
|
48
52
|
!sameInstant(outcome.previousExpiresAt, outcome.expiresAt));
|
|
49
53
|
}
|
|
50
|
-
|
|
54
|
+
/* ── L11 · la caducidad de una TUPLA de OpenFGA, en UN sitio ───────────── */
|
|
55
|
+
/**
|
|
56
|
+
* El nombre de la condición de caducidad que escribe el paquete (`current_time < valid_until`). Es la MISMA cadena que
|
|
57
|
+
* `FACTS_EXPIRY_CONDITION` del generador del modelo (`openfga_facts.ts` la re-exporta desde aquí: una sola fuente).
|
|
58
|
+
*/
|
|
59
|
+
export const FGA_EXPIRY_CONDITION = 'not_expired';
|
|
60
|
+
const NONE = Object.freeze({ kind: 'none' });
|
|
61
|
+
export function fgaTupleExpiry(condition) {
|
|
62
|
+
if (condition === undefined || condition === null)
|
|
63
|
+
return NONE;
|
|
64
|
+
if (typeof condition !== 'object' || Array.isArray(condition)) {
|
|
65
|
+
return { kind: 'unparseable', detail: `la condición no es un objeto (${JSON.stringify(condition)})` };
|
|
66
|
+
}
|
|
67
|
+
const { name, context } = condition;
|
|
68
|
+
if (typeof name !== 'string' || name === '') {
|
|
69
|
+
return { kind: 'unparseable', detail: `condición sin nombre (${JSON.stringify(condition)})` };
|
|
70
|
+
}
|
|
71
|
+
if (name !== FGA_EXPIRY_CONDITION)
|
|
72
|
+
return { kind: 'foreign', detail: `condición '${name}'` };
|
|
73
|
+
if (context === undefined || context === null || typeof context !== 'object' || Array.isArray(context)) {
|
|
74
|
+
return { kind: 'unparseable', detail: `${FGA_EXPIRY_CONDITION} sin contexto (sin valid_until el servidor responde 400 al evaluarla)` };
|
|
75
|
+
}
|
|
76
|
+
const keys = Object.keys(context);
|
|
77
|
+
if (!keys.includes('valid_until')) {
|
|
78
|
+
return { kind: 'unparseable', detail: `${FGA_EXPIRY_CONDITION} sin valid_until en el contexto (${keys.join(', ') || 'vacío'})` };
|
|
79
|
+
}
|
|
80
|
+
if (keys.length !== 1) {
|
|
81
|
+
return {
|
|
82
|
+
kind: 'foreign',
|
|
83
|
+
detail: `${FGA_EXPIRY_CONDITION} con contexto {${keys.sort().join(', ')}}: el paquete solo escribe {valid_until} (un current_time en la tupla gana al de la petición)`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const raw = context.valid_until;
|
|
87
|
+
const ns = typeof raw === 'string' ? fgaTimestampNs(raw) : null;
|
|
88
|
+
if (ns === null)
|
|
89
|
+
return { kind: 'unparseable', detail: `valid_until ilegible (${JSON.stringify(raw)})` };
|
|
90
|
+
return { kind: 'expiry', at: new Date(Number(ceilDiv(ns, 1000000n))) };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* **La gramática de marca de tiempo que acepta OpenFGA v1.19 en `valid_until`, en nanosegundos desde la época**
|
|
94
|
+
* (F0 · M-b, medido): `YYYY-MM-DDTHH:MM:SS[(.|,)d+](Z|±hh:mm)`, fracción de cualquier longitud TRUNCADA a 9 dígitos
|
|
95
|
+
* (sin redondeo), coma decimal admitida, desfase con `hh` de 0 a 24 y `mm` de 0 a 59 (`+24:00`/`-24:00`/`+24:01` se
|
|
96
|
+
* aceptan; `+99:00` no; 25–98 sin medir ⇒ aquí `null`, fail-closed), fecha y hora válidas del calendario gregoriano
|
|
97
|
+
* proléptico (año 0000–9999; `02-30`, `T24:00:00`, minuto/segundo 60 ⇒ `null`). Todo lo demás ⇒ `null`.
|
|
98
|
+
*/
|
|
99
|
+
export function fgaTimestampNs(value) {
|
|
100
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:[.,](\d+))?(?:(Z)|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
101
|
+
if (!match)
|
|
102
|
+
return null;
|
|
103
|
+
const [, ys, mos, ds, hs, mis, ss, fraction, zulu, sign, ohs, oms] = match;
|
|
104
|
+
const year = Number(ys);
|
|
105
|
+
const month = Number(mos);
|
|
106
|
+
const day = Number(ds);
|
|
107
|
+
const hour = Number(hs);
|
|
108
|
+
const minute = Number(mis);
|
|
109
|
+
const second = Number(ss);
|
|
110
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
|
|
111
|
+
return null;
|
|
112
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
113
|
+
return null;
|
|
114
|
+
let offsetSeconds = 0;
|
|
115
|
+
if (!zulu) {
|
|
116
|
+
const offsetHours = Number(ohs);
|
|
117
|
+
const offsetMinutes = Number(oms);
|
|
118
|
+
if (offsetHours > 24 || offsetMinutes > 59)
|
|
119
|
+
return null;
|
|
120
|
+
offsetSeconds = (sign === '-' ? -1 : 1) * (offsetHours * 3600 + offsetMinutes * 60);
|
|
121
|
+
}
|
|
122
|
+
const seconds = BigInt(daysFromCivil(year, month, day)) * 86400n + BigInt(hour * 3600 + minute * 60 + second - offsetSeconds);
|
|
123
|
+
const nanos = fraction ? BigInt(fraction.slice(0, 9).padEnd(9, '0')) : 0n;
|
|
124
|
+
return seconds * 1000000000n + nanos;
|
|
125
|
+
}
|
|
126
|
+
function daysInMonth(year, month) {
|
|
127
|
+
if (month === 2)
|
|
128
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
|
|
129
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
130
|
+
}
|
|
131
|
+
/** Días desde 1970-01-01 de una fecha del gregoriano proléptico (H. Hinnant, `days_from_civil`). */
|
|
132
|
+
function daysFromCivil(year, month, day) {
|
|
133
|
+
const y = month <= 2 ? year - 1 : year;
|
|
134
|
+
const era = Math.floor(y / 400);
|
|
135
|
+
const yoe = y - era * 400;
|
|
136
|
+
const doy = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
|
|
137
|
+
const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
|
|
138
|
+
return era * 146_097 + doe - 719_468;
|
|
139
|
+
}
|
|
140
|
+
/** Techo de una división entera de BigInt (la de BigInt trunca hacia cero). */
|
|
141
|
+
function ceilDiv(n, d) {
|
|
142
|
+
const q = n / d;
|
|
143
|
+
return n % d !== 0n && n > 0n ? q + 1n : q;
|
|
144
|
+
}
|
package/build/src/freeze.d.ts
CHANGED
|
@@ -197,4 +197,3 @@ export declare function releaseFreeze(token: FreezeToken, input: {
|
|
|
197
197
|
export declare function isOperatorHolder(holder: string | null): boolean;
|
|
198
198
|
/** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
|
|
199
199
|
export declare function freezeKindOf(holder: string | null): FreezeKind | 'unknown';
|
|
200
|
-
//# sourceMappingURL=freeze.d.ts.map
|
package/build/src/freeze.js
CHANGED
|
@@ -53,4 +53,3 @@ export interface HierarchicalResolverOptions {
|
|
|
53
53
|
* ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
|
|
54
54
|
*/
|
|
55
55
|
export declare function hierarchicalScopeResolver(options: HierarchicalResolverOptions): ScopeChainResolver;
|
|
56
|
-
//# sourceMappingURL=hierarchical_resolver.d.ts.map
|