@dotrino/vault 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -42,7 +42,7 @@
42
42
  "pairing"
43
43
  ],
44
44
  "peerDependencies": {
45
- "@dotrino/identity": ">=0.27.0",
45
+ "@dotrino/identity": ">=0.30.0",
46
46
  "@dotrino/proxy-client": ">=0.6.0"
47
47
  },
48
48
  "license": "MIT",
package/src/enroll.js CHANGED
@@ -50,6 +50,19 @@ const SCOPE_TO_CAP = { 'vault:sign': 'sign', 'vault:store': 'store', 'vault:read
50
50
  export const scopeToCaps = (scope) =>
51
51
  (Array.isArray(scope) ? scope : [scope]).map((s) => SCOPE_TO_CAP[s]).filter(Boolean)
52
52
 
53
+ /**
54
+ * El CN de un servicio sale de su scope: `vault:secrets:proxy` ⇒ CN `proxy`. Con CN, el
55
+ * miembro entra al acta como SERVICIO —solo abre su propio cajón— en vez de como un
56
+ * dispositivo del usuario. Es la frontera, y vive en el acta para que se pueda comprobar.
57
+ */
58
+ export function scopeToCn (scope) {
59
+ for (const s of (Array.isArray(scope) ? scope : [scope])) {
60
+ const m = /^vault:secrets:([a-z0-9-]{1,32})$/.exec(String(s || ''))
61
+ if (m) return m[1]
62
+ }
63
+ return null
64
+ }
65
+
53
66
  /** Token aleatorio de 128 bits en hex. */
54
67
  export function randToken () {
55
68
  const b = crypto.getRandomValues(new Uint8Array(16))
@@ -95,13 +108,29 @@ export function createEnrollDesk ({
95
108
  const reply = (to, obj) => { try { send(to, obj) } catch (e) { log('[vault] no se pudo responder:', e.message) } }
96
109
  const isFresh = (d) => typeof d?.ts === 'number' && Math.abs(Date.now() - d.ts) <= FRESH_WINDOW_MS
97
110
 
98
- /** Inicia un emparejamiento: token + nonce de sesión. NO firma nada todavía. */
99
- function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '' } = {}) {
111
+ /**
112
+ * Inicia un emparejamiento: token + nonce de sesión. NO firma nada todavía.
113
+ *
114
+ * `mode` y `account` son LO QUE LA BÓVEDA DECLARA que va a pasar, y viajan en el QR
115
+ * para que el aparato pueda **decirlo antes de hacerlo** en vez de emparejar a
116
+ * ciegas (decisión V9 de `docs/vinculacion-de-cuentas.md`: pregunta el vault, el
117
+ * dispositivo muestra el proceso y sus consecuencias):
118
+ *
119
+ * · `mode: 'join'` → el dispositivo estrena una cuenta suya y entra a la de la
120
+ * bóveda. Es lo único que existe hoy.
121
+ * · `mode: 'adopt'` → la bóveda se quedaría con la cuenta que trae el aparato
122
+ * (camino A). Reservado: todavía no hay protocolo.
123
+ * · `account` → cómo se llama la cuenta de la bóveda, para nombrarla en el
124
+ * aviso. Es ORIENTATIVO (un nombre que puso su dueño); la
125
+ * identidad de verdad de la cuenta es `iss`.
126
+ */
127
+ function startPairing ({ scope = defaultScope, ttlMs = defaultTtlMs, label = '', mode = 'join', account = '' } = {}) {
100
128
  pending.clear() // uno a la vez: una sesión nueva supersede a la anterior
101
129
  const token = randToken()
102
130
  const sn = randToken()
103
- pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, state: 'AWAITING_ENROLL' })
104
- return { token, qr: { v: 2, iss, proxy, token, sn }, expiresInMs: PAIRING_TTL_MS }
131
+ const acct = String(account || '').slice(0, 40)
132
+ pending.set(token, { token, exp: Date.now() + PAIRING_TTL_MS, scope, ttlMs, label, sn, mode, account: acct, state: 'AWAITING_ENROLL' })
133
+ return { token, qr: { v: 2, iss, proxy, token, sn, m: mode, ...(acct ? { acct } : {}) }, expiresInMs: PAIRING_TTL_MS }
105
134
  }
106
135
 
107
136
  function stopPairing (token) { pending.delete(token) }
@@ -215,8 +244,9 @@ export function createEnrollDesk ({
215
244
  let acta = null
216
245
  try {
217
246
  if (typeof identity.admitMember === 'function') {
218
- const caps = scopeToCaps(pend.scope)
219
- if (caps.length) await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', caps, cert, continuity: pend.continuity || null })
247
+ const cn = scopeToCn(pend.scope)
248
+ const caps = cn ? ['secrets'] : scopeToCaps(pend.scope)
249
+ if (caps.length) await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
220
250
  }
221
251
  acta = (await identity.profileActa?.())?.acta || null
222
252
  } catch (e) { log('[vault] no se pudo admitir en el acta:', e.message) }
package/src/service.js CHANGED
@@ -249,7 +249,7 @@ export async function waitForSecrets ({ dir, ns, proxyUrl, masterPubkey, device,
249
249
  } catch (e) {
250
250
  // Lo NO transitorio no se arregla reintentando: falta de enrolamiento,
251
251
  // cert revocado/vencido o scope equivocado exigen re-emparejar → se corta.
252
- if (/sin enrolar|ns inválido|no autorizado: (revoked|expired|scope|untrusted-issuer|cert-device-mismatch)/.test(e.message)) throw e
252
+ if (/sin enrolar|ns inválido|no autorizado: (revoked|expired|scope|cn|untrusted-issuer|cert-device-mismatch)/.test(e.message)) throw e
253
253
  try { onRetry?.(e, delay) } catch (_) {}
254
254
  await new Promise((r) => setTimeout(r, delay))
255
255
  delay = Math.min(maxRetryMs, Math.round(delay * 1.6))