Skip to content
Merged
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
5 changes: 3 additions & 2 deletions apps/mobile/src/features/cloud/linkEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,8 @@ describe("mobile cloud link environment client", () => {
Response.json(
{
_tag: "EnvironmentHttpUnauthorizedError",
message: "Invalid environment bearer session.",
reason: "cloud_cli_authorization_required",
message: "Run `t3 connect link` to authorize this environment.",
},
{ status: 401 },
),
Expand All @@ -623,7 +624,7 @@ describe("mobile cloud link environment client", () => {
).pipe(Effect.flip);
expect(error._tag).toBe("CloudEnvironmentLinkError");
expect(error.message).toBe(
"Could not obtain environment link proof: Invalid environment bearer session.",
"Could not obtain environment link proof: Run `t3 connect link` to authorize this environment.",
);
expect(fetchMock).toHaveBeenCalledTimes(2);
}),
Expand Down
251 changes: 212 additions & 39 deletions apps/server/src/cloud/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";
import * as Tracer from "effect/Tracer";
import { HttpClient, HttpServerRequest } from "effect/unstable/http";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpClientError from "effect/unstable/http/HttpClientError";
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
import { EnvironmentHttpInternalServerError } from "@t3tools/contracts";

import { RelayClientTracer } from "@t3tools/shared/relayTracing";
import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import * as CliTokenManager from "./CliTokenManager.ts";
import { consumeCloudReplayGuards, reconcileDesiredCloudLink } from "./http.ts";
import {
CloudRelayRequestError,
consumeCloudReplayGuards,
reconcileDesiredCloudLink,
} from "./http.ts";
import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
import { traceAuthenticatedRelayRequest, traceRelayRequest } from "./traceRelayRequest.ts";

const encodeEnvironmentHttpInternalServerError = Schema.encodeUnknownSync(
EnvironmentHttpInternalServerError,
);
const decodeEnvironmentHttpInternalServerError = Schema.decodeUnknownSync(
EnvironmentHttpInternalServerError,
);

const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
new ServerSecretStore.SecretStorePersistError({
operation: "create",
Expand Down Expand Up @@ -42,6 +62,55 @@ function makeSecretStore(
};
}

function reconcileWith(input: {
readonly getExisting: CliTokenManager.CloudCliTokenManager["Service"]["getExisting"];
readonly httpClient?: HttpClient.HttpClient;
readonly env?: Readonly<Record<string, string>>;
}) {
return reconcileDesiredCloudLink("http://127.0.0.1:3774").pipe(
Effect.provideService(
ServerSecretStore.ServerSecretStore,
makeSecretStore(unusedSecretStoreOperation),
),
Effect.provideService(
ServerEnvironment.ServerEnvironment,
ServerEnvironment.ServerEnvironment.of({
getEnvironmentId: unusedSecretStoreOperation(),
getDescriptor: unusedSecretStoreOperation(),
}),
),
Effect.provideService(
ManagedEndpointRuntime.CloudManagedEndpointRuntime,
ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
applyConfig: unusedSecretStoreOperation,
} satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]),
),
Effect.provideService(
EnvironmentAuth.EnvironmentAuth,
EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]),
),
Effect.provideService(
CliTokenManager.CloudCliTokenManager,
CliTokenManager.CloudCliTokenManager.of({
get: unusedSecretStoreOperation(),
getExisting: input.getExisting,
hasCredential: unusedSecretStoreOperation(),
clear: unusedSecretStoreOperation(),
}),
),
Effect.provideService(
HttpClient.HttpClient,
input.httpClient ?? HttpClient.make(() => unusedSecretStoreOperation()),
),
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
ConfigProvider.layer(ConfigProvider.fromEnv({ env: input.env ?? {} })),
),
),
);
}

it("preserves messages surfaced by cloud 500 responses", () => {
const cause = new Error("cloud operation failed");

Expand Down Expand Up @@ -95,6 +164,84 @@ describe("consumeCloudReplayGuards", () => {
);
});

describe("CloudRelayRequestError", () => {
it("classifies response failures without deriving its message from the cause", () => {
const requestUrl =
"https://relay-user:relay-password@relay.example.test/private/environment-links?token=relay-secret#relay-fragment";
const request = HttpClientRequest.post(requestUrl);
const response = HttpClientResponse.fromWeb(
request,
new Response("sensitive upstream response", { status: 502 }),
);
const upstreamCause = new Error("sensitive upstream response details");
const cause = new HttpClientError.HttpClientError({
reason: new HttpClientError.StatusCodeError({
request,
response,
cause: upstreamCause,
}),
});

const error = CloudRelayRequestError.fromClientFailure({
operation: "create-environment-link",
url: request.url,
cause,
});

expect(error).toMatchObject({
operation: "create-environment-link",
phase: "check-response-status",
method: "POST",
requestUrlInputLength: requestUrl.length,
requestUrlProtocol: "https:",
requestUrlHostname: "relay.example.test",
responseStatus: 502,
cause,
});
expect(error.cause).toBe(cause);
expect(error).not.toHaveProperty("url");
expect(error.message).toBe(
"T3 Connect relay create-environment-link failed during check-response-status with response status 502.",
);
expect(error.message).not.toContain(upstreamCause.message);
for (const secret of [
"relay-user",
"relay-password",
"/private/environment-links",
"relay-secret",
"relay-fragment",
]) {
expect(error.message).not.toContain(secret);
expect(Object.values(error).join(" ")).not.toContain(secret);
}
});
});

it("keeps internal causes out of encoded HTTP error bodies", () => {
const cause = new Error("private upstream detail");
const error = new EnvironmentHttpInternalServerError({
operation: "generate_link_proof",
cause,
});

expect(error.cause).toBe(cause);
expect(encodeEnvironmentHttpInternalServerError(error)).toEqual({
_tag: "EnvironmentHttpInternalServerError",
operation: "generate_link_proof",
message: "Could not generate environment link proof.",
});
});

it("decodes legacy message-only HTTP errors during rolling deployments", () => {
const error = decodeEnvironmentHttpInternalServerError({
_tag: "EnvironmentHttpInternalServerError",
message: "Legacy environment server failure.",
});

expect(error.operation).toBeUndefined();
expect(error.message).toBe("Legacy environment server failure.");
});

describe("relay request tracing", () => {
it.effect("does not accept an unauthenticated request trace parent", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -162,48 +309,74 @@ describe("relay request tracing", () => {
describe("reconcileDesiredCloudLink", () => {
it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () =>
Effect.gen(function* () {
const error = yield* Effect.flip(reconcileDesiredCloudLink("http://127.0.0.1:3774"));
const error = yield* Effect.flip(
reconcileWith({ getExisting: Effect.succeed(Option.none()) }),
);

expect(error).toMatchObject({
_tag: "EnvironmentHttpUnauthorizedError",
reason: "cloud_cli_authorization_required",
message: "Run `t3 connect link` to authorize this environment.",
});
}).pipe(
Effect.provideService(
ServerSecretStore.ServerSecretStore,
makeSecretStore(unusedSecretStoreOperation),
),
Effect.provideService(
ServerEnvironment.ServerEnvironment,
ServerEnvironment.ServerEnvironment.of({
getEnvironmentId: unusedSecretStoreOperation(),
getDescriptor: unusedSecretStoreOperation(),
}),
),
Effect.provideService(
ManagedEndpointRuntime.CloudManagedEndpointRuntime,
ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
applyConfig: unusedSecretStoreOperation,
} satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]),
),
Effect.provideService(
EnvironmentAuth.EnvironmentAuth,
EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]),
),
Effect.provideService(
CliTokenManager.CloudCliTokenManager,
CliTokenManager.CloudCliTokenManager.of({
get: unusedSecretStoreOperation(),
getExisting: Effect.succeed(Option.none()),
hasCredential: unusedSecretStoreOperation(),
clear: unusedSecretStoreOperation(),
}),
);

it.effect("redacts relay transport failures behind a stable structural message", () => {
const transportCause = new Error("upstream included a sensitive database password");
const capturedLogs: Array<ReadonlyArray<unknown>> = [];
const logger = Logger.make(({ message }) => {
capturedLogs.push(Array.isArray(message) ? message : [message]);
});
const httpClient = HttpClient.make((request) =>
Effect.fail(
new HttpClientError.HttpClientError({
reason: new HttpClientError.TransportError({ request, cause: transportCause }),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make(() => unusedSecretStoreOperation()),
),
Effect.provide(NodeServices.layer),
),
);
);

return Effect.gen(function* () {
const error = yield* Effect.flip(
reconcileWith({
getExisting: Effect.succeed(
Option.some({
accessToken: "access-token",
refreshToken: "refresh-token",
expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
}),
),
httpClient,
env: { T3CODE_RELAY_URL: "https://relay.example.test" },
}).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))),
);

expect(error).toMatchObject({
_tag: "EnvironmentHttpInternalServerError",
operation: "relay_request",
relayOperation: "create-link-challenge",
relayPhase: "send-request",
message: "T3 Connect relay create-link-challenge failed during send-request.",
cause: {
_tag: "CloudRelayRequestError",
operation: "create-link-challenge",
phase: "send-request",
},
});
expect(error.message).not.toContain(transportCause.message);
expect(capturedLogs).toHaveLength(1);
const logFields = capturedLogs[0]?.find(
(value): value is Record<string, unknown> => typeof value === "object" && value !== null,
);
expect(logFields).toMatchObject({
operation: "relay_request",
relayOperation: "create-link-challenge",
relayPhase: "send-request",
causeTag: "CloudRelayRequestError",
});
expect(logFields).not.toHaveProperty("cause");
expect(capturedLogs[0]?.filter((value) => typeof value === "string").join(" ")).not.toContain(
transportCause.message,
);
});
});
});
Loading
Loading