@unified-desktop/bridge 0.0.0-stage → 0.2.3

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.
@@ -0,0 +1,36 @@
1
+ import type { AppBridge, RuntimeKind } from '../types';
2
+ import { type ClipboardBridge } from './clipboard';
3
+ import { type DeepLinkBridge } from './deep-link';
4
+ import { type DialogBridge } from './dialog';
5
+ import { type FileSystemBridge } from './file-system';
6
+ import { type HttpClient } from './http';
7
+ import { type Logger } from './logging';
8
+ import { type NotificationBridge } from './notification';
9
+ import { type ProcessBridge } from './process';
10
+ import { type ShellBridge } from './shell';
11
+ import { type UpdaterBridge } from './updater';
12
+ import { type UploadBridge } from './upload';
13
+ import { type WebSocketBridge } from './websocket';
14
+ import { type WindowStateBridge } from './window-state';
15
+ import { type StoreBridge } from './store';
16
+ import { type SecureStorageBridge } from './secure-storage';
17
+ import { type PermissionBridge } from './permissions';
18
+ export interface BridgeOverrides {
19
+ http?: HttpClient;
20
+ clipboard?: ClipboardBridge;
21
+ dialog?: DialogBridge;
22
+ notification?: NotificationBridge;
23
+ upload?: UploadBridge;
24
+ websocket?: WebSocketBridge;
25
+ store?: StoreBridge;
26
+ secureStorage?: SecureStorageBridge;
27
+ permissions?: PermissionBridge;
28
+ fileSystem?: Partial<FileSystemBridge>;
29
+ shell?: Partial<ShellBridge>;
30
+ process?: Partial<ProcessBridge>;
31
+ deepLink?: Partial<DeepLinkBridge>;
32
+ updater?: Partial<UpdaterBridge>;
33
+ windowState?: Partial<WindowStateBridge>;
34
+ logger?: Logger;
35
+ }
36
+ export declare function createCommonBridge(runtime: RuntimeKind, baseOverrides?: BridgeOverrides, customOverrides?: BridgeOverrides): AppBridge;
@@ -0,0 +1,5 @@
1
+ export interface ClipboardBridge {
2
+ readText(): Promise<string>;
3
+ writeText(text: string): Promise<void>;
4
+ }
5
+ export declare function createClipboardBridge(): ClipboardBridge;
@@ -0,0 +1,5 @@
1
+ export interface DeepLinkBridge {
2
+ getInitialUrl(): Promise<string | null>;
3
+ subscribe(listener: (url: string) => void): () => void;
4
+ }
5
+ export declare function createWebDeepLinkBridge(): DeepLinkBridge;
@@ -0,0 +1,6 @@
1
+ export interface DialogBridge {
2
+ alert(message: string, title?: string): Promise<void>;
3
+ confirm(message: string, title?: string): Promise<boolean>;
4
+ prompt(message: string, defaultValue?: string, title?: string): Promise<string | null>;
5
+ }
6
+ export declare function createDialogBridge(): DialogBridge;
@@ -0,0 +1,35 @@
1
+ import type { SaveTextFileInput } from '../types';
2
+ import type { BridgePermissionName } from './permissions';
3
+ export interface FilePickerFilter {
4
+ name: string;
5
+ extensions: string[];
6
+ }
7
+ export interface FilePickerOptions {
8
+ title?: string;
9
+ defaultPath?: string;
10
+ filters?: FilePickerFilter[];
11
+ multiple?: boolean;
12
+ writable?: boolean;
13
+ /** Save handles are limited to the capability that authorized the picker. */
14
+ permission?: 'file-write' | 'file-download';
15
+ }
16
+ export interface FileHandle {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly kind: 'file' | 'directory';
20
+ readonly writable: boolean;
21
+ readonly size?: number;
22
+ }
23
+ export interface FileSystemBridge {
24
+ readTextFile(path: string): Promise<string>;
25
+ saveTextFile(input: SaveTextFileInput): Promise<void>;
26
+ pickFiles(options?: FilePickerOptions): Promise<FileHandle[]>;
27
+ pickDirectory(options?: FilePickerOptions): Promise<FileHandle | null>;
28
+ pickSaveFile(options?: FilePickerOptions): Promise<FileHandle | null>;
29
+ readFile(handle: FileHandle): Promise<Uint8Array>;
30
+ writeFile(handle: FileHandle, data: Uint8Array): Promise<void>;
31
+ releaseFileHandle(handle: FileHandle): Promise<void>;
32
+ }
33
+ export declare function createWebFileSystemBridge(): FileSystemBridge & {
34
+ hasPermission(name: BridgePermissionName, id: string): boolean;
35
+ };
@@ -0,0 +1 @@
1
+ export * from './http';
@@ -0,0 +1,24 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
2
+ export type HttpResponseType = 'json' | 'text' | 'arrayBuffer';
3
+ export interface HttpRequest {
4
+ url: string;
5
+ method?: HttpMethod;
6
+ query?: Record<string, string | number | boolean | null | undefined>;
7
+ headers?: Record<string, string>;
8
+ body?: unknown;
9
+ responseType?: HttpResponseType;
10
+ signal?: AbortSignal;
11
+ timeoutMs?: number;
12
+ }
13
+ export interface HttpResponse<T> {
14
+ status: number;
15
+ headers: Headers;
16
+ data: T;
17
+ }
18
+ export interface HttpClient {
19
+ request<T = unknown>(input: HttpRequest): Promise<HttpResponse<T>>;
20
+ }
21
+ export declare function createHttpClient(options?: {
22
+ baseUrl?: string;
23
+ fetchImpl?: typeof fetch;
24
+ }): HttpClient;
@@ -0,0 +1,16 @@
1
+ export * from './clipboard';
2
+ export * from './deep-link';
3
+ export * from './dialog';
4
+ export * from './file-system';
5
+ export * from './http';
6
+ export * from './logging';
7
+ export * from './notification';
8
+ export * from './process';
9
+ export * from './shell';
10
+ export * from './store';
11
+ export * from './secure-storage';
12
+ export * from './permissions';
13
+ export * from './updater';
14
+ export * from './upload';
15
+ export * from './websocket';
16
+ export * from './window-state';
@@ -0,0 +1,11 @@
1
+ import type { RuntimeKind } from '../types';
2
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ export type LogContext = Record<string, unknown>;
4
+ export interface Logger {
5
+ debug(message: string, context?: LogContext): void;
6
+ info(message: string, context?: LogContext): void;
7
+ warn(message: string, context?: LogContext): void;
8
+ error(message: string, context?: LogContext): void;
9
+ }
10
+ export type LogSink = (level: LogLevel, message: string, context?: LogContext) => void;
11
+ export declare function createLogger(runtime: RuntimeKind, sink?: LogSink): Logger;
@@ -0,0 +1,10 @@
1
+ export interface NotificationInput {
2
+ title: string;
3
+ body?: string;
4
+ tag?: string;
5
+ }
6
+ export interface NotificationBridge {
7
+ requestPermission(): Promise<NotificationPermission>;
8
+ show(input: NotificationInput): Promise<void>;
9
+ }
10
+ export declare function createNotificationBridge(): NotificationBridge;
@@ -0,0 +1,15 @@
1
+ export type BridgePermissionName = 'microphone' | 'camera' | 'notifications' | 'clipboard-read' | 'clipboard-write' | 'file-read' | 'file-write' | 'file-download' | 'directory-read' | 'directory-write';
2
+ export type BridgePermissionState = 'granted' | 'denied' | 'prompt' | 'unsupported';
3
+ export interface PermissionRequestOptions {
4
+ handleId?: string;
5
+ }
6
+ export interface PermissionStatus {
7
+ name: BridgePermissionName;
8
+ state: BridgePermissionState;
9
+ scope?: string;
10
+ }
11
+ export interface PermissionBridge {
12
+ query(name: BridgePermissionName, options?: PermissionRequestOptions): Promise<PermissionStatus>;
13
+ request(name: BridgePermissionName, options?: PermissionRequestOptions): Promise<PermissionStatus>;
14
+ }
15
+ export declare function createWebPermissionBridge(hasPermission?: (name: BridgePermissionName, id: string) => boolean): PermissionBridge;
@@ -0,0 +1,12 @@
1
+ import type { RuntimeKind } from '../types';
2
+ export interface ProcessInfo {
3
+ runtime: RuntimeKind;
4
+ userAgent: string;
5
+ pid?: number;
6
+ }
7
+ export interface ProcessBridge {
8
+ info(): Promise<ProcessInfo>;
9
+ exit(code?: number): Promise<void>;
10
+ restart(): Promise<void>;
11
+ }
12
+ export declare function createWebProcessBridge(runtime: RuntimeKind): ProcessBridge;
@@ -0,0 +1,7 @@
1
+ export interface SecureStorageBridge {
2
+ get(key: string): Promise<string | null>;
3
+ set(key: string, value: string): Promise<void>;
4
+ has(key: string): Promise<boolean>;
5
+ delete(key: string): Promise<boolean>;
6
+ }
7
+ export declare function createStubSecureStorageBridge(): SecureStorageBridge;
@@ -0,0 +1,6 @@
1
+ export interface ShellBridge {
2
+ openExternal(url: string): Promise<void>;
3
+ openPath(path: string): Promise<void>;
4
+ revealItem(path: string): Promise<void>;
5
+ }
6
+ export declare function createWebShellBridge(): ShellBridge;
@@ -0,0 +1,21 @@
1
+ export type StoreValue = null | boolean | number | string | StoreValue[] | {
2
+ [key: string]: StoreValue;
3
+ };
4
+ export interface StoreChange {
5
+ key: string;
6
+ value: StoreValue | undefined;
7
+ }
8
+ export interface StoreBridge {
9
+ get<T extends StoreValue = StoreValue>(key: string): Promise<T | undefined>;
10
+ set<T extends StoreValue = StoreValue>(key: string, value: T): Promise<void>;
11
+ has(key: string): Promise<boolean>;
12
+ delete(key: string): Promise<boolean>;
13
+ clear(): Promise<void>;
14
+ keys(): Promise<string[]>;
15
+ entries<T extends StoreValue = StoreValue>(): Promise<Array<[string, T]>>;
16
+ save(): Promise<void>;
17
+ subscribe(listener: (change: StoreChange) => void): () => void;
18
+ /** Release browser-level listeners owned by this instance. */
19
+ dispose?(): void;
20
+ }
21
+ export declare function createWebStoreBridge(storageKey?: string, storage?: Storage): StoreBridge;
@@ -0,0 +1,7 @@
1
+ import type { UpdateInfo } from '../types';
2
+ export interface UpdaterBridge {
3
+ check(): Promise<UpdateInfo | null>;
4
+ downloadAndInstall(update: UpdateInfo): Promise<void>;
5
+ subscribe(listener: (progress: number) => void): () => void;
6
+ }
7
+ export declare function createStubUpdaterBridge(): UpdaterBridge;
@@ -0,0 +1,22 @@
1
+ export interface UploadRequest {
2
+ url: string;
3
+ method?: 'POST' | 'PUT' | 'PATCH';
4
+ headers?: Record<string, string>;
5
+ body: XMLHttpRequestBodyInit;
6
+ signal?: AbortSignal;
7
+ timeoutMs?: number;
8
+ onProgress?: (progress: {
9
+ loaded: number;
10
+ total?: number;
11
+ percent?: number;
12
+ }) => void;
13
+ }
14
+ export interface UploadResponse<T = unknown> {
15
+ status: number;
16
+ headers: Headers;
17
+ data: T;
18
+ }
19
+ export interface UploadBridge {
20
+ send<T = unknown>(request: UploadRequest): Promise<UploadResponse<T>>;
21
+ }
22
+ export declare function createUploadBridge(): UploadBridge;
@@ -0,0 +1,20 @@
1
+ export type WebSocketStatus = 'idle' | 'connecting' | 'open' | 'closed';
2
+ export interface WebSocketOptions {
3
+ url: string;
4
+ protocols?: string | string[];
5
+ reconnect?: boolean;
6
+ maxRetries?: number;
7
+ retryDelayMs?: number;
8
+ }
9
+ export interface WebSocketClient {
10
+ readonly status: WebSocketStatus;
11
+ connect(): Promise<void>;
12
+ send(data: string | ArrayBuffer | Blob): void;
13
+ close(code?: number, reason?: string): void;
14
+ onMessage(listener: (data: unknown) => void): () => void;
15
+ onStatus(listener: (status: WebSocketStatus) => void): () => void;
16
+ }
17
+ export interface WebSocketBridge {
18
+ create(options: WebSocketOptions): WebSocketClient;
19
+ }
20
+ export declare function createWebSocketBridge(): WebSocketBridge;
@@ -0,0 +1,14 @@
1
+ import type { RuntimeKind } from '../types';
2
+ export interface WindowBounds {
3
+ x?: number;
4
+ y?: number;
5
+ width: number;
6
+ height: number;
7
+ maximized?: boolean;
8
+ }
9
+ export interface WindowStateBridge {
10
+ get(): Promise<WindowBounds | null>;
11
+ set(bounds: WindowBounds): Promise<void>;
12
+ subscribe(listener: (bounds: WindowBounds) => void): () => void;
13
+ }
14
+ export declare function createWebWindowStateBridge(runtime: RuntimeKind, key?: string): WindowStateBridge;
@@ -0,0 +1,19 @@
1
+ import type { BridgeOverrides } from './common/bridge';
2
+ export interface BridgeConfig {
3
+ apiBaseUrl?: string;
4
+ runtimePermissions?: Record<string, {
5
+ enabled?: boolean;
6
+ requirePicker?: boolean;
7
+ }>;
8
+ externalProtocols?: string[];
9
+ updater?: {
10
+ enabled: boolean;
11
+ channel: string;
12
+ requestTimeoutMs: number;
13
+ };
14
+ overrides?: BridgeOverrides;
15
+ }
16
+ /** Call before getBridge(). Configuration belongs to the app, never the SDK package. */
17
+ export declare function configureBridge(config: BridgeConfig): void;
18
+ export declare function getBridgeConfig(): BridgeConfig;
19
+ export declare function lockBridgeConfig(): void;
@@ -0,0 +1,112 @@
1
+ import {
2
+ assertSafeExternalUrl,
3
+ createCommonBridge,
4
+ createDialogBridge,
5
+ createLogger,
6
+ createWebPermissionBridge,
7
+ toBridgeError
8
+ } from "./chunk-XDKQZC4S.js";
9
+
10
+ // src/electron.ts
11
+ async function invoke(channel, payload) {
12
+ try {
13
+ return await window.electronAPI.invoke(channel, payload);
14
+ } catch (error) {
15
+ throw toBridgeError(error);
16
+ }
17
+ }
18
+ function on(channel, listener) {
19
+ try {
20
+ return window.electronAPI.on(channel, listener);
21
+ } catch (error) {
22
+ throw toBridgeError(error);
23
+ }
24
+ }
25
+ function createElectronBridge(overrides = {}) {
26
+ if (!window.electronAPI) {
27
+ throw new Error("Electron preload API is not available.");
28
+ }
29
+ const store = {
30
+ get: async (key) => invoke("store:get", { key }),
31
+ set: async (key, value) => invoke("store:set", { key, value }),
32
+ has: (key) => invoke("store:has", { key }),
33
+ delete: (key) => invoke("store:delete", { key }),
34
+ clear: () => invoke("store:clear"),
35
+ keys: () => invoke("store:keys"),
36
+ entries: () => invoke("store:entries"),
37
+ save: () => invoke("store:save"),
38
+ subscribe: (listener) => on("store:changed", listener)
39
+ };
40
+ const fileSystem = {
41
+ readTextFile: (path) => invoke("fs:readTextFile", { path }),
42
+ saveTextFile: (input) => invoke("fs:saveTextFile", input),
43
+ pickFiles: (options) => invoke("file:pick", options),
44
+ pickDirectory: (options) => invoke("file:pickDirectory", options),
45
+ pickSaveFile: (options) => invoke("file:pickSave", options),
46
+ readFile: async (handle) => new Uint8Array(await invoke("file:read", { id: handle.id })),
47
+ writeFile: (handle, data) => invoke("file:write", { id: handle.id, data }),
48
+ releaseFileHandle: (handle) => invoke("file:release", { id: handle.id })
49
+ };
50
+ const secureStorage = {
51
+ get: (key) => invoke("secure:get", { key }),
52
+ set: (key, value) => invoke("secure:set", { key, value }),
53
+ has: (key) => invoke("secure:has", { key }),
54
+ delete: (key) => invoke("secure:delete", { key })
55
+ };
56
+ const webDialog = createDialogBridge();
57
+ const nativeDialog = {
58
+ alert: (message, title) => invoke("dialog:alert", { message, title }),
59
+ confirm: (message, title) => invoke("dialog:confirm", { message, title }),
60
+ prompt: webDialog.prompt
61
+ };
62
+ const browserPermissions = createWebPermissionBridge();
63
+ const permissions = {
64
+ async query(name, options) {
65
+ const native = await invoke("permission:query", { name, ...options });
66
+ return native.state === "prompt" && !name.startsWith("file-") && !name.startsWith("directory-") ? browserPermissions.query(name, options) : native;
67
+ },
68
+ async request(name, options) {
69
+ const native = await invoke("permission:request", { name, ...options });
70
+ return native.state === "prompt" && !name.startsWith("file-") && !name.startsWith("directory-") ? browserPermissions.request(name, options) : native;
71
+ }
72
+ };
73
+ return createCommonBridge("electron", {
74
+ logger: createLogger("electron", (level, message, context) => {
75
+ void invoke("log:write", { level, message, context }).catch(() => void 0);
76
+ }),
77
+ fileSystem,
78
+ dialog: nativeDialog,
79
+ shell: {
80
+ openExternal: async (url) => {
81
+ await invoke("app:openExternal", { url: assertSafeExternalUrl(url) });
82
+ },
83
+ openPath: (path) => invoke("shell:openPath", { path }),
84
+ revealItem: (path) => invoke("shell:revealItem", { path })
85
+ },
86
+ process: {
87
+ info: () => invoke("process:getInfo"),
88
+ exit: (code) => invoke("process:exit", { code }),
89
+ restart: () => invoke("process:restart")
90
+ },
91
+ deepLink: {
92
+ getInitialUrl: () => invoke("deep-link:getInitial"),
93
+ subscribe: (listener) => on("deep-link:open", listener)
94
+ },
95
+ updater: {
96
+ check: () => invoke("app:checkUpdate"),
97
+ downloadAndInstall: () => invoke("app:downloadAndInstall"),
98
+ subscribe: (listener) => on("updater:progress", listener)
99
+ },
100
+ windowState: {
101
+ get: () => invoke("window:getState"),
102
+ set: (bounds) => invoke("window:setState", bounds),
103
+ subscribe: (listener) => on("window:stateChanged", listener)
104
+ },
105
+ store,
106
+ secureStorage,
107
+ permissions
108
+ }, overrides);
109
+ }
110
+ export {
111
+ createElectronBridge
112
+ };
@@ -0,0 +1,3 @@
1
+ import { type BridgeOverrides } from './common/bridge';
2
+ import type { AppBridge } from './types';
3
+ export declare function createElectronBridge(overrides?: BridgeOverrides): AppBridge;
@@ -0,0 +1,13 @@
1
+ export type AppErrorCode = 'CANCELLED' | 'PERMISSION_DENIED' | 'NOT_FOUND' | 'UNSUPPORTED' | 'INVALID_INPUT' | 'NETWORK_ERROR' | 'UNKNOWN';
2
+ export interface AppErrorShape {
3
+ code: AppErrorCode;
4
+ message: string;
5
+ retryable: boolean;
6
+ }
7
+ export declare class BridgeError extends Error implements AppErrorShape {
8
+ readonly code: AppErrorCode;
9
+ readonly retryable: boolean;
10
+ constructor({ code, message, retryable }: AppErrorShape);
11
+ }
12
+ /** Convert shell-specific rejected values into one stable error type for UI code. */
13
+ export declare function toBridgeError(error: unknown, fallback?: Pick<AppErrorShape, 'code' | 'retryable'>): BridgeError;
@@ -0,0 +1,15 @@
1
+ import type { AppBridge } from './types';
2
+ export { configureBridge } from './config';
3
+ export type { BridgeConfig } from './config';
4
+ export type { AppBridge, RuntimeKind, SaveTextFileInput, UpdateInfo } from './types';
5
+ export { BridgeError, toBridgeError } from './errors';
6
+ export type { AppErrorCode, AppErrorShape } from './errors';
7
+ export { createCommonBridge } from './common/bridge';
8
+ export type { BridgeOverrides } from './common/bridge';
9
+ export * from './common';
10
+ export declare function isTauri(): boolean;
11
+ export declare function isElectron(): boolean;
12
+ /** Resolve the shell adapter once and reuse it for the lifetime of the page. */
13
+ export declare function getBridge(): Promise<AppBridge>;
14
+ /** Shared React/Vue startup handoff after the first paint. */
15
+ export declare function finishDesktopStartup(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ import {
2
+ BridgeError,
3
+ configureBridge,
4
+ createClipboardBridge,
5
+ createCommonBridge,
6
+ createDialogBridge,
7
+ createHttpClient,
8
+ createLogger,
9
+ createNotificationBridge,
10
+ createStubSecureStorageBridge,
11
+ createStubUpdaterBridge,
12
+ createUploadBridge,
13
+ createWebDeepLinkBridge,
14
+ createWebFileSystemBridge,
15
+ createWebPermissionBridge,
16
+ createWebProcessBridge,
17
+ createWebShellBridge,
18
+ createWebSocketBridge,
19
+ createWebStoreBridge,
20
+ createWebWindowStateBridge,
21
+ getBridgeConfig,
22
+ lockBridgeConfig,
23
+ toBridgeError
24
+ } from "./chunk-XDKQZC4S.js";
25
+
26
+ // src/index.ts
27
+ function isTauri() {
28
+ return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
29
+ }
30
+ function isElectron() {
31
+ return typeof window !== "undefined" && Boolean(window.electronAPI);
32
+ }
33
+ var bridgePromise;
34
+ function getBridge() {
35
+ bridgePromise ?? (bridgePromise = (async () => {
36
+ lockBridgeConfig();
37
+ if (isTauri()) {
38
+ const { createTauriBridge } = await import("./tauri-QULQ7SNH.js");
39
+ return createTauriBridge(getBridgeConfig().overrides);
40
+ }
41
+ if (isElectron()) {
42
+ const { createElectronBridge } = await import("./electron-NLAZ6NWN.js");
43
+ return createElectronBridge(getBridgeConfig().overrides);
44
+ }
45
+ const { createWebBridge } = await import("./web-GCAG6XIV.js");
46
+ return createWebBridge(getBridgeConfig().overrides);
47
+ })());
48
+ return bridgePromise;
49
+ }
50
+ async function finishDesktopStartup() {
51
+ if (typeof window === "undefined") return;
52
+ await new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
53
+ document.getElementById("boot-splash")?.classList.add("is-hidden");
54
+ if (isTauri()) {
55
+ const { invoke } = await import("@tauri-apps/api/core");
56
+ await invoke("finish_startup");
57
+ }
58
+ }
59
+ export {
60
+ BridgeError,
61
+ configureBridge,
62
+ createClipboardBridge,
63
+ createCommonBridge,
64
+ createDialogBridge,
65
+ createHttpClient,
66
+ createLogger,
67
+ createNotificationBridge,
68
+ createStubSecureStorageBridge,
69
+ createStubUpdaterBridge,
70
+ createUploadBridge,
71
+ createWebDeepLinkBridge,
72
+ createWebFileSystemBridge,
73
+ createWebPermissionBridge,
74
+ createWebProcessBridge,
75
+ createWebShellBridge,
76
+ createWebSocketBridge,
77
+ createWebStoreBridge,
78
+ createWebWindowStateBridge,
79
+ finishDesktopStartup,
80
+ getBridge,
81
+ isElectron,
82
+ isTauri,
83
+ toBridgeError
84
+ };
@@ -0,0 +1,2 @@
1
+ /** Validate URLs before they reach a shell or the browser. Native layers repeat this check. */
2
+ export declare function assertSafeExternalUrl(value: string): string;