@jantstack/adonis-authz 2.5.0-alpha.3 → 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 +414 -35
- 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 +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 +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 +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 +7 -5
- 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 +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 +11 -5
- package/build/src/drivers/database_relations_driver.js +90 -18
- 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 +121 -1
- package/build/src/errors.js +153 -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 +156 -28
- 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 +0 -1
- package/build/src/scope_outbox.js +0 -1
- 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 +7 -1
- package/build/src/testing/contract.js +77 -1
- 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 +89 -14
- package/build/src/types.js +4 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthorizationConfigError } from '../errors.js';
|
|
1
2
|
import type { CatalogProjectionRole, HolderTypeMap } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* `holderTypes` tiene que ser INYECTIVO. Si dos morph names caen en el mismo
|
|
@@ -402,4 +403,93 @@ export declare function factsRelationRewrites(objectType: RelationObjectType): R
|
|
|
402
403
|
* con nombre y no se toca). Cualquier otra diferencia es una regla DISTINTA.
|
|
403
404
|
*/
|
|
404
405
|
export declare function canonicalRewrite(node: unknown): unknown;
|
|
405
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Qué tipos compara cada puerta (L11): `roles` (los holders, `role`, `role_binding`, `scope`), `relations` (los holders,
|
|
408
|
+
* `group` y los tipos de la config) y `all` (el modelo ENTERO que generaría la config, más todo tipo que el servido
|
|
409
|
+
* traiga de más: la relectura de `openfga:provision`). Las condiciones y `schema_version` se comparan SIEMPRE.
|
|
410
|
+
*/
|
|
411
|
+
export type FactsModelSlice = 'roles' | 'relations' | 'all';
|
|
412
|
+
/** Una diferencia del modelo servido respecto del generado, por RUTA legible. `type: null` = global (condiciones…). */
|
|
413
|
+
export interface FactsModelDifference {
|
|
414
|
+
type: string | null;
|
|
415
|
+
path: string;
|
|
416
|
+
}
|
|
417
|
+
export interface FactsModelComparison {
|
|
418
|
+
equal: boolean;
|
|
419
|
+
differences: FactsModelDifference[];
|
|
420
|
+
/**
|
|
421
|
+
* La forma con la que el modelo servido trae cada tipo de relaciones de la config: `deny` (con `denied_<rel>`) o
|
|
422
|
+
* `plain`. Con opt-in siempre `deny`; sin él, **D8 por TIPO ENTERO** (cierra P2: un modelo MEZCLADO, unas relaciones
|
|
423
|
+
* con la forma con deny y otras sin ella, no lo genera ninguna config y es diferencia). Lo usa `decideMany`.
|
|
424
|
+
*/
|
|
425
|
+
denyForm: ReadonlyMap<string, 'plain' | 'deny'>;
|
|
426
|
+
/** P_M: los permisos que declara el propio modelo servido (las bases de `role#permits_*`), ordenados. */
|
|
427
|
+
permissions: string[];
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* **Forma canónica de un modelo (o de cualquier trozo) de OpenFGA** para compararlo con el que genera el paquete
|
|
431
|
+
* (L11; extiende `canonicalRewrite`). MEDIDO contra v1.19.0 (`:8090`): el servidor devuelve el ruido de proto3
|
|
432
|
+
* —`module: ""`, `source_info: null`, `condition: ""` en cada restricción, `metadata: null`, `generic_types: []`,
|
|
433
|
+
* `object: ""` en cada `computedUserset`— y otro orden de claves. Se quitan los valores por defecto de proto3 (`""`,
|
|
434
|
+
* `null`, `[]`), **NUNCA un `{}`** (`this: {}` y `wildcard: {}` significan algo), se ordenan las claves y los hijos
|
|
435
|
+
* CONMUTATIVOS (`child` de `union`/`intersection` y `directly_related_user_types`; `difference` tiene `base`/
|
|
436
|
+
* `subtract` con nombre y no se toca). Una restricción DUPLICADA no se funde: es una diferencia.
|
|
437
|
+
*/
|
|
438
|
+
export declare function canonicalModel(node: unknown): unknown;
|
|
439
|
+
/**
|
|
440
|
+
* **El punto de paso ÚNICO de L11** (lote «antes de RC», decisión del dueño 2026-10-17 (1); linaje L11): compara el
|
|
441
|
+
* modelo que SIRVE el servidor con el que genera la config de este proceso, POR TIPO, en reglas, restricciones de
|
|
442
|
+
* tipo (con su condición) y condiciones. Pura y sin SDK. Determinista: no busca entre combinaciones.
|
|
443
|
+
*
|
|
444
|
+
* 1. `P_M` = los permisos que declara el propio servido (`role#permits_*`): la igualdad es «módulo el catálogo», que
|
|
445
|
+
* cambia en runtime (el desfase catálogo ↔ modelo lo cuenta el reconcile, `permission-not-in-model`).
|
|
446
|
+
* 2. `D8_M` = los tipos de la config SIN opt-in que el servido trae con la forma con deny (algún `denied_*`): se
|
|
447
|
+
* generan con `deny` para comparar — por TIPO ENTERO —.
|
|
448
|
+
* 3. Se genera `openFgaFactsModel(holderTypes, P_M, config con D8_M)`; si el generador lanza (un `permits_*` del
|
|
449
|
+
* servido que no es un nombre publicable), es 500 `E_AUTHZ_CONFIG`, nunca «igual» (H11 d).
|
|
450
|
+
* 4. Diferencias por ruta: `<tipo> (tipo ausente)`, `<tipo>#<rel> (falta)`, `(relación de más)` —una relación EXTRA
|
|
451
|
+
* es deriva, D-L11-3—, `(regla)`, `(restricciones de tipo)`, `<tipo>.<campo>`, `conditions.<nombre> (…)`,
|
|
452
|
+
* `schema_version`. Un tipo AJENO al `slice` no se mira (límite declarado y razonado, caso C7); con `all`, sí.
|
|
453
|
+
*/
|
|
454
|
+
export declare function factsModelDifferences(served: unknown, inputs: {
|
|
455
|
+
holderTypes: HolderTypeMap;
|
|
456
|
+
relations?: FactsRelationsConfig;
|
|
457
|
+
slice: FactsModelSlice;
|
|
458
|
+
}): FactsModelComparison;
|
|
459
|
+
/** Cuántas rutas nombra como mucho la letra del 500 de L11 (el resto se cuenta). */
|
|
460
|
+
export declare const FACTS_MODEL_DRIFT_LISTED = 20;
|
|
461
|
+
/**
|
|
462
|
+
* **La LETRA del 500 de la puerta L11**, la MISMA en los dos drivers `openfga`: qué driver, qué operación, qué
|
|
463
|
+
* `modelId`, las diferencias por ruta y la receta. Cero llamadas que evalúen o escriban.
|
|
464
|
+
*/
|
|
465
|
+
export declare function factsModelDriftError(driver: string, operation: string, modelId: string | undefined, differences: readonly FactsModelDifference[]): AuthorizationConfigError & {
|
|
466
|
+
modelDifferences: string[];
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* **El token del opt-in de `openfga` EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21, «(b) con
|
|
470
|
+
* candado»). Lleva la VERSIÓN a propósito (auditor B1/B7, tester E1): con `true` nada obligaría a releer los límites NUEVOS
|
|
471
|
+
* cuando cambien; con `'openfga@2.5'` la 2.6 cambiará el token y un `^2.5.0` que se actualice hace ruido (500) en vez de
|
|
472
|
+
* colarse. NO se exporta por `/openfga`: el consumidor escribe el LITERAL, que es lo que demuestra que leyó «Limits».
|
|
473
|
+
*/
|
|
474
|
+
export declare const OPENFGA_EXPERIMENTAL_TOKEN = "openfga@2.5";
|
|
475
|
+
/**
|
|
476
|
+
* **La puerta del opt-in experimental, en los DOS constructores `openfga`** (firma 4; mismo patrón que
|
|
477
|
+
* `E_AUTHZ_SCOPE_DRIFT_UNGUARDED`: «una recomendación no es un mecanismo»). En 2.5.x `openfga` tiene límites declarados
|
|
478
|
+
* en README «Limits» (carreras entre escritores, árbol con outbox sin medir, el gemelo `permits_*`…) que se cierran en
|
|
479
|
+
* 2.6, así que construirlo exige escribir `experimental: 'openfga@2.5'`. Cualquier otra cosa —ausente, `true`,
|
|
480
|
+
* `'acknowledged'`, otra versión, el token con espacios— es 500 `E_AUTHZ_CONFIG` al CONSTRUIR, como PRIMERA línea del
|
|
481
|
+
* constructor: ni se valida lo demás ni se crea el cliente, así que hay CERO llamadas al servidor. Va en el constructor
|
|
482
|
+
* y no en el stub ni en el registro de la factory: `manager.driver()` (y con él `authz:catalog:sync`),
|
|
483
|
+
* `authz:reconcile --to=openfga`, `authz:relations:reconcile`, el provider y la construcción directa pasan todos por
|
|
484
|
+
* aquí, y un config solo-`database` que REGISTRA la factory `openfga` sin usarla arranca (las factories son perezosas).
|
|
485
|
+
* `openfga:provision` NO construye un driver (crea el store y publica el modelo con el SDK): corre sin la marca, y el
|
|
486
|
+
* primer driver que sirva con ese store, no.
|
|
487
|
+
*/
|
|
488
|
+
export declare function assertExperimentalOptIn(driver: string, value: unknown): void;
|
|
489
|
+
/**
|
|
490
|
+
* **`modelId` OBLIGATORIO en los dos drivers `openfga`** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2);
|
|
491
|
+
* D14 ampliado: siempre, con o sin deny). Sin el pin la puerta L11 compara el modelo que el driver lee al arrancar y el
|
|
492
|
+
* servidor evalúa cada petición con el ÚLTIMO del store: un `openfga:provision` desde otro proceso cambiaría lo que se
|
|
493
|
+
* concede sin que ninguna comparación lo vea. 500 al CONSTRUIR, con la receta.
|
|
494
|
+
*/
|
|
495
|
+
export declare function assertPinnedModel(driver: string, modelId: unknown): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { InvalidIdentityError, InvalidSlugError, ModelTooLargeError, AuthorizationConfigError, RelationConfigError, } from '../errors.js';
|
|
1
|
+
import { InvalidIdentityError, InvalidSlugError, ModelTooLargeError, AuthorizationConfigError, RelationConfigError, markDeploymentFailure, } from '../errors.js';
|
|
2
2
|
import { slugAsRelation, RESERVED_SLUG_PREFIXES, RESERVED_FACTS_TYPES } from '../identity.js';
|
|
3
3
|
import { APP_SCOPE_TYPE } from '../types.js';
|
|
4
|
+
import { FGA_EXPIRY_CONDITION } from '../expiry.js';
|
|
4
5
|
/**
|
|
5
6
|
* Modo `facts` — el MODELO (c2) y la proyección del catálogo, sin `@openfga/sdk`.
|
|
6
7
|
*
|
|
@@ -202,7 +203,7 @@ export const FACTS_ROOTED_RELATION = 'rooted';
|
|
|
202
203
|
* `role_binding#assignee` desde el modo `facts`, y desde R-15 también la de
|
|
203
204
|
* cada sujeto de una relación ReBAC (`document#viewer`, `group#member`…).
|
|
204
205
|
*/
|
|
205
|
-
export const FACTS_EXPIRY_CONDITION =
|
|
206
|
+
export const FACTS_EXPIRY_CONDITION = FGA_EXPIRY_CONDITION;
|
|
206
207
|
/* ── Relaciones derivadas de un permiso ─────────────────────────────────── */
|
|
207
208
|
/**
|
|
208
209
|
* Relaciones propias del modelo (no derivadas de ningún permiso). Un permiso
|
|
@@ -899,4 +900,277 @@ export function canonicalRewrite(node) {
|
|
|
899
900
|
}
|
|
900
901
|
return out;
|
|
901
902
|
}
|
|
902
|
-
|
|
903
|
+
/**
|
|
904
|
+
* **Forma canónica de un modelo (o de cualquier trozo) de OpenFGA** para compararlo con el que genera el paquete
|
|
905
|
+
* (L11; extiende `canonicalRewrite`). MEDIDO contra v1.19.0 (`:8090`): el servidor devuelve el ruido de proto3
|
|
906
|
+
* —`module: ""`, `source_info: null`, `condition: ""` en cada restricción, `metadata: null`, `generic_types: []`,
|
|
907
|
+
* `object: ""` en cada `computedUserset`— y otro orden de claves. Se quitan los valores por defecto de proto3 (`""`,
|
|
908
|
+
* `null`, `[]`), **NUNCA un `{}`** (`this: {}` y `wildcard: {}` significan algo), se ordenan las claves y los hijos
|
|
909
|
+
* CONMUTATIVOS (`child` de `union`/`intersection` y `directly_related_user_types`; `difference` tiene `base`/
|
|
910
|
+
* `subtract` con nombre y no se toca). Una restricción DUPLICADA no se funde: es una diferencia.
|
|
911
|
+
*/
|
|
912
|
+
export function canonicalModel(node) {
|
|
913
|
+
if (Array.isArray(node))
|
|
914
|
+
return node.map(canonicalModel);
|
|
915
|
+
if (node === null || typeof node !== 'object')
|
|
916
|
+
return node;
|
|
917
|
+
// Cierre de L11 (⚪ A3.2 del auditor): SIN prototipo — una relación o condición de más llamada `__proto__` (el servidor
|
|
918
|
+
// v1.19 la acepta, medido) cambiaba el prototipo de un objeto literal en vez de ser una clave, y el comparador decía
|
|
919
|
+
// «igual» (D-L11-3: relación de más = deriva). Con `Object.create(null)` es una clave propia más y se ve.
|
|
920
|
+
const out = Object.create(null);
|
|
921
|
+
for (const key of Object.keys(node).sort()) {
|
|
922
|
+
const value = node[key];
|
|
923
|
+
if (value === undefined || value === null || value === '')
|
|
924
|
+
continue;
|
|
925
|
+
if (Array.isArray(value) && value.length === 0)
|
|
926
|
+
continue;
|
|
927
|
+
out[key] = canonicalModel(value);
|
|
928
|
+
}
|
|
929
|
+
for (const key of ['child', 'directly_related_user_types']) {
|
|
930
|
+
if (Array.isArray(out[key]))
|
|
931
|
+
out[key] = sortByJson(out[key]);
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
function sortByJson(items) {
|
|
936
|
+
return [...items].sort((a, b) => {
|
|
937
|
+
const x = JSON.stringify(a);
|
|
938
|
+
const y = JSON.stringify(b);
|
|
939
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const sameCanon = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
943
|
+
/**
|
|
944
|
+
* **El punto de paso ÚNICO de L11** (lote «antes de RC», decisión del dueño 2026-10-17 (1); linaje L11): compara el
|
|
945
|
+
* modelo que SIRVE el servidor con el que genera la config de este proceso, POR TIPO, en reglas, restricciones de
|
|
946
|
+
* tipo (con su condición) y condiciones. Pura y sin SDK. Determinista: no busca entre combinaciones.
|
|
947
|
+
*
|
|
948
|
+
* 1. `P_M` = los permisos que declara el propio servido (`role#permits_*`): la igualdad es «módulo el catálogo», que
|
|
949
|
+
* cambia en runtime (el desfase catálogo ↔ modelo lo cuenta el reconcile, `permission-not-in-model`).
|
|
950
|
+
* 2. `D8_M` = los tipos de la config SIN opt-in que el servido trae con la forma con deny (algún `denied_*`): se
|
|
951
|
+
* generan con `deny` para comparar — por TIPO ENTERO —.
|
|
952
|
+
* 3. Se genera `openFgaFactsModel(holderTypes, P_M, config con D8_M)`; si el generador lanza (un `permits_*` del
|
|
953
|
+
* servido que no es un nombre publicable), es 500 `E_AUTHZ_CONFIG`, nunca «igual» (H11 d).
|
|
954
|
+
* 4. Diferencias por ruta: `<tipo> (tipo ausente)`, `<tipo>#<rel> (falta)`, `(relación de más)` —una relación EXTRA
|
|
955
|
+
* es deriva, D-L11-3—, `(regla)`, `(restricciones de tipo)`, `<tipo>.<campo>`, `conditions.<nombre> (…)`,
|
|
956
|
+
* `schema_version`. Un tipo AJENO al `slice` no se mira (límite declarado y razonado, caso C7); con `all`, sí.
|
|
957
|
+
*/
|
|
958
|
+
export function factsModelDifferences(served, inputs) {
|
|
959
|
+
const model = served;
|
|
960
|
+
if (!model || typeof model !== 'object' || !Array.isArray(model.type_definitions)) {
|
|
961
|
+
throw new AuthorizationConfigError('L11: el modelo servido no trae type_definitions; no se puede comparar con el que genera la config (no se toma por igual).');
|
|
962
|
+
}
|
|
963
|
+
const differences = [];
|
|
964
|
+
const servedTypes = new Map();
|
|
965
|
+
for (const definition of model.type_definitions) {
|
|
966
|
+
const type = definition?.type;
|
|
967
|
+
if (typeof type !== 'string' || type === '') {
|
|
968
|
+
differences.push({ type: null, path: 'type_definitions (un tipo sin nombre)' });
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (servedTypes.has(type))
|
|
972
|
+
differences.push({ type, path: `${type} (tipo duplicado)` });
|
|
973
|
+
servedTypes.set(type, definition);
|
|
974
|
+
}
|
|
975
|
+
const permissions = inputs.slice === 'relations'
|
|
976
|
+
? []
|
|
977
|
+
: Object.keys(servedTypes.get(FACTS_ROLE_TYPE)?.relations ?? {})
|
|
978
|
+
.filter((name) => name.startsWith(FACTS_PERMITS_PREFIX))
|
|
979
|
+
.map((name) => name.slice(FACTS_PERMITS_PREFIX.length))
|
|
980
|
+
.sort();
|
|
981
|
+
const denyForm = new Map();
|
|
982
|
+
let relationsConfig;
|
|
983
|
+
if (inputs.relations && inputs.slice !== 'roles') {
|
|
984
|
+
relationsConfig = {
|
|
985
|
+
objectTypes: inputs.relations.objectTypes.map((spec) => {
|
|
986
|
+
if (spec.deny !== undefined) {
|
|
987
|
+
denyForm.set(spec.type, 'deny');
|
|
988
|
+
return spec;
|
|
989
|
+
}
|
|
990
|
+
const d8 = Object.keys(servedTypes.get(spec.type)?.relations ?? {}).some((name) => name.startsWith(FACTS_DENIED_PREFIX));
|
|
991
|
+
denyForm.set(spec.type, d8 ? 'deny' : 'plain');
|
|
992
|
+
return d8 ? { ...spec, deny: 'read' } : spec;
|
|
993
|
+
}),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
let generated;
|
|
997
|
+
try {
|
|
998
|
+
generated = openFgaFactsModel(inputs.holderTypes, permissions, relationsConfig);
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
throw markDeploymentFailure(new AuthorizationConfigError(`L11: no se puede generar el modelo de la config con los permisos que declara el modelo servido ` +
|
|
1002
|
+
`(${error?.message ?? String(error)}); no se toma por igual.`, { cause: error }));
|
|
1003
|
+
}
|
|
1004
|
+
const generatedTypes = new Map(generated.type_definitions.map((definition) => [definition.type, definition]));
|
|
1005
|
+
const holders = Object.values(inputs.holderTypes);
|
|
1006
|
+
const sliceTypes = inputs.slice === 'all'
|
|
1007
|
+
? [...generatedTypes.keys()]
|
|
1008
|
+
: inputs.slice === 'roles'
|
|
1009
|
+
? [...holders, FACTS_ROLE_TYPE, FACTS_BINDING_TYPE, FACTS_SCOPE_TYPE]
|
|
1010
|
+
: [...holders, FACTS_GROUP_TYPE, ...(inputs.relations?.objectTypes ?? []).map((t) => t.type)];
|
|
1011
|
+
for (const type of sliceTypes)
|
|
1012
|
+
compareTypeDefinition(type, servedTypes.get(type), generatedTypes.get(type), differences);
|
|
1013
|
+
if (inputs.slice === 'all') {
|
|
1014
|
+
for (const type of servedTypes.keys()) {
|
|
1015
|
+
if (!generatedTypes.has(type))
|
|
1016
|
+
differences.push({ type, path: `${type} (tipo de más)` });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const servedConditions = (canonicalModel(model.conditions ?? {}) ?? {});
|
|
1020
|
+
const generatedConditions = canonicalModel(generated.conditions);
|
|
1021
|
+
for (const name of [...new Set([...Object.keys(servedConditions), ...Object.keys(generatedConditions)])].sort()) {
|
|
1022
|
+
if (!(name in servedConditions))
|
|
1023
|
+
differences.push({ type: null, path: `conditions.${name} (falta)` });
|
|
1024
|
+
else if (!(name in generatedConditions))
|
|
1025
|
+
differences.push({ type: null, path: `conditions.${name} (condición de más)` });
|
|
1026
|
+
else if (!sameCanon(servedConditions[name], generatedConditions[name]))
|
|
1027
|
+
differences.push({ type: null, path: `conditions.${name} (distinta)` });
|
|
1028
|
+
}
|
|
1029
|
+
if (!sameCanon(canonicalModel(model.schema_version), generated.schema_version)) {
|
|
1030
|
+
differences.push({ type: null, path: `schema_version (${JSON.stringify(model.schema_version ?? null)})` });
|
|
1031
|
+
}
|
|
1032
|
+
for (const key of Object.keys(model).sort()) {
|
|
1033
|
+
if (['id', 'schema_version', 'type_definitions', 'conditions'].includes(key))
|
|
1034
|
+
continue;
|
|
1035
|
+
const value = model[key];
|
|
1036
|
+
if (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0))
|
|
1037
|
+
continue;
|
|
1038
|
+
differences.push({ type: null, path: `modelo.${key}` });
|
|
1039
|
+
}
|
|
1040
|
+
return { equal: differences.length === 0, differences, denyForm, permissions };
|
|
1041
|
+
}
|
|
1042
|
+
function compareTypeDefinition(type, served, generated, out) {
|
|
1043
|
+
if (generated === undefined)
|
|
1044
|
+
return;
|
|
1045
|
+
if (served === undefined) {
|
|
1046
|
+
out.push({ type, path: `${type} (tipo ausente del modelo)` });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const s = canonicalModel(served);
|
|
1050
|
+
const g = canonicalModel(generated);
|
|
1051
|
+
const sRelations = s.relations ?? {};
|
|
1052
|
+
const gRelations = g.relations ?? {};
|
|
1053
|
+
const reported = new Set();
|
|
1054
|
+
for (const name of [...new Set([...Object.keys(sRelations), ...Object.keys(gRelations)])].sort()) {
|
|
1055
|
+
if (!(name in sRelations))
|
|
1056
|
+
out.push({ type, path: `${type}#${name} (falta)` });
|
|
1057
|
+
else if (!(name in gRelations))
|
|
1058
|
+
out.push({ type, path: `${type}#${name} (relación de más)` });
|
|
1059
|
+
else if (!sameCanon(sRelations[name], gRelations[name]))
|
|
1060
|
+
out.push({ type, path: `${type}#${name} (regla)` });
|
|
1061
|
+
else
|
|
1062
|
+
continue;
|
|
1063
|
+
reported.add(name);
|
|
1064
|
+
}
|
|
1065
|
+
const sMeta = s.metadata?.relations ?? {};
|
|
1066
|
+
const gMeta = g.metadata?.relations ?? {};
|
|
1067
|
+
for (const name of [...new Set([...Object.keys(sMeta), ...Object.keys(gMeta)])].sort()) {
|
|
1068
|
+
if (reported.has(name))
|
|
1069
|
+
continue;
|
|
1070
|
+
if (!sameCanon(sMeta[name], gMeta[name]))
|
|
1071
|
+
out.push({ type, path: `${type}#${name} (restricciones de tipo)` });
|
|
1072
|
+
}
|
|
1073
|
+
const rest = (definition) => {
|
|
1074
|
+
const { type: _t, relations: _r, metadata, ...others } = definition;
|
|
1075
|
+
const { relations: _mr, ...metaOthers } = (metadata ?? {});
|
|
1076
|
+
return { others, metaOthers };
|
|
1077
|
+
};
|
|
1078
|
+
const sr = rest(s);
|
|
1079
|
+
const gr = rest(g);
|
|
1080
|
+
for (const key of [...new Set([...Object.keys(sr.others), ...Object.keys(gr.others)])].sort()) {
|
|
1081
|
+
if (!sameCanon(sr.others[key], gr.others[key]))
|
|
1082
|
+
out.push({ type, path: `${type}.${key}` });
|
|
1083
|
+
}
|
|
1084
|
+
for (const key of [...new Set([...Object.keys(sr.metaOthers), ...Object.keys(gr.metaOthers)])].sort()) {
|
|
1085
|
+
if (!sameCanon(sr.metaOthers[key], gr.metaOthers[key]))
|
|
1086
|
+
out.push({ type, path: `${type}.metadata.${key}` });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/** Cuántas rutas nombra como mucho la letra del 500 de L11 (el resto se cuenta). */
|
|
1090
|
+
export const FACTS_MODEL_DRIFT_LISTED = 20;
|
|
1091
|
+
/**
|
|
1092
|
+
* **La LETRA del 500 de la puerta L11**, la MISMA en los dos drivers `openfga`: qué driver, qué operación, qué
|
|
1093
|
+
* `modelId`, las diferencias por ruta y la receta. Cero llamadas que evalúen o escriban.
|
|
1094
|
+
*/
|
|
1095
|
+
export function factsModelDriftError(driver, operation, modelId, differences) {
|
|
1096
|
+
const paths = differences.map((d) => d.path);
|
|
1097
|
+
const listed = paths.slice(0, FACTS_MODEL_DRIFT_LISTED).join('; ');
|
|
1098
|
+
const more = paths.length > FACTS_MODEL_DRIFT_LISTED ? ` … y ${paths.length - FACTS_MODEL_DRIFT_LISTED} más` : '';
|
|
1099
|
+
const error = new AuthorizationConfigError(`${driver}.${operation}: el modelo que sirve este driver (modelId ${modelId ?? '(ninguno)'}) NO es el que genera la ` +
|
|
1100
|
+
`config de este proceso (L11: reglas, restricciones de tipo y condiciones, por tipo) — ${listed}${more}. Con ese ` +
|
|
1101
|
+
'modelo el servidor decide con reglas o condiciones que la config no declara: puede conceder lo que la config —y ' +
|
|
1102
|
+
'database con la misma config— niega, o leer caducidades con otra condición. Republica el modelo desde la config ' +
|
|
1103
|
+
'actual (node ace openfga:provision --store-id=…) y fija el modelId nuevo (OPENFGA_MODEL_ID) en TODA la flota antes ' +
|
|
1104
|
+
'de desplegar. No se ha llamado al servidor para evaluar ni escribir nada.');
|
|
1105
|
+
error.modelDifferences = paths;
|
|
1106
|
+
// Cierre de L11 (🟠 A1): fallo del DESPLIEGUE ⇒ transitorio para el relay (no consume intento ni aparca).
|
|
1107
|
+
return markDeploymentFailure(error);
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* **El token del opt-in de `openfga` EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21, «(b) con
|
|
1111
|
+
* candado»). Lleva la VERSIÓN a propósito (auditor B1/B7, tester E1): con `true` nada obligaría a releer los límites NUEVOS
|
|
1112
|
+
* cuando cambien; con `'openfga@2.5'` la 2.6 cambiará el token y un `^2.5.0` que se actualice hace ruido (500) en vez de
|
|
1113
|
+
* colarse. NO se exporta por `/openfga`: el consumidor escribe el LITERAL, que es lo que demuestra que leyó «Limits».
|
|
1114
|
+
*/
|
|
1115
|
+
export const OPENFGA_EXPERIMENTAL_TOKEN = 'openfga@2.5';
|
|
1116
|
+
/**
|
|
1117
|
+
* **La puerta del opt-in experimental, en los DOS constructores `openfga`** (firma 4; mismo patrón que
|
|
1118
|
+
* `E_AUTHZ_SCOPE_DRIFT_UNGUARDED`: «una recomendación no es un mecanismo»). En 2.5.x `openfga` tiene límites declarados
|
|
1119
|
+
* en README «Limits» (carreras entre escritores, árbol con outbox sin medir, el gemelo `permits_*`…) que se cierran en
|
|
1120
|
+
* 2.6, así que construirlo exige escribir `experimental: 'openfga@2.5'`. Cualquier otra cosa —ausente, `true`,
|
|
1121
|
+
* `'acknowledged'`, otra versión, el token con espacios— es 500 `E_AUTHZ_CONFIG` al CONSTRUIR, como PRIMERA línea del
|
|
1122
|
+
* constructor: ni se valida lo demás ni se crea el cliente, así que hay CERO llamadas al servidor. Va en el constructor
|
|
1123
|
+
* y no en el stub ni en el registro de la factory: `manager.driver()` (y con él `authz:catalog:sync`),
|
|
1124
|
+
* `authz:reconcile --to=openfga`, `authz:relations:reconcile`, el provider y la construcción directa pasan todos por
|
|
1125
|
+
* aquí, y un config solo-`database` que REGISTRA la factory `openfga` sin usarla arranca (las factories son perezosas).
|
|
1126
|
+
* `openfga:provision` NO construye un driver (crea el store y publica el modelo con el SDK): corre sin la marca, y el
|
|
1127
|
+
* primer driver que sirva con ese store, no.
|
|
1128
|
+
*/
|
|
1129
|
+
export function assertExperimentalOptIn(driver, value) {
|
|
1130
|
+
if (typeof value === 'string' && value === OPENFGA_EXPERIMENTAL_TOKEN)
|
|
1131
|
+
return;
|
|
1132
|
+
throw new AuthorizationConfigError(experimentalOptInMessage(driver, describeOptInValue(value)));
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Qué llegó, SIN tocar el valor (cierre 1 del candado, ⚪ 5 del auditor): `String(value)` de un `Object.create(null)` o de
|
|
1136
|
+
* un objeto cuyo `toString` lanza sustituía el 500 por un `TypeError` o por el error del consumidor. Solo `typeof` y, para
|
|
1137
|
+
* un string, `JSON.stringify` (que no invoca nada del valor).
|
|
1138
|
+
*/
|
|
1139
|
+
function describeOptInValue(value) {
|
|
1140
|
+
if (value === undefined)
|
|
1141
|
+
return 'nada';
|
|
1142
|
+
if (value === null)
|
|
1143
|
+
return 'null';
|
|
1144
|
+
if (typeof value === 'string')
|
|
1145
|
+
return JSON.stringify(value);
|
|
1146
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'bigint')
|
|
1147
|
+
return `${typeof value} ${value}`;
|
|
1148
|
+
return `un ${Array.isArray(value) ? 'array' : typeof value}`;
|
|
1149
|
+
}
|
|
1150
|
+
function experimentalOptInMessage(driver, got) {
|
|
1151
|
+
return (`${driver}: el driver openfga es EXPERIMENTAL en 2.5.x y NO es apto para producción: tiene límites declarados ` +
|
|
1152
|
+
'(README «Limits»: carreras entre escritores del driver de roles, el árbol con outbox sin medir, el gemelo ' +
|
|
1153
|
+
'permits_*…) que se cierran en 2.6. Para usarlo igualmente (CI, ensayo, desarrollo) opta de forma EXPLÍCITA con ' +
|
|
1154
|
+
`experimental: '${OPENFGA_EXPERIMENTAL_TOKEN}' en las opciones del driver (llegó ${got}). El token lleva la versión ` +
|
|
1155
|
+
'a propósito: true u otra versión no valen; el token y el comportamiento experimental de openfga pueden cambiar en ' +
|
|
1156
|
+
'2.6 (una minor) sin major. ' +
|
|
1157
|
+
'En producción usa database; para MIGRAR a database desde openfga (authz:reconcile --to=database, con openfga como ' +
|
|
1158
|
+
'ORIGEN) la factory openfga también necesita la marca: leer el store para salir de él es construir este driver. ' +
|
|
1159
|
+
'No se ha creado el cliente ni se ha llamado al servidor.');
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* **`modelId` OBLIGATORIO en los dos drivers `openfga`** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2);
|
|
1163
|
+
* D14 ampliado: siempre, con o sin deny). Sin el pin la puerta L11 compara el modelo que el driver lee al arrancar y el
|
|
1164
|
+
* servidor evalúa cada petición con el ÚLTIMO del store: un `openfga:provision` desde otro proceso cambiaría lo que se
|
|
1165
|
+
* concede sin que ninguna comparación lo vea. 500 al CONSTRUIR, con la receta.
|
|
1166
|
+
*/
|
|
1167
|
+
export function assertPinnedModel(driver, modelId) {
|
|
1168
|
+
if (typeof modelId === 'string' && modelId.trim() !== '')
|
|
1169
|
+
return;
|
|
1170
|
+
throw new AuthorizationConfigError(`${driver}: falta modelId (llegó ${JSON.stringify(modelId ?? null)}). Desde 2.5.0-alpha.4 el modelo FIJADO es ` +
|
|
1171
|
+
'obligatorio en los dos drivers openfga, con o sin deny (L11): el driver comprueba UNA vez que ese modelo es el que ' +
|
|
1172
|
+
'genera tu config y el servidor evalúa con ESE modelo; sin el pin evaluaría con el último del store. Receta: ' +
|
|
1173
|
+
'`node ace openfga:provision` (o `--store-id=$OPENFGA_STORE_ID` sobre un store existente) imprime OPENFGA_MODEL_ID; ' +
|
|
1174
|
+
'ponlo en el .env de TODA la flota y pásalo como modelId (el stub de config/authorization.ts lo hace con ' +
|
|
1175
|
+
"env.get('OPENFGA_MODEL_ID')).");
|
|
1176
|
+
}
|
|
@@ -7,8 +7,10 @@ export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
|
7
7
|
* **Tope de operaciones por `Write` en FGA** — `--max-tuples-per-write` del servidor, default 100. MEDIDO en v1.19.0
|
|
8
8
|
* (`openfga-dev`, lote D · cierre 1 de D1): 100 deletes en un `Write` entran; 101 —o 50 writes + 51 deletes— es 400
|
|
9
9
|
* «The number of write operations exceeds the allowed limit of 100» y NO se aplica nada (el `Write` es transaccional).
|
|
10
|
-
* Es también la frontera de la purga atómica (`#purgeByFilters`): una purga que cabe aquí es UN
|
|
11
|
-
*
|
|
10
|
+
* Es también la frontera de la purga atómica PARA EL ESCRITOR (`#purgeByFilters`): una purga que cabe aquí es UN
|
|
11
|
+
* `Write`, sin PREFIJO que cortar —un `Check` concurrente SÍ puede observar ese `Write` a medias: lector rasgado, fila
|
|
12
|
+
* «torn read» de Limits, decisión (6a) del dueño—; con más membresías de grupo que esto, el residuo es una fila DECLARADA
|
|
13
|
+
* (README, «Deny on relations»).
|
|
12
14
|
* **Requisito del despliegue: `--max-tuples-per-write` ≥ 100** (README, «Limits»). Un servidor con el tope MÁS BAJO
|
|
13
15
|
* responde 400 `exceeded_entity_limit` al primer `Write` que lo pase, y ése puede no ser el primero de la purga (con
|
|
14
16
|
* tope 50 y `[3 allows][60 membresías][40 denies]` el `Write` de 3 ya se aplicó): el driver lo dice como **500
|
|
@@ -56,8 +58,12 @@ export declare const MONOTONIC_CLOCK: unique symbol;
|
|
|
56
58
|
export interface OpenFgaRelationsDriverOptions {
|
|
57
59
|
apiUrl: string;
|
|
58
60
|
storeId: string;
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
+
/**
|
|
62
|
+
* **Pin del model FUSIONADO — OBLIGATORIO** (L11, decisión del dueño 2026-10-17 (2): D14 ampliado a los dos drivers
|
|
63
|
+
* `openfga`, siempre). Sin él cada `Check` evalúa contra el ÚLTIMO modelo del store mientras la puerta L11 compara
|
|
64
|
+
* el del arranque. `openfga:provision` lo imprime (`OPENFGA_MODEL_ID`).
|
|
65
|
+
*/
|
|
66
|
+
modelId: string;
|
|
61
67
|
/** Holders del consumidor: morph name → tipo FGA. El MISMO mapa del driver de roles. */
|
|
62
68
|
holderTypes: HolderTypeMap;
|
|
63
69
|
/** Deadline de cada llamada en ms (default 5000): vencido ⇒ 503. */
|
|
@@ -106,6 +112,12 @@ export interface OpenFgaRelationsDriverOptions {
|
|
|
106
112
|
* `RelationsManager` con `clock` (`withClock`).
|
|
107
113
|
*/
|
|
108
114
|
now?: Clock;
|
|
115
|
+
/**
|
|
116
|
+
* **`openfga` es EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4): el MISMO literal versionado que el driver de roles.
|
|
117
|
+
* Sin él, 500 `E_AUTHZ_CONFIG` al construir (primera línea, sin crear el cliente). Opcional en el tipo para que el stub
|
|
118
|
+
* que solo registra la factory compile; obligatorio al construir.
|
|
119
|
+
*/
|
|
120
|
+
experimental?: 'openfga@2.5';
|
|
109
121
|
}
|
|
110
122
|
export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
111
123
|
#private;
|
|
@@ -149,6 +161,16 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
149
161
|
*/
|
|
150
162
|
withClock(now: Clock): RelationsDriver;
|
|
151
163
|
relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* **El techo de anidación de grupos de ESTE driver** (lote «antes de RC» · R1, decisiones del dueño 2026-10-17 (5) y
|
|
166
|
+
* (5-bis), opción (a)): ACTIVO por defecto. Se CALIBRA contra el servidor (`#calibratedGroupDepth`) y es el mínimo de
|
|
167
|
+
* la sonda positiva y la negativa (22 en OpenFGA v1.19 con `--resolve-node-limit` por defecto, medido en F0 · M-c); el
|
|
168
|
+
* `maxGroupDepth` de la config solo puede BAJARLO — uno mayor que el calibrado es 500 `E_AUTHZ_CONFIG` con las dos
|
|
169
|
+
* cifras (el despliegue promete un techo que el servidor no resuelve) —, y `false` lo apaga de forma explícita (sin
|
|
170
|
+
* calibrar). Solo lectura: lo usa `relate` antes de escribir una arista `group#member` y el plan de
|
|
171
|
+
* `reconcileRelations` sobre el destino.
|
|
172
|
+
*/
|
|
173
|
+
groupDepthLimit(): Promise<number | null>;
|
|
152
174
|
unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
|
|
153
175
|
/**
|
|
154
176
|
* **`deny` = UN `Write` de la tupla `denied_<rel>`** (lote D · D1): sin caducidad (D3), así que no hay renovación
|
|
@@ -156,14 +178,14 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
156
178
|
* CON condición devuelve 409 y, desde el cierre 1 de D2 (🟡 4), sale como 500 `E_AUTHZ_CONFIG` con la receta
|
|
157
179
|
* (`AuthorizationConfigError.conditionalDeny`; hasta entonces era un 503 que se repetía para siempre). Antes del `Write`: la
|
|
158
180
|
* puerta de `{ transaction }` (L-5), F-05 sobre el nombre PÚBLICO, la puerta del opt-in (fase `'write'`), sin
|
|
159
|
-
* caducidad, la partición, `*`, el sujeto y la
|
|
181
|
+
* caducidad, la partición, `*`, el sujeto y la puerta del modelo (L11). CERO llamadas si algo de eso falla.
|
|
160
182
|
*/
|
|
161
183
|
deny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
|
|
162
184
|
/**
|
|
163
185
|
* **`removeDeny` = UN delete de `denied_<rel>`** (no-op si no está). Sobre CUALQUIER tipo declarado (D8/R2-4 b):
|
|
164
186
|
* `Write` con delete no valida la relación contra el modelo (medido en v1.19.0), así que un deny vivo de un tipo
|
|
165
187
|
* que retiró el opt-in se puede borrar. Ojo: quitar un deny CONCEDE (filas declaradas S8–S10).
|
|
166
|
-
* **La
|
|
188
|
+
* **La puerta del modelo (L11, antes D-31) SÍ corre aquí** (letra corregida en el cierre 1 de D1, ⚪ 5 del auditor:
|
|
167
189
|
* la decisión 4 del informe decía que no): con el opt-in vivo y un modelo fijado sin `denied_*` ⇒ 500 antes del
|
|
168
190
|
* delete. Con el opt-in RETIRADO no hay guarda (el tipo no está en la config con `deny`) y el delete borra de verdad
|
|
169
191
|
* (medido por el auditor: `listDenies` de 1 a 0 y `check` vuelve a `true` con el modelo viejo) — retirar es reversible.
|
|
@@ -209,7 +231,7 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
209
231
|
decideMany(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<RelationDecision[]>;
|
|
210
232
|
/**
|
|
211
233
|
* **`denyPhase` — ¿admite este driver escrituras de deny en el tipo?** (lote D · D2, R2-5). La fase del opt-in EN
|
|
212
|
-
* LA CONFIG DE ESTE DRIVER y, con opt-in, el modelo FIJADO comprobado con la MISMA
|
|
234
|
+
* LA CONFIG DE ESTE DRIVER y, con opt-in, el modelo FIJADO comprobado con la MISMA puerta L11 que `deny` (500 si
|
|
213
235
|
* no declara `denied_<rel>`). F-05 estricto del par, antes de nada (422, cero llamadas). La llama
|
|
214
236
|
* `reconcileRelations` en la fase de plan cuando el origen trae denies: un destino `openfga` sin `'write'` en el
|
|
215
237
|
* tipo, o con un pin a un modelo sin el deny, NIEGA la pasada antes de la primera escritura (jamás `skipped`).
|
|
@@ -218,7 +240,7 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
218
240
|
type: string;
|
|
219
241
|
}, relation: string): Promise<'read' | 'write' | null>;
|
|
220
242
|
/** `listDenies` = `Read` por `(object, denied_<rel>)`: los denies DIRECTOS (hechos). Cualquier tipo declarado (D8). */
|
|
221
|
-
listDenies(relation: string, object: RelObject, partition: ScopeRef
|
|
243
|
+
listDenies(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
222
244
|
/**
|
|
223
245
|
* `listObjects` = `ListObjects` (resuelve includes + usersets del lado del
|
|
224
246
|
* servidor) + FILTRO por partición + señal de TRUNCADO (`#listObjectsPage`:
|
|
@@ -227,14 +249,14 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
227
249
|
* se consume entre todas — otra razón para señalar el truncado y no callar
|
|
228
250
|
* (documentado en el README).
|
|
229
251
|
*/
|
|
230
|
-
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef
|
|
252
|
+
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef): Promise<RelationObjectsPage>;
|
|
231
253
|
/**
|
|
232
254
|
* `listSubjects` = `Read` por `(object, relation)` — hechos DIRECTOS del
|
|
233
255
|
* relation EXACTO (invariante 7): ni transitivo (eso es `membersOf`) ni
|
|
234
256
|
* derivado por includes. `usersetSubjects: true` ⇒ los usersets
|
|
235
257
|
* (`group#member`) salen junto con los holders.
|
|
236
258
|
*/
|
|
237
|
-
listSubjects(relation: string, object: RelObject, partition: ScopeRef
|
|
259
|
+
listSubjects(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
238
260
|
/**
|
|
239
261
|
* **`membersOf` ⇒ 500 `E_AUTHZ_UNSUPPORTED`** (cara `whenFalse`): la
|
|
240
262
|
* membresía TRANSITIVA de un grupo sería `ListUsers`, que trunca al tope del
|
|
@@ -260,12 +282,19 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
260
282
|
* la CADUCADA también llega, con su `expiresAt` (R-15), para que el destino
|
|
261
283
|
* la cuente en `skipped`.
|
|
262
284
|
*
|
|
285
|
+
* **Una tupla con condición AJENA llega marcada** (`invalidCondition`, L11) y
|
|
286
|
+
* con `expiresAt` = la ÉPOCA. Eso es conservador SOLO para los allows: un
|
|
287
|
+
* lector que ignore la marca lee el deny (o la membresía de `group` que lo
|
|
288
|
+
* transporta) como VENCIDO y deja de negar —fail-OPEN— (cierre-2 · ⚪ C3).
|
|
289
|
+
* Ninguna fecha sirve para los dos tipos de tupla: el lector tiene que mirar
|
|
290
|
+
* `invalidCondition` ANTES que `expiresAt` (el plan del paquete lo hace).
|
|
291
|
+
*
|
|
263
292
|
* **Coste: O(tuplas del store) por PASADA** (2.4.0-alpha.6): es un barrido
|
|
264
293
|
* entero por llamada, sea cual sea el tamaño de la partición (medido: 5 000
|
|
265
294
|
* tuplas ⇒ 50 páginas ≈ 170 ms; 15 000 ⇒ 151 páginas ≈ 0,5 s). Para K
|
|
266
295
|
* particiones usa `enumerateRelationsMany`: UN barrido en vez de K.
|
|
267
296
|
*/
|
|
268
|
-
enumerateRelations(partition: ScopeRef,
|
|
297
|
+
enumerateRelations(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
|
|
269
298
|
/**
|
|
270
299
|
* **`enumerateRelationsMany` — K particiones en UN barrido** (2.4.0-alpha.6;
|
|
271
300
|
* panel relay, analista §3: con 3 particiones 1,4 s → 0,46 s). MISMA
|
|
@@ -292,7 +321,7 @@ export declare class OpenFgaRelationsDriver implements RelationsDriver {
|
|
|
292
321
|
* se cuenta en `diagnostics.unparseableTuples` y se avisa. Sin particiones
|
|
293
322
|
* es 500 `E_AUTHZ_CONFIG` con cero lecturas: no hay «éxito vacío».
|
|
294
323
|
*/
|
|
295
|
-
enumerateRelationsMany(partitions: ScopeRef[],
|
|
324
|
+
enumerateRelationsMany(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
|
|
296
325
|
/**
|
|
297
326
|
* Borra TODAS las tuplas del objeto (cualquier relación) y demuestra cero.
|
|
298
327
|
* Hasta 2.5.0 barría las DOS ortografías del uuid de partición (🟡2);
|
|
@@ -325,4 +354,3 @@ export declare function purgeWrites<K extends {
|
|
|
325
354
|
relation: string;
|
|
326
355
|
object: string;
|
|
327
356
|
}>(keys: readonly K[]): K[][];
|
|
328
|
-
//# sourceMappingURL=openfga_relations_driver.d.ts.map
|