import type { Session } from "electron";import { session } from "electron";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Layer from "effect/Layer";import * as PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema";import * as SynchronizedRef from "effect/SynchronizedRef"; const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; // Permissions granted to preview web content. `clipboard-sanitized-write` is the// Electron permission behind `navigator.clipboard.writeText()` — note it is NOT// `clipboard-write`, which is not a valid Electron permission name. Async// clipboard writes are gated by the permission *check* handler (not only the// request handler), so both handlers must allow it; otherwise built-in "Copy"// buttons — e.g. the Next.js / Vercel error overlay — fail with// `Failed to execute 'writeText' on 'Clipboard': Write permission denied`.const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet<string> = new Set([ "clipboard-read", "clipboard-sanitized-write", "notifications", "geolocation",]); export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()( "BrowserSessionPartitionDerivationError", { scope: Schema.String, cause: Schema.instanceOf(PlatformError.PlatformError), },) { override get message(): string { return `Failed to derive a desktop preview browser partition for scope ${this.scope}.`; }} export class BrowserSessionCreationError extends Schema.TaggedErrorClass<BrowserSessionCreationError>()( "BrowserSessionCreationError", { scope: Schema.String, partition: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to create a desktop preview browser session for scope ${this.scope} (partition ${this.partition}).`; }} export class BrowserSessionStorageClearError extends Schema.TaggedErrorClass<BrowserSessionStorageClearError>()( "BrowserSessionStorageClearError", { partition: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to clear desktop preview browser storage for partition ${this.partition}.`; }} export class BrowserSessionCacheClearError extends Schema.TaggedErrorClass<BrowserSessionCacheClearError>()( "BrowserSessionCacheClearError", { partition: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to clear the desktop preview browser cache for partition ${this.partition}.`; }} export const BrowserSessionGetSessionError = Schema.Union([ BrowserSessionPartitionDerivationError, BrowserSessionCreationError,]);export type BrowserSessionGetSessionError = typeof BrowserSessionGetSessionError.Type;export const isBrowserSessionGetSessionError = Schema.is(BrowserSessionGetSessionError); export const BrowserSessionError = Schema.Union([ BrowserSessionPartitionDerivationError, BrowserSessionCreationError, BrowserSessionStorageClearError, BrowserSessionCacheClearError,]);export type BrowserSessionError = typeof BrowserSessionError.Type;export const isBrowserSessionError = Schema.is(BrowserSessionError); export class BrowserSession extends Context.Service< BrowserSession, { readonly getPartition: ( scope?: string, ) => Effect.Effect<string, BrowserSessionPartitionDerivationError>; readonly isPartition: (partition: string) => boolean; readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionGetSessionError>; readonly clearCookies: () => Effect.Effect<void, BrowserSessionStorageClearError>; readonly clearCache: () => Effect.Effect<void, BrowserSessionCacheClearError>; }>()("@t3tools/desktop/preview/BrowserSession") {} export const make = Effect.gen(function* BrowserSessionMake() { const crypto = yield* Crypto.Crypto; const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map()); const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") { const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe( Effect.mapError( (cause) => new BrowserSessionPartitionDerivationError({ scope, cause, }), ), ); return `${PREVIEW_PARTITION_PREFIX}${Encoding.encodeHex(digest).slice(0, 20)}`; }); const getSession = Effect.fn("BrowserSession.getSession")(function* (scope = "shared") { const partition = yield* getPartition(scope); return yield* SynchronizedRef.modifyEffect(sessionsRef, (sessions) => { const existing = sessions.get(partition); if (existing) return Effect.succeed([existing, sessions] as const); return Effect.try({ try: () => { const browserSession = session.fromPartition(partition); const userAgent = browserSession .getUserAgent() .replace(/Electron\/[\d.]+ /, "") .replace(/\s*t3code\/[\d.]+/, ""); browserSession.setUserAgent(userAgent); browserSession.setPermissionRequestHandler((_webContents, permission, callback) => { callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission)); }); browserSession.setPermissionCheckHandler((_webContents, permission) => ALLOWED_PREVIEW_PERMISSIONS.has(permission), ); const next = new Map(sessions); next.set(partition, browserSession); return [browserSession, next] as const; }, catch: (cause) => new BrowserSessionCreationError({ scope, partition, cause, }), }); }); }); return BrowserSession.of({ getPartition, isPartition: (partition) => partition.startsWith(PREVIEW_PARTITION_PREFIX), getSession, clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () { const sessions = yield* SynchronizedRef.get(sessionsRef); yield* Effect.all( [...sessions.entries()].map(([partition, browserSession]) => Effect.tryPromise({ try: () => browserSession.clearStorageData({ storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"], }), catch: (cause) => new BrowserSessionStorageClearError({ partition, cause, }), }), ), { concurrency: "unbounded", discard: true }, ); }), clearCache: Effect.fn("BrowserSession.clearCache")(function* () { const sessions = yield* SynchronizedRef.get(sessionsRef); yield* Effect.all( [...sessions.entries()].map(([partition, browserSession]) => Effect.tryPromise({ try: () => browserSession.clearCache(), catch: (cause) => new BrowserSessionCacheClearError({ partition, cause, }), }), ), { concurrency: "unbounded", discard: true }, ); }), });}).pipe(Effect.withSpan("BrowserSession.make")); export const layer = Layer.effect(BrowserSession, make);
import type { Session } from "electron";
import { session } from "electron";
import * as Context from "effect/Context";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as Encoding from "effect/Encoding";
import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";
import * as SynchronizedRef from "effect/SynchronizedRef";
const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-";
// Permissions granted to preview web content. `clipboard-sanitized-write` is the
// Electron permission behind `navigator.clipboard.writeText()` — note it is NOT
// `clipboard-write`, which is not a valid Electron permission name. Async
// clipboard writes are gated by the permission *check* handler (not only the
// request handler), so both handlers must allow it; otherwise built-in "Copy"
// buttons — e.g. the Next.js / Vercel error overlay — fail with
// `Failed to execute 'writeText' on 'Clipboard': Write permission denied`.
const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet<string> = new Set([
"clipboard-read",
"clipboard-sanitized-write",
"notifications",
"geolocation",
]);
export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass<BrowserSessionPartitionDerivationError>()(
"BrowserSessionPartitionDerivationError",
{
scope: Schema.String,
cause: Schema.instanceOf(PlatformError.PlatformError),
},
) {
override get message(): string {
return `Failed to derive a desktop preview browser partition for scope ${this.scope}.`;
}
}
export class BrowserSessionCreationError extends Schema.TaggedErrorClass<BrowserSessionCreationError>()(
"BrowserSessionCreationError",
{
scope: Schema.String,
partition: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to create a desktop preview browser session for scope ${this.scope} (partition ${this.partition}).`;
}
}
export class BrowserSessionStorageClearError extends Schema.TaggedErrorClass<BrowserSessionStorageClearError>()(
"BrowserSessionStorageClearError",
{
partition: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to clear desktop preview browser storage for partition ${this.partition}.`;
}
}
export class BrowserSessionCacheClearError extends Schema.TaggedErrorClass<BrowserSessionCacheClearError>()(
"BrowserSessionCacheClearError",
{
partition: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to clear the desktop preview browser cache for partition ${this.partition}.`;
}
}
export const BrowserSessionGetSessionError = Schema.Union([
BrowserSessionPartitionDerivationError,
BrowserSessionCreationError,
]);
export type BrowserSessionGetSessionError = typeof BrowserSessionGetSessionError.Type;
export const isBrowserSessionGetSessionError = Schema.is(BrowserSessionGetSessionError);
export const BrowserSessionError = Schema.Union([
BrowserSessionPartitionDerivationError,
BrowserSessionCreationError,
BrowserSessionStorageClearError,
BrowserSessionCacheClearError,
]);
export type BrowserSessionError = typeof BrowserSessionError.Type;
export const isBrowserSessionError = Schema.is(BrowserSessionError);
export class BrowserSession extends Context.Service<
BrowserSession,
{
readonly getPartition: (
scope?: string,
) => Effect.Effect<string, BrowserSessionPartitionDerivationError>;
readonly isPartition: (partition: string) => boolean;
readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionGetSessionError>;
readonly clearCookies: () => Effect.Effect<void, BrowserSessionStorageClearError>;
readonly clearCache: () => Effect.Effect<void, BrowserSessionCacheClearError>;
}
>()("@t3tools/desktop/preview/BrowserSession") {}
export const make = Effect.gen(function* BrowserSessionMake() {
const crypto = yield* Crypto.Crypto;
const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map());
const getPartition = Effect.fn("BrowserSession.getPartition")(function* (scope = "shared") {
const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(scope)).pipe(
Effect.mapError(
(cause) =>
new BrowserSessionPartitionDerivationError({
scope,
cause,
}),
),
);
return `${PREVIEW_PARTITION_PREFIX}${Encoding.encodeHex(digest).slice(0, 20)}`;
});
const getSession = Effect.fn("BrowserSession.getSession")(function* (scope = "shared") {
const partition = yield* getPartition(scope);
return yield* SynchronizedRef.modifyEffect(sessionsRef, (sessions) => {
const existing = sessions.get(partition);
if (existing) return Effect.succeed([existing, sessions] as const);
return Effect.try({
try: () => {
const browserSession = session.fromPartition(partition);
const userAgent = browserSession
.getUserAgent()
.replace(/Electron\/[\d.]+ /, "")
.replace(/\s*t3code\/[\d.]+/, "");
browserSession.setUserAgent(userAgent);
browserSession.setPermissionRequestHandler((_webContents, permission, callback) => {
callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission));
});
browserSession.setPermissionCheckHandler((_webContents, permission) =>
ALLOWED_PREVIEW_PERMISSIONS.has(permission),
);
const next = new Map(sessions);
next.set(partition, browserSession);
return [browserSession, next] as const;
},
catch: (cause) =>
new BrowserSessionCreationError({
scope,
partition,
cause,
}),
});
});
});
return BrowserSession.of({
getPartition,
isPartition: (partition) => partition.startsWith(PREVIEW_PARTITION_PREFIX),
getSession,
clearCookies: Effect.fn("BrowserSession.clearCookies")(function* () {
const sessions = yield* SynchronizedRef.get(sessionsRef);
yield* Effect.all(
[...sessions.entries()].map(([partition, browserSession]) =>
Effect.tryPromise({
try: () =>
browserSession.clearStorageData({
storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"],
}),
catch: (cause) =>
new BrowserSessionStorageClearError({
partition,
cause,
}),
}),
),
{ concurrency: "unbounded", discard: true },
);
}),
clearCache: Effect.fn("BrowserSession.clearCache")(function* () {
const sessions = yield* SynchronizedRef.get(sessionsRef);
yield* Effect.all(
[...sessions.entries()].map(([partition, browserSession]) =>
Effect.tryPromise({
try: () => browserSession.clearCache(),
catch: (cause) =>
new BrowserSessionCacheClearError({
partition,
cause,
}),
}),
),
{ concurrency: "unbounded", discard: true },
);
}),
});
}).pipe(Effect.withSpan("BrowserSession.make"));
export const layer = Layer.effect(BrowserSession, make);