@unified-desktop/bridge 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/chunk-XDKQZC4S.js +936 -0
- package/dist/common/bridge.d.ts +36 -0
- package/dist/common/clipboard.d.ts +5 -0
- package/dist/common/deep-link.d.ts +5 -0
- package/dist/common/dialog.d.ts +6 -0
- package/dist/common/file-system.d.ts +35 -0
- package/dist/common/http-client.d.ts +1 -0
- package/dist/common/http.d.ts +24 -0
- package/dist/common/index.d.ts +16 -0
- package/dist/common/logging.d.ts +11 -0
- package/dist/common/notification.d.ts +10 -0
- package/dist/common/permissions.d.ts +15 -0
- package/dist/common/process.d.ts +12 -0
- package/dist/common/secure-storage.d.ts +7 -0
- package/dist/common/shell.d.ts +6 -0
- package/dist/common/store.d.ts +21 -0
- package/dist/common/updater.d.ts +7 -0
- package/dist/common/upload.d.ts +22 -0
- package/dist/common/websocket.d.ts +20 -0
- package/dist/common/window-state.d.ts +14 -0
- package/dist/config.d.ts +19 -0
- package/dist/electron-NLAZ6NWN.js +112 -0
- package/dist/electron.d.ts +3 -0
- package/dist/errors.d.ts +13 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +84 -0
- package/dist/security.d.ts +2 -0
- package/dist/tauri-QULQ7SNH.js +394 -0
- package/dist/tauri.d.ts +3 -0
- package/dist/types.d.ts +57 -0
- package/dist/web-GCAG6XIV.js +11 -0
- package/dist/web.d.ts +3 -0
- package/package.json +38 -4
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
var defaults = {
|
|
7
|
+
runtimePermissions: Object.fromEntries(["microphone", "notifications", "clipboard-read", "clipboard-write", "file-read", "file-write", "file-download", "directory-read", "directory-write"].map((name) => [name, { enabled: true }])),
|
|
8
|
+
externalProtocols: ["https:", "mailto:"],
|
|
9
|
+
updater: { enabled: false, channel: "stable", requestTimeoutMs: 3e4 }
|
|
10
|
+
};
|
|
11
|
+
var configuration = defaults;
|
|
12
|
+
var locked = false;
|
|
13
|
+
function configureBridge(config) {
|
|
14
|
+
if (locked) throw new Error("Configure the bridge before resolving it.");
|
|
15
|
+
configuration = { ...defaults, ...config };
|
|
16
|
+
}
|
|
17
|
+
function getBridgeConfig() {
|
|
18
|
+
return configuration;
|
|
19
|
+
}
|
|
20
|
+
function lockBridgeConfig() {
|
|
21
|
+
locked = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/errors.ts
|
|
25
|
+
var BridgeError = class extends Error {
|
|
26
|
+
constructor({ code, message, retryable }) {
|
|
27
|
+
super(message);
|
|
28
|
+
__publicField(this, "code");
|
|
29
|
+
__publicField(this, "retryable");
|
|
30
|
+
this.name = "BridgeError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.retryable = retryable;
|
|
33
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var knownErrorCodes = /* @__PURE__ */ new Set([
|
|
37
|
+
"CANCELLED",
|
|
38
|
+
"PERMISSION_DENIED",
|
|
39
|
+
"NOT_FOUND",
|
|
40
|
+
"UNSUPPORTED",
|
|
41
|
+
"INVALID_INPUT",
|
|
42
|
+
"NETWORK_ERROR",
|
|
43
|
+
"UNKNOWN"
|
|
44
|
+
]);
|
|
45
|
+
function isErrorPayload(value) {
|
|
46
|
+
return typeof value === "object" && value !== null && "message" in value;
|
|
47
|
+
}
|
|
48
|
+
function toBridgeError(error, fallback = {
|
|
49
|
+
code: "UNKNOWN",
|
|
50
|
+
retryable: false
|
|
51
|
+
}) {
|
|
52
|
+
if (error instanceof BridgeError) return error;
|
|
53
|
+
if (isErrorPayload(error)) {
|
|
54
|
+
const code = knownErrorCodes.has(error.code) ? error.code : fallback.code;
|
|
55
|
+
return new BridgeError({
|
|
56
|
+
code,
|
|
57
|
+
message: String(error.message),
|
|
58
|
+
retryable: typeof error.retryable === "boolean" ? error.retryable : fallback.retryable
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return new BridgeError({
|
|
62
|
+
...fallback,
|
|
63
|
+
message: error instanceof Error ? error.message : String(error)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/common/clipboard.ts
|
|
68
|
+
function createClipboardBridge() {
|
|
69
|
+
return {
|
|
70
|
+
async readText() {
|
|
71
|
+
try {
|
|
72
|
+
if (!navigator.clipboard?.readText) throw new Error("Clipboard API is unavailable.");
|
|
73
|
+
return await navigator.clipboard.readText();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new BridgeError({
|
|
76
|
+
code: "PERMISSION_DENIED",
|
|
77
|
+
message: error instanceof Error ? error.message : "Unable to read clipboard.",
|
|
78
|
+
retryable: false
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async writeText(text) {
|
|
83
|
+
try {
|
|
84
|
+
if (!navigator.clipboard?.writeText) throw new Error("Clipboard API is unavailable.");
|
|
85
|
+
await navigator.clipboard.writeText(text);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new BridgeError({
|
|
88
|
+
code: "PERMISSION_DENIED",
|
|
89
|
+
message: error instanceof Error ? error.message : "Unable to write clipboard.",
|
|
90
|
+
retryable: false
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/common/deep-link.ts
|
|
98
|
+
function createWebDeepLinkBridge() {
|
|
99
|
+
return {
|
|
100
|
+
async getInitialUrl() {
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
subscribe(listener) {
|
|
104
|
+
const handleChange = () => listener(window.location.href);
|
|
105
|
+
window.addEventListener("popstate", handleChange);
|
|
106
|
+
window.addEventListener("hashchange", handleChange);
|
|
107
|
+
return () => {
|
|
108
|
+
window.removeEventListener("popstate", handleChange);
|
|
109
|
+
window.removeEventListener("hashchange", handleChange);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/common/dialog.ts
|
|
116
|
+
function createDialogBridge() {
|
|
117
|
+
return {
|
|
118
|
+
async alert(message, title) {
|
|
119
|
+
window.alert(title ? `${title}
|
|
120
|
+
|
|
121
|
+
${message}` : message);
|
|
122
|
+
},
|
|
123
|
+
async confirm(message, title) {
|
|
124
|
+
return window.confirm(title ? `${title}
|
|
125
|
+
|
|
126
|
+
${message}` : message);
|
|
127
|
+
},
|
|
128
|
+
async prompt(message, defaultValue, title) {
|
|
129
|
+
return window.prompt(title ? `${title}
|
|
130
|
+
|
|
131
|
+
${message}` : message, defaultValue ?? "");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/common/file-system.ts
|
|
137
|
+
function createWebFileSystemBridge() {
|
|
138
|
+
const handles = /* @__PURE__ */ new Map();
|
|
139
|
+
const assertEnabled = (name) => {
|
|
140
|
+
if (getBridgeConfig().runtimePermissions?.[name]?.enabled !== true) throw new BridgeError({ code: "PERMISSION_DENIED", message: `The ${name} capability is disabled by security policy.`, retryable: false });
|
|
141
|
+
};
|
|
142
|
+
const bridge = {
|
|
143
|
+
hasPermission(name, id) {
|
|
144
|
+
const handle = handles.get(id);
|
|
145
|
+
return Boolean(handle && handle.permission === name);
|
|
146
|
+
},
|
|
147
|
+
async readTextFile(path) {
|
|
148
|
+
assertEnabled("file-read");
|
|
149
|
+
if (getBridgeConfig().runtimePermissions?.["file-read"]?.requirePicker !== false) throw new BridgeError({ code: "PERMISSION_DENIED", message: "Select a file and use its handle to read it.", retryable: false });
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(path);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new BridgeError({
|
|
154
|
+
code: response.status === 404 ? "NOT_FOUND" : "NETWORK_ERROR",
|
|
155
|
+
message: `Unable to read ${path} (${response.status}).`,
|
|
156
|
+
retryable: response.status >= 500
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return response.text();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw toBridgeError(error, { code: "NETWORK_ERROR", retryable: true });
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
async saveTextFile({ suggestedName, contents }) {
|
|
165
|
+
assertEnabled("file-download");
|
|
166
|
+
if (getBridgeConfig().runtimePermissions?.["file-download"]?.requirePicker !== false) {
|
|
167
|
+
const handle = await bridge.pickSaveFile({ defaultPath: suggestedName, permission: "file-download" });
|
|
168
|
+
if (!handle) throw new BridgeError({ code: "CANCELLED", message: "File save was cancelled.", retryable: false });
|
|
169
|
+
try {
|
|
170
|
+
await bridge.writeFile(handle, new TextEncoder().encode(contents));
|
|
171
|
+
} finally {
|
|
172
|
+
await bridge.releaseFileHandle(handle);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
|
|
178
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
179
|
+
const anchor = document.createElement("a");
|
|
180
|
+
anchor.href = objectUrl;
|
|
181
|
+
anchor.download = suggestedName;
|
|
182
|
+
anchor.click();
|
|
183
|
+
URL.revokeObjectURL(objectUrl);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw toBridgeError(error);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
async pickFiles(options) {
|
|
189
|
+
assertEnabled("file-read");
|
|
190
|
+
if (typeof document === "undefined") return [];
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const input = document.createElement("input");
|
|
193
|
+
input.type = "file";
|
|
194
|
+
input.multiple = options?.multiple === true;
|
|
195
|
+
input.accept = options?.filters?.flatMap((filter) => filter.extensions.map((extension) => `.${extension.replace(/^\./, "")}`)).join(",") ?? "";
|
|
196
|
+
input.onchange = () => {
|
|
197
|
+
const selected = Array.from(input.files ?? []).map((file) => {
|
|
198
|
+
const id = crypto.randomUUID();
|
|
199
|
+
handles.set(id, { file, permission: "file-read" });
|
|
200
|
+
return { id, name: file.name, kind: "file", writable: false, size: file.size };
|
|
201
|
+
});
|
|
202
|
+
resolve(selected);
|
|
203
|
+
};
|
|
204
|
+
input.oncancel = () => resolve([]);
|
|
205
|
+
input.click();
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
async pickDirectory(options) {
|
|
209
|
+
assertEnabled(options?.writable ? "directory-write" : "directory-read");
|
|
210
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: "A native directory dialog is unavailable in the browser.", retryable: false });
|
|
211
|
+
},
|
|
212
|
+
async pickSaveFile(options) {
|
|
213
|
+
const permission = options?.permission ?? "file-write";
|
|
214
|
+
if (!["file-write", "file-download"].includes(permission)) throw new BridgeError({ code: "INVALID_INPUT", message: "Invalid save permission.", retryable: false });
|
|
215
|
+
assertEnabled(permission);
|
|
216
|
+
const browser = typeof window === "undefined" ? void 0 : window;
|
|
217
|
+
if (!browser?.showSaveFilePicker) throw new BridgeError({ code: "UNSUPPORTED", message: "A save picker is unavailable in this browser.", retryable: false });
|
|
218
|
+
try {
|
|
219
|
+
const save = await browser.showSaveFilePicker({ suggestedName: options?.defaultPath });
|
|
220
|
+
const id = crypto.randomUUID();
|
|
221
|
+
handles.set(id, { save, permission });
|
|
222
|
+
return { id, name: save.name, kind: "file", writable: true };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof DOMException && error.name === "AbortError") return null;
|
|
225
|
+
throw toBridgeError(error);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
async readFile(handle) {
|
|
229
|
+
assertEnabled("file-read");
|
|
230
|
+
const file = handles.get(handle.id)?.file;
|
|
231
|
+
if (!file) throw new BridgeError({ code: "NOT_FOUND", message: "The file handle is no longer available.", retryable: false });
|
|
232
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
233
|
+
},
|
|
234
|
+
async writeFile(handle, data) {
|
|
235
|
+
const owned = handles.get(handle.id);
|
|
236
|
+
if (!owned) throw new BridgeError({ code: "NOT_FOUND", message: "The file handle is no longer available.", retryable: false });
|
|
237
|
+
assertEnabled(owned.permission);
|
|
238
|
+
if (!owned.save) throw new BridgeError({ code: "PERMISSION_DENIED", message: "This file handle is read-only.", retryable: false });
|
|
239
|
+
const writer = await owned.save.createWritable();
|
|
240
|
+
try {
|
|
241
|
+
await writer.write(data);
|
|
242
|
+
await writer.close();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
await writer.abort?.().catch(() => void 0);
|
|
245
|
+
throw toBridgeError(error);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async releaseFileHandle(handle) {
|
|
249
|
+
handles.delete(handle.id);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
return bridge;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/common/http.ts
|
|
256
|
+
function buildUrl(baseUrl, input) {
|
|
257
|
+
const fallbackBase = typeof window !== "undefined" ? window.location.href : "http://localhost/";
|
|
258
|
+
const url = new URL(input.url, baseUrl || fallbackBase);
|
|
259
|
+
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
260
|
+
if (value !== null && value !== void 0) url.searchParams.set(key, String(value));
|
|
261
|
+
}
|
|
262
|
+
return url.toString();
|
|
263
|
+
}
|
|
264
|
+
function prepareBody(body, headers) {
|
|
265
|
+
if (body === void 0 || body === null) return void 0;
|
|
266
|
+
if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData || body instanceof URLSearchParams) {
|
|
267
|
+
return body;
|
|
268
|
+
}
|
|
269
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
270
|
+
return JSON.stringify(body);
|
|
271
|
+
}
|
|
272
|
+
function createTimeoutSignal(signal, timeoutMs) {
|
|
273
|
+
if (!timeoutMs && !signal) return { signal: void 0, cleanup: () => void 0 };
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
const abort = () => controller.abort();
|
|
276
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
277
|
+
const timeout = timeoutMs ? globalThis.setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
278
|
+
return {
|
|
279
|
+
signal: controller.signal,
|
|
280
|
+
cleanup: () => {
|
|
281
|
+
signal?.removeEventListener("abort", abort);
|
|
282
|
+
if (timeout !== void 0) globalThis.clearTimeout(timeout);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function createHttpClient(options = {}) {
|
|
287
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
288
|
+
return {
|
|
289
|
+
async request(input) {
|
|
290
|
+
const headers = new Headers(input.headers);
|
|
291
|
+
const { signal, cleanup } = createTimeoutSignal(input.signal, input.timeoutMs);
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetchImpl(buildUrl(options.baseUrl ?? "", input), {
|
|
294
|
+
method: input.method ?? "GET",
|
|
295
|
+
headers,
|
|
296
|
+
body: prepareBody(input.body, headers),
|
|
297
|
+
signal
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
throw new BridgeError({
|
|
301
|
+
code: response.status >= 500 ? "NETWORK_ERROR" : "UNKNOWN",
|
|
302
|
+
message: `HTTP ${response.status} for ${input.method ?? "GET"} ${input.url}`,
|
|
303
|
+
retryable: response.status >= 500
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const responseType = input.responseType ?? "json";
|
|
307
|
+
const data = responseType === "arrayBuffer" ? await response.arrayBuffer() : responseType === "text" ? await response.text() : await response.text().then((text) => text ? JSON.parse(text) : void 0);
|
|
308
|
+
return { status: response.status, headers: response.headers, data };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof BridgeError) throw error;
|
|
311
|
+
throw new BridgeError({
|
|
312
|
+
code: input.signal?.aborted ? "CANCELLED" : "NETWORK_ERROR",
|
|
313
|
+
message: error instanceof Error ? error.message : "HTTP request failed.",
|
|
314
|
+
retryable: !input.signal?.aborted
|
|
315
|
+
});
|
|
316
|
+
} finally {
|
|
317
|
+
cleanup();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/common/logging.ts
|
|
324
|
+
function createLogger(runtime, sink) {
|
|
325
|
+
const write = (level, message, context) => {
|
|
326
|
+
sink?.(level, message, { runtime, ...context });
|
|
327
|
+
const method = level === "debug" ? console.debug : console[level];
|
|
328
|
+
method(`[${runtime}] ${message}`, context ?? "");
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
debug: (message, context) => write("debug", message, context),
|
|
332
|
+
info: (message, context) => write("info", message, context),
|
|
333
|
+
warn: (message, context) => write("warn", message, context),
|
|
334
|
+
error: (message, context) => write("error", message, context)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/common/notification.ts
|
|
339
|
+
function createNotificationBridge() {
|
|
340
|
+
return {
|
|
341
|
+
async requestPermission() {
|
|
342
|
+
if (!("Notification" in window)) return "denied";
|
|
343
|
+
return Notification.requestPermission();
|
|
344
|
+
},
|
|
345
|
+
async show(input) {
|
|
346
|
+
if (!("Notification" in window)) {
|
|
347
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: "Notifications are unavailable.", retryable: false });
|
|
348
|
+
}
|
|
349
|
+
const permission = Notification.permission === "default" ? await Notification.requestPermission() : Notification.permission;
|
|
350
|
+
if (permission !== "granted") {
|
|
351
|
+
throw new BridgeError({ code: "PERMISSION_DENIED", message: "Notification permission was not granted.", retryable: false });
|
|
352
|
+
}
|
|
353
|
+
new Notification(input.title, { body: input.body, tag: input.tag });
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/common/process.ts
|
|
359
|
+
function createWebProcessBridge(runtime) {
|
|
360
|
+
const unsupported = (operation) => new BridgeError({
|
|
361
|
+
code: "UNSUPPORTED",
|
|
362
|
+
message: `${operation} is unavailable in a browser.`,
|
|
363
|
+
retryable: false
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
async info() {
|
|
367
|
+
return { runtime, userAgent: navigator.userAgent };
|
|
368
|
+
},
|
|
369
|
+
async exit() {
|
|
370
|
+
throw unsupported("Exiting the process");
|
|
371
|
+
},
|
|
372
|
+
async restart() {
|
|
373
|
+
throw unsupported("Restarting the process");
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/security.ts
|
|
379
|
+
function assertSafeExternalUrl(value) {
|
|
380
|
+
let url;
|
|
381
|
+
try {
|
|
382
|
+
url = new URL(value);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new BridgeError({
|
|
385
|
+
code: "INVALID_INPUT",
|
|
386
|
+
message: "A valid external URL is required.",
|
|
387
|
+
retryable: false
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (!getBridgeConfig().externalProtocols?.includes(url.protocol)) {
|
|
391
|
+
throw new BridgeError({
|
|
392
|
+
code: "UNSUPPORTED",
|
|
393
|
+
message: `External protocol is not allowed: ${url.protocol}`,
|
|
394
|
+
retryable: false
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return url.toString();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/common/shell.ts
|
|
401
|
+
function createWebShellBridge() {
|
|
402
|
+
return {
|
|
403
|
+
async openExternal(url) {
|
|
404
|
+
const opened = window.open(assertSafeExternalUrl(url), "_blank", "noopener,noreferrer");
|
|
405
|
+
if (!opened) {
|
|
406
|
+
throw new BridgeError({ code: "PERMISSION_DENIED", message: "The browser blocked opening an external window.", retryable: false });
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
async openPath() {
|
|
410
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: "Opening local paths is unavailable in a browser.", retryable: false });
|
|
411
|
+
},
|
|
412
|
+
async revealItem() {
|
|
413
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: "Revealing local paths is unavailable in a browser.", retryable: false });
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/common/updater.ts
|
|
419
|
+
function createStubUpdaterBridge() {
|
|
420
|
+
return {
|
|
421
|
+
async check() {
|
|
422
|
+
return null;
|
|
423
|
+
},
|
|
424
|
+
async downloadAndInstall() {
|
|
425
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: "The updater is not configured for this shell.", retryable: false });
|
|
426
|
+
},
|
|
427
|
+
subscribe() {
|
|
428
|
+
return () => void 0;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/common/upload.ts
|
|
434
|
+
function createUploadBridge() {
|
|
435
|
+
return {
|
|
436
|
+
send(request) {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
const xhr = new XMLHttpRequest();
|
|
439
|
+
xhr.open(request.method ?? "POST", request.url);
|
|
440
|
+
for (const name of Object.keys(request.headers ?? {})) {
|
|
441
|
+
xhr.setRequestHeader(name, request.headers[name]);
|
|
442
|
+
}
|
|
443
|
+
if (request.timeoutMs) xhr.timeout = request.timeoutMs;
|
|
444
|
+
xhr.upload.onprogress = (event) => {
|
|
445
|
+
request.onProgress?.({
|
|
446
|
+
loaded: event.loaded,
|
|
447
|
+
total: event.lengthComputable ? event.total : void 0,
|
|
448
|
+
percent: event.lengthComputable ? event.loaded / event.total * 100 : void 0
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
xhr.onload = () => {
|
|
452
|
+
let data;
|
|
453
|
+
try {
|
|
454
|
+
const contentType = xhr.getResponseHeader("content-type") ?? "";
|
|
455
|
+
data = contentType.includes("application/json") ? JSON.parse(xhr.responseText || "null") : xhr.responseText;
|
|
456
|
+
} catch {
|
|
457
|
+
reject(new BridgeError({ code: "UNKNOWN", message: "Upload response was not valid JSON.", retryable: false }));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
461
|
+
reject(new BridgeError({ code: xhr.status >= 500 ? "NETWORK_ERROR" : "UNKNOWN", message: `Upload failed with HTTP ${xhr.status}.`, retryable: xhr.status >= 500 }));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const headers = new Headers();
|
|
465
|
+
for (const line of xhr.getAllResponseHeaders().trim().split(/\r?\n/)) {
|
|
466
|
+
const separator = line.indexOf(":");
|
|
467
|
+
if (separator > 0) headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
468
|
+
}
|
|
469
|
+
resolve({ status: xhr.status, headers, data });
|
|
470
|
+
};
|
|
471
|
+
xhr.onerror = () => reject(new BridgeError({ code: "NETWORK_ERROR", message: "Upload request failed.", retryable: true }));
|
|
472
|
+
xhr.ontimeout = () => reject(new BridgeError({ code: "NETWORK_ERROR", message: "Upload request timed out.", retryable: true }));
|
|
473
|
+
xhr.onabort = () => reject(new BridgeError({ code: "CANCELLED", message: "Upload was cancelled.", retryable: false }));
|
|
474
|
+
const abort = () => xhr.abort();
|
|
475
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
476
|
+
xhr.onloadend = () => request.signal?.removeEventListener("abort", abort);
|
|
477
|
+
if (request.signal?.aborted) {
|
|
478
|
+
xhr.abort();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
xhr.send(request.body);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/common/websocket.ts
|
|
488
|
+
function assertWebSocketUrl(value) {
|
|
489
|
+
let url;
|
|
490
|
+
try {
|
|
491
|
+
url = new URL(value);
|
|
492
|
+
} catch {
|
|
493
|
+
throw new BridgeError({ code: "INVALID_INPUT", message: "A valid WebSocket URL is required.", retryable: false });
|
|
494
|
+
}
|
|
495
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
|
496
|
+
throw new BridgeError({ code: "UNSUPPORTED", message: `WebSocket protocol is not allowed: ${url.protocol}`, retryable: false });
|
|
497
|
+
}
|
|
498
|
+
return url.toString();
|
|
499
|
+
}
|
|
500
|
+
var ReconnectingWebSocket = class {
|
|
501
|
+
constructor(options) {
|
|
502
|
+
__publicField(this, "options", options);
|
|
503
|
+
__publicField(this, "socket");
|
|
504
|
+
__publicField(this, "currentStatus", "idle");
|
|
505
|
+
__publicField(this, "retries", 0);
|
|
506
|
+
__publicField(this, "manuallyClosed", false);
|
|
507
|
+
__publicField(this, "connection");
|
|
508
|
+
__publicField(this, "rejectConnection");
|
|
509
|
+
__publicField(this, "reconnectTimer");
|
|
510
|
+
__publicField(this, "generation", 0);
|
|
511
|
+
__publicField(this, "messageListeners", /* @__PURE__ */ new Set());
|
|
512
|
+
__publicField(this, "statusListeners", /* @__PURE__ */ new Set());
|
|
513
|
+
}
|
|
514
|
+
get status() {
|
|
515
|
+
return this.currentStatus;
|
|
516
|
+
}
|
|
517
|
+
setStatus(status2) {
|
|
518
|
+
this.currentStatus = status2;
|
|
519
|
+
for (const listener of this.statusListeners) listener(status2);
|
|
520
|
+
}
|
|
521
|
+
connect() {
|
|
522
|
+
if (this.connection) return this.connection;
|
|
523
|
+
this.manuallyClosed = false;
|
|
524
|
+
if (this.reconnectTimer) {
|
|
525
|
+
globalThis.clearTimeout(this.reconnectTimer);
|
|
526
|
+
this.reconnectTimer = void 0;
|
|
527
|
+
}
|
|
528
|
+
this.setStatus("connecting");
|
|
529
|
+
const generation = ++this.generation;
|
|
530
|
+
this.connection = new Promise((resolve, reject) => {
|
|
531
|
+
this.rejectConnection = reject;
|
|
532
|
+
const socket = new WebSocket(this.options.url, this.options.protocols);
|
|
533
|
+
this.socket = socket;
|
|
534
|
+
const isCurrent = () => generation === this.generation && socket === this.socket;
|
|
535
|
+
const fail = (message) => {
|
|
536
|
+
if (isCurrent()) {
|
|
537
|
+
this.connection = void 0;
|
|
538
|
+
this.rejectConnection = void 0;
|
|
539
|
+
reject(new BridgeError({ code: "NETWORK_ERROR", message, retryable: true }));
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
socket.onopen = () => {
|
|
543
|
+
if (!isCurrent()) return;
|
|
544
|
+
this.retries = 0;
|
|
545
|
+
this.setStatus("open");
|
|
546
|
+
resolve();
|
|
547
|
+
};
|
|
548
|
+
socket.onmessage = (event) => {
|
|
549
|
+
if (!isCurrent()) return;
|
|
550
|
+
let data = event.data;
|
|
551
|
+
if (typeof event.data === "string") {
|
|
552
|
+
try {
|
|
553
|
+
data = JSON.parse(event.data);
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const listener of this.messageListeners) listener(data);
|
|
558
|
+
};
|
|
559
|
+
socket.onerror = () => {
|
|
560
|
+
if (this.currentStatus !== "open") fail("WebSocket connection failed.");
|
|
561
|
+
};
|
|
562
|
+
socket.onclose = () => {
|
|
563
|
+
if (!isCurrent()) return;
|
|
564
|
+
const wasConnecting = this.currentStatus === "connecting";
|
|
565
|
+
this.socket = void 0;
|
|
566
|
+
this.connection = void 0;
|
|
567
|
+
this.rejectConnection = void 0;
|
|
568
|
+
this.setStatus("closed");
|
|
569
|
+
if (wasConnecting) reject(new BridgeError({ code: "NETWORK_ERROR", message: "WebSocket closed before opening.", retryable: true }));
|
|
570
|
+
if (!this.manuallyClosed && this.options.reconnect && this.retries < (this.options.maxRetries ?? 5)) {
|
|
571
|
+
this.retries += 1;
|
|
572
|
+
this.reconnectTimer = globalThis.setTimeout(() => {
|
|
573
|
+
this.reconnectTimer = void 0;
|
|
574
|
+
void this.connect().catch(() => void 0);
|
|
575
|
+
}, this.options.retryDelayMs ?? 1e3);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
return this.connection;
|
|
580
|
+
}
|
|
581
|
+
send(data) {
|
|
582
|
+
if (this.socket?.readyState !== WebSocket.OPEN) throw new BridgeError({ code: "UNSUPPORTED", message: "WebSocket is not open.", retryable: true });
|
|
583
|
+
this.socket.send(data);
|
|
584
|
+
}
|
|
585
|
+
close(code, reason) {
|
|
586
|
+
this.manuallyClosed = true;
|
|
587
|
+
this.generation += 1;
|
|
588
|
+
if (this.reconnectTimer) {
|
|
589
|
+
globalThis.clearTimeout(this.reconnectTimer);
|
|
590
|
+
this.reconnectTimer = void 0;
|
|
591
|
+
}
|
|
592
|
+
const rejectConnection = this.rejectConnection;
|
|
593
|
+
this.connection = void 0;
|
|
594
|
+
this.rejectConnection = void 0;
|
|
595
|
+
if (this.currentStatus === "connecting") {
|
|
596
|
+
rejectConnection?.(new BridgeError({ code: "NETWORK_ERROR", message: "WebSocket connection was closed before opening.", retryable: true }));
|
|
597
|
+
}
|
|
598
|
+
this.socket?.close(code, reason);
|
|
599
|
+
this.socket = void 0;
|
|
600
|
+
this.setStatus("closed");
|
|
601
|
+
}
|
|
602
|
+
onMessage(listener) {
|
|
603
|
+
this.messageListeners.add(listener);
|
|
604
|
+
return () => this.messageListeners.delete(listener);
|
|
605
|
+
}
|
|
606
|
+
onStatus(listener) {
|
|
607
|
+
this.statusListeners.add(listener);
|
|
608
|
+
return () => this.statusListeners.delete(listener);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
function createWebSocketBridge() {
|
|
612
|
+
return {
|
|
613
|
+
create: (options) => new ReconnectingWebSocket({ ...options, url: assertWebSocketUrl(options.url) })
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/common/window-state.ts
|
|
618
|
+
function createWebWindowStateBridge(runtime, key = "unified-desktop.window-state") {
|
|
619
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
620
|
+
return {
|
|
621
|
+
async get() {
|
|
622
|
+
try {
|
|
623
|
+
const raw = localStorage.getItem(key);
|
|
624
|
+
return raw ? JSON.parse(raw) : null;
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async set(bounds) {
|
|
630
|
+
localStorage.setItem(key, JSON.stringify(bounds));
|
|
631
|
+
for (const listener of listeners) listener(bounds);
|
|
632
|
+
window.dispatchEvent(new CustomEvent(`${runtime}:window-state`, { detail: bounds }));
|
|
633
|
+
},
|
|
634
|
+
subscribe(listener) {
|
|
635
|
+
listeners.add(listener);
|
|
636
|
+
return () => listeners.delete(listener);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/common/store.ts
|
|
642
|
+
var forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
643
|
+
function assertKey(key) {
|
|
644
|
+
if (typeof key !== "string" || key.trim() === "" || key.length > 256 || forbiddenKeys.has(key)) {
|
|
645
|
+
throw new BridgeError({ code: "INVALID_INPUT", message: "Store keys must be non-empty strings up to 256 characters.", retryable: false });
|
|
646
|
+
}
|
|
647
|
+
return key;
|
|
648
|
+
}
|
|
649
|
+
function assertValue(value) {
|
|
650
|
+
try {
|
|
651
|
+
const serialized = JSON.stringify(value);
|
|
652
|
+
if (serialized === void 0) throw new Error("undefined is not supported");
|
|
653
|
+
return JSON.parse(serialized);
|
|
654
|
+
} catch {
|
|
655
|
+
throw new BridgeError({ code: "INVALID_INPUT", message: "Store values must be JSON-serializable.", retryable: false });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function cloneValue(value) {
|
|
659
|
+
if (value === void 0) return value;
|
|
660
|
+
return JSON.parse(JSON.stringify(value));
|
|
661
|
+
}
|
|
662
|
+
function isStoreValue(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
663
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
664
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
665
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
666
|
+
ancestors.add(value);
|
|
667
|
+
const valid = Array.isArray(value) ? value.every((item) => isStoreValue(item, ancestors)) : Object.entries(value).every(([key, item]) => !forbiddenKeys.has(key) && isStoreValue(item, ancestors));
|
|
668
|
+
ancestors.delete(value);
|
|
669
|
+
return valid;
|
|
670
|
+
}
|
|
671
|
+
function readValues(storage, storageKey) {
|
|
672
|
+
const raw = storage.getItem(storageKey);
|
|
673
|
+
if (!raw) return /* @__PURE__ */ Object.create(null);
|
|
674
|
+
try {
|
|
675
|
+
const parsed = JSON.parse(raw);
|
|
676
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !isStoreValue(parsed)) {
|
|
677
|
+
throw new Error("Store contains invalid JSON values.");
|
|
678
|
+
}
|
|
679
|
+
const values = /* @__PURE__ */ Object.create(null);
|
|
680
|
+
for (const [key, value] of Object.entries(parsed)) values[key] = value;
|
|
681
|
+
return values;
|
|
682
|
+
} catch {
|
|
683
|
+
throw new BridgeError({ code: "UNKNOWN", message: `Unable to parse store ${storageKey}.`, retryable: false });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function createMemoryStorage() {
|
|
687
|
+
const values = /* @__PURE__ */ new Map();
|
|
688
|
+
return {
|
|
689
|
+
get length() {
|
|
690
|
+
return values.size;
|
|
691
|
+
},
|
|
692
|
+
clear() {
|
|
693
|
+
values.clear();
|
|
694
|
+
},
|
|
695
|
+
getItem(key) {
|
|
696
|
+
return values.get(key) ?? null;
|
|
697
|
+
},
|
|
698
|
+
key(index) {
|
|
699
|
+
return Array.from(values.keys())[index] ?? null;
|
|
700
|
+
},
|
|
701
|
+
removeItem(key) {
|
|
702
|
+
values.delete(key);
|
|
703
|
+
},
|
|
704
|
+
setItem(key, value) {
|
|
705
|
+
values.set(key, value);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function createWebStoreBridge(storageKey = "unified-desktop-store", storage) {
|
|
710
|
+
const backingStorage = storage ?? (typeof localStorage !== "undefined" ? localStorage : createMemoryStorage());
|
|
711
|
+
let values = readValues(backingStorage, storageKey);
|
|
712
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
713
|
+
const persist = () => {
|
|
714
|
+
try {
|
|
715
|
+
backingStorage.setItem(storageKey, JSON.stringify(values));
|
|
716
|
+
} catch (error) {
|
|
717
|
+
throw new BridgeError({
|
|
718
|
+
code: "UNKNOWN",
|
|
719
|
+
message: error instanceof Error ? error.message : `Unable to persist store ${storageKey}.`,
|
|
720
|
+
retryable: false
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const notify = (change) => listeners.forEach((listener) => listener(change));
|
|
725
|
+
const onStorage = (event) => {
|
|
726
|
+
if (event.storageArea !== backingStorage || event.key !== storageKey) return;
|
|
727
|
+
const previous = values;
|
|
728
|
+
values = readValues(backingStorage, storageKey);
|
|
729
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(values)]);
|
|
730
|
+
for (const key of keys) {
|
|
731
|
+
const before = Object.hasOwn(previous, key) ? JSON.stringify(previous[key]) : void 0;
|
|
732
|
+
const after = Object.hasOwn(values, key) ? JSON.stringify(values[key]) : void 0;
|
|
733
|
+
if (before !== after) notify({ key, value: cloneValue(values[key]) });
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
|
|
737
|
+
return {
|
|
738
|
+
async get(key) {
|
|
739
|
+
return cloneValue(values[assertKey(key)]);
|
|
740
|
+
},
|
|
741
|
+
async set(key, value) {
|
|
742
|
+
const safeKey = assertKey(key);
|
|
743
|
+
values[safeKey] = assertValue(value);
|
|
744
|
+
persist();
|
|
745
|
+
notify({ key: safeKey, value: cloneValue(values[safeKey]) });
|
|
746
|
+
},
|
|
747
|
+
async has(key) {
|
|
748
|
+
return Object.hasOwn(values, assertKey(key));
|
|
749
|
+
},
|
|
750
|
+
async delete(key) {
|
|
751
|
+
const safeKey = assertKey(key);
|
|
752
|
+
if (!Object.hasOwn(values, safeKey)) return false;
|
|
753
|
+
delete values[safeKey];
|
|
754
|
+
persist();
|
|
755
|
+
notify({ key: safeKey, value: void 0 });
|
|
756
|
+
return true;
|
|
757
|
+
},
|
|
758
|
+
async clear() {
|
|
759
|
+
const previousKeys = Object.keys(values);
|
|
760
|
+
values = /* @__PURE__ */ Object.create(null);
|
|
761
|
+
persist();
|
|
762
|
+
previousKeys.forEach((key) => notify({ key, value: void 0 }));
|
|
763
|
+
},
|
|
764
|
+
async keys() {
|
|
765
|
+
return Object.keys(values);
|
|
766
|
+
},
|
|
767
|
+
async entries() {
|
|
768
|
+
return Object.entries(values).map(([key, value]) => [key, cloneValue(value)]);
|
|
769
|
+
},
|
|
770
|
+
async save() {
|
|
771
|
+
persist();
|
|
772
|
+
},
|
|
773
|
+
subscribe(listener) {
|
|
774
|
+
listeners.add(listener);
|
|
775
|
+
return () => listeners.delete(listener);
|
|
776
|
+
},
|
|
777
|
+
dispose() {
|
|
778
|
+
if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
|
|
779
|
+
listeners.clear();
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/common/secure-storage.ts
|
|
785
|
+
function createStubSecureStorageBridge() {
|
|
786
|
+
const unsupported = () => new BridgeError({
|
|
787
|
+
code: "UNSUPPORTED",
|
|
788
|
+
message: "Secure storage is unavailable in this runtime.",
|
|
789
|
+
retryable: false
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
async get() {
|
|
793
|
+
throw unsupported();
|
|
794
|
+
},
|
|
795
|
+
async set() {
|
|
796
|
+
throw unsupported();
|
|
797
|
+
},
|
|
798
|
+
async has() {
|
|
799
|
+
throw unsupported();
|
|
800
|
+
},
|
|
801
|
+
async delete() {
|
|
802
|
+
throw unsupported();
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/common/permissions.ts
|
|
808
|
+
function status(name, state, scope) {
|
|
809
|
+
return scope ? { name, state, scope } : { name, state };
|
|
810
|
+
}
|
|
811
|
+
function isEnabled(name) {
|
|
812
|
+
return getBridgeConfig().runtimePermissions?.[name]?.enabled === true;
|
|
813
|
+
}
|
|
814
|
+
function browserPermissionName(name) {
|
|
815
|
+
if (name === "microphone" || name === "camera") return name;
|
|
816
|
+
if (name === "notifications") return "notifications";
|
|
817
|
+
if (name === "clipboard-read" || name === "clipboard-write") return name;
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
async function queryBrowser(name) {
|
|
821
|
+
if (!isEnabled(name)) return status(name, "unsupported");
|
|
822
|
+
if (name === "file-download") return getBridgeConfig().runtimePermissions?.[name]?.requirePicker !== false ? status(name, "prompt", "save-dialog") : status(name, "granted", "browser-download");
|
|
823
|
+
if (name.startsWith("file-") || name.startsWith("directory-")) return status(name, "prompt", "native-dialog");
|
|
824
|
+
if (typeof navigator === "undefined") return status(name, "unsupported");
|
|
825
|
+
const permissionName = browserPermissionName(name);
|
|
826
|
+
if (!permissionName || !navigator.permissions?.query) return status(name, "prompt");
|
|
827
|
+
try {
|
|
828
|
+
const result = await navigator.permissions.query({ name: permissionName });
|
|
829
|
+
return status(name, result.state);
|
|
830
|
+
} catch {
|
|
831
|
+
return status(name, "prompt");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function requestMedia(name) {
|
|
835
|
+
if (typeof navigator === "undefined") return status(name, "unsupported");
|
|
836
|
+
if (!navigator.mediaDevices?.getUserMedia) return status(name, "unsupported");
|
|
837
|
+
try {
|
|
838
|
+
const stream = await navigator.mediaDevices.getUserMedia(name === "microphone" ? { audio: true } : { video: true });
|
|
839
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
840
|
+
return status(name, "granted");
|
|
841
|
+
} catch (error) {
|
|
842
|
+
const normalized = toBridgeError(error, { code: "PERMISSION_DENIED", retryable: false });
|
|
843
|
+
return status(name, normalized.code === "UNSUPPORTED" ? "unsupported" : "denied");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function createWebPermissionBridge(hasPermission = () => false) {
|
|
847
|
+
return {
|
|
848
|
+
async query(name, options) {
|
|
849
|
+
if (!isEnabled(name)) return status(name, "unsupported");
|
|
850
|
+
if (options?.handleId && (name.startsWith("file-") || name.startsWith("directory-"))) {
|
|
851
|
+
return status(name, hasPermission(name, options.handleId) ? "granted" : "denied", "selected-handle");
|
|
852
|
+
}
|
|
853
|
+
return queryBrowser(name);
|
|
854
|
+
},
|
|
855
|
+
async request(name, options) {
|
|
856
|
+
if (!isEnabled(name)) return status(name, "unsupported");
|
|
857
|
+
if (options?.handleId && (name.startsWith("file-") || name.startsWith("directory-"))) {
|
|
858
|
+
return status(name, hasPermission(name, options.handleId) ? "granted" : "denied", "selected-handle");
|
|
859
|
+
}
|
|
860
|
+
if (name === "microphone" || name === "camera") return requestMedia(name);
|
|
861
|
+
if (name === "notifications") {
|
|
862
|
+
if (typeof window === "undefined" || !("Notification" in window)) return status(name, "unsupported");
|
|
863
|
+
const permission = await Notification.requestPermission();
|
|
864
|
+
return status(name, permission === "default" ? "prompt" : permission);
|
|
865
|
+
}
|
|
866
|
+
return queryBrowser(name);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/common/bridge.ts
|
|
872
|
+
function mergeCapability(base, override) {
|
|
873
|
+
return { ...base, ...override };
|
|
874
|
+
}
|
|
875
|
+
function createCommonBridge(runtime, baseOverrides = {}, customOverrides = {}) {
|
|
876
|
+
const overrides = { ...baseOverrides, ...customOverrides };
|
|
877
|
+
for (const name of ["fileSystem", "shell", "process", "deepLink", "updater", "windowState"]) {
|
|
878
|
+
if (customOverrides[name]) Object.assign(overrides, { [name]: { ...baseOverrides[name], ...customOverrides[name] } });
|
|
879
|
+
}
|
|
880
|
+
const { hasPermission, ...webFiles } = createWebFileSystemBridge();
|
|
881
|
+
const fileSystem = mergeCapability(webFiles, overrides.fileSystem);
|
|
882
|
+
const shell = mergeCapability(createWebShellBridge(), overrides.shell);
|
|
883
|
+
const process = mergeCapability(createWebProcessBridge(runtime), overrides.process);
|
|
884
|
+
const deepLink = mergeCapability(createWebDeepLinkBridge(), overrides.deepLink);
|
|
885
|
+
const updater = mergeCapability(createStubUpdaterBridge(), overrides.updater);
|
|
886
|
+
const windowState = mergeCapability(createWebWindowStateBridge(runtime), overrides.windowState);
|
|
887
|
+
return {
|
|
888
|
+
runtime,
|
|
889
|
+
http: overrides.http ?? createHttpClient({ baseUrl: getBridgeConfig().apiBaseUrl }),
|
|
890
|
+
logger: overrides.logger ?? createLogger(runtime),
|
|
891
|
+
fileSystem,
|
|
892
|
+
deepLink,
|
|
893
|
+
clipboard: overrides.clipboard ?? createClipboardBridge(),
|
|
894
|
+
dialog: overrides.dialog ?? createDialogBridge(),
|
|
895
|
+
notification: overrides.notification ?? createNotificationBridge(),
|
|
896
|
+
process,
|
|
897
|
+
shell,
|
|
898
|
+
updater,
|
|
899
|
+
upload: overrides.upload ?? createUploadBridge(),
|
|
900
|
+
websocket: overrides.websocket ?? createWebSocketBridge(),
|
|
901
|
+
store: overrides.store ?? createWebStoreBridge(),
|
|
902
|
+
secureStorage: overrides.secureStorage ?? createStubSecureStorageBridge(),
|
|
903
|
+
permissions: overrides.permissions ?? createWebPermissionBridge(hasPermission),
|
|
904
|
+
windowState,
|
|
905
|
+
readTextFile: fileSystem.readTextFile,
|
|
906
|
+
saveTextFile: fileSystem.saveTextFile,
|
|
907
|
+
openExternal: shell.openExternal,
|
|
908
|
+
checkUpdate: updater.check
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export {
|
|
913
|
+
configureBridge,
|
|
914
|
+
getBridgeConfig,
|
|
915
|
+
lockBridgeConfig,
|
|
916
|
+
BridgeError,
|
|
917
|
+
toBridgeError,
|
|
918
|
+
createClipboardBridge,
|
|
919
|
+
createWebDeepLinkBridge,
|
|
920
|
+
createDialogBridge,
|
|
921
|
+
createWebFileSystemBridge,
|
|
922
|
+
createHttpClient,
|
|
923
|
+
createLogger,
|
|
924
|
+
createNotificationBridge,
|
|
925
|
+
createWebProcessBridge,
|
|
926
|
+
assertSafeExternalUrl,
|
|
927
|
+
createWebShellBridge,
|
|
928
|
+
createStubUpdaterBridge,
|
|
929
|
+
createUploadBridge,
|
|
930
|
+
createWebSocketBridge,
|
|
931
|
+
createWebWindowStateBridge,
|
|
932
|
+
createWebStoreBridge,
|
|
933
|
+
createStubSecureStorageBridge,
|
|
934
|
+
createWebPermissionBridge,
|
|
935
|
+
createCommonBridge
|
|
936
|
+
};
|