@jantstack/adonis-authz 2.5.0-alpha.4 → 2.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -56
- package/build/commands/authz_catalog_prune_orphans.d.ts +17 -2
- package/build/commands/authz_catalog_prune_orphans.js +28 -5
- package/build/commands/authz_scopes_relay.js +5 -1
- package/build/commands/authz_scopes_requeue.js +2 -1
- package/build/commands/authz_upgrade_1x.d.ts +216 -0
- package/build/commands/authz_upgrade_1x.js +903 -0
- package/build/commands/main.d.ts +2 -1
- package/build/commands/main.js +5 -3
- package/build/index.d.ts +2 -2
- package/build/src/catalog/catalog_cache.js +2 -1
- package/build/src/drivers/database_driver.d.ts +54 -2
- package/build/src/drivers/database_driver.js +138 -23
- package/build/src/drivers/database_relations_driver.js +25 -15
- package/build/src/errors.d.ts +39 -0
- package/build/src/errors.js +8 -0
- package/build/src/identity.d.ts +7 -1
- package/build/src/identity.js +29 -0
- package/build/src/manager.d.ts +24 -0
- package/build/src/manager.js +225 -57
- package/build/src/scope_outbox.d.ts +36 -2
- package/build/src/scope_outbox.js +78 -17
- package/build/src/shared/sql_expiry.d.ts +7 -3
- package/build/src/shared/sql_expiry.js +41 -7
- package/build/src/testing/contract.d.ts +37 -4
- package/build/src/testing/contract.js +280 -22
- package/build/src/testing/relations_contract.d.ts +11 -1
- package/build/src/testing/relations_contract.js +107 -18
- package/build/src/types.d.ts +49 -0
- package/build/src/types.js +2 -0
- package/package.json +1 -1
|
@@ -85,7 +85,9 @@ const MYSQL_LOCK_NAME_MAX = 64;
|
|
|
85
85
|
* coincidieran en los primeros 64 COMPARTÍAN lease y se excluían para siempre
|
|
86
86
|
* — y el modo de fallo era `busy: true`, que no lanza: el relay de uno de los
|
|
87
87
|
* dos no corría nunca, en silencio. Desde alpha.5 el nombre es un hash
|
|
88
|
-
* ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona
|
|
88
|
+
* ESTABLE del nombre completo (`current`, 59 caracteres), que no colisiona entre
|
|
89
|
+
* TABLAS — entre BASES del mismo servidor sí: el «nombre completo» no llevaba la
|
|
90
|
+
* base (rotación 2 · R2-01, abajo).
|
|
89
91
|
*
|
|
90
92
|
* **Transición**: `sqlScopeOutbox.acquire()` toma el lock viejo (`legacy`, el
|
|
91
93
|
* recortado) Y el nuevo durante alpha.5, porque en un rolling deploy un
|
|
@@ -114,13 +116,47 @@ export const SCOPE_OUTBOX_LEGACY_LOCK_UNTIL = '2.4.0-alpha.6';
|
|
|
114
116
|
* desde ≤ alpha.4 en MySQL exige parada completa del relay, no despliegue rodante.
|
|
115
117
|
*/
|
|
116
118
|
export const SCOPE_OUTBOX_TAKES_LEGACY_LOCK = false;
|
|
117
|
-
|
|
119
|
+
/**
|
|
120
|
+
* **La identidad de la BASE en el nombre del lease** (rotación 2 de M4 · R2-01, decisión del dueño 2026-10-22 (1)).
|
|
121
|
+
* `GET_LOCK` es un cerrojo de SERVIDOR, y hasta 2.5.0-alpha.4 el nombre era `<conexión>:<tabla>`: dos despliegues en
|
|
122
|
+
* BASES distintas del mismo MySQL con la conexión por defecto de Adonis y la tabla del stub calculaban el MISMO nombre y
|
|
123
|
+
* se excluían (`busy`, exit 0) — el síntoma que R6 de alpha.5 daba por cerrado nunca lo estuvo, porque el «nombre
|
|
124
|
+
* completo» no llevaba la base. Desde este cierre `current` lleva la base que la PROPIA conexión que sostiene el lease
|
|
125
|
+
* lee con `DATABASE()` (no la del config: es la base contra la que esa sesión resuelve la tabla sin cualificar, o sea la
|
|
126
|
+
* identidad real de la cola; un alias de host o una URL distinta que apunte a la misma base dan el mismo nombre).
|
|
127
|
+
*
|
|
128
|
+
* En MINÚSCULAS a propósito: los dos errores posibles no cuestan lo mismo. Fundir `App` y `app` (dos bases distintas en
|
|
129
|
+
* un servidor sensible a mayúsculas) solo hace que esos dos despliegues compartan lease —vivacidad, `busy`—; separar la
|
|
130
|
+
* misma base escrita de dos formas por dos procesos del MISMO despliegue serían dos escritores —seguridad—. Se elige el
|
|
131
|
+
* error barato. `DATABASE()` `NULL` (sesión sin base por defecto: la tabla tiene que ir cualificada) entra como cadena
|
|
132
|
+
* vacía: los procesos de ese despliegue siguen calculando el mismo nombre.
|
|
133
|
+
*
|
|
134
|
+
* Los tres nombres: `legacy` (el recortado a 64 de ≤ alpha.4, retirado en alpha.7, se conserva para el caso que lo
|
|
135
|
+
* fija), `unscoped` (el sha1 sin base de 2.4.0-alpha.5..2.5.0-alpha.4) y `current` (el sha1 con base). Sin `database`
|
|
136
|
+
* —quien no ha abierto conexión: los casos de identidad pura— `current` es `unscoped`.
|
|
137
|
+
*/
|
|
138
|
+
export function scopeOutboxLockNames(connection, table, database) {
|
|
118
139
|
const full = `authz_scope_outbox:${connection ?? 'default'}:${table}`;
|
|
140
|
+
const sha = (value) => `authz_scope_outbox:${createHash('sha1').update(value).digest('hex')}`;
|
|
141
|
+
const unscoped = sha(full);
|
|
119
142
|
return {
|
|
120
143
|
legacy: full.slice(0, MYSQL_LOCK_NAME_MAX),
|
|
121
|
-
|
|
144
|
+
unscoped,
|
|
145
|
+
current: database === undefined ? unscoped : sha(`${full}@db:${String(database ?? '').toLowerCase()}`),
|
|
122
146
|
};
|
|
123
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* **La transición del nombre sin base** (rotación 2 · R2-01, firmada: «como alpha.5»). En 2.5.x `acquire()` toma el
|
|
150
|
+
* nombre VIEJO (`unscoped`, el de 2.4.0-alpha.5..2.5.0-alpha.4) y DESPUÉS el nuevo (`current`, con la base), y suelta
|
|
151
|
+
* los dos: en un despliegue rodante un proceso de alpha.4 solo conoce el viejo, y los dos tienen que seguir
|
|
152
|
+
* excluyéndose. **El precio, declarado**: mientras se tome el viejo, dos despliegues en bases distintas del mismo
|
|
153
|
+
* servidor SIGUEN compartiéndolo — R2-01 queda abierto en 2.5.x y se cierra al retirarlo en 2.6 (README «Limits»,
|
|
154
|
+
* `relay.mysql-unscoped-lease`; backlog 2.6). Atado a la versión como en alpha.6: `tests/rotacion2_cierre.spec.ts` exige
|
|
155
|
+
* que valga exactamente «`package.json` < `SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN`» (`2.6.0-alpha.0`: toda 2.6, sus
|
|
156
|
+
* prereleases incluidas), así que el bump a 2.6 pone la suite en rojo hasta retirarlo.
|
|
157
|
+
*/
|
|
158
|
+
export const SCOPE_OUTBOX_UNSCOPED_LOCK_RETIRED_IN = '2.6.0-alpha.0';
|
|
159
|
+
export const SCOPE_OUTBOX_TAKES_UNSCOPED_LOCK = true;
|
|
124
160
|
/** Hash de 32 bits con signo (lo que acepta `pg_try_advisory_xact_lock`). */
|
|
125
161
|
function hash32(value) {
|
|
126
162
|
let hash = 0;
|
|
@@ -374,7 +410,6 @@ export function sqlScopeOutbox(options = {}, database = db) {
|
|
|
374
410
|
* pasada, que es el precio de que dos pasadas no se pisen.
|
|
375
411
|
*/
|
|
376
412
|
async acquire() {
|
|
377
|
-
const names = scopeOutboxLockNames(connection, table);
|
|
378
413
|
/** El nombre del cerrojo de PROCESO y del advisory de PG: el completo (allí no hay recorte). */
|
|
379
414
|
const name = `authz_scope_outbox:${connection ?? 'default'}:${table}`;
|
|
380
415
|
const client = lockClientOf(database, connection);
|
|
@@ -397,11 +432,17 @@ export function sqlScopeOutbox(options = {}, database = db) {
|
|
|
397
432
|
return { release: async () => void (await trx.rollback()) };
|
|
398
433
|
}
|
|
399
434
|
if (dialect.startsWith('mysql')) {
|
|
400
|
-
// **
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
//
|
|
404
|
-
//
|
|
435
|
+
// **La base en el nombre** (rotación 2 · R2-01): la lee la MISMA sesión que va a sostener el cerrojo.
|
|
436
|
+
const database = firstValue(await query('acquire', () => trx.rawQuery('select database() as db')), 'db');
|
|
437
|
+
const names = scopeOutboxLockNames(connection, table, database === null || database === undefined ? null : String(database));
|
|
438
|
+
// **Transición** (2.5.x): el VIEJO (sin base) primero y el NUEVO después; si cualquiera falla se sueltan los
|
|
439
|
+
// ya tomados y es `busy`. Un proceso de alpha.4 solo toma el viejo: los dos siguen excluyéndose.
|
|
440
|
+
// (El recortado de ≤ alpha.4 sigue retirado: `SCOPE_OUTBOX_TAKES_LEGACY_LOCK` es `false` desde alpha.7.)
|
|
441
|
+
const order = [
|
|
442
|
+
...(SCOPE_OUTBOX_TAKES_UNSCOPED_LOCK ? [names.unscoped] : []),
|
|
443
|
+
names.current,
|
|
444
|
+
...(SCOPE_OUTBOX_TAKES_LEGACY_LOCK ? [names.legacy] : []),
|
|
445
|
+
];
|
|
405
446
|
const taken = [];
|
|
406
447
|
const take = async (lockName) => {
|
|
407
448
|
const result = await query('acquire', () => trx.rawQuery('select get_lock(?, 0) as taken', [lockName]));
|
|
@@ -410,18 +451,38 @@ export function sqlScopeOutbox(options = {}, database = db) {
|
|
|
410
451
|
taken.push(lockName);
|
|
411
452
|
return ok;
|
|
412
453
|
};
|
|
454
|
+
// `GET_LOCK` es de SESIÓN, no de la transacción: revertir no lo suelta, y una conexión que vuelve al pool
|
|
455
|
+
// con el cerrojo puesto deja la cola `busy` para siempre. Se sueltan TODOS los tomados (en orden inverso)
|
|
456
|
+
// aunque uno falle, y la transacción se revierte al final pase lo que pase.
|
|
457
|
+
let released = false;
|
|
413
458
|
const releaseAll = async () => {
|
|
414
|
-
|
|
415
|
-
|
|
459
|
+
if (released)
|
|
460
|
+
return;
|
|
461
|
+
released = true;
|
|
462
|
+
let failure;
|
|
463
|
+
for (const lockName of [...taken].reverse()) {
|
|
464
|
+
try {
|
|
416
465
|
await trx.rawQuery('select release_lock(?)', [lockName]);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
failure ??= error;
|
|
469
|
+
}
|
|
417
470
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
471
|
+
await trx.rollback();
|
|
472
|
+
if (failure !== undefined)
|
|
473
|
+
throw failure;
|
|
421
474
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
475
|
+
try {
|
|
476
|
+
for (const lockName of order) {
|
|
477
|
+
if (!(await take(lockName))) {
|
|
478
|
+
await releaseAll();
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
await releaseAll().catch(() => { });
|
|
485
|
+
throw error;
|
|
425
486
|
}
|
|
426
487
|
return { release: releaseAll };
|
|
427
488
|
}
|
|
@@ -26,7 +26,7 @@ export interface ExpiryCodec {
|
|
|
26
26
|
bind(at: Date): unknown;
|
|
27
27
|
/** Expresión de SELECT que devuelve la columna de forma que `fromDb` la lea sin depender del cliente. */
|
|
28
28
|
select(column: string, alias?: string): unknown;
|
|
29
|
-
/** Lo que devuelve el SELECT de `select(...)` → `Date | null
|
|
29
|
+
/** Lo que devuelve el SELECT de `select(...)` → `Date | null` (`null` = columna NULL); presente e ilegible LANZA (RR-2). */
|
|
30
30
|
fromDb(value: unknown): Date | null;
|
|
31
31
|
}
|
|
32
32
|
/** ¿Es MySQL? (el mismo conjunto que decide el codec). */
|
|
@@ -44,8 +44,12 @@ export declare function dialectOf(connection: any): string;
|
|
|
44
44
|
export declare function isSqliteDialect(connection: any): boolean;
|
|
45
45
|
/** `2030-06-15T12:00:00.000Z` → `2030-06-15 12:00:00.000` (UTC, con milisegundos). */
|
|
46
46
|
export declare function formatUtcDatetime(at: Date): string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`. Lo que no es una fecha REAL lanza
|
|
49
|
+
* `unreadableExpiry` (RR-2): la fecha cero, un mes 13, un 31 de febrero o un `24:00:00` (que `Date` desbordaría en
|
|
50
|
+
* silencio al día o al mes siguiente) no se leen como nada.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseUtcDatetime(value: string): Date;
|
|
49
53
|
/**
|
|
50
54
|
* El codec para una conexión de Lucid (default: la primaria). Se decide por
|
|
51
55
|
* dialecto una vez por driver; MySQL es el único que necesita algo distinto
|
|
@@ -26,20 +26,54 @@ export function isSqliteDialect(connection) {
|
|
|
26
26
|
export function formatUtcDatetime(at) {
|
|
27
27
|
return at.toISOString().slice(0, 23).replace('T', ' ');
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Una caducidad PRESENTE que no se puede leer (rotación 3 · cierre 2, RR-2 del re-ataque): es un error, nunca «sin
|
|
31
|
+
* caducidad». Hasta aquí `parseUtcDatetime` y el codec identidad devolvían `null` para una fecha que no lo es —la fecha
|
|
32
|
+
* cero `0000-00-00` que MySQL deja con un `sql_mode` no estricto, un `-infinity` de PostgreSQL, un texto en SQLite—, y
|
|
33
|
+
* `null` es PERMANENTE para quien lo consume: el re-grant la conservaba como «sin caducidad», `reconcile --to=openfga`
|
|
34
|
+
* y el export de `relations:reconcile` escribían una tupla permanente para una fila que en SQL ya no concede. Ahora es
|
|
35
|
+
* 500 `E_AUTHZ_INTERNAL` en la lectura: lo que lee la caducidad para reescribirla o migrarla se para antes de escribir
|
|
36
|
+
* (`reconcile --to=database` incluido, que antes la reescribía con la del origen: E1, declarado en «Limits»). La DECISIÓN
|
|
37
|
+
* de SQL (`expires_at > now`) NO pasa por aquí y no es fail-closed para todo valor (re-ataque 2 · C1, corregido): no
|
|
38
|
+
* concede con la fecha cero de MySQL ni con `-infinity`, pero SÍ concede —para siempre— con `+infinity` en PostgreSQL y
|
|
39
|
+
* con un TEXTO en SQLite (`TEXT > INTEGER` es verdadero), aunque sea una fecha pasada. El paquete nunca escribe esos
|
|
40
|
+
* valores; está declarado en «Limits» con su receta (revoke + grant) y el arreglo de fondo va a 2.6. `null`/`undefined` siguen siendo «sin
|
|
41
|
+
* caducidad»: es lo que guarda una columna NULL.
|
|
42
|
+
*/
|
|
43
|
+
function unreadableExpiry(value) {
|
|
44
|
+
const shown = value instanceof Date ? 'Invalid Date' : JSON.stringify(typeof value === 'string' ? value.slice(0, 40) : String(value));
|
|
45
|
+
return new AuthorizationInternalError(`expires_at ilegible (${shown}): una caducidad presente que no es un instante es un error, nunca «sin caducidad». ` +
|
|
46
|
+
'Corrige la fila (o bórrala) antes de releerla, migrarla o reescribirla.');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`. Lo que no es una fecha REAL lanza
|
|
50
|
+
* `unreadableExpiry` (RR-2): la fecha cero, un mes 13, un 31 de febrero o un `24:00:00` (que `Date` desbordaría en
|
|
51
|
+
* silencio al día o al mes siguiente) no se leen como nada.
|
|
52
|
+
*/
|
|
30
53
|
export function parseUtcDatetime(value) {
|
|
31
|
-
const match = /^(\d{4}
|
|
54
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?$/.exec(value.trim());
|
|
32
55
|
if (!match)
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
56
|
+
throw unreadableExpiry(value);
|
|
57
|
+
const [, year, month, day, hour, minute, second, fraction] = match;
|
|
58
|
+
const wall = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
|
|
59
|
+
const date = new Date(`${wall}.${(fraction ?? '').padEnd(3, '0').slice(0, 3)}Z`);
|
|
60
|
+
// Ida y vuelta: `Date` acepta `2030-02-31` (⇒ 3 de marzo) y `24:00:00` (⇒ día siguiente); una fecha real vuelve igual.
|
|
61
|
+
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 19) !== wall)
|
|
62
|
+
throw unreadableExpiry(value);
|
|
63
|
+
return date;
|
|
37
64
|
}
|
|
38
65
|
const identityCodec = {
|
|
39
66
|
toDb: (expiresAt) => expiresAt,
|
|
40
67
|
bind: (at) => at,
|
|
41
68
|
select: (column, alias) => (alias && alias !== column ? `${column} as ${alias}` : column),
|
|
42
|
-
|
|
69
|
+
// RR-2: `toExpiryDate` da `null` para lo que no es fecha (y deja pasar un `Date` inválido); aquí eso es un error.
|
|
70
|
+
fromDb: (value) => {
|
|
71
|
+
const date = toExpiryDate(value);
|
|
72
|
+
if (date === null ? !(value === null || value === undefined || value === '') : Number.isNaN(date.getTime())) {
|
|
73
|
+
throw unreadableExpiry(value);
|
|
74
|
+
}
|
|
75
|
+
return date;
|
|
76
|
+
},
|
|
43
77
|
};
|
|
44
78
|
function mysqlCodec(connection) {
|
|
45
79
|
const raw = (sql, bindings) => typeof connection?.raw === 'function' ? connection.raw(sql, bindings) : db.raw(sql, bindings);
|
|
@@ -37,6 +37,10 @@ import type { ContractScopeTree } from './scope_tree.js';
|
|
|
37
37
|
* `truncationSignal: false` (con `exhaustiveLists`) y `hierarchyFacts: false`;
|
|
38
38
|
* los demás llegan con su fase, y mientras tanto declararlos `true` hace que
|
|
39
39
|
* la suite lance al registrarse: una promesa sin juez no pasa.
|
|
40
|
+
*
|
|
41
|
+
* Las claves OPCIONALES (`?:`, regla D15) se pueden omitir: entonces la cara
|
|
42
|
+
* la elige la declaración normalizada del DRIVER (`isOmittedOptionalCapability`);
|
|
43
|
+
* declararlas fija la cara y el juez exige que el driver diga lo mismo.
|
|
40
44
|
*/
|
|
41
45
|
export interface DriverCapabilities {
|
|
42
46
|
/** El driver materializa el árbol como hechos propios (Fase 3b). */
|
|
@@ -132,8 +136,10 @@ export interface DriverCapabilities {
|
|
|
132
136
|
* y MySQL, y es la promesa que el README hace.
|
|
133
137
|
*
|
|
134
138
|
* Con `false` (SQLite, que serializa bloqueando la BASE ENTERA) el juez
|
|
135
|
-
* solo puede exigir lo innegociable: nunca dos ganadores
|
|
136
|
-
*
|
|
139
|
+
* solo puede exigir lo innegociable: nunca dos ganadores con el MISMO owner
|
|
140
|
+
* (el del ancestro puede ensombrecer al del descendiente: los dos finales
|
|
141
|
+
* legales de 3G · Y1, también en esta cara desde la rotación 2) y el
|
|
142
|
+
* perdedor no escribe — su transacción puede morir con `SQLITE_BUSY`, que es un 503
|
|
137
143
|
* legítimo. Declararlo `true` en un motor así convierte un flake en un
|
|
138
144
|
* fallo; declararlo `false` en PG/MySQL deja pasar un mutante que
|
|
139
145
|
* convierte la colisión en 503. Solo tiene casos en `level: '2.2'`.
|
|
@@ -202,12 +208,39 @@ export interface DriverCapabilities {
|
|
|
202
208
|
enumerateFacts: boolean;
|
|
203
209
|
/**
|
|
204
210
|
* **El driver es EXPERIMENTAL** (estrategia 2.5.0, firma 4; `AuthorizationDriverCapabilities.experimental`). Opcional
|
|
205
|
-
* (regla D15:
|
|
206
|
-
*
|
|
211
|
+
* (regla D15): si el harness la OMITE, el juez juzga la cara que el DRIVER declara (su declaración normalizada; ensayo
|
|
212
|
+
* rc.1 de COGNITIV) y un harness publicado sin la clave sigue valiendo; si la declara, manda el harness. Con `true` el
|
|
213
|
+
* juez fija que el driver lo declara y que `authorization.capabilities()` lo publica; con `false`, que el driver NO lo
|
|
207
214
|
* declara y la copia normalizada dice `false`. No apaga ningún caso. `openfga` = `true` en 2.5.x; `database` = `false`.
|
|
208
215
|
*/
|
|
209
216
|
experimental?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* **Toda decisión que combina concesión y deny se lee en UNA foto** (rotación 2 · cierre 3, L12;
|
|
219
|
+
* `AuthorizationDriverCapabilities.snapshotDecisions`). Opcional (regla D15): si el harness la OMITE, el juez juzga la
|
|
220
|
+
* cara que el DRIVER declara —el `database` del paquete declara `true`, así que un harness de alpha.4 que lo registra
|
|
221
|
+
* sin la clave se juzga en la cara `true` (ensayo rc.1 de COGNITIV)—; si la declara, manda el harness y contradecir al
|
|
222
|
+
* driver es rojo. Solo tiene casos en `level: '2.1'` o superior (necesita `listDenies`). Con `true`
|
|
223
|
+
* el juez fija que el driver trae `readSnapshot`, que cada lectura pedida es EXACTAMENTE la del método homónimo (y lo
|
|
224
|
+
* no pedido no viene), que una pregunta mal formada es 422, y que el manager compone `effectivePermissions`,
|
|
225
|
+
* `authorizedScopes` y la policy de `defineScopedRole` con UNA llamada a `readSnapshot` y ninguna a
|
|
226
|
+
* `rolesInChain`/`listScopes`/`listDenies` (el espía cuenta solo las llamadas del MANAGER: lo que `readSnapshot` llame
|
|
227
|
+
* por dentro no cuenta). Con `false`, que el manager NO llama a `readSnapshot` —aunque el driver traiga un método con
|
|
228
|
+
* ese nombre: decide la capacidad— y compone como antes restando los denies (el hueco declarado de un driver sin la
|
|
229
|
+
* capacidad); sin `listDenies` la cara se queda en la declaración y su título lo dice. Que la foto sea de verdad UNA no lo puede
|
|
230
|
+
* observar un juez genérico sin meterse en el backend: en `database` lo fijan los casos de entrelazado forzado y la
|
|
231
|
+
* guardia `database.decision.snapshot` del paquete.
|
|
232
|
+
*/
|
|
233
|
+
snapshotDecisions?: boolean;
|
|
210
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* **Clave D15 OMITIDA ⇒ la declaración del driver** (ensayo rc.1 de COGNITIV, día 1, hallazgo 1; firma del dueño
|
|
237
|
+
* 2026-10-23). Una clave opcional que el harness no declara (`undefined`) no es `false`: el juez la toma de la
|
|
238
|
+
* declaración NORMALIZADA del driver (`normalizeAuthorizationCapabilities`) y juzga ESA cara. Si el harness la declara,
|
|
239
|
+
* manda el harness (y contradecir al driver sigue siendo rojo). Un harness publicado antes de que la clave existiera no
|
|
240
|
+
* puede declararla: tratarla como `false` ponía rojo a un harness verde contra un driver que la declara `true` (el propio
|
|
241
|
+
* `database` con `snapshotDecisions`), que es lo que D15 prohíbe.
|
|
242
|
+
*/
|
|
243
|
+
export declare function isOmittedOptionalCapability(capabilities: DriverCapabilities, capability: keyof DriverCapabilities): boolean;
|
|
211
244
|
export type ContractLevel = 'core' | '2.0' | '2.1' | '2.2';
|
|
212
245
|
export interface DriverContractHarness {
|
|
213
246
|
name: string;
|