@jantstack/adonis-authz 1.1.0 → 2.4.0-alpha.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/LICENSE +1 -1
- package/README.md +837 -51
- package/build/commands/authz_catalog_diff.d.ts +28 -0
- package/build/commands/authz_catalog_diff.d.ts.map +1 -0
- package/build/commands/authz_catalog_diff.js +67 -0
- package/build/commands/authz_catalog_diff.js.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.js +136 -0
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
- package/build/commands/authz_catalog_sync.d.ts +37 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -0
- package/build/commands/authz_catalog_sync.js +81 -0
- package/build/commands/authz_catalog_sync.js.map +1 -0
- package/build/commands/authz_freeze.d.ts +44 -0
- package/build/commands/authz_freeze.d.ts.map +1 -0
- package/build/commands/authz_freeze.js +95 -0
- package/build/commands/authz_freeze.js.map +1 -0
- package/build/commands/authz_reconcile.d.ts +102 -0
- package/build/commands/authz_reconcile.d.ts.map +1 -0
- package/build/commands/authz_reconcile.js +294 -0
- package/build/commands/authz_reconcile.js.map +1 -0
- package/build/commands/authz_relations_reconcile.d.ts +73 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
- package/build/commands/authz_relations_reconcile.js +225 -0
- package/build/commands/authz_relations_reconcile.js.map +1 -0
- package/build/commands/authz_scopes_relay.d.ts +47 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -0
- package/build/commands/authz_scopes_relay.js +141 -0
- package/build/commands/authz_scopes_relay.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +37 -0
- package/build/commands/authz_unfreeze.d.ts.map +1 -0
- package/build/commands/authz_unfreeze.js +92 -0
- package/build/commands/authz_unfreeze.js.map +1 -0
- package/build/commands/main.d.ts +8 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +8 -1
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +46 -4
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +91 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/configure.d.ts +11 -0
- package/build/configure.d.ts.map +1 -1
- package/build/configure.js +37 -1
- package/build/configure.js.map +1 -1
- package/build/index.d.ts +75 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +67 -5
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +26 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +48 -2
- package/build/providers/authz_provider.js.map +1 -1
- package/build/services/relations.d.ts +14 -0
- package/build/services/relations.d.ts.map +1 -0
- package/build/services/relations.js +17 -0
- package/build/services/relations.js.map +1 -0
- package/build/src/catalog/catalog.d.ts +289 -0
- package/build/src/catalog/catalog.d.ts.map +1 -0
- package/build/src/catalog/catalog.js +859 -0
- package/build/src/catalog/catalog.js.map +1 -0
- package/build/src/catalog/catalog_cache.d.ts +324 -0
- package/build/src/catalog/catalog_cache.d.ts.map +1 -0
- package/build/src/catalog/catalog_cache.js +666 -0
- package/build/src/catalog/catalog_cache.js.map +1 -0
- package/build/src/clock.d.ts +24 -0
- package/build/src/clock.d.ts.map +1 -0
- package/build/src/clock.js +7 -0
- package/build/src/clock.js.map +1 -0
- package/build/src/define_config.d.ts +201 -7
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +324 -15
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +1107 -128
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +75 -0
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/database_relations_driver.js +450 -0
- package/build/src/drivers/database_relations_driver.js.map +1 -0
- package/build/src/drivers/openfga_driver.d.ts +963 -55
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2693 -372
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +369 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -0
- package/build/src/drivers/openfga_facts.js +813 -0
- package/build/src/drivers/openfga_facts.js.map +1 -0
- package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/openfga_relations_driver.js +466 -0
- package/build/src/drivers/openfga_relations_driver.js.map +1 -0
- package/build/src/errors.d.ts +586 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +583 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/expiry.d.ts +27 -0
- package/build/src/expiry.d.ts.map +1 -0
- package/build/src/expiry.js +50 -0
- package/build/src/expiry.js.map +1 -0
- package/build/src/freeze.d.ts +120 -0
- package/build/src/freeze.d.ts.map +1 -0
- package/build/src/freeze.js +172 -0
- package/build/src/freeze.js.map +1 -0
- package/build/src/hierarchical_resolver.d.ts +56 -0
- package/build/src/hierarchical_resolver.d.ts.map +1 -0
- package/build/src/hierarchical_resolver.js +87 -0
- package/build/src/hierarchical_resolver.js.map +1 -0
- package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
- package/build/src/http/app_access_middleware.d.ts.map +1 -0
- package/build/src/{middleware → http}/app_access_middleware.js +10 -26
- package/build/src/http/app_access_middleware.js.map +1 -0
- package/build/src/http/resource_access_middleware.d.ts +105 -0
- package/build/src/http/resource_access_middleware.d.ts.map +1 -0
- package/build/src/http/resource_access_middleware.js +81 -0
- package/build/src/http/resource_access_middleware.js.map +1 -0
- package/build/src/identity.d.ts +228 -0
- package/build/src/identity.d.ts.map +1 -0
- package/build/src/identity.js +457 -0
- package/build/src/identity.js.map +1 -0
- package/build/src/manager.d.ts +536 -7
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +2676 -23
- package/build/src/manager.js.map +1 -1
- package/build/src/memoize_ancestors.d.ts +23 -0
- package/build/src/memoize_ancestors.d.ts.map +1 -0
- package/build/src/memoize_ancestors.js +42 -0
- package/build/src/memoize_ancestors.js.map +1 -0
- package/build/src/models/authz_assignment.d.ts +11 -11
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +11 -11
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +17 -11
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_permission.js +4 -0
- package/build/src/models/authz_permission.js.map +1 -1
- package/build/src/models/authz_role.d.ts +19 -12
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role.js +6 -1
- package/build/src/models/authz_role.js.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +11 -11
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +29 -0
- package/build/src/openfga.d.ts.map +1 -0
- package/build/src/openfga.js +26 -0
- package/build/src/openfga.js.map +1 -0
- package/build/src/reconcile.d.ts +37 -0
- package/build/src/reconcile.d.ts.map +1 -0
- package/build/src/reconcile.js +69 -0
- package/build/src/reconcile.js.map +1 -0
- package/build/src/relation_partition_trigger.d.ts +8 -0
- package/build/src/relation_partition_trigger.d.ts.map +1 -0
- package/build/src/relation_partition_trigger.js +85 -0
- package/build/src/relation_partition_trigger.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +58 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -0
- package/build/src/relations/define_relations_config.js +144 -0
- package/build/src/relations/define_relations_config.js.map +1 -0
- package/build/src/relations/manager.d.ts +38 -0
- package/build/src/relations/manager.d.ts.map +1 -0
- package/build/src/relations/manager.js +156 -0
- package/build/src/relations/manager.js.map +1 -0
- package/build/src/relations/reconcile.d.ts +62 -0
- package/build/src/relations/reconcile.d.ts.map +1 -0
- package/build/src/relations/reconcile.js +138 -0
- package/build/src/relations/reconcile.js.map +1 -0
- package/build/src/relations_config_store.d.ts +22 -0
- package/build/src/relations_config_store.d.ts.map +1 -0
- package/build/src/relations_config_store.js +74 -0
- package/build/src/relations_config_store.js.map +1 -0
- package/build/src/scope_outbox.d.ts +69 -0
- package/build/src/scope_outbox.d.ts.map +1 -0
- package/build/src/scope_outbox.js +291 -0
- package/build/src/scope_outbox.js.map +1 -0
- package/build/src/shared/backend_guard.d.ts +106 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/shared/backend_guard.js +246 -0
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/shared/sql_expiry.d.ts +53 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/shared/sql_expiry.js +66 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/sql_descendants.d.ts +97 -0
- package/build/src/sql_descendants.d.ts.map +1 -0
- package/build/src/sql_descendants.js +203 -0
- package/build/src/sql_descendants.js.map +1 -0
- package/build/src/testing/contract.d.ts +212 -7
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +3449 -24
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +10 -2
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +5 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +284 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -0
- package/build/src/testing/migration_contract.js +586 -0
- package/build/src/testing/migration_contract.js.map +1 -0
- package/build/src/testing/relations_contract.d.ts +51 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -0
- package/build/src/testing/relations_contract.js +654 -0
- package/build/src/testing/relations_contract.js.map +1 -0
- package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
- package/build/src/testing/relations_reconcile_contract.js +172 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -0
- package/build/src/testing/scope_tree.d.ts +65 -0
- package/build/src/testing/scope_tree.d.ts.map +1 -0
- package/build/src/testing/scope_tree.js +145 -0
- package/build/src/testing/scope_tree.js.map +1 -0
- package/build/src/traits/authz_scopes.d.ts +30 -6
- package/build/src/traits/authz_scopes.d.ts.map +1 -1
- package/build/src/traits/authz_scopes.js +30 -18
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +12 -12
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +1313 -28
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +20 -4
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/app_acl.stub +4 -2
- package/build/stubs/config/authorization.stub +168 -14
- package/build/stubs/migration.stub +183 -13
- package/build/stubs/scopes_outbox_migration.stub +57 -0
- package/package.json +14 -7
- package/build/commands/openfga_import.d.ts +0 -28
- package/build/commands/openfga_import.d.ts.map +0 -1
- package/build/commands/openfga_import.js +0 -74
- package/build/commands/openfga_import.js.map +0 -1
- package/build/src/catalog.d.ts +0 -3
- package/build/src/catalog.d.ts.map +0 -1
- package/build/src/catalog.js +0 -103
- package/build/src/catalog.js.map +0 -1
- package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
- package/build/src/middleware/app_access_middleware.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { systemClock } from './clock.js';
|
|
2
|
+
/**
|
|
3
|
+
* Semántica de `expiresAt` en un re-grant (L0.4), compartida por los drivers
|
|
4
|
+
* para que ambos respondan igual:
|
|
5
|
+
*
|
|
6
|
+
* omitido (`undefined`) → no tocar una caducidad VIGENTE; si la asignación
|
|
7
|
+
* ya expiró, revive sin caducidad (es un grant nuevo)
|
|
8
|
+
* `null` → quitar la caducidad, explícitamente
|
|
9
|
+
* `Date` → fijarla (también a un instante pasado: expira)
|
|
10
|
+
*
|
|
11
|
+
* El defecto que cierra: "asegúrate de que tiene el rol" (grant sin opciones,
|
|
12
|
+
* idempotente por naturaleza) convertía un acceso temporal en permanente.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveGrantExpiry(previous, requested, now = systemClock()) {
|
|
15
|
+
if (requested !== undefined)
|
|
16
|
+
return requested;
|
|
17
|
+
return isActiveExpiry(previous, now) ? previous : null;
|
|
18
|
+
}
|
|
19
|
+
/** Vigente: sin caducidad o con caducidad futura. */
|
|
20
|
+
export function isActiveExpiry(expiresAt, now = systemClock()) {
|
|
21
|
+
return expiresAt === null || expiresAt.getTime() > now.getTime();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* ¿Mismo instante? Compara el tiempo, no la referencia ni la cadena:
|
|
25
|
+
* `2026-01-01T00:00:00Z` y `…00.000Z` son el mismo momento y no justifican
|
|
26
|
+
* una reescritura (que en openfga es delete+write con ventana de denegación).
|
|
27
|
+
*/
|
|
28
|
+
export function sameInstant(a, b) {
|
|
29
|
+
if (!a && !b)
|
|
30
|
+
return true;
|
|
31
|
+
if (!a || !b)
|
|
32
|
+
return false;
|
|
33
|
+
return a.getTime() === b.getTime();
|
|
34
|
+
}
|
|
35
|
+
/** Lo que devuelve el backend como caducidad (número, ISO, Date) → Date | null. */
|
|
36
|
+
export function toExpiryDate(value) {
|
|
37
|
+
if (value === null || value === undefined || value === '')
|
|
38
|
+
return null;
|
|
39
|
+
if (value instanceof Date)
|
|
40
|
+
return value;
|
|
41
|
+
const date = new Date(value);
|
|
42
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
43
|
+
}
|
|
44
|
+
/** ¿El outcome de un grant cambió la caducidad de una asignación que ya existía? */
|
|
45
|
+
export function expiryChanged(outcome) {
|
|
46
|
+
return (outcome.existed &&
|
|
47
|
+
outcome.previousExpiresAt !== undefined &&
|
|
48
|
+
!sameInstant(outcome.previousExpiresAt, outcome.expiresAt));
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=expiry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiry.js","sourceRoot":"","sources":["../../src/expiry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAqB,EACrB,SAAkC,EAClC,MAAY,WAAW,EAAE;IAEzB,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AACxD,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,cAAc,CAAC,SAAsB,EAAE,MAAY,WAAW,EAAE;IAC9E,OAAO,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAA;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAA0B,EAAE,CAA0B;IAChF,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IACzB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1B,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAA;AACpC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACtE,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAA;IACvC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAY,CAAC,CAAA;IACnC,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAAC,OAAqB;IACjD,OAAO,CACL,OAAO,CAAC,OAAO;QACf,OAAO,CAAC,iBAAiB,KAAK,SAAS;QACvC,CAAC,WAAW,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,SAAS,CAAC,CAC3D,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
|
|
3
|
+
* B + E-analista). Primitivas SQL puras; la policy (quién congela, el
|
|
4
|
+
* anidado, el contexto del operador) vive en el manager.
|
|
5
|
+
*
|
|
6
|
+
* El freeze cuelga de la fila `id = 2` de `authz_catalog_version` — la señal
|
|
7
|
+
* entre procesos que el paquete ya usa (invariante 14). La colocación está
|
|
8
|
+
* MEDIDA (panel 3, analista §4.1): en la fila `id = 1` la renovación del
|
|
9
|
+
* lease espera al `FOR UPDATE` de `withAuthzCatalogWrite` (816 ms en PG /
|
|
10
|
+
* 805 en MySQL con un sync de 800 ms); en su propia fila, 10-12 ms. Y la
|
|
11
|
+
* consulta de la barrera es PROPIA y sin memo (juez C2): colgarse del memo
|
|
12
|
+
* del catálogo haría del freeze «una ventana acotada de freeze que todavía
|
|
13
|
+
* no aplica» con `catalogRevalidate: { everyMs }`. El coste que se publica
|
|
14
|
+
* es el medido: +0,14 ms p50 por escritura (PG; +0,11 MySQL), 0 por
|
|
15
|
+
* `authorize`.
|
|
16
|
+
*
|
|
17
|
+
* Las tres reglas del protocolo:
|
|
18
|
+
* - **Token de dueño** (`fence` + `holder`): `acquire` sube el fence y solo
|
|
19
|
+
* hay UN freeze vivo a la vez; `release`/`renew` son condicionales por
|
|
20
|
+
* token, así que un `unfreeze` ajeno o rezagado es un no-op (auditor
|
|
21
|
+
* A1.3: el `finally` de una ventana interior ya no puede levantar la
|
|
22
|
+
* barrera de otra).
|
|
23
|
+
* - **Lease**: `untilMs` en ms de PARED (el reloj del config, el mismo que
|
|
24
|
+
* decide caducidades). Vencido ⇒ el freeze deja de estar vivo y la flota
|
|
25
|
+
* vuelve a escribir SOLA — tras un `SIGKILL` nadie limpia nada (medido:
|
|
26
|
+
* 2 527 ms con lease de 3 000). `untilMs: null` = sin caducidad (la
|
|
27
|
+
* ventana del operador).
|
|
28
|
+
* - **Fence publicado**: la renovación exige `freeze_until_ms > now`; una
|
|
29
|
+
* renovación que toca 0 filas significa que el lease se PERDIÓ a mitad
|
|
30
|
+
* (pausa de GC, base caída más tiempo que el lease) y quien lo sostiene
|
|
31
|
+
* lo marca `lapsed` — la pasada no se certifica (juez C4).
|
|
32
|
+
*
|
|
33
|
+
* La fila ausente (o las columnas: una base migrada con el stub < 2.3) es
|
|
34
|
+
* **503 «migración 2.0 no aplicada»** en toda escritura, jamás «no
|
|
35
|
+
* congelado»: el mismo patrón que la fila `id = 1` del memo. Un upsert
|
|
36
|
+
* perezoso desde la barrera sería una escritura en el camino de lectura,
|
|
37
|
+
* fuera de `withAuthzCatalogWrite`, y una carrera gratis.
|
|
38
|
+
*/
|
|
39
|
+
/** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
|
|
40
|
+
export declare const FREEZE_ROW_ID = 2;
|
|
41
|
+
/** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
|
|
42
|
+
export declare const DEFAULT_FREEZE_LEASE_MS = 15000;
|
|
43
|
+
/** Quién congela: el dueño va escrito en la fila (`<kind>:<pid>:<hex>`). */
|
|
44
|
+
export type FreezeKind = 'operator' | 'reconcile' | 'platform';
|
|
45
|
+
/** El token del dueño: lo devuelve `freeze()` y es lo ÚNICO que levanta o renueva ese freeze. */
|
|
46
|
+
export interface FreezeToken {
|
|
47
|
+
/** Número de generación: sube en cada `acquire`, nunca se reutiliza. */
|
|
48
|
+
fence: number;
|
|
49
|
+
/** `<kind>:<pid>:<hex>`: identifica al proceso que congeló. */
|
|
50
|
+
holder: string;
|
|
51
|
+
}
|
|
52
|
+
/** La fila del freeze, tal como está en la base. */
|
|
53
|
+
export interface FreezeRow {
|
|
54
|
+
reason: string | null;
|
|
55
|
+
holder: string | null;
|
|
56
|
+
untilMs: number | null;
|
|
57
|
+
fence: number;
|
|
58
|
+
}
|
|
59
|
+
export interface FreezeSqlOptions {
|
|
60
|
+
/** Etiqueta del driver en los errores 503 (default `freeze`). */
|
|
61
|
+
driver?: string;
|
|
62
|
+
/** Deadline de cada consulta (default 5000): vencido ⇒ 503. */
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Cliente sobre el que leer (la TRANSACCIÓN del llamante, si la escritura
|
|
66
|
+
* llegó con una — `ScopeTreeWriteOptions.transaction`). Sin esto, la
|
|
67
|
+
* barrera necesitaría una SEGUNDA conexión del pool mientras el consumidor
|
|
68
|
+
* sostiene la suya, y con un pool de 1 (SQLite `:memory:`) eso es un
|
|
69
|
+
* interbloqueo, no una lectura. El precio, declarado: en un motor cuyo
|
|
70
|
+
* aislamiento por defecto congela la foto de las lecturas (InnoDB
|
|
71
|
+
* REPEATABLE READ), un freeze puesto DESPUÉS de abrir esa transacción
|
|
72
|
+
* puede no verse desde dentro — la misma ventana que una escritura que ya
|
|
73
|
+
* pasó su barrera (tester A4), y por eso la promesa publicada es «otro
|
|
74
|
+
* proceso recibe 503», no «ninguna escritura entra».
|
|
75
|
+
*/
|
|
76
|
+
client?: {
|
|
77
|
+
from: (table: string) => any;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Lee la fila del freeze (503 clasificado si la base no responde; 503 «migración no aplicada» si no existe). */
|
|
81
|
+
export declare function readFreezeRow(options?: FreezeSqlOptions): Promise<FreezeRow>;
|
|
82
|
+
/** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
|
|
83
|
+
export declare function freezeIsLive(row: FreezeRow, nowMs: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
|
|
86
|
+
* `acquire` a la vez acaban en exactamente uno). Devuelve el token, o `null`
|
|
87
|
+
* si hay un freeze vivo de otro dueño (el llamante decide si eso es 423 o un
|
|
88
|
+
* contexto que reconoce).
|
|
89
|
+
*/
|
|
90
|
+
export declare function acquireFreeze(input: {
|
|
91
|
+
reason: string;
|
|
92
|
+
holder: string;
|
|
93
|
+
untilMs: number | null;
|
|
94
|
+
nowMs: number;
|
|
95
|
+
}, options?: FreezeSqlOptions): Promise<FreezeToken | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Renueva el lease, CONDICIONAL por token y solo si aún no venció
|
|
98
|
+
* (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
|
|
99
|
+
* otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
|
|
100
|
+
* renovar — nunca «recupera» un freeze que ya no es suyo.
|
|
101
|
+
*/
|
|
102
|
+
export declare function renewFreeze(token: FreezeToken, input: {
|
|
103
|
+
untilMs: number;
|
|
104
|
+
nowMs: number;
|
|
105
|
+
}, options?: FreezeSqlOptions): Promise<boolean>;
|
|
106
|
+
/**
|
|
107
|
+
* Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
|
|
108
|
+
* nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
|
|
109
|
+
* estaba vencido o el token ya no era el de la fila, hubo una ventana en la
|
|
110
|
+
* que otros procesos pudieron escribir — es lo que el reporte publica.
|
|
111
|
+
*/
|
|
112
|
+
export declare function releaseFreeze(token: FreezeToken, input: {
|
|
113
|
+
nowMs: number;
|
|
114
|
+
}, options?: FreezeSqlOptions): Promise<{
|
|
115
|
+
released: boolean;
|
|
116
|
+
lapsed: boolean;
|
|
117
|
+
}>;
|
|
118
|
+
/** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`). */
|
|
119
|
+
export declare function freezeKindOf(holder: string | null): FreezeKind | 'unknown';
|
|
120
|
+
//# sourceMappingURL=freeze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,wIAAwI;AACxI,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAI7C,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAA;AAE9D,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAA;CACf;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;KAAE,CAAA;CAC1C;AAyBD,iHAAiH;AACjH,wBAAsB,aAAa,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmBtF;AAED,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAChF,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA8B7B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACzC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAejD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,SAAS,CAG1E"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import db from '@adonisjs/lucid/services/db';
|
|
2
|
+
import { guardSql } from './shared/backend_guard.js';
|
|
3
|
+
import { AuthorizationBackendError } from './errors.js';
|
|
4
|
+
import { CATALOG_VERSION_TABLE } from './catalog/catalog_cache.js';
|
|
5
|
+
/**
|
|
6
|
+
* El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
|
|
7
|
+
* B + E-analista). Primitivas SQL puras; la policy (quién congela, el
|
|
8
|
+
* anidado, el contexto del operador) vive en el manager.
|
|
9
|
+
*
|
|
10
|
+
* El freeze cuelga de la fila `id = 2` de `authz_catalog_version` — la señal
|
|
11
|
+
* entre procesos que el paquete ya usa (invariante 14). La colocación está
|
|
12
|
+
* MEDIDA (panel 3, analista §4.1): en la fila `id = 1` la renovación del
|
|
13
|
+
* lease espera al `FOR UPDATE` de `withAuthzCatalogWrite` (816 ms en PG /
|
|
14
|
+
* 805 en MySQL con un sync de 800 ms); en su propia fila, 10-12 ms. Y la
|
|
15
|
+
* consulta de la barrera es PROPIA y sin memo (juez C2): colgarse del memo
|
|
16
|
+
* del catálogo haría del freeze «una ventana acotada de freeze que todavía
|
|
17
|
+
* no aplica» con `catalogRevalidate: { everyMs }`. El coste que se publica
|
|
18
|
+
* es el medido: +0,14 ms p50 por escritura (PG; +0,11 MySQL), 0 por
|
|
19
|
+
* `authorize`.
|
|
20
|
+
*
|
|
21
|
+
* Las tres reglas del protocolo:
|
|
22
|
+
* - **Token de dueño** (`fence` + `holder`): `acquire` sube el fence y solo
|
|
23
|
+
* hay UN freeze vivo a la vez; `release`/`renew` son condicionales por
|
|
24
|
+
* token, así que un `unfreeze` ajeno o rezagado es un no-op (auditor
|
|
25
|
+
* A1.3: el `finally` de una ventana interior ya no puede levantar la
|
|
26
|
+
* barrera de otra).
|
|
27
|
+
* - **Lease**: `untilMs` en ms de PARED (el reloj del config, el mismo que
|
|
28
|
+
* decide caducidades). Vencido ⇒ el freeze deja de estar vivo y la flota
|
|
29
|
+
* vuelve a escribir SOLA — tras un `SIGKILL` nadie limpia nada (medido:
|
|
30
|
+
* 2 527 ms con lease de 3 000). `untilMs: null` = sin caducidad (la
|
|
31
|
+
* ventana del operador).
|
|
32
|
+
* - **Fence publicado**: la renovación exige `freeze_until_ms > now`; una
|
|
33
|
+
* renovación que toca 0 filas significa que el lease se PERDIÓ a mitad
|
|
34
|
+
* (pausa de GC, base caída más tiempo que el lease) y quien lo sostiene
|
|
35
|
+
* lo marca `lapsed` — la pasada no se certifica (juez C4).
|
|
36
|
+
*
|
|
37
|
+
* La fila ausente (o las columnas: una base migrada con el stub < 2.3) es
|
|
38
|
+
* **503 «migración 2.0 no aplicada»** en toda escritura, jamás «no
|
|
39
|
+
* congelado»: el mismo patrón que la fila `id = 1` del memo. Un upsert
|
|
40
|
+
* perezoso desde la barrera sería una escritura en el camino de lectura,
|
|
41
|
+
* fuera de `withAuthzCatalogWrite`, y una carrera gratis.
|
|
42
|
+
*/
|
|
43
|
+
/** La fila del freeze durable (la `id = 1` es la versión del catálogo). */
|
|
44
|
+
export const FREEZE_ROW_ID = 2;
|
|
45
|
+
/** Lease por defecto (analista §2.3): 15 s renovados cada 5 s cubren cualquier pausa razonable y acotan la parada tras un `SIGKILL`. */
|
|
46
|
+
export const DEFAULT_FREEZE_LEASE_MS = 15_000;
|
|
47
|
+
const DEFAULT_FREEZE_TIMEOUT_MS = 5_000;
|
|
48
|
+
function opts(options) {
|
|
49
|
+
return { driver: options.driver ?? 'freeze', timeoutMs: options.timeoutMs ?? DEFAULT_FREEZE_TIMEOUT_MS };
|
|
50
|
+
}
|
|
51
|
+
function missingRow(driver) {
|
|
52
|
+
const error = new AuthorizationBackendError(driver, 'freeze.row', new Error(`no hay fila id = ${FREEZE_ROW_ID} en ${CATALOG_VERSION_TABLE}`));
|
|
53
|
+
error.message =
|
|
54
|
+
`${driver}: no se puede leer el freeze compartido (${CATALOG_VERSION_TABLE}, id = ${FREEZE_ROW_ID}). ` +
|
|
55
|
+
`Probablemente la migración 2.0 no está aplicada (la siembra el stub publicado; la receta está en el CHANGELOG). ` +
|
|
56
|
+
`Sin la fila las escrituras NO proceden: tratarla como «no congelado» sería fail-open.`;
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
function toMs(raw) {
|
|
60
|
+
if (raw === null || raw === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const value = typeof raw === 'number' ? raw : Number(raw);
|
|
63
|
+
return Number.isFinite(value) ? value : null;
|
|
64
|
+
}
|
|
65
|
+
/** Lee la fila del freeze (503 clasificado si la base no responde; 503 «migración no aplicada» si no existe). */
|
|
66
|
+
export async function readFreezeRow(options = {}) {
|
|
67
|
+
const { driver, timeoutMs } = opts(options);
|
|
68
|
+
const client = options.client ?? db;
|
|
69
|
+
const rows = await guardSql(driver, 'freeze.read', timeoutMs, () => client
|
|
70
|
+
.from(CATALOG_VERSION_TABLE)
|
|
71
|
+
.where('id', FREEZE_ROW_ID)
|
|
72
|
+
.select('freeze_reason', 'freeze_holder', 'freeze_until_ms', 'freeze_fence'));
|
|
73
|
+
if (rows.length === 0)
|
|
74
|
+
throw missingRow(driver);
|
|
75
|
+
const row = rows[0];
|
|
76
|
+
const fence = toMs(row.freeze_fence);
|
|
77
|
+
if (fence === null)
|
|
78
|
+
throw missingRow(driver);
|
|
79
|
+
return {
|
|
80
|
+
reason: row.freeze_reason === null || row.freeze_reason === undefined ? null : String(row.freeze_reason),
|
|
81
|
+
holder: row.freeze_holder === null || row.freeze_holder === undefined ? null : String(row.freeze_holder),
|
|
82
|
+
untilMs: toMs(row.freeze_until_ms),
|
|
83
|
+
fence,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** ¿Está VIVO este freeze en el instante `nowMs`? (motivo puesto y lease sin vencer; `untilMs: null` no vence). */
|
|
87
|
+
export function freezeIsLive(row, nowMs) {
|
|
88
|
+
if (row.reason === null)
|
|
89
|
+
return false;
|
|
90
|
+
return row.untilMs === null || row.untilMs > nowMs;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
|
|
94
|
+
* `acquire` a la vez acaban en exactamente uno). Devuelve el token, o `null`
|
|
95
|
+
* si hay un freeze vivo de otro dueño (el llamante decide si eso es 423 o un
|
|
96
|
+
* contexto que reconoce).
|
|
97
|
+
*/
|
|
98
|
+
export async function acquireFreeze(input, options = {}) {
|
|
99
|
+
const { driver, timeoutMs } = opts(options);
|
|
100
|
+
const updated = await guardSql(driver, 'freeze.acquire', timeoutMs, () => db
|
|
101
|
+
.from(CATALOG_VERSION_TABLE)
|
|
102
|
+
.where('id', FREEZE_ROW_ID)
|
|
103
|
+
.where((q) => {
|
|
104
|
+
// Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma).
|
|
105
|
+
q.whereNull('freeze_reason').orWhere((expired) => {
|
|
106
|
+
expired.whereNotNull('freeze_until_ms').andWhere('freeze_until_ms', '<=', input.nowMs);
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
.update({
|
|
110
|
+
freeze_reason: input.reason,
|
|
111
|
+
freeze_holder: input.holder,
|
|
112
|
+
freeze_until_ms: input.untilMs,
|
|
113
|
+
freeze_fence: db.raw('freeze_fence + 1'),
|
|
114
|
+
updated_at: new Date(input.nowMs),
|
|
115
|
+
}));
|
|
116
|
+
if (Number(updated) === 0) {
|
|
117
|
+
// O hay un freeze vivo, o la fila no existe: distinguirlo es leerla.
|
|
118
|
+
const row = await readFreezeRow(options);
|
|
119
|
+
if (freezeIsLive(row, input.nowMs))
|
|
120
|
+
return null;
|
|
121
|
+
// Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta una vez.
|
|
122
|
+
return acquireFreeze(input, options);
|
|
123
|
+
}
|
|
124
|
+
const row = await readFreezeRow(options);
|
|
125
|
+
if (row.holder !== input.holder)
|
|
126
|
+
return null; // otro acquire ganó justo después (no debería: el nuestro dejó la fila viva)
|
|
127
|
+
return { fence: row.fence, holder: input.holder };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Renueva el lease, CONDICIONAL por token y solo si aún no venció
|
|
131
|
+
* (`freeze_until_ms > now`). `false` = el lease se PERDIÓ (caducó a mitad, o
|
|
132
|
+
* otro dueño tomó el freeze): el llamante lo marca `lapsed` y deja de
|
|
133
|
+
* renovar — nunca «recupera» un freeze que ya no es suyo.
|
|
134
|
+
*/
|
|
135
|
+
export async function renewFreeze(token, input, options = {}) {
|
|
136
|
+
const { driver, timeoutMs } = opts(options);
|
|
137
|
+
const updated = await guardSql(driver, 'freeze.renew', timeoutMs, () => db
|
|
138
|
+
.from(CATALOG_VERSION_TABLE)
|
|
139
|
+
.where('id', FREEZE_ROW_ID)
|
|
140
|
+
.where('freeze_fence', token.fence)
|
|
141
|
+
.where('freeze_holder', token.holder)
|
|
142
|
+
.where('freeze_until_ms', '>', input.nowMs)
|
|
143
|
+
.update({ freeze_until_ms: input.untilMs, updated_at: new Date(input.nowMs) }));
|
|
144
|
+
return Number(updated) > 0;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Levanta el freeze de ESTE token (condicional: uno ajeno o rezagado no toca
|
|
148
|
+
* nada). Devuelve además `lapsed`: si en el instante de cerrar el lease ya
|
|
149
|
+
* estaba vencido o el token ya no era el de la fila, hubo una ventana en la
|
|
150
|
+
* que otros procesos pudieron escribir — es lo que el reporte publica.
|
|
151
|
+
*/
|
|
152
|
+
export async function releaseFreeze(token, input, options = {}) {
|
|
153
|
+
const { driver, timeoutMs } = opts(options);
|
|
154
|
+
const row = await readFreezeRow(options);
|
|
155
|
+
const mine = row.fence === token.fence && row.holder === token.holder;
|
|
156
|
+
const lapsed = !mine || (row.untilMs !== null && row.untilMs <= input.nowMs);
|
|
157
|
+
if (!mine)
|
|
158
|
+
return { released: false, lapsed };
|
|
159
|
+
const updated = await guardSql(driver, 'freeze.release', timeoutMs, () => db
|
|
160
|
+
.from(CATALOG_VERSION_TABLE)
|
|
161
|
+
.where('id', FREEZE_ROW_ID)
|
|
162
|
+
.where('freeze_fence', token.fence)
|
|
163
|
+
.where('freeze_holder', token.holder)
|
|
164
|
+
.update({ freeze_reason: null, freeze_holder: null, freeze_until_ms: null, updated_at: new Date(input.nowMs) }));
|
|
165
|
+
return { released: Number(updated) > 0, lapsed };
|
|
166
|
+
}
|
|
167
|
+
/** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`). */
|
|
168
|
+
export function freezeKindOf(holder) {
|
|
169
|
+
const prefix = holder?.split(':', 1)[0];
|
|
170
|
+
return prefix === 'operator' || prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=freeze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ScopeChainResolver, ScopeRef } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Un nodo del árbol del consumidor, tal como está en SU tabla (2.5-B · K1):
|
|
4
|
+
* - `self`: el propio scope CANÓNICO (la fila leída: su `type` y su `uuid`
|
|
5
|
+
* tal como están guardados, no tal como llegó la pregunta);
|
|
6
|
+
* - `parent`: el padre directo (`ScopeRef`; si es `app`, la cadena termina
|
|
7
|
+
* ahí), o `null` si el nodo es de primer nivel ⇒ cuelga de `app`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ScopeNode {
|
|
10
|
+
self: ScopeRef;
|
|
11
|
+
parent: ScopeRef | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `nodeOf` del consumidor: la fila de un scope.
|
|
15
|
+
* - `ScopeNode`: el nodo existe; `self` es su identidad canónica y `parent`
|
|
16
|
+
* su padre (`null` = primer nivel);
|
|
17
|
+
* - `undefined`: el scope NO existe ⇒ el resolutor devuelve `null` y el
|
|
18
|
+
* motor deniega/rechaza escribir (L0.3). Vale también a mitad de cadena:
|
|
19
|
+
* un hijo cuyo padre ya no existe es un árbol roto, no un nodo de `app`.
|
|
20
|
+
* Si lanza, el error se propaga tal cual y el driver lo clasifica como 503
|
|
21
|
+
* `E_AUTHZ_RESOLVER_FAILED` (nunca `[]`, nunca `false`). Un `self` o un
|
|
22
|
+
* padre mal formado (`{app, uuid}`, tipo en mayúsculas…) es 503
|
|
23
|
+
* `E_AUTHZ_RESOLVER_FAILED` del propio resolutor (F6): nunca se normaliza en
|
|
24
|
+
* silencio. Un `self` que no sea el scope preguntado (otro tipo, o un uuid
|
|
25
|
+
* que no es el mismo id salvo mayúsculas/guiones) también es 503: la fila
|
|
26
|
+
* que devuelves tiene que ser la del scope que se te pidió.
|
|
27
|
+
*
|
|
28
|
+
* Por qué devuelve el nodo entero y no solo el padre (`parentOf` de 2.1): con
|
|
29
|
+
* el tipo `uuid` de PostgreSQL o una collation `*_ci`, `SELECT … WHERE uuid =
|
|
30
|
+
* ?` encuentra la fila para un ALIAS del id (mayúsculas, sin guiones); si el
|
|
31
|
+
* resolutor solo devolvía el padre, la cadena se construía con el alias del
|
|
32
|
+
* llamante y los hechos —guardados canónicos— no casaban (el deny se evadía).
|
|
33
|
+
*/
|
|
34
|
+
export type NodeOf = (scope: ScopeRef) => Promise<ScopeNode | undefined>;
|
|
35
|
+
export interface HierarchicalResolverOptions {
|
|
36
|
+
nodeOf: NodeOf;
|
|
37
|
+
/**
|
|
38
|
+
* Número máximo de ANCESTROS (`app` incluida; el propio scope no cuenta).
|
|
39
|
+
* Default 64. Superado ⇒ lanza 500 `E_AUTHZ_SCOPE_TOO_DEEP`: truncar
|
|
40
|
+
* devolvería una cadena sin la raíz y el deny de `app` dejaría de verse
|
|
41
|
+
* (fail-open).
|
|
42
|
+
*/
|
|
43
|
+
maxDepth?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Construye un `ScopeChainResolver` a partir de un `nodeOf` (2.1, B4; 2.5-B ·
|
|
47
|
+
* K1): lee el nodo (canónico), sube padre a padre hasta `app`, con conjunto
|
|
48
|
+
* de visitados (un ciclo lanza 422 `E_AUTHZ_SCOPE_CYCLE`: FGA evaluaría el
|
|
49
|
+
* ciclo y un grant en cualquier nodo concedería en la raíz, S2) y cota de
|
|
50
|
+
* profundidad que lanza. Cuesta una llamada a `nodeOf` por nivel: envuélvelo
|
|
51
|
+
* con `memoizeAncestors` (o usa `forRequest()`) para que una request no lo
|
|
52
|
+
* pague por cada pregunta. La cadena que devuelve es `[self canónico,
|
|
53
|
+
* ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
|
|
54
|
+
*/
|
|
55
|
+
export declare function hierarchicalScopeResolver(options: HierarchicalResolverOptions): ScopeChainResolver;
|
|
56
|
+
//# sourceMappingURL=hierarchical_resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hierarchical_resolver.d.ts","sourceRoot":"","sources":["../../src/hierarchical_resolver.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAA;AAExE,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,2BAA2B,GAAG,kBAAkB,CA8ElG"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ScopeCycleError, ScopeResolverError, ScopeTooDeepError } from './errors.js';
|
|
2
|
+
import { assertScope } from './identity.js';
|
|
3
|
+
import { APP_SCOPE, APP_SCOPE_TYPE } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Construye un `ScopeChainResolver` a partir de un `nodeOf` (2.1, B4; 2.5-B ·
|
|
6
|
+
* K1): lee el nodo (canónico), sube padre a padre hasta `app`, con conjunto
|
|
7
|
+
* de visitados (un ciclo lanza 422 `E_AUTHZ_SCOPE_CYCLE`: FGA evaluaría el
|
|
8
|
+
* ciclo y un grant en cualquier nodo concedería en la raíz, S2) y cota de
|
|
9
|
+
* profundidad que lanza. Cuesta una llamada a `nodeOf` por nivel: envuélvelo
|
|
10
|
+
* con `memoizeAncestors` (o usa `forRequest()`) para que una request no lo
|
|
11
|
+
* pague por cada pregunta. La cadena que devuelve es `[self canónico,
|
|
12
|
+
* ...padres canónicos, APP_SCOPE]`: cada nivel es la fila leída.
|
|
13
|
+
*/
|
|
14
|
+
export function hierarchicalScopeResolver(options) {
|
|
15
|
+
const { nodeOf } = options;
|
|
16
|
+
const maxDepth = options.maxDepth ?? 64;
|
|
17
|
+
if (typeof nodeOf !== 'function') {
|
|
18
|
+
throw new TypeError('hierarchicalScopeResolver: nodeOf debe ser una función');
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) {
|
|
21
|
+
throw new TypeError(`hierarchicalScopeResolver: maxDepth debe ser un entero >= 1 (llegó ${String(maxDepth)})`);
|
|
22
|
+
}
|
|
23
|
+
// Separador no imprimible, escrito como escape (nunca como carácter
|
|
24
|
+
// literal): sin él `org`+`a-1` y `o`+`rga-1` colisionaban y un árbol
|
|
25
|
+
// legítimo era un 422 falso de ciclo (F6, CR4).
|
|
26
|
+
const key = (s) => `${s.type}\u001f${s.uuid ?? ''}`;
|
|
27
|
+
const fold = (uuid) => (uuid ?? '').toLowerCase().replaceAll('-', '');
|
|
28
|
+
return async (scope) => {
|
|
29
|
+
if (scope.type === APP_SCOPE_TYPE)
|
|
30
|
+
return [APP_SCOPE];
|
|
31
|
+
const label = `hierarchicalScopeResolver(${scope.type}:${scope.uuid ?? ''})`;
|
|
32
|
+
const chain = [];
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
let asked = scope;
|
|
35
|
+
for (;;) {
|
|
36
|
+
const node = await nodeOf(asked);
|
|
37
|
+
if (node === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
// La RESPUESTA del árbol se valida (D13, F6, K1): un `self` o un padre
|
|
40
|
+
// mal formado no es un nodo, es un fallo de la dependencia ⇒ 503 con la
|
|
41
|
+
// causa; y `self` tiene que ser la fila del scope que se pidió.
|
|
42
|
+
if (!node || typeof node !== 'object' || !('self' in node) || !('parent' in node)) {
|
|
43
|
+
throw new ScopeResolverError(label, new TypeError(`nodeOf devolvió ${typeof node} en vez de { self, parent } | undefined`));
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
assertScope(node.self);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new ScopeResolverError(label, error);
|
|
50
|
+
}
|
|
51
|
+
if (node.self.type !== asked.type || fold(node.self.uuid) !== fold(asked.uuid)) {
|
|
52
|
+
throw new ScopeResolverError(label, new TypeError(`nodeOf(${asked.type}:${asked.uuid ?? ''}) devolvió self = ${node.self.type}:${node.self.uuid ?? ''}: no es la fila del scope pedido`));
|
|
53
|
+
}
|
|
54
|
+
const self = node.self;
|
|
55
|
+
const k = key(self);
|
|
56
|
+
if (visited.has(k)) {
|
|
57
|
+
throw new ScopeCycleError(`hierarchicalScopeResolver: ciclo al resolver ${scope.type}:${scope.uuid} — ` +
|
|
58
|
+
`${self.type}:${self.uuid} ya está en la cadena (${chain.map((s) => `${s.type}:${s.uuid}`).join(' → ') || 'vacía'})`);
|
|
59
|
+
}
|
|
60
|
+
visited.add(k);
|
|
61
|
+
chain.push(self);
|
|
62
|
+
// Los ancestros serán (nodos leídos − 1) + `app` = nodos leídos: si ya
|
|
63
|
+
// superan la cota, se lanza ahora — nunca se devuelve una cadena corta.
|
|
64
|
+
if (chain.length > maxDepth) {
|
|
65
|
+
throw new ScopeTooDeepError(`hierarchicalScopeResolver: la cadena de ${scope.type}:${scope.uuid} supera maxDepth=${maxDepth} ` +
|
|
66
|
+
`(truncarla dejaría fuera la raíz y sus denies)`);
|
|
67
|
+
}
|
|
68
|
+
const parent = node.parent;
|
|
69
|
+
if (parent === null) {
|
|
70
|
+
chain.push(APP_SCOPE);
|
|
71
|
+
return chain;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
assertScope(parent);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new ScopeResolverError(label, error);
|
|
78
|
+
}
|
|
79
|
+
if (parent.type === APP_SCOPE_TYPE) {
|
|
80
|
+
chain.push(APP_SCOPE);
|
|
81
|
+
return chain;
|
|
82
|
+
}
|
|
83
|
+
asked = parent;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=hierarchical_resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hierarchical_resolver.js","sourceRoot":"","sources":["../../src/hierarchical_resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAiDtD;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAoC;IAC5E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAA;IACvC,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,sEAAsE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IAChH,CAAC;IACD,oEAAoE;IACpE,qEAAqE;IACrE,gDAAgD;IAChD,MAAM,GAAG,GAAG,CAAC,CAAW,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAA;IAC7D,MAAM,IAAI,GAAG,CAAC,IAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAEpF,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;YAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QACrD,MAAM,KAAK,GAAG,6BAA6B,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE,GAAG,CAAA;QAC5E,MAAM,KAAK,GAAe,EAAE,CAAA;QAC5B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;QACjC,IAAI,KAAK,GAAG,KAAK,CAAA;QACjB,SAAS,CAAC;YACR,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAA;YAChC,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAA;YACnC,uEAAuE;YACvE,wEAAwE;YACxE,gEAAgE;YAChE,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;gBAClF,MAAM,IAAI,kBAAkB,CAAC,KAAK,EAAE,IAAI,SAAS,CAAC,mBAAmB,OAAO,IAAI,yCAAyC,CAAC,CAAC,CAAA;YAC7H,CAAC;YACD,IAAI,CAAC;gBACH,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACxB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YAC5C,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/E,MAAM,IAAI,kBAAkB,CAC1B,KAAK,EACL,IAAI,SAAS,CACX,UAAU,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE,qBAAqB,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,kCAAkC,CACtI,CACF,CAAA;YACH,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;YACtB,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;YACnB,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnB,MAAM,IAAI,eAAe,CACvB,gDAAgD,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK;oBAC3E,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,0BAA0B,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,GAAG,CACvH,CAAA;YACH,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACd,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAChB,uEAAuE;YACvE,wEAAwE;YACxE,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;gBAC5B,MAAM,IAAI,iBAAiB,CACzB,2CAA2C,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,oBAAoB,QAAQ,GAAG;oBAChG,gDAAgD,CACnD,CAAA;YACH,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;YAC1B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;gBACrB,OAAO,KAAK,CAAA;YACd,CAAC;YACD,IAAI,CAAC;gBACH,WAAW,CAAC,MAAM,CAAC,CAAA;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YAC5C,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBACnC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;gBACrB,OAAO,KAAK,CAAA;YACd,CAAC;YACD,KAAK,GAAG,MAAM,CAAA;QAChB,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -7,17 +7,19 @@ import type { NextFn } from '@adonisjs/core/types/http';
|
|
|
7
7
|
*
|
|
8
8
|
* Reglas:
|
|
9
9
|
* - No autenticado → 401.
|
|
10
|
-
* - Holder sin el
|
|
10
|
+
* - Holder sin el permiso requerido a nivel app → 403.
|
|
11
11
|
* - El ACL decide por IDENTIDAD; el scope del token lo acota
|
|
12
12
|
* middleware.ability() aparte (el token nunca amplía, solo restringe).
|
|
13
|
+
* - Solo `permission`. `{ role }` se retiró (L0.6): `hasRole` es membresía
|
|
14
|
+
* y el deny no la gobierna, así que un PEP sobre un rol era indenegable.
|
|
15
|
+
* Toda decisión de acceso pasa por `authorize`.
|
|
13
16
|
*
|
|
14
17
|
* Uso: middleware.appAccess({ permission: 'audit:read' })
|
|
15
|
-
* middleware.appAccess({ role: 'superadmin' })
|
|
16
18
|
*/
|
|
19
|
+
export interface AppAccessOptions {
|
|
20
|
+
permission: string;
|
|
21
|
+
}
|
|
17
22
|
export default class AppAccessMiddleware {
|
|
18
|
-
handle(ctx: HttpContext, next: NextFn, options
|
|
19
|
-
permission?: string;
|
|
20
|
-
role?: string;
|
|
21
|
-
}): Promise<any>;
|
|
23
|
+
handle(ctx: HttpContext, next: NextFn, options: AppAccessOptions): Promise<any>;
|
|
22
24
|
}
|
|
23
25
|
//# sourceMappingURL=app_access_middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app_access_middleware.d.ts","sourceRoot":"","sources":["../../../src/http/app_access_middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAMvD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAChC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB;CAgDvE"}
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { Exception } from '@adonisjs/core/exceptions';
|
|
2
2
|
import authorization from '../../services/main.js';
|
|
3
3
|
import { APP_SCOPE } from '../types.js';
|
|
4
|
-
|
|
5
|
-
* Enforcement del ACL de NIVEL APP (gemelo de orgAccess para el scope
|
|
6
|
-
* global). Debe ir DESPUÉS de `middleware.auth(...)` — típicamente con el
|
|
7
|
-
* guard `admins`, aunque funciona con cualquier holder con morph name.
|
|
8
|
-
*
|
|
9
|
-
* Reglas:
|
|
10
|
-
* - No autenticado → 401.
|
|
11
|
-
* - Holder sin el rol/permiso requerido a nivel app → 403.
|
|
12
|
-
* - El ACL decide por IDENTIDAD; el scope del token lo acota
|
|
13
|
-
* middleware.ability() aparte (el token nunca amplía, solo restringe).
|
|
14
|
-
*
|
|
15
|
-
* Uso: middleware.appAccess({ permission: 'audit:read' })
|
|
16
|
-
* middleware.appAccess({ role: 'superadmin' })
|
|
17
|
-
*/
|
|
4
|
+
import { AuthorizationConfigError, RoleIsNotAccessError } from '../errors.js';
|
|
18
5
|
export default class AppAccessMiddleware {
|
|
19
|
-
async handle(ctx, next, options
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
async handle(ctx, next, options) {
|
|
7
|
+
// Antes de mirar si hay usuario: una ruta mal declarada tiene que saltar
|
|
8
|
+
// en el primer request de desarrollo, autenticado o no.
|
|
9
|
+
if (options && 'role' in options) {
|
|
10
|
+
throw new RoleIsNotAccessError(options.role);
|
|
11
|
+
}
|
|
12
|
+
if (!options?.permission) {
|
|
13
|
+
throw new AuthorizationConfigError("middleware.appAccess requiere `{ permission }` (p. ej. appAccess({ permission: 'audit:read' }))");
|
|
22
14
|
}
|
|
23
15
|
const holder = ctx.auth?.user;
|
|
24
16
|
if (!holder) {
|
|
@@ -39,15 +31,7 @@ export default class AppAccessMiddleware {
|
|
|
39
31
|
`no por la clave primaria del modelo.`, { status: 500 });
|
|
40
32
|
}
|
|
41
33
|
const subject = { type: morph, uuid: holder.uuid };
|
|
42
|
-
if (
|
|
43
|
-
return ctx.response.forbidden({
|
|
44
|
-
message: 'Permiso insuficiente',
|
|
45
|
-
statusCode: 403,
|
|
46
|
-
errors: [],
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
if (options.permission &&
|
|
50
|
-
!(await authorization.authorize(subject, options.permission, APP_SCOPE))) {
|
|
34
|
+
if (!(await authorization.authorize(subject, options.permission, APP_SCOPE))) {
|
|
51
35
|
return ctx.response.forbidden({
|
|
52
36
|
message: 'Permiso insuficiente',
|
|
53
37
|
statusCode: 403,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app_access_middleware.js","sourceRoot":"","sources":["../../../src/http/app_access_middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,aAAa,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAsB7E,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACtC,KAAK,CAAC,MAAM,CAAC,GAAgB,EAAE,IAAY,EAAE,OAAyB;QACpE,yEAAyE;QACzE,wDAAwD;QACxD,IAAI,OAAO,IAAI,MAAM,IAAK,OAAkB,EAAE,CAAC;YAC7C,MAAM,IAAI,oBAAoB,CAAE,OAAe,CAAC,IAAI,CAAC,CAAA;QACvD,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,wBAAwB,CAChC,iGAAiG,CAClG,CAAA;QACH,CAAC;QAED,MAAM,MAAM,GAAI,GAAW,CAAC,IAAI,EAAE,IAA6D,CAAA;QAC/F,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QAC9F,CAAC;QAED,mEAAmE;QACnE,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAA;QACnC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,mEAAmE,EAAE;gBACvF,MAAM,EAAE,GAAG;aACZ,CAAC,CAAA;QACJ,CAAC;QAED,kEAAkE;QAClE,wEAAwE;QACxE,yEAAyE;QACzE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,SAAS,CACjB,cAAc,KAAK,mEAAmE;gBACpF,sCAAsC,EACxC,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAA;QACH,CAAC;QAED,MAAM,OAAO,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;QAElD,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;YAC7E,OAAO,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC5B,OAAO,EAAE,sBAAsB;gBAC/B,UAAU,EAAE,GAAG;gBACf,MAAM,EAAE,EAAE;aACX,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;CACF"}
|