Skip to content

Commit c7c7f99

Browse files
authored
Merge branch 'main' into legs/sidebar-rerenders
2 parents 6f1805a + 5467d11 commit c7c7f99

96 files changed

Lines changed: 4328 additions & 1048 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/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.0.16",
3+
"version": "0.0.17",
44
"private": true,
55
"main": "dist-electron/main.js",
66
"scripts": {

apps/desktop/src/backendPort.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,57 @@ describe("resolveDesktopBackendPort", () => {
3636
]);
3737
});
3838

39+
it("treats wildcard-bound ports as unavailable even when loopback probing succeeds", async () => {
40+
const canListenOnHost = vi.fn(async (port: number, host: string) => {
41+
if (port === 3773 && host === "127.0.0.1") return true;
42+
if (port === 3773 && host === "0.0.0.0") return false;
43+
return port === 3774;
44+
});
45+
46+
await expect(
47+
resolveDesktopBackendPort({
48+
host: "127.0.0.1",
49+
requiredHosts: ["0.0.0.0"],
50+
startPort: 3773,
51+
canListenOnHost,
52+
}),
53+
).resolves.toBe(3774);
54+
55+
expect(canListenOnHost.mock.calls).toEqual([
56+
[3773, "127.0.0.1"],
57+
[3773, "0.0.0.0"],
58+
[3774, "127.0.0.1"],
59+
[3774, "0.0.0.0"],
60+
]);
61+
});
62+
63+
it("checks overlapping hosts sequentially to avoid self-interference", async () => {
64+
let inFlightCount = 0;
65+
const canListenOnHost = vi.fn(async (_port: number, _host: string) => {
66+
inFlightCount += 1;
67+
const overlapped = inFlightCount > 1;
68+
await Promise.resolve();
69+
inFlightCount -= 1;
70+
return !overlapped;
71+
});
72+
73+
await expect(
74+
resolveDesktopBackendPort({
75+
host: "127.0.0.1",
76+
requiredHosts: ["0.0.0.0", "::"],
77+
startPort: 3773,
78+
maxPort: 3773,
79+
canListenOnHost,
80+
}),
81+
).resolves.toBe(3773);
82+
83+
expect(canListenOnHost.mock.calls).toEqual([
84+
[3773, "127.0.0.1"],
85+
[3773, "0.0.0.0"],
86+
[3773, "::"],
87+
]);
88+
});
89+
3990
it("fails when the scan range is exhausted", async () => {
4091
const canListenOnHost = vi.fn(async () => false);
4192

@@ -46,7 +97,9 @@ describe("resolveDesktopBackendPort", () => {
4697
maxPort: 65535,
4798
canListenOnHost,
4899
}),
49-
).rejects.toThrow("No desktop backend port is available on 127.0.0.1 between 65534 and 65535");
100+
).rejects.toThrow(
101+
"No desktop backend port is available on hosts 127.0.0.1 between 65534 and 65535",
102+
);
50103

51104
expect(canListenOnHost.mock.calls).toEqual([
52105
[65534, "127.0.0.1"],

apps/desktop/src/backendPort.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ResolveDesktopBackendPortOptions {
88
readonly host: string;
99
readonly startPort?: number;
1010
readonly maxPort?: number;
11+
readonly requiredHosts?: ReadonlyArray<string>;
1112
readonly canListenOnHost?: (port: number, host: string) => Promise<boolean>;
1213
}
1314

@@ -21,10 +22,37 @@ const defaultCanListenOnHost = async (port: number, host: string): Promise<boole
2122
const isValidPort = (port: number): boolean =>
2223
Number.isInteger(port) && port >= 1 && port <= MAX_TCP_PORT;
2324

25+
const normalizeHosts = (
26+
host: string,
27+
requiredHosts: ReadonlyArray<string>,
28+
): ReadonlyArray<string> =>
29+
Array.from(
30+
new Set(
31+
[host, ...requiredHosts]
32+
.map((candidate) => candidate.trim())
33+
.filter((candidate) => candidate.length > 0),
34+
),
35+
);
36+
37+
async function canListenOnAllHosts(
38+
port: number,
39+
hosts: ReadonlyArray<string>,
40+
canListenOnHost: (port: number, host: string) => Promise<boolean>,
41+
): Promise<boolean> {
42+
for (const candidateHost of hosts) {
43+
if (!(await canListenOnHost(port, candidateHost))) {
44+
return false;
45+
}
46+
}
47+
48+
return true;
49+
}
50+
2451
export async function resolveDesktopBackendPort({
2552
host,
2653
startPort = DEFAULT_DESKTOP_BACKEND_PORT,
2754
maxPort = MAX_TCP_PORT,
55+
requiredHosts = [],
2856
canListenOnHost = defaultCanListenOnHost,
2957
}: ResolveDesktopBackendPortOptions): Promise<number> {
3058
if (!isValidPort(startPort)) {
@@ -39,15 +67,17 @@ export async function resolveDesktopBackendPort({
3967
throw new Error(`Desktop backend max port ${maxPort} is below start port ${startPort}`);
4068
}
4169

70+
const hostsToCheck = normalizeHosts(host, requiredHosts);
71+
4272
// Keep desktop startup predictable across app restarts by probing upward from
4373
// the same preferred port instead of picking a fresh ephemeral port.
4474
for (let port = startPort; port <= maxPort; port += 1) {
45-
if (await canListenOnHost(port, host)) {
75+
if (await canListenOnAllHosts(port, hostsToCheck, canListenOnHost)) {
4676
return port;
4777
}
4878
}
4979

5080
throw new Error(
51-
`No desktop backend port is available on ${host} between ${startPort} and ${maxPort}`,
81+
`No desktop backend port is available on hosts ${hostsToCheck.join(", ")} between ${startPort} and ${maxPort}`,
5282
);
5383
}

apps/desktop/src/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000;
117117
const DESKTOP_UPDATE_CHANNEL = "latest";
118118
const DESKTOP_UPDATE_ALLOW_PRERELEASE = false;
119119
const DESKTOP_LOOPBACK_HOST = "127.0.0.1";
120+
const DESKTOP_REQUIRED_PORT_PROBE_HOSTS = ["0.0.0.0", "::"] as const;
120121

121122
type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];
122123
type LinuxDesktopNamedApp = Electron.App & {
@@ -1695,6 +1696,14 @@ function createWindow(): BrowserWindow {
16951696
);
16961697
}
16971698

1699+
if (params.mediaType === "image") {
1700+
menuTemplate.push({
1701+
label: "Copy Image",
1702+
click: () => window.webContents.copyImageAt(params.x, params.y),
1703+
});
1704+
menuTemplate.push({ type: "separator" });
1705+
}
1706+
16981707
menuTemplate.push(
16991708
{ role: "cut", enabled: params.editFlags.canCut },
17001709
{ role: "copy", enabled: params.editFlags.canCopy },
@@ -1773,6 +1782,7 @@ async function bootstrap(): Promise<void> {
17731782
(await resolveDesktopBackendPort({
17741783
host: DESKTOP_LOOPBACK_HOST,
17751784
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
1785+
requiredHosts: DESKTOP_REQUIRED_PORT_PROBE_HOSTS,
17761786
}));
17771787
writeDesktopLogHeader(
17781788
configuredBackendPort === undefined

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "t3",
3-
"version": "0.0.16",
3+
"version": "0.0.17",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/server/scripts/cli.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog
1414
import rootPackageJson from "../../../package.json" with { type: "json" };
1515
import serverPackageJson from "../package.json" with { type: "json" };
1616

17+
interface PackageJson {
18+
name: string;
19+
repository: {
20+
type: string;
21+
url: string;
22+
directory: string;
23+
};
24+
bin: Record<string, string>;
25+
type: string;
26+
version: string;
27+
engines: Record<string, string>;
28+
files: string[];
29+
dependencies: Record<string, string>;
30+
overrides: Record<string, string>;
31+
}
32+
1733
class CliError extends Data.TaggedError("CliError")<{
1834
readonly message: string;
1935
readonly cause?: unknown;
@@ -192,22 +208,28 @@ const publishCmd = Command.make(
192208
// Resolve catalog dependencies before any file mutations. If this throws,
193209
// acquire fails and no release hook runs, so filesystem must still be untouched.
194210
const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
195-
const pkg = {
211+
const pkg: PackageJson = {
196212
name: serverPackageJson.name,
197213
repository: serverPackageJson.repository,
198214
bin: serverPackageJson.bin,
199215
type: serverPackageJson.type,
200216
version,
201217
engines: serverPackageJson.engines,
202218
files: serverPackageJson.files,
203-
dependencies: serverPackageJson.dependencies as Record<string, unknown>,
219+
dependencies: serverPackageJson.dependencies,
220+
overrides: rootPackageJson.overrides,
204221
};
205222

206223
pkg.dependencies = resolveCatalogDependencies(
207224
pkg.dependencies,
208225
rootPackageJson.workspaces.catalog,
209226
"apps/server dependencies",
210227
);
228+
pkg.overrides = resolveCatalogDependencies(
229+
pkg.overrides,
230+
rootPackageJson.workspaces.catalog,
231+
"root overrides",
232+
);
211233

212234
const original = yield* fs.readFileString(packageJsonPath);
213235
yield* fs.writeFileString(backupPath, original);

apps/server/src/auth/Layers/ServerAuth.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
AuthError,
2121
type ServerAuthShape,
2222
} from "../Services/ServerAuth.ts";
23-
import { SessionCredentialService } from "../Services/SessionCredentialService.ts";
23+
import {
24+
SessionCredentialError,
25+
SessionCredentialService,
26+
} from "../Services/SessionCredentialService.ts";
2427
import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";
2528

2629
type BootstrapExchangeResult = {
@@ -65,6 +68,13 @@ export const makeServerAuth = Effect.gen(function* () {
6568

6669
const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>
6770
sessions.verify(token).pipe(
71+
Effect.tapError((cause: SessionCredentialError) =>
72+
Effect.logWarning("Rejected authenticated session credential.").pipe(
73+
Effect.annotateLogs({
74+
reason: cause.message,
75+
}),
76+
),
77+
),
6878
Effect.map((session) => ({
6979
sessionId: session.sessionId,
7080
subject: session.subject,

apps/server/src/auth/Layers/ServerAuthPolicy.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {
3333

3434
expect(descriptor.policy).toBe("desktop-managed-local");
3535
expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap"]);
36+
expect(descriptor.sessionCookieName).toBe("t3_session_3773");
3637
}).pipe(
3738
Effect.provide(
3839
makeServerAuthPolicyLayer({
3940
mode: "desktop",
41+
port: 3773,
4042
}),
4143
),
4244
),
@@ -66,6 +68,7 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {
6668

6769
expect(descriptor.policy).toBe("loopback-browser");
6870
expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]);
71+
expect(descriptor.sessionCookieName).toBe("t3_session");
6972
}).pipe(
7073
Effect.provide(
7174
makeServerAuthPolicyLayer({

apps/server/src/auth/Layers/ServerAuthPolicy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Effect, Layer } from "effect";
33

44
import { ServerConfig } from "../../config.ts";
55
import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
6-
import { SESSION_COOKIE_NAME } from "../utils.ts";
6+
import { resolveSessionCookieName } from "../utils.ts";
77
import { isLoopbackHost, isWildcardHost } from "../../startupAccess.ts";
88

99
export const makeServerAuthPolicy = Effect.gen(function* () {
@@ -30,7 +30,10 @@ export const makeServerAuthPolicy = Effect.gen(function* () {
3030
policy,
3131
bootstrapMethods,
3232
sessionMethods: ["browser-session-cookie", "bearer-session-token"],
33-
sessionCookieName: SESSION_COOKIE_NAME,
33+
sessionCookieName: resolveSessionCookieName({
34+
mode: config.mode,
35+
port: config.port,
36+
}),
3437
};
3538

3639
return {

apps/server/src/auth/Layers/ServerSecretStore.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Crypto from "node:crypto";
22

3-
import { Effect, FileSystem, Layer, Path } from "effect";
3+
import { Effect, FileSystem, Layer, Path, Predicate } from "effect";
44
import * as PlatformError from "effect/PlatformError";
55

66
import { ServerConfig } from "../../config.ts";
@@ -28,17 +28,14 @@ export const makeServerSecretStore = Effect.gen(function* () {
2828

2929
const resolveSecretPath = (name: string) => path.join(serverConfig.secretsDir, `${name}.bin`);
3030

31-
const isMissingSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
32-
cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound";
33-
34-
const isAlreadyExistsSecretFileError = (cause: unknown): cause is PlatformError.PlatformError =>
35-
cause instanceof PlatformError.PlatformError && cause.reason._tag === "AlreadyExists";
31+
const isPlatformError = (u: unknown): u is PlatformError.PlatformError =>
32+
Predicate.isTagged(u, "PlatformError");
3633

3734
const get: ServerSecretStoreShape["get"] = (name) =>
3835
fileSystem.readFile(resolveSecretPath(name)).pipe(
3936
Effect.map((bytes) => Uint8Array.from(bytes)),
4037
Effect.catch((cause) =>
41-
isMissingSecretFileError(cause)
38+
cause.reason._tag === "NotFound"
4239
? Effect.succeed(null)
4340
: Effect.fail(
4441
new SecretStoreError({
@@ -108,7 +105,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
108105
return create(name, generated).pipe(
109106
Effect.as(Uint8Array.from(generated)),
110107
Effect.catchTag("SecretStoreError", (error) =>
111-
isAlreadyExistsSecretFileError(error.cause)
108+
isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists"
112109
? get(name).pipe(
113110
Effect.flatMap((created) =>
114111
created !== null
@@ -129,7 +126,7 @@ export const makeServerSecretStore = Effect.gen(function* () {
129126
const remove: ServerSecretStoreShape["remove"] = (name) =>
130127
fileSystem.remove(resolveSecretPath(name)).pipe(
131128
Effect.catch((cause) =>
132-
isMissingSecretFileError(cause)
129+
cause.reason._tag === "NotFound"
133130
? Effect.void
134131
: Effect.fail(
135132
new SecretStoreError({

0 commit comments

Comments
 (0)