| 1 | import { normalizeHostedServer as hostedServerUrl } from "@t3tools/shared/hostedServerUrl"; |
| 2 | // @effect-diagnostics nodeBuiltinImport:off - Verify the desktop-only PKCE bytes independently. |
| 3 | import * as NodeCrypto from "node:crypto"; |
| 4 | import { describe, expect, it } from "@effect/vitest"; |
| 5 | import { |
| 6 | createHostedAuthAttempt, |
| 7 | isHostedAuthCallback, |
| 8 | readHostedAuthCallback, |
| 9 | } from "./hostedAuthHandoff.ts"; |
| 10 | |
| 11 | const server = "https://knife.example.test"; |
| 12 | const callback = (state: string, origin = server) => |
| 13 | `t3code://app/self-hosted/callback?${new URLSearchParams({ code: "one-time-proof", state, server: origin })}`; |
| 14 | |
| 15 | describe("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 | }); |