Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import type { Session, WebContents } from "electron";
|
|
import { isLoopbackHost } from "./setup-config.js";
|
|
|
|
type AppPermissionTarget = {
|
|
webContents: WebContents;
|
|
url: string;
|
|
};
|
|
|
|
function httpOrigin(value: string | undefined): string | null {
|
|
if (!value) return null;
|
|
try {
|
|
const url = new URL(value);
|
|
// LAN HTTP remains a supported server target, but cannot safely receive host permissions.
|
|
return url.protocol === "https:" || (url.protocol === "http:" && isLoopbackHost(url.hostname))
|
|
? url.origin
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Install before loading any content, including setup, storage probes and OAuth popups. */
|
|
export function installSessionPermissions(
|
|
targetSession: Session,
|
|
getApp: () => AppPermissionTarget | null,
|
|
) {
|
|
function currentApp() {
|
|
const app = getApp();
|
|
if (!app || app.webContents.isDestroyed() || app.webContents.session !== targetSession) {
|
|
return null;
|
|
}
|
|
const origin = httpOrigin(app.url);
|
|
if (origin === null || httpOrigin(app.webContents.getURL()) !== origin) return null;
|
|
return { ...app, origin };
|
|
}
|
|
|
|
targetSession.setPermissionRequestHandler((contents, permission, callback, details) => {
|
|
const app = currentApp();
|
|
if (
|
|
!app ||
|
|
contents !== app.webContents ||
|
|
!details.isMainFrame ||
|
|
httpOrigin(details.requestingUrl) !== app.origin
|
|
) {
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
if (permission === "media") {
|
|
// Dictation needs only the microphone. OS consent still applies after this grant.
|
|
callback(
|
|
"securityOrigin" in details &&
|
|
httpOrigin(details.securityOrigin) === app.origin &&
|
|
"mediaTypes" in details &&
|
|
(details.mediaTypes?.length ?? 0) > 0 &&
|
|
details.mediaTypes?.every((type) => type === "audio") === true,
|
|
);
|
|
return;
|
|
}
|
|
callback(permission === "notifications" || permission === "clipboard-sanitized-write");
|
|
});
|
|
|
|
targetSession.setPermissionCheckHandler((contents, permission, requestingOrigin, details) => {
|
|
const app = currentApp();
|
|
if (!app || httpOrigin(requestingOrigin) !== app.origin) return false;
|
|
if (
|
|
(details.embeddingOrigin !== undefined &&
|
|
httpOrigin(details.embeddingOrigin) !== app.origin) ||
|
|
(details.securityOrigin !== undefined && httpOrigin(details.securityOrigin) !== app.origin)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Electron can check notifications without a WebContents. Require both origins;
|
|
// never extend this origin-only exception to capture, clipboard or other permissions.
|
|
if (contents === null) {
|
|
return permission === "notifications" && httpOrigin(details.embeddingOrigin) === app.origin;
|
|
}
|
|
if (
|
|
contents !== app.webContents ||
|
|
!details.isMainFrame ||
|
|
httpOrigin(details.requestingUrl) !== app.origin
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (permission === "media") return details.mediaType === "audio";
|
|
return permission === "notifications" || permission === "clipboard-sanitized-write";
|
|
});
|
|
}
|