@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
|
@@ -28,6 +28,7 @@ import { assertNotFrozenRow } from '../freeze.js';
|
|
|
28
28
|
import { isRelUserset, normalizeRelationsCapabilities } from '../types.js';
|
|
29
29
|
import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject, assertRelationDenyable, assertDenySubject, assertDenyNoExpiry, } from './define_relations_config.js';
|
|
30
30
|
import { assertCheckManyObjects } from './check_many.js';
|
|
31
|
+
import { assertEnumeratePage, assertNoListPage } from './page.js';
|
|
31
32
|
/**
|
|
32
33
|
* ¿Cada POSICIÓN del array es un boolean? Por índice, nunca con `some`/`every`
|
|
33
34
|
* (que saltan los huecos de un array disperso: 🟡 A3 del auditor del lote A).
|
|
@@ -327,7 +328,8 @@ export class RelationsManager {
|
|
|
327
328
|
timeoutMs: this.#options.freezeTimeoutMs,
|
|
328
329
|
});
|
|
329
330
|
}
|
|
330
|
-
async relate(subject, relation, object, partition,
|
|
331
|
+
async relate(subject, relation, object, partition, rawOptions) {
|
|
332
|
+
const options = knownWriteOptions(rawOptions);
|
|
331
333
|
this.#assertTransactional(options, 'relate');
|
|
332
334
|
await this.#assertNotFrozen('relate');
|
|
333
335
|
this.#assertDeclared(object, relation);
|
|
@@ -346,7 +348,8 @@ export class RelationsManager {
|
|
|
346
348
|
await this.#write(event, () => this.#driver.relate(subject, relation, object, partition, options));
|
|
347
349
|
await this.#notify(event);
|
|
348
350
|
}
|
|
349
|
-
async unrelate(subject, relation, object, partition,
|
|
351
|
+
async unrelate(subject, relation, object, partition, rawOptions) {
|
|
352
|
+
const options = knownWriteOptions(rawOptions);
|
|
350
353
|
this.#assertTransactional(options, 'unrelate');
|
|
351
354
|
await this.#assertNotFrozen('unrelate');
|
|
352
355
|
this.#assertDeclared(object, relation);
|
|
@@ -474,12 +477,13 @@ export class RelationsManager {
|
|
|
474
477
|
await this.#notify(event);
|
|
475
478
|
}
|
|
476
479
|
/** Los sujetos con deny DIRECTO de `relation` sobre `object` (hechos: no resta ni deriva). Cualquier tipo declarado (D8). */
|
|
477
|
-
async listDenies(relation, object, partition
|
|
480
|
+
async listDenies(relation, object, partition) {
|
|
478
481
|
this.#assertDeclared(object, relation);
|
|
479
482
|
this.#assertDenies('listDenies');
|
|
480
483
|
assertPartition(partition);
|
|
481
484
|
this.#assertObjectId(object);
|
|
482
|
-
|
|
485
|
+
assertNoListPage('listDenies', arguments[3], this.#driverLabel());
|
|
486
|
+
return this.#driver.listDenies(relation, object, partition);
|
|
483
487
|
}
|
|
484
488
|
/**
|
|
485
489
|
* **La primitiva de CADENA** (lote D · D9, decisión del dueño 2026-10-14): una decisión por objeto, POR POSICIÓN —
|
|
@@ -509,6 +513,10 @@ export class RelationsManager {
|
|
|
509
513
|
}
|
|
510
514
|
return answers;
|
|
511
515
|
}
|
|
516
|
+
/** El nombre del driver en los mensajes (la clave en `relations.drivers`, como en las puertas de `{ transaction }`). */
|
|
517
|
+
#driverLabel() {
|
|
518
|
+
return this.#options.driverName ?? 'relations';
|
|
519
|
+
}
|
|
512
520
|
/**
|
|
513
521
|
* **D-28 · la capacidad `relationDenies`** (regla D15: opcional, normalizada a `false`): sin ella, o sin el método
|
|
514
522
|
* del puerto, las cuatro operaciones del deny son 500 `E_AUTHZ_UNSUPPORTED` nombrándose, con CERO llamadas al
|
|
@@ -520,17 +528,26 @@ export class RelationsManager {
|
|
|
520
528
|
throw new UnsupportedOperationError(operation, 'el deny de relaciones', this.#options.driverName ?? 'relations', `El driver no declara relationDenies: true (o no trae '${operation}'): no sabe escribir ni honrar un deny. ` +
|
|
521
529
|
`Los dos drivers del paquete (database, openfga) lo traen desde 2.5.0-alpha.3.`);
|
|
522
530
|
}
|
|
523
|
-
|
|
531
|
+
/**
|
|
532
|
+
* **Los cuatro listados no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez): `page` salió de la firma
|
|
533
|
+
* de `listObjects`/`listSubjects`/`listDenies`/`membersOf` (nunca hizo nada) y un valor distinto de `undefined` que
|
|
534
|
+
* llegue por JS o `as any` es 500 `E_AUTHZ_UNSUPPORTED` con CERO llamadas al driver (`assertNoListPage`), DESPUÉS de
|
|
535
|
+
* F-05 y de la partición (RC74-5: primero la identidad de la pregunta). Sin página, la lista es completa o lleva
|
|
536
|
+
* `truncated`; nunca un cursor.
|
|
537
|
+
*/
|
|
538
|
+
async listObjects(subject, relation, objectType, partition) {
|
|
524
539
|
this.#assertDeclared({ type: objectType }, relation);
|
|
525
540
|
assertPartition(partition);
|
|
526
541
|
this.#assertSubject(subject, partition);
|
|
527
|
-
|
|
542
|
+
assertNoListPage('listObjects', arguments[4], this.#driverLabel());
|
|
543
|
+
return this.#driver.listObjects(subject, relation, objectType, partition);
|
|
528
544
|
}
|
|
529
|
-
async listSubjects(relation, object, partition
|
|
545
|
+
async listSubjects(relation, object, partition) {
|
|
530
546
|
this.#assertDeclared(object, relation);
|
|
531
547
|
assertPartition(partition);
|
|
532
548
|
this.#assertObjectId(object);
|
|
533
|
-
|
|
549
|
+
assertNoListPage('listSubjects', arguments[3], this.#driverLabel());
|
|
550
|
+
return this.#driver.listSubjects(relation, object, partition);
|
|
534
551
|
}
|
|
535
552
|
/**
|
|
536
553
|
* **Las purgas notifican** (2.4.0-alpha.3 · E, hallazgo #4; D-1/D-3): UN
|
|
@@ -592,7 +609,7 @@ export class RelationsManager {
|
|
|
592
609
|
* `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
|
|
593
610
|
* trunca), jamás un `skip`.
|
|
594
611
|
*/
|
|
595
|
-
async membersOf(object, relation, partition
|
|
612
|
+
async membersOf(object, relation, partition) {
|
|
596
613
|
// Cierre-2 · 🟡 3: `membersOf` toma `(object, relation)` —la forma exacta
|
|
597
614
|
// que F-05 guarda— y era la octava operación fuera de «las siete»; un
|
|
598
615
|
// driver de terceros con `membersOfNative: true` sobre un store compartido
|
|
@@ -605,7 +622,8 @@ export class RelationsManager {
|
|
|
605
622
|
throw new UnsupportedOperationError('membersOf', 'la membresía transitiva de un grupo', 'relations', `Solo el driver 'database' la resuelve (CTE recursiva); en 'openfga' la transitiva sería ` +
|
|
606
623
|
`'ListUsers', que trunca. Usa 'listSubjects' para los hechos directos.`);
|
|
607
624
|
}
|
|
608
|
-
|
|
625
|
+
assertNoListPage('membersOf', arguments[3], this.#driverLabel());
|
|
626
|
+
return this.#driver.membersOf(object, relation, partition);
|
|
609
627
|
}
|
|
610
628
|
/** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
|
|
611
629
|
async enumerateRelations(partition, page) {
|
|
@@ -613,7 +631,10 @@ export class RelationsManager {
|
|
|
613
631
|
if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
|
|
614
632
|
throw new UnsupportedOperationError('enumerateRelations', 'ser origen de authz:reconcile de relaciones', 'relations');
|
|
615
633
|
}
|
|
616
|
-
|
|
634
|
+
// 7.4 (injerto 4): la FORMA de la página (limit entero ≥ 1, after cadena no vacía) ⇒ 422 sin tocar el driver; qué
|
|
635
|
+
// cursor es legítimo lo decide el driver (el manager no sabe qué emite).
|
|
636
|
+
const checked = assertEnumeratePage('enumerateRelations', page, 'opaque', this.#driverLabel());
|
|
637
|
+
return this.#driver.enumerateRelations(partition, checked);
|
|
617
638
|
}
|
|
618
639
|
/**
|
|
619
640
|
* **K particiones en UN barrido** (2.4.0-alpha.6) si el driver trae
|
|
@@ -646,9 +667,10 @@ export class RelationsManager {
|
|
|
646
667
|
if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
|
|
647
668
|
throw new UnsupportedOperationError('enumerateRelationsMany', 'ser origen de authz:reconcile de relaciones', 'relations');
|
|
648
669
|
}
|
|
670
|
+
const checked = assertEnumeratePage('enumerateRelationsMany', page, 'opaque', this.#driverLabel());
|
|
649
671
|
if (typeof this.#driver.enumerateRelationsMany === 'function')
|
|
650
|
-
return this.#driver.enumerateRelationsMany(partitions,
|
|
651
|
-
if (
|
|
672
|
+
return this.#driver.enumerateRelationsMany(partitions, checked);
|
|
673
|
+
if (checked?.after !== undefined) {
|
|
652
674
|
throw new AuthorizationConfigError('enumerateRelationsMany: el driver de relaciones no implementa enumerateRelationsMany y el respaldo del manager (K llamadas ' +
|
|
653
675
|
'a enumerateRelations) no sabe reanudar con un cursor: `after` no se acepta aquí (reanudar aplicaría el cursor de UNA ' +
|
|
654
676
|
'partición a todas y perdería tuplas en silencio). Implementa enumerateRelationsMany en el driver, o pagina partición a ' +
|
|
@@ -656,7 +678,7 @@ export class RelationsManager {
|
|
|
656
678
|
}
|
|
657
679
|
const tuples = [];
|
|
658
680
|
for (const partition of partitions) {
|
|
659
|
-
const one = await this.#driver.enumerateRelations(partition,
|
|
681
|
+
const one = await this.#driver.enumerateRelations(partition, checked);
|
|
660
682
|
if (one.cursor !== undefined) {
|
|
661
683
|
throw new AuthorizationConfigError(`enumerateRelationsMany: la partición ${scopeKey(partition)} tiene más tuplas de las que caben en una página y el driver ` +
|
|
662
684
|
'de relaciones no implementa enumerateRelationsMany: el respaldo del manager (K llamadas a enumerateRelations) no ' +
|
|
@@ -681,4 +703,27 @@ export class RelationsManager {
|
|
|
681
703
|
assertSubject(subject);
|
|
682
704
|
}
|
|
683
705
|
}
|
|
684
|
-
|
|
706
|
+
/**
|
|
707
|
+
* **Las opciones de `relate`/`unrelate` que cruzan al driver** (cierre de R1 · 🟡 2 del auditor + 🟠 C del tester): un
|
|
708
|
+
* objeto NUEVO con SOLO las claves conocidas (`transaction`, `actor`, `expiresAt`, con su presencia: «omitido» ≠
|
|
709
|
+
* `undefined` explícito no importa al driver, pero `'expiresAt' in options` sí al evento), leídas UNA vez — un `Proxy`
|
|
710
|
+
* de la app no llega al driver, ni sus símbolos, ni respuestas distintas entre la validación y la escritura. Lo que no
|
|
711
|
+
* es un objeto pasa tal cual (las validaciones de siempre lo juzgan).
|
|
712
|
+
*/
|
|
713
|
+
function knownWriteOptions(options) {
|
|
714
|
+
if (options === undefined || options === null || typeof options !== 'object')
|
|
715
|
+
return options;
|
|
716
|
+
const out = {};
|
|
717
|
+
for (const key of KNOWN_WRITE_OPTION_KEYS)
|
|
718
|
+
if (key in options)
|
|
719
|
+
out[key] = options[key];
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* **La lista de claves que cruzan, EXHAUSTIVA por tipo** (cierre-2 de R1 · D3 (b), injerto del analista y del auditor):
|
|
724
|
+
* el `satisfies` exige UNA entrada por cada clave de `RelationWriteOptions` (y ninguna más). Una clave nueva del tipo —una
|
|
725
|
+
* de seguridad, por ejemplo— que no se decida aquí NO COMPILA: hasta el cierre-2 se perdía en silencio en el manager
|
|
726
|
+
* (fallo abierto si la clave era una guarda). Decidir = `true` (cruza al driver) o sacarla del tipo.
|
|
727
|
+
*/
|
|
728
|
+
const KNOWN_WRITE_OPTIONS = { transaction: true, actor: true, expiresAt: true };
|
|
729
|
+
export const KNOWN_WRITE_OPTION_KEYS = Object.freeze(Object.keys(KNOWN_WRITE_OPTIONS));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RelationPage } from '../types.js';
|
|
2
|
+
/** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
|
|
3
|
+
export declare const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
|
|
4
|
+
/** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
|
|
5
|
+
export declare const RELATIONS_ENUMERATE_MAX_LIMIT = 1000;
|
|
6
|
+
/** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
|
|
7
|
+
export declare function assertNoListPage(operation: string, page: unknown, driver: string): void;
|
|
8
|
+
/** Qué cursor emite quien valida: `uuid` (database), `none` (openfga, el doble), `opaque` (el manager). */
|
|
9
|
+
export type EnumerateCursorKind = 'uuid' | 'none' | 'opaque';
|
|
10
|
+
/**
|
|
11
|
+
* Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
|
|
12
|
+
* se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
|
|
13
|
+
* quien consulta usa la copia, jamás la página original.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertEnumeratePage(operation: string, page: unknown, cursor: EnumerateCursorKind, driver: string): RelationPage | undefined;
|
|
16
|
+
/** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
|
|
17
|
+
export declare function enumerateLimit(page: {
|
|
18
|
+
limit?: number;
|
|
19
|
+
} | undefined): number;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **La página de relaciones, en UN sitio** (lote «antes de RC» · 7.4, opción 4 del juez —`loteRC-74-paginacion-juez.md`,
|
|
3
|
+
* 88,5/100—, decisión del dueño 2026-10-17 (7.4)). Dos reglas, compartidas por el manager, los dos drivers y el doble:
|
|
4
|
+
*
|
|
5
|
+
* 1. **Los `list*` no paginan en 2.5.** `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en
|
|
6
|
+
* la firma. `undefined` (ausente o explícito) es «sin página»; CUALQUIER otro valor —`null`, `{}`, `{ after: '' }`,
|
|
7
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` ⇒ 500 `E_AUTHZ_UNSUPPORTED` nombrando la operación, con CERO
|
|
8
|
+
* llamadas al backend (`assertNoListPage`). Se mira `!== undefined`, jamás `arguments.length` (RC74-3: quien reenvía
|
|
9
|
+
* un opcional vacío no se rompe). Orden de las guardas (RC74-5): F-05 y partición ANTES; la página, la última.
|
|
10
|
+
* 2. **`enumerate*` valida su página** (`assertEnumeratePage`): `limit` entero ≥ 1 o 422 `E_AUTHZ_INVALID_PAGE`
|
|
11
|
+
* (`NaN` generaba una consulta SIN `LIMIT`, tester H2), por encima de `RELATIONS_ENUMERATE_MAX_LIMIT` se RECORTA
|
|
12
|
+
* (`enumerateLimit`: con cursor, sin pérdida; declarado en «Limits»); `after` cadena no vacía o 422, y según el
|
|
13
|
+
* cursor que el driver emite: `uuid` (`database`: el uuid canónico de la fila; un cursor ajeno era un 503 en PG y
|
|
14
|
+
* un fin silencioso en MySQL/SQLite, H3), `none` (`openfga` y el doble no emiten cursor ⇒ 500) u `opaque` (el
|
|
15
|
+
* manager, que no sabe qué emite el driver: solo la forma).
|
|
16
|
+
*
|
|
17
|
+
* Lo que NO hace (H3 a medias, a propósito): atar el cursor a su partición —un cursor de A reutilizado en B salta
|
|
18
|
+
* filas— exige un sobre de cursor; es del lote C / 2.6.
|
|
19
|
+
*/
|
|
20
|
+
import { InvalidRelationPageError, UnsupportedOperationError } from '../errors.js';
|
|
21
|
+
import { isCanonicalUuid } from '../identity.js';
|
|
22
|
+
/** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
|
|
23
|
+
export const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
|
|
24
|
+
/** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
|
|
25
|
+
export const RELATIONS_ENUMERATE_MAX_LIMIT = 1_000;
|
|
26
|
+
/** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
|
|
27
|
+
export function assertNoListPage(operation, page, driver) {
|
|
28
|
+
if (page !== undefined)
|
|
29
|
+
throw UnsupportedOperationError.listPage(operation, driver);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
|
|
33
|
+
* se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
|
|
34
|
+
* quien consulta usa la copia, jamás la página original.
|
|
35
|
+
*/
|
|
36
|
+
export function assertEnumeratePage(operation, page, cursor, driver) {
|
|
37
|
+
if (page === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (page === null || typeof page !== 'object' || Array.isArray(page)) {
|
|
40
|
+
throw new InvalidRelationPageError(`${operation}: la página tiene que ser un objeto { limit?, after? } (llegó ${describe(page)}).`);
|
|
41
|
+
}
|
|
42
|
+
const { limit, after } = page;
|
|
43
|
+
if (limit !== undefined && !(typeof limit === 'number' && Number.isInteger(limit) && limit >= 1)) {
|
|
44
|
+
throw new InvalidRelationPageError(`${operation}: page.limit tiene que ser un entero ≥ 1 (llegó ${describe(limit)}); por encima de ` +
|
|
45
|
+
`${RELATIONS_ENUMERATE_MAX_LIMIT} se recorta, no es un error.`);
|
|
46
|
+
}
|
|
47
|
+
if (after === undefined)
|
|
48
|
+
return limit === undefined ? {} : { limit };
|
|
49
|
+
if (typeof after !== 'string' || after.length === 0) {
|
|
50
|
+
throw new InvalidRelationPageError(`${operation}: page.after tiene que ser el cursor que devolvió la página anterior (llegó ${describe(after)}).`);
|
|
51
|
+
}
|
|
52
|
+
if (cursor === 'none')
|
|
53
|
+
throw UnsupportedOperationError.enumerateCursor(operation, driver);
|
|
54
|
+
if (cursor === 'uuid' && !isCanonicalUuid(after)) {
|
|
55
|
+
throw new InvalidRelationPageError(`${operation}: page.after no es un cursor de este driver ('${driver}' emite el uuid canónico de la última fila leída); ` +
|
|
56
|
+
`pasa el cursor que devolvió la página anterior, sin tocarlo.`);
|
|
57
|
+
}
|
|
58
|
+
return limit === undefined ? { after } : { limit, after };
|
|
59
|
+
}
|
|
60
|
+
/** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
|
|
61
|
+
export function enumerateLimit(page) {
|
|
62
|
+
return Math.min(page?.limit ?? RELATIONS_ENUMERATE_DEFAULT_LIMIT, RELATIONS_ENUMERATE_MAX_LIMIT);
|
|
63
|
+
}
|
|
64
|
+
function describe(value) {
|
|
65
|
+
if (typeof value === 'string')
|
|
66
|
+
return `'${value.slice(0, 40)}'`;
|
|
67
|
+
if (typeof value === 'number')
|
|
68
|
+
return String(value);
|
|
69
|
+
if (value === null)
|
|
70
|
+
return 'null';
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return 'un array';
|
|
73
|
+
return typeof value;
|
|
74
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Clock } from '../clock.js';
|
|
2
2
|
import type { RelationsDriver, ScopeRef } from '../types.js';
|
|
3
|
+
import type { DenyReachFinding } from './deny_reach.js';
|
|
3
4
|
import type { RelationsConfig } from './define_relations_config.js';
|
|
4
5
|
export interface RelationsReconcileOptions {
|
|
5
6
|
/** El driver del que se LEEN las tuplas (origen). Necesita `enumerateRelations`. */
|
|
@@ -50,6 +51,16 @@ export interface RelationsReconcileOptions {
|
|
|
50
51
|
* sigue — nunca un throw a mitad de pasada.
|
|
51
52
|
*/
|
|
52
53
|
toConfig?: RelationsConfig;
|
|
54
|
+
/**
|
|
55
|
+
* **El detector del alcance de los denies (8)**: con él, la pasada lista en `report.denyReach` los denies vivos de sus
|
|
56
|
+
* particiones (origen y destino) cuyo alcance encoge de `baseline` (la config persistida; `null` = sin línea base, solo
|
|
57
|
+
* huérfanos) a `current` (la config que sirve el proceso). Solo LEE lo que la pasada ya enumeró: cero lecturas y cero
|
|
58
|
+
* escrituras de más. Lo pide `authz:relations:reconcile --dry-run`.
|
|
59
|
+
*/
|
|
60
|
+
denyReach?: {
|
|
61
|
+
baseline: RelationsConfig | null;
|
|
62
|
+
current: RelationsConfig;
|
|
63
|
+
};
|
|
53
64
|
/**
|
|
54
65
|
* El reloj que decide qué tupla del ORIGEN está CADUCADA (R-15): una caducada
|
|
55
66
|
* no se migra —se cuenta en `skipped.expired`— y no respalda nada en el
|
|
@@ -80,7 +91,7 @@ export interface RelationsReconcileOptions {
|
|
|
80
91
|
* cierre 5 de D2 (opción A) NIEGA SU PARTICIÓN (`partitionsRefused`: cero escrituras y cero borrados en ella, el resto
|
|
81
92
|
* migra); la línea la nombra con su receta y es deriva. `withheld-by-unsafe-deny` (cierre 4) se fue con la retención.
|
|
82
93
|
*/
|
|
83
|
-
export declare const RELATIONS_RECONCILE_DETAIL_REASONS: readonly ["invalid-identity", "userset-unsupported", "undeclared", "deny-conditional", "renewal-unsafe"];
|
|
94
|
+
export declare const RELATIONS_RECONCILE_DETAIL_REASONS: readonly ["invalid-identity", "userset-unsupported", "undeclared", "deny-conditional", "renewal-unsafe", "invalid-condition"];
|
|
84
95
|
export type RelationsReconcileDetailReason = (typeof RELATIONS_RECONCILE_DETAIL_REASONS)[number];
|
|
85
96
|
export interface RelationsReconcileDetail {
|
|
86
97
|
/** De qué store llegó enumerada. */
|
|
@@ -315,13 +326,57 @@ export interface RelationsReconcileReport {
|
|
|
315
326
|
* la pasada real dan los mismos números. Es deriva (exit ≠ 0) mientras exista. Opcional en el tipo; ausente = `[]`.
|
|
316
327
|
*/
|
|
317
328
|
partitionsRefused?: RelationsPartitionRefusal[];
|
|
329
|
+
/**
|
|
330
|
+
* **Las particiones cuya cadena de grupos FINAL excede el techo del destino** (lote «antes de RC» · R1, decisión del
|
|
331
|
+
* dueño 2026-10-17 (5); 🔴 C-1 del auditor): por cada una, la profundidad (saltos `group⊂group` del camino más largo,
|
|
332
|
+
* con los ciclos contados de forma conservadora), el techo (`to.groupDepthLimit()`, el CALIBRADO del destino) y la
|
|
333
|
+
* cadena. Esa partición está además en `partitionsRefused` (causa `too-deep`): cero escrituras en ella — saltar la
|
|
334
|
+
* arista que excede quitaría los denies que transporta y fabricaría una concesión —. Lo decide el PLAN: mismos números
|
|
335
|
+
* en `--dry-run` y en la real. Opcional en el tipo; ausente = `[]`.
|
|
336
|
+
*/
|
|
337
|
+
tooDeep?: Array<{
|
|
338
|
+
partition: string;
|
|
339
|
+
depth: number;
|
|
340
|
+
limit: number;
|
|
341
|
+
chain: string[];
|
|
342
|
+
}>;
|
|
343
|
+
/**
|
|
344
|
+
* **El detector de (8)** (lote RC · sublote (8)): solo si se pidió (`denyReach` en las opciones; el comando lo pide en
|
|
345
|
+
* `--dry-run`). Los denies VIVOS de las particiones de la pasada —en el ORIGEN y en el DESTINO, enumerados por la misma
|
|
346
|
+
* pasada— juzgados con `denyReachCheck` (`src/relations/deny_reach.ts`, el mismo punto de paso que la guarda de
|
|
347
|
+
* `database` y que `checkRelationsConfigDenyReach`), en las dos direcciones entre la línea base (`baseline`: la config
|
|
348
|
+
* persistida) y la config que SIRVE el proceso (`current`). Sirve a los dos drivers; `findings` y `stale` son deriva
|
|
349
|
+
* (exit ≠ 0); `unjudged` (sin línea base) NO lo es (firma (8 · `--dry-run`): exit 0 con su línea, el runbook no cambia).
|
|
350
|
+
*/
|
|
351
|
+
denyReach?: {
|
|
352
|
+
/** `'persisted'`: se comparó contra la config persistida; `'none'`: sin línea base, solo se buscan huérfanos. */
|
|
353
|
+
baseline: 'persisted' | 'none';
|
|
354
|
+
/** Persistida → servida: `shrunk` y `orphan` (también sin línea base). `count` = claves de tupla DISTINTAS (firma (8 · `count`)). */
|
|
355
|
+
findings: Array<DenyReachFinding & {
|
|
356
|
+
sides: Array<'origen' | 'destino'>;
|
|
357
|
+
}>;
|
|
358
|
+
/** Servida → persistida: la línea base RANCIA (`stale`, solo con línea base; firma (8 · rancia)). */
|
|
359
|
+
stale: Array<DenyReachFinding & {
|
|
360
|
+
sides: Array<'origen' | 'destino'>;
|
|
361
|
+
}>;
|
|
362
|
+
/** Sin línea base: cuántos denies vivos (claves distintas, no huérfanos) quedan SIN juzgar. `0` con línea base. */
|
|
363
|
+
unjudged: number;
|
|
364
|
+
};
|
|
318
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Por qué se NIEGA una partición: `renewal-unsafe` (membresía portadora que cambia de caducidad), `deny-conditional` (deny
|
|
368
|
+
* con caducidad que el destino no puede rehacer) o, desde el cierre de L11 (🟡 A2 del auditor), `invalid-condition`: una
|
|
369
|
+
* tupla de `group` (una membresía, portadora posible) con una condición que el paquete no escribe ni sabe leer
|
|
370
|
+
* (`foreign`/`unparseable`) en una partición con ALGÚN deny — el servidor puede estar evaluándola VIVA (un `not_expired`
|
|
371
|
+
* con `current_time` congelado lo está, medido) y transportando ese deny; saltarla como allow fabricaba una concesión.
|
|
372
|
+
*/
|
|
373
|
+
export type RelationsPartitionRefusalReason = 'renewal-unsafe' | 'deny-conditional' | 'invalid-condition' | 'too-deep';
|
|
319
374
|
/** Una partición NEGADA (cierre 5 de D2): por qué, qué receta, y lo que la pasada habría hecho en ella y NO hizo. */
|
|
320
375
|
export interface RelationsPartitionRefusal {
|
|
321
376
|
partition: string;
|
|
322
377
|
/** Las causas, una por tupla: `renewal-unsafe` (membresía portadora) o `deny-conditional` (deny con caducidad). */
|
|
323
378
|
causes: Array<{
|
|
324
|
-
reason:
|
|
379
|
+
reason: RelationsPartitionRefusalReason;
|
|
325
380
|
tuple: string;
|
|
326
381
|
}>;
|
|
327
382
|
/** La deriva que queda a la vista: lo que la pasada escribiría, renovaría y (con `--prune`) borraría en ella. */
|
|
@@ -397,7 +452,7 @@ export declare function denyRefusalRecipe(reason: DenyRefusal['reason'], objectT
|
|
|
397
452
|
* openfga/openfga#2151—) NO existe forma sin ventana de cambiar la caducidad de una membresía que transporta un deny, ni
|
|
398
453
|
* de rehacer permanente un deny con caducidad. Ninguna de las salidas de abajo se presenta como segura.
|
|
399
454
|
*/
|
|
400
|
-
export declare function partitionRefusalRecipe(reasons: Iterable<
|
|
455
|
+
export declare function partitionRefusalRecipe(reasons: Iterable<RelationsPartitionRefusalReason>): string;
|
|
401
456
|
/**
|
|
402
457
|
* Migra las tuplas de relación de `from` a `to` en `partition`. Idempotente
|
|
403
458
|
* (una segunda pasada no escribe nada) y bidireccional (funciona en cualquier
|
|
@@ -469,4 +524,3 @@ export declare const RELATION_RESIDUES: Readonly<Record<RelationsReconcileDetail
|
|
|
469
524
|
how: string;
|
|
470
525
|
why: string;
|
|
471
526
|
}>>;
|
|
472
|
-
//# sourceMappingURL=reconcile.d.ts.map
|