@dotrino/identity 0.56.0 → 0.57.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/vault/acta.js +13 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/vault/acta.js CHANGED
@@ -68,6 +68,12 @@ export const PAIRED_CAPS = Object.freeze(['sign', 'store', 'read'])
68
68
  /** Capacidades de un SERVICIO (con CN): solo su propio cajón de secretos. */
69
69
  export const SERVICE_CAPS = Object.freeze(['secrets'])
70
70
 
71
+ /**
72
+ * Qué capacidades puede llevar un miembro según tenga cajón (CN) o no. Con CN: su cajón
73
+ * y, si se le conceden, las de aparato. Sin CN: solo las de aparato (no hay cajón que abrir).
74
+ */
75
+ export const allowedCaps = (cn) => (cn ? [...SERVICE_CAPS, ...DEVICE_CAPS] : [...DEVICE_CAPS])
76
+
71
77
  /** Un CN válido: minúsculas, números y guiones (igual que el namespace de secretos). */
72
78
  export const isValidCn = (cn) => typeof cn === 'string' && /^[a-z0-9-]{1,32}$/.test(cn)
73
79
 
@@ -178,7 +184,6 @@ export function checkShape (acta) {
178
184
  // mezcle las dos cosas es lo que hace que el límite sea real y no una costumbre.
179
185
  if (m.cn != null) {
180
186
  if (!isValidCn(m.cn)) return 'cn-invalido'
181
- if (m.caps.some((c) => !SERVICE_CAPS.includes(c))) return 'servicio-con-capacidades-de-dispositivo'
182
187
  } else if (m.caps.includes('secrets')) {
183
188
  return 'secretos-sin-cn'
184
189
  }
@@ -306,8 +311,10 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
306
311
  encPub: m.encPub || null,
307
312
  label: String(m.label || '').slice(0, 60),
308
313
  cn,
309
- // Un servicio (con CN) solo puede tener `secrets`; un dispositivo, las otras tres.
310
- caps: cleanCaps(m.caps).filter((c) => (cn ? SERVICE_CAPS : DEVICE_CAPS).includes(c)),
314
+ // PERMISOS, no tipos (2026-08-22): el CN dice qué cajón abre; no recorta lo demás.
315
+ // Un miembro con CN puede llevar además las capacidades de aparato (un bot que
316
+ // firma como aparato del acta Y lee su cajón). Sin CN, `secrets` no significa nada.
317
+ caps: cleanCaps(m.caps).filter((c) => allowedCaps(cn).includes(c)),
311
318
  addedAt: now,
312
319
  cert: m.cert || null,
313
320
  // Puente con la identidad que este miembro traía de antes (ver makeContinuity).
@@ -331,9 +338,9 @@ export async function applyChanges (acta, changes, { by, now = Date.now(), sealP
331
338
  case 'caps': {
332
339
  const m = find(ch.pub)
333
340
  if (!m) throw new Error('caps: that member is not in the record')
334
- // No se puede ascender un servicio a dispositivo cambiándole las capacidades: para
335
- // eso hay que sacarlo y volver a admitirlo, que es un gesto visible.
336
- m.caps = cleanCaps(ch.caps).filter((c) => (m.cn ? SERVICE_CAPS : DEVICE_CAPS).includes(c))
341
+ // El CN no cambia por aquí (quitar el cajón es sacar al miembro y volver a admitirlo,
342
+ // que es un gesto visible); las capacidades sí, dentro de lo que su CN permite.
343
+ m.caps = cleanCaps(ch.caps).filter((c) => allowedCaps(m.cn).includes(c))
337
344
  // CONCEDER LIMPIA LA RENUNCIA. `effectiveCaps` resta lo renunciado, así que sin
338
345
  // esto una renuncia sellada era IRREVERSIBLE: el master le devolvía el permiso en
339
346
  // `caps` y la resta seguía dejándolo fuera, para siempre. La interfaz promete