Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"electron": "44.4.2",
"electron-store": "^8.2.0",
"electron-updater": "^6.8.9",
"electron-webauthn": "1.3.1",
"ffi-rs": "1.3.2",
"playwright-core": "1.60.0",
"react-grab": "^0.1.32"
"react-grab": "^0.1.32",
"tldts": "7.4.2"
},
"devDependencies": {
"@effect/vitest": "catalog:",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as PreviewPasskeys from "../preview/Passkeys.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
Expand Down Expand Up @@ -276,6 +277,7 @@ const startup = Effect.gen(function* () {
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
const updates = yield* DesktopUpdates.DesktopUpdates;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const previewPasskeys = yield* PreviewPasskeys.PreviewPasskeys;

yield* shellEnvironment.installIntoProcess;
const hasCommandLinePasswordStore =
Expand Down Expand Up @@ -329,6 +331,7 @@ const startup = Effect.gen(function* () {
});
}
yield* appIdentity.configure;
yield* previewPasskeys.configure;
yield* applicationMenu.configure;
yield* updates.configure;
yield* DesktopRemoteUpdates.listen;
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import * as BrowserImport from "./preview/BrowserImport/BrowserImport.ts";
import * as LinuxBrowserSecret from "./preview/BrowserImport/LinuxBrowserSecret.ts";
import * as BrowserSession from "./preview/BrowserSession.ts";
import * as PreviewManager from "./preview/Manager.ts";
import * as PreviewPasskeys from "./preview/Passkeys.ts";
import * as DesktopWindow from "./window/DesktopWindow.ts";
import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts";
import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts";
Expand Down Expand Up @@ -165,6 +166,7 @@ const layerDesktopPreview = PreviewManager.layer.pipe(
// service alongside the manager; both sit on the same BrowserSession.
Layer.provideMerge(BrowserImport.layer.pipe(Layer.provide(LinuxBrowserSecret.layer))),
Layer.provideMerge(BrowserSession.layer),
Layer.provideMerge(PreviewPasskeys.layer),
Layer.provideMerge(layerDesktopFoundation),
);

Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/preview-pick-preload.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
import { ipcRenderer } from "electron";

import { PASSKEY_BRIDGE_ARGUMENT } from "./preview/GuestProtocol.ts";
import { installPasskeyBridge } from "./preview/PasskeyBridge.ts";
import "./preview/PickPreload.ts";

// Electron hands webPreferences.additionalArguments to sandboxed preloads in process.argv.
if (process.argv.includes(PASSKEY_BRIDGE_ARGUMENT)) {
installPasskeyBridge((channel, publicKey) => ipcRenderer.invoke(channel, publicKey));
}
4 changes: 4 additions & 0 deletions apps/desktop/src/preview/GuestProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export const RECORDING_POINTER_CHANNEL = "preview:recording-pointer";
export const RECORDING_KEY_CHANNEL = "preview:recording-key";
export const RECORDING_INPUT_CHANNEL = "preview:recording-input";
export const RECORDING_CONTROLLER_CHANNEL = "preview:recording-controller";
export const PASSKEY_CREATE_CHANNEL = "preview:passkey-create";
export const PASSKEY_GET_CHANNEL = "preview:passkey-get";
/** Renderer argument that turns on the guest passkey bridge; see Passkeys.ts. */
export const PASSKEY_BRIDGE_ARGUMENT = "--t3code-preview-passkey-bridge";
8 changes: 8 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts"
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as BrowserSession from "./BrowserSession.ts";
import * as PreviewManager from "./Manager.ts";
import * as PreviewPasskeys from "./Passkeys.ts";

describe("fitPictureInPictureContentSize", () => {
it("preserves the PiP content area across aspect-ratio changes", () => {
Expand Down Expand Up @@ -276,6 +277,13 @@ const layer = PreviewManager.layer.pipe(
}),
),
Layer.provideMerge(layerBrowserSession),
Layer.provideMerge(
Layer.mock(PreviewPasskeys.PreviewPasskeys)({
bridgeEnabled: false,
installSessionHandlers: () => {},
attachGuest: () => () => {},
}),
),
Layer.provideMerge(layerEnvironment),
Layer.provideMerge(layerFileSystem),
Layer.provideMerge(Path.layer),
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
import { PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL } from "../ipc/channels.ts";
import * as BrowserSession from "./BrowserSession.ts";
import * as PreviewPasskeys from "./Passkeys.ts";
import {
ANNOTATION_CAPTURED_CHANNEL,
ANNOTATION_THEME_CHANNEL,
Expand Down Expand Up @@ -641,6 +642,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
) {
const fileSystem = yield* FileSystem.FileSystem;
const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
const passkeys = yield* PreviewPasskeys.PreviewPasskeys;
const hostPlatform = yield* HostProcessPlatform;
const path = yield* Path.Path;
const parentScope = yield* Scope.Scope;
Expand Down Expand Up @@ -1803,6 +1805,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
) {
const scope = yield* Scope.fork(parentScope, "sequential");
const attachmentId = Symbol();
let detachPasskeys = () => {};
let documentId = 0;
let nextRequestId = 0;
let activeCapture: {
Expand Down Expand Up @@ -2115,6 +2118,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
wc.ipc.off(HUMAN_INPUT_CHANNEL, humanInput);
wc.ipc.off(RECORDING_INPUT_CHANNEL, recordingInput);
wc.ipc.off(MOUSE_NAVIGATE_CHANNEL, mouseNavigate);
detachPasskeys();
}).pipe(Effect.ignore),
);
const install = Effect.fn("PreviewManager.installWebContentsListeners")(function* () {
Expand All @@ -2135,6 +2139,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
wc.ipc.on(HUMAN_INPUT_CHANNEL, humanInput);
wc.ipc.on(RECORDING_INPUT_CHANNEL, recordingInput);
wc.ipc.on(MOUSE_NAVIGATE_CHANNEL, mouseNavigate);
detachPasskeys = passkeys.attachGuest(wc);
wc.setWindowOpenHandler((details) => {
if (previewWindowOpenAction(details) === "popup") {
return { action: "allow", overrideBrowserWindowOptions: POPUP_WINDOW_OPTIONS };
Expand Down Expand Up @@ -5208,6 +5213,7 @@ export class PreviewManager extends Context.Service<
export const make = Effect.gen(function* PreviewManagerMake() {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const browserSession = yield* BrowserSession.BrowserSession;
const passkeys = yield* PreviewPasskeys.PreviewPasskeys;
const operations = yield* makeNativeOperations(
environment.browserArtifactsDir,
environment.path.join(environment.dirname, "preview-pip-preload.cjs"),
Expand All @@ -5225,6 +5231,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
),
);
operations.installDownloadHandler(session);
passkeys.installSessionHandlers(session);
return session;
},
),
Expand Down
59 changes: 59 additions & 0 deletions apps/desktop/src/preview/PasskeyAttestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Reads the raw `authData` bytes out of a CBOR attestation object
* (`{ fmt, attStmt, authData }`). Returns undefined for anything malformed.
* Only the definite-length encodings authenticators emit are understood.
*/
export const authenticatorDataFromAttestation = (bytes: Uint8Array) => {
let offset = 0;
const readHead = () => {
const initial = bytes[offset++];
if (initial === undefined) return undefined;
const info = initial & 31;
let length = info;
if (info >= 24) {
if (info > 27) return undefined;
const size = 1 << (info - 24);
if (offset + size > bytes.length) return undefined;
length = 0;
for (let index = 0; index < size; index++) length = length * 256 + (bytes[offset++] ?? 0);
}
return { major: initial >> 5, length };
};
const skipValue = (): boolean => {
const head = readHead();
if (!head) return false;
switch (head.major) {
case 2:
case 3:
offset += head.length;
return offset <= bytes.length;
case 4:
for (let index = 0; index < head.length; index++) if (!skipValue()) return false;
return true;
case 5:
for (let index = 0; index < head.length * 2; index++) if (!skipValue()) return false;
return true;
case 6:
return skipValue();
default:
return true;
}
};

const map = readHead();
if (map?.major !== 5) return undefined;
for (let entry = 0; entry < map.length; entry++) {
const key = readHead();
if (key?.major !== 3 || offset + key.length > bytes.length) return undefined;
const name = new TextDecoder().decode(bytes.subarray(offset, offset + key.length));
offset += key.length;
if (name !== "authData") {
if (!skipValue()) return undefined;
continue;
}
const value = readHead();
if (value?.major !== 2 || offset + value.length > bytes.length) return undefined;
return bytes.slice(offset, offset + value.length);
}
return undefined;
};
Loading
Loading