1import { normalizeHostedServer as hostedServerUrl } from "@t3tools/shared/hostedServerUrl";
2// @effect-diagnostics nodeBuiltinImport:off - Verify the desktop-only PKCE bytes independently.
3import * as NodeCrypto from "node:crypto";
4import { describe, expect, it } from "@effect/vitest";
5import {
6 createHostedAuthAttempt,
7 isHostedAuthCallback,
8 readHostedAuthCallback,
9} from "./hostedAuthHandoff.ts";
10
11const server = "https://knife.example.test";
12const callback = (state: string, origin = server) =>
13 `t3code://app/self-hosted/callback?${new URLSearchParams({ code: "one-time-proof", state, server: origin })}`;
14
15describe("desktop hosted sign-in handoff", () => {
16 it("creates independent state and S256 proof material for each login", () => {
17 const first = createHostedAuthAttempt(server, 0);
18 const second = createHostedAuthAttempt(server, 0);
19 expect(first.verifier).toHaveLength(43);
20 expect(first.codeChallenge).toBe(
21 NodeCrypto.createHash("sha256").update(first.verifier).digest("base64url"),
22 );
23 expect(first.state).not.toBe(second.state);
24 expect(first.verifier).not.toBe(second.verifier);
25 expect(readHostedAuthCallback(callback(first.state), first, 1)).toEqual({
26 code: "one-time-proof",
27 codeVerifier: first.verifier,
28 });
29 });
30
31 it.each([
32 "http://knife.example.test",
33 "https://user:password@knife.example.test",
34 "https://knife.example.test?target=other",
35 "https://knife.example.test#token",
36 ])("rejects unsafe server addresses: %s", (address) => {
37 expect(() => hostedServerUrl(address)).toThrow();
38 });
39
40 it("rejects a mounted path and canonicalizes root HTTPS origins", () => {
41 expect(() => hostedServerUrl("https://knife.example.test/knife/")).toThrow();
42 expect(hostedServerUrl(`${server}/`)).toBe(server);
43 });
44
45 it.each([
46 "https://app/self-hosted/callback",
47 "t3code://other/self-hosted/callback",
48 "t3code://user@app/self-hosted/callback",
49 "t3code://app:123/self-hosted/callback",
50 "t3code://app/self-hosted/callback/extra",
51 ])("does not intercept unrelated links: %s", (url) => {
52 expect(isHostedAuthCallback(url)).toBe(false);
53 });
54
55 it("rejects callbacks from another server, login, or expired attempt", () => {
56 const pending = createHostedAuthAttempt(server, 100);
57 expect(() =>
58 readHostedAuthCallback(callback(pending.state, "https://other.example.test"), pending, 101),
59 ).toThrow();
60 expect(() => readHostedAuthCallback(callback("wrong-state"), pending, 101)).toThrow();
61 expect(() =>
62 readHostedAuthCallback(callback(pending.state), pending, pending.expiresAt),
63 ).toThrow();
64 expect(() =>
65 readHostedAuthCallback(callback(pending.state) + "&code=other", pending, 101),
66 ).toThrow();
67 expect(() =>
68 readHostedAuthCallback(callback(pending.state) + "#proof", pending, 101),
69 ).toThrow();
70 });
71});