@dotrino/identity 0.115.0 → 0.117.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 (82) hide show
  1. package/package.json +4 -1
  2. package/src/node.js +4 -180
  3. package/vault/content.js +28 -1
  4. package/vault/core.js +66 -7
  5. package/vault/facade.js +200 -0
  6. package/vault/index.html +10 -0
  7. package/vault/remote.js +45 -5
  8. package/vault/vault.js +29 -58
  9. package/vault/vendor/compat/VERSION.txt +3 -0
  10. package/vault/vendor/compat/advisory.js +141 -0
  11. package/vault/vendor/compat/index.js +188 -0
  12. package/vault/vendor/compat/ranges.js +93 -0
  13. package/vault/vendor/opaque/VERSION.txt +1 -1
  14. package/vault/vendor/passmanager/VERSION.txt +3 -0
  15. package/vault/vendor/passmanager/crypto.js +167 -0
  16. package/vault/vendor/passmanager/fields.js +183 -0
  17. package/vault/vendor/passmanager/generate.js +74 -0
  18. package/vault/vendor/passmanager/import.js +156 -0
  19. package/vault/vendor/passmanager/index.js +27 -0
  20. package/vault/vendor/passmanager/match.js +137 -0
  21. package/vault/vendor/passmanager/model.js +332 -0
  22. package/vault/vendor/passmanager/pubkey.js +33 -0
  23. package/vault/vendor/passmanager/sealed/convert.js +72 -0
  24. package/vault/vendor/passmanager/sealed/device.js +211 -0
  25. package/vault/vendor/passmanager/sealed/entry.js +209 -0
  26. package/vault/vendor/passmanager/sealed/index.js +20 -0
  27. package/vault/vendor/passmanager/sealed/keys.js +120 -0
  28. package/vault/vendor/passmanager/sealed/recovery.js +136 -0
  29. package/vault/vendor/passmanager/sealed/store.js +508 -0
  30. package/vault/vendor/passmanager/session-cache.js +60 -0
  31. package/vault/vendor/passmanager/totp.js +111 -0
  32. package/vault/vendor/passmanager/transport/protocol.js +83 -0
  33. package/vault/vendor/passmanager/transport/proxy.js +112 -0
  34. package/vault/vendor/passmanager/transport/sealed.js +74 -0
  35. package/vault/vendor/passmanager/vault/approval.js +63 -0
  36. package/vault/vendor/passmanager/vault/errors.js +34 -0
  37. package/vault/vendor/passmanager/vault/guard.js +114 -0
  38. package/vault/vendor/passmanager/vault/interface.js +34 -0
  39. package/vault/vendor/passmanager/vault/local.js +213 -0
  40. package/vault/vendor/passmanager/vault/remote.js +85 -0
  41. package/vault/vendor/passmanager/vault/responder.js +192 -0
  42. package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
  43. package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
  44. package/vault/vendor/passmanager/vault/sealed.js +362 -0
  45. package/vault/vendor/passmanager/webauthn.js +221 -0
  46. package/vault/vendor/proxy-client/VERSION.txt +1 -1
  47. package/vault/vendor/proxy-client/client.js +136 -11
  48. package/vault/vendor/proxy-client/index.js +1 -0
  49. package/vault/vendor/proxy-client/parts.js +127 -0
  50. package/vault/vendor/proxy-client/webrtc.js +23 -0
  51. package/vault/vendor/roadmap/VERSION.txt +3 -0
  52. package/vault/vendor/roadmap/index.js +108 -0
  53. package/vault/vendor/roadmap/manifests.js +802 -0
  54. package/vault/vendor/store/VERSION.txt +3 -0
  55. package/vault/vendor/store/core.js +309 -0
  56. package/vault/vendor/vault/VERSION.txt +4 -1
  57. package/vault/vendor/vault/admin.js +437 -0
  58. package/vault/vendor/vault/browser.js +167 -0
  59. package/vault/vendor/vault/core/approvals.js +204 -0
  60. package/vault/vendor/vault/core/blocked.js +68 -0
  61. package/vault/vendor/vault/core/browserDisk.js +109 -0
  62. package/vault/vendor/vault/core/commKey.js +68 -0
  63. package/vault/vendor/vault/core/disk.js +65 -0
  64. package/vault/vendor/vault/core/libVersion.js +8 -0
  65. package/vault/vendor/vault/core/machines.js +66 -0
  66. package/vault/vendor/vault/core/notices.js +71 -0
  67. package/vault/vendor/vault/core/notifiable.js +73 -0
  68. package/vault/vendor/vault/core/passwords.js +125 -0
  69. package/vault/vendor/vault/core/sealKey.js +77 -0
  70. package/vault/vendor/vault/core/sealer.js +172 -0
  71. package/vault/vendor/vault/core/sealers.js +159 -0
  72. package/vault/vendor/vault/core/secretsStore.js +1326 -0
  73. package/vault/vendor/vault/core/sha256.js +69 -0
  74. package/vault/vendor/vault/core/store.js +68 -0
  75. package/vault/vendor/vault/core/subacta.js +101 -0
  76. package/vault/vendor/vault/core/threadStore.js +273 -0
  77. package/vault/vendor/vault/core/transport.js +270 -0
  78. package/vault/vendor/vault/core/vault.js +4414 -0
  79. package/vault/vendor/vault/enroll.js +8 -4
  80. package/vault/vendor/vault/procContext.js +97 -0
  81. package/vault/vendor/vault/revocation.js +67 -0
  82. package/vault/vendor/vault/sealed.js +84 -0
@@ -0,0 +1,199 @@
1
+ // LA BÓVEDA SELLADA CUANDO QUIEN GUARDA Y QUIEN LEE SON EL MISMO PROCESO.
2
+ //
3
+ // `SealedVault` es el aparato hablando con una bóveda que está al otro lado de un
4
+ // transporte. Esto es el otro caso, y existe de verdad en dos sitios del ecosistema:
5
+ //
6
+ // · **`dotrino-passmanager serve`** — el mismo proceso atiende por el proxio y es
7
+ // también la línea de comandos con la que el dueño mira lo suyo;
8
+ // · **la bóveda de dentro de la extensión** — no hay nadie más a quien preguntar.
9
+ //
10
+ // En los dos, quien lee lo hace con UNA envoltura: la de `#recovery` (el dueño con su
11
+ // contraseña) o la del propio aparato. Se le dice cuál al construirla y no se adivina:
12
+ // adivinarlo sería leer con la que hubiera, y entonces «no puedo abrir esto» y «esto no es
13
+ // para mí» se verían igual.
14
+ //
15
+ // Cumple el contrato de siempre (`./interface.js`) para que nada de lo que hay encima
16
+ // tenga que aprender otra forma de pedir las cosas.
17
+
18
+ import { VaultError, CODES } from './errors.js'
19
+ import { findForUrl } from '../match.js'
20
+ import { siteIndex, fieldIndex, valueDigest } from '../sealed/keys.js'
21
+ import { buildSealedEntry, openSealedEntry, openSealedView } from '../sealed/device.js'
22
+ import { mergePatch } from '../model.js'
23
+
24
+ /** Los nombres FIJOS, que viajan tal cual: vocabulario cerrado e igual para todos. */
25
+ const FIXED = ['name', 'username', 'secret', 'totp', 'notes']
26
+
27
+ export class SealedLocalVault {
28
+ /**
29
+ * @param {SealedStore} sealed el almacén de sobres
30
+ * @param {object} opts
31
+ * `readerPub` con qué envoltura se lee: `RECOVERY` o la pública de este aparato
32
+ * `openSealed({ wrap, envelope })` abre un sobre. La privada no sale de donde viva.
33
+ * `author` `{ publickey, sign(body) }` quien firma lo que se escribe
34
+ * `recipients()` → `{ recoveryPub, main, passkeys }` a quién se le envuelve
35
+ * `keys()` → las llaves del perfil (`profileKeys`), para huellas y resúmenes
36
+ */
37
+ constructor (sealed, { readerPub, openSealed, author, recipients, keys } = {}) {
38
+ if (!sealed) throw new Error('SealedLocalVault: missing the sealed store')
39
+ if (!readerPub) throw new Error('SealedLocalVault: say which wrap this one reads with')
40
+ for (const n of ['openSealed', 'author', 'recipients', 'keys']) {
41
+ if (!arguments[1]?.[n]) throw new Error(`SealedLocalVault: missing ${n}`)
42
+ }
43
+ this.sealed = sealed
44
+ this.readerPub = readerPub
45
+ this.openSealed = openSealed
46
+ this.author = author
47
+ this.recipients = recipients
48
+ this.keys = keys
49
+ }
50
+
51
+ get capabilities () {
52
+ // `canList` es `true` y aquí sí es verdad: quien pregunta es el dueño en su propia
53
+ // máquina, no un aparato pidiendo por el proxio. Lo que no existe en remoto sigue sin
54
+ // existir — esto no se asoma por ningún transporte.
55
+ return { canWrite: true, canList: true, needsApproval: false, sealed: true }
56
+ }
57
+
58
+ /** De la clave que usa la interfaz a la que guarda la bóveda. La misma que `SealedVault`. */
59
+ async #sealedKey (k) {
60
+ if (FIXED.includes(k) || k.startsWith('wa.')) return k
61
+ const { kidx } = await this.keys()
62
+ return 'f:' + await fieldIndex(kidx, k)
63
+ }
64
+
65
+ /** Las vistas, abiertas. Es lo que se enseña en una lista: sin valores privados. */
66
+ async list () {
67
+ const vistas = await this.sealed.views({ pub: this.readerPub })
68
+ const out = []
69
+ for (const v of vistas) {
70
+ const abierta = await openSealedView({ ...v, openSealed: this.openSealed })
71
+ // `fieldHashes` con el nombre que ya usa la interfaz para comparar sin abrir nada.
72
+ // Los resúmenes están GUARDADOS y su clave viaja por huella, así que aquí se
73
+ // traducen de vuelta — la vista sí lleva los nombres de verdad, porque va sellada.
74
+ // Sin esto, «¿ya está guardado igual?» se contesta que no siempre, y el aviso de
75
+ // guardar sale en cada formulario que ya tenías.
76
+ const fieldHashes = {}
77
+ for (const k of [...(abierta.fieldKeys || []), 'notes']) {
78
+ const clave = await this.#sealedKey(k)
79
+ if (abierta.digests?.[clave] !== undefined) fieldHashes[k] = abierta.digests[clave]
80
+ }
81
+ out.push({ ...abierta, fieldHashes })
82
+ }
83
+ return out
84
+ }
85
+
86
+ /**
87
+ * El resumen de un valor, para decir si ya está guardado igual **sin abrir nada** (§2.6).
88
+ * No hay nonce que compartir: el `id` de la entrada va dentro, que es lo que impide que
89
+ * un valor repetido en dos entradas se note.
90
+ */
91
+ async digest (id, key, value) {
92
+ const { kcmp } = await this.keys()
93
+ return valueDigest(kcmp, id, key, String(value))
94
+ }
95
+
96
+ /**
97
+ * Lo que hay para una URL, ya ORDENADO por lo bien que casa: las entradas, no los pares
98
+ * `{entry, match}` de `findForUrl`. Es la misma forma que devuelve `SealedVault.find`,
99
+ * y tiene que serlo — quien llama no debería notar de qué bóveda le están contestando.
100
+ */
101
+ async find (url) {
102
+ return findForUrl(await this.list(), url).map((x) => x.entry)
103
+ }
104
+
105
+ /**
106
+ * EN QUÉ SITIOS HAY ALGO, y cuánto en cada uno. Misma forma que `LocalVault.sites()`
107
+ * —`[{ site, count }]`— porque es lo que pinta el gestor: devolver una lista de cadenas
108
+ * dejaba la fila de dominios vacía sin un solo error.
109
+ *
110
+ * El sitio vacío es «sirve en cualquier parte» y va al final, que es donde se busca.
111
+ */
112
+ async sites () {
113
+ const cuenta = new Map()
114
+ for (const e of await this.list()) {
115
+ for (const s of (e.sites?.length ? e.sites : [''])) cuenta.set(s, (cuenta.get(s) || 0) + 1)
116
+ }
117
+ return [...cuenta.entries()]
118
+ .map(([site, count]) => ({ site, count }))
119
+ .sort((a, b) => (a.site === '' ? 1 : b.site === '' ? -1 : a.site.localeCompare(b.site)))
120
+ }
121
+
122
+ /**
123
+ * BUSCAR por texto. Se mira lo mismo que mira `SealedVault`: el nombre, el SITIO y la
124
+ * **pista** —el usuario tapado que la vista lleva—, que es por lo que la gente busca de
125
+ * verdad («ana@…»). Dejarla fuera hacía que buscar la cuenta de otro dominio no
126
+ * encontrara nada, sin un solo error.
127
+ *
128
+ * Se busca sobre las VISTAS, no sobre los valores: aquí no se abre ninguna contraseña
129
+ * para contestar a un buscador.
130
+ */
131
+ async search (texto, { limit = 20 } = {}) {
132
+ const q = String(texto || '').trim().toLowerCase()
133
+ if (!q) return []
134
+ const out = []
135
+ for (const v of await this.list()) {
136
+ if (out.length >= limit) break
137
+ if ([v.title, v.hint, ...(v.sites || [])].filter(Boolean).join(' ').toLowerCase().includes(q)) out.push(v)
138
+ }
139
+ return out
140
+ }
141
+
142
+ /** Una entrada ENTERA, o solo los campos que se pidan. */
143
+ async get (id, { keys = null } = {}) {
144
+ let got
145
+ try { got = await this.sealed.get(id, { pub: this.readerPub, keys }) }
146
+ catch (e) {
147
+ if (e?.code === 'not-found') throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
148
+ // «no me la envolvieron a mí» no es «no existe», y confundirlas manda a buscar el
149
+ // problema al sitio equivocado.
150
+ if (e?.code === 'not-yours') throw new VaultError(CODES.NOT_ALLOWED, 'esta punta no tiene llave para esa entrada')
151
+ throw e
152
+ }
153
+ return openSealedEntry({ got, openSealed: this.openSealed })
154
+ }
155
+
156
+ /**
157
+ * GUARDAR. Se cierra aquí y sale cerrado: la entrada se cifra, su llave se envuelve a
158
+ * los destinatarios que diga el acta AHORA y el conjunto va firmado.
159
+ *
160
+ * Los destinatarios se piden en cada escritura y no se cachean: un aparato que acaba de
161
+ * entrar tiene que poder leer lo siguiente que se guarde.
162
+ */
163
+ async put (entry) {
164
+ const sobre = await buildSealedEntry({
165
+ plain: entry,
166
+ keys: await this.keys(),
167
+ recipients: await this.recipients(),
168
+ author: this.author
169
+ })
170
+ await this.sealed.putSealed(sobre)
171
+ // La VISTA, como devuelve `LocalVault.put`: quien llama enseña el nombre de lo que
172
+ // acaba de guardar. Devolver solo el id dejaba un «Guardada: undefined» en la CLI.
173
+ return { id: sobre.entry.id, title: entry.title || '', sites: entry.sites || [] }
174
+ }
175
+
176
+ /**
177
+ * Cambiar unos campos. Se hace leyendo y volviendo a escribir porque aquí quien lee y
178
+ * quien escribe son el mismo: no hay nada que ahorrar ocultándose un valor a sí mismo.
179
+ * (En remoto SÍ lo hay, y por eso allí existe `patchSealed`.)
180
+ *
181
+ * La fusión la hace `mergePatch`, la MISMA que `LocalVault` — y tiene que ser la misma,
182
+ * porque no es un `{...a, ...b}`: un campo sin `value` dice «déjale el que tenga», no
183
+ * «bórralo». Marcar uno como privado llega así, y fusionarlo a lo bruto le borraba el
184
+ * valor. Costó encontrarlo porque no falla: guarda, y el campo ya no está.
185
+ */
186
+ async patch (id, changes = {}) {
187
+ return this.put(mergePatch(await this.get(id), changes))
188
+ }
189
+
190
+ async remove (id) { return this.sealed.remove(id) }
191
+
192
+ /** La huella de un sitio, por si quien llama quiere preguntar por ella directamente. */
193
+ async siteIndexOf (site) {
194
+ const { kidx } = await this.keys()
195
+ return siteIndex(kidx, site)
196
+ }
197
+ }
198
+
199
+ export default { SealedLocalVault }
@@ -0,0 +1,159 @@
1
+ // EL LADO QUE RESPONDE, con el formato sellado.
2
+ //
3
+ // Hace lo mismo que `VaultResponder` —es donde vive la política: quién puede pedir, qué
4
+ // exige un dedo encima y qué queda anotado— con una diferencia que lo cambia todo: **esta
5
+ // bóveda no puede abrir nada**. Así que lo que entrega son sobres, y lo que comprueba es lo
6
+ // que se puede comprobar sin ninguna llave.
7
+ //
8
+ // Va aparte del responder de siempre y no dentro de él porque el protocolo es OTRO
9
+ // (`pm2.*`, ver `../transport/protocol.js`): mezclarlos daría una clase con dos modos, que
10
+ // es como se acaba sirviendo el formato viejo «mientras tanto» — o sea, el agujero otra vez.
11
+
12
+ import { isRequest, reply, replyError, SEALED_OPS } from '../transport/protocol.js'
13
+ import { ApprovalGate } from './approval.js'
14
+ import { ALWAYS_PRIVATE } from '../fields.js'
15
+ import { CODES } from './errors.js'
16
+ import { PASSKEY_FIELD } from '../sealed/entry.js'
17
+
18
+ /** Lo que es privado POR LO QUE ES, sin mirar ninguna marca. */
19
+ const SIEMPRE_PRIVADO = new Set([...ALWAYS_PRIVATE.filter((k) => k !== 'webauthn'), PASSKEY_FIELD, 'wa.userHandle'])
20
+
21
+ export class SealedResponder {
22
+ /**
23
+ * @param {object} opts
24
+ * `client` cliente de `@dotrino/proxy-client` conectado e identificado
25
+ * `store` una `SealedStore`
26
+ * `recipients()` → `{ recoveryPub, main, passkeys }`, del acta
27
+ * `isAllowed(pubkey)` quién puede pedir (el acta: `passwords`). Por defecto NADIE.
28
+ * `needsApproval(pubkey)` si ese aparato tiene que pedir permiso (el acta:
29
+ * `unattended`). Se compone con el criterio de abajo: **solo lo privado pregunta**.
30
+ * `approve({ op, payload, pubkey })` pide el visto bueno y espera
31
+ * `encPubOf(pubkey)` para sellarle la respuesta
32
+ * `onRequest(record)` la bitácora
33
+ */
34
+ constructor ({ client, store, recipients, isAllowed, needsApproval, approve, encPubOf, onRequest } = {}) {
35
+ this.client = client
36
+ this.store = store
37
+ this.recipients = recipients || (async () => ({ recoveryPub: null, main: [], passkeys: [] }))
38
+ this.isAllowed = isAllowed || (() => false)
39
+ this.encPubOf = encPubOf || (() => null)
40
+ this.needsApproval = needsApproval || (async () => true)
41
+ this.approve = approve || (async () => false)
42
+ this.onRequest = onRequest || (() => {})
43
+ this._handler = null
44
+ this.gate = new ApprovalGate({
45
+ ask: ({ op, payload, pubkey }) => this.approve({ op, payload, pubkey }),
46
+ scope: ({ pubkey }) => pubkey
47
+ })
48
+ }
49
+
50
+ revokeApproval (pubkey) { this.gate.revoke(pubkey || null) }
51
+ isApproved (pubkey) { return this.gate.granted(pubkey) }
52
+
53
+ /**
54
+ * ¿LO QUE SE PIDE ES PRIVADO? Aquí se contesta SIN abrir nada, y por eso hace falta la
55
+ * lista en claro de la entrada (`priv`): la bóveda no ve la vista.
56
+ *
57
+ * Sin `keys` se pide la entrada entera, y eso siempre es privado: pedir todo es pedir la
58
+ * contraseña.
59
+ */
60
+ async wantsPrivate (op, payload) {
61
+ if (op !== 'pm2.get') return false
62
+ const keys = payload?.keys
63
+ if (!Array.isArray(keys)) return true
64
+ const priv = new Set(await this.store.privateKeysOf(payload?.id))
65
+ return keys.some((k) => SIEMPRE_PRIVADO.has(k) || priv.has(k))
66
+ }
67
+
68
+ start () {
69
+ if (this._handler) return
70
+ this._handler = async (from, msg, meta) => {
71
+ const p = typeof msg === 'string' ? (() => { try { return JSON.parse(msg) } catch (_) { return null } })() : msg
72
+ if (!isRequest(p)) return
73
+ // QUIÉN PREGUNTA lo dice el proxio (`fromPubkey`: la llave con la que se identificó esa
74
+ // conexión) o un saludo previo (`pubkeyOfToken`). Aquí se leía `meta.pubkey`, que no
75
+ // existe, y se caía al token: ninguna bóveda sabía a quién sellarle la respuesta, no
76
+ // contestaba, y del otro lado se veía «nadie respondió». Sin llave no hay a quién
77
+ // contestar sellado, así que no se contesta — el token no es una llave.
78
+ const pubkey = meta?.fromPubkey || this.client?.pubkeyOfToken?.(from) || null
79
+ if (!pubkey) { this.onRequest({ op: p.op, outcome: 'anonymous', from: null }); return }
80
+ await this.handle({ from, pubkey, msg: p, sealed: meta?.sealed === true })
81
+ }
82
+ this.client.on('message', this._handler)
83
+ }
84
+
85
+ stop () {
86
+ if (!this._handler) return
87
+ this.client.off?.('message', this._handler)
88
+ this._handler = null
89
+ }
90
+
91
+ /** Contesta UNA petición. Público para que las pruebas no tengan que fingir un socket. */
92
+ async handle ({ from, pubkey, msg, sealed = true }) {
93
+ const { rid, op, payload } = msg
94
+ const anota = (outcome, extra = {}) => this.onRequest({ op, outcome, from: pubkey, ...extra })
95
+ // EL SELLADO DEL TRANSPORTE NO ES OPCIONAL (CONVENCIONES §4.1): lo que llega en claro
96
+ // no se atiende, porque contestarlo sería mandar sobres a quien no sabemos que es.
97
+ //
98
+ // Los códigos son los del responder de siempre (`responder.js`): «la política dice que
99
+ // no» es `denied` y «el humano dijo que no» es `not-approved`. Se arreglan distinto —uno
100
+ // dando permiso al aparato, el otro volviendo a pedir— y quien pide los distingue así.
101
+ if (!sealed) { anota('unsealed'); return this.#send(from, pubkey, replyError(rid, CODES.UNSEALED, 'unsealed request')) }
102
+ if (!SEALED_OPS.includes(op)) { anota('unknown-op'); return this.#send(from, pubkey, replyError(rid, CODES.DENIED, 'operation not allowed: ' + op)) }
103
+ if (!this.isAllowed(pubkey)) { anota('denied'); return this.#send(from, pubkey, replyError(rid, CODES.DENIED, 'this device cannot ask for passwords')) }
104
+
105
+ try {
106
+ if (await this.#needsApproval(op, payload, pubkey)) {
107
+ // `allow`, no `ask`: es el que RECUERDA el sí y junta las simultáneas. `ask` es la
108
+ // función cruda de preguntar, y usarla directamente hacía sonar el teléfono en cada
109
+ // petición — que es como se enseña a aprobar sin mirar.
110
+ const ok = await this.gate.allow({ op, payload, pubkey })
111
+ if (!ok) { anota('rejected'); return this.#send(from, pubkey, replyError(rid, CODES.NOT_APPROVED, 'not approved on the trusted device')) }
112
+ }
113
+ const result = await this.#run(op, payload, pubkey)
114
+ anota('ok')
115
+ return this.#send(from, pubkey, reply(rid, result))
116
+ } catch (e) {
117
+ anota('error', { code: e?.code })
118
+ return this.#send(from, pubkey, replyError(rid, e?.code || CODES.UNKNOWN, e?.message || 'error'))
119
+ }
120
+ }
121
+
122
+ async #needsApproval (op, payload, pubkey) {
123
+ return await this.needsApproval(pubkey) && await this.wantsPrivate(op, payload)
124
+ }
125
+
126
+ async #run (op, payload = {}, pubkey) {
127
+ switch (op) {
128
+ case 'pm2.recipients': return this.recipients()
129
+ case 'pm2.profile':
130
+ // Con `set` se estrena (al convertir); sin él, se pide la que hay.
131
+ if (payload.set) return this.store.setProfile(payload.set)
132
+ return this.store.profile({ pub: pubkey })
133
+ case 'pm2.views': return this.store.views({ pub: pubkey })
134
+ case 'pm2.find': return this.store.find({ pub: pubkey, idx: payload.idx || [] })
135
+ case 'pm2.passkey': return this.store.findPasskey({ pub: pubkey, rp: payload.rp, cred: payload.cred })
136
+ case 'pm2.get': return this.store.get(payload.id, { pub: pubkey, keys: payload.keys })
137
+ case 'pm2.put': return this.store.putSealed({ ...payload, by: pubkey })
138
+ case 'pm2.patch': return this.store.patchSealed({ ...payload, by: pubkey })
139
+ case 'pm2.remove': return this.store.remove(payload.id)
140
+ default: throw Object.assign(new Error('operation not allowed: ' + op), { code: CODES.DENIED })
141
+ }
142
+ }
143
+
144
+ /** `to` es por dónde se contesta (el token); `pubkey`, a quién se le sella. */
145
+ #send (to, pubkey, msg) {
146
+ const encPub = this.encPubOf(pubkey)
147
+ // La respuesta va SELLADA: el proxio enruta pero no cifra. Si no se puede sellar, no se
148
+ // manda — mandarlo en claro sería entregar los sobres y las envolturas a quien mire.
149
+ if (encPub && typeof this.client.sendSealedTo === 'function') {
150
+ return this.client.sendSealedTo(to, msg, { peerEncPub: encPub })
151
+ }
152
+ if (typeof this.client.sendSealed === 'function' && encPub) {
153
+ return this.client.sendSealed([to], msg, { peerEncPub: encPub })
154
+ }
155
+ throw Object.assign(new Error('cannot seal the reply to that device: it has no encryption key in the record'), { code: 'no-encpub' })
156
+ }
157
+ }
158
+
159
+ export default SealedResponder