@dotrino/identity 0.107.0 → 0.107.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.107.0",
3
+ "version": "0.107.2",
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/src/index.d.ts CHANGED
@@ -188,6 +188,18 @@ export class Identity {
188
188
  canApproveVault (): Promise<boolean>
189
189
  getVaultCert (): Promise<any>
190
190
  onVault (handler: (payload: any) => void): () => void
191
+ /** El perfil ACTIVO de este aparato (el que usan las apps; `@dotrino/store` se ata a él). */
192
+ currentProfile (): Promise<{ id: string; name?: string; pubkey?: string } | null>
193
+ /** Los perfiles de este aparato. */
194
+ listProfiles (): Promise<Array<{ id: string; name?: string; pubkey?: string; current?: boolean; avatar?: string }>>
195
+ /** Cambia el perfil activo (la app recarga: no es reactivo). */
196
+ switchProfile (id: string): Promise<any>
197
+ /** Cifra con la llave de la CUENTA: el sobre lleva la marca `t: 'dotrino-cek'` y lo abren todos los aparatos del acta. */
198
+ sealContent (plaintext: string): Promise<{ t: 'dotrino-cek'; gen: number; iv: string; ct: string }>
199
+ /** Abre un sobre de la cuenta (cualquier generación que este aparato tenga). */
200
+ openContent (envelope: { gen: number; iv: string; ct: string; t?: string }): Promise<string>
201
+ /** Mi copia de la llave de la cuenta vigente, o null si todavía no me la han envuelto. */
202
+ contentKey (): Promise<{ gen: number; cek: string } | null>
191
203
  // self-vault (este dispositivo ES el vault, daemon dentro del iframe)
192
204
  selfVaultStatus (): Promise<{ enabled: boolean; running: boolean }>
193
205
  setSelfVault (enabled: boolean): Promise<{ ok: true; enabled: boolean }>
package/src/index.js CHANGED
@@ -139,6 +139,7 @@ export class Identity {
139
139
  const err = new Error(msg.error)
140
140
  if (msg.code) err.code = msg.code
141
141
  if (msg.detail) err.detail = msg.detail
142
+ if (msg.reason) err.reason = msg.reason
142
143
  pending.reject(err)
143
144
  }
144
145
  else pending.resolve(msg.result)
package/vault/content.js CHANGED
@@ -118,56 +118,25 @@ export const CEK_ENVELOPE = 'dotrino-cek'
118
118
  export const isCekEnvelope = (v) => !!v && typeof v === 'object' && v.t === CEK_ENVELOPE &&
119
119
  Number.isInteger(v.gen) && typeof v.iv === 'string' && typeof v.ct === 'string'
120
120
 
121
- /** Cifra con la CEK. Devuelve un sobre `{ t, gen, iv, ct }` (el `gen` dice con cuál se cifró). */
121
+ /**
122
+ * Cifra con una clave simétrica. Devuelve `{ gen, iv, ct }` (el `gen` dice con cuál).
123
+ *
124
+ * Es GENÉRICA: la usan la llave de la cuenta y también llaves que no son de la cuenta (los
125
+ * cajones de secretos, cada entrada del gestor de contraseñas, el transporte). Por eso NO
126
+ * lleva la marca: la marca dice «esto es de la cuenta» y solo la pone `sealAccount`. En
127
+ * 0.107.0 la ponía aquí, y marcaba como de la cuenta sobres que la cuenta no abre.
128
+ */
122
129
  export async function encryptWithCek ({ cek, gen, plaintext }) {
123
130
  const k = await subtle.importKey('raw', fromB64(cek), { name: 'AES-GCM' }, false, ['encrypt'])
124
131
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
125
132
  const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, k, new TextEncoder().encode(plaintext))
126
- return { t: CEK_ENVELOPE, gen, iv: b64(iv), ct: b64(ct) }
133
+ return { gen, iv: b64(iv), ct: b64(ct) }
127
134
  }
128
135
 
129
- /**
130
- * VUELVE A CERRAR con la generación vigente todo sobre de una generación anterior que haya
131
- * dentro de `value`, a cualquier profundidad. Devuelve una copia (no toca el original) y
132
- * cuántos sobres cambió.
133
- *
134
- * Es lo que hace la bóveda al abrirse después de una rotación (dueño, 2026-09-30): lo que
135
- * sirve va SIEMPRE con la llave vigente, así que un aparato que entra después —que solo
136
- * recibe esa generación— abre todo, y la llave vieja deja de abrir nada porque ya no se
137
- * le sirve nada cerrado con ella.
138
- *
139
- * `reseal(envelope)` abre y vuelve a cerrar un sobre; lo pone quien tiene las llaves. Si
140
- * un sobre no se puede abrir, se PARA: no se deja a medias ni se salta en silencio.
141
- *
142
- * @param {any} value
143
- * @param {{ gen: number, reseal: (env: object) => Promise<object> }} o
144
- * @returns {Promise<{ value: any, changed: number }>}
145
- */
146
- export async function resealStale (value, { gen, reseal }) {
147
- if (!Number.isInteger(gen)) throw new Error('resealStale: the current generation is required')
148
- let changed = 0
149
- const walk = async (v) => {
150
- if (isCekEnvelope(v)) {
151
- if (v.gen === gen) return v
152
- const nuevo = await reseal(v)
153
- if (!isCekEnvelope(nuevo) || nuevo.gen !== gen) throw new Error(`resealStale: reseal did not return an envelope of generation ${gen}`)
154
- changed++
155
- return nuevo
156
- }
157
- if (Array.isArray(v)) {
158
- const out = []
159
- for (const x of v) out.push(await walk(x))
160
- return out
161
- }
162
- if (v && typeof v === 'object') {
163
- const out = {}
164
- for (const [k, x] of Object.entries(v)) out[k] = await walk(x)
165
- return out
166
- }
167
- return v
168
- }
169
- const out = await walk(value)
170
- return { value: out, changed }
136
+ /** Cierra con la LLAVE DE LA CUENTA (generación `gen`): el sobre lleva la marca. */
137
+ export async function sealAccount ({ cek, gen, plaintext }) {
138
+ if (!Number.isInteger(gen) || gen < 1) throw new Error('sealAccount: the account key generation starts at 1')
139
+ return { t: CEK_ENVELOPE, ...(await encryptWithCek({ cek, gen, plaintext })) }
171
140
  }
172
141
 
173
142
  /**
@@ -198,7 +167,81 @@ export async function decryptWithKeyring ({ envelope, keyring, myPub, myEncPriva
198
167
  return new TextDecoder().decode(pt)
199
168
  }
200
169
 
170
+ /**
171
+ * MIGRACIÓN, con fecha de caducidad: hasta 0.107.0 los sobres de la cuenta salían SIN marca,
172
+ * como `{ gen, iv, ct }` a secas. Hasta esta fecha, `resealStale` también prueba esos: si
173
+ * abren con la llave de la cuenta, los vuelve a cerrar ya marcados; si no abren, no eran de
174
+ * la cuenta y se dejan como están. Pasada la fecha, un sobre sin marca ya no se toca. Se
175
+ * quita el código cuando pase (y su prueba con él).
176
+ */
177
+ export const LEGACY_UNMARKED_UNTIL = Date.parse('2027-06-30T00:00:00Z')
178
+
179
+ /** ¿Tiene la forma EXACTA de un sobre de la cuenta anterior a la marca? */
180
+ export const isLegacyCekEnvelope = (v) => !!v && typeof v === 'object' && !Array.isArray(v) &&
181
+ Object.keys(v).length === 3 && Number.isInteger(v.gen) && v.gen >= 1 &&
182
+ typeof v.iv === 'string' && typeof v.ct === 'string'
183
+
184
+ /**
185
+ * VUELVE A CERRAR con la generación vigente todo sobre de la cuenta de una generación
186
+ * anterior que haya dentro de `value`, a cualquier profundidad. Devuelve una copia (no toca
187
+ * el original) y cuántos sobres cambió.
188
+ *
189
+ * Es lo que hace la bóveda al abrirse y después de una rotación (dueño, 2026-09-30): lo que
190
+ * sirve va SIEMPRE con la llave vigente, así que un aparato que entra después —que solo
191
+ * recibe esa generación— abre todo, y la llave vieja deja de abrir nada porque ya no se le
192
+ * sirve nada cerrado con ella.
193
+ *
194
+ * `reseal(envelope)` abre y vuelve a cerrar un sobre con la llave de la cuenta; lo pone quien
195
+ * la tiene, y debe devolver un sobre MARCADO de la generación `gen`.
196
+ *
197
+ * · Un sobre MARCADO que no abre PARA todo: es de la cuenta y algo va mal; no se deja a
198
+ * medias ni se salta en silencio.
199
+ * · Un sobre SIN MARCA (migración, hasta `LEGACY_UNMARKED_UNTIL`) se prueba aunque ya
200
+ * tenga la generación vigente —hay que ponerle la marca—; si no abre, se deja y se
201
+ * cuenta en `skipped`.
202
+ *
203
+ * @param {any} value
204
+ * @param {{ gen: number, reseal: (env: object) => Promise<object>, now?: number }} o
205
+ * @returns {Promise<{ value: any, changed: number, skipped: number }>}
206
+ */
207
+ export async function resealStale (value, { gen, reseal, now = Date.now() }) {
208
+ if (!Number.isInteger(gen)) throw new Error('resealStale: the current generation is required')
209
+ const migrar = now < LEGACY_UNMARKED_UNTIL
210
+ let changed = 0; let skipped = 0
211
+ const comprobar = (nuevo) => {
212
+ if (!isCekEnvelope(nuevo) || nuevo.gen !== gen) throw new Error(`resealStale: reseal did not return a marked envelope of generation ${gen}`)
213
+ return nuevo
214
+ }
215
+ const walk = async (v) => {
216
+ if (isCekEnvelope(v)) {
217
+ if (v.gen === gen) return v
218
+ const nuevo = comprobar(await reseal(v))
219
+ changed++
220
+ return nuevo
221
+ }
222
+ if (migrar && isLegacyCekEnvelope(v)) {
223
+ let nuevo
224
+ try { nuevo = await reseal(v) } catch (_) { skipped++; return v }
225
+ changed++
226
+ return comprobar(nuevo)
227
+ }
228
+ if (Array.isArray(v)) {
229
+ const out = []
230
+ for (const x of v) out.push(await walk(x))
231
+ return out
232
+ }
233
+ if (v && typeof v === 'object') {
234
+ const out = {}
235
+ for (const [k, x] of Object.entries(v)) out[k] = await walk(x)
236
+ return out
237
+ }
238
+ return v
239
+ }
240
+ const out = await walk(value)
241
+ return { value: out, changed, skipped }
242
+ }
243
+
201
244
  export default {
202
245
  makeContentKey, wrapForMember, openWrap, makeGeneration, myContentKey,
203
- encryptWithCek, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, resealStale
246
+ encryptWithCek, sealAccount, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, isLegacyCekEnvelope, LEGACY_UNMARKED_UNTIL, resealStale
204
247
  }
package/vault/core.js CHANGED
@@ -2695,13 +2695,14 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
2695
2695
  async contentKey () { return myCek() },
2696
2696
 
2697
2697
  /**
2698
- * Cifra algo con la clave de contenido del perfil. Devuelve el sobre `{gen,iv,ct}`.
2699
- * La llave privada de cifrado NUNCA sale de aquí: se cifra y descifra dentro.
2698
+ * Cifra algo con la clave de contenido del perfil. Devuelve el sobre MARCADO
2699
+ * `{ t: 'dotrino-cek', gen, iv, ct }` (la marca deja a la bóveda reconocerlo y volver a
2700
+ * cerrarlo al rotar). La llave privada de cifrado NUNCA sale de aquí.
2700
2701
  */
2701
2702
  async sealContent ({ plaintext } = {}) {
2702
2703
  const mine = await myCek()
2703
2704
  if (!mine) throw new Error('this device does not hold the profile content key yet')
2704
- return Content.encryptWithCek({ cek: mine.cek, gen: mine.gen, plaintext: String(plaintext) })
2705
+ return Content.sealAccount({ cek: mine.cek, gen: mine.gen, plaintext: String(plaintext) })
2705
2706
  },
2706
2707
 
2707
2708
  /**
package/vault/vault.js CHANGED
@@ -540,7 +540,10 @@ import { useBackend as usePeerBackend } from './peerStore.js'
540
540
  reply({
541
541
  error: e?.message || String(e),
542
542
  ...(e?.code ? { code: e.code } : {}),
543
- ...(e?.detail ? { detail: e.detail } : {})
543
+ ...(e?.detail ? { detail: e.detail } : {}),
544
+ // El MOTIVO de un rechazo de la bóveda (`expired`, `revoked`, `acta`…): sin él, «la
545
+ // bóveda te rechaza» no dice si hay que volver a enlazar o esperar.
546
+ ...(e?.reason ? { reason: e.reason } : {})
544
547
  })
545
548
  }
546
549
  })