@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.
- package/dist/app.d.ts +128 -0
- package/dist/app.js +285 -0
- package/dist/appicon.d.ts +4 -0
- package/dist/appicon.js +16 -0
- package/dist/bridge.d.ts +79 -0
- package/dist/bridge.js +55 -0
- package/dist/browserwindow.d.ts +115 -0
- package/dist/browserwindow.js +237 -0
- package/dist/channel.d.ts +22 -0
- package/dist/channel.js +114 -0
- package/dist/dialog.d.ts +47 -0
- package/dist/dialog.js +22 -0
- package/dist/index.d.ts +83 -96
- package/dist/index.js +26 -253
- package/dist/installer.d.ts +126 -0
- package/dist/installer.js +84 -0
- package/dist/ipc/node.d.ts +7 -0
- package/dist/ipc/node.js +25 -0
- package/dist/menu.d.ts +56 -0
- package/dist/menu.js +208 -0
- package/dist/modules.d.ts +16 -0
- package/dist/modules.js +13 -0
- package/dist/nativeimage.d.ts +41 -0
- package/dist/nativeimage.js +376 -0
- package/dist/node/worker-runner.d.ts +1 -0
- package/dist/node/worker-runner.js +35 -0
- package/dist/node/worker.d.ts +29 -0
- package/dist/node/worker.js +61 -0
- package/dist/port.d.ts +28 -0
- package/dist/port.js +42 -0
- package/dist/power.d.ts +33 -0
- package/dist/power.js +99 -0
- package/dist/protocol.d.ts +14 -0
- package/dist/protocol.js +46 -0
- package/dist/safestorage.d.ts +16 -0
- package/dist/safestorage.js +21 -0
- package/dist/screen.d.ts +92 -0
- package/dist/screen.js +121 -0
- package/dist/session.d.ts +24 -0
- package/dist/session.js +42 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.js +34 -0
- package/dist/tray.d.ts +30 -0
- package/dist/tray.js +69 -0
- package/dist/updater/boot.d.ts +20 -0
- package/dist/updater/boot.js +49 -0
- package/dist/updater/delta.d.ts +30 -0
- package/dist/updater/delta.js +96 -0
- package/dist/updater/yaml.d.ts +6 -0
- package/dist/updater/yaml.js +160 -0
- package/dist/updater.d.ts +119 -0
- package/dist/updater.js +429 -0
- package/dist/webcontents.d.ts +44 -0
- package/dist/webcontents.js +109 -0
- package/dist/webrequest.d.ts +23 -0
- package/dist/webrequest.js +76 -0
- package/package.json +7 -6
- package/LICENSE +0 -201
package/dist/theme.d.ts
ADDED
|
@@ -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,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 };
|