Skip to content

Commit fb36d5e

Browse files
juliusmarmingecodex
andcommitted
Rewrite client connection architecture
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 696c4a4 commit fb36d5e

246 files changed

Lines changed: 20222 additions & 20538 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/app/DesktopCloudAuthTokenStore.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem";
55
import * as Layer from "effect/Layer";
66
import * as Option from "effect/Option";
77

8-
import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
8+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
99
import * as DesktopConfig from "./DesktopConfig.ts";
1010
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
1111
import * as DesktopCloudAuthTokenStore from "./DesktopCloudAuthTokenStore.ts";

apps/desktop/src/app/DesktopCloudAuthTokenStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as Path from "effect/Path";
1111
import * as PlatformError from "effect/PlatformError";
1212
import * as Schema from "effect/Schema";
1313

14-
import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
14+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
1515
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
1616

1717
interface CloudAuthTokenDocument {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as NodeServices from "@effect/platform-node/NodeServices";
2+
import { assert, describe, it } from "@effect/vitest";
3+
import * as Effect from "effect/Effect";
4+
import * as FileSystem from "effect/FileSystem";
5+
import * as Layer from "effect/Layer";
6+
import * as Option from "effect/Option";
7+
8+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
9+
import * as DesktopConfig from "./DesktopConfig.ts";
10+
import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts";
11+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
12+
13+
const textDecoder = new TextDecoder();
14+
const textEncoder = new TextEncoder();
15+
16+
function makeSafeStorageLayer(available: boolean) {
17+
return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {
18+
isEncryptionAvailable: Effect.succeed(available),
19+
encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),
20+
decryptString: (value) => {
21+
const decoded = textDecoder.decode(value);
22+
if (!decoded.startsWith("encrypted:")) {
23+
return Effect.fail(
24+
new ElectronSafeStorage.ElectronSafeStorageDecryptError({
25+
cause: new Error("invalid encrypted catalog"),
26+
}),
27+
);
28+
}
29+
return Effect.succeed(decoded.slice("encrypted:".length));
30+
},
31+
} satisfies ElectronSafeStorage.ElectronSafeStorageShape);
32+
}
33+
34+
function makeLayer(baseDir: string, encryptionAvailable = true) {
35+
const environmentLayer = DesktopEnvironment.layer({
36+
dirname: "/repo/apps/desktop/src",
37+
homeDirectory: baseDir,
38+
platform: "darwin",
39+
processArch: "arm64",
40+
appVersion: "1.2.3",
41+
appPath: "/repo",
42+
isPackaged: true,
43+
resourcesPath: "/missing/resources",
44+
runningUnderArm64Translation: false,
45+
}).pipe(
46+
Layer.provide(
47+
Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),
48+
),
49+
);
50+
51+
return DesktopConnectionCatalogStore.layer.pipe(
52+
Layer.provideMerge(environmentLayer),
53+
Layer.provideMerge(makeSafeStorageLayer(encryptionAvailable)),
54+
Layer.provideMerge(NodeServices.layer),
55+
);
56+
}
57+
58+
const withStore = <A, E, R>(
59+
effect: Effect.Effect<A, E, R | DesktopConnectionCatalogStore.DesktopConnectionCatalogStore>,
60+
encryptionAvailable = true,
61+
) =>
62+
Effect.gen(function* () {
63+
const fileSystem = yield* FileSystem.FileSystem;
64+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
65+
prefix: "t3-desktop-connection-catalog-test-",
66+
});
67+
return yield* effect.pipe(Effect.provide(makeLayer(baseDir, encryptionAvailable)));
68+
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
69+
70+
describe("DesktopConnectionCatalogStore", () => {
71+
it.effect("persists, reads, and clears an encrypted connection catalog", () =>
72+
withStore(
73+
Effect.gen(function* () {
74+
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
75+
const catalog = '{"schemaVersion":1,"targets":[]}';
76+
77+
assert.isTrue(yield* store.set(catalog));
78+
assert.deepStrictEqual(yield* store.get, Option.some(catalog));
79+
80+
yield* store.clear;
81+
assert.deepStrictEqual(yield* store.get, Option.none());
82+
}),
83+
),
84+
);
85+
86+
it.effect("does not persist when secure storage is unavailable", () =>
87+
withStore(
88+
Effect.gen(function* () {
89+
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
90+
assert.isFalse(yield* store.set("{}"));
91+
assert.deepStrictEqual(yield* store.get, Option.none());
92+
}),
93+
false,
94+
),
95+
);
96+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { fromLenientJson } from "@t3tools/shared/schemaJson";
2+
import * as Context from "effect/Context";
3+
import * as Crypto from "effect/Crypto";
4+
import * as Data from "effect/Data";
5+
import * as Effect from "effect/Effect";
6+
import * as Encoding from "effect/Encoding";
7+
import * as FileSystem from "effect/FileSystem";
8+
import * as Layer from "effect/Layer";
9+
import * as Option from "effect/Option";
10+
import * as Path from "effect/Path";
11+
import * as PlatformError from "effect/PlatformError";
12+
import * as Schema from "effect/Schema";
13+
14+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
15+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
16+
17+
const ConnectionCatalogDocument = Schema.Struct({
18+
version: Schema.Literal(1),
19+
encryptedCatalog: Schema.String,
20+
});
21+
type ConnectionCatalogDocument = typeof ConnectionCatalogDocument.Type;
22+
23+
const ConnectionCatalogDocumentJson = fromLenientJson(ConnectionCatalogDocument);
24+
const decodeConnectionCatalogDocumentJson = Schema.decodeEffect(ConnectionCatalogDocumentJson);
25+
const encodeConnectionCatalogDocumentJson = Schema.encodeEffect(ConnectionCatalogDocumentJson);
26+
27+
export class DesktopConnectionCatalogStoreWriteError extends Data.TaggedError(
28+
"DesktopConnectionCatalogStoreWriteError",
29+
)<{
30+
readonly cause: PlatformError.PlatformError | Schema.SchemaError;
31+
}> {
32+
override get message() {
33+
return `Failed to write desktop connection catalog: ${this.cause.message}`;
34+
}
35+
}
36+
37+
export class DesktopConnectionCatalogStoreDecodeError extends Data.TaggedError(
38+
"DesktopConnectionCatalogStoreDecodeError",
39+
)<{
40+
readonly cause: Encoding.EncodingError;
41+
}> {
42+
override get message() {
43+
return "Failed to decode the desktop connection catalog.";
44+
}
45+
}
46+
47+
export interface DesktopConnectionCatalogStoreShape {
48+
readonly get: Effect.Effect<
49+
Option.Option<string>,
50+
| DesktopConnectionCatalogStoreDecodeError
51+
| ElectronSafeStorage.ElectronSafeStorageAvailabilityError
52+
| ElectronSafeStorage.ElectronSafeStorageDecryptError
53+
>;
54+
readonly set: (
55+
catalog: string,
56+
) => Effect.Effect<
57+
boolean,
58+
| DesktopConnectionCatalogStoreWriteError
59+
| ElectronSafeStorage.ElectronSafeStorageAvailabilityError
60+
| ElectronSafeStorage.ElectronSafeStorageEncryptError
61+
>;
62+
readonly clear: Effect.Effect<void>;
63+
}
64+
65+
export class DesktopConnectionCatalogStore extends Context.Service<
66+
DesktopConnectionCatalogStore,
67+
DesktopConnectionCatalogStoreShape
68+
>()("@t3tools/desktop/app/DesktopConnectionCatalogStore") {}
69+
70+
function decodeSecretBytes(
71+
encoded: string,
72+
): Effect.Effect<Uint8Array, DesktopConnectionCatalogStoreDecodeError> {
73+
return Effect.fromResult(Encoding.decodeBase64(encoded)).pipe(
74+
Effect.mapError((cause) => new DesktopConnectionCatalogStoreDecodeError({ cause })),
75+
);
76+
}
77+
78+
const readDocument = (
79+
fileSystem: FileSystem.FileSystem,
80+
catalogPath: string,
81+
): Effect.Effect<Option.Option<ConnectionCatalogDocument>> =>
82+
fileSystem.readFileString(catalogPath).pipe(
83+
Effect.option,
84+
Effect.flatMap(
85+
Option.match({
86+
onNone: () => Effect.succeed(Option.none<ConnectionCatalogDocument>()),
87+
onSome: (raw) => decodeConnectionCatalogDocumentJson(raw).pipe(Effect.option),
88+
}),
89+
),
90+
);
91+
92+
const writeDocument = Effect.fn("desktop.connectionCatalogStore.writeDocument")(function* (input: {
93+
readonly fileSystem: FileSystem.FileSystem;
94+
readonly path: Path.Path;
95+
readonly catalogPath: string;
96+
readonly document: ConnectionCatalogDocument;
97+
readonly suffix: string;
98+
}): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> {
99+
const directory = input.path.dirname(input.catalogPath);
100+
const tempPath = `${input.catalogPath}.${process.pid}.${input.suffix}.tmp`;
101+
const encoded = yield* encodeConnectionCatalogDocumentJson(input.document);
102+
yield* input.fileSystem.makeDirectory(directory, { recursive: true });
103+
yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`);
104+
yield* input.fileSystem.rename(tempPath, input.catalogPath);
105+
});
106+
107+
export const layer = Layer.effect(
108+
DesktopConnectionCatalogStore,
109+
Effect.gen(function* () {
110+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
111+
const fileSystem = yield* FileSystem.FileSystem;
112+
const path = yield* Path.Path;
113+
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
114+
const crypto = yield* Crypto.Crypto;
115+
const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
116+
117+
return DesktopConnectionCatalogStore.of({
118+
get: Effect.gen(function* () {
119+
const document = yield* readDocument(fileSystem, catalogPath);
120+
if (Option.isNone(document) || !(yield* safeStorage.isEncryptionAvailable)) {
121+
return Option.none<string>();
122+
}
123+
const bytes = yield* decodeSecretBytes(document.value.encryptedCatalog);
124+
return Option.some(yield* safeStorage.decryptString(bytes));
125+
}).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")),
126+
set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) {
127+
if (!(yield* safeStorage.isEncryptionAvailable)) {
128+
return false;
129+
}
130+
const encryptedCatalog = Encoding.encodeBase64(yield* safeStorage.encryptString(catalog));
131+
const suffix = (yield* crypto.randomUUIDv4.pipe(
132+
Effect.mapError((cause) => new DesktopConnectionCatalogStoreWriteError({ cause })),
133+
)).replace(/-/g, "");
134+
yield* writeDocument({
135+
fileSystem,
136+
path,
137+
catalogPath,
138+
document: { version: 1, encryptedCatalog },
139+
suffix,
140+
}).pipe(Effect.mapError((cause) => new DesktopConnectionCatalogStoreWriteError({ cause })));
141+
return true;
142+
}),
143+
clear: fileSystem.remove(catalogPath, { force: true }).pipe(
144+
Effect.catch(() => Effect.void),
145+
Effect.withSpan("desktop.connectionCatalogStore.clear"),
146+
),
147+
});
148+
}),
149+
);

apps/desktop/src/backend/tailscaleEndpointProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd
121121
input.readMagicDnsName ??
122122
readTailscaleStatus.pipe(
123123
Effect.map((status) => status.magicDnsName),
124-
Effect.catch(() => Effect.succeed<string | null>(null)),
124+
Effect.orElseSucceed(() => null),
125125
);
126126
const dnsName =
127127
input.statusJson === undefined

apps/desktop/src/electron/ElectronSafeStorage.ts

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,16 @@
1-
import * as Context from "effect/Context";
2-
import * as Data from "effect/Data";
31
import * as Effect from "effect/Effect";
42
import * as Layer from "effect/Layer";
53

64
import * as Electron from "electron";
75

8-
export class ElectronSafeStorageAvailabilityError extends Data.TaggedError(
9-
"ElectronSafeStorageAvailabilityError",
10-
)<{
11-
readonly cause: unknown;
12-
}> {
13-
override get message() {
14-
return "Electron safe storage failed to check encryption availability.";
15-
}
16-
}
17-
18-
export class ElectronSafeStorageEncryptError extends Data.TaggedError(
19-
"ElectronSafeStorageEncryptError",
20-
)<{
21-
readonly cause: unknown;
22-
}> {
23-
override get message() {
24-
return "Electron safe storage failed to encrypt a string.";
25-
}
26-
}
27-
28-
export class ElectronSafeStorageDecryptError extends Data.TaggedError(
29-
"ElectronSafeStorageDecryptError",
30-
)<{
31-
readonly cause: unknown;
32-
}> {
33-
override get message() {
34-
return "Electron safe storage failed to decrypt a string.";
35-
}
36-
}
37-
38-
export interface ElectronSafeStorageShape {
39-
readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>;
40-
readonly encryptString: (
41-
value: string,
42-
) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>;
43-
readonly decryptString: (
44-
value: Uint8Array,
45-
) => Effect.Effect<string, ElectronSafeStorageDecryptError>;
46-
}
47-
48-
export class ElectronSafeStorage extends Context.Service<
6+
import {
497
ElectronSafeStorage,
50-
ElectronSafeStorageShape
51-
>()("@t3tools/desktop/electron/ElectronSafeStorage") {}
8+
ElectronSafeStorageAvailabilityError,
9+
ElectronSafeStorageDecryptError,
10+
ElectronSafeStorageEncryptError,
11+
} from "./ElectronSafeStorageService.ts";
12+
13+
export * from "./ElectronSafeStorageService.ts";
5214

5315
const make = ElectronSafeStorage.of({
5416
isEncryptionAvailable: Effect.try({
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as Context from "effect/Context";
2+
import * as Data from "effect/Data";
3+
import type * as Effect from "effect/Effect";
4+
5+
export class ElectronSafeStorageAvailabilityError extends Data.TaggedError(
6+
"ElectronSafeStorageAvailabilityError",
7+
)<{
8+
readonly cause: unknown;
9+
}> {
10+
override get message() {
11+
return "Electron safe storage failed to check encryption availability.";
12+
}
13+
}
14+
15+
export class ElectronSafeStorageEncryptError extends Data.TaggedError(
16+
"ElectronSafeStorageEncryptError",
17+
)<{
18+
readonly cause: unknown;
19+
}> {
20+
override get message() {
21+
return "Electron safe storage failed to encrypt a string.";
22+
}
23+
}
24+
25+
export class ElectronSafeStorageDecryptError extends Data.TaggedError(
26+
"ElectronSafeStorageDecryptError",
27+
)<{
28+
readonly cause: unknown;
29+
}> {
30+
override get message() {
31+
return "Electron safe storage failed to decrypt a string.";
32+
}
33+
}
34+
35+
export interface ElectronSafeStorageShape {
36+
readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>;
37+
readonly encryptString: (
38+
value: string,
39+
) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>;
40+
readonly decryptString: (
41+
value: Uint8Array,
42+
) => Effect.Effect<string, ElectronSafeStorageDecryptError>;
43+
}
44+
45+
export class ElectronSafeStorage extends Context.Service<
46+
ElectronSafeStorage,
47+
ElectronSafeStorageShape
48+
>()("@t3tools/desktop/electron/ElectronSafeStorageService/ElectronSafeStorage") {}

0 commit comments

Comments
 (0)