@dotrino/identity 0.69.0 → 0.71.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.69.0",
3
+ "version": "0.71.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
@@ -62,6 +62,18 @@ const ACTA_LEIBLES = Object.freeze([1, 2, 3, 4])
62
62
  const V_CON_CADENA = 4
63
63
  /** ¿Esta acta lleva el eslabón? Las anteriores se leen igual; no pueden encadenar. */
64
64
  const conCadena = (acta) => Number(acta?.v) >= V_CON_CADENA
65
+ /**
66
+ * Solo `https`, y sin excepción para localhost: esta dirección la va a abrir un TERCERO
67
+ * que no te conoce, y mandarlo por texto plano es dejar que cualquiera en el camino le
68
+ * conteste otra cosa. Que no lleve `#` ni credenciales: no es un enlace para una persona.
69
+ */
70
+ const isChainUrl = (u) => {
71
+ if (typeof u !== 'string' || u.length > 300) return false
72
+ try {
73
+ const x = new URL(u)
74
+ return x.protocol === 'https:' && !x.hash && !x.username && !x.password
75
+ } catch (_) { return false }
76
+ }
65
77
  /** Desde esta versión, sellar es solo un permiso y no hay campo `sealer`. */
66
78
  const V_SIN_CAMPO_SELLADOR = 3
67
79
  /** ¿Este acta es de las viejas, con el campo? */
@@ -221,12 +233,30 @@ export const canSeal = (acta, pub, renounces = []) =>
221
233
  * sellador, con todas las capacidades. `profileId` = su pubkey → el nombre del perfil es
222
234
  * estable para siempre y coincide con la identidad que el usuario ya tenía (cero migración).
223
235
  */
224
- export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', now = Date.now() }) {
236
+ export function genesisActa ({ pub, encPub = null, sealPub = null, label = '', chainUrl = null, now = Date.now() }) {
225
237
  if (!isPub(pub)) throw new Error('genesisActa: missing genesis pubkey')
238
+ if (chainUrl != null && !isChainUrl(chainUrl)) throw new Error('genesisActa: chainUrl must be https')
226
239
  return {
227
240
  v: ACTA_V,
228
241
  profileId: pub,
229
242
  sealedBy: pub,
243
+ /**
244
+ * DÓNDE PREGUNTAR SI ESTA CADENA SIGUE VIGENTE. Va en el GÉNESIS y en ningún otro
245
+ * sitio, y esa es toda la idea.
246
+ *
247
+ * La cadena que te llega prueba quién puede sellar, pero no que no haya algo más
248
+ * nuevo — si retiraste una bóveda, quien tenga la cadena vieja la sigue aceptando.
249
+ * Para enterarse hay que poder mirar a algún lado.
250
+ *
251
+ * Si la dirección viajara en la parte cambiable, el sellador expulsado la cambiaría
252
+ * a la suya y te mandaría a mirar su rama. En el génesis no puede: está autofirmada
253
+ * por la llave que da nombre al perfil, y cambiarla exige esa llave.
254
+ *
255
+ * `null` es lo normal y no es un defecto: una cuenta de una sola bóveda no tiene nada
256
+ * que pueda quedar obsoleto —su conjunto de selladores no puede cambiar— así que no
257
+ * hay a dónde preguntar ni falta que hace.
258
+ */
259
+ chainUrl: chainUrl || null,
230
260
  seq: 1,
231
261
  prev: null,
232
262
  // LA CADENA DE SELLADORES. El génesis es el ancla: está autofirmado por la llave que
@@ -258,6 +288,7 @@ export function checkShape (acta) {
258
288
  if (!acta || typeof acta !== 'object') return 'no-acta'
259
289
  if (!ACTA_LEIBLES.includes(acta.v)) return 'version'
260
290
  if (!isPub(acta.profileId) || !isPub(acta.sealedBy)) return 'shape'
291
+ if (acta.chainUrl != null && !isChainUrl(acta.chainUrl)) return 'chainurl'
261
292
  // El campo solo existe —y solo se exige— en las viejas.
262
293
  if (conCampoSellador(acta) && !isPub(acta.sealer)) return 'shape'
263
294
  if (!conCampoSellador(acta) && acta.sealer != null) return 'sealer-no-va-en-v3'
@@ -358,6 +389,35 @@ export async function sealActa ({ acta, privateKey, privateJwk }) {
358
389
  * aceptando a un sellador que ya retiraste. Es el problema de siempre y hace falta un
359
390
  * oráculo, no más firmas.
360
391
  */
392
+ /**
393
+ * ¿DE QUIÉN ES ESTO? La pregunta que se hacen geo, reputación y cualquiera que reciba algo
394
+ * firmado de un desconocido. Es lo único que necesitan llamar.
395
+ *
396
+ * Devuelve el `profileId` —la identidad— o por qué no.
397
+ *
398
+ * EL CERTIFICADO NO HACE FALTA AQUÍ, y conviene decirlo porque es lo que uno espera:
399
+ * el acta ya dice quién puede firmar por la identidad, y la cadena prueba el acta. El
400
+ * certificado es del protocolo aparato↔bóveda (autenticar peticiones), no de atribuir
401
+ * contenido. Mezclarlos obligaba a que cada post arrastrara un papel que no aporta nada.
402
+ *
403
+ * Lo que SÍ hay que entender: esto dice «esta identidad firmó esto», no «esto es
404
+ * reciente». Si la cadena que te llega es vieja, sigue verificando — para eso está el
405
+ * registro público, que es otra capa.
406
+ */
407
+ export async function verifySignedBy ({ data, signature, publickey, chain, expectedProfileId = null } = {}) {
408
+ if (!data || typeof signature !== 'string' || typeof publickey !== 'string') {
409
+ return { ok: false, reason: 'shape' }
410
+ }
411
+ const c = await verifySealerChain(chain, { expectedProfileId })
412
+ if (!c.ok) return { ok: false, reason: 'cadena:' + c.reason }
413
+ const cabeza = chain[chain.length - 1]
414
+ // Quien firmó tiene que ser miembro CON `sign` en el acta vigente de esa cadena. Si le
415
+ // quitaste el permiso, sus firmas dejan de contar en cuanto se ve un acta más nueva.
416
+ if (!memberCanSign(cabeza, publickey)) return { ok: false, reason: 'firmante-no-autorizado' }
417
+ if (!(await verifyDeviceSig({ publickey, data, signature }))) return { ok: false, reason: 'firma-invalida' }
418
+ return { ok: true, profileId: c.profileId, seq: c.seq, signer: publickey }
419
+ }
420
+
361
421
  export async function verifySealerChain (chain, { expectedProfileId = null } = {}) {
362
422
  if (!Array.isArray(chain) || !chain.length) return { ok: false, reason: 'cadena-vacia' }
363
423
  const [raiz] = chain
@@ -909,7 +969,7 @@ export async function canAdopt ({ candidate, current }) {
909
969
  }
910
970
 
911
971
  export default {
912
- ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, verifySealerChain,
972
+ ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, verifySealerChain, verifySignedBy,
913
973
  sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
914
974
  makeContinuity, verifyContinuity,
915
975
  cardBody, makeProfileCard, verifyProfileCard, canAdoptCard, sealersOf, canSeal,
package/vault/core.js CHANGED
@@ -1357,7 +1357,22 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
1357
1357
  if (data == null) throw new Error('data required')
1358
1358
  const local = async () => {
1359
1359
  const bytes = new TextEncoder().encode(canonicalStringify(data))
1360
- return { signature: await signBytes(keypair.privateKey, bytes), publickey: publickeyJwkStr }
1360
+ const acta = loadActa()
1361
+ return {
1362
+ signature: await signBytes(keypair.privateKey, bytes),
1363
+ publickey: publickeyJwkStr,
1364
+ // A NOMBRE DE QUIÉN VA. `publickey` es la llave de ESTE aparato, y las apps la
1365
+ // venían guardando como si fuera la identidad: publicar desde el teléfono y
1366
+ // desde el PC quedaba a nombre de dos «personas» distintas, y la reputación se
1367
+ // repartía entre ellas en vez de acumularse.
1368
+ //
1369
+ // La identidad es el `profileId` —la llave del génesis, que no cambia nunca— y
1370
+ // la cadena es lo que prueba que este firmante le pertenece. Van juntos porque
1371
+ // por separado no sirven: el `profileId` solo es una afirmación, y la cadena sin
1372
+ // él no dice a quién atribuir.
1373
+ profileId: acta?.profileId || publickeyJwkStr,
1374
+ chain: sealerChain()
1375
+ }
1361
1376
  }
1362
1377
  const acta = loadActa()
1363
1378
  const puedeFirmar = !acta || Acta.memberCan(acta, publickeyJwkStr, 'sign', loadRenounces())