Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions db/migrations-wstore/000012_tabtemplate.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS db_tabtemplate;
5 changes: 5 additions & 0 deletions db/migrations-wstore/000012_tabtemplate.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS db_tabtemplate (
oid varchar(36) PRIMARY KEY,
version int NOT NULL,
data json NOT NULL
);
1 change: 1 addition & 0 deletions electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const config = {
singleArchFiles: "**/dist/bin/wavesrv.*",
entitlements: "build/entitlements.mac.plist",
entitlementsInherit: "build/entitlements.mac.plist",
notarize: !!process.env.APPLE_TEAM_ID,
extendInfo: {
NSContactsUsageDescription: "A CLI application running in Wave wants to use your contacts.",
NSRemindersUsageDescription: "A CLI application running in Wave wants to use your reminders.",
Expand Down
20 changes: 20 additions & 0 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,26 @@ export function initIpcHandlers() {
return `data:image/png;base64,${base64String}`;
});

electron.ipcMain.handle("inject-cookies", async (event, cookies: Electron.CookiesSetDetails[]) => {
try {
const webviewSession = electron.session.fromPartition("persist:webwidgets");
const results = [];

for (const cookie of cookies) {
try {
await webviewSession.cookies.set(cookie);
results.push({ success: true, name: cookie.name });
} catch (error) {
results.push({ success: false, name: cookie.name, error: error.message });
}
}

return { success: true, results };
} catch (error) {
return { success: false, error: error.message };
}
});

electron.ipcMain.on("get-env", (event, varName) => {
event.returnValue = process.env[varName] ?? null;
});
Expand Down
22 changes: 22 additions & 0 deletions emain/emain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as services from "../frontend/app/store/services";
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil-base";
import { fireAndForget, sleep } from "../frontend/util/util";
import { AuthKey, configureAuthKeyRequestInjection } from "./authkey";
import { registerGeolocationIPC } from "./geolocation";
import {
getActivityState,
getAndClearTermCommandsDurable,
Expand Down Expand Up @@ -371,6 +372,25 @@ globalEvents.on("windows-updated", () => {
makeAndSetAppMenu();
});

function configureWebviewPermissions() {
// Set up permission handler for web widgets that need geolocation, media, etc.
// Using a named partition allows for persistent session storage and proper permission handling
const webviewSession = electron.session.fromPartition("persist:webwidgets");

webviewSession.setPermissionRequestHandler((webContents, permission, callback) => {
const allowedPermissions = ["geolocation", "media", "mediaKeySystem"];
if (allowedPermissions.includes(permission)) {
console.log(`[webview-session] Granting permission: ${permission}`);
callback(true);
} else {
console.log(`[webview-session] Denying permission: ${permission}`);
callback(false);
}
});

console.log("[webview-session] Permission handler configured for persist:webwidgets partition");
}

async function appMain() {
// Set disableHardwareAcceleration as early as possible, if required.
const launchSettings = getLaunchSettings();
Expand Down Expand Up @@ -399,6 +419,8 @@ async function appMain() {
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
await electronApp.whenReady();
configureAuthKeyRequestInjection(electron.session.defaultSession);
configureWebviewPermissions();
registerGeolocationIPC();
initIpcHandlers();

await sleep(10); // wait a bit for wavesrv to be ready
Expand Down
270 changes: 270 additions & 0 deletions emain/geolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import * as electron from "electron";
import { spawn } from "child_process";
import * as os from "os";
import * as path from "path";
import * as fs from "fs";

interface GeolocationPosition {
latitude: number;
longitude: number;
accuracy: number;
altitude?: number;
altitudeAccuracy?: number;
heading?: number;
speed?: number;
}

interface GeolocationResult {
success: boolean;
position?: GeolocationPosition;
error?: string;
}

// Cache for location data (avoid hitting location services too often)
let cachedLocation: GeolocationPosition | null = null;
let cacheTimestamp: number = 0;
const CACHE_DURATION_MS = 60000; // 1 minute cache

/**
* Swift helper script for macOS CoreLocation
* This script requests location authorization and returns current location
*/
const SWIFT_LOCATION_SCRIPT = `
import CoreLocation
import Foundation

class LocationHelper: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
let semaphore = DispatchSemaphore(value: 0)
var result: [String: Any] = ["success": false, "error": "Timeout"]

override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}

func requestLocation() {
let status = manager.authorizationStatus

if status == .notDetermined {
manager.requestWhenInUseAuthorization()
// Wait a bit for authorization
Thread.sleep(forTimeInterval: 0.5)
}

let newStatus = manager.authorizationStatus
if newStatus == .denied || newStatus == .restricted {
result = ["success": false, "error": "Location access denied"]
return
}

manager.requestLocation()
_ = semaphore.wait(timeout: .now() + 10)
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
result = [
"success": true,
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"accuracy": location.horizontalAccuracy,
"altitude": location.altitude,
"altitudeAccuracy": location.verticalAccuracy,
"heading": location.course,
"speed": location.speed
]
}
semaphore.signal()
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
result = ["success": false, "error": error.localizedDescription]
semaphore.signal()
}
}

let helper = LocationHelper()
helper.requestLocation()

if let jsonData = try? JSONSerialization.data(withJSONObject: helper.result),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
`;

/**
* Get location using macOS CoreLocation via Swift
*/
async function getMacOSLocation(): Promise<GeolocationResult> {
return new Promise((resolve) => {
const tmpDir = os.tmpdir();
const scriptPath = path.join(tmpDir, "wave-location-helper.swift");

// Write the Swift script to temp
fs.writeFileSync(scriptPath, SWIFT_LOCATION_SCRIPT);

// Execute with swift
const proc = spawn("swift", [scriptPath], {
timeout: 15000,
});

let stdout = "";
let stderr = "";

proc.stdout.on("data", (data) => {
stdout += data.toString();
});

proc.stderr.on("data", (data) => {
stderr += data.toString();
});

proc.on("close", (code) => {
// Clean up temp file
try {
fs.unlinkSync(scriptPath);
} catch (e) {
// Ignore cleanup errors
}

if (code !== 0) {
console.log("[geolocation] Swift helper failed:", stderr);
resolve({ success: false, error: `Swift execution failed: ${stderr}` });
return;
}

try {
const result = JSON.parse(stdout.trim());
if (result.success) {
resolve({
success: true,
position: {
latitude: result.latitude,
longitude: result.longitude,
accuracy: result.accuracy,
altitude: result.altitude,
altitudeAccuracy: result.altitudeAccuracy,
heading: result.heading >= 0 ? result.heading : undefined,
speed: result.speed >= 0 ? result.speed : undefined,
},
});
} else {
resolve({ success: false, error: result.error });
}
} catch (e) {
console.log("[geolocation] Failed to parse Swift output:", stdout);
resolve({ success: false, error: "Failed to parse location data" });
}
});

proc.on("error", (err) => {
console.log("[geolocation] Failed to spawn Swift:", err);
resolve({ success: false, error: err.message });
});
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/**
* Fallback: IP-based geolocation using free API
*/
async function getIPBasedLocation(): Promise<GeolocationResult> {
try {
// Use multiple free IP geolocation services as fallback
const response = await fetch("https://ipapi.co/json/", {
headers: { "User-Agent": "WaveTerm" },
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();

if (data.latitude && data.longitude) {
return {
success: true,
position: {
latitude: data.latitude,
longitude: data.longitude,
accuracy: 10000, // IP-based is ~city level accuracy
},
};
}

return { success: false, error: "No location in response" };
} catch (e) {
console.log("[geolocation] IP-based lookup failed:", e);
return { success: false, error: e.message };
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

IP-based geolocation missing timeout and has privacy implications.

  1. The fetch call has no timeout, which could hang indefinitely if the service is unresponsive.
  2. Consider documenting that this fallback sends the user's IP address to a third-party service (ipapi.co), as this may have privacy implications users should be aware of.
🛡️ Add timeout to fetch call
 async function getIPBasedLocation(): Promise<GeolocationResult> {
     try {
         // Use multiple free IP geolocation services as fallback
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 10000);
+
         const response = await fetch("https://ipapi.co/json/", {
             headers: { "User-Agent": "WaveTerm" },
+            signal: controller.signal,
         });
+        clearTimeout(timeoutId);

         if (!response.ok) {
             throw new Error(`HTTP ${response.status}`);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@emain/geolocation.ts` around lines 175 - 204, getIPBasedLocation currently
calls fetch("https://ipapi.co/json/") with no timeout and no privacy notice;
update getIPBasedLocation to use an AbortController with a reasonable timeout
(e.g., 5s) by creating an AbortController, passing its signal to fetch, setting
a setTimeout to call controller.abort() and clearing the timeout on success, and
handle the abort error path so the function returns a failure with a clear
message; also add a brief inline comment or docstring in getIPBasedLocation
noting that this fallback sends the user's IP to a third-party service
(ipapi.co) for geolocation to surface the privacy implication to readers.


/**
* Get current location with caching
*/
export async function getCurrentPosition(): Promise<GeolocationResult> {
// Check cache first
const now = Date.now();
if (cachedLocation && (now - cacheTimestamp) < CACHE_DURATION_MS) {
console.log("[geolocation] Returning cached location");
return { success: true, position: cachedLocation };
}

let result: GeolocationResult;

// Try platform-specific location first
if (process.platform === "darwin") {
console.log("[geolocation] Attempting macOS CoreLocation...");
result = await getMacOSLocation();

if (result.success) {
console.log("[geolocation] CoreLocation succeeded");
cachedLocation = result.position;
cacheTimestamp = now;
return result;
}
console.log("[geolocation] CoreLocation failed:", result.error);
}

// Fallback to IP-based geolocation
console.log("[geolocation] Falling back to IP-based geolocation...");
result = await getIPBasedLocation();

if (result.success) {
console.log("[geolocation] IP-based geolocation succeeded");
cachedLocation = result.position;
cacheTimestamp = now;
}

return result;
}

/**
* Configure geolocation for webview sessions
* This injects a custom geolocation provider into webviews
*/
export function configureGeolocationForSession(session: electron.Session) {
// We'll inject a polyfill into webviews that calls back to the main process
// for geolocation data instead of relying on Chromium's built-in provider

session.webRequest.onBeforeRequest({ urls: ["*://*/*"] }, (details, callback) => {
callback({});
});

console.log("[geolocation] Session configured for geolocation support");
}

/**
* IPC handler for geolocation requests from renderer/webview
*/
export function registerGeolocationIPC() {
electron.ipcMain.handle("get-geolocation", async () => {
return getCurrentPosition();
});

console.log("[geolocation] IPC handler registered");
}
Loading