@dotrino/content 0.4.2 → 0.5.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/README.md CHANGED
@@ -47,6 +47,28 @@ Requiere **Node ≥ 22.5**. El core usa solo `node:crypto` y `node:sqlite`; la
47
47
  identidad viene de los pilares del ecosistema (`@dotrino/remote-agent`,
48
48
  `@dotrino/identity`).
49
49
 
50
+ ## Actualizarse
51
+
52
+ El node **se actualiza solo** (CONVENCIONES §15): mira una vez al día si hay versión nueva,
53
+ comprueba lo que entrega npm contra el `npm-integrity.json` de la release de este repo y
54
+ lo instala. Lo hace `@dotrino/update/npm`; aquí solo se cablea.
55
+
56
+ ```sh
57
+ dotrino-content update # dice cómo está
58
+ dotrino-content update --approval on # antes de instalar, pide permiso a quien aprueba en tu bóveda
59
+ dotrino-content update --notify off # no avisa de que se actualizó
60
+ ```
61
+
62
+ - Los dos ajustes son de **esta instancia** (viven junto a su enlace) y no tienen que ver
63
+ con el de la bóveda. Por defecto: se actualiza sin preguntar y avisa.
64
+ - El permiso se pide **una vez por versión**; sin respuesta en un día cuenta como no, y
65
+ `dotrino-content info` lo dice.
66
+ - **Solo se actualiza sola una instalación global de npm en un prefijo del usuario**
67
+ (`npm i -g @dotrino/content` con nvm o un prefijo propio). Si el prefijo es de root, avisa a
68
+ quien aprueba de que hay versión y necesita permisos de administrador. Con `npx`, dentro
69
+ de un proyecto o desde un checkout no se toca nada.
70
+ - Solo se reinicia si hay quien lo levante (systemd, pm2).
71
+
50
72
  ## Plano de control (Fase 2, por el proxy, cifrado)
51
73
 
52
74
  Con el node enlazado, tus propias apps —cualquier aparato con un certificado de **la
package/bin/cli.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * CLI de dotrino-content.
4
4
  *
5
5
  * dotrino-content enroll <código> enlaza este node a tu vault (una vez)
6
+ * dotrino-content update [--approval on|off] [--notify on|off]
7
+ * cómo se actualiza este node (CONVENCIONES §15)
6
8
  * dotrino-content start [--port 3777] [--dir <ruta>] [--max-gb <n>] [--gc-min <min>]
7
9
  * [--no-agent] arranca sin el plano de control
8
10
  * [--public] abre el puerto de VISTAS PREVIAS (§7.2)
@@ -34,11 +36,26 @@ const USAGE = `uso:
34
36
  dotrino-content enroll <código>
35
37
  dotrino-content info [--json] qué aparato es este node: su ID (el de «dotrino-vault members»),
36
38
  su bóveda y sus permisos. Sin red.
39
+ dotrino-content update [--approval on|off] [--notify on|off]
40
+ este node se actualiza solo. --approval on: antes pide permiso a
41
+ quien aprueba en tu bóveda. --notify off: no avisa de que se
42
+ actualizó. Sin opciones, dice cómo está.
37
43
  dotrino-content start [--port 3777] [--dir <ruta>] [--max-gb <n>] [--max-blob-mb <n>] [--gc-min <min>] [--no-agent]
38
44
  [--public] [--public-port 3778] [--public-host 0.0.0.0] [--public-max-kb 512]
39
45
  [--public-rate 60] [--public-egress-gb <n>] [--public-url https://…] [--app-url https://…]
40
46
  [--public-index]`
41
47
 
48
+ // `update`: los dos ajustes de actualización de ESTA instancia (CONVENCIONES §15). Va antes
49
+ // de `parseArgs` porque `--approval` y `--notify` se pueden dar sin valor (para preguntar
50
+ // cómo están), y `parseArgs` no admite una opción que a veces lleva valor y a veces no.
51
+ if (process.argv[2] === 'update') {
52
+ const { updatePrefsCommand } = await import('@dotrino/update/npm')
53
+ const r = updatePrefsCommand(process.argv.slice(3), { dir: linkDir(), lang: 'es' })
54
+ if (!r.handled) { console.error(`uso: dotrino-content update [--approval on|off] [--notify on|off]`); process.exit(2) }
55
+ ;(r.ok ? console.log : console.error)(r.text)
56
+ process.exit(r.ok ? 0 : 2)
57
+ }
58
+
42
59
  const { values, positionals } = parseArgs({
43
60
  allowPositionals: true,
44
61
  options: {
@@ -84,6 +101,13 @@ if (cmd === 'info') {
84
101
  if (!link) { console.error(`este node no está enlazado (${dir}): dotrino-content enroll <código>`); process.exit(1) }
85
102
  const info = await deviceInfo(link, { kind: 'content', ns: 'content', version: VERSION, dir })
86
103
  console.log(values.json ? JSON.stringify(info, null, 2) : formatDeviceInfo(info))
104
+ // Y si hay una actualización que no se instaló (no se aprobó, o necesita root), se dice
105
+ // aquí: es donde se mira qué es este aparato. Fuera del JSON, que lo lee una máquina.
106
+ if (!values.json) {
107
+ const { updateStatusText } = await import('@dotrino/update/npm')
108
+ const estado = updateStatusText({ dir, current: VERSION, lang: 'es' })
109
+ if (estado) console.log('\n' + estado)
110
+ }
87
111
  process.exit(0)
88
112
  }
89
113
 
@@ -172,13 +196,6 @@ server.listen(port, '127.0.0.1', () => {
172
196
  console.log(`blobs: ${s.blobs} · bytes: ${s.bytes}${maxBytes ? ` / ${maxBytes}` : ''}`)
173
197
  })
174
198
 
175
- // §15: una vez al día mira si hay versión nueva y lo dice en el log. Solo avisa.
176
- const { watchForUpdate } = await import('@dotrino/update')
177
- watchForUpdate({
178
- current: VERSION, source: 'npm', pkg: '@dotrino/content',
179
- onNewer: (r) => console.log(`[content] version ${r.version} is available (running ${r.current}): npm i -g @dotrino/content@${r.version}`)
180
- })
181
-
182
199
  // Plano de control: solo si este node ya está enlazado a un vault. Sin enlace sigue
183
200
  // siendo lo de antes (un node local), y se dice en voz alta para que nadie crea que
184
201
  // tiene administración remota cuando no la tiene.
@@ -226,11 +243,29 @@ if (values.public) {
226
243
  })
227
244
  }
228
245
 
246
+ /** @type {null | (() => void)} */
247
+ let stopSelfUpdate = null
229
248
  const shutdown = () => {
230
249
  clearInterval(gcTimer)
250
+ try { stopSelfUpdate?.() } catch (_) {}
231
251
  try { agent?.close() } catch (_) {}
232
252
  try { publicServer?.close() } catch (_) {}
233
253
  server.close(() => { node.close(); process.exit(0) })
234
254
  }
235
255
  process.on('SIGINT', shutdown)
236
256
  process.on('SIGTERM', shutdown)
257
+
258
+ // §15: SE ACTUALIZA SOLO. Mira al arrancar y una vez al día; lo que baja se comprueba
259
+ // contra la release de GitHub antes de tocar el disco, y solo se reinicia si hay quien lo
260
+ // levante. Los dos ajustes (`dotrino-content update`) son de esta instancia. Va al final
261
+ // porque reiniciar es `shutdown`. Desde un checkout (el node de Dotrino, que se despliega
262
+ // con git) no se toca nada: lo dice una vez y sigue.
263
+ const { startSelfUpdate } = await import('../src/selfUpdate.js')
264
+ const { serviceDir } = await import('../src/vaultEnv.js')
265
+ stopSelfUpdate = startSelfUpdate({
266
+ version: VERSION,
267
+ dir: linkDir(),
268
+ conn: isEnrolled() ? () => ({ dir: serviceDir() }) : null,
269
+ restart: shutdown,
270
+ log
271
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/content",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Nodo de contenido del ecosistema Dotrino: guarda los bytes del usuario direccionados por su hash (cid), en su propia maquina y, si el dueno quiere, respaldados en su propio bucket (R2, Backblaze, Hetzner, Storj o S3). Lo publico sale por URL directa; lo privado, cifrado y solo por la red Dotrino.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,9 +25,9 @@
25
25
  "dependencies": {
26
26
  "@dotrino/identity": "0.105.0",
27
27
  "@dotrino/proxy-client": "0.25.0",
28
- "@dotrino/remote-agent": "0.12.0",
29
- "@dotrino/update": "0.2.2",
30
- "@dotrino/vault": "0.79.0"
28
+ "@dotrino/remote-agent": "0.13.0",
29
+ "@dotrino/update": "0.5.0",
30
+ "@dotrino/vault": "0.82.0"
31
31
  },
32
32
  "keywords": [
33
33
  "dotrino",
@@ -0,0 +1,81 @@
1
+ /**
2
+ * selfUpdate.js — este node SE ACTUALIZA SOLO (CONVENCIONES §15, dueño 2026-10-08).
3
+ *
4
+ * No implementa nada de eso: mirar, comprobar lo que entrega npm contra la release de
5
+ * GitHub, instalar y recordar lo ya preguntado es de `@dotrino/update/npm`; preguntarle a
6
+ * la bóveda y avisarle es de `@dotrino/vault/service`. Aquí solo está el pegamento, y es
7
+ * lo único que conoce las dos piezas a la vez.
8
+ *
9
+ * Los dos ajustes son de ESTA instancia (`update-prefs.json` en su carpeta) y no tienen
10
+ * que ver con el de la bóveda:
11
+ * · `approval` (apagado): encendido, antes de instalar se le pregunta a la bóveda.
12
+ * · `notify` (encendido): se avisa a quien aprueba de que el node se actualizó, o de
13
+ * que hay versión nueva y esta instalación necesita root.
14
+ *
15
+ * SIN BÓVEDA NO HAY A QUIÉN PREGUNTAR NI A QUIÉN AVISAR, y se dice tal cual: no se pasan
16
+ * los ganchos, así que con `approval` encendida y sin enrolar el resultado es
17
+ * `no-approver` y no se instala. Suponer un sí sería saltarse lo que el dueño pidió.
18
+ */
19
+ import { watchSelfUpdateNpm } from '@dotrino/update/npm'
20
+ import { askUpdateApproval, reportUpdated, reportUpdateNeedsRoot } from '@dotrino/vault/service'
21
+
22
+ export const PKG = '@dotrino/content'
23
+ export const REPO = 'imdotrino/dotrino-content'
24
+
25
+ /**
26
+ * Los tres ganchos que hablan con la bóveda. `conn()` da, EN EL MOMENTO de llamar, con
27
+ * qué identidad se le habla (`{ dir }` o `{ proxyUrl, masterPubkey, device, cert }`): el
28
+ * papel se renueva con el proceso en marcha y uno leído al arrancar ya no valdría.
29
+ *
30
+ * @param {{ product: string, conn: () => object, vault?: any }} o
31
+ */
32
+ export function vaultUpdateHooks ({ product, conn, vault = { askUpdateApproval, reportUpdated, reportUpdateNeedsRoot } }) {
33
+ return {
34
+ // true = sí · false = no, o pasó el día sin respuesta · lanza = no se pudo preguntar
35
+ // (eso no es una negativa: `@dotrino/update` lo reintenta en la próxima pasada).
36
+ mayUpdate: async ({ version, from }) => {
37
+ try {
38
+ return (await vault.askUpdateApproval({ product, version, from, ...conn() })).ok === true
39
+ } catch (e) {
40
+ // El pedido quedó pendiente y venció sin que nadie contestara: vencido es no.
41
+ if (e?.code === 'unanswered') return false
42
+ throw e
43
+ }
44
+ },
45
+ onUpdated: ({ version, from }) => vault.reportUpdated({ product, version, from, ...conn() }),
46
+ onNeedsRoot: ({ version, from }) => vault.reportUpdateNeedsRoot({ product, version, from, ...conn() })
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Arranca el vigilante. Devuelve cómo pararlo.
52
+ *
53
+ * @param {{
54
+ * version: string, dir: string, conn?: (() => object) | null,
55
+ * restart: (version: string) => void, log?: (m: string) => void,
56
+ * watch?: typeof watchSelfUpdateNpm, vault?: any
57
+ * }} o
58
+ * dir: la carpeta del enlace de esta instancia (ahí viven sus ajustes de actualización).
59
+ * conn: cómo hablarle a la bóveda, o `null` si este node no está enrolado.
60
+ * restart: salir limpio para que systemd/pm2 levante la versión nueva.
61
+ */
62
+ export function startSelfUpdate ({ version, dir, conn = null, restart, log = console.log, watch = watchSelfUpdateNpm, vault }) {
63
+ return watch({
64
+ pkg: PKG,
65
+ current: version,
66
+ repo: REPO,
67
+ dir,
68
+ ...(conn ? vaultUpdateHooks({ product: PKG, conn, ...(vault ? { vault } : {}) }) : {}),
69
+ onInstalled: ({ version: to, restart: canRestart }) => {
70
+ if (canRestart) {
71
+ log(`[update] ${PKG} ${to} installed · restarting to run it`)
72
+ restart(to)
73
+ } else {
74
+ log(`[update] ${PKG} ${to} installed · nothing restarts this process, so it keeps running ${version} until the next start`)
75
+ }
76
+ },
77
+ log
78
+ })
79
+ }
80
+
81
+ export default { PKG, REPO, vaultUpdateHooks, startSelfUpdate }