@owear/core 0.1.2 → 0.1.4

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 (58) hide show
  1. package/dist/app.d.ts +128 -0
  2. package/dist/app.js +285 -0
  3. package/dist/appicon.d.ts +4 -0
  4. package/dist/appicon.js +16 -0
  5. package/dist/bridge.d.ts +79 -0
  6. package/dist/bridge.js +55 -0
  7. package/dist/browserwindow.d.ts +115 -0
  8. package/dist/browserwindow.js +237 -0
  9. package/dist/channel.d.ts +22 -0
  10. package/dist/channel.js +114 -0
  11. package/dist/dialog.d.ts +47 -0
  12. package/dist/dialog.js +22 -0
  13. package/dist/index.d.ts +83 -96
  14. package/dist/index.js +26 -253
  15. package/dist/installer.d.ts +126 -0
  16. package/dist/installer.js +84 -0
  17. package/dist/ipc/node.d.ts +7 -0
  18. package/dist/ipc/node.js +25 -0
  19. package/dist/menu.d.ts +56 -0
  20. package/dist/menu.js +208 -0
  21. package/dist/modules.d.ts +16 -0
  22. package/dist/modules.js +13 -0
  23. package/dist/nativeimage.d.ts +41 -0
  24. package/dist/nativeimage.js +376 -0
  25. package/dist/node/worker-runner.d.ts +1 -0
  26. package/dist/node/worker-runner.js +35 -0
  27. package/dist/node/worker.d.ts +29 -0
  28. package/dist/node/worker.js +61 -0
  29. package/dist/port.d.ts +28 -0
  30. package/dist/port.js +42 -0
  31. package/dist/power.d.ts +33 -0
  32. package/dist/power.js +99 -0
  33. package/dist/protocol.d.ts +14 -0
  34. package/dist/protocol.js +46 -0
  35. package/dist/safestorage.d.ts +16 -0
  36. package/dist/safestorage.js +21 -0
  37. package/dist/screen.d.ts +92 -0
  38. package/dist/screen.js +121 -0
  39. package/dist/session.d.ts +24 -0
  40. package/dist/session.js +42 -0
  41. package/dist/theme.d.ts +30 -0
  42. package/dist/theme.js +34 -0
  43. package/dist/tray.d.ts +30 -0
  44. package/dist/tray.js +69 -0
  45. package/dist/updater/boot.d.ts +20 -0
  46. package/dist/updater/boot.js +49 -0
  47. package/dist/updater/delta.d.ts +30 -0
  48. package/dist/updater/delta.js +96 -0
  49. package/dist/updater/yaml.d.ts +6 -0
  50. package/dist/updater/yaml.js +160 -0
  51. package/dist/updater.d.ts +119 -0
  52. package/dist/updater.js +429 -0
  53. package/dist/webcontents.d.ts +44 -0
  54. package/dist/webcontents.js +109 -0
  55. package/dist/webrequest.d.ts +23 -0
  56. package/dist/webrequest.js +76 -0
  57. package/package.json +7 -6
  58. package/LICENSE +0 -201
@@ -0,0 +1,30 @@
1
+ export interface ThemeInfo {
2
+ dark: boolean;
3
+ source: 'system' | 'light' | 'dark';
4
+ highContrast: boolean;
5
+ reducedTransparency: boolean;
6
+ }
7
+ /**
8
+ * Tema nativo del sistema (módulo `theme`). `watch()` emite `theme.changed`
9
+ * (recibible con `channel.on('theme.changed', …)` o en el renderer con
10
+ * `ow.on('theme.changed', …)`).
11
+ */
12
+ export declare const theme: {
13
+ get(): Promise<ThemeInfo>;
14
+ isDark(): Promise<boolean>;
15
+ /** Fuerza la preferencia: 'system' | 'light' | 'dark'. También aplica el
16
+ * esquema al contenido (Windows: WebView2 `PreferredColorScheme`). */
17
+ setSource(source: "system" | "light" | "dark"): Promise<ThemeInfo>;
18
+ watch(): Promise<void>;
19
+ unwatch(): Promise<void>;
20
+ };
21
+ /** Alias estilo Electron de `theme`. */
22
+ export declare const nativeTheme: {
23
+ get(): Promise<ThemeInfo>;
24
+ isDark(): Promise<boolean>;
25
+ /** Fuerza la preferencia: 'system' | 'light' | 'dark'. También aplica el
26
+ * esquema al contenido (Windows: WebView2 `PreferredColorScheme`). */
27
+ setSource(source: "system" | "light" | "dark"): Promise<ThemeInfo>;
28
+ watch(): Promise<void>;
29
+ unwatch(): Promise<void>;
30
+ };
package/dist/theme.js ADDED
@@ -0,0 +1,34 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // @owear/core/src/theme.ts — nativeTheme (módulo `theme`).
5
+ import { channel, invokeNative } from './channel.js';
6
+ /**
7
+ * Tema nativo del sistema (módulo `theme`). `watch()` emite `theme.changed`
8
+ * (recibible con `channel.on('theme.changed', …)` o en el renderer con
9
+ * `ow.on('theme.changed', …)`).
10
+ */
11
+ export const theme = {
12
+ get() {
13
+ return invokeNative('theme', 'get');
14
+ },
15
+ isDark() {
16
+ return invokeNative('theme', 'isDark');
17
+ },
18
+ /** Fuerza la preferencia: 'system' | 'light' | 'dark'. También aplica el
19
+ * esquema al contenido (Windows: WebView2 `PreferredColorScheme`). */
20
+ async setSource(source) {
21
+ const info = await invokeNative('theme', 'setSource', source);
22
+ const scheme = source === 'dark' ? 2 : source === 'light' ? 1 : 0;
23
+ void channel.call('window.setColorScheme', { scheme }).catch(() => undefined);
24
+ return info;
25
+ },
26
+ watch() {
27
+ return invokeNative('theme', 'watch');
28
+ },
29
+ unwatch() {
30
+ return invokeNative('theme', 'unwatch');
31
+ },
32
+ };
33
+ /** Alias estilo Electron de `theme`. */
34
+ export const nativeTheme = theme;
package/dist/tray.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Menu } from './menu.js';
3
+ import { NativeImage } from './nativeimage.js';
4
+ /** Tray actual (solo uno). Lo lee el ruteo de eventos del SDK. */
5
+ export declare let currentTray: Tray | null;
6
+ export interface TrayEvent {
7
+ button: 'left' | 'right' | 'double';
8
+ windowId: number;
9
+ }
10
+ /**
11
+ * Icono de bandeja (intermedio Tauri/Electron). El menú contextual reutiliza
12
+ * `Menu`. Eventos: `click`, `right-click`, `double-click`.
13
+ *
14
+ * const tray = new Tray(nativeImage.createFromPath('icon.png'))
15
+ * tray.setContextMenu(Menu.buildFromTemplate([{ label: 'Mostrar', click: () => win.show() }]))
16
+ * tray.on('click', () => win.show())
17
+ */
18
+ export declare class Tray extends EventEmitter {
19
+ private _menu;
20
+ private _id;
21
+ constructor(image?: NativeImage | string);
22
+ get id(): string;
23
+ setImage(image: NativeImage | string): void;
24
+ setPressedImage(image: NativeImage | string): void;
25
+ setToolTip(tip: string): void;
26
+ setTitle(title: string): void;
27
+ setContextMenu(menu: Menu | null): void;
28
+ popupContextMenu(menu?: Menu): void;
29
+ destroy(): void;
30
+ }
package/dist/tray.js ADDED
@@ -0,0 +1,69 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // @owear/core/src/tray.ts — Tray (C6).
5
+ import { EventEmitter } from 'node:events';
6
+ import { invokeNative } from './channel.js';
7
+ import { app } from './app.js';
8
+ import { nativeImage } from './nativeimage.js';
9
+ /** Tray actual (solo uno). Lo lee el ruteo de eventos del SDK. */
10
+ export let currentTray = null;
11
+ function toPngBase64(image) {
12
+ const img = typeof image === 'string' ? nativeImage.createFromPath(image) : image;
13
+ return img.toPNG().toString('base64');
14
+ }
15
+ /**
16
+ * Icono de bandeja (intermedio Tauri/Electron). El menú contextual reutiliza
17
+ * `Menu`. Eventos: `click`, `right-click`, `double-click`.
18
+ *
19
+ * const tray = new Tray(nativeImage.createFromPath('icon.png'))
20
+ * tray.setContextMenu(Menu.buildFromTemplate([{ label: 'Mostrar', click: () => win.show() }]))
21
+ * tray.on('click', () => win.show())
22
+ */
23
+ export class Tray extends EventEmitter {
24
+ _menu = null;
25
+ _id;
26
+ constructor(image) {
27
+ super();
28
+ this._id = `tray-${Math.random().toString(36).slice(2, 8)}`;
29
+ currentTray = this;
30
+ app
31
+ .whenReady()
32
+ .then(async () => {
33
+ await invokeNative('tray', 'create', this._id).catch(() => undefined);
34
+ if (image)
35
+ this.setImage(image);
36
+ })
37
+ .catch(() => undefined);
38
+ }
39
+ get id() {
40
+ return this._id;
41
+ }
42
+ setImage(image) {
43
+ void invokeNative('tray', 'setImage', toPngBase64(image)).catch(() => undefined);
44
+ }
45
+ setPressedImage(image) {
46
+ void invokeNative('tray', 'setPressedImage', toPngBase64(image)).catch(() => undefined);
47
+ }
48
+ setToolTip(tip) {
49
+ void invokeNative('tray', 'setToolTip', tip).catch(() => undefined);
50
+ }
51
+ setTitle(title) {
52
+ void invokeNative('tray', 'setTitle', title).catch(() => undefined);
53
+ }
54
+ setContextMenu(menu) {
55
+ this._menu = menu;
56
+ void invokeNative('tray', 'setContextMenu', { items: menu ? menu._toJSON() : [] }).catch(() => undefined);
57
+ }
58
+ popupContextMenu(menu) {
59
+ const m = menu ?? this._menu;
60
+ if (m)
61
+ void invokeNative('tray', 'setContextMenu', { items: m._toJSON() }).catch(() => undefined);
62
+ void invokeNative('tray', 'popupContextMenu').catch(() => undefined);
63
+ }
64
+ destroy() {
65
+ if (currentTray === this)
66
+ currentTray = null;
67
+ void invokeNative('tray', 'destroy').catch(() => undefined);
68
+ }
69
+ }
@@ -0,0 +1,20 @@
1
+ export interface BootRecord {
2
+ version: string;
3
+ attempts: number;
4
+ updatedAt: string;
5
+ }
6
+ /** Ruta del registro de arranque dentro de `<userData>`. */
7
+ export declare function bootRecordPath(userDataDir: string): string;
8
+ /** Lee el registro; null si no existe o está corrupto. */
9
+ export declare function readBoot(file: string): BootRecord | null;
10
+ /** Escribe el registro (crea el directorio si hace falta). */
11
+ export declare function writeBoot(file: string, rec: BootRecord): void;
12
+ /**
13
+ * Calcula el estado del siguiente arranque:
14
+ * - misma versión que la anterior → `attempts + 1`; si llega al umbral, rollback.
15
+ * - versión distinta (o primer arranque) → `attempts = 1`, sin rollback.
16
+ */
17
+ export declare function nextBoot(prev: BootRecord | null, version: string, threshold: number, now?: () => Date): {
18
+ record: BootRecord;
19
+ shouldRollback: boolean;
20
+ };
@@ -0,0 +1,49 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // @owear/core/src/updater/boot.ts — registro de arranque para el **rollback**.
5
+ //
6
+ // Tras aplicar un update el binario nuevo arranca con una copia de seguridad
7
+ // (`<exe>.owprev`). Si el arranque se repite sin que la app marque "sano"
8
+ // (crash loop), el updater revierte al binario anterior.
9
+ //
10
+ // El contador vive en `<userData>/update-boot.json`:
11
+ // { "version": "1.4.0", "attempts": 2, "updatedAt": "…" }
12
+ // Cada arranque incrementa `attempts`; cuando la app confirma salud
13
+ // (`commit`), se pone a 0. Si `attempts` alcanza el umbral → rollback.
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ /** Ruta del registro de arranque dentro de `<userData>`. */
17
+ export function bootRecordPath(userDataDir) {
18
+ return path.join(userDataDir, 'update-boot.json');
19
+ }
20
+ /** Lee el registro; null si no existe o está corrupto. */
21
+ export function readBoot(file) {
22
+ try {
23
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
24
+ if (typeof raw?.version !== 'string' || typeof raw?.attempts !== 'number')
25
+ return null;
26
+ return { version: raw.version, attempts: raw.attempts, updatedAt: String(raw.updatedAt ?? '') };
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /** Escribe el registro (crea el directorio si hace falta). */
33
+ export function writeBoot(file, rec) {
34
+ fs.mkdirSync(path.dirname(file), { recursive: true });
35
+ fs.writeFileSync(file, JSON.stringify(rec) + '\n');
36
+ }
37
+ /**
38
+ * Calcula el estado del siguiente arranque:
39
+ * - misma versión que la anterior → `attempts + 1`; si llega al umbral, rollback.
40
+ * - versión distinta (o primer arranque) → `attempts = 1`, sin rollback.
41
+ */
42
+ export function nextBoot(prev, version, threshold, now = () => new Date()) {
43
+ const sameVersion = !!prev && prev.version === version;
44
+ const attempts = sameVersion ? prev.attempts + 1 : 1;
45
+ return {
46
+ record: { version, attempts, updatedAt: now().toISOString() },
47
+ shouldRollback: sameVersion && attempts >= threshold,
48
+ };
49
+ }
@@ -0,0 +1,30 @@
1
+ /** sha256 (hex) de cada bloque de `data` (tamaño `blockSize`). */
2
+ export declare function blockHashes(data: Buffer, blockSize: number): string[];
3
+ /** Bloque de bytes [start, end] (end INCLUSIVE), como un HTTP Range. */
4
+ export interface ByteRange {
5
+ start: number;
6
+ end: number;
7
+ }
8
+ export interface DeltaPlan {
9
+ blockSize: number;
10
+ total: number;
11
+ reused: number;
12
+ changed: number;
13
+ ranges: ByteRange[];
14
+ /** Bytes a descargar si se aplica el delta. */
15
+ downloadBytes: number;
16
+ /** Bytes del artefacto completo. */
17
+ totalBytes: number;
18
+ /** Ratio de cambio (0..1). */
19
+ ratio: number;
20
+ }
21
+ /**
22
+ * Plan de delta: compara los hashes locales con los remotos y agrupa los
23
+ * bloques cambiados en rangos contiguos. `remoteSize` acota el último bloque.
24
+ */
25
+ export declare function planDelta(localHashes: string[], remoteHashes: string[], blockSize: number, remoteSize: number): DeltaPlan;
26
+ /**
27
+ * Ensambla el artefacto nuevo reutilizando los bloques locales y pidiendo los
28
+ * cambiados con `fetchRange` (que debe devolver exactamente ese rango).
29
+ */
30
+ export declare function assemble(local: Buffer, remoteHashes: string[], plan: DeltaPlan, remoteSize: number, fetchRange: (r: ByteRange) => Promise<Buffer>): Promise<Buffer>;
@@ -0,0 +1,96 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // @owear/core/src/updater/delta.ts — delta por bloques (estilo blockmap).
5
+ //
6
+ // El artefacto (p. ej. el binario único) se parte en bloques de tamaño fijo y
7
+ // se publica un blockmap con el sha256 de cada bloque. En el cliente se
8
+ // comparan los bloques locales con los remotos y **solo se descargan los que
9
+ // cambian** (HTTP Range), reutilizando el resto del fichero actual.
10
+ import { createHash } from 'node:crypto';
11
+ /** sha256 (hex) de cada bloque de `data` (tamaño `blockSize`). */
12
+ export function blockHashes(data, blockSize) {
13
+ const out = [];
14
+ if (data.length === 0) {
15
+ out.push(createHash('sha256').update(Buffer.alloc(0)).digest('hex'));
16
+ return out;
17
+ }
18
+ for (let off = 0; off < data.length; off += blockSize) {
19
+ out.push(createHash('sha256')
20
+ .update(data.subarray(off, Math.min(off + blockSize, data.length)))
21
+ .digest('hex'));
22
+ }
23
+ return out;
24
+ }
25
+ /**
26
+ * Plan de delta: compara los hashes locales con los remotos y agrupa los
27
+ * bloques cambiados en rangos contiguos. `remoteSize` acota el último bloque.
28
+ */
29
+ export function planDelta(localHashes, remoteHashes, blockSize, remoteSize) {
30
+ const total = remoteHashes.length;
31
+ const ranges = [];
32
+ let cur = null;
33
+ let changed = 0;
34
+ const blockEnd = (i) => Math.min((i + 1) * blockSize, remoteSize) - 1;
35
+ for (let i = 0; i < total; i++) {
36
+ const same = i < localHashes.length && localHashes[i] === remoteHashes[i];
37
+ if (same) {
38
+ if (cur) {
39
+ ranges.push(cur);
40
+ cur = null;
41
+ }
42
+ continue;
43
+ }
44
+ changed++;
45
+ const start = i * blockSize;
46
+ if (cur && start === cur.end + 1)
47
+ cur.end = blockEnd(i);
48
+ else {
49
+ if (cur)
50
+ ranges.push(cur);
51
+ cur = { start, end: blockEnd(i) };
52
+ }
53
+ }
54
+ if (cur)
55
+ ranges.push(cur);
56
+ const downloadBytes = ranges.reduce((n, r) => n + (r.end - r.start + 1), 0);
57
+ return {
58
+ blockSize,
59
+ total,
60
+ reused: total - changed,
61
+ changed,
62
+ ranges,
63
+ downloadBytes,
64
+ totalBytes: remoteSize,
65
+ ratio: total ? changed / total : 1,
66
+ };
67
+ }
68
+ /**
69
+ * Ensambla el artefacto nuevo reutilizando los bloques locales y pidiendo los
70
+ * cambiados con `fetchRange` (que debe devolver exactamente ese rango).
71
+ */
72
+ export async function assemble(local, remoteHashes, plan, remoteSize, fetchRange) {
73
+ const out = Buffer.alloc(remoteSize);
74
+ // bloque = reutilizado salvo que caiga dentro de un rango de descarga
75
+ let ri = 0;
76
+ for (let i = 0; i < remoteHashes.length; i++) {
77
+ const start = i * plan.blockSize;
78
+ const end = Math.min(start + plan.blockSize, remoteSize);
79
+ while (ri < plan.ranges.length && plan.ranges[ri].end < start)
80
+ ri++;
81
+ const inRange = ri < plan.ranges.length && plan.ranges[ri].start <= start;
82
+ if (!inRange) {
83
+ // reutiliza del local (si existe y coincide); si no, se pedirá abajo
84
+ if (start < local.length)
85
+ local.copy(out, start, start, Math.min(end, local.length));
86
+ }
87
+ }
88
+ // descarga de los rangos cambiados
89
+ for (const r of plan.ranges) {
90
+ const buf = await fetchRange(r);
91
+ if (buf.length !== r.end - r.start + 1)
92
+ throw new Error(`rango ${r.start}-${r.end}: recibidos ${buf.length} bytes`);
93
+ buf.copy(out, r.start);
94
+ }
95
+ return out;
96
+ }
@@ -0,0 +1,6 @@
1
+ export type YamlValue = string | number | boolean | null | YamlMap | YamlValue[];
2
+ export interface YamlMap {
3
+ [k: string]: YamlValue;
4
+ }
5
+ /** Parsea un manifiesto YAML a un objeto JS. */
6
+ export declare function parseYaml(text: string): YamlValue;
@@ -0,0 +1,160 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // @owear/core/src/updater/yaml.ts — parser YAML mínimo (subconjunto) para los
5
+ // manifiestos de auto-update. Soporta:
6
+ // - mapas (`key: value`), anidados por indentación
7
+ // - listas (`- item`) de escalares o mapas
8
+ // - escalares (texto, comillas simples/dobles, números, true/false/null)
9
+ // - bloque literal (`key: |` y `key: >`) con líneas indentadas
10
+ // No soporta: anchors/aliases, flow style, multi-documento, tags. Suficiente
11
+ // para `latest.yml` / `<channel>.yml` (estilo electron-builder).
12
+ function stripComment(s) {
13
+ // Solo elimina comentarios fuera de comillas.
14
+ let inS = false;
15
+ let inD = false;
16
+ for (let i = 0; i < s.length; i++) {
17
+ const c = s[i];
18
+ if (c === "'" && !inD)
19
+ inS = !inS;
20
+ else if (c === '"' && !inS)
21
+ inD = !inD;
22
+ else if (c === '#' && !inS && !inD && (i === 0 || s[i - 1] === ' '))
23
+ return s.slice(0, i);
24
+ }
25
+ return s;
26
+ }
27
+ function tokenize(text) {
28
+ const out = [];
29
+ for (const raw of text.replace(/\r\n?/g, '\n').split('\n')) {
30
+ const noComment = stripComment(raw);
31
+ if (noComment.trim() === '' || /^---\s*$/.test(noComment.trim()) || /^\.\.\.\s*$/.test(noComment.trim()))
32
+ continue;
33
+ const indent = noComment.match(/^ */)[0].length;
34
+ out.push({ indent, text: noComment.slice(indent) });
35
+ }
36
+ return out;
37
+ }
38
+ function parseScalar(s) {
39
+ const t = s.trim();
40
+ if (t === '' || t === '~' || t === 'null')
41
+ return null;
42
+ if (t === 'true')
43
+ return true;
44
+ if (t === 'false')
45
+ return false;
46
+ if (/^'.*'$/.test(t))
47
+ return t.slice(1, -1).replace(/''/g, "'");
48
+ if (/^".*"$/.test(t)) {
49
+ try {
50
+ return JSON.parse(t);
51
+ }
52
+ catch {
53
+ return t.slice(1, -1);
54
+ }
55
+ }
56
+ if (/^-?\d+$/.test(t))
57
+ return parseInt(t, 10);
58
+ if (/^-?\d*\.\d+$/.test(t))
59
+ return parseFloat(t);
60
+ return t;
61
+ }
62
+ /** Divide "key: value" por el primer ':' seguido de espacio o fin. */
63
+ function splitKey(text) {
64
+ let inS = false;
65
+ let inD = false;
66
+ for (let i = 0; i < text.length; i++) {
67
+ const c = text[i];
68
+ if (c === "'" && !inD)
69
+ inS = !inS;
70
+ else if (c === '"' && !inS)
71
+ inD = !inD;
72
+ else if (c === ':' && !inS && !inD && (i + 1 === text.length || text[i + 1] === ' '))
73
+ return { key: text.slice(0, i).trim(), rest: text.slice(i + 1).trim() };
74
+ }
75
+ return null;
76
+ }
77
+ function parseBlock(lines, start, indent) {
78
+ if (start >= lines.length)
79
+ return { value: null, next: start };
80
+ const first = lines[start];
81
+ // Lista
82
+ if (first.text.startsWith('- ') || first.text === '-') {
83
+ const arr = [];
84
+ let i = start;
85
+ while (i < lines.length && lines[i].indent === indent && (lines[i].text.startsWith('- ') || lines[i].text === '-')) {
86
+ const item = lines[i].text === '-' ? '' : lines[i].text.slice(2);
87
+ if (item === '') {
88
+ const sub = parseBlock(lines, i + 1, lines[i + 1]?.indent ?? indent + 2);
89
+ arr.push(sub.value);
90
+ i = sub.next;
91
+ }
92
+ else {
93
+ const kv = splitKey(item);
94
+ if (kv) {
95
+ // mapa en línea (primer campo) + campos siguientes más indentados
96
+ const obj = {};
97
+ obj[kv.key] = kv.rest === '' ? null : parseScalar(kv.rest);
98
+ const sub = parseBlock(lines, i + 1, lines[i + 1]?.indent ?? indent + 2);
99
+ if (sub.value && typeof sub.value === 'object' && !Array.isArray(sub.value)) {
100
+ Object.assign(obj, sub.value);
101
+ i = sub.next;
102
+ }
103
+ else {
104
+ i++;
105
+ }
106
+ arr.push(obj);
107
+ }
108
+ else {
109
+ arr.push(parseScalar(item));
110
+ i++;
111
+ }
112
+ }
113
+ }
114
+ return { value: arr, next: i };
115
+ }
116
+ // Mapa
117
+ const obj = {};
118
+ let i = start;
119
+ while (i < lines.length && lines[i].indent === indent) {
120
+ const kv = splitKey(lines[i].text);
121
+ if (!kv)
122
+ break;
123
+ if (kv.rest === '|' || kv.rest === '>') {
124
+ const blockIndent = lines[i + 1]?.indent ?? indent + 2;
125
+ const parts = [];
126
+ i++;
127
+ while (i < lines.length && lines[i].indent >= blockIndent) {
128
+ parts.push(lines[i].text);
129
+ i++;
130
+ }
131
+ obj[kv.key] = kv.rest === '>' ? parts.join(' ') : parts.join('\n');
132
+ continue;
133
+ }
134
+ if (kv.rest === '') {
135
+ // valor anidado (mapa/lista) o null
136
+ const nextLine = lines[i + 1];
137
+ if (nextLine && nextLine.indent > indent) {
138
+ const sub = parseBlock(lines, i + 1, nextLine.indent);
139
+ obj[kv.key] = sub.value;
140
+ i = sub.next;
141
+ }
142
+ else {
143
+ obj[kv.key] = null;
144
+ i++;
145
+ }
146
+ }
147
+ else {
148
+ obj[kv.key] = parseScalar(kv.rest);
149
+ i++;
150
+ }
151
+ }
152
+ return { value: obj, next: i };
153
+ }
154
+ /** Parsea un manifiesto YAML a un objeto JS. */
155
+ export function parseYaml(text) {
156
+ const lines = tokenize(text);
157
+ if (!lines.length)
158
+ return null;
159
+ return parseBlock(lines, 0, lines[0].indent).value;
160
+ }
@@ -0,0 +1,119 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type BootRecord } from './updater/boot.js';
3
+ export interface FeedOptions {
4
+ provider?: 'generic' | 'github';
5
+ /** Base del feed (generic) o `owner/repo` (github). */
6
+ url: string;
7
+ channel?: string;
8
+ /** Clave pública Ed25519 (PEM o base64 SPKI). */
9
+ publicKey?: string;
10
+ }
11
+ export interface UpdateFile {
12
+ url: string;
13
+ sha256?: string;
14
+ sha512?: string;
15
+ size?: number;
16
+ }
17
+ export interface UpdateInfo {
18
+ version: string;
19
+ notes?: string;
20
+ releaseDate?: string;
21
+ mandatory?: boolean;
22
+ sha256?: string;
23
+ sha512?: string;
24
+ size?: number;
25
+ blockSize?: number;
26
+ blockmap?: string;
27
+ /** Firma Ed25519 de la metadata: `"<version>:<sha256>"`. */
28
+ signature?: string;
29
+ /** Firma Ed25519 del artefacto completo (defensa en profundidad). */
30
+ binarySig?: string;
31
+ file: UpdateFile;
32
+ }
33
+ export interface ProgressInfo {
34
+ total: number;
35
+ transferred: number;
36
+ percent: number;
37
+ bytesPerSecond: number;
38
+ }
39
+ export interface UpdateCheckResult {
40
+ updateInfo: UpdateInfo;
41
+ }
42
+ export interface InstallOptions {
43
+ /** Guarda una copia del binario actual para poder revertir (rollback). */
44
+ backup?: boolean;
45
+ }
46
+ export interface BootGuardOptions {
47
+ /** Arranques sin confirmar antes de revertir. */
48
+ threshold?: number;
49
+ /** ms de vida "sana" tras los que se confirma el update. */
50
+ healthDelayMs?: number;
51
+ }
52
+ declare function semverGt(a: string, b: string): boolean;
53
+ declare function isPrerelease(v: string): boolean;
54
+ declare class AutoUpdater extends EventEmitter {
55
+ currentVersion: string;
56
+ channel: string;
57
+ allowPrerelease: boolean;
58
+ autoDownload: boolean;
59
+ autoInstallOnAppQuit: boolean;
60
+ /** Nº de reintentos por petición (con backoff exponencial). */
61
+ maxRetries: number;
62
+ /** Espera base entre reintentos (ms); se duplica cada intento. */
63
+ retryDelay: number;
64
+ /** Timeout por petición (ms). */
65
+ requestTimeout: number;
66
+ private feed;
67
+ private info;
68
+ private newFile;
69
+ private healthTimer;
70
+ setFeedURL(opts: FeedOptions): void;
71
+ private manifestUrl;
72
+ private publicKey;
73
+ /** Verifica la firma Ed25519 del manifiesto (sobre `"<version>:<sha256>"`). */
74
+ private verifyManifest;
75
+ private backoff;
76
+ /**
77
+ * `fetch` con timeout y reintentos (backoff exponencial) para errores de red
78
+ * y estados transitorios. Devuelve la respuesta (incluidos 4xx no
79
+ * reintentables, para que el llamante dé un error concreto).
80
+ */
81
+ private request;
82
+ checkForUpdates(): Promise<UpdateCheckResult | null>;
83
+ private fetchRange;
84
+ /** Delta por bloques: blockmap + reutilización local + rangos. */
85
+ private downloadDelta;
86
+ /**
87
+ * Descarga completa con **reanudación**: si la conexión cae a mitad, reintenta
88
+ * con `Range: bytes=<recibido>-` y continúa en vez de empezar de cero.
89
+ */
90
+ private downloadFull;
91
+ /** Descarga el update (delta si hay blockmap; si no, completa con resume). */
92
+ downloadUpdate(): Promise<string[]>;
93
+ /**
94
+ * Aplica el update (reemplazo atómico + relaunch) vía el módulo nativo.
95
+ *
96
+ * El kernel se reemplaza a sí mismo y se re-ejecuta (`execv`) dentro de esta
97
+ * llamada, así que **no habrá respuesta**: la conexión cae. Lo tratamos como
98
+ * éxito (el proceso nuevo arranca con el binario actualizado).
99
+ */
100
+ quitAndInstall(opts?: InstallOptions): Promise<void>;
101
+ /**
102
+ * Revierte al binario anterior (`<exe>.owprev`) y relanza. Solo tiene efecto
103
+ * si hay una copia de seguridad pendiente (p. ej. tras un update con backup).
104
+ */
105
+ rollback(): Promise<void>;
106
+ /** Confirma el update (borra la copia de seguridad) → desarma el rollback. */
107
+ commitUpdate(): Promise<void>;
108
+ /**
109
+ * Arma el **guard de arranque** tras un update: si el binario nuevo se
110
+ * reinicia sin llegar a "sano" (crash loop) más de `threshold` veces,
111
+ * revierte. Si arranca bien `healthDelayMs`, confirma (borra el backup).
112
+ *
113
+ * app.whenReady().then(() => autoUpdater.armBootGuard())
114
+ */
115
+ armBootGuard(opts?: BootGuardOptions): Promise<'idle' | 'armed' | 'rolled-back'>;
116
+ }
117
+ export declare const autoUpdater: AutoUpdater;
118
+ export { semverGt, isPrerelease };
119
+ export type { BootRecord };