@jantstack/adonis-authz 2.4.0-alpha.9 → 2.5.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 +296 -15
- package/build/commands/authz_freeze.js +1 -1
- package/build/commands/authz_freeze.js.map +1 -1
- package/build/commands/authz_reconcile.d.ts +6 -2
- package/build/commands/authz_reconcile.d.ts.map +1 -1
- package/build/commands/authz_reconcile.js +129 -13
- package/build/commands/authz_reconcile.js.map +1 -1
- package/build/commands/authz_relations_reconcile.d.ts +66 -19
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +212 -39
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/commands/authz_scopes_relay.d.ts.map +1 -1
- package/build/commands/authz_scopes_relay.js +81 -1
- package/build/commands/authz_scopes_relay.js.map +1 -1
- package/build/commands/authz_unfreeze.d.ts +12 -4
- package/build/commands/authz_unfreeze.d.ts.map +1 -1
- package/build/commands/authz_unfreeze.js +74 -9
- package/build/commands/authz_unfreeze.js.map +1 -1
- package/build/index.d.ts +6 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +1 -1
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +2 -1
- package/build/providers/authz_provider.js.map +1 -1
- package/build/src/define_config.d.ts +9 -0
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js +11 -0
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +1 -0
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +18 -6
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +91 -44
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +16 -1
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +66 -20
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +16 -4
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +71 -34
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +50 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +50 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +63 -4
- package/build/src/freeze.d.ts.map +1 -1
- package/build/src/freeze.js +107 -18
- package/build/src/freeze.js.map +1 -1
- package/build/src/identity.d.ts +120 -9
- package/build/src/identity.d.ts.map +1 -1
- package/build/src/identity.js +164 -12
- package/build/src/identity.js.map +1 -1
- package/build/src/manager.d.ts +64 -2
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +262 -35
- package/build/src/manager.js.map +1 -1
- package/build/src/openfga.d.ts +1 -1
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +1 -1
- package/build/src/openfga.js.map +1 -1
- package/build/src/relations/check_many.d.ts +17 -0
- package/build/src/relations/check_many.d.ts.map +1 -0
- package/build/src/relations/check_many.js +57 -0
- package/build/src/relations/check_many.js.map +1 -0
- package/build/src/relations/manager.d.ts +20 -0
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +84 -13
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +67 -0
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +123 -30
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/relations/tuple_key.d.ts +25 -0
- package/build/src/relations/tuple_key.d.ts.map +1 -0
- package/build/src/relations/tuple_key.js +60 -0
- package/build/src/relations/tuple_key.js.map +1 -0
- package/build/src/shared/backend_guard.d.ts +30 -11
- package/build/src/shared/backend_guard.d.ts.map +1 -1
- package/build/src/shared/backend_guard.js +45 -15
- package/build/src/shared/backend_guard.js.map +1 -1
- package/build/src/shared/sql_expiry.d.ts +2 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -1
- package/build/src/shared/sql_expiry.js +4 -0
- package/build/src/shared/sql_expiry.js.map +1 -1
- package/build/src/testing/main.d.ts +3 -1
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +2 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +9 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -1
- package/build/src/testing/migration_contract.js +5 -2
- package/build/src/testing/migration_contract.js.map +1 -1
- package/build/src/testing/relations_contract.d.ts +14 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -1
- package/build/src/testing/relations_contract.js +297 -21
- package/build/src/testing/relations_contract.js.map +1 -1
- package/build/src/testing/test_clock.d.ts +15 -0
- package/build/src/testing/test_clock.d.ts.map +1 -0
- package/build/src/testing/test_clock.js +65 -0
- package/build/src/testing/test_clock.js.map +1 -0
- package/build/src/types.d.ts +108 -9
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js.map +1 -1
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
|
package/build/src/freeze.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import db from '@adonisjs/lucid/services/db';
|
|
2
2
|
import { guardSql, withDeadline } from './shared/backend_guard.js';
|
|
3
|
-
import { AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError } from './errors.js';
|
|
3
|
+
import { AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, UnsupportedDialectError, } from './errors.js';
|
|
4
4
|
import { CATALOG_VERSION_TABLE } from './catalog/catalog_cache.js';
|
|
5
|
+
import { dialectOf, isMysqlDialect, isSqliteDialect } from './shared/sql_expiry.js';
|
|
5
6
|
/**
|
|
6
7
|
* El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
|
|
7
8
|
* B + E-analista). Primitivas SQL puras; la policy (quién congela, el
|
|
@@ -134,21 +135,64 @@ export async function assertNotFrozenRow(operation, options) {
|
|
|
134
135
|
throw new AuthorizationFrozenError(`${operation}: el motor de autorización está congelado (${row.reason})${lift} y no acepta escrituras. ` +
|
|
135
136
|
`Las lecturas siguen funcionando; reintenta esta escritura cuando la ventana termine.`);
|
|
136
137
|
}
|
|
138
|
+
/** El holder con el que se publica una fila con motivo y SIN holder (legada o a mano); también es su token. */
|
|
139
|
+
export const UNKNOWN_HOLDER = '?';
|
|
140
|
+
/** Prefijo del holder de la ventana del OPERADOR (`freeze()` con `kind: 'operator'`). */
|
|
141
|
+
export const OPERATOR_HOLDER_PREFIX = 'operator:';
|
|
142
|
+
/**
|
|
143
|
+
* `isOperatorHolder` en SQL, por motor (cierre-2 de alpha.10 · C3): los
|
|
144
|
+
* `OPERATOR_HOLDER_PREFIX.length` primeros CARACTERES de `freeze_holder` igual a
|
|
145
|
+
* `operator:`, sensible a mayúsculas y acentos. SQLite: `substr()` no hereda
|
|
146
|
+
* collation ⇒ `BINARY`. PostgreSQL: `COLLATE "C"` (byte a byte aunque la base
|
|
147
|
+
* declare otra). MySQL: la collation de la columna es la de la tabla (`_ai_ci` por
|
|
148
|
+
* defecto: `OPERATOR:` y `opérator:` serían iguales), así que se compara como
|
|
149
|
+
* BINARIO. El prefijo es ASCII, así que «9 caracteres» y «9 unidades UTF-16» (JS)
|
|
150
|
+
* coinciden siempre que la comparación sea verdadera. Sin `LIKE`: ni comodines ni
|
|
151
|
+
* collation. Cualquier otro dialecto ⇒ 500 `E_AUTHZ_UNSUPPORTED_DIALECT` ANTES de
|
|
152
|
+
* tocar la fila (re-ataque del cierre-2 · ⚪ 3: el `substr` de SQLite en MSSQL
|
|
153
|
+
* sería un error del motor envuelto como 503, que invita a reintentar para siempre).
|
|
154
|
+
*/
|
|
155
|
+
export function operatorHolderSql(connection = db.connection()) {
|
|
156
|
+
const length = OPERATOR_HOLDER_PREFIX.length;
|
|
157
|
+
if (isMysqlDialect(connection)) {
|
|
158
|
+
return { sql: `CAST(SUBSTRING(freeze_holder, 1, ${length}) AS BINARY) = CAST(? AS BINARY)`, bindings: [OPERATOR_HOLDER_PREFIX] };
|
|
159
|
+
}
|
|
160
|
+
if (dialectOf(connection).startsWith('postgres')) {
|
|
161
|
+
return { sql: `substr(freeze_holder, 1, ${length}) COLLATE "C" = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
|
|
162
|
+
}
|
|
163
|
+
if (isSqliteDialect(connection)) {
|
|
164
|
+
return { sql: `substr(freeze_holder, 1, ${length}) = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
|
|
165
|
+
}
|
|
166
|
+
throw new UnsupportedDialectError(`freeze: dialecto '${dialectOf(connection)}' sin observación en la suite (hoy: PostgreSQL, MySQL 8 y SQLite)`);
|
|
167
|
+
}
|
|
137
168
|
/**
|
|
138
169
|
* Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
|
|
139
|
-
* `acquire` a la vez acaban en exactamente uno).
|
|
140
|
-
*
|
|
141
|
-
*
|
|
170
|
+
* `acquire` a la vez acaban en exactamente uno). Una fila con el lease VENCIDO
|
|
171
|
+
* se toma encima (el dueño murió: la flota vuelve sola) **salvo que sea una
|
|
172
|
+
* ventana de OPERADOR** (cierre del auditor de alpha.10 · R2): esa exclusión va
|
|
173
|
+
* DENTRO del `WHERE` del mismo `UPDATE`, en los tres motores. Hasta el cierre
|
|
174
|
+
* vivía en `freeze()` como «leo la fila, compruebo, tomo», y con un reloj que
|
|
175
|
+
* cruzaba el vencimiento entre la comprobación y el UPDATE la pasada pisaba la
|
|
176
|
+
* ventana del operador sin 503 — el TOCTOU que el atómico existe para cerrar.
|
|
142
177
|
*/
|
|
143
|
-
export async function
|
|
178
|
+
export async function acquireFreezeOutcome(input, options = {}, attempt = 0) {
|
|
144
179
|
const { driver, timeoutMs } = opts(options);
|
|
180
|
+
// Fuera de `freezeSql`: un dialecto no soportado es config (500), no un backend caído (503).
|
|
181
|
+
const operator = operatorHolderSql();
|
|
145
182
|
const updated = await freezeSql(driver, 'freeze.acquire', timeoutMs, () => db
|
|
146
183
|
.from(CATALOG_VERSION_TABLE)
|
|
147
184
|
.where('id', FREEZE_ROW_ID)
|
|
148
185
|
.where((q) => {
|
|
149
|
-
// Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma)
|
|
186
|
+
// Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma)…
|
|
150
187
|
q.whereNull('freeze_reason').orWhere((expired) => {
|
|
151
|
-
expired
|
|
188
|
+
expired
|
|
189
|
+
.whereNotNull('freeze_until_ms')
|
|
190
|
+
.andWhere('freeze_until_ms', '<=', input.nowMs)
|
|
191
|
+
// …salvo la ventana del OPERADOR vencida: es evidencia, no espacio libre (R2).
|
|
192
|
+
.andWhere((notOperator) => {
|
|
193
|
+
// La regla ÚNICA (`isOperatorHolder`), sin `LIKE`: comparación EXACTA de prefijo (cierre-2 · C3).
|
|
194
|
+
notOperator.whereNull('freeze_holder').orWhereRaw(`NOT (${operator.sql})`, operator.bindings);
|
|
195
|
+
});
|
|
152
196
|
});
|
|
153
197
|
})
|
|
154
198
|
.update({
|
|
@@ -159,17 +203,36 @@ export async function acquireFreeze(input, options = {}) {
|
|
|
159
203
|
updated_at: new Date(input.nowMs),
|
|
160
204
|
}));
|
|
161
205
|
if (Number(updated) === 0) {
|
|
162
|
-
// O hay un freeze vivo, o la fila no existe: distinguirlo es leerla.
|
|
206
|
+
// O hay un freeze vivo, o una ventana de operador vencida, o la fila no existe: distinguirlo es leerla.
|
|
163
207
|
const row = await readFreezeRow(options);
|
|
164
208
|
if (freezeIsLive(row, input.nowMs))
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
209
|
+
return { status: 'held', row };
|
|
210
|
+
if (row.reason !== null && freezeKindOf(row.holder) === 'operator')
|
|
211
|
+
return { status: 'expired-operator', row };
|
|
212
|
+
// Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta UNA vez.
|
|
213
|
+
// Cierre-2 · C3: tras el reintento, una fila que NO está viva no es `held` (su
|
|
214
|
+
// letra dice «VIVO»). Con la regla única del prefijo esta rama solo la alcanza
|
|
215
|
+
// una fila que cambió dos veces entre UPDATE y SELECT: 503 reintentable que lo dice.
|
|
216
|
+
if (attempt >= 1) {
|
|
217
|
+
const error = new AuthorizationBackendError(driver, 'freeze.acquire', new Error('la fila del freeze cambió entre el UPDATE condicional y su relectura dos veces seguidas'));
|
|
218
|
+
error.message =
|
|
219
|
+
`${driver}: no se pudo tomar el freeze: la fila (${row.holder ?? 'sin holder'}, fence ${row.fence}) NO está viva ` +
|
|
220
|
+
`y aun así el UPDATE condicional no casó dos veces seguidas — otro proceso la está cambiando a la vez ` +
|
|
221
|
+
`(rama «contención tras reintento», no «freeze VIVO»). Reintenta.`;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return acquireFreezeOutcome(input, options, attempt + 1);
|
|
168
225
|
}
|
|
169
226
|
const row = await readFreezeRow(options);
|
|
227
|
+
// Otro acquire ganó justo después (no debería: el nuestro dejó la fila viva).
|
|
170
228
|
if (row.holder !== input.holder)
|
|
171
|
-
return
|
|
172
|
-
return { fence: row.fence, holder: input.holder };
|
|
229
|
+
return { status: 'held', row };
|
|
230
|
+
return { status: 'acquired', token: { fence: row.fence, holder: input.holder } };
|
|
231
|
+
}
|
|
232
|
+
/** `acquireFreezeOutcome` reducido a «el token o `null`» (lo usan los casos que siembran una fila ajena). */
|
|
233
|
+
export async function acquireFreeze(input, options = {}) {
|
|
234
|
+
const outcome = await acquireFreezeOutcome(input, options);
|
|
235
|
+
return outcome.status === 'acquired' ? outcome.token : null;
|
|
173
236
|
}
|
|
174
237
|
/**
|
|
175
238
|
* Renueva el lease, CONDICIONAL por token y solo si aún no venció
|
|
@@ -197,7 +260,16 @@ export async function renewFreeze(token, input, options = {}) {
|
|
|
197
260
|
export async function releaseFreeze(token, input, options = {}) {
|
|
198
261
|
const { driver, timeoutMs } = opts(options);
|
|
199
262
|
const row = await readFreezeRow(options);
|
|
200
|
-
|
|
263
|
+
// La fila SIN holder (legada o escrita a mano) se publica como `UNKNOWN_HOLDER`
|
|
264
|
+
// en `freezeStatus()`/`lastFreezeStatus()`, así que ese es su token: sin esto
|
|
265
|
+
// `authz:unfreeze` no casaba nunca y repetía su aviso para siempre (cierre del
|
|
266
|
+
// auditor de alpha.10 · ⚪ 5). `freeze()` jamás escribe ese holder.
|
|
267
|
+
// Cierre-2 · C4: `'?'` publica DOS filas —holder NULL y holder LITERAL `'?'` (a
|
|
268
|
+
// mano)—, así que el token casa las dos (`IS NULL OR = '?'`); casar solo NULL
|
|
269
|
+
// dejaba la literal sin salida y `authz:unfreeze` en bucle con exit 1. Sigue
|
|
270
|
+
// atado al fence y nunca casa un holder real.
|
|
271
|
+
const unknownHolder = token.holder === UNKNOWN_HOLDER;
|
|
272
|
+
const mine = row.fence === token.fence && (unknownHolder ? row.holder === null || row.holder === UNKNOWN_HOLDER : row.holder === token.holder);
|
|
201
273
|
const lapsed = !mine || (row.untilMs !== null && row.untilMs <= input.nowMs);
|
|
202
274
|
if (!mine)
|
|
203
275
|
return { released: false, lapsed };
|
|
@@ -205,13 +277,30 @@ export async function releaseFreeze(token, input, options = {}) {
|
|
|
205
277
|
.from(CATALOG_VERSION_TABLE)
|
|
206
278
|
.where('id', FREEZE_ROW_ID)
|
|
207
279
|
.where('freeze_fence', token.fence)
|
|
208
|
-
.where('freeze_holder', token.holder)
|
|
280
|
+
.where((q) => unknownHolder ? q.whereNull('freeze_holder').orWhere('freeze_holder', UNKNOWN_HOLDER) : q.where('freeze_holder', token.holder))
|
|
209
281
|
.update({ freeze_reason: null, freeze_holder: null, freeze_until_ms: null, updated_at: new Date(input.nowMs) }));
|
|
210
282
|
return { released: Number(updated) > 0, lapsed };
|
|
211
283
|
}
|
|
212
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* **La regla ÚNICA del prefijo de operador** (cierre-2 de alpha.10 · C3): los
|
|
286
|
+
* `OPERATOR_HOLDER_PREFIX.length` primeros caracteres del holder son EXACTAMENTE
|
|
287
|
+
* `operator:` — sensible a mayúsculas y a acentos, sin comodines. La MISMA
|
|
288
|
+
* comparación va en el `WHERE` de `acquireFreezeOutcome` (`operatorHolderSql`) y
|
|
289
|
+
* aquí; hasta el cierre-2 el SQL usaba `LIKE` (insensible a mayúsculas en SQLite y
|
|
290
|
+
* en MySQL `_ci`) y JS partía por `:`, así que un holder `OPERATOR:…` caducado era
|
|
291
|
+
* «de operador» para el UPDATE y `unknown` para JS ⇒ 423 «VIVO» sobre una fila
|
|
292
|
+
* caducada, distinto por motor.
|
|
293
|
+
*/
|
|
294
|
+
export function isOperatorHolder(holder) {
|
|
295
|
+
return typeof holder === 'string' && holder.slice(0, OPERATOR_HOLDER_PREFIX.length) === OPERATOR_HOLDER_PREFIX;
|
|
296
|
+
}
|
|
297
|
+
/** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
|
|
213
298
|
export function freezeKindOf(holder) {
|
|
214
|
-
|
|
215
|
-
|
|
299
|
+
if (isOperatorHolder(holder))
|
|
300
|
+
return 'operator';
|
|
301
|
+
if (typeof holder !== 'string' || !holder.includes(':'))
|
|
302
|
+
return 'unknown';
|
|
303
|
+
const prefix = holder.slice(0, holder.indexOf(':'));
|
|
304
|
+
return prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
|
|
216
305
|
}
|
|
217
306
|
//# sourceMappingURL=freeze.js.map
|
package/build/src/freeze.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,6BAA6B,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,
|
|
1
|
+
{"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
|
package/build/src/identity.d.ts
CHANGED
|
@@ -97,6 +97,36 @@ export declare function isValidRelationId(value: unknown): value is string;
|
|
|
97
97
|
* 422 aquí, antes de serializarse a nada.
|
|
98
98
|
*/
|
|
99
99
|
export declare function scopeKey(scope: ScopeRef): string;
|
|
100
|
+
/**
|
|
101
|
+
* **Las formas que PostgreSQL funde con `uuid` — y solo esas** (cierre-3 del
|
|
102
|
+
* lote A, decisión del dueño 2026-10-15 (3)): dado el uuid de un scope tal
|
|
103
|
+
* como lo escribió el llamante, la ortografía CANÓNICA (8-4-4-4-12) con la
|
|
104
|
+
* que un motor pudo fundirlo, o `[]` si no es alias de nada (ya es canónica,
|
|
105
|
+
* o no es un uuid, o es una forma que ningún motor del README funde).
|
|
106
|
+
*
|
|
107
|
+
* Es la regla del FAN-OUT de roles (`scopeSpellings`: la purga sin cadena
|
|
108
|
+
* de `scopes.detached`, `revoke`/`removeDeny`), y es a propósito MÁS
|
|
109
|
+
* ESTRECHA que `isNonCanonicalUuid`, la del 422 de las particiones de
|
|
110
|
+
* relaciones: son DOS propósitos. Rechazar de más (422 por `-<32hex>`) es
|
|
111
|
+
* inocuo, el llamante lo ve y lo corrige; purgar de más NO lo es, porque la
|
|
112
|
+
* canónica que se añade puede ser un scope VIVO de otro tenant en un
|
|
113
|
+
* consumidor con ids de texto elegibles por el usuario (🟡 C2-1/C2-2 del
|
|
114
|
+
* re-ataque al cierre-2: hasta el cierre-3 `scopeSpellings` usaba la ancha y
|
|
115
|
+
* `a0e-<hex de la víctima>` arrastraba a la víctima a la purga). Expandir
|
|
116
|
+
* una forma que ningún motor funde es coste sin beneficio: nunca hay hechos
|
|
117
|
+
* canónicos que sean «suyos».
|
|
118
|
+
*
|
|
119
|
+
* **Formas de más de 36 caracteres** (⚪ C2-4): PG funde también `4×8`
|
|
120
|
+
* (39 caracteres, el ejemplo literal de su doc), pero `IDENTITY_LIMITS.uuid`
|
|
121
|
+
* es 36 —la columna publicada— y `assertScope` la rechaza ANTES de llegar
|
|
122
|
+
* aquí con 422 `E_AUTHZ_INVALID_IDENTITY`: ruidoso y visible para el
|
|
123
|
+
* consumidor, nunca una purga a medias en silencio. El conjunto de este
|
|
124
|
+
* fan-out es, por tanto, «lo que PG funde Y cabe en 36».
|
|
125
|
+
*
|
|
126
|
+
* La inversa NO existe: dada la canónica no se enumeran sus alias (serían
|
|
127
|
+
* 2^7 ortografías); el fan-out va siempre de alias ⇒ canónica.
|
|
128
|
+
*/
|
|
129
|
+
export declare function pgFusedSpellings(uuid: string): string[];
|
|
100
130
|
/**
|
|
101
131
|
* **Las ORTOGRAFÍAS de un scope con las que hay que limpiar cuando ya no hay
|
|
102
132
|
* cadena con la que canonizar** (3b-2h · 🟠 3, auditor R2).
|
|
@@ -114,17 +144,98 @@ export declare function scopeKey(scope: ScopeRef): string;
|
|
|
114
144
|
* fail-closed; en escritura era fail-OPEN.
|
|
115
145
|
*
|
|
116
146
|
* 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.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
147
|
+
* llamante **y** la canónica de la que puede ser alias. **La regla de «¿de
|
|
148
|
+
* qué canónica puede ser alias?» es `pgFusedSpellings`**: exactamente lo que
|
|
149
|
+
* el tipo `uuid` de PostgreSQL funde (sin guiones, `8-4-4-16`, `8-8-8-8`,
|
|
150
|
+
* `12-4-4-12`…, medido), ni una forma más. Hasta el cierre-2 del lote A solo
|
|
151
|
+
* se expandía la forma SIN guiones y `8-4-4-16` dejaba la canónica viva
|
|
152
|
+
* (🟠 R1); el cierre-2 pasó a `isNonCanonicalUuid`, que es MÁS ANCHA que lo
|
|
153
|
+
* que PG funde (`-<32hex>`, `<32hex>-`, `a0e-ebc99…`, `--`: ids legales de
|
|
154
|
+
* la gramática que PG rechaza), y eso ampliaba el alcance de una purga sin
|
|
155
|
+
* cadena a scopes VIVOS ajenos (🟡 C2-1); el cierre-3 separa las dos reglas
|
|
156
|
+
* por propósito. El alias por mayúsculas ya muere en la puerta
|
|
157
|
+
* (`assertScope`). Así el consumidor que guarda sus uuids canónicos (el caso
|
|
158
|
+
* normal) no paga NADA, y el que los guarda de otra forma sigue purgando su
|
|
159
|
+
* clave y, además, la canónica. Normalizar en vez de expandir no vale: sin
|
|
160
|
+
* la fila no se sabe cuál de las dos es la buena, y elegir mal deja los
|
|
161
|
+
* hechos vivos, que es el fallo que se está cerrando.
|
|
162
|
+
*
|
|
163
|
+
* Esta función es PURA (no mira el árbol). Quien la consume es UN solo punto
|
|
164
|
+
* de paso, `canonicalScopeTargets` (`backend_guard.ts`; lo usan
|
|
165
|
+
* `scopes.detached` sin cadena en el manager y el fan-out de
|
|
166
|
+
* `revoke`/`removeDeny` en los dos drivers, 3b-8 · A4), que además DESCARTA
|
|
167
|
+
* toda ortografía que el resolutor reconozca como scope VIVO distinto del
|
|
168
|
+
* pedido (cierre-3 · 🟡 C2-2/C2-3): nunca se purga un scope vivo que no es
|
|
169
|
+
* el que se pidió.
|
|
126
170
|
*/
|
|
127
171
|
export declare function scopeSpellings(scope: ScopeRef): ScopeRef[];
|
|
172
|
+
/** ¿Es un uuid canónico escrito SIN guiones (32 hex en minúsculas)? */
|
|
173
|
+
export declare function isDashlessUuid(value: unknown): value is string;
|
|
174
|
+
/** La forma 8-4-4-4-12 de un uuid de 32 hex (la ortografía CANÓNICA). */
|
|
175
|
+
export declare function dashedUuid(dashless: string): string;
|
|
176
|
+
/**
|
|
177
|
+
* **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
|
|
178
|
+
* auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
|
|
179
|
+
* guiones, sea 32 hex y NO esté exactamente en la forma 8-4-4-4-12. Cubre el
|
|
180
|
+
* 32 hex sin guiones (lo único que D4 cerraba) Y cualquier otra colocación de
|
|
181
|
+
* guiones —`8-4-4-16`, cuatro grupos, la canónica sin su último guion…—.
|
|
182
|
+
*
|
|
183
|
+
* **Es la regla ANCHA, y se usa SOLO para el 422 de las particiones de
|
|
184
|
+
* relaciones** (`assertPartition`; cierre-3 del lote A, decisión del dueño
|
|
185
|
+
* 2026-10-15 (3)): es un SUPERCONJUNTO de lo que el tipo `uuid` de
|
|
186
|
+
* PostgreSQL funde —incluye `-<32hex>`, `<32hex>-`, `a0e-ebc99…` o `8--24`,
|
|
187
|
+
* que PG rechaza con «invalid input syntax» (medido, PG 18)— y ahí ser más
|
|
188
|
+
* ancha es inocuo: un 422 de más lo ve y lo corrige el llamante, y una
|
|
189
|
+
* partición que «parece un uuid» sin serlo tampoco merece ser una clave
|
|
190
|
+
* distinta de la canónica. Lo que PG funde DE VERDAD es `pgFusedSpellings`,
|
|
191
|
+
* la regla del fan-out de ROLES, donde expandir de más sí cuesta (purgar un
|
|
192
|
+
* scope vivo ajeno). Un valor que sin guiones no sea 32 hex (31, 33, un
|
|
193
|
+
* carácter no hex, `_`/`.`) NO es un uuid y es un id legal.
|
|
194
|
+
* Es la MISMA función que usa el detector de la receta del README, por
|
|
195
|
+
* construcción (el `upgrade_recipe.spec` lo mide: lo que `assertPartition`
|
|
196
|
+
* rechaza, el detector lo lista).
|
|
197
|
+
*/
|
|
198
|
+
export declare function isNonCanonicalUuid(value: unknown): value is string;
|
|
199
|
+
/**
|
|
200
|
+
* **La partición de una relación se escribe en la forma CANÓNICA 8-4-4-4-12,
|
|
201
|
+
* o es 422** (D4, decisión del dueño 2026-10-14; `panel-tenencia-2-juez.md`
|
|
202
|
+
* §5; ampliado el 2026-10-15 a toda ortografía no canónica, 🟠 A2).
|
|
203
|
+
* `assertScope` vale para un scope de ROLES porque ahí la identidad la
|
|
204
|
+
* canoniza la cadena del resolutor (invariante 17) y, donde no hay cadena,
|
|
205
|
+
* `scopeSpellings` barre la ortografía del llamante y la canónica de la que
|
|
206
|
+
* puede ser alias, con SU regla (`pgFusedSpellings`, lo que PG funde de
|
|
207
|
+
* verdad: cierre-3). En RELACIONES no hay cadena con
|
|
208
|
+
* la que canonizar: la clave de partición es un string comparado byte a
|
|
209
|
+
* byte, así que `unit|aaaa…` (32 hex) y `unit|aaaa-…` eran DOS particiones,
|
|
210
|
+
* la purga canónica no veía la otra y, con el 422, las tuplas viejas sin
|
|
211
|
+
* guiones quedaban inalcanzables para siempre (tester H5). Y la clase no era
|
|
212
|
+
* solo «sin guiones»: PostgreSQL funde también `8-4-4-16`, cuatro grupos de
|
|
213
|
+
* ocho, la canónica sin un guion… (🟠 A2) — cualquier colocación de guiones
|
|
214
|
+
* sobre los mismos 32 hex —, así que la regla es «32 hex sin guiones y no
|
|
215
|
+
* exactamente 8-4-4-4-12 ⇒ 422» (`isNonCanonicalUuid`). La regla es una sola
|
|
216
|
+
* y va en un solo sitio: toda operación que recibe una partición —manager,
|
|
217
|
+
* los dos drivers, el doble publicado y `reconcileRelations`— pasa por aquí
|
|
218
|
+
* (o por `partitionKey`, que la llama), y la guardia de superficie
|
|
219
|
+
* (`tests/guard_surface.spec.ts`, columna `partition`) lo sondea en cada
|
|
220
|
+
* operación con las DOS formas (sin guiones y `8-4-4-16`). Las filas escritas
|
|
221
|
+
* antes de 2.5.0 con una ortografía no canónica se detectan, se listan y se
|
|
222
|
+
* BORRAN con la receta del README («Upgrading to 2.5.0», fail-closed por
|
|
223
|
+
* defecto: eran concesiones invisibles a la purga canónica); re-clavarlas es
|
|
224
|
+
* el paso opt-in `--rekey`. La ejecuta `tests/upgrade_recipe.spec.ts` en los
|
|
225
|
+
* tres motores, con las dos ramas.
|
|
226
|
+
*
|
|
227
|
+
* Un id de partición que no sea un uuid sigue valiendo (`[a-z0-9._-]{1,36}`:
|
|
228
|
+
* 32 caracteres no todos hex, 31 o 33 hex…). Los ids de OBJETO y de SUJETO
|
|
229
|
+
* no se rechazan: se comparan byte a byte y el README lo declara.
|
|
230
|
+
*/
|
|
231
|
+
export declare function assertPartition(partition: ScopeRef): void;
|
|
232
|
+
/**
|
|
233
|
+
* La clave de partición de una RELACIÓN: `scopeKey` con la regla de
|
|
234
|
+
* `assertPartition` delante. Es lo que los dos drivers de relaciones (y el
|
|
235
|
+
* doble) usan para componer `partition_key` / el id de objeto del store: no
|
|
236
|
+
* hay camino hacia el backend que no pase por aquí.
|
|
237
|
+
*/
|
|
238
|
+
export declare function partitionKey(partition: ScopeRef): string;
|
|
128
239
|
/**
|
|
129
240
|
* La inversa de `scopeKey` para claves que vienen de la BASE (el owner de un
|
|
130
241
|
* rol): `app` ⇒ la raíz; `<tipo>|<uuid>` ⇒ el scope; cualquier otra forma
|
|
@@ -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;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAA;AAEnC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAEtF;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAOjE;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAIhD;
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"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"}
|
package/build/src/identity.js
CHANGED
|
@@ -199,6 +199,54 @@ export function scopeKey(scope) {
|
|
|
199
199
|
}
|
|
200
200
|
/** Un uuid canónico escrito SIN guiones: 32 dígitos hexadecimales. */
|
|
201
201
|
const DASHLESS_UUID = /^[0-9a-f]{32}$/;
|
|
202
|
+
/**
|
|
203
|
+
* **Lo que el tipo `uuid` de PostgreSQL FUNDE de verdad**: 32 hex con, como
|
|
204
|
+
* mucho, UN guion detrás de cada grupo de cuatro. Es la letra de la doc
|
|
205
|
+
* (postgresql.org/docs/current/datatype-uuid.html, «PostgreSQL also accepts
|
|
206
|
+
* the following alternative forms for input: […] omitting some or all
|
|
207
|
+
* hyphens, adding a hyphen after any group of four digits») y está MEDIDO
|
|
208
|
+
* contra PostgreSQL 18 (`select '<forma>'::uuid`, cierre-3 del lote A):
|
|
209
|
+
* funde la canónica, `32 hex`, `8-4-4-16`, `8-8-8-8`, `12-4-4-12`, `8-24` y
|
|
210
|
+
* `4×8` (39 caracteres); RECHAZA `-<32hex>`, `<32hex>-`, `a0e-ebc99…` (guion
|
|
211
|
+
* tras tres) y `8--24` (dos guiones seguidos) con «invalid input syntax for
|
|
212
|
+
* type uuid». Mayúsculas y llaves también las funde, pero no pasan
|
|
213
|
+
* `assertScope` (minúsculas, `[a-z0-9._-]`).
|
|
214
|
+
*/
|
|
215
|
+
const PG_FUSED_UUID = /^([0-9a-f]{4}-?){7}[0-9a-f]{4}$/;
|
|
216
|
+
/**
|
|
217
|
+
* **Las formas que PostgreSQL funde con `uuid` — y solo esas** (cierre-3 del
|
|
218
|
+
* lote A, decisión del dueño 2026-10-15 (3)): dado el uuid de un scope tal
|
|
219
|
+
* como lo escribió el llamante, la ortografía CANÓNICA (8-4-4-4-12) con la
|
|
220
|
+
* que un motor pudo fundirlo, o `[]` si no es alias de nada (ya es canónica,
|
|
221
|
+
* o no es un uuid, o es una forma que ningún motor del README funde).
|
|
222
|
+
*
|
|
223
|
+
* Es la regla del FAN-OUT de roles (`scopeSpellings`: la purga sin cadena
|
|
224
|
+
* de `scopes.detached`, `revoke`/`removeDeny`), y es a propósito MÁS
|
|
225
|
+
* ESTRECHA que `isNonCanonicalUuid`, la del 422 de las particiones de
|
|
226
|
+
* relaciones: son DOS propósitos. Rechazar de más (422 por `-<32hex>`) es
|
|
227
|
+
* inocuo, el llamante lo ve y lo corrige; purgar de más NO lo es, porque la
|
|
228
|
+
* canónica que se añade puede ser un scope VIVO de otro tenant en un
|
|
229
|
+
* consumidor con ids de texto elegibles por el usuario (🟡 C2-1/C2-2 del
|
|
230
|
+
* re-ataque al cierre-2: hasta el cierre-3 `scopeSpellings` usaba la ancha y
|
|
231
|
+
* `a0e-<hex de la víctima>` arrastraba a la víctima a la purga). Expandir
|
|
232
|
+
* una forma que ningún motor funde es coste sin beneficio: nunca hay hechos
|
|
233
|
+
* canónicos que sean «suyos».
|
|
234
|
+
*
|
|
235
|
+
* **Formas de más de 36 caracteres** (⚪ C2-4): PG funde también `4×8`
|
|
236
|
+
* (39 caracteres, el ejemplo literal de su doc), pero `IDENTITY_LIMITS.uuid`
|
|
237
|
+
* es 36 —la columna publicada— y `assertScope` la rechaza ANTES de llegar
|
|
238
|
+
* aquí con 422 `E_AUTHZ_INVALID_IDENTITY`: ruidoso y visible para el
|
|
239
|
+
* consumidor, nunca una purga a medias en silencio. El conjunto de este
|
|
240
|
+
* fan-out es, por tanto, «lo que PG funde Y cabe en 36».
|
|
241
|
+
*
|
|
242
|
+
* La inversa NO existe: dada la canónica no se enumeran sus alias (serían
|
|
243
|
+
* 2^7 ortografías); el fan-out va siempre de alias ⇒ canónica.
|
|
244
|
+
*/
|
|
245
|
+
export function pgFusedSpellings(uuid) {
|
|
246
|
+
if (typeof uuid !== 'string' || !PG_FUSED_UUID.test(uuid) || CANONICAL_UUID.test(uuid))
|
|
247
|
+
return [];
|
|
248
|
+
return [dashedUuid(uuid.replace(/-/g, ''))];
|
|
249
|
+
}
|
|
202
250
|
/**
|
|
203
251
|
* **Las ORTOGRAFÍAS de un scope con las que hay que limpiar cuando ya no hay
|
|
204
252
|
* cadena con la que canonizar** (3b-2h · 🟠 3, auditor R2).
|
|
@@ -216,23 +264,127 @@ const DASHLESS_UUID = /^[0-9a-f]{32}$/;
|
|
|
216
264
|
* fail-closed; en escritura era fail-OPEN.
|
|
217
265
|
*
|
|
218
266
|
* La salida es no elegir: una purga sin cadena limpia la ortografía del
|
|
219
|
-
* llamante **y** la canónica de la que puede ser alias.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
267
|
+
* llamante **y** la canónica de la que puede ser alias. **La regla de «¿de
|
|
268
|
+
* qué canónica puede ser alias?» es `pgFusedSpellings`**: exactamente lo que
|
|
269
|
+
* el tipo `uuid` de PostgreSQL funde (sin guiones, `8-4-4-16`, `8-8-8-8`,
|
|
270
|
+
* `12-4-4-12`…, medido), ni una forma más. Hasta el cierre-2 del lote A solo
|
|
271
|
+
* se expandía la forma SIN guiones y `8-4-4-16` dejaba la canónica viva
|
|
272
|
+
* (🟠 R1); el cierre-2 pasó a `isNonCanonicalUuid`, que es MÁS ANCHA que lo
|
|
273
|
+
* que PG funde (`-<32hex>`, `<32hex>-`, `a0e-ebc99…`, `--`: ids legales de
|
|
274
|
+
* la gramática que PG rechaza), y eso ampliaba el alcance de una purga sin
|
|
275
|
+
* cadena a scopes VIVOS ajenos (🟡 C2-1); el cierre-3 separa las dos reglas
|
|
276
|
+
* por propósito. El alias por mayúsculas ya muere en la puerta
|
|
277
|
+
* (`assertScope`). Así el consumidor que guarda sus uuids canónicos (el caso
|
|
278
|
+
* normal) no paga NADA, y el que los guarda de otra forma sigue purgando su
|
|
279
|
+
* clave y, además, la canónica. Normalizar en vez de expandir no vale: sin
|
|
280
|
+
* la fila no se sabe cuál de las dos es la buena, y elegir mal deja los
|
|
281
|
+
* hechos vivos, que es el fallo que se está cerrando.
|
|
282
|
+
*
|
|
283
|
+
* Esta función es PURA (no mira el árbol). Quien la consume es UN solo punto
|
|
284
|
+
* de paso, `canonicalScopeTargets` (`backend_guard.ts`; lo usan
|
|
285
|
+
* `scopes.detached` sin cadena en el manager y el fan-out de
|
|
286
|
+
* `revoke`/`removeDeny` en los dos drivers, 3b-8 · A4), que además DESCARTA
|
|
287
|
+
* toda ortografía que el resolutor reconozca como scope VIVO distinto del
|
|
288
|
+
* pedido (cierre-3 · 🟡 C2-2/C2-3): nunca se purga un scope vivo que no es
|
|
289
|
+
* el que se pidió.
|
|
228
290
|
*/
|
|
229
291
|
export function scopeSpellings(scope) {
|
|
230
292
|
assertScope(scope);
|
|
231
293
|
const uuid = scope.uuid;
|
|
232
|
-
if (uuid === null
|
|
294
|
+
if (uuid === null)
|
|
233
295
|
return [scope];
|
|
234
|
-
|
|
235
|
-
|
|
296
|
+
return [scope, ...pgFusedSpellings(uuid).map((canonical) => ({ type: scope.type, uuid: canonical }))];
|
|
297
|
+
}
|
|
298
|
+
/** ¿Es un uuid canónico escrito SIN guiones (32 hex en minúsculas)? */
|
|
299
|
+
export function isDashlessUuid(value) {
|
|
300
|
+
return typeof value === 'string' && DASHLESS_UUID.test(value);
|
|
301
|
+
}
|
|
302
|
+
/** La forma 8-4-4-4-12 de un uuid de 32 hex (la ortografía CANÓNICA). */
|
|
303
|
+
export function dashedUuid(dashless) {
|
|
304
|
+
return `${dashless.slice(0, 8)}-${dashless.slice(8, 12)}-${dashless.slice(12, 16)}-${dashless.slice(16, 20)}-${dashless.slice(20)}`;
|
|
305
|
+
}
|
|
306
|
+
/* ── Partición de RELACIONES (D4, plan 2.5 · lote A) ───────────────────── */
|
|
307
|
+
/** La ortografía CANÓNICA de un uuid: exactamente 8-4-4-4-12, minúsculas. */
|
|
308
|
+
const CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
309
|
+
/**
|
|
310
|
+
* **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
|
|
311
|
+
* auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
|
|
312
|
+
* guiones, sea 32 hex y NO esté exactamente en la forma 8-4-4-4-12. Cubre el
|
|
313
|
+
* 32 hex sin guiones (lo único que D4 cerraba) Y cualquier otra colocación de
|
|
314
|
+
* guiones —`8-4-4-16`, cuatro grupos, la canónica sin su último guion…—.
|
|
315
|
+
*
|
|
316
|
+
* **Es la regla ANCHA, y se usa SOLO para el 422 de las particiones de
|
|
317
|
+
* relaciones** (`assertPartition`; cierre-3 del lote A, decisión del dueño
|
|
318
|
+
* 2026-10-15 (3)): es un SUPERCONJUNTO de lo que el tipo `uuid` de
|
|
319
|
+
* PostgreSQL funde —incluye `-<32hex>`, `<32hex>-`, `a0e-ebc99…` o `8--24`,
|
|
320
|
+
* que PG rechaza con «invalid input syntax» (medido, PG 18)— y ahí ser más
|
|
321
|
+
* ancha es inocuo: un 422 de más lo ve y lo corrige el llamante, y una
|
|
322
|
+
* partición que «parece un uuid» sin serlo tampoco merece ser una clave
|
|
323
|
+
* distinta de la canónica. Lo que PG funde DE VERDAD es `pgFusedSpellings`,
|
|
324
|
+
* la regla del fan-out de ROLES, donde expandir de más sí cuesta (purgar un
|
|
325
|
+
* scope vivo ajeno). Un valor que sin guiones no sea 32 hex (31, 33, un
|
|
326
|
+
* carácter no hex, `_`/`.`) NO es un uuid y es un id legal.
|
|
327
|
+
* Es la MISMA función que usa el detector de la receta del README, por
|
|
328
|
+
* construcción (el `upgrade_recipe.spec` lo mide: lo que `assertPartition`
|
|
329
|
+
* rechaza, el detector lo lista).
|
|
330
|
+
*/
|
|
331
|
+
export function isNonCanonicalUuid(value) {
|
|
332
|
+
if (typeof value !== 'string')
|
|
333
|
+
return false;
|
|
334
|
+
return DASHLESS_UUID.test(value.replace(/-/g, '')) && !CANONICAL_UUID.test(value);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* **La partición de una relación se escribe en la forma CANÓNICA 8-4-4-4-12,
|
|
338
|
+
* o es 422** (D4, decisión del dueño 2026-10-14; `panel-tenencia-2-juez.md`
|
|
339
|
+
* §5; ampliado el 2026-10-15 a toda ortografía no canónica, 🟠 A2).
|
|
340
|
+
* `assertScope` vale para un scope de ROLES porque ahí la identidad la
|
|
341
|
+
* canoniza la cadena del resolutor (invariante 17) y, donde no hay cadena,
|
|
342
|
+
* `scopeSpellings` barre la ortografía del llamante y la canónica de la que
|
|
343
|
+
* puede ser alias, con SU regla (`pgFusedSpellings`, lo que PG funde de
|
|
344
|
+
* verdad: cierre-3). En RELACIONES no hay cadena con
|
|
345
|
+
* la que canonizar: la clave de partición es un string comparado byte a
|
|
346
|
+
* byte, así que `unit|aaaa…` (32 hex) y `unit|aaaa-…` eran DOS particiones,
|
|
347
|
+
* la purga canónica no veía la otra y, con el 422, las tuplas viejas sin
|
|
348
|
+
* guiones quedaban inalcanzables para siempre (tester H5). Y la clase no era
|
|
349
|
+
* solo «sin guiones»: PostgreSQL funde también `8-4-4-16`, cuatro grupos de
|
|
350
|
+
* ocho, la canónica sin un guion… (🟠 A2) — cualquier colocación de guiones
|
|
351
|
+
* sobre los mismos 32 hex —, así que la regla es «32 hex sin guiones y no
|
|
352
|
+
* exactamente 8-4-4-4-12 ⇒ 422» (`isNonCanonicalUuid`). La regla es una sola
|
|
353
|
+
* y va en un solo sitio: toda operación que recibe una partición —manager,
|
|
354
|
+
* los dos drivers, el doble publicado y `reconcileRelations`— pasa por aquí
|
|
355
|
+
* (o por `partitionKey`, que la llama), y la guardia de superficie
|
|
356
|
+
* (`tests/guard_surface.spec.ts`, columna `partition`) lo sondea en cada
|
|
357
|
+
* operación con las DOS formas (sin guiones y `8-4-4-16`). Las filas escritas
|
|
358
|
+
* antes de 2.5.0 con una ortografía no canónica se detectan, se listan y se
|
|
359
|
+
* BORRAN con la receta del README («Upgrading to 2.5.0», fail-closed por
|
|
360
|
+
* defecto: eran concesiones invisibles a la purga canónica); re-clavarlas es
|
|
361
|
+
* el paso opt-in `--rekey`. La ejecuta `tests/upgrade_recipe.spec.ts` en los
|
|
362
|
+
* tres motores, con las dos ramas.
|
|
363
|
+
*
|
|
364
|
+
* Un id de partición que no sea un uuid sigue valiendo (`[a-z0-9._-]{1,36}`:
|
|
365
|
+
* 32 caracteres no todos hex, 31 o 33 hex…). Los ids de OBJETO y de SUJETO
|
|
366
|
+
* no se rechazan: se comparan byte a byte y el README lo declara.
|
|
367
|
+
*/
|
|
368
|
+
export function assertPartition(partition) {
|
|
369
|
+
assertScope(partition);
|
|
370
|
+
if (partition.uuid !== null && isNonCanonicalUuid(partition.uuid)) {
|
|
371
|
+
const canonical = dashedUuid(partition.uuid.replace(/-/g, ''));
|
|
372
|
+
throw new InvalidIdentityError(`Partición inválida: el uuid '${partition.uuid}' del scope '${partition.type}' no está en la forma canónica ` +
|
|
373
|
+
`8-4-4-4-12 (${partition.uuid.includes('-') ? 'guiones mal colocados' : 'sin guiones, 32 hex'}). ` +
|
|
374
|
+
`Las particiones de relaciones se comparan byte a byte y no se canonizan: escríbelo como '${canonical}'. ` +
|
|
375
|
+
`Las tuplas escritas con una ortografía no canónica antes de 2.5.0 se detectan y se borran con la receta ` +
|
|
376
|
+
`del README («Upgrading to 2.5.0»; re-clavarlas es el paso opt-in --rekey).`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* La clave de partición de una RELACIÓN: `scopeKey` con la regla de
|
|
381
|
+
* `assertPartition` delante. Es lo que los dos drivers de relaciones (y el
|
|
382
|
+
* doble) usan para componer `partition_key` / el id de objeto del store: no
|
|
383
|
+
* hay camino hacia el backend que no pase por aquí.
|
|
384
|
+
*/
|
|
385
|
+
export function partitionKey(partition) {
|
|
386
|
+
assertPartition(partition);
|
|
387
|
+
return scopeKey(partition);
|
|
236
388
|
}
|
|
237
389
|
/**
|
|
238
390
|
* La inversa de `scopeKey` para claves que vienen de la BASE (el owner de un
|