@jantstack/adonis-authz 2.0.0-alpha.1 → 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/README.md +462 -35
- package/build/commands/authz_catalog_diff.js +1 -1
- package/build/commands/authz_catalog_diff.js.map +1 -1
- 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 +17 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -1
- package/build/commands/authz_catalog_sync.js +27 -4
- package/build/commands/authz_catalog_sync.js.map +1 -1
- 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 +6 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +6 -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 +90 -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 +39 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +35 -6
- 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.d.ts → catalog/catalog.d.ts} +41 -2
- package/build/src/catalog/catalog.d.ts.map +1 -0
- package/build/src/{catalog.js → catalog/catalog.js} +120 -14
- package/build/src/catalog/catalog.js.map +1 -0
- package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
- package/build/src/catalog/catalog_cache.d.ts.map +1 -0
- package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
- package/build/src/catalog/catalog_cache.js.map +1 -0
- package/build/src/define_config.d.ts +101 -3
- 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 +86 -4
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +425 -11
- 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 +713 -119
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2048 -476
- 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 +258 -5
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +238 -7
- package/build/src/errors.js.map +1 -1
- 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/http/app_access_middleware.d.ts.map +1 -0
- 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 +74 -1
- package/build/src/identity.d.ts.map +1 -1
- package/build/src/identity.js +100 -2
- package/build/src/identity.js.map +1 -1
- package/build/src/manager.d.ts +315 -4
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1187 -181
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +6 -6
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +6 -6
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +6 -6
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +6 -6
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +6 -6
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +18 -2
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +15 -1
- package/build/src/openfga.js.map +1 -1
- 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/{drivers → shared}/backend_guard.d.ts +14 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.js +26 -1
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/sql_descendants.d.ts +47 -1
- package/build/src/sql_descendants.d.ts.map +1 -1
- package/build/src/sql_descendants.js +75 -1
- package/build/src/sql_descendants.js.map +1 -1
- package/build/src/testing/contract.d.ts +74 -0
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +672 -169
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +6 -0
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +3 -0
- 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/traits/authz_scopes.js +1 -1
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +7 -7
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +865 -82
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +10 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +104 -4
- package/build/stubs/migration.stub +126 -0
- package/build/stubs/scopes_outbox_migration.stub +57 -0
- package/package.json +4 -2
- package/build/commands/openfga_import.d.ts +0 -34
- package/build/commands/openfga_import.d.ts.map +0 -1
- package/build/commands/openfga_import.js +0 -97
- package/build/commands/openfga_import.js.map +0 -1
- package/build/src/catalog.d.ts.map +0 -1
- package/build/src/catalog.js.map +0 -1
- package/build/src/catalog_cache.d.ts.map +0 -1
- package/build/src/catalog_cache.js.map +0 -1
- package/build/src/drivers/backend_guard.d.ts.map +0 -1
- package/build/src/drivers/backend_guard.js.map +0 -1
- package/build/src/drivers/sql_expiry.d.ts.map +0 -1
- package/build/src/drivers/sql_expiry.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
- /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
- /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.js +0 -0
|
@@ -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 @@
|
|
|
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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { NextFn } from '@adonisjs/core/types/http';
|
|
3
|
+
import type { ScopeRef } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Enforcement de acceso A UN RECURSO concreto. COMPONE el `authorize` que ya
|
|
6
|
+
* existe: el consumidor dice CÓMO cargar el recurso (`load(ctx, id)`) y en qué
|
|
7
|
+
* `{ scope }` vive, y el middleware pregunta al motor sobre ese scope. No es un
|
|
8
|
+
* modelo ni un driver nuevo: es el borde HTTP del scope de un recurso.
|
|
9
|
+
*
|
|
10
|
+
* El ORDEN de las respuestas ES la propiedad de seguridad — un 403 donde
|
|
11
|
+
* debería ir un 404 filtra qué recursos existen (enumeración):
|
|
12
|
+
*
|
|
13
|
+
* 1. 401 si no hay usuario autenticado (antes de nada).
|
|
14
|
+
* 2. 403 si el `gate(ctx)` del consumidor (una ability previa, p. ej.
|
|
15
|
+
* «es admin del tenant») lo niega.
|
|
16
|
+
* 3. 404 si el CONTENEDOR no existe (`containerParam`, cuando se declara).
|
|
17
|
+
* 4. 404 si el RECURSO no existe (`load` ⇒ null) — MISMO cuerpo que el 404
|
|
18
|
+
* del contenedor, para no distinguir «no existe» de «no es tuyo».
|
|
19
|
+
* 5. authorize sobre el `{ scope }` que devolvió `load`, con `permission`
|
|
20
|
+
* (o `readPermission` en métodos seguros): un `false` aquí también
|
|
21
|
+
* es 404 con el MISMO cuerpo, NO un 403 — que no puedas verlo no
|
|
22
|
+
* revela que existe.
|
|
23
|
+
*
|
|
24
|
+
* Invariantes que NO se negocian:
|
|
25
|
+
* - `AuthorizationBackendError` (503) NUNCA se disfraza de 404/403: si el
|
|
26
|
+
* backend cae (`gate`/`load`/`authorize` lanzan), el error sube tal cual.
|
|
27
|
+
* Es lo que distingue «denegado» de «no pude comprobar». El middleware NO
|
|
28
|
+
* envuelve estas llamadas en try/catch a propósito.
|
|
29
|
+
* - Prohibido `role`: como `appAccess`, solo acepta `permission`/
|
|
30
|
+
* `readPermission`. `{ role }` es 500 con receta (membresía no es acceso, y
|
|
31
|
+
* el deny no la gobierna: toda decisión pasa por `authorize`).
|
|
32
|
+
* - Sin segundo `authorize`: se llama UNA sola vez. No re-autoriza.
|
|
33
|
+
*
|
|
34
|
+
* Limitación conocida — canal de TEMPORIZACIÓN (auditor Fase 5, ⚪): el status
|
|
35
|
+
* y el cuerpo de «no existe» y «existe pero no es tuyo» son idénticos, pero el
|
|
36
|
+
* TIEMPO no: un id inexistente responde 404 sin round-trip a `authorize`; uno
|
|
37
|
+
* ajeno responde el mismo 404 tras esa llamada. Es inherente a load→authorize
|
|
38
|
+
* (no se puede autorizar el scope de algo que no se ha cargado), no un defecto;
|
|
39
|
+
* quien necesite cerrar el canal iguala el tiempo en su capa (delay constante),
|
|
40
|
+
* no en el middleware.
|
|
41
|
+
*
|
|
42
|
+
* Pureza: el middleware NO importa aliases del consumidor; `load`/`gate` llegan
|
|
43
|
+
* INYECTADOS en las opciones de la ruta.
|
|
44
|
+
*
|
|
45
|
+
* Uso:
|
|
46
|
+
* router
|
|
47
|
+
* .get('/orgs/:orgId/documents/:id', [DocumentsController, 'show'])
|
|
48
|
+
* .use(middleware.resourceAccess({
|
|
49
|
+
* resource: 'document',
|
|
50
|
+
* param: 'id',
|
|
51
|
+
* containerParam: 'orgId',
|
|
52
|
+
* permission: 'documents:write',
|
|
53
|
+
* readPermission: 'documents:read',
|
|
54
|
+
* load: (ctx, id) => Document.query().where('id', id).first(),
|
|
55
|
+
* gate: (ctx) => ctx.auth.user!.isMemberOf(ctx.params.orgId),
|
|
56
|
+
* }))
|
|
57
|
+
*/
|
|
58
|
+
export interface ResourceAccessOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Nombre del recurso. Es la clave bajo la que el recurso cargado (con su
|
|
61
|
+
* scope) se PROPAGA al `ctx` para el controlador (`ctx[resource]`), así que
|
|
62
|
+
* dos `resourceAccess` anidados no se pisan.
|
|
63
|
+
*/
|
|
64
|
+
resource: string;
|
|
65
|
+
/** Nombre del param de ruta que trae el id del recurso (`ctx.params[param]`). */
|
|
66
|
+
param: string;
|
|
67
|
+
/**
|
|
68
|
+
* Nombre del param de ruta del contenedor (p. ej. el tenant de una ruta
|
|
69
|
+
* anidada). Cuando se declara, un contenedor ausente en la ruta es 404 —el
|
|
70
|
+
* MISMO cuerpo que un recurso ausente— y se comprueba ANTES de cargar el
|
|
71
|
+
* recurso. La pertenencia real del recurso al contenedor la comprueba
|
|
72
|
+
* `load` (que ve `ctx.params`): devolver `null` allí es el mismo 404, así que
|
|
73
|
+
* «no existe» y «no está en este contenedor» son indistinguibles.
|
|
74
|
+
*/
|
|
75
|
+
containerParam?: string;
|
|
76
|
+
/** Permiso requerido (métodos que mutan, y lectura si no hay `readPermission`). */
|
|
77
|
+
permission: string;
|
|
78
|
+
/**
|
|
79
|
+
* Permiso requerido en métodos SEGUROS (GET/HEAD). Si se omite, una lectura
|
|
80
|
+
* exige `permission` — nunca es gratis. Suele ser el permiso MENOS
|
|
81
|
+
* privilegiado (ver vs escribir).
|
|
82
|
+
*/
|
|
83
|
+
readPermission?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Carga el recurso por su id y devuelve dónde vive (`{ scope }`) o `null` si
|
|
86
|
+
* no existe / no es visible. Del consumidor: ve el `ctx` entero (params,
|
|
87
|
+
* usuario). Un throw es «no pude comprobar» (sube como 503), NO un 404.
|
|
88
|
+
*/
|
|
89
|
+
load: (ctx: HttpContext, id: string) => Promise<{
|
|
90
|
+
scope: ScopeRef;
|
|
91
|
+
} | null> | {
|
|
92
|
+
scope: ScopeRef;
|
|
93
|
+
} | null;
|
|
94
|
+
/**
|
|
95
|
+
* Ability previa del consumidor (p. ej. «es admin del tenant»). Opcional; si
|
|
96
|
+
* se declara y devuelve `false`, es 403 —una restricción ADICIONAL, evaluada
|
|
97
|
+
* antes de tocar el recurso—. Si se omite, no hay pre-check: la seguridad la
|
|
98
|
+
* da igualmente `authorize`.
|
|
99
|
+
*/
|
|
100
|
+
gate?: (ctx: HttpContext) => Promise<boolean> | boolean;
|
|
101
|
+
}
|
|
102
|
+
export default class ResourceAccessMiddleware {
|
|
103
|
+
handle(ctx: HttpContext, next: NextFn, options: ResourceAccessOptions): Promise<any>;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=resource_access_middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource_access_middleware.d.ts","sourceRoot":"","sources":["../../../src/http/resource_access_middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAGvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,IAAI,EAAE,CACJ,GAAG,EAAE,WAAW,EAChB,EAAE,EAAE,MAAM,KACP,OAAO,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC,GAAG;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAA;IACrE;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;CACxD;AAKD,MAAM,CAAC,OAAO,OAAO,wBAAwB;IACrC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;CA6F5E"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Exception } from '@adonisjs/core/exceptions';
|
|
2
|
+
import authorization from '../../services/main.js';
|
|
3
|
+
import { AuthorizationConfigError, RoleIsNotAccessError } from '../errors.js';
|
|
4
|
+
/** Métodos HTTP seguros: usan `readPermission` si se declara. */
|
|
5
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD']);
|
|
6
|
+
export default class ResourceAccessMiddleware {
|
|
7
|
+
async handle(ctx, next, options) {
|
|
8
|
+
// Config PRIMERO, antes de mirar si hay usuario: una ruta mal declarada
|
|
9
|
+
// tiene que saltar en el primer request de desarrollo, autenticado o no.
|
|
10
|
+
if (options && 'role' in options) {
|
|
11
|
+
throw new RoleIsNotAccessError(options.role);
|
|
12
|
+
}
|
|
13
|
+
if (!options?.permission) {
|
|
14
|
+
throw new AuthorizationConfigError("middleware.resourceAccess requiere `{ permission }` (p. ej. resourceAccess({ permission: 'documents:write', ... }))");
|
|
15
|
+
}
|
|
16
|
+
if (!options.param) {
|
|
17
|
+
throw new AuthorizationConfigError('middleware.resourceAccess requiere `{ param }` — el nombre del param de ruta con el id del recurso');
|
|
18
|
+
}
|
|
19
|
+
if (typeof options.load !== 'function') {
|
|
20
|
+
throw new AuthorizationConfigError('middleware.resourceAccess requiere `load(ctx, id)` — cómo cargar el recurso y en qué `{ scope }` vive');
|
|
21
|
+
}
|
|
22
|
+
if (!options.resource) {
|
|
23
|
+
throw new AuthorizationConfigError('middleware.resourceAccess requiere `{ resource }` — el nombre bajo el que se propaga el recurso al controlador');
|
|
24
|
+
}
|
|
25
|
+
// ── 1. 401 ────────────────────────────────────────────────────────────
|
|
26
|
+
const holder = ctx.auth?.user;
|
|
27
|
+
if (!holder) {
|
|
28
|
+
return ctx.response.unauthorized({ message: 'No autenticado', statusCode: 401, errors: [] });
|
|
29
|
+
}
|
|
30
|
+
// El holder tiene que ser un subject válido (mismos guards que appAccess).
|
|
31
|
+
// Son errores de programación (500), no de acceso: nunca degradan a 404.
|
|
32
|
+
const morph = holder.__morphMapName;
|
|
33
|
+
if (!morph) {
|
|
34
|
+
throw new Exception('El modelo autenticado no tiene @MorphMap — no es un holder válido', {
|
|
35
|
+
status: 500,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!holder.uuid) {
|
|
39
|
+
throw new Exception(`El holder '${morph}' no expone 'uuid' — el motor identifica a los holders por uuid, ` +
|
|
40
|
+
`no por la clave primaria del modelo.`, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
const subject = { type: morph, uuid: holder.uuid };
|
|
43
|
+
// ── 2. 403 gate del consumidor ────────────────────────────────────────
|
|
44
|
+
// Sin try/catch: si el gate lanza (backend caído), el 503 sube tal cual.
|
|
45
|
+
if (options.gate && !(await options.gate(ctx))) {
|
|
46
|
+
return ctx.response.forbidden({ message: 'Permiso insuficiente', statusCode: 403, errors: [] });
|
|
47
|
+
}
|
|
48
|
+
// El 404 del contenedor, del recurso ausente y del authorize negativo
|
|
49
|
+
// comparten EXACTAMENTE el mismo cuerpo: nada distingue los tres.
|
|
50
|
+
const notFound = () => ctx.response.notFound({ message: 'Recurso no encontrado', statusCode: 404, errors: [] });
|
|
51
|
+
// ── 3. 404 contenedor (ANTES que el recurso) ──────────────────────────
|
|
52
|
+
if (options.containerParam) {
|
|
53
|
+
const containerId = ctx.params?.[options.containerParam];
|
|
54
|
+
if (!containerId)
|
|
55
|
+
return notFound();
|
|
56
|
+
}
|
|
57
|
+
// ── 4. 404 recurso ────────────────────────────────────────────────────
|
|
58
|
+
const id = ctx.params?.[options.param];
|
|
59
|
+
if (!id)
|
|
60
|
+
return notFound();
|
|
61
|
+
// Un throw de `load` NO se atrapa: «no pude comprobar» (503) ≠ «no existe».
|
|
62
|
+
const loaded = await options.load(ctx, String(id));
|
|
63
|
+
if (!loaded)
|
|
64
|
+
return notFound();
|
|
65
|
+
// ── 5. authorize (UNA vez) sobre el scope del recurso ─────────────────
|
|
66
|
+
const method = String(ctx.request?.method?.() ?? 'GET').toUpperCase();
|
|
67
|
+
const permission = SAFE_METHODS.has(method) && options.readPermission
|
|
68
|
+
? options.readPermission
|
|
69
|
+
: options.permission;
|
|
70
|
+
if (!(await authorization.authorize(subject, permission, loaded.scope))) {
|
|
71
|
+
// Negado ⇒ 404 con el MISMO cuerpo, NO 403: que no puedas verlo no
|
|
72
|
+
// revela que existe.
|
|
73
|
+
return notFound();
|
|
74
|
+
}
|
|
75
|
+
// El recurso cargado (con su scope) llega al controlador sin recargarlo.
|
|
76
|
+
;
|
|
77
|
+
ctx[options.resource] = loaded;
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=resource_access_middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource_access_middleware.js","sourceRoot":"","sources":["../../../src/http/resource_access_middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,aAAa,MAAM,wBAAwB,CAAA;AAElD,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAoG7E,iEAAiE;AACjE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;AAE7C,MAAM,CAAC,OAAO,OAAO,wBAAwB;IAC3C,KAAK,CAAC,MAAM,CAAC,GAAgB,EAAE,IAAY,EAAE,OAA8B;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,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,qHAAqH,CACtH,CAAA;QACH,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,wBAAwB,CAChC,oGAAoG,CACrG,CAAA;QACH,CAAC;QACD,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,IAAI,wBAAwB,CAChC,uGAAuG,CACxG,CAAA;QACH,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtB,MAAM,IAAI,wBAAwB,CAChC,gHAAgH,CACjH,CAAA;QACH,CAAC;QAED,yEAAyE;QACzE,MAAM,MAAM,GAAI,GAAW,CAAC,IAAI,EAAE,IAErB,CAAA;QACb,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,2EAA2E;QAC3E,yEAAyE;QACzE,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;QACD,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;QACD,MAAM,OAAO,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;QAElD,yEAAyE;QACzE,yEAAyE;QACzE,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC/C,OAAO,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QACjG,CAAC;QAED,sEAAsE;QACtE,kEAAkE;QAClE,MAAM,QAAQ,GAAG,GAAG,EAAE,CACpB,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QAE1F,yEAAyE;QACzE,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC3B,MAAM,WAAW,GAAI,GAAW,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YACjE,IAAI,CAAC,WAAW;gBAAE,OAAO,QAAQ,EAAE,CAAA;QACrC,CAAC;QAED,yEAAyE;QACzE,MAAM,EAAE,GAAI,GAAW,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAC/C,IAAI,CAAC,EAAE;YAAE,OAAO,QAAQ,EAAE,CAAA;QAC1B,4EAA4E;QAC5E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;QAClD,IAAI,CAAC,MAAM;YAAE,OAAO,QAAQ,EAAE,CAAA;QAE9B,yEAAyE;QACzE,MAAM,MAAM,GAAG,MAAM,CAAE,GAAW,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;QAC9E,MAAM,UAAU,GACd,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,cAAc;YAChD,CAAC,CAAC,OAAO,CAAC,cAAc;YACxB,CAAC,CAAC,OAAO,CAAC,UAAU,CAAA;QAExB,IAAI,CAAC,CAAC,MAAM,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACxE,mEAAmE;YACnE,qBAAqB;YACrB,OAAO,QAAQ,EAAE,CAAA;QACnB,CAAC;QAED,yEAAyE;QACzE,CAAC;QAAC,GAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAA;QACxC,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;CACF"}
|
package/build/src/identity.d.ts
CHANGED
|
@@ -54,6 +54,37 @@ export declare function assertSubject(subject: SubjectRef): void;
|
|
|
54
54
|
export declare function assertScope(scope: ScopeRef): void;
|
|
55
55
|
/** Tipo de scope suelto (el de `listRoleScopes`). */
|
|
56
56
|
export declare function assertScopeType(scopeType: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Longitud máxima del id de un objeto/holder de RELACIÓN (columna
|
|
59
|
+
* `varchar(64)` de `authz_relations`). Más ancha que `IDENTITY_LIMITS.uuid`
|
|
60
|
+
* (36): el `object.id` de ReBAC lo elige el consumidor y no tiene por qué ser
|
|
61
|
+
* un uuid.
|
|
62
|
+
*/
|
|
63
|
+
export declare const RELATION_ID_LIMIT = 64;
|
|
64
|
+
/**
|
|
65
|
+
* **La gramática del id de un objeto de RELACIÓN** (R-16, Fase 4 · lote 4-7).
|
|
66
|
+
*
|
|
67
|
+
* Un `object.id` de relaciones va en la columna `varchar(64)` de
|
|
68
|
+
* `authz_relations` y, en el store compartido, en el id de objeto de FGA
|
|
69
|
+
* (`document:<partitionKey>|<object.id>`). El driver `database` ya lo validaba
|
|
70
|
+
* (`RELATION_ID_FORMAT`), pero ni el manager ni el driver `openfga` lo hacían:
|
|
71
|
+
* el `openfga` solo medía la LONGITUD. Un `object.id` con un `|` dentro
|
|
72
|
+
* (`'a|b'`) se ESCRIBÍA y `check` lo honraba, pero al parsear de vuelta (corte
|
|
73
|
+
* por el ÚLTIMO `|`) la partición salía mal, así que `listObjects` lo filtraba
|
|
74
|
+
* y `enumerateRelations` lo descartaba: una relación que concede y que NO se ve
|
|
75
|
+
* en la enumeración ni migra —pérdida SILENCIOSA en `reconcile`, justo lo que
|
|
76
|
+
* el driver promete que no pasa— y una divergencia con `database`, que lo
|
|
77
|
+
* rechaza con 422. Y los caracteres estructurales (`:`/`#`/espacio) salían como
|
|
78
|
+
* 503 mal clasificado, cuando por invariante 5 una entrada malformada es 422.
|
|
79
|
+
*
|
|
80
|
+
* Misma lista blanca que el resto de la identidad (`COMPONENT_FORMAT`):
|
|
81
|
+
* minúsculas, dígitos y `._-`; excluye `|`, `#`, `:`, `@`, `*`, `~`, espacios y
|
|
82
|
+
* control. La aplican el manager (una vez por llamada) Y cada driver (defensa
|
|
83
|
+
* en profundidad), como el resto de la gramática.
|
|
84
|
+
*/
|
|
85
|
+
export declare function assertRelationId(kind: string, value: unknown): asserts value is string;
|
|
86
|
+
/** Versión booleana para caminos de LECTURA (ids que vienen del backend): no lanza. */
|
|
87
|
+
export declare function isValidRelationId(value: unknown): value is string;
|
|
57
88
|
/**
|
|
58
89
|
* Clave textual de un scope: `app` para la raíz, `<tipo>|<uuid>` para el
|
|
59
90
|
* resto (≤ 57 caracteres). Es la MISMA en todo el paquete: el id de los
|
|
@@ -66,6 +97,34 @@ export declare function assertScopeType(scopeType: string): void;
|
|
|
66
97
|
* 422 aquí, antes de serializarse a nada.
|
|
67
98
|
*/
|
|
68
99
|
export declare function scopeKey(scope: ScopeRef): string;
|
|
100
|
+
/**
|
|
101
|
+
* **Las ORTOGRAFÍAS de un scope con las que hay que limpiar cuando ya no hay
|
|
102
|
+
* cadena con la que canonizar** (3b-2h · 🟠 3, auditor R2).
|
|
103
|
+
*
|
|
104
|
+
* La identidad de un scope es la canónica que devuelve el resolutor
|
|
105
|
+
* (invariante 17) y el paquete la usa para TODOS los hechos… mientras la fila
|
|
106
|
+
* del consumidor existe. `scopes.detached` es justo la operación que admite
|
|
107
|
+
* que ya NO exista (3F · S1: se purga igual, con el scope tal cual), y ahí no
|
|
108
|
+
* hay con qué canonizar: se usa la ortografía del llamante. Medido contra
|
|
109
|
+
* PostgreSQL: el tipo `uuid` funde `01a0…-…` con el mismo uuid SIN guiones,
|
|
110
|
+
* así que el `DELETE` del controlador acierta con el alias, el `detached`
|
|
111
|
+
* llega con el alias, `purgeScope` demuestra cero sobre un objeto que no
|
|
112
|
+
* existe y el scope REAL conserva su `parent` y su `binding`: en modo
|
|
113
|
+
* `facts`, **concede para siempre**. En lectura el mismo alias es
|
|
114
|
+
* fail-closed; en escritura era fail-OPEN.
|
|
115
|
+
*
|
|
116
|
+
* La salida es no elegir: una purga sin cadena limpia la ortografía del
|
|
117
|
+
* llamante **y** la canónica de la que puede ser alias. Solo se expande el
|
|
118
|
+
* alias SIN guiones (32 hex ⇒ también la forma 8-4-4-4-12), que es la única
|
|
119
|
+
* que un motor produce fundiendo —PostgreSQL canoniza CON guiones; el alias
|
|
120
|
+
* por mayúsculas ya muere en la puerta (`assertScope`)—; así el consumidor
|
|
121
|
+
* que guarda sus uuids con guiones (el caso normal) no paga NADA, y el que
|
|
122
|
+
* los guarda sin guiones sigue purgando su clave y, además, una que nunca
|
|
123
|
+
* tiene hechos. Normalizar en vez de expandir no vale: sin la fila no se sabe
|
|
124
|
+
* cuál de las dos es la buena, y elegir mal deja los hechos vivos, que es el
|
|
125
|
+
* fallo que se está cerrando.
|
|
126
|
+
*/
|
|
127
|
+
export declare function scopeSpellings(scope: ScopeRef): ScopeRef[];
|
|
69
128
|
/**
|
|
70
129
|
* La inversa de `scopeKey` para claves que vienen de la BASE (el owner de un
|
|
71
130
|
* rol): `app` ⇒ la raíz; `<tipo>|<uuid>` ⇒ el scope; cualquier otra forma
|
|
@@ -75,7 +134,7 @@ export declare function scopeKey(scope: ScopeRef): string;
|
|
|
75
134
|
export declare function scopeFromKey(key: string): ScopeRef | null;
|
|
76
135
|
/**
|
|
77
136
|
* Nombres que el modelo FGA del modo `facts` usa como relaciones propias
|
|
78
|
-
* (`scope#parent`, `role_binding#assignee`, `
|
|
137
|
+
* (`scope#parent`, `role_binding#assignee`, `scope#denied_<P>`…). Un
|
|
79
138
|
* permiso llamado `parent` invalidaría el modelo entero (S14): se rechaza en
|
|
80
139
|
* el núcleo, para ambos drivers, no el día de la migración.
|
|
81
140
|
*/
|
|
@@ -87,6 +146,20 @@ export declare const RESERVED_SLUGS: ReadonlySet<string>;
|
|
|
87
146
|
* (S4: `can_docs_write` anulaba el deny de `docs:write`).
|
|
88
147
|
*/
|
|
89
148
|
export declare const RESERVED_SLUG_PREFIXES: readonly string[];
|
|
149
|
+
/**
|
|
150
|
+
* **Los TIPOS de objeto que el modelo `facts` (y el `group` de relaciones) YA
|
|
151
|
+
* declaran** (Fase 4 · ⚪4). Un tipo de objeto de relaciones que se llame como
|
|
152
|
+
* uno de estos DUPLICARÍA un tipo del store compartido: `role_binding` sobre
|
|
153
|
+
* todo —si el driver de relaciones pudiera declararlo, podría componer el id
|
|
154
|
+
* de un binding real y `relate` sería una escalada a `roles.authorize`—. Vive
|
|
155
|
+
* en `identity.ts` (la gramática compartida) porque lo consumen DOS módulos
|
|
156
|
+
* que la frontera de pureza mantiene disjuntos: el generador del modelo
|
|
157
|
+
* (`src/drivers/openfga_facts.ts`, que no puede depender de `relations/`) y
|
|
158
|
+
* `defineRelationsConfig` (`src/relations/`, que no puede depender de
|
|
159
|
+
* `drivers/`). `deny_binding` sigue reservado aunque el modelo (c2r) ya no lo
|
|
160
|
+
* emita: un catálogo migrado del modo `resolver` no puede resucitarlo.
|
|
161
|
+
*/
|
|
162
|
+
export declare const RESERVED_FACTS_TYPES: ReadonlySet<string>;
|
|
90
163
|
/**
|
|
91
164
|
* Longitud máxima de un slug. Un nombre de relación en FGA admite 50
|
|
92
165
|
* caracteres y el modelo deriva `permits_<slug>` (el prefijo más largo, 8):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;EAI1B,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,yCAAyC,CAAA;AAgBnE,uGAAuG;AACvG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAMvF;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE7D;AAoDD,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAMvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAqBjD;AAED,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAIhD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAMzD;AAWD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"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"}
|