import { readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth";import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import * as ConfigProvider from "effect/ConfigProvider";import * as Crypto from "effect/Crypto";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Terminal from "effect/Terminal";import * as HttpClient from "effect/unstable/http/HttpClient";import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import * as CliTokenManager from "./CliTokenManager.ts";import type { OutOfBandOAuthPromptInput } from "./CliTokenManager.ts"; // pk_test_<base64 of "clerk.example.test$">const TEST_ENV = { T3CODE_CLERK_PUBLISHABLE_KEY: "pk_test_Y2xlcmsuZXhhbXBsZS50ZXN0JA==", T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: "oauth_client_test", T3CODE_HOSTED_APP_URL: "https://hosted.example.test",}; interface RecordedTokenRequest { readonly url: string; readonly params: URLSearchParams;} // A JWT whose payload claims { email: "theo@example.test" } (signature is not// verified — the CLI only reads the claim to display the connected account).const TestIdTokenHeaderJson = Schema.fromJsonString(Schema.Struct({ alg: Schema.Literal("none") }));const TestIdTokenPayloadJson = Schema.fromJsonString(Schema.Struct({ email: Schema.String }));const encodeTestIdTokenHeader = Schema.encodeSync(TestIdTokenHeaderJson);const encodeTestIdTokenPayload = Schema.encodeSync(TestIdTokenPayloadJson);const idTokenWithEmail = (() => { const header = Encoding.encodeBase64Url(encodeTestIdTokenHeader({ alg: "none" })); const payload = Encoding.encodeBase64Url( encodeTestIdTokenPayload({ email: "theo@example.test" }), ); return `${header}.${payload}.`;})(); const TestTokenResponseJson = Schema.fromJsonString( Schema.Struct({ access_token: Schema.String, refresh_token: Schema.String, id_token: Schema.String, expires_in: Schema.Number, token_type: Schema.String, }),);const encodeTestTokenResponse = Schema.encodeSync(TestTokenResponseJson); const makeTokenEndpointLayer = ( requests: Array<RecordedTokenRequest>, options?: { readonly idToken?: string },) => Layer.succeed( HttpClient.HttpClient, HttpClient.make((request) => Effect.sync(() => { const body = request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""; requests.push({ url: request.url, params: new URLSearchParams(body) }); return HttpClientResponse.fromWeb( request, new Response( encodeTestTokenResponse({ access_token: "access-token-1", refresh_token: "refresh-token-1", id_token: options?.idToken ?? idTokenWithEmail, expires_in: 3600, token_type: "bearer", }), { status: 200, headers: { "content-type": "application/json" } }, ), ); }), ), ); const provideTestEnv = Effect.provide( ConfigProvider.layer(ConfigProvider.fromEnv({ env: TEST_ENV })),); const isAuthorizationError = Schema.is(CliTokenManager.CloudCliAuthorizationError); class PromptRejectedError extends Schema.TaggedError<PromptRejectedError>()("PromptRejectedError", { message: Schema.String,}) {} const makeTestTerminal = (queue: Queue.Queue<Terminal.UserInput>) => Terminal.make({ columns: Effect.succeed(80), rows: Effect.succeed(24), readInput: Effect.succeed(Queue.asDequeue(queue)), readLine: Effect.never, display: () => Effect.void, }); const userInput = (name: string): Terminal.UserInput => ({ input: Option.some(name), key: { name, ctrl: false, meta: false, shift: name !== name.toLowerCase() },}); it.effect("opens the browser on Enter and switches the active flow on H", () => Effect.gen(function* () { const queue = yield* Queue.make<Terminal.UserInput>(); yield* Queue.offerAll(queue, [userInput("enter"), userInput("H")]); const opened: Array<string> = []; const result = yield* CliTokenManager.waitForLoopbackAuthorization({ authorizationUrl: "https://clerk.example.test/authorize", callback: Effect.never, terminal: makeTestTerminal(queue), launchBrowser: (url) => Effect.sync(() => { opened.push(url); }), }); assert.deepEqual(opened, ["https://clerk.example.test/authorize"]); assert.deepEqual(result, { _tag: "HeadlessRequested" }); }),); it.effect("finishes normally when the browser callback wins", () => Effect.gen(function* () { const queue = yield* Queue.make<Terminal.UserInput>(); const callback = yield* Deferred.make<string>(); yield* Deferred.succeed(callback, "clerk-code-123"); const result = yield* CliTokenManager.waitForLoopbackAuthorization({ authorizationUrl: "https://clerk.example.test/authorize", callback: Deferred.await(callback), terminal: makeTestTerminal(queue), launchBrowser: () => Effect.die("browser launch should not run"), }); assert.deepEqual(result, { _tag: "AuthorizationCode", code: "clerk-code-123" }); }),); it.layer(NodeServices.layer)("CliTokenManager.outOfBandOAuthLogin", (it) => { it.effect("prints a hosted authorize URL and exchanges the out-of-band code with PKCE", () => Effect.gen(function* () { const requests: Array<RecordedTokenRequest> = []; let seenAuthorizeUrl = ""; const { token, identity } = yield* CliTokenManager.outOfBandOAuthLogin( ({ authorizeUrl, validate }: OutOfBandOAuthPromptInput) => Effect.gen(function* () { seenAuthorizeUrl = authorizeUrl; const request = readConnectAuthorizeRequest(new URL(authorizeUrl)); assert.isNotNull(request); return yield* validate(`clerk-code-123.${request!.state}`).pipe( Effect.mapError((message) => new PromptRejectedError({ message })), ); }), ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv); const authorizeUrl = new URL(seenAuthorizeUrl); assert.equal(authorizeUrl.origin, "https://hosted.example.test"); assert.equal(authorizeUrl.pathname, "/connect"); const request = readConnectAuthorizeRequest(authorizeUrl); assert.isNotNull(request); assert.match(request!.state, /^[A-Za-z0-9_-]{22}$/); assert.equal(token.accessToken, "access-token-1"); assert.equal(token.refreshToken, "refresh-token-1"); assert.equal(token.identity, "theo@example.test"); // The id_token's email claim is surfaced so connect can show the account. assert.equal(identity, "theo@example.test"); assert.lengthOf(requests, 1); const exchange = requests[0]!; assert.equal(exchange.url, "https://clerk.example.test/oauth/token"); assert.equal(exchange.params.get("grant_type"), "authorization_code"); assert.equal(exchange.params.get("code"), "clerk-code-123"); assert.equal( exchange.params.get("redirect_uri"), "https://hosted.example.test/connect/callback", ); assert.equal(exchange.params.get("client_id"), "oauth_client_test"); // The verifier must hash to the challenge advertised in the authorize URL. const verifier = exchange.params.get("code_verifier"); assert.isNotNull(verifier); const crypto = yield* Crypto.Crypto; const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier!)); assert.equal(Encoding.encodeBase64Url(digest), request!.challenge); }), ); it.effect("rejects out-of-band codes whose state does not match the request", () => Effect.gen(function* () { const requests: Array<RecordedTokenRequest> = []; const validationErrors: Array<string> = []; const result = yield* CliTokenManager.outOfBandOAuthLogin( ({ validate }: OutOfBandOAuthPromptInput) => validate("clerk-code-123.wrong-state").pipe( Effect.tapError((message) => Effect.sync(() => validationErrors.push(message))), Effect.mapError((message) => new PromptRejectedError({ message })), ), ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip); assert.lengthOf(requests, 0); assert.lengthOf(validationErrors, 1); assert.include(validationErrors[0], "different connect request"); assert.instanceOf(result, PromptRejectedError); }), ); it.effect("ignores an id_token whose claims are not valid JSON", () => Effect.gen(function* () { const requests: Array<RecordedTokenRequest> = []; const malformedIdToken = `header.${Encoding.encodeBase64Url("not-json")}.signature`; const { identity } = yield* CliTokenManager.outOfBandOAuthLogin( ({ authorizeUrl }: OutOfBandOAuthPromptInput) => { const request = readConnectAuthorizeRequest(new URL(authorizeUrl)); assert.isNotNull(request); return Effect.succeed(`clerk-code-123.${request!.state}`); }, ).pipe( Effect.provide(makeTokenEndpointLayer(requests, { idToken: malformedIdToken })), provideTestEnv, ); assert.isNull(identity); assert.lengthOf(requests, 1); }), ); it.effect("fails without touching the token endpoint when the prompt returns garbage", () => Effect.gen(function* () { const requests: Array<RecordedTokenRequest> = []; const result = yield* CliTokenManager.outOfBandOAuthLogin(() => Effect.succeed("not-a-connect-code"), ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip); assert.lengthOf(requests, 0); assert.isTrue(isAuthorizationError(result)); }), );});
import { readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Crypto from "effect/Crypto";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Encoding from "effect/Encoding";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Schema from "effect/Schema";
import * as Terminal from "effect/Terminal";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
import * as CliTokenManager from "./CliTokenManager.ts";
import type { OutOfBandOAuthPromptInput } from "./CliTokenManager.ts";
// pk_test_<base64 of "clerk.example.test$">
const TEST_ENV = {
T3CODE_CLERK_PUBLISHABLE_KEY: "pk_test_Y2xlcmsuZXhhbXBsZS50ZXN0JA==",
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: "oauth_client_test",
T3CODE_HOSTED_APP_URL: "https://hosted.example.test",
};
interface RecordedTokenRequest {
readonly url: string;
readonly params: URLSearchParams;
}
// A JWT whose payload claims { email: "theo@example.test" } (signature is not
// verified — the CLI only reads the claim to display the connected account).
const TestIdTokenHeaderJson = Schema.fromJsonString(Schema.Struct({ alg: Schema.Literal("none") }));
const TestIdTokenPayloadJson = Schema.fromJsonString(Schema.Struct({ email: Schema.String }));
const encodeTestIdTokenHeader = Schema.encodeSync(TestIdTokenHeaderJson);
const encodeTestIdTokenPayload = Schema.encodeSync(TestIdTokenPayloadJson);
const idTokenWithEmail = (() => {
const header = Encoding.encodeBase64Url(encodeTestIdTokenHeader({ alg: "none" }));
const payload = Encoding.encodeBase64Url(
encodeTestIdTokenPayload({ email: "theo@example.test" }),
);
return `${header}.${payload}.`;
})();
const TestTokenResponseJson = Schema.fromJsonString(
Schema.Struct({
access_token: Schema.String,
refresh_token: Schema.String,
id_token: Schema.String,
expires_in: Schema.Number,
token_type: Schema.String,
}),
);
const encodeTestTokenResponse = Schema.encodeSync(TestTokenResponseJson);
const makeTokenEndpointLayer = (
requests: Array<RecordedTokenRequest>,
options?: { readonly idToken?: string },
) =>
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.sync(() => {
const body =
request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "";
requests.push({ url: request.url, params: new URLSearchParams(body) });
return HttpClientResponse.fromWeb(
request,
new Response(
encodeTestTokenResponse({
access_token: "access-token-1",
refresh_token: "refresh-token-1",
id_token: options?.idToken ?? idTokenWithEmail,
expires_in: 3600,
token_type: "bearer",
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
}),
),
);
const provideTestEnv = Effect.provide(
ConfigProvider.layer(ConfigProvider.fromEnv({ env: TEST_ENV })),
);
const isAuthorizationError = Schema.is(CliTokenManager.CloudCliAuthorizationError);
class PromptRejectedError extends Schema.TaggedError<PromptRejectedError>()("PromptRejectedError", {
message: Schema.String,
}) {}
const makeTestTerminal = (queue: Queue.Queue<Terminal.UserInput>) =>
Terminal.make({
columns: Effect.succeed(80),
rows: Effect.succeed(24),
readInput: Effect.succeed(Queue.asDequeue(queue)),
readLine: Effect.never,
display: () => Effect.void,
});
const userInput = (name: string): Terminal.UserInput => ({
input: Option.some(name),
key: { name, ctrl: false, meta: false, shift: name !== name.toLowerCase() },
});
it.effect("opens the browser on Enter and switches the active flow on H", () =>
Effect.gen(function* () {
const queue = yield* Queue.make<Terminal.UserInput>();
yield* Queue.offerAll(queue, [userInput("enter"), userInput("H")]);
const opened: Array<string> = [];
const result = yield* CliTokenManager.waitForLoopbackAuthorization({
authorizationUrl: "https://clerk.example.test/authorize",
callback: Effect.never,
terminal: makeTestTerminal(queue),
launchBrowser: (url) =>
Effect.sync(() => {
opened.push(url);
}),
});
assert.deepEqual(opened, ["https://clerk.example.test/authorize"]);
assert.deepEqual(result, { _tag: "HeadlessRequested" });
}),
);
it.effect("finishes normally when the browser callback wins", () =>
Effect.gen(function* () {
const queue = yield* Queue.make<Terminal.UserInput>();
const callback = yield* Deferred.make<string>();
yield* Deferred.succeed(callback, "clerk-code-123");
const result = yield* CliTokenManager.waitForLoopbackAuthorization({
authorizationUrl: "https://clerk.example.test/authorize",
callback: Deferred.await(callback),
terminal: makeTestTerminal(queue),
launchBrowser: () => Effect.die("browser launch should not run"),
});
assert.deepEqual(result, { _tag: "AuthorizationCode", code: "clerk-code-123" });
}),
);
it.layer(NodeServices.layer)("CliTokenManager.outOfBandOAuthLogin", (it) => {
it.effect("prints a hosted authorize URL and exchanges the out-of-band code with PKCE", () =>
Effect.gen(function* () {
const requests: Array<RecordedTokenRequest> = [];
let seenAuthorizeUrl = "";
const { token, identity } = yield* CliTokenManager.outOfBandOAuthLogin(
({ authorizeUrl, validate }: OutOfBandOAuthPromptInput) =>
Effect.gen(function* () {
seenAuthorizeUrl = authorizeUrl;
const request = readConnectAuthorizeRequest(new URL(authorizeUrl));
assert.isNotNull(request);
return yield* validate(`clerk-code-123.${request!.state}`).pipe(
Effect.mapError((message) => new PromptRejectedError({ message })),
);
}),
).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv);
const authorizeUrl = new URL(seenAuthorizeUrl);
assert.equal(authorizeUrl.origin, "https://hosted.example.test");
assert.equal(authorizeUrl.pathname, "/connect");
const request = readConnectAuthorizeRequest(authorizeUrl);
assert.isNotNull(request);
assert.match(request!.state, /^[A-Za-z0-9_-]{22}$/);
assert.equal(token.accessToken, "access-token-1");
assert.equal(token.refreshToken, "refresh-token-1");
assert.equal(token.identity, "theo@example.test");
// The id_token's email claim is surfaced so connect can show the account.
assert.equal(identity, "theo@example.test");
assert.lengthOf(requests, 1);
const exchange = requests[0]!;
assert.equal(exchange.url, "https://clerk.example.test/oauth/token");
assert.equal(exchange.params.get("grant_type"), "authorization_code");
assert.equal(exchange.params.get("code"), "clerk-code-123");
assert.equal(
exchange.params.get("redirect_uri"),
"https://hosted.example.test/connect/callback",
);
assert.equal(exchange.params.get("client_id"), "oauth_client_test");
// The verifier must hash to the challenge advertised in the authorize URL.
const verifier = exchange.params.get("code_verifier");
assert.isNotNull(verifier);
const crypto = yield* Crypto.Crypto;
const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier!));
assert.equal(Encoding.encodeBase64Url(digest), request!.challenge);
}),
);
it.effect("rejects out-of-band codes whose state does not match the request", () =>
Effect.gen(function* () {
const requests: Array<RecordedTokenRequest> = [];
const validationErrors: Array<string> = [];
const result = yield* CliTokenManager.outOfBandOAuthLogin(
({ validate }: OutOfBandOAuthPromptInput) =>
validate("clerk-code-123.wrong-state").pipe(
Effect.tapError((message) => Effect.sync(() => validationErrors.push(message))),
Effect.mapError((message) => new PromptRejectedError({ message })),
),
).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip);
assert.lengthOf(requests, 0);
assert.lengthOf(validationErrors, 1);
assert.include(validationErrors[0], "different connect request");
assert.instanceOf(result, PromptRejectedError);
}),
);
it.effect("ignores an id_token whose claims are not valid JSON", () =>
Effect.gen(function* () {
const requests: Array<RecordedTokenRequest> = [];
const malformedIdToken = `header.${Encoding.encodeBase64Url("not-json")}.signature`;
const { identity } = yield* CliTokenManager.outOfBandOAuthLogin(
({ authorizeUrl }: OutOfBandOAuthPromptInput) => {
const request = readConnectAuthorizeRequest(new URL(authorizeUrl));
assert.isNotNull(request);
return Effect.succeed(`clerk-code-123.${request!.state}`);
},
).pipe(
Effect.provide(makeTokenEndpointLayer(requests, { idToken: malformedIdToken })),
provideTestEnv,
);
assert.isNull(identity);
assert.lengthOf(requests, 1);
}),
);
it.effect("fails without touching the token endpoint when the prompt returns garbage", () =>
Effect.gen(function* () {
const requests: Array<RecordedTokenRequest> = [];
const result = yield* CliTokenManager.outOfBandOAuthLogin(() =>
Effect.succeed("not-a-connect-code"),
).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip);
assert.lengthOf(requests, 0);
assert.isTrue(isAuthorizationError(result));
}),
);
});