@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
|
@@ -29,13 +29,15 @@ import { AuthorizationBackendTimeoutError, AuthorizationConfigError, InvalidIden
|
|
|
29
29
|
import { assertPartition, assertSubject, assertExpiresAt, partitionKey } from '../identity.js';
|
|
30
30
|
import { relationPartitionTrigger, relationPartitionTriggerDrops } from '../relation_partition_trigger.js';
|
|
31
31
|
import { isClock, systemClock } from '../clock.js';
|
|
32
|
-
import { resolveGrantExpiry, sameInstant } from '../expiry.js';
|
|
32
|
+
import { isActiveExpiry, resolveGrantExpiry, sameInstant } from '../expiry.js';
|
|
33
|
+
import { assertGroupDepthSettings, groupDepthLimitError, groupNestingVerdict, isGroupNesting, plannedByReconcile } from '../relations/group_depth.js';
|
|
33
34
|
import { sqlExpiryCodec } from '../shared/sql_expiry.js';
|
|
34
35
|
import { guardSql, isDeadlock, isUniqueViolation, withDeadline } from '../shared/backend_guard.js';
|
|
35
36
|
import { assertCallerTransaction } from '../shared/transaction_guard.js';
|
|
36
37
|
import { isRelUserset } from '../types.js';
|
|
37
38
|
import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject, assertRelationDenyable, assertDenySubject, assertDenyNoExpiry, cutPathAllows, decideFrom, deniableAbove, deniedRelationOf, storedRelationKind, denyPhaseOf, relationsDown, RELATION_DENY_PREFIX, } from '../relations/define_relations_config.js';
|
|
38
39
|
import { assertCheckManyObjects, distinctObjects, expandByPosition } from '../relations/check_many.js';
|
|
40
|
+
import { assertEnumeratePage, assertNoListPage, enumerateLimit } from '../relations/page.js';
|
|
39
41
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
40
42
|
/**
|
|
41
43
|
* **Cota de candidatos de `membersOf` cuando el objeto tiene un deny vigente** (lote D · cierre 1 de D1, 🟡 4): con deny
|
|
@@ -58,9 +60,6 @@ const GROUP_MEMBER = 'member';
|
|
|
58
60
|
* userset puede colisionar con ella.
|
|
59
61
|
*/
|
|
60
62
|
const HOLDER_RELATION = '';
|
|
61
|
-
/** Tamaño de página por defecto y tope de `enumerateRelations` (origen de reconcile). */
|
|
62
|
-
const DEFAULT_ENUMERATE_LIMIT = 100;
|
|
63
|
-
const MAX_ENUMERATE_LIMIT = 1_000;
|
|
64
63
|
/** Formato de id de relación: minúsculas, dígitos y `._-`; sin separadores del store. */
|
|
65
64
|
const RELATION_ID_FORMAT = /^[a-z0-9_.-]+$/;
|
|
66
65
|
const DIALECTS = {
|
|
@@ -116,6 +115,9 @@ export class DatabaseRelationsDriver {
|
|
|
116
115
|
#database;
|
|
117
116
|
#expiryCodec;
|
|
118
117
|
constructor(config, options = {}, database = db) {
|
|
118
|
+
// Cierre de R1 · 🟡 4 del auditor: el driver RE-VALIDA el techo de la config (una config extendida con spread no pasó
|
|
119
|
+
// por `defineRelationsConfig`; `NaN` o `enforceGroupDepth: true` sin número apagaban el gate en silencio). 500.
|
|
120
|
+
assertGroupDepthSettings(config.maxGroupDepth, config.enforceGroupDepth, 'DatabaseRelationsDriver');
|
|
119
121
|
this.#config = config;
|
|
120
122
|
this.#connectionName = options.connection;
|
|
121
123
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -137,6 +139,8 @@ export class DatabaseRelationsDriver {
|
|
|
137
139
|
// Cierre 2 de D2 · 🟠 N1: renovar (otra caducidad) es delete + insert en UNA transacción (`#upsert`): ningún
|
|
138
140
|
// instante sin la tupla, así que una membresía que transporta un deny se renueva sin ventana.
|
|
139
141
|
atomicRenewal: true,
|
|
142
|
+
// Estrategia 2.5.0 · firma 4 (D15): `false` EXPLÍCITO — `database` es el driver ESTABLE de 2.5.0.
|
|
143
|
+
experimental: false,
|
|
140
144
|
});
|
|
141
145
|
}
|
|
142
146
|
/**
|
|
@@ -367,6 +371,54 @@ export class DatabaseRelationsDriver {
|
|
|
367
371
|
this.#assertObject(object);
|
|
368
372
|
await this.#deleteTuple('removeDeny', subject, deniedRelationOf(relation), object, partition, options);
|
|
369
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* **El techo de anidación de grupos de ESTE driver** (lote «antes de RC» · R1, decisión del dueño 2026-10-17 (5)):
|
|
376
|
+
* `database` no tiene techo técnico (la CTE deduplica y termina con ciclos), así que es `null` SALVO el opt-in
|
|
377
|
+
* explícito `database.enforceGroupDepth: true`, que aplica el `maxGroupDepth` de la config (obligatorio y numérico
|
|
378
|
+
* con el opt-in: lo valida `defineRelationsConfig` al arrancar).
|
|
379
|
+
*/
|
|
380
|
+
async groupDepthLimit() {
|
|
381
|
+
return this.#groupDepthLimit();
|
|
382
|
+
}
|
|
383
|
+
#groupDepthLimit() {
|
|
384
|
+
const { maxGroupDepth, enforceGroupDepth } = assertGroupDepthSettings(this.#config.maxGroupDepth, this.#config.enforceGroupDepth, 'DatabaseRelationsDriver');
|
|
385
|
+
return enforceGroupDepth && typeof maxGroupDepth === 'number' ? maxGroupDepth : null;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* **El gate de R1 en `database`** (opt-in): la arista `group:<parent>#member@group:<child>#member` que se va a escribir
|
|
389
|
+
* VIVA no puede cerrar un ciclo ni dejar un camino de grupos anidados más largo que el techo. El grafo se LEE POR EL
|
|
390
|
+
* MISMO CLIENTE QUE ESCRIBE (`trx`: la transacción del llamante con `{ transaction }`, o la interna de la escritura) —
|
|
391
|
+
* T-6 del tester: leído por la conexión del motor, un llamante que en UNA transacción escribe `A⊂B` y luego `B⊂C`
|
|
392
|
+
* pasaría las dos (la segunda no ve la primera, sin confirmar). Lo que esto NO cierra, declarado: dos transacciones
|
|
393
|
+
* CONCURRENTES que escriben cada una media cadena (cada una ve su foto, sin cerrojo) — la carrera entre procesos.
|
|
394
|
+
* Una sentencia por nivel del BFS (y por cada 500 grupos del nivel), con la caducidad ESTRICTA de la lectura.
|
|
395
|
+
*/
|
|
396
|
+
async #assertGroupNesting(trx, operation, partitionKey, edge, now, limit) {
|
|
397
|
+
const neighbours = async (groups, column) => {
|
|
398
|
+
const out = [];
|
|
399
|
+
for (let i = 0; i < groups.length; i += 500) {
|
|
400
|
+
const chunk = groups.slice(i, i + 500);
|
|
401
|
+
const rows = await this.#sql(`${operation}.groupDepth`, () => this.#whereActive(trx
|
|
402
|
+
.from(RELATIONS_TABLE)
|
|
403
|
+
.where('partition_key', partitionKey)
|
|
404
|
+
.where('object_type', GROUP_TYPE)
|
|
405
|
+
.where('relation', GROUP_MEMBER)
|
|
406
|
+
.where('subject_type', GROUP_TYPE)
|
|
407
|
+
.where('subject_relation', GROUP_MEMBER)
|
|
408
|
+
.whereIn(column, chunk), now).select({ child: 'subject_uuid', parent: 'object_uuid' }));
|
|
409
|
+
for (const row of rows)
|
|
410
|
+
out.push({ child: row.child, parent: row.parent });
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
};
|
|
414
|
+
const verdict = await groupNestingVerdict(edge, limit, {
|
|
415
|
+
parentsOf: (groups) => neighbours(groups, 'subject_uuid'),
|
|
416
|
+
childrenOf: (groups) => neighbours(groups, 'object_uuid'),
|
|
417
|
+
});
|
|
418
|
+
if (verdict.kind !== 'ok') {
|
|
419
|
+
throw groupDepthLimitError(verdict, `group:${edge.parent}#member@group:${edge.child}#member`, limit);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
370
422
|
/** El check-then-delete-insert idempotente de `relate`/`deny` sobre la relación ALMACENADA. */
|
|
371
423
|
async #upsert(operation, subject, relation, object, partition, requested, options) {
|
|
372
424
|
const partitionKey = this.#keyOf(partition);
|
|
@@ -399,10 +451,21 @@ export class DatabaseRelationsDriver {
|
|
|
399
451
|
// Los tres estados (invariante 10): omitido preserva la VIGENTE (una
|
|
400
452
|
// caducada revive sin caducidad), null la quita, Date la fija.
|
|
401
453
|
const expiresAt = resolveGrantExpiry(previous, requested, now);
|
|
454
|
+
// Idempotente (invariante 6): la misma caducidad no reescribe nada — y va ANTES del gate de R1: un `relate`
|
|
455
|
+
// repetido de una arista preexistente (sembrada antes de alpha.4 o con el gate apagado) sigue siendo un no-op.
|
|
456
|
+
if (current && sameInstant(previous, expiresAt))
|
|
457
|
+
return;
|
|
458
|
+
// R1 (opt-in en `database`): una arista de anidación que queda VIVA —nueva, renovada o revivida— pasa el gate,
|
|
459
|
+
// leyendo el grafo por ESTE `trx` (T-6). Una vencida no conecta nada (la misma regla que la lectura). Antes del
|
|
460
|
+
// DELETE: un 422 no deja la arista vieja borrada.
|
|
461
|
+
const limit = this.#groupDepthLimit();
|
|
462
|
+
if (limit !== null &&
|
|
463
|
+
isGroupNesting(subject, relation, object) &&
|
|
464
|
+
isActiveExpiry(expiresAt, now) &&
|
|
465
|
+
!plannedByReconcile(options, partition, { child: subject.object.id, parent: object.id })) {
|
|
466
|
+
await this.#assertGroupNesting(trx, operation, partitionKey, { child: subject.object.id, parent: object.id }, now, limit);
|
|
467
|
+
}
|
|
402
468
|
if (current) {
|
|
403
|
-
// Idempotente (invariante 6): la misma caducidad no reescribe nada.
|
|
404
|
-
if (sameInstant(previous, expiresAt))
|
|
405
|
-
return;
|
|
406
469
|
// **INSERT/DELETE-ONLY (decisión (c))**: cambiar la caducidad es
|
|
407
470
|
// BORRAR la fila e INSERTAR otra —nunca un UPDATE—; la fila nueva
|
|
408
471
|
// tiene otro `uuid`, que es lo que lo hace observable. Dentro de la
|
|
@@ -592,10 +655,12 @@ export class DatabaseRelationsDriver {
|
|
|
592
655
|
}
|
|
593
656
|
return { distinct, index, facts };
|
|
594
657
|
}
|
|
595
|
-
async listObjects(subject, relation, objectType, partition
|
|
658
|
+
async listObjects(subject, relation, objectType, partition) {
|
|
596
659
|
assertRelationDeclared(this.#config, { type: objectType }, relation);
|
|
597
660
|
assertPartition(partition);
|
|
598
661
|
this.#assertId('el tipo de objeto', objectType, RELATION_ID_MAX);
|
|
662
|
+
// 7.4: sin página (un quinto argumento ≠ undefined ⇒ 500 con cero sentencias), después de F-05 y la partición.
|
|
663
|
+
assertNoListPage('listObjects', arguments[4], 'database');
|
|
599
664
|
const partitionKey = this.#keyOf(partition);
|
|
600
665
|
const s = this.#subjectColumns(subject, partitionKey);
|
|
601
666
|
// Lote D · D-15: `listObjects` RESTA (la misma regla de corte que `check`, objeto a objeto).
|
|
@@ -630,14 +695,15 @@ export class DatabaseRelationsDriver {
|
|
|
630
695
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
631
696
|
return { objects };
|
|
632
697
|
}
|
|
633
|
-
async listSubjects(relation, object, partition
|
|
698
|
+
async listSubjects(relation, object, partition) {
|
|
634
699
|
assertRelationDeclared(this.#config, object, relation);
|
|
635
700
|
assertPartition(partition);
|
|
636
701
|
this.#assertObject(object);
|
|
637
|
-
|
|
702
|
+
assertNoListPage('listSubjects', arguments[3], 'database');
|
|
703
|
+
return this.#directSubjects('listSubjects', relation, object, partition);
|
|
638
704
|
}
|
|
639
705
|
/** Los sujetos DIRECTOS vigentes de UNA relación almacenada (`listSubjects`; `listDenies` con `denied_<rel>`). */
|
|
640
|
-
async #directSubjects(operation, relation, object, partition
|
|
706
|
+
async #directSubjects(operation, relation, object, partition) {
|
|
641
707
|
const partitionKey = this.#keyOf(partition);
|
|
642
708
|
// DIRECTOS del relation EXACTO (invariante 7): ni transitivo (eso es
|
|
643
709
|
// `membersOf`) ni derivado por includes. `usersetSubjects: true` ⇒ los
|
|
@@ -657,11 +723,12 @@ export class DatabaseRelationsDriver {
|
|
|
657
723
|
* `listSubjects` sobre la relación almacenada `denied_<rel>`; no resta ni deriva—. Sobre CUALQUIER tipo declarado
|
|
658
724
|
* (D8/R2-4 b: con el opt-in retirado, los denies vivos se siguen pudiendo listar y borrar).
|
|
659
725
|
*/
|
|
660
|
-
async listDenies(relation, object, partition
|
|
726
|
+
async listDenies(relation, object, partition) {
|
|
661
727
|
assertRelationDeclared(this.#config, object, relation);
|
|
662
728
|
assertPartition(partition);
|
|
663
729
|
this.#assertObject(object);
|
|
664
|
-
|
|
730
|
+
assertNoListPage('listDenies', arguments[3], 'database');
|
|
731
|
+
return this.#directSubjects('listDenies', deniedRelationOf(relation), object, partition);
|
|
665
732
|
}
|
|
666
733
|
/**
|
|
667
734
|
* **`denyPhase` — ¿admite este driver escrituras de deny en el tipo?** (lote D · D2, R2-5): la fase del opt-in EN
|
|
@@ -679,12 +746,13 @@ export class DatabaseRelationsDriver {
|
|
|
679
746
|
* directos): en un grupo con `member(g1#member, g2)`, `membersOf(g2)` trae a
|
|
680
747
|
* los holders de g1 y `listSubjects(member, g2)` solo el userset `g1#member`.
|
|
681
748
|
*/
|
|
682
|
-
async membersOf(object, relation, partition
|
|
749
|
+
async membersOf(object, relation, partition) {
|
|
683
750
|
// Cierre-2 · 🟡 3: F-05 (par, ESTRICTA) también en `membersOf`, la octava
|
|
684
751
|
// operación del puerto; antes solo `#assertObject` (gramática).
|
|
685
752
|
assertRelationDeclared(this.#config, object, relation);
|
|
686
753
|
assertPartition(partition);
|
|
687
754
|
this.#assertObject(object);
|
|
755
|
+
assertNoListPage('membersOf', arguments[3], 'database');
|
|
688
756
|
const partitionKey = this.#keyOf(partition);
|
|
689
757
|
const relations = this.#expandDown(object.type, relation);
|
|
690
758
|
const meta = this.#dialectMeta();
|
|
@@ -692,6 +760,11 @@ export class DatabaseRelationsDriver {
|
|
|
692
760
|
const relPlaceholders = relations.map(() => '?').join(', ');
|
|
693
761
|
const at = this.#expiry.bind(this.#now());
|
|
694
762
|
const active = this.#activeSql(q, 'r');
|
|
763
|
+
// Rotación 2 · cierre 3b (L12, H1 del auditor): la PUERTA «¿hay algún `denied_*` vigente en el objeto?» va en la
|
|
764
|
+
// MISMA sentencia que los candidatos (una rama más del `UNION`, `foto_kind = 'd'`). Antes era una segunda sentencia y
|
|
765
|
+
// un offboarding suelto (unrelate y luego removeDeny) que caía entre las dos devolvía los candidatos de la primera
|
|
766
|
+
// SIN juzgarlos: listaba a un sujeto que no era miembro en NINGÚN estado real.
|
|
767
|
+
const deniedRelations = deniableAbove(this.#config, object.type, relation).map(deniedRelationOf);
|
|
695
768
|
// `grp`: los grupos alcanzables por usersets desde (object, RS) —directos y
|
|
696
769
|
// anidados—. Ancla de UN solo SELECT (MySQL no mezcla UNION ALL con la
|
|
697
770
|
// recursión): los usersets directos del objeto para cualquier rel de RS.
|
|
@@ -709,16 +782,22 @@ export class DatabaseRelationsDriver {
|
|
|
709
782
|
`AND r.${q('relation')} = '${GROUP_MEMBER}' AND r.${q('subject_type')} = '${GROUP_TYPE}' ` +
|
|
710
783
|
`AND r.${q('subject_relation')} = '${GROUP_MEMBER}' AND ${active} ` +
|
|
711
784
|
`) ` +
|
|
712
|
-
// Holders directos del objeto para RS, UNION holders directos de cualquier grupo de `grp
|
|
713
|
-
|
|
785
|
+
// Holders directos del objeto para RS, UNION holders directos de cualquier grupo de `grp` (`foto_kind = 'm'`),
|
|
786
|
+
// UNION la puerta de denies (`foto_kind = 'd'`, una fila como mucho: el `UNION` funde los `NULL`).
|
|
787
|
+
`SELECT ${meta.hint}'m' AS ${q('foto_kind')}, ${q('subject_type')} AS ${q('subject_type')}, ${q('subject_uuid')} AS ${q('subject_uuid')} ` +
|
|
714
788
|
`FROM ${q(RELATIONS_TABLE)} r ` +
|
|
715
789
|
`WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = ? AND r.${q('object_uuid')} = ? ` +
|
|
716
790
|
`AND r.${q('relation')} IN (${relPlaceholders}) AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active} ` +
|
|
717
791
|
`UNION ` +
|
|
718
|
-
`SELECT r.${q('subject_type')}, r.${q('subject_uuid')} FROM ${q(RELATIONS_TABLE)} r ` +
|
|
792
|
+
`SELECT 'm', r.${q('subject_type')}, r.${q('subject_uuid')} FROM ${q(RELATIONS_TABLE)} r ` +
|
|
719
793
|
`JOIN grp ON r.${q('object_uuid')} = grp.g_uuid ` +
|
|
720
794
|
`WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = '${GROUP_TYPE}' ` +
|
|
721
|
-
`AND r.${q('relation')} = '${GROUP_MEMBER}' AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active}
|
|
795
|
+
`AND r.${q('relation')} = '${GROUP_MEMBER}' AND COALESCE(r.${q('subject_relation')}, '') = '' AND ${active}` +
|
|
796
|
+
(deniedRelations.length
|
|
797
|
+
? ` UNION SELECT 'd', NULL, NULL FROM ${q(RELATIONS_TABLE)} r ` +
|
|
798
|
+
`WHERE r.${q('partition_key')} = ? AND r.${q('object_type')} = ? AND r.${q('object_uuid')} = ? ` +
|
|
799
|
+
`AND r.${q('relation')} IN (${deniedRelations.map(() => '?').join(', ')}) AND ${active}`
|
|
800
|
+
: '');
|
|
722
801
|
const bindings = [
|
|
723
802
|
// grp ancla
|
|
724
803
|
partitionKey, object.type, object.id, ...relations, at,
|
|
@@ -728,22 +807,21 @@ export class DatabaseRelationsDriver {
|
|
|
728
807
|
partitionKey, object.type, object.id, ...relations, at,
|
|
729
808
|
// holders de los grupos
|
|
730
809
|
partitionKey, at,
|
|
810
|
+
// la puerta de denies (misma sentencia)
|
|
811
|
+
...(deniedRelations.length ? [partitionKey, object.type, object.id, ...deniedRelations, at] : []),
|
|
731
812
|
];
|
|
732
813
|
const rows = rowsOf(await this.#raw('membersOf', sql, bindings));
|
|
733
|
-
const candidates = rows
|
|
814
|
+
const candidates = rows
|
|
815
|
+
.filter((row) => String(row.foto_kind) === 'm')
|
|
816
|
+
.map((row) => ({ type: String(row.subject_type), uuid: String(row.subject_uuid) }));
|
|
817
|
+
const denied = rows.some((row) => String(row.foto_kind) === 'd');
|
|
734
818
|
// **Lote D · `membersOf` RESTA** (juez ronda 2: «listObjects y membersOf restan»; D8: siempre, con o sin opt-in).
|
|
735
819
|
// Si el objeto tiene alguna fila `denied_*` vigente, cada candidato se juzga con `check` —el MISMO corte de camino
|
|
736
820
|
// que el resto de lecturas, un `check` por candidato—; sin filas deny, cero consultas de más (la respuesta y el coste
|
|
737
|
-
// de 2.4.0-alpha.10). Coste declarado: O(miembros) consultas cuando el objeto tiene denies.
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
.where('object_type', object.type)
|
|
742
|
-
.where('object_uuid', object.id)
|
|
743
|
-
.whereIn('relation', deniableAbove(this.#config, object.type, relation).map(deniedRelationOf)), this.#now())
|
|
744
|
-
.select('uuid')
|
|
745
|
-
.limit(1));
|
|
746
|
-
if (denied.length === 0)
|
|
821
|
+
// de 2.4.0-alpha.10). Coste declarado: O(miembros) consultas cuando el objeto tiene denies. La puerta sale de la
|
|
822
|
+
// MISMA sentencia que los candidatos (L12): sin deny en esa foto, todos los candidatos son miembros en ese estado.
|
|
823
|
+
// Con deny, cada candidato se juzga con su `check` (una foto por candidato, como cada posición de `authorizeMany`).
|
|
824
|
+
if (!denied)
|
|
747
825
|
return { subjects: candidates };
|
|
748
826
|
// **Cierre 1 de D1 · 🟡 4 del auditor**: con deny, N `check` en serie sin cota ni deadline total era una
|
|
749
827
|
// amplificación barata para quien puede escribir un deny (disponibilidad, no concesión). Cota DECLARADA
|
|
@@ -844,9 +922,12 @@ export class DatabaseRelationsDriver {
|
|
|
844
922
|
*/
|
|
845
923
|
async enumerateRelations(partition, page) {
|
|
846
924
|
assertPartition(partition);
|
|
925
|
+
// 7.4 (injerto 4, H2/H3): limit entero ≥ 1 y after = un uuid canónico (el cursor que ESTE driver emite) o 422,
|
|
926
|
+
// con cero sentencias; un limit por encima del techo se recorta en #enumerateRows.
|
|
927
|
+
const checked = assertEnumeratePage('enumerateRelations', page, 'uuid', 'database');
|
|
847
928
|
const wanted = new Map();
|
|
848
929
|
wanted.set(partitionKey(partition), partition);
|
|
849
|
-
return this.#enumerateRows('enumerateRelations', wanted,
|
|
930
|
+
return this.#enumerateRows('enumerateRelations', wanted, checked);
|
|
850
931
|
}
|
|
851
932
|
/**
|
|
852
933
|
* **`enumerateRelationsMany` — K particiones en UNA consulta** (2.4.0-alpha.8
|
|
@@ -875,7 +956,8 @@ export class DatabaseRelationsDriver {
|
|
|
875
956
|
if (!wanted.has(key))
|
|
876
957
|
wanted.set(key, partition);
|
|
877
958
|
}
|
|
878
|
-
|
|
959
|
+
const checked = assertEnumeratePage('enumerateRelationsMany', page, 'uuid', 'database');
|
|
960
|
+
return this.#enumerateRows('enumerateRelationsMany', wanted, checked);
|
|
879
961
|
}
|
|
880
962
|
/**
|
|
881
963
|
* La consulta de `enumerateRelations`/`enumerateRelationsMany`: los hechos
|
|
@@ -887,7 +969,8 @@ export class DatabaseRelationsDriver {
|
|
|
887
969
|
async #enumerateRows(operation, wanted, page) {
|
|
888
970
|
const partitionKeys = [...wanted.keys()];
|
|
889
971
|
const declaredTypes = [GROUP_TYPE, ...this.#config.objectTypes.map((t) => t.type)];
|
|
890
|
-
|
|
972
|
+
// La COPIA validada en la entrada (assertEnumeratePage), no la página del llamante: aquí solo el default y el RECORTE al techo, declarado en «Limits».
|
|
973
|
+
const limit = enumerateLimit(page);
|
|
891
974
|
const after = page?.after;
|
|
892
975
|
const rows = await this.#sql(operation, () => {
|
|
893
976
|
let q = this.#connection()
|
|
@@ -959,4 +1042,3 @@ function rowsOf(result) {
|
|
|
959
1042
|
}
|
|
960
1043
|
return result?.rows ?? [];
|
|
961
1044
|
}
|
|
962
|
-
//# sourceMappingURL=database_relations_driver.js.map
|
|
@@ -142,8 +142,12 @@ export declare function isDuplicateWrite(error: unknown): boolean;
|
|
|
142
142
|
export interface OpenFgaDriverOptions {
|
|
143
143
|
apiUrl: string;
|
|
144
144
|
storeId: string;
|
|
145
|
-
/**
|
|
146
|
-
|
|
145
|
+
/**
|
|
146
|
+
* **Pin del model — OBLIGATORIO** (lote «antes de RC» · L11, decisión del dueño 2026-10-17 (2)): el driver comprueba
|
|
147
|
+
* UNA vez por proceso que ese modelo es el que genera su config (la puerta L11) y el servidor evalúa con ÉL. Sin él,
|
|
148
|
+
* 500 `E_AUTHZ_CONFIG` al construir con la receta (`openfga:provision` imprime `OPENFGA_MODEL_ID`).
|
|
149
|
+
*/
|
|
150
|
+
modelId: string;
|
|
147
151
|
/** Jerarquía del consumidor (ver ScopeChainResolver). */
|
|
148
152
|
resolveChain?: ScopeChainResolver;
|
|
149
153
|
/**
|
|
@@ -225,6 +229,14 @@ export interface OpenFgaDriverOptions {
|
|
|
225
229
|
* valor «truthy» no es una aceptación.
|
|
226
230
|
*/
|
|
227
231
|
acceptScopeDriftRisk?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* **`openfga` es EXPERIMENTAL en 2.5.x** (estrategia 2.5.0, firma 4 del dueño 2026-10-21): no apto para producción,
|
|
234
|
+
* con límites declarados en README «Limits» que se cierran en 2.6. Construirlo exige este LITERAL, versionado; sin él
|
|
235
|
+
* (o con `true`, `'acknowledged'`, otra versión…) el constructor lanza 500 `E_AUTHZ_CONFIG` como primera línea, sin
|
|
236
|
+
* crear el cliente. Opcional en el TIPO —como `acceptScopeDriftRisk`— para que un config que solo REGISTRA la factory
|
|
237
|
+
* `openfga` (el stub publicado) siga compilando; obligatorio al construir.
|
|
238
|
+
*/
|
|
239
|
+
experimental?: 'openfga@2.5';
|
|
228
240
|
}
|
|
229
241
|
/**
|
|
230
242
|
* **El gate de construcción de `facts`** (3b-2d, panel 2 cruce 4 · S5).
|
|
@@ -247,6 +259,13 @@ export declare function assertScopeDriftGuarded(options: {
|
|
|
247
259
|
acceptScopeDriftRisk?: boolean;
|
|
248
260
|
}): void;
|
|
249
261
|
export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
262
|
+
/**
|
|
263
|
+
* **Vueltas del settle** (lote C · carreras, resolución 7 del juez; linaje L13): aplicaciones que
|
|
264
|
+
* `settleRoleVisibility` intenta antes de rendirse. Al agotarlas BORRA las aristas `scope#binding` de los pares (fail-CLOSED,
|
|
265
|
+
* intra-tenant: el churn exige `within` en los dos padres) y lanza 409 `E_AUTHZ_WRITE_CONFLICT` marcado PARCIAL
|
|
266
|
+
* (invariante 13: `indeterminate: true` en `onWrite`).
|
|
267
|
+
*/
|
|
268
|
+
export declare const SETTLE_MAX_ROUNDS = 5;
|
|
250
269
|
/**
|
|
251
270
|
* Cota de páginas de una enumeración (1.000.000 de tuplas a 100 por página).
|
|
252
271
|
* Un `continuation_token` que no avanza —un servidor roto, un proxy o una
|
|
@@ -284,6 +303,7 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
284
303
|
canonicalScopeReads: false;
|
|
285
304
|
enumerateFacts: true;
|
|
286
305
|
transactionalWrites: false;
|
|
306
|
+
experimental: true;
|
|
287
307
|
}>;
|
|
288
308
|
private client;
|
|
289
309
|
private chainResolver;
|
|
@@ -306,6 +326,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
306
326
|
* con el que este driver escribe y responde, no el último del store.
|
|
307
327
|
*/
|
|
308
328
|
private readonly modelId;
|
|
329
|
+
/**
|
|
330
|
+
* **La comparación modelo ↔ config de L11, UNA por proceso** (`modelGate`). Es un OBJETO que se MUTA y nunca se
|
|
331
|
+
* reasigna (H11 b): las vistas `withChainResolver`/`withClock` son `Object.create(this)` y lo heredan por prototipo,
|
|
332
|
+
* así que `forRequest()` no relee el modelo por petición.
|
|
333
|
+
*/
|
|
334
|
+
private readonly modelMemo;
|
|
309
335
|
private consistency;
|
|
310
336
|
/** Reloj de pared del driver (J1): el ÚNICO `now` de checks, filtros y re-grant. */
|
|
311
337
|
private now;
|
|
@@ -355,6 +381,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
355
381
|
* `authorize` evaluaba el deny en un instante y el rol en otro.
|
|
356
382
|
*/
|
|
357
383
|
private checkContext;
|
|
384
|
+
/**
|
|
385
|
+
* **La lectura del modelo, UNA por proceso** (L11): `readAuthorizationModel` del `modelId` FIJADO y
|
|
386
|
+
* `factsModelDifferences` con `slice: 'roles'` (los holders, `role`, `role_binding`, `scope` y las condiciones; la
|
|
387
|
+
* igualdad es «módulo el catálogo»: `P_M` sale del propio modelo). Se memoriza el ÉXITO —también con diferencias— y un
|
|
388
|
+
* generador que no puede generar (500); **un fallo al LEER el modelo NO se memoriza, sea 503 o 500** (cierre de L11).
|
|
389
|
+
*/
|
|
390
|
+
private modelGate;
|
|
391
|
+
/**
|
|
392
|
+
* **La puerta L11 del driver de ROLES** (lote «antes de RC», decisión del dueño 2026-10-17 (1); hasta L11 este driver
|
|
393
|
+
* no comparaba NINGUNA regla —P7: solo el conjunto de `permits_*`, «no es una barrera de seguridad»—: un `can_<P>` sin
|
|
394
|
+
* `but not denied_<P>`, un `rooted` cambiado o un `assignee` sin `not_expired` concedían o negaban sin error). Toda
|
|
395
|
+
* operación que llega al store la pasa ENTERA, justo después de validar su entrada y antes de la primera llamada
|
|
396
|
+
* (catálogo, resolutor y cliente incluidos); `enumerateFacts` —el ORIGEN de una migración— solo la parte
|
|
397
|
+
* `conditions`. Distinta ⇒ 500 `E_AUTHZ_CONFIG` con las rutas y CERO llamadas que evalúen o escriban.
|
|
398
|
+
*/
|
|
399
|
+
private assertModel;
|
|
358
400
|
/**
|
|
359
401
|
* Consulta al catálogo local clasificando su fallo. Con este driver el
|
|
360
402
|
* catálogo SQL sigue siendo una dependencia dura de cada pregunta: su caída
|
|
@@ -368,6 +410,58 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
368
410
|
* alineada por ese id: un resultado por check, ni uno más ni uno menos.
|
|
369
411
|
*/
|
|
370
412
|
private batchCheckAll;
|
|
413
|
+
/**
|
|
414
|
+
* **El catálogo de una ESCRITURA** (K4, resolución 2 del juez): con la versión contrastada en fresco SIEMPRE
|
|
415
|
+
* (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
|
|
416
|
+
* `view()` (el contrato del memo es suyo).
|
|
417
|
+
*/
|
|
418
|
+
private writeCatalog;
|
|
419
|
+
/**
|
|
420
|
+
* K4 (segunda mitad): un uuid de rol que el STORE tiene y el catálogo de la escritura no conoce fuerza UNA recarga del
|
|
421
|
+
* memo (`invalidate` + foto en fresco). Si tras ella sigue ausente, el rol se borró: quien llama BORRA su arista.
|
|
422
|
+
*/
|
|
423
|
+
private writeCatalogKnowing;
|
|
424
|
+
/**
|
|
425
|
+
* Un `batchCheck` con `HIGHER_CONSISTENCY` para el settle, SIN colapsar los errores por ítem: el llamante decide qué
|
|
426
|
+
* hacer con un `E_AUTHZ_RESOLUTION_LIMIT` de `ancestor` (caer a `storeChain`, condición 2 de la resolución 9) y propaga
|
|
427
|
+
* el resto (clasificados por el MISMO `fgaFailureError` que `alignBatchResults`).
|
|
428
|
+
*/
|
|
429
|
+
private checkForWrite;
|
|
430
|
+
/**
|
|
431
|
+
* **La decisión del settle**, con el STORE (todas las lecturas con HIGHER, en UN `batchCheck`): para cada par, si la
|
|
432
|
+
* arista TIENE que estar (`want`) y si ESTÁ (`has`). `want` exige, por este orden:
|
|
433
|
+
* - el rol en el catálogo de la escritura (K4: en fresco y, si falta, tras una recarga; si sigue faltando, `known: false`
|
|
434
|
+
* y la arista se borra — «rol ausente ⇒ borra»);
|
|
435
|
+
* - declarado para el NIVEL del scope (3b-2g · R1);
|
|
436
|
+
* - `role_binding#role` VIVO (auditor ⚪ 8: tras un `revoke`/`purgeScope`/`purgeRole` concurrente el settle no resucita
|
|
437
|
+
* la arista; `liveness: false` solo en la decisión PREVIA del `grant`, que escribe esa arista en el mismo `Write`);
|
|
438
|
+
* - y, si el rol es LOCAL, su owner ancestro-o-igual del scope EN EL STORE: atajo `scope == owner` (`ancestor` no es
|
|
439
|
+
* reflexivo, K6), `Check(scope:<owner> ancestor scope:<S>)` y, si ese `Check` excede la cota del servidor (medido en M2:
|
|
440
|
+
* solo cuando el owner SÍ es ancestro a > 26 saltos), la cadena del store salto a salto (`storeChain`) — nunca «borrar
|
|
441
|
+
* por si acaso» (fail-closed aleatorio cerca de la cota). La laxitud de `ancestor` ante DOS padres es deriva que ya
|
|
442
|
+
* denuncian `moved` y reconcile (riesgo declarado del juez).
|
|
443
|
+
*/
|
|
444
|
+
private decideRoleVisibility;
|
|
445
|
+
/**
|
|
446
|
+
* **El ÚNICO sitio que da de ALTA una arista `scope#binding` de rol local** (y borra las que se le pidan), en lotes ≤ 100
|
|
447
|
+
* (`Write` con `Ignore` en las dos direcciones: dice el estado que DEBE quedar, no el delta — invariante 6).
|
|
448
|
+
*/
|
|
449
|
+
private writeBindingEdges;
|
|
450
|
+
/**
|
|
451
|
+
* **`settleRoleVisibility`** — el punto de paso de L13. Decide con el store (`decideRoleVisibility`), y si alguna
|
|
452
|
+
* arista no está como tiene que estar, aplica la diferencia y VUELVE A LEER; termina cuando una lectura POSTERIOR a su
|
|
453
|
+
* última escritura ve el store coherente (punto fijo: «borro una vez» deja CLOSED persistente al entrar, R1-entrar).
|
|
454
|
+
*
|
|
455
|
+
* **Cota** (resolución 7): tras `SETTLE_MAX_ROUNDS` aplicaciones sin converger (un churn que mueve el árbol en cada
|
|
456
|
+
* relectura) se RINDE: borra las aristas de los pares que NO convergieron —y, en el mismo `Write`, `giveUpDeletes` (el `assignee` de un
|
|
457
|
+
* `grant` que CREÓ la asignación: vuelta a «no ocurrió»)— y lanza 409 `E_AUTHZ_WRITE_CONFLICT` marcado PARCIAL
|
|
458
|
+
* (`indeterminate: true`). Fail-CLOSED declarado e intra-tenant; en un re-grant el 409 NO significa «no asignado».
|
|
459
|
+
*
|
|
460
|
+
* Devuelve los pares cuyo rol el catálogo ya no conoce (`known: false`): el `grant` que los CREÓ deshace su asignación.
|
|
461
|
+
*/
|
|
462
|
+
private settleRoleVisibility;
|
|
463
|
+
/** Las vueltas del settle (lo que hasta el cierre 1 era el cuerpo de `settleRoleVisibility`). */
|
|
464
|
+
private settleRounds;
|
|
371
465
|
private findPermission;
|
|
372
466
|
/**
|
|
373
467
|
* Filtro por catálogo de las lecturas de membresía (`listRoles`,
|
|
@@ -438,6 +532,13 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
438
532
|
*/
|
|
439
533
|
private rejectTransaction;
|
|
440
534
|
grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
|
|
535
|
+
/**
|
|
536
|
+
* El cuerpo de ESCRITURA del `grant` (sin la decisión de visibilidad): lee la asignación, decide entre no-op, write o
|
|
537
|
+
* delete+write, y re-aplica ante una carrera. `structure` son las aristas que van con el `assignee` en el MISMO `Write`
|
|
538
|
+
* (las tres para un rol global; sin la `scope#binding` si el store dice que el rol local no es visible ahí). `progress`
|
|
539
|
+
* cuenta los `Write` INTENTADOS (el settle del `grant` local corre si hubo alguno, también si falló: invariante 13).
|
|
540
|
+
*/
|
|
541
|
+
private writeGrant;
|
|
441
542
|
/**
|
|
442
543
|
* Write directo de la asignación CON su estructura (3b-2f · R3); si algo ya
|
|
443
544
|
* estaba, camino largo. Devuelve si existía la ASIGNACIÓN —lo dice la
|
|
@@ -550,8 +651,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
550
651
|
* borrar una tupla inexistente—, la cadena del padre NUEVO en el store
|
|
551
652
|
* (3b-8 · A5: el anti-ciclos del consumidor no ve un store
|
|
552
653
|
* desincronizado) y **UN solo `Write`** con el delete del padre viejo y el
|
|
553
|
-
* write del nuevo, que es atómico
|
|
554
|
-
*
|
|
654
|
+
* write del nuevo, que es atómico PARA EL ESCRITOR (el store nunca guarda
|
|
655
|
+
* los dos padres ni ninguno) pero NO para un lector concurrente: un `Check`
|
|
656
|
+
* que dura lo que el `Write` puede leer la arista vieja por una rama del
|
|
657
|
+
* modelo y la nueva por otra (lector rasgado, fila «torn read» de Limits del
|
|
658
|
+
* README; medido 1,2–7,1 %, decisión del dueño 6b). O(profundidad) requests;
|
|
659
|
+
* con roles locales, además el barrido F4 (ver `reparent`).
|
|
555
660
|
*/
|
|
556
661
|
onScopeMoved(child: ScopeRef, newParent: ScopeRef): Promise<void>;
|
|
557
662
|
/**
|
|
@@ -623,8 +728,8 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
623
728
|
*/
|
|
624
729
|
private reparent;
|
|
625
730
|
/**
|
|
626
|
-
* **El barrido del rol local** (3b-2e · E1; decisión del dueño del
|
|
627
|
-
* 2026-08-30, opción 1).
|
|
731
|
+
* **El barrido PREVIO del rol local** (3b-2e · E1; decisión del dueño del
|
|
732
|
+
* 2026-08-30, opción 1; F4: solo borra).
|
|
628
733
|
*
|
|
629
734
|
* En (c2) el modelo no tiene `owner`, así que `authorize` NO vuelve a
|
|
630
735
|
* decidir con el árbol de hoy si un rol LOCAL sigue siendo visible: un
|
|
@@ -634,11 +739,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
634
739
|
* un **fail-open** — y encima uno que solo se ve comparando drivers.
|
|
635
740
|
*
|
|
636
741
|
* Lo que se toca es la arista `scope#binding`, que es lo que hace
|
|
637
|
-
* ALCANZABLE la asignación:
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
* ni nada de un rol GLOBAL, cuya
|
|
742
|
+
* ALCANZABLE la asignación: el barrido previo la BORRA donde el owner del
|
|
743
|
+
* rol no estará en la cadena NUEVA; las ALTAS (volver a su sitio restaura)
|
|
744
|
+
* son del settle de después (`settleSubtree`), el punto de paso único de
|
|
745
|
+
* L13. No se toca el `assignee` —el hecho de la asignación no cambia porque
|
|
746
|
+
* el árbol se mueva, igual que en `database`— ni nada de un rol GLOBAL, cuya
|
|
747
|
+
* visibilidad no depende del árbol.
|
|
642
748
|
*
|
|
643
749
|
* **Por subárbol, no por nodo** (consecuencia 2): los descendientes del
|
|
644
750
|
* nodo movido también cambian de cadena.
|
|
@@ -647,9 +753,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
647
753
|
* que no usa delegación— son **cero** requests y `moved` sigue siendo el
|
|
648
754
|
* `Read` + `Write` del cruce 8. Con roles locales: una lectura por rol local
|
|
649
755
|
* (sus bindings, por `role_binding#role`), la bajada del subárbol y un
|
|
650
|
-
* `Write` por lote.
|
|
756
|
+
* `Write` por lote (el coste O(roles locales) por `moved` es el 🟡 6, declarado hasta 2.6).
|
|
651
757
|
*/
|
|
652
758
|
private sweepLocalRoleBindings;
|
|
759
|
+
/**
|
|
760
|
+
* Los bindings de roles LOCALES que cuelgan del subárbol de `movedKey` en el STORE (HIGHER), con la cadena de cada
|
|
761
|
+
* scope del subárbol. El catálogo es el de una ESCRITURA (K4: en fresco), y un uuid que el store tiene y el catálogo no
|
|
762
|
+
* conoce fuerza una recarga (`writeCatalogKnowing`); los roles se enumeran por el catálogo (el amarillo 6 —enumerar
|
|
763
|
+
* desde el store— es de 2.6). `chain` es la cadena que el nodo TENDRÁ (barrido previo) o, sin ella, la del store.
|
|
764
|
+
*/
|
|
765
|
+
private localBindingsBelow;
|
|
766
|
+
/**
|
|
767
|
+
* **El settle del subárbol de un cambio de árbol** (lote C · L13): los bindings de roles locales del subárbol de
|
|
768
|
+
* `movedKey` (en el store, HIGHER, catálogo en fresco) pasan por `settleRoleVisibility`. Lo llaman `reparent` tras la
|
|
769
|
+
* arista, su atajo idempotente y su `finally`.
|
|
770
|
+
*/
|
|
771
|
+
private settleSubtree;
|
|
653
772
|
/**
|
|
654
773
|
* **El barrido por NIVEL** (3b-2g · R1; decisión del dueño del 2026-08-30
|
|
655
774
|
* (2), mismo mecanismo que E1).
|
|
@@ -660,18 +779,22 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
660
779
|
* —donde `declaredRoleAt` se evalúa en cada pregunta— y **sigue
|
|
661
780
|
* concediendo** en `facts`, que es la divergencia R1 del lote 2e.
|
|
662
781
|
*
|
|
663
|
-
* Se cierra igual que el owner:
|
|
664
|
-
* bindings de ESE rol
|
|
782
|
+
* Se cierra igual que el owner: dejando la arista `scope#binding` de los
|
|
783
|
+
* bindings de ESE rol como dice la regla única de visibilidad. Lo llama
|
|
665
784
|
* `projectCatalogRole`, que es el hook de «una escritura de catálogo cambió
|
|
666
785
|
* este rol»: el manager lo dispara tras `defineScopedRole`/`updateScopedRole`
|
|
667
786
|
* y un escritor «a mano» de `authz_*` tiene el mismo deber que ya tenía con
|
|
668
787
|
* el espejo de permisos (sin él, en `facts` un rol recién definido no
|
|
669
788
|
* concedería nada y quitarle un permiso seguiría concediéndolo).
|
|
670
789
|
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
790
|
+
* **Desde el lote C pasa ENTERO por el settle** (L13; 🟠 1 de F4-carreras: este barrido leía `storeChain` y escribía
|
|
791
|
+
* DESPUÉS, y un `moved` que barriera entre medias quedaba pisado — fail-OPEN persistente entre tenants). El catálogo es
|
|
792
|
+
* el de una escritura (K4, hallazgo 4 del panel TREE): con un memo rancio un rol recién definido en otro proceso se
|
|
793
|
+
* ignoraba. Un rol que el catálogo ya no declara —tras la recarga— no tiene visibilidad que decidir: sus aristas se BORRAN
|
|
794
|
+
* («rol ausente ⇒ borra», K4); el resto de sus hechos son de `purgeRole`/`authz:reconcile`.
|
|
795
|
+
*
|
|
796
|
+
* Coste: una lectura (los bindings del rol) y un `batchCheck` (HIGHER) por vuelta del settle; un `Write` por lote solo
|
|
797
|
+
* si hay algo que corregir. Un rol sin bindings —todo `defineScopedRole`— son 0 escrituras.
|
|
675
798
|
*/
|
|
676
799
|
private sweepRoleVisibility;
|
|
677
800
|
/**
|
|
@@ -681,15 +804,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
681
804
|
* NIVEL de ese scope (3b-2g · R1) y ser global o tener a su owner en la
|
|
682
805
|
* cadena (3b-2e · E1). Visible ⇒ la arista se (re)escribe; no visible ⇒ se
|
|
683
806
|
* borra. El `assignee` no se toca: la asignación existe igual, lo que
|
|
684
|
-
* cambia es dónde se la ve.
|
|
807
|
+
* cambia es dónde se la ve. (Desde el lote C solo lo usa el barrido PREVIO, que solo borra.)
|
|
685
808
|
*/
|
|
686
809
|
private classifyBindingEdge;
|
|
687
810
|
/**
|
|
688
|
-
* Aplica
|
|
689
|
-
*
|
|
690
|
-
* `Ignore` en las dos direcciones: el barrido dice el estado que DEBE
|
|
691
|
-
* quedar, no el delta — borrar lo que ya no está y reescribir lo que ya
|
|
692
|
-
* estaba son no-ops, no errores (invariante 6).
|
|
811
|
+
* Aplica los BORRADOS de un barrido (lote C · L13: `applyBindingSweep` ya no da ALTAS; las altas de `scope#binding` de
|
|
812
|
+
* rol local solo salen del settle). Borrar nunca concede, así que va directo, en lotes ≤ 100 con `Ignore`.
|
|
693
813
|
*/
|
|
694
814
|
private applyBindingSweep;
|
|
695
815
|
/**
|
|
@@ -698,7 +818,8 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
698
818
|
* tiene que gobernar el barrido; el resolutor del consumidor no participa.
|
|
699
819
|
* Un nodo con más de un padre es deriva y se dice (`assertOneParent`), y el
|
|
700
820
|
* recorrido está acotado por el mismo tope de páginas que las
|
|
701
|
-
* enumeraciones: un ciclo escrito a mano no cuelga el proceso.
|
|
821
|
+
* enumeraciones: un ciclo escrito a mano no cuelga el proceso. Alimenta
|
|
822
|
+
* escrituras (anti-ciclos de `reparent`, barrido, caída del settle) ⇒ HIGHER (hallazgo 3 TREE).
|
|
702
823
|
*/
|
|
703
824
|
private storeChain;
|
|
704
825
|
/**
|
|
@@ -826,6 +947,13 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
826
947
|
* divergencia R1 del lote 2e.
|
|
827
948
|
*/
|
|
828
949
|
projectCatalogRole(roleUuid: string): Promise<void>;
|
|
950
|
+
/**
|
|
951
|
+
* La proyección `permits_<P>` de UN rol (la otra mitad de `projectCatalogRole`). **Hallazgo 4 del panel TREE (K4)**: el
|
|
952
|
+
* catálogo es el de una ESCRITURA (`writeCatalog`, versión en fresco) — con un memo `{ everyMs }` que todavía no conoce
|
|
953
|
+
* un rol recién definido en otro proceso, `rolePermissionsOf` devolvía `[]` y esto BORRABA sus `permits_*` (fail-closed
|
|
954
|
+
* medido: `dave@C`). Mutante `project.memo-catalog`.
|
|
955
|
+
*/
|
|
956
|
+
private projectRolePermits;
|
|
829
957
|
/**
|
|
830
958
|
* **Reconstruye el store desde `authz_*` + el árbol del consumidor.**
|
|
831
959
|
*
|
|
@@ -1085,6 +1213,12 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
1085
1213
|
* default `?? []` sobre `tuples` vuelva a este fichero.
|
|
1086
1214
|
*/
|
|
1087
1215
|
private tuplesOrThrow;
|
|
1216
|
+
/**
|
|
1217
|
+
* **`readAllTuples` para una lectura que ALIMENTA UNA ESCRITURA** (lote C · carreras, resolución 8; hallazgo 3 TREE):
|
|
1218
|
+
* con `HIGHER_CONSISTENCY` siempre (`WRITE_CONSISTENCY`). Es el helper ÚNICO: la guardia de fuente de
|
|
1219
|
+
* `tests/visibility_settle.spec.ts` exige que los métodos que escriben lean por aquí (o por `checkForWrite`).
|
|
1220
|
+
*/
|
|
1221
|
+
private readForWrite;
|
|
1088
1222
|
private readAllTuples;
|
|
1089
1223
|
}
|
|
1090
1224
|
/**
|
|
@@ -1120,4 +1254,3 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
1120
1254
|
* en silencio los datos de quien ya usaba ese nombre).
|
|
1121
1255
|
*/
|
|
1122
1256
|
export declare const OWN_FACTS_OBJECT_TYPES: ReadonlySet<string>;
|
|
1123
|
-
//# sourceMappingURL=openfga_driver.d.ts.map
|