Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
7 changes: 6 additions & 1 deletion apps/mobile/src/components/ProjectFavicon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SymbolView } from "expo-symbols";
import { useState } from "react";
import { Image, View } from "react-native";
import { resolveRemoteHttpUrl } from "../lib/remoteUrl";
import { useThemeColor } from "../lib/useThemeColor";

/* ─── Favicon cache (matches web pattern) ────────────────────────────── */
Expand All @@ -19,7 +20,11 @@ export function ProjectFavicon(props: {

const faviconUrl =
props.httpBaseUrl && props.workspaceRoot
? `${props.httpBaseUrl}/api/project-favicon?cwd=${encodeURIComponent(props.workspaceRoot)}`
? resolveRemoteHttpUrl({
httpBaseUrl: props.httpBaseUrl,
pathname: "/api/project-favicon",
searchParams: { cwd: props.workspaceRoot },
})
: null;

const [status, setStatus] = useState<"loading" | "loaded" | "error">(() =>
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/threads/threadPresentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { StatusTone } from "../../components/StatusPill";
import { EnvironmentScopedThreadShell } from "@t3tools/client-runtime";
import { resolveRemoteHttpUrl } from "../../lib/remoteUrl";

export function threadSortValue(thread: EnvironmentScopedThreadShell): number {
const candidate = Date.parse(thread.updatedAt ?? thread.createdAt);
Expand Down Expand Up @@ -48,6 +49,8 @@ export function messageImageUrl(httpBaseUrl: string | null, attachmentId: string
return null;
}

const url = new URL(`/attachments/${encodeURIComponent(attachmentId)}`, httpBaseUrl);
return url.toString();
return resolveRemoteHttpUrl({
httpBaseUrl,
pathname: `/attachments/${encodeURIComponent(attachmentId)}`,
});
}
25 changes: 25 additions & 0 deletions apps/mobile/src/lib/remoteUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as Effect from "effect/Effect";

import { normalizeBasePath } from "@t3tools/shared/basePath";

export function resolveRemoteHttpUrl(input: {
readonly httpBaseUrl: string;
readonly pathname: string;
readonly searchParams?: Readonly<Record<string, string | null | undefined>>;
}): string {
const url = new URL(input.httpBaseUrl);
const basePath = Effect.runSync(normalizeBasePath(url.pathname));
const pathname = input.pathname.startsWith("/") ? input.pathname : `/${input.pathname}`;

url.pathname = `${basePath}${pathname}`;
url.search = "";
url.hash = "";

for (const [key, value] of Object.entries(input.searchParams ?? {})) {
if (value !== null && value !== undefined) {
url.searchParams.set(key, value);
}
}

return url.toString();
}
19 changes: 10 additions & 9 deletions apps/server/src/auth/Layers/ServerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SessionCredentialService,
} from "../Services/SessionCredentialService.ts";
import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";
import { ServerConfig } from "../../config.ts";

type BootstrapExchangeResult = {
readonly response: AuthBootstrapResult;
Expand Down Expand Up @@ -67,6 +68,7 @@ export const makeServerAuth = Effect.gen(function* () {
const bootstrapCredentials = yield* BootstrapCredentialService;
const authControlPlane = yield* AuthControlPlane;
const sessions = yield* SessionCredentialService;
const serverConfig = yield* ServerConfig;
const descriptor = yield* policy.getDescriptor();

const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>
Expand Down Expand Up @@ -316,15 +318,14 @@ export const makeServerAuth = Effect.gen(function* () {
);

const issueStartupPairingUrl: ServerAuthShape["issueStartupPairingUrl"] = (baseUrl) =>
issuePairingCredential({ role: "owner" }).pipe(
Effect.map((issued) => {
const url = new URL(baseUrl);
url.pathname = "/pair";
url.searchParams.delete("token");
url.hash = new URLSearchParams([["token", issued.credential]]).toString();
return url.toString();
}),
);
Effect.gen(function* () {
const issued = yield* issuePairingCredential({ role: "owner" });
const url = new URL(baseUrl);
url.pathname = `${serverConfig.basePath}/pair`;
url.searchParams.delete("token");
url.hash = new URLSearchParams([["token", issued.credential]]).toString();
return url.toString();
});

const issueWebSocketToken: ServerAuthShape["issueWebSocketToken"] = (session) =>
sessions.issueWebSocketToken(session.sessionId).pipe(
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/auth/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { AuthError, ServerAuth } from "./Services/ServerAuth.ts";
import { SessionCredentialService } from "./Services/SessionCredentialService.ts";
import { deriveAuthClientMetadata } from "./utils.ts";
import { browserApiCorsHeaders } from "../httpCors.ts";
import { ServerConfig } from "../config.ts";

export const respondToAuthError = (error: AuthError) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -70,6 +71,7 @@ export const authBootstrapRouteLayer = HttpRouter.add(
const request = yield* HttpServerRequest.HttpServerRequest;
const serverAuth = yield* ServerAuth;
const sessions = yield* SessionCredentialService;
const config = yield* ServerConfig;
const payload = yield* HttpServerRequest.schemaBodyJson(AuthBootstrapInput).pipe(
Effect.mapError(
(cause) =>
Expand All @@ -92,7 +94,7 @@ export const authBootstrapRouteLayer = HttpRouter.add(
HttpServerResponse.setCookie(sessions.cookieName, result.sessionToken, {
expires: DateTime.toDate(result.response.expiresAt),
httpOnly: true,
path: "/",
path: `${config.basePath}/`,
sameSite: "lax",
}),
);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { join } from "node:path";
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NetService from "@t3tools/shared/Net";
import { ROOT_BASE_PATH } from "@t3tools/shared/basePath";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -76,6 +77,7 @@ const makeCliTestServerConfig = (baseDir: string) =>
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
} satisfies ServerConfigShape;
Expand Down
9 changes: 5 additions & 4 deletions apps/server/src/cli/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
formatPairingCredentialList,
formatSessionList,
} from "../cliAuthFormat.ts";
import { ServerConfig } from "../config.ts";
import { ServerConfig, type ServerConfigShape } from "../config.ts";
import {
authLocationFlags,
type CliAuthLocationFlags,
Expand All @@ -25,7 +25,7 @@ import {

const runWithAuthControlPlane = <A, E>(
flags: CliAuthLocationFlags,
run: (authControlPlane: AuthControlPlaneShape) => Effect.Effect<A, E>,
run: (authControlPlane: AuthControlPlaneShape, config: ServerConfigShape) => Effect.Effect<A, E>,
options?: {
readonly quietLogs?: boolean;
},
Expand All @@ -36,7 +36,7 @@ const runWithAuthControlPlane = <A, E>(
const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel;
return yield* Effect.gen(function* () {
const authControlPlane = yield* AuthControlPlane;
return yield* run(authControlPlane);
return yield* run(authControlPlane, config);
}).pipe(
Effect.provide(
Layer.mergeAll(AuthControlPlaneRuntimeLive).pipe(
Expand Down Expand Up @@ -94,7 +94,7 @@ const pairingCreateCommand = Command.make("create", {
Command.withHandler((flags) =>
runWithAuthControlPlane(
flags,
(authControlPlane) =>
(authControlPlane, config) =>
Effect.gen(function* () {
const issued = yield* authControlPlane.createPairingLink({
role: "client",
Expand All @@ -105,6 +105,7 @@ const pairingCreateCommand = Command.make("create", {
const output = formatIssuedPairingCredential(issued, {
json: flags.json,
...(Option.isSome(flags.baseUrl) ? { baseUrl: flags.baseUrl.value } : {}),
basePath: config.basePath,
});
yield* Console.log(output);
}),
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
} from "@t3tools/contracts";
import * as NetService from "@t3tools/shared/Net";
import { ROOT_BASE_PATH } from "@t3tools/shared/basePath";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { deriveServerPaths } from "../config.ts";
import { resolveServerConfig } from "./config.ts";
Expand Down Expand Up @@ -46,7 +47,6 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
otlpExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
} as const;

const openBootstrapFd = Effect.fn(function* (payload: DesktopBackendBootstrapValue) {
const fs = yield* FileSystem.FileSystem;
const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
Expand All @@ -73,6 +73,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -116,6 +117,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: true,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand All @@ -139,6 +141,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.some(true),
logWebSocketEvents: Option.some(true),
basePath: Option.some("/custom/"),
tailscaleServeEnabled: Option.some(true),
tailscaleServePort: Option.some(8443),
},
Expand Down Expand Up @@ -182,6 +185,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: true,
logWebSocketEvents: true,
basePath: "/custom",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
});
Expand Down Expand Up @@ -213,6 +217,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.some(false),
logWebSocketEvents: Option.some(false),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -251,6 +256,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: "desktop-bootstrap-token",
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand Down Expand Up @@ -288,6 +294,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -325,6 +332,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: "desktop-token",
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand All @@ -351,6 +359,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -410,6 +419,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -450,6 +460,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: "desktop-token",
autoBootstrapProjectFromCwd: true,
logWebSocketEvents: true,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand Down Expand Up @@ -486,6 +497,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -519,6 +531,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand All @@ -543,6 +556,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
basePath: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Expand Down Expand Up @@ -582,6 +596,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
basePath: ROOT_BASE_PATH,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
});
Expand Down
Loading
Loading