@jantstack/adonis-authz 2.4.0-alpha.9 → 2.5.0-alpha.2
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 +468 -30
- package/build/commands/authz_freeze.js +1 -1
- package/build/commands/authz_freeze.js.map +1 -1
- package/build/commands/authz_reconcile.d.ts +6 -2
- package/build/commands/authz_reconcile.d.ts.map +1 -1
- package/build/commands/authz_reconcile.js +129 -13
- package/build/commands/authz_reconcile.js.map +1 -1
- package/build/commands/authz_relations_reconcile.d.ts +16 -24
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +198 -49
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/commands/authz_scopes_relay.d.ts.map +1 -1
- package/build/commands/authz_scopes_relay.js +81 -1
- package/build/commands/authz_scopes_relay.js.map +1 -1
- package/build/commands/authz_unfreeze.d.ts +12 -4
- package/build/commands/authz_unfreeze.d.ts.map +1 -1
- package/build/commands/authz_unfreeze.js +74 -9
- package/build/commands/authz_unfreeze.js.map +1 -1
- package/build/index.d.ts +6 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +1 -1
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +5 -1
- package/build/providers/authz_provider.js.map +1 -1
- package/build/src/define_config.d.ts +41 -0
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js +120 -0
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +1 -0
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +18 -6
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +94 -46
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +46 -5
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +115 -29
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +82 -13
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +216 -55
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +86 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +86 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +63 -4
- package/build/src/freeze.d.ts.map +1 -1
- package/build/src/freeze.js +107 -18
- package/build/src/freeze.js.map +1 -1
- package/build/src/identity.d.ts +120 -9
- package/build/src/identity.d.ts.map +1 -1
- package/build/src/identity.js +164 -12
- package/build/src/identity.js.map +1 -1
- package/build/src/manager.d.ts +64 -2
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +267 -35
- package/build/src/manager.js.map +1 -1
- package/build/src/openfga.d.ts +2 -2
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +2 -2
- package/build/src/openfga.js.map +1 -1
- package/build/src/relations/check_many.d.ts +17 -0
- package/build/src/relations/check_many.d.ts.map +1 -0
- package/build/src/relations/check_many.js +57 -0
- package/build/src/relations/check_many.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +74 -1
- package/build/src/relations/define_relations_config.d.ts.map +1 -1
- package/build/src/relations/define_relations_config.js +201 -7
- package/build/src/relations/define_relations_config.js.map +1 -1
- package/build/src/relations/manager.d.ts +20 -0
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +88 -15
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +134 -3
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +340 -36
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/relations/tuple_key.d.ts +25 -0
- package/build/src/relations/tuple_key.d.ts.map +1 -0
- package/build/src/relations/tuple_key.js +60 -0
- package/build/src/relations/tuple_key.js.map +1 -0
- package/build/src/relations_config_store.d.ts.map +1 -1
- package/build/src/relations_config_store.js +29 -7
- package/build/src/relations_config_store.js.map +1 -1
- package/build/src/relations_reconcile.d.ts +79 -0
- package/build/src/relations_reconcile.d.ts.map +1 -0
- package/build/src/relations_reconcile.js +85 -0
- package/build/src/relations_reconcile.js.map +1 -0
- package/build/src/scope_outbox.d.ts +9 -5
- package/build/src/scope_outbox.d.ts.map +1 -1
- package/build/src/scope_outbox.js +7 -4
- package/build/src/scope_outbox.js.map +1 -1
- package/build/src/shared/backend_guard.d.ts +30 -11
- package/build/src/shared/backend_guard.d.ts.map +1 -1
- package/build/src/shared/backend_guard.js +45 -15
- package/build/src/shared/backend_guard.js.map +1 -1
- package/build/src/shared/sql_expiry.d.ts +2 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -1
- package/build/src/shared/sql_expiry.js +4 -0
- package/build/src/shared/sql_expiry.js.map +1 -1
- package/build/src/testing/main.d.ts +3 -1
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +2 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +9 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -1
- package/build/src/testing/migration_contract.js +5 -2
- package/build/src/testing/migration_contract.js.map +1 -1
- package/build/src/testing/relations_contract.d.ts +14 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -1
- package/build/src/testing/relations_contract.js +367 -21
- package/build/src/testing/relations_contract.js.map +1 -1
- package/build/src/testing/scope_outbox_contract.d.ts +14 -1
- package/build/src/testing/scope_outbox_contract.d.ts.map +1 -1
- package/build/src/testing/scope_outbox_contract.js +126 -48
- package/build/src/testing/scope_outbox_contract.js.map +1 -1
- package/build/src/testing/test_clock.d.ts +15 -0
- package/build/src/testing/test_clock.d.ts.map +1 -0
- package/build/src/testing/test_clock.js +65 -0
- package/build/src/testing/test_clock.js.map +1 -0
- package/build/src/types.d.ts +115 -12
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js.map +1 -1
- package/package.json +6 -1
package/build/src/manager.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { Exception } from '@adonisjs/core/exceptions';
|
|
3
3
|
import { v7 as uuidv7 } from 'uuid';
|
|
4
|
-
import {
|
|
4
|
+
import { activeRelationsDriverKey, assertAuthorizationConfigShape } from './define_config.js';
|
|
5
|
+
import { assertCatalogUuid, assertIdentity, assertScope, assertScopeType, assertSubject, assertValidSlug, chainKeysFrom, normalizeRoleQuery, scopeFromKey, scopeKey, } from './identity.js';
|
|
5
6
|
import { expiryChanged } from './expiry.js';
|
|
6
7
|
import { randomBytes } from 'node:crypto';
|
|
7
|
-
import { DEFAULT_FREEZE_LEASE_MS,
|
|
8
|
-
import { assertKnownScope, isAuthzError, resolveChain, rootOnlyResolver } from './shared/backend_guard.js';
|
|
8
|
+
import { DEFAULT_FREEZE_LEASE_MS, acquireFreezeOutcome, UNKNOWN_HOLDER, freezeIsLive, freezeKindOf, readFreezeRow, assertNotFrozenRow, releaseFreeze, renewFreeze, } from './freeze.js';
|
|
9
|
+
import { assertKnownScope, canonicalScopeTargets, isAuthzError, resolveChain, rootOnlyResolver } from './shared/backend_guard.js';
|
|
9
10
|
import { CatalogCache, GLOBAL_OWNER_KEY, invalidateAuthzCatalog, isRoleVisibleWith, readLocalRoles, withAuthzCatalogWrite } from './catalog/catalog_cache.js';
|
|
10
11
|
import { assertAssignableAt } from './catalog/catalog.js';
|
|
11
12
|
import { systemClock } from './clock.js';
|
|
12
|
-
import { ActorRequiredError, AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, AuthorizationConfigError, AuthorizationInternalError, CatalogConflictError, InvalidIdentityError, MassPurgeRefusedError, PruneInterruptedError, NoDescendantsResolverError, NotWithinError, PermissionNotDelegableError, RankExceededError, RoleImmutableError, RoleLevelAboveOwnerError, ScopeCycleError, ScopeResolverError, TooManyScopesError, UnknownPermissionError, UnknownRoleError, FreezeHeldError, UnsupportedOperationError, ViewExpiredError, WithinRequiredError, WithinRootForbiddenError, ScopeDriftUnguardedError, ScopeDepthLimitError, ScopeRequeueRefusedError, } from './errors.js';
|
|
13
|
+
import { ActorRequiredError, AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, AuthorizationConfigError, AuthorizationInternalError, CatalogConflictError, InvalidIdentityError, MassPurgeRefusedError, PruneInterruptedError, NoDescendantsResolverError, NotWithinError, PermissionNotDelegableError, RankExceededError, RoleImmutableError, RoleLevelAboveOwnerError, ScopeCycleError, ScopeResolverError, TooManyScopesError, UnknownPermissionError, UnknownRoleError, FreezeHeldError, UnsupportedOperationError, ViewExpiredError, WithinRequiredError, WithinRootForbiddenError, ScopeDriftUnguardedError, ScopeDepthLimitError, ScopeRequeueRefusedError, StaleSourceRefusedError, OperatorWindowExpiredError, } from './errors.js';
|
|
13
14
|
import { APP_SCOPE_TYPE, normalizeAuthorizationCapabilities } from './types.js';
|
|
14
15
|
import { memoizeAncestors } from './memoize_ancestors.js';
|
|
15
16
|
import { AUTHZ_TABLES_ORIGIN } from './reconcile.js';
|
|
@@ -20,12 +21,26 @@ const ROLE_DESCRIPTION_MAX = 500;
|
|
|
20
21
|
function describeAnswer(answer) {
|
|
21
22
|
if (!Array.isArray(answer))
|
|
22
23
|
return `${answer === null ? 'null' : typeof answer} (no es un array)`;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
for (let i = 0; i < answer.length; i++) {
|
|
25
|
+
if (!(i in answer))
|
|
26
|
+
return `un array de ${answer.length} con un HUECO en la posición ${i}`;
|
|
27
|
+
if (typeof answer[i] !== 'boolean')
|
|
28
|
+
return `un array de ${answer.length} con un elemento que no es boolean en la posición ${i} (${typeof answer[i]})`;
|
|
26
29
|
}
|
|
27
30
|
return `un array de ${answer.length}`;
|
|
28
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* ¿Cada POSICIÓN del array es un boolean? Por índice, nunca con `some`/`every`
|
|
34
|
+
* (que saltan los huecos de un array disperso: 🟡 A3 del auditor del lote A;
|
|
35
|
+
* gemelo de `checkMany` en `src/relations/manager.ts`).
|
|
36
|
+
*/
|
|
37
|
+
function everyPositionBoolean(answers) {
|
|
38
|
+
for (let i = 0; i < answers.length; i++) {
|
|
39
|
+
if (typeof answers[i] !== 'boolean')
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
29
44
|
/**
|
|
30
45
|
* Configs que ya recibieron el aviso de seguridad opt-in (B7/B1): una vez
|
|
31
46
|
* por config, no por instancia — `forRequest()` construye managers hijos con
|
|
@@ -140,6 +155,11 @@ export class AuthorizationManager {
|
|
|
140
155
|
*/
|
|
141
156
|
#heldFreeze = null;
|
|
142
157
|
constructor(config) {
|
|
158
|
+
// Cierre 1 del lote B · 🟡 6 (L5): la FORMA del config se valida aquí también —
|
|
159
|
+
// el provider construye con `new AuthorizationManager(config)` y un consumidor
|
|
160
|
+
// puede saltarse `defineConfig`; un typo (`requireWithing`) no puede entrar en
|
|
161
|
+
// silencio por ninguna puerta.
|
|
162
|
+
assertAuthorizationConfigShape(config);
|
|
143
163
|
this.#config = config;
|
|
144
164
|
if (config.clock !== undefined && typeof config.clock !== 'function') {
|
|
145
165
|
throw new AuthorizationConfigError(`AuthorizationManager: config.clock debe ser una función () => Date (llegó ${typeof config.clock})`);
|
|
@@ -406,12 +426,30 @@ export class AuthorizationManager {
|
|
|
406
426
|
const finalReason = reason ?? 'una operación de plataforma';
|
|
407
427
|
const holder = `${kind}:${process.pid}:${randomBytes(4).toString('hex')}`;
|
|
408
428
|
const nowMs = root.#wallMs();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
429
|
+
// alpha.10 · S2 (+ cierre del auditor · R2): una ventana de OPERADOR
|
|
430
|
+
// caducada no se pisa en silencio. Una fila con el lease vencido se toma
|
|
431
|
+
// encima (así la flota vuelve sola tras un SIGKILL de una pasada), pero la
|
|
432
|
+
// del operador es EVIDENCIA: pisarla y soltarla después la deja a NULL y
|
|
433
|
+
// borra el único rastro de que las escrituras volvieron a entrar a mitad
|
|
434
|
+
// del cutover. La exclusión vive DENTRO del UPDATE de `acquireFreezeOutcome`
|
|
435
|
+
// (atómica): aquí solo se lee el veredicto y la fila para redactarlo — un
|
|
436
|
+
// «compruebo y luego tomo» con dos lecturas del reloj era un TOCTOU.
|
|
437
|
+
const outcome = await acquireFreezeOutcome({ reason: finalReason, holder, untilMs: leaseMs === null ? null : nowMs + leaseMs, nowMs }, { driver: this.#config.default });
|
|
438
|
+
if (outcome.status === 'expired-operator') {
|
|
439
|
+
const row = outcome.row;
|
|
440
|
+
const when = row.untilMs === null ? '?' : new Date(row.untilMs).toISOString();
|
|
441
|
+
throw new OperatorWindowExpiredError(`freeze (${reason ?? kind}): la ventana del OPERADOR (${row.holder ?? '?'}, ${row.reason}, fence ` +
|
|
442
|
+
`${row.fence}) CADUCÓ el ${when} y nadie la cerró: desde entonces las escrituras volvieron a entrar en ` +
|
|
443
|
+
`toda la flota. No se toma una ventana propia encima —borraría ese rastro al soltarla y la pasada se ` +
|
|
444
|
+
`certificaría como si hubiera corrido dentro del cutover—. Reconócela con authz:unfreeze (dice cuánto ` +
|
|
445
|
+
`tiempo estuvo abierta y limpia la fila) y reabre la ventana con authz:freeze antes de repetir.`);
|
|
446
|
+
}
|
|
447
|
+
if (outcome.status === 'held') {
|
|
448
|
+
const row = outcome.row;
|
|
412
449
|
throw new FreezeHeldError(`freeze: ya hay un freeze VIVO de otro dueño (${row.holder ?? '?'}, fence ${row.fence}, motivo: ${row.reason ?? '?'}). ` +
|
|
413
450
|
`Espera a que termine, o levántalo con authz:unfreeze si su proceso murió sin lease.`);
|
|
414
451
|
}
|
|
452
|
+
const token = outcome.token;
|
|
415
453
|
const held = { token, reason: finalReason, kind, leaseMs, timer: null, lapsed: false };
|
|
416
454
|
if (leaseMs !== null) {
|
|
417
455
|
const interval = Math.max(250, Math.floor(leaseMs / 3));
|
|
@@ -472,6 +510,39 @@ export class AuthorizationManager {
|
|
|
472
510
|
get frozen() {
|
|
473
511
|
return this.#root().#heldFreeze !== null;
|
|
474
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* La clave (en `relations.drivers`) del driver de RELACIONES activo:
|
|
515
|
+
* `relations.default ?? default` (2.4.0-alpha.10). La lee el guard de la
|
|
516
|
+
* dirección rancia de `runRelationsReconcile`; desde el cierre del auditor
|
|
517
|
+
* (R4) el llamante ya no puede contradecirla con un `activeKey` propio.
|
|
518
|
+
*/
|
|
519
|
+
relationsDriverKey() {
|
|
520
|
+
return activeRelationsDriverKey(this.#root().#config);
|
|
521
|
+
}
|
|
522
|
+
/** Las claves registradas en `relations.drivers` del config de ESTE manager (vacío si no hay relaciones). */
|
|
523
|
+
relationsDriverKeys() {
|
|
524
|
+
return Object.keys(this.#root().#config.relations?.drivers ?? {});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* La factory registrada bajo `key` en `relations.drivers` de ESTE manager, o
|
|
528
|
+
* `undefined` (cierre del auditor de alpha.10 · R4). `runRelationsReconcile`
|
|
529
|
+
* resuelve el origen y el destino por aquí —del MISMO config que decide el
|
|
530
|
+
* activo— y juzga la identidad por la factory resuelta, no por la etiqueta:
|
|
531
|
+
* dos claves con la misma factory son el mismo driver (y dos factories DISTINTAS
|
|
532
|
+
* sobre el mismo backend NO: límite declarado, cierre-2 · C1).
|
|
533
|
+
*
|
|
534
|
+
* **API de PLATAFORMA, junto a `driver()`** (cierre-2 · C6): con esta factory
|
|
535
|
+
* cualquier código que tenga el manager —vistas de `forRequest()` incluidas—
|
|
536
|
+
* construye un driver de relaciones NO activo y escribe en él sin `assertWrite`,
|
|
537
|
+
* `requireActor`, freeze ni `onRelationWrite`. No añade capacidad (el config ya
|
|
538
|
+
* da las factories), pero es otra cosa que buscar en una revisión.
|
|
539
|
+
*/
|
|
540
|
+
relationsDriverFactory(key) {
|
|
541
|
+
const drivers = this.#root().#config.relations?.drivers;
|
|
542
|
+
if (drivers === undefined || !Object.prototype.hasOwnProperty.call(drivers, key))
|
|
543
|
+
return undefined;
|
|
544
|
+
return drivers[key];
|
|
545
|
+
}
|
|
475
546
|
/** El freeze VIVO de la fila compartida, o `null`. Lo lee cualquiera; solo el token lo levanta. */
|
|
476
547
|
async freezeStatus() {
|
|
477
548
|
const row = await readFreezeRow({ driver: this.#config.default });
|
|
@@ -479,12 +550,42 @@ export class AuthorizationManager {
|
|
|
479
550
|
return null;
|
|
480
551
|
return {
|
|
481
552
|
reason: row.reason,
|
|
482
|
-
holder: row.holder ??
|
|
553
|
+
holder: row.holder ?? UNKNOWN_HOLDER,
|
|
483
554
|
kind: freezeKindOf(row.holder),
|
|
484
555
|
untilMs: row.untilMs,
|
|
485
556
|
fence: row.fence,
|
|
486
557
|
};
|
|
487
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* **La ÚLTIMA ventana de la fila, también la CADUCADA** (2.4.0-alpha.10, 🟠 2
|
|
561
|
+
* del sexto informe de COGNITIV). `freezeStatus()` responde solo lo VIVO —y
|
|
562
|
+
* así lo siguen usando la barrera y los comandos—, de modo que una ventana
|
|
563
|
+
* cuyo lease venció era indistinguible de «nunca hubo»: `authz:unfreeze`
|
|
564
|
+
* decía éxito y el último paso del cutover no delataba que las escrituras
|
|
565
|
+
* habían vuelto a entrar a mitad. Se distingue por la fila: `releaseFreeze`
|
|
566
|
+
* pone el motivo a `null` al levantar, así que motivo puesto + `untilMs` en
|
|
567
|
+
* el pasado es una ventana que CADUCÓ sin que nadie la cerrase.
|
|
568
|
+
*
|
|
569
|
+
* `null` = nunca hubo o ya se levantó; `expired: false` = viva (lo mismo que
|
|
570
|
+
* `freezeStatus()`); `expired: true` = caducada. `checkedAtMs` es el instante
|
|
571
|
+
* del reloj del config con el que se decidió.
|
|
572
|
+
*/
|
|
573
|
+
async lastFreezeStatus() {
|
|
574
|
+
const row = await readFreezeRow({ driver: this.#config.default });
|
|
575
|
+
if (row.reason === null)
|
|
576
|
+
return null;
|
|
577
|
+
const nowMs = this.#root().#wallMs();
|
|
578
|
+
return {
|
|
579
|
+
reason: row.reason,
|
|
580
|
+
holder: row.holder ?? UNKNOWN_HOLDER,
|
|
581
|
+
kind: freezeKindOf(row.holder),
|
|
582
|
+
untilMs: row.untilMs,
|
|
583
|
+
fence: row.fence,
|
|
584
|
+
expired: !freezeIsLive(row, nowMs),
|
|
585
|
+
// El instante (reloj del config) con el que se decidió `expired`.
|
|
586
|
+
checkedAtMs: nowMs,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
488
589
|
/**
|
|
489
590
|
* `freeze()` + `finally unfreeze(token)`. El `finally` es la parte que
|
|
490
591
|
* importa: una migración que revienta a la mitad no puede dejar la
|
|
@@ -502,7 +603,13 @@ export class AuthorizationManager {
|
|
|
502
603
|
operatorAsContext: options.operatorAsContext === true,
|
|
503
604
|
});
|
|
504
605
|
try {
|
|
505
|
-
return await fn({
|
|
606
|
+
return await fn({
|
|
607
|
+
fence: context.fence,
|
|
608
|
+
leaseMs: context.leaseMs,
|
|
609
|
+
lapsed: context.lapsed,
|
|
610
|
+
...(context.untilMs === undefined ? {} : { untilMs: context.untilMs }),
|
|
611
|
+
...(context.window === undefined ? {} : { window: context.window }),
|
|
612
|
+
});
|
|
506
613
|
}
|
|
507
614
|
finally {
|
|
508
615
|
await context.release();
|
|
@@ -528,6 +635,7 @@ export class AuthorizationManager {
|
|
|
528
635
|
return {
|
|
529
636
|
fence: outer.token.fence,
|
|
530
637
|
leaseMs: outer.leaseMs,
|
|
638
|
+
window: 'enclosing',
|
|
531
639
|
release: async () => { },
|
|
532
640
|
lapsed: () => root.#tokenLapsed(outer.token, outer.lapsed),
|
|
533
641
|
};
|
|
@@ -536,9 +644,14 @@ export class AuthorizationManager {
|
|
|
536
644
|
const status = await this.freezeStatus();
|
|
537
645
|
if (status !== null && status.kind === 'operator') {
|
|
538
646
|
const token = { fence: status.fence, holder: status.holder };
|
|
647
|
+
// alpha.10 · 3: la caducidad REAL de la ventana adoptada. Hasta alpha.9
|
|
648
|
+
// `leaseMs: null` fijo — el reporte decía «sin caducidad» de una ventana
|
|
649
|
+
// abierta con `--lease-ms` (la certificación sí leía la fila).
|
|
539
650
|
return {
|
|
540
651
|
fence: status.fence,
|
|
541
|
-
leaseMs: null,
|
|
652
|
+
leaseMs: status.untilMs === null ? null : Math.max(0, status.untilMs - root.#wallMs()),
|
|
653
|
+
untilMs: status.untilMs,
|
|
654
|
+
window: 'operator',
|
|
542
655
|
release: async () => { },
|
|
543
656
|
lapsed: () => root.#tokenLapsed(token, false),
|
|
544
657
|
};
|
|
@@ -549,6 +662,7 @@ export class AuthorizationManager {
|
|
|
549
662
|
return {
|
|
550
663
|
fence: token.fence,
|
|
551
664
|
leaseMs: held.leaseMs,
|
|
665
|
+
window: 'own',
|
|
552
666
|
release: async () => {
|
|
553
667
|
try {
|
|
554
668
|
await this.unfreeze(token);
|
|
@@ -731,14 +845,19 @@ export class AuthorizationManager {
|
|
|
731
845
|
//
|
|
732
846
|
// Y cuando NO hay cadena —la fila ya no existe, que es el orden
|
|
733
847
|
// soportado de `detached` (3F · S1)— no hay con qué canonizar: se
|
|
734
|
-
// purgan
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
848
|
+
// purgan las ortografías de las que el uuid del llamante puede ser
|
|
849
|
+
// alias (`scopeSpellings`, 3b-2h · 🟠 3: lo que PG funde,
|
|
850
|
+
// `pgFusedSpellings`) MENOS las que el resolutor reconozca como un
|
|
851
|
+
// scope VIVO distinto del pedido (cierre-3 del lote A · 🟡 C2-2: con
|
|
852
|
+
// ids de texto, la canónica de un alias puede ser la unit viva de otro
|
|
853
|
+
// tenant, y `purgeScope` en `database` le borraría los denies). Con la
|
|
854
|
+
// fila viva esto es exactamente una, la de la tabla. El punto de paso
|
|
855
|
+
// es ÚNICO y compartido con `revoke`/`removeDeny` de los dos drivers
|
|
856
|
+
// (`canonicalScopeTargets`); un resolutor que falla al contrastar es
|
|
857
|
+
// 503, nunca «purgar por si acaso». Antes se usaba la del llamante a
|
|
858
|
+
// secas: `purgeScope` demostraba cero sobre un objeto que no existe,
|
|
859
|
+
// devolvía OK, y el scope real seguía concediendo para siempre.
|
|
860
|
+
const targets = await canonicalScopeTargets(this.#freshResolver(), child, 'scopes.detached');
|
|
742
861
|
const outbox = this.#outbox();
|
|
743
862
|
if (outbox) {
|
|
744
863
|
// La identidad se resuelve AQUÍ, con la fila del consumidor todavía
|
|
@@ -857,12 +976,13 @@ export class AuthorizationManager {
|
|
|
857
976
|
throw new AuthorizationConfigError(`authz:scopes:relay: deadDetail solo admite 'full' (llegó ${JSON.stringify(options.deadDetail)}); sin él, bajo only, dead[] va sin error.`);
|
|
858
977
|
}
|
|
859
978
|
const inputs = await _a.#relayInputs(outbox, options.only, batchSize, options.deadDetail);
|
|
860
|
-
const { only, filter, dead, deadCount, deadEntries,
|
|
979
|
+
const { only, filter, dead, deadCount, deadEntries, unseenOf } = inputs;
|
|
861
980
|
if (dryRun) {
|
|
862
981
|
const batch = await outbox.pending(limit, undefined, filter);
|
|
863
982
|
_a.#assertOnlyHonoured(batch, only);
|
|
864
983
|
const extra = batch.length >= limit ? true : false;
|
|
865
984
|
const seen = new Set([...batch.map((b) => String(b.id)), ...deadEntries.map((d) => String(d.id))]);
|
|
985
|
+
const unseen = await unseenOf(seen);
|
|
866
986
|
// La edad es GLOBAL: sin filtro la cabeza es `batch[0]`; con `only` hay que mirar la cola entera.
|
|
867
987
|
const queue = filter === undefined ? this.#queueOf(batch[0]) : (await this.#queueStatus(outbox)).queue;
|
|
868
988
|
return {
|
|
@@ -878,7 +998,7 @@ export class AuthorizationManager {
|
|
|
878
998
|
dryRun: true,
|
|
879
999
|
wouldApply: batch.map((item) => ({ id: item.id, change: item.change, attempts: item.attempts })),
|
|
880
1000
|
entries: deadEntries,
|
|
881
|
-
|
|
1001
|
+
...unseen,
|
|
882
1002
|
};
|
|
883
1003
|
}
|
|
884
1004
|
// El lease del escritor ÚNICO. Una outbox que no sabe darlo se comporta
|
|
@@ -919,8 +1039,14 @@ export class AuthorizationManager {
|
|
|
919
1039
|
// llegar a `batchSize`). Una sola lectura de más, y solo si no cabía.
|
|
920
1040
|
const census = dead.length < batchSize ? dead : await _a.#deadLetters(outbox, PARKED_CENSUS_MAX);
|
|
921
1041
|
const deadCount = census.length;
|
|
922
|
-
/**
|
|
923
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* Lo aparcado que se pidió (con `only`) o todo lo aparcado, para `entries`.
|
|
1044
|
+
* Cierre-2 · C2 (gemelo): con `only` sale del CENSO completo si la lectura se
|
|
1045
|
+
* cortó en `batchSize` — si no, un id pedido aparcado más allá del primer lote
|
|
1046
|
+
* era `missing` («no estaba ni pendiente ni aparcado») estando aparcado.
|
|
1047
|
+
*/
|
|
1048
|
+
const deadSource = only !== undefined && read.length >= batchSize ? census : read;
|
|
1049
|
+
const deadEntries = deadSource
|
|
924
1050
|
.filter((item) => only === undefined || only.has(String(item.id)))
|
|
925
1051
|
.map((item) => ({
|
|
926
1052
|
id: item.id,
|
|
@@ -929,8 +1055,29 @@ export class AuthorizationManager {
|
|
|
929
1055
|
attempts: item.attempts,
|
|
930
1056
|
...(item.error === undefined ? {} : { reason: item.error }),
|
|
931
1057
|
}));
|
|
932
|
-
|
|
933
|
-
|
|
1058
|
+
/**
|
|
1059
|
+
* **Qué fue de los ids pedidos que la pasada NO vio** (cierre-2 de alpha.10 ·
|
|
1060
|
+
* C2). Hasta el cierre-2 todos eran `missing`; pero un id fuera del lote porque
|
|
1061
|
+
* la pasada alcanzó `limit` sigue PENDIENTE. Se pregunta a la cola —filtrada por
|
|
1062
|
+
* esos ids, sin límite propio, UNA lectura y solo si hay alguno sin ver—: lo que
|
|
1063
|
+
* sigue pendiente es `notEvaluated`; el resto, `missing` (no está en la cola: lo
|
|
1064
|
+
* aparcado ya está en `seen`).
|
|
1065
|
+
*/
|
|
1066
|
+
const unseenOf = async (seen) => {
|
|
1067
|
+
if (only === undefined)
|
|
1068
|
+
return { missing: [], notEvaluated: [] };
|
|
1069
|
+
const unseen = [...only.values()].filter((id) => !seen.has(id)).map((id) => _a.#rawId(onlyIds, id));
|
|
1070
|
+
if (unseen.length === 0)
|
|
1071
|
+
return { missing: [], notEvaluated: [] };
|
|
1072
|
+
const stillPending = await outbox.pending(unseen.length, undefined, { ids: unseen });
|
|
1073
|
+
_a.#assertOnlyHonoured(stillPending, new Set(unseen.map(String)));
|
|
1074
|
+
const pendingIds = new Set(stillPending.map((item) => String(item.id)));
|
|
1075
|
+
return {
|
|
1076
|
+
missing: unseen.filter((id) => !pendingIds.has(String(id))),
|
|
1077
|
+
notEvaluated: unseen.filter((id) => pendingIds.has(String(id))),
|
|
1078
|
+
};
|
|
1079
|
+
};
|
|
1080
|
+
return { only, filter, dead, census, deadCount, deadEntries, unseenOf };
|
|
934
1081
|
}
|
|
935
1082
|
/** El reporte de una pasada que NO entró: otra tiene el lease. Nada aplicado, nada tocado. */
|
|
936
1083
|
async #busyReport(outbox, inputs) {
|
|
@@ -949,6 +1096,7 @@ export class AuthorizationManager {
|
|
|
949
1096
|
wouldApply: [],
|
|
950
1097
|
entries: inputs.deadEntries,
|
|
951
1098
|
missing: [],
|
|
1099
|
+
notEvaluated: [],
|
|
952
1100
|
};
|
|
953
1101
|
}
|
|
954
1102
|
/**
|
|
@@ -1020,7 +1168,7 @@ export class AuthorizationManager {
|
|
|
1020
1168
|
* (auditor 🟠 5: tomar el lease dos veces era `busy` en verde).
|
|
1021
1169
|
*/
|
|
1022
1170
|
async #drainScopeChanges(outbox, input) {
|
|
1023
|
-
const { limit, batchSize, only, filter, dead, census, deadCount, deadEntries,
|
|
1171
|
+
const { limit, batchSize, only, filter, dead, census, deadCount, deadEntries, unseenOf } = input;
|
|
1024
1172
|
const driver = await this.driver();
|
|
1025
1173
|
const applied = [];
|
|
1026
1174
|
const deferred = [];
|
|
@@ -1188,7 +1336,7 @@ export class AuthorizationManager {
|
|
|
1188
1336
|
dryRun: false,
|
|
1189
1337
|
wouldApply: [],
|
|
1190
1338
|
entries,
|
|
1191
|
-
|
|
1339
|
+
...(await unseenOf(new Set(entries.map((e) => String(e.id))))),
|
|
1192
1340
|
};
|
|
1193
1341
|
}
|
|
1194
1342
|
/**
|
|
@@ -1586,6 +1734,49 @@ export class AuthorizationManager {
|
|
|
1586
1734
|
throw new UnsupportedOperationError('reconcile', `authz:reconcile --to=${name}`, name, `El driver '${name}' no sabe reconstruirse desde 'authz_*' + el árbol del consumidor. ` +
|
|
1587
1735
|
`El driver 'database' es ese caso a propósito: sus tablas son el ORIGEN.`);
|
|
1588
1736
|
}
|
|
1737
|
+
// **La dirección RANCIA** (2.4.0-alpha.10; 🔴 4 del sexto informe de
|
|
1738
|
+
// COGNITIV y cuarta iteración del LINAJE «origen rancio del reconcile»:
|
|
1739
|
+
// 3b-5 cerró la pasada por defecto, el primer guard de alpha.10 cerró
|
|
1740
|
+
// `--from`, y quedaba abierto el origen elegido POR DESCARTE). Regla del
|
|
1741
|
+
// dueño: el tercer defecto de un linaje se arregla estructuralmente, así
|
|
1742
|
+
// que el guard NO mira la bandera, mira el ORIGEN RESUELTO — con la MISMA
|
|
1743
|
+
// decisión que `#factsOrigin` (`#isMaintenancePass`), no con una copia:
|
|
1744
|
+
// si `to` es el driver ACTIVO, el único origen legítimo es `to` mismo (la
|
|
1745
|
+
// pasada de MANTENIMIENTO); cualquier otro —`--from`, el driver elegido por
|
|
1746
|
+
// descarte o las tablas `authz_*`— dejó de ver los hechos en el cutover y la
|
|
1747
|
+
// pasada re-escribiría lo revocado después (con o sin --prune; el seguro de
|
|
1748
|
+
// borrado masivo no salta: el origen no está vacío). Se decide por `to`,
|
|
1749
|
+
// nunca por `from`: el cutover inicial y la vuelta atrás ANTES de cambiar
|
|
1750
|
+
// `default` tienen `to` ≠ activo y pasan. Antes de resolver el origen y de
|
|
1751
|
+
// congelar. `--dry-run` no lanza (es el verificador): lo marca y
|
|
1752
|
+
// `reconcileLines` lo hace deriva. `from === to` sigue siendo el 500
|
|
1753
|
+
// E_AUTHZ_CONFIG de 3b-3b.
|
|
1754
|
+
const stale = this.#sameDriver(name, this.#config.default) &&
|
|
1755
|
+
(options.from === undefined || !this.#sameDriver(options.from, name)) &&
|
|
1756
|
+
!this.#isMaintenancePass(name, target, options.from);
|
|
1757
|
+
const allowStale = options.allowStaleSource === true;
|
|
1758
|
+
if (stale && !allowStale && options.dryRun !== true) {
|
|
1759
|
+
const how = options.from !== undefined ? `--from=${options.from}` : 'sin --from';
|
|
1760
|
+
// Cierre del auditor (⚪ 8): con UN solo driver registrado no hay descarte que
|
|
1761
|
+
// hacer; la letra no puede hablar de un origen que no existe.
|
|
1762
|
+
const others = Object.keys(this.#config.drivers ?? {}).filter((key) => !this.#sameDriver(key, name));
|
|
1763
|
+
const originLabel = options.from !== undefined
|
|
1764
|
+
? `el origen '${options.from}' no lo es`
|
|
1765
|
+
: others.length === 0
|
|
1766
|
+
? `no hay ningún otro driver registrado ni pasada de mantenimiento para él (no guarda sus hechos en su ` +
|
|
1767
|
+
`backend: no declara hierarchyFacts), así que no hay nada de dónde reconciliar hacia '${name}'`
|
|
1768
|
+
: `sin --from el origen se elegiría POR DESCARTE (las tablas authz_* u otro driver registrado: ` +
|
|
1769
|
+
`${others.join(', ')}), y no es '${name}'`;
|
|
1770
|
+
throw new StaleSourceRefusedError(`authz:reconcile --to=${name} ${how}: '${name}' es el driver ACTIVO (config.default` +
|
|
1771
|
+
`${name === this.#config.default ? '' : `: la clave resuelve la MISMA factory que '${this.#config.default}'`}) y ${originLabel}. ` +
|
|
1772
|
+
`Esa dirección re-escribiría en el driver activo hechos que el origen ya no refleja: resucitaría las ` +
|
|
1773
|
+
`revocaciones hechas desde el cutover (y con --prune borraría lo concedido después). No se ha leído, ` +
|
|
1774
|
+
`congelado ni escrito nada. Hacia el driver activo el único origen es él mismo: la pasada de mantenimiento ` +
|
|
1775
|
+
`(authz:reconcile --to=${name} [--dry-run], sin --from) si su driver guarda sus hechos; y si lo que quieres ` +
|
|
1776
|
+
`es volver atrás, migra en la otra dirección ANTES de cambiar config.default. Si de verdad el origen sigue ` +
|
|
1777
|
+
`siendo la fuente de verdad (el motor aún no ha escrito desde '${name}'), la decisión humana es ` +
|
|
1778
|
+
`--allow-stale-source (allowStaleSource: true).`);
|
|
1779
|
+
}
|
|
1589
1780
|
// **De dónde salen los HECHOS** (3b-5): la decisión que faltaba, y la que
|
|
1590
1781
|
// el destino no puede tomar por su cuenta. Ver `#factsOrigin`.
|
|
1591
1782
|
const origin = await this.#factsOrigin(name, target, options.from);
|
|
@@ -1596,7 +1787,7 @@ export class AuthorizationManager {
|
|
|
1596
1787
|
// `authz_*` no construye ningún driver de más. Y el origen se resuelve
|
|
1597
1788
|
// UNA vez.
|
|
1598
1789
|
facts: origin.enumerate,
|
|
1599
|
-
factsOrigin: { name: origin.name, authzTables: origin.authzTables },
|
|
1790
|
+
factsOrigin: { name: origin.name, authzTables: origin.authzTables, ...(origin.self ? { self: true } : {}) },
|
|
1600
1791
|
// El techo de profundidad del consumidor, si lo declaró (alpha.5 · R0):
|
|
1601
1792
|
// el mismo mando que el gate de `scopes.*`.
|
|
1602
1793
|
...(this.#config.scopes?.maxDepth === undefined ? {} : { maxDepth: this.#config.scopes.maxDepth }),
|
|
@@ -1618,6 +1809,8 @@ export class AuthorizationManager {
|
|
|
1618
1809
|
// Quién fue el origen se DICE, siempre: es la diferencia entre una
|
|
1619
1810
|
// migración y una pasada de mantenimiento contra el driver activo.
|
|
1620
1811
|
report.factsFrom = origin.resolved();
|
|
1812
|
+
if (stale)
|
|
1813
|
+
report.staleSource = { to: name, from: origin.resolved(), allowed: allowStale };
|
|
1621
1814
|
return report;
|
|
1622
1815
|
};
|
|
1623
1816
|
// La pasada que escribe congela; el verificador NO (ver el docblock).
|
|
@@ -1641,6 +1834,8 @@ export class AuthorizationManager {
|
|
|
1641
1834
|
lapsed: await window.lapsed(),
|
|
1642
1835
|
leaseMs: window.leaseMs,
|
|
1643
1836
|
fence: window.fence,
|
|
1837
|
+
...(window.untilMs === undefined ? {} : { untilMs: window.untilMs }),
|
|
1838
|
+
...(window.window === undefined ? {} : { window: window.window }),
|
|
1644
1839
|
};
|
|
1645
1840
|
return report;
|
|
1646
1841
|
}
|
|
@@ -1668,7 +1863,9 @@ export class AuthorizationManager {
|
|
|
1668
1863
|
* MANTENIMIENTO: los hechos se leen del propio destino por el puerto
|
|
1669
1864
|
* (`enumerateFacts`), se rehace lo DERIVADO (marcador, catálogo, árbol)
|
|
1670
1865
|
* y se aplica el barrido de visibilidad del invariante 18 con el árbol
|
|
1671
|
-
* y el catálogo de HOY. No se inventa
|
|
1866
|
+
* y el catálogo de HOY. No se inventa ningún hecho; con `--prune` se
|
|
1867
|
+
* borran los que ya no respaldan nada (scopes que ya no resuelven,
|
|
1868
|
+
* caducados, roles/permisos retirados, y los que sobran). Un
|
|
1672
1869
|
* destino activo con `hierarchyFacts` que no sepa enumerar sus hechos
|
|
1673
1870
|
* es 500 `E_AUTHZ_UNSUPPORTED`: leerle `authz_*` sería justo el defecto.
|
|
1674
1871
|
* 2. **`--from=<nombre>` manda**, y por eso se resuelve YA: de la
|
|
@@ -1682,7 +1879,7 @@ export class AuthorizationManager {
|
|
|
1682
1879
|
* de 3b-3b (`#factsEnumerator`).
|
|
1683
1880
|
*/
|
|
1684
1881
|
async #factsOrigin(to, target, from) {
|
|
1685
|
-
if (
|
|
1882
|
+
if (this.#isMaintenancePass(to, target, from)) {
|
|
1686
1883
|
if (typeof target.enumerateFacts !== 'function') {
|
|
1687
1884
|
throw new UnsupportedOperationError('enumerateFacts', `authz:reconcile --to=${to}`, to, `El motor SIRVE desde '${to}' y ese driver declara que el árbol y los hechos viven en su backend ` +
|
|
1688
1885
|
`(hierarchyFacts), así que 'authz_assignments'/'authz_denies' NO son la fuente de verdad de sus ` +
|
|
@@ -1692,6 +1889,7 @@ export class AuthorizationManager {
|
|
|
1692
1889
|
return {
|
|
1693
1890
|
name: to,
|
|
1694
1891
|
authzTables: false,
|
|
1892
|
+
self: true,
|
|
1695
1893
|
enumerate: (page) => target.enumerateFacts(page),
|
|
1696
1894
|
resolved: () => to,
|
|
1697
1895
|
};
|
|
@@ -1717,10 +1915,44 @@ export class AuthorizationManager {
|
|
|
1717
1915
|
});
|
|
1718
1916
|
return { name: AUTHZ_TABLES_ORIGIN, authzTables: true, enumerate, resolved: () => resolvedName };
|
|
1719
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* **¿Es la pasada de MANTENIMIENTO?** (3b-5; punto ÚNICO desde alpha.10): el
|
|
1920
|
+
* destino es el driver ACTIVO, sus hechos viven en su backend
|
|
1921
|
+
* (`hierarchyFacts`) y nadie nombró otro origen. Es la ÚNICA forma en la que
|
|
1922
|
+
* el origen resuelto de una pasada hacia el driver activo es el propio
|
|
1923
|
+
* destino, y la usan los dos que la necesitan: `#factsOrigin` (de dónde se
|
|
1924
|
+
* leen los hechos) y el guard de la dirección rancia de `reconcile` (si ese
|
|
1925
|
+
* origen es legítimo). Dos copias de esta condición eran dos respuestas.
|
|
1926
|
+
*/
|
|
1927
|
+
#isMaintenancePass(to, target, from) {
|
|
1928
|
+
return from === undefined && this.#sameDriver(to, this.#config.default) && target.capabilities?.hierarchyFacts === true;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* **¿Dos claves de `drivers` son el MISMO driver?** (cierre del auditor de
|
|
1932
|
+
* alpha.10 · R4, gemelo de roles): misma clave o la MISMA factory registrada
|
|
1933
|
+
* bajo dos claves. El guard de la dirección rancia, el mantenimiento, el
|
|
1934
|
+
* `from === to` y el descarte juzgan la identidad RESUELTA, no la etiqueta:
|
|
1935
|
+
* con `drivers: { openfga: f, fga: f }` y `default: 'openfga'`, `--to=fga
|
|
1936
|
+
* --from=database` es la misma dirección rancia que `--to=openfga`.
|
|
1937
|
+
*
|
|
1938
|
+
* **Límite DECLARADO (cierre-2 · C1)**: la identidad es la REFERENCIA de la
|
|
1939
|
+
* factory, no el backend. Dos factories distintas que construyen el mismo
|
|
1940
|
+
* store (`openfga: () => new OpenFga…(…)`, `fga: () => new OpenFga…(…)`) NO son
|
|
1941
|
+
* el mismo driver para este predicado y el alias pasa el guard. Identidad de
|
|
1942
|
+
* backend declarada por el driver ⇒ roadmap 2.5.
|
|
1943
|
+
*/
|
|
1944
|
+
#sameDriver(a, b) {
|
|
1945
|
+
if (a === b)
|
|
1946
|
+
return true;
|
|
1947
|
+
const drivers = this.#config.drivers ?? {};
|
|
1948
|
+
const own = (key) => (Object.prototype.hasOwnProperty.call(drivers, key) ? drivers[key] : undefined);
|
|
1949
|
+
const fa = own(a);
|
|
1950
|
+
return fa !== undefined && fa === own(b);
|
|
1951
|
+
}
|
|
1720
1952
|
/** El driver que `--from` nombra, con los dos errores de 3b-3b intactos. */
|
|
1721
1953
|
async #originDriver(from, to) {
|
|
1722
1954
|
const registered = Object.keys(this.#config.drivers ?? {});
|
|
1723
|
-
if (from
|
|
1955
|
+
if (this.#sameDriver(from, to)) {
|
|
1724
1956
|
throw new AuthorizationConfigError(`authz:reconcile --from=${from} --to=${to}: el origen y el destino son el mismo driver. ` +
|
|
1725
1957
|
`Si lo que quieres es VERIFICAR y reparar lo derivado del driver activo, no lo digas con --from: ` +
|
|
1726
1958
|
`la pasada ya lee sus hechos de él cuando es el driver por defecto.`);
|
|
@@ -1756,7 +1988,7 @@ export class AuthorizationManager {
|
|
|
1756
1988
|
const registered = Object.keys(this.#config.drivers ?? {});
|
|
1757
1989
|
const candidates = [];
|
|
1758
1990
|
for (const candidate of registered) {
|
|
1759
|
-
if (candidate
|
|
1991
|
+
if (this.#sameDriver(candidate, to))
|
|
1760
1992
|
continue;
|
|
1761
1993
|
const driver = await this.#config.drivers[candidate]();
|
|
1762
1994
|
if (typeof driver.enumerateFacts === 'function')
|
|
@@ -2188,7 +2420,7 @@ export class AuthorizationManager {
|
|
|
2188
2420
|
const answer = await driver.authorizeMany(subject, permission, scopes);
|
|
2189
2421
|
// Un `boolean[]` desalineado se leería por posición (F5, CR3): es un
|
|
2190
2422
|
// bug del driver, no una decisión. 500 nombrando al culpable.
|
|
2191
|
-
if (!Array.isArray(answer) || answer.length !== scopes.length || answer
|
|
2423
|
+
if (!Array.isArray(answer) || answer.length !== scopes.length || !everyPositionBoolean(answer)) {
|
|
2192
2424
|
throw new AuthorizationInternalError(`authorizeMany: el driver '${this.#config.default}' devolvió ${describeAnswer(answer)} para ${scopes.length} scopes; ` +
|
|
2193
2425
|
`el puerto exige un boolean[] con exactamente una posición por scope.`);
|
|
2194
2426
|
}
|