@dotrino/identity 0.22.1 → 0.24.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.22.1",
3
+ "version": "0.24.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",
@@ -21,7 +21,10 @@
21
21
  "import": "./vault/avatar.js"
22
22
  },
23
23
  "./vault/core.js": "./vault/core.js",
24
- "./vault/remote.js": "./vault/remote.js"
24
+ "./vault/remote.js": "./vault/remote.js",
25
+ "./acta": {
26
+ "import": "./vault/acta.js"
27
+ }
25
28
  },
26
29
  "files": [
27
30
  "src",
package/src/index.js CHANGED
@@ -256,6 +256,50 @@ export class Identity {
256
256
  return this._call('listDelegations')
257
257
  }
258
258
 
259
+ // ----- Acta de perfil: qué llaves son tuyas y qué puede hacer cada una -----
260
+ // Un perfil es un conjunto de llaves ligadas por certificados, con una política firmada
261
+ // por UN solo sellador (el «master»). Ninguna llave privada viaja nunca.
262
+ // Diseño: dotrino-vault/docs/acta-de-perfil.md
263
+
264
+ /** El acta vigente + si este dispositivo es el master + sus capacidades efectivas. */
265
+ async profileActa () { return this._call('profileActa') }
266
+
267
+ /** Miembros del perfil, ya con id legible y capacidades efectivas. */
268
+ async profileMembers () { return this._call('profileMembers') }
269
+
270
+ /** Dónde estoy yo: { inProfile, profileId, seq, isMaster, caps, id }. */
271
+ async myMembership () { return this._call('myMembership') }
272
+
273
+ /** ¿Es ESTE dispositivo el master (el único que puede cambiar el acta)? */
274
+ async isMaster () { return this._call('isMaster') }
275
+
276
+ /** Admite un miembro nuevo (solo el master). */
277
+ async admitMember (member) { return this._call('admitMember', member) }
278
+
279
+ /** Cambia las capacidades de un miembro (solo el master). */
280
+ async setCaps (pub, caps) { return this._call('setCaps', { pub, caps }) }
281
+
282
+ /** Expulsa a un miembro (solo el master; al master no se le puede expulsar). */
283
+ async removeMember (pub) { return this._call('removeMember', { pub }) }
284
+
285
+ /**
286
+ * Traspasa el master a otro miembro — dispositivo → bóveda, o bóveda → bóveda al mudarse
287
+ * de PC. Si `member` viene, se admite y se nombra en el MISMO seq (sin ventana intermedia).
288
+ */
289
+ async handoverMaster (to, member = null) { return this._call('handoverMaster', { to, member }) }
290
+
291
+ /**
292
+ * Este dispositivo se quita capacidades a sí mismo (p. ej. dejar de firmar). Es
293
+ * unilateral y funciona con la bóveda apagada, que es justo cuando hace falta.
294
+ */
295
+ async renounceCaps (caps) { return this._call('renounceCaps', { caps }) }
296
+
297
+ /** Absorbe en el acta una renuncia de otro miembro (solo el master). */
298
+ async absorbRenounce (record) { return this._call('absorbRenounce', { record }) }
299
+
300
+ /** Adopta un acta recibida de otro miembro (gana el seq mayor; a igual seq, el traspaso). */
301
+ async adoptActa (acta) { return this._call('adoptActa', { acta }) }
302
+
259
303
  // ----- Emparejar ESTE navegador/dispositivo con el vault del usuario (Fase 1) -----
260
304
 
261
305
  /**
@@ -319,11 +363,11 @@ export class Identity {
319
363
  }
320
364
 
321
365
  // ----- Self-vault: ESTE dispositivo actúa como su propia bóveda/CA -----
322
- // El daemon device-vault vive dentro del iframe (no requiere el binario del PC ni
323
- // vault.dotrino.com/pair). Cualquier app puede activarlo, generar códigos de
324
- // emparejamiento, aprobar SAS y revocar máquinas — todo por RPC al iframe.
325
- // El daemon sólo corre en una pestaña visible a la vez (navigator.locks), pero los
326
- // getters (status/pending/machines) y revoke sirven desde cualquier pestaña.
366
+ // El daemon device-vault vive dentro del iframe (no requiere el binario del PC).
367
+ // Se gestiona desde profile.dotrino.com/#myvault. Cualquier app puede activarlo,
368
+ // generar códigos de emparejamiento, aprobar SAS y revocar máquinas — todo por RPC
369
+ // al iframe. El daemon sólo corre en una pestaña visible a la vez (navigator.locks),
370
+ // pero los getters (status/pending/machines) y revoke sirven desde cualquier pestaña.
327
371
  /** { enabled, running }: si el modo self está activado y si esta pestaña sostiene el daemon. */
328
372
  async selfVaultStatus () { return this._call('selfVaultStatus') }
329
373
  /** Activa/desactiva el modo self-vault en este dispositivo. */
package/src/node.js CHANGED
@@ -150,6 +150,18 @@ export class Identity {
150
150
  signDelegation (sub, scope, opts = {}) { return this._h('signDelegation', { sub, scope, ...opts }) }
151
151
  revokeDelegation (nonce) { return this._h('revokeDelegation', { nonce }) }
152
152
  listDelegations () { return this._h('listDelegations') }
153
+ // Acta de perfil (qué llaves son del perfil y qué puede cada una; ver acta-de-perfil.md)
154
+ profileActa () { return this._h('profileActa') }
155
+ profileMembers () { return this._h('profileMembers') }
156
+ myMembership () { return this._h('myMembership') }
157
+ isMaster () { return this._h('isMaster') }
158
+ admitMember (member) { return this._h('admitMember', member) }
159
+ setCaps (pub, caps) { return this._h('setCaps', { pub, caps }) }
160
+ removeMember (pub) { return this._h('removeMember', { pub }) }
161
+ handoverMaster (to, member = null) { return this._h('handoverMaster', { to, member }) }
162
+ renounceCaps (caps) { return this._h('renounceCaps', { caps }) }
163
+ absorbRenounce (record) { return this._h('absorbRenounce', { record }) }
164
+ adoptActa (acta) { return this._h('adoptActa', { acta }) }
153
165
  // Emparejar ESTE dispositivo con el vault del usuario (Fase 1)
154
166
  enrollDevice (qr) { return this._h('vaultPair', { qr }) }
155
167
  vaultStatus () { return this._h('vaultStatus') }
package/vault/acta.js ADDED
@@ -0,0 +1,315 @@
1
+ /**
2
+ * acta.js — el ACTA DE PERFIL: qué llaves son del mismo perfil y qué puede hacer cada una.
3
+ *
4
+ * Un perfil no es una llave: es un CONJUNTO de llaves miembro, ligadas por certificados,
5
+ * más una política firmada. Ninguna llave privada viaja nunca; lo que se mueve es el acta.
6
+ * Diseño completo y decisiones en `dotrino-vault/docs/acta-de-perfil.md`.
7
+ *
8
+ * Las cuatro reglas que este módulo hace cumplir:
9
+ * 1. **Un solo sellador** (el «master»). Solo el sellador vigente puede producir la
10
+ * siguiente acta. Como las llaves son intransferibles no se puede clonar → dos actas
11
+ * legítimas con el mismo `seq` son imposibles: no hay bifurcaciones que resolver.
12
+ * 2. **`seq` monotónico + `prev`** (hash del acta anterior): cadena verificable.
13
+ * 3. **Nunca dejar el perfil sin quien firme**: un cambio que quite el último `sign` se
14
+ * rechaza.
15
+ * 4. **Renunciar es aparte**: un miembro se quita capacidades a sí mismo con un registro
16
+ * suelto que solo QUITA (§2.2). No toca el acta ni el `seq`, así que funciona con la
17
+ * bóveda apagada; el master lo absorbe después.
18
+ *
19
+ * DOS CAMPOS, NO UNO: `sealedBy` es la llave que FIRMÓ esta acta y `sealer` es quien queda
20
+ * como master de aquí en adelante. Normalmente coinciden; en un TRASPASO no, porque el acta
21
+ * que nombra al nuevo master la firma el saliente. Esa distinción es la que hace el traspaso
22
+ * auto-verificable: la firma del saliente es la prueba de su propia degradación.
23
+ *
24
+ * Los tiempos son epoch en ms (UTC) e INFORMATIVOS: la precedencia va por `seq` y por la
25
+ * regla de traspaso (§2.4.1), nunca por reloj — si dependiera de la hora, un master mintiendo
26
+ * reescribiría el orden cambiando el reloj de su máquina.
27
+ *
28
+ * Módulo PURO: sin kv, sin red, sin disco. Cripto de `./capabilities.js`.
29
+ */
30
+ import { canonicalStringify } from './core.js'
31
+ import { signWithDevice, verifyDeviceSig, pubkeyId } from './capabilities.js'
32
+
33
+ export const ACTA_V = 1
34
+
35
+ /** Lista CERRADA de capacidades. Sellar y admitir no están: eso es ser el master. */
36
+ export const CAPS = Object.freeze(['sign', 'store', 'read'])
37
+
38
+ /** Cada capacidad es uno de los scopes que ya existen en los certs (sin scopes nuevos). */
39
+ export const CAP_SCOPE = Object.freeze({ sign: 'vault:sign', store: 'vault:store', read: 'vault:read' })
40
+
41
+ const enc = (s) => new TextEncoder().encode(s)
42
+ const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('')
43
+ const isPub = (v) => typeof v === 'string' && v.length > 0
44
+ const cleanCaps = (caps) => [...new Set((Array.isArray(caps) ? caps : []).filter((c) => CAPS.includes(c)))].sort()
45
+
46
+ /** El acta SIN la firma: es lo que se sella y sobre lo que se calcula el hash. */
47
+ export function actaBody (acta) {
48
+ const { sig, ...body } = acta || {}
49
+ return body
50
+ }
51
+
52
+ /** Hash del acta (hex de SHA-256 sobre el cuerpo canónico). Es lo que apunta el `prev`. */
53
+ export async function actaHash (acta) {
54
+ return hex(await crypto.subtle.digest('SHA-256', enc(canonicalStringify(actaBody(acta)))))
55
+ }
56
+
57
+ /** Id legible de un miembro (mismo formato que el deviceId del emparejamiento). */
58
+ export async function memberId (pub) {
59
+ const id = (await pubkeyId(pub)).slice(0, 8).toUpperCase()
60
+ return id.slice(0, 4) + '-' + id.slice(4, 8)
61
+ }
62
+
63
+ /** ¿Esta acta es un traspaso? (la firmó uno y nombra master a otro) */
64
+ export const isHandover = (acta) => !!acta && acta.sealer !== acta.sealedBy
65
+
66
+ /**
67
+ * Acta de génesis: un perfil recién nacido tiene UN miembro (esta llave), que además es el
68
+ * sellador, con todas las capacidades. `profileId` = su pubkey → el nombre del perfil es
69
+ * estable para siempre y coincide con la identidad que el usuario ya tenía (cero migración).
70
+ */
71
+ export function genesisActa ({ pub, encPub = null, label = '', now = Date.now() }) {
72
+ if (!isPub(pub)) throw new Error('genesisActa: falta la pubkey de la génesis')
73
+ return {
74
+ v: ACTA_V,
75
+ profileId: pub,
76
+ sealer: pub,
77
+ sealedBy: pub,
78
+ seq: 1,
79
+ prev: null,
80
+ members: [{ pub, encPub, label: String(label || '').slice(0, 60), caps: [...CAPS], addedAt: now, cert: null }],
81
+ revoked: [],
82
+ renounced: [],
83
+ updatedAt: now
84
+ }
85
+ }
86
+
87
+ /** Comprobaciones de FORMA (sin cripto): que el acta sea un acta. */
88
+ export function checkShape (acta) {
89
+ if (!acta || typeof acta !== 'object') return 'no-acta'
90
+ if (acta.v !== ACTA_V) return 'version'
91
+ if (!isPub(acta.profileId) || !isPub(acta.sealer) || !isPub(acta.sealedBy)) return 'shape'
92
+ if (!Number.isInteger(acta.seq) || acta.seq < 1) return 'seq'
93
+ if (acta.seq > 1 && typeof acta.prev !== 'string') return 'prev'
94
+ if (!Array.isArray(acta.members) || acta.members.length === 0) return 'members'
95
+ for (const m of acta.members) {
96
+ if (!isPub(m?.pub) || !Array.isArray(m?.caps)) return 'member'
97
+ if (m.caps.some((c) => !CAPS.includes(c))) return 'cap-desconocida'
98
+ }
99
+ if (new Set(acta.members.map((m) => m.pub)).size !== acta.members.length) return 'miembro-duplicado'
100
+ if (!acta.members.some((m) => m.pub === acta.sealer)) return 'sealer-no-es-miembro'
101
+ if (!acta.members.some((m) => m.caps.includes('sign'))) return 'sin-firmante'
102
+ return null
103
+ }
104
+
105
+ /** Sella (firma) un acta. `privateKey` puede ser una CryptoKey no extractable. */
106
+ export async function sealActa ({ acta, privateKey, privateJwk }) {
107
+ const shape = checkShape(acta)
108
+ if (shape) throw new Error('acta inválida: ' + shape)
109
+ const { signature } = await signWithDevice({ privateKey, privateJwk, publickey: acta.sealedBy, data: actaBody(acta) })
110
+ return { ...acta, sig: signature }
111
+ }
112
+
113
+ /**
114
+ * Verifica un acta: forma + firma de `sealedBy`. Que ESE firmante estuviera autorizado no se
115
+ * decide aquí sino al adoptarla (`canAdopt`), comparándola con la que ya tienes.
116
+ * @returns {{ok:boolean, reason?:string}}
117
+ */
118
+ export async function verifyActa ({ acta, expectedProfileId } = {}) {
119
+ const shape = checkShape(acta)
120
+ if (shape) return { ok: false, reason: shape }
121
+ if (typeof acta.sig !== 'string') return { ok: false, reason: 'sin-firma' }
122
+ if (expectedProfileId != null && acta.profileId !== expectedProfileId) return { ok: false, reason: 'otro-perfil' }
123
+ const ok = await verifyDeviceSig({ publickey: acta.sealedBy, data: actaBody(acta), signature: acta.sig })
124
+ return ok ? { ok: true } : { ok: false, reason: 'firma-invalida' }
125
+ }
126
+
127
+ /**
128
+ * Aplica cambios y devuelve el acta SIGUIENTE, **sin firmar** (hay que `sealActa`).
129
+ * `by` es quien va a sellar: si no es el sellador vigente, se rechaza (regla 1).
130
+ *
131
+ * Cambios: `{op:'admit', member}` · `{op:'caps', pub, caps}` · `{op:'remove', pub}` ·
132
+ * `{op:'handover', to}` · `{op:'revoke', nonce, until}` · `{op:'renounce', record}`.
133
+ * Van en ARRAY porque hay combinaciones que deben ser atómicas: admitir al nuevo sellador y
134
+ * traspasarle el master ocurre en el MISMO `seq` (§2.1.3).
135
+ */
136
+ export async function applyChanges (acta, changes, { by, now = Date.now() } = {}) {
137
+ const shape = checkShape(acta)
138
+ if (shape) throw new Error('acta inválida: ' + shape)
139
+ if (!by) throw new Error('applyChanges: falta `by` (quién sella)')
140
+ if (by !== acta.sealer) throw new Error('solo el master puede cambiar el acta; este dispositivo no lo es')
141
+
142
+ const list = Array.isArray(changes) ? changes : [changes]
143
+ if (list.length === 0) throw new Error('applyChanges: no hay cambios')
144
+
145
+ const next = {
146
+ ...acta,
147
+ sealedBy: by,
148
+ seq: acta.seq + 1,
149
+ prev: await actaHash(acta),
150
+ members: acta.members.map((m) => ({ ...m, caps: [...m.caps] })),
151
+ revoked: [...(acta.revoked || [])],
152
+ renounced: [...(acta.renounced || [])],
153
+ updatedAt: now
154
+ }
155
+ delete next.sig
156
+
157
+ const find = (pub) => next.members.find((m) => m.pub === pub)
158
+
159
+ for (const ch of list) {
160
+ switch (ch?.op) {
161
+ case 'admit': {
162
+ const m = ch.member
163
+ if (!isPub(m?.pub)) throw new Error('admit: falta la pubkey del miembro')
164
+ if (find(m.pub)) throw new Error('admit: ese miembro ya está en el acta')
165
+ next.members.push({
166
+ pub: m.pub,
167
+ encPub: m.encPub || null,
168
+ label: String(m.label || '').slice(0, 60),
169
+ caps: cleanCaps(m.caps),
170
+ addedAt: now,
171
+ cert: m.cert || null
172
+ })
173
+ break
174
+ }
175
+ case 'caps': {
176
+ const m = find(ch.pub)
177
+ if (!m) throw new Error('caps: ese miembro no está en el acta')
178
+ m.caps = cleanCaps(ch.caps)
179
+ break
180
+ }
181
+ case 'remove': {
182
+ const i = next.members.findIndex((m) => m.pub === ch.pub)
183
+ if (i < 0) throw new Error('remove: ese miembro no está en el acta')
184
+ if (next.members[i].pub === next.sealer) throw new Error('remove: no puedes expulsar al master; primero traspasa el sellado')
185
+ next.members.splice(i, 1)
186
+ break
187
+ }
188
+ case 'handover': {
189
+ if (!find(ch.to)) throw new Error('handover: el nuevo master tiene que ser miembro (admítelo en el mismo cambio)')
190
+ next.sealer = ch.to
191
+ break
192
+ }
193
+ case 'revoke': {
194
+ if (typeof ch.nonce !== 'string') throw new Error('revoke: falta el nonce')
195
+ next.revoked.push({ nonce: ch.nonce, until: ch.until || (now + 30 * 24 * 60 * 60 * 1000) })
196
+ break
197
+ }
198
+ case 'renounce': {
199
+ // Absorbe un registro de renuncia ya verificado (§2.2): solo puede QUITAR.
200
+ const r = ch.record
201
+ const m = find(r?.member)
202
+ if (!m) break // el que renunció ya no está: nada que hacer
203
+ m.caps = m.caps.filter((c) => !(r.caps || []).includes(c))
204
+ next.renounced = next.renounced.filter((x) => x.member !== r.member)
205
+ next.renounced.push(r)
206
+ break
207
+ }
208
+ default:
209
+ throw new Error('cambio desconocido: ' + ch?.op)
210
+ }
211
+ }
212
+
213
+ // Poda: las revocaciones vencidas ya no hacen falta (el cert al que apuntan expiró).
214
+ next.revoked = next.revoked.filter((r) => !r.until || r.until > now)
215
+
216
+ // Reglas de cierre: sin firmante no se puede operar, y sin sellador no se puede cambiar.
217
+ if (!next.members.some((m) => m.caps.includes('sign'))) {
218
+ throw new Error('el cambio dejaría el perfil sin ningún miembro que pueda firmar')
219
+ }
220
+ if (!next.members.some((m) => m.pub === next.sealer)) {
221
+ throw new Error('el cambio dejaría el acta sin master')
222
+ }
223
+ return next
224
+ }
225
+
226
+ // ----- renuncia (§2.2): el único cambio que no pasa por el master -----
227
+
228
+ /** Crea el registro firmado con el que un miembro se QUITA capacidades a sí mismo. */
229
+ export async function makeRenounce ({ member, caps, privateKey, privateJwk, now = Date.now() }) {
230
+ const body = { op: 'renounce', member, caps: cleanCaps(caps), ts: now }
231
+ if (body.caps.length === 0) throw new Error('renuncia: no hay capacidades que quitar')
232
+ const { signature } = await signWithDevice({ privateKey, privateJwk, publickey: member, data: body })
233
+ return { ...body, sig: signature }
234
+ }
235
+
236
+ /**
237
+ * ¿Es válida esta renuncia? La firma tiene que ser del PROPIO miembro. Como solo puede
238
+ * quitar, cualquiera puede honrarla sin riesgo, sin esperar a que el master la selle.
239
+ */
240
+ export async function verifyRenounce (record) {
241
+ if (!record || record.op !== 'renounce' || !isPub(record.member) || typeof record.sig !== 'string') return false
242
+ if (!Array.isArray(record.caps) || record.caps.length === 0) return false
243
+ const { sig, ...body } = record
244
+ return verifyDeviceSig({ publickey: record.member, data: body, signature: sig })
245
+ }
246
+
247
+ /**
248
+ * Capacidad EFECTIVA de un miembro: lo que dice el acta menos lo que haya renunciado.
249
+ * `extraRenounces` son renuncias sueltas ya verificadas que todavía no absorbió el master.
250
+ */
251
+ export function effectiveCaps (acta, pub, extraRenounces = []) {
252
+ const m = (acta?.members || []).find((x) => x.pub === pub)
253
+ if (!m) return []
254
+ const quitadas = new Set()
255
+ for (const r of [...(acta.renounced || []), ...extraRenounces]) {
256
+ if (r?.member === pub) for (const c of (r.caps || [])) quitadas.add(c)
257
+ }
258
+ return m.caps.filter((c) => !quitadas.has(c))
259
+ }
260
+
261
+ /** ¿Puede este miembro hacer `cap` según el acta? (con el cert se cruza aparte: cert ∩ acta). */
262
+ export function memberCan (acta, pub, cap, extraRenounces = []) {
263
+ return effectiveCaps(acta, pub, extraRenounces).includes(cap)
264
+ }
265
+
266
+ // ----- adopción y empates (§2.4.1) -----
267
+
268
+ /**
269
+ * ¿Adopto `candidate` en lugar de `current`?
270
+ *
271
+ * · `seq` mayor y contigua → sí, si encadena (`prev` = hash de la mía) y la firmó quien
272
+ * era master en la mía (`sealedBy === current.sealer`).
273
+ * · `seq` mayor con HUECO (estuve apagado) → solo si la firmó el master que conozco; si
274
+ * hubo un traspaso durante el hueco no puedo comprobarlo y toca re-admitirse (§1.3).
275
+ * · MISMO `seq` → gana la que TRASPASA. Es el caso del master obsoleto: un vault restaurado
276
+ * de un respaldo anterior al traspaso sella su propio `seq` creyéndose master, pero el
277
+ * acta del traspaso la firmó él mismo y esa firma prueba su degradación. Si las dos
278
+ * traspasan a destinos distintos (master mintiendo), desempata el hash menor —
279
+ * determinista, y nada salva de un master hostil.
280
+ * · `seq` menor → nunca (jamás retroceder).
281
+ *
282
+ * @returns {Promise<{adopt:boolean, reason:string}>}
283
+ */
284
+ export async function canAdopt ({ candidate, current }) {
285
+ const v = await verifyActa({ acta: candidate, expectedProfileId: current?.profileId })
286
+ if (!v.ok) return { adopt: false, reason: v.reason }
287
+ if (!current) return { adopt: true, reason: 'sin-acta-previa' }
288
+
289
+ if (candidate.seq > current.seq) {
290
+ if (candidate.sealedBy !== current.sealer) return { adopt: false, reason: 'sellador-no-autorizado' }
291
+ if (candidate.seq === current.seq + 1 && candidate.prev !== await actaHash(current)) {
292
+ return { adopt: false, reason: 'no-encadena' }
293
+ }
294
+ return { adopt: true, reason: 'seq-mayor' }
295
+ }
296
+
297
+ if (candidate.seq === current.seq) {
298
+ const [hCan, hCur] = [await actaHash(candidate), await actaHash(current)]
299
+ if (hCan === hCur) return { adopt: false, reason: 'misma-acta' }
300
+ if (candidate.sealedBy !== current.sealedBy) return { adopt: false, reason: 'otro-sellador' }
301
+ const canT = isHandover(candidate)
302
+ const curT = isHandover(current)
303
+ if (canT && !curT) return { adopt: true, reason: 'traspaso-gana' }
304
+ if (!canT && curT) return { adopt: false, reason: 'traspaso-gana' }
305
+ return hCan < hCur ? { adopt: true, reason: 'desempate-hash' } : { adopt: false, reason: 'desempate-hash' }
306
+ }
307
+
308
+ return { adopt: false, reason: 'seq-menor' }
309
+ }
310
+
311
+ export default {
312
+ ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, isHandover,
313
+ sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
314
+ effectiveCaps, memberCan, canAdopt
315
+ }