118 lines · 4.1 KB · TypeScript
RawDownload
import { verifyDpopProof } from "@t3tools/shared/dpop";import { describe, expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import { decodeJwt, SignJWT } from "jose";import { vi } from "vite-plus/test"; import {  browserCryptoLayer,  BrowserDpopKeyError,  BrowserDpopProofError,  createBrowserDpopProof,  generateBrowserDpopKey,  isBrowserDpopError,} from "./dpop"; describe("browser DPoP proofs", () => {  it.effect("signs relay resource proofs with an access-token hash", () =>    Effect.gen(function* () {      vi.stubGlobal("indexedDB", undefined);      const proofKey = yield* generateBrowserDpopKey;      const proof = yield* createBrowserDpopProof({        method: "POST",        url: "https://relay.example.test/v1/environments/env-1/connect?ignored=true",        accessToken: "relay-access-token",        proofKey,      }).pipe(Effect.provide(browserCryptoLayer));      const issuedAt = decodeJwt(proof.proof).iat;      expect(issuedAt).toBeTypeOf("number");       expect(        verifyDpopProof({          proof: proof.proof,          method: "POST",          url: "https://relay.example.test/v1/environments/env-1/connect",          expectedThumbprint: proof.thumbprint,          expectedAccessToken: "relay-access-token",          nowEpochSeconds: issuedAt!,        }),      ).toMatchObject({ ok: true });    }),  );   it.effect("preserves safe invalid URL context and the parser cause", () =>    Effect.gen(function* () {      const proofKey = yield* generateBrowserDpopKey;      const url = "http://";      const error = yield* createBrowserDpopProof({        method: "POST",        url,        proofKey,      }).pipe(Effect.provide(browserCryptoLayer), Effect.flip);       expect(error).toBeInstanceOf(BrowserDpopProofError);      expect(error).toMatchObject({        operation: "normalize-url",        method: "POST",        requestTarget: "<invalid-url>",        urlLength: url.length,        thumbprint: proofKey.thumbprint,      });      expect(error).not.toHaveProperty("url");      expect(error).not.toHaveProperty("normalizedUrl");      expect(error.cause).toBeInstanceOf(Error);      expect(error.message).not.toContain((error.cause as Error).message);      expect(isBrowserDpopError(error)).toBe(true);    }),  );   it.effect("redacts URL credentials, query, and fragment from proof errors", () =>    Effect.gen(function* () {      const proofKey = yield* generateBrowserDpopKey;      const cause = new Error("signing failed");      const sign = vi.spyOn(SignJWT.prototype, "sign").mockRejectedValueOnce(cause);      const url = "https://user:password@example.com/oauth/token?access_token=secret#fragment";       const error = yield* createBrowserDpopProof({        method: "POST",        url,        proofKey,      }).pipe(Effect.provide(browserCryptoLayer), Effect.flip);       expect(error).toBeInstanceOf(BrowserDpopProofError);      expect(error).toMatchObject({        operation: "sign",        method: "POST",        requestTarget: "https://example.com/oauth/token",        urlLength: url.length,        thumbprint: proofKey.thumbprint,        cause,      });      expect(error).not.toHaveProperty("url");      expect(error).not.toHaveProperty("normalizedUrl");      expect(error.message).not.toContain("user");      expect(error.message).not.toContain("password");      expect(error.message).not.toContain("access_token");      expect(error.message).not.toContain("secret");      expect(error.message).not.toContain("fragment");      sign.mockRestore();    }),  );   it.effect("preserves the browser crypto cause when key generation fails", () =>    Effect.gen(function* () {      const cause = new Error("browser crypto unavailable");      const generateKey = vi        .spyOn(globalThis.crypto.subtle, "generateKey")        .mockRejectedValueOnce(cause);       const error = yield* generateBrowserDpopKey.pipe(Effect.flip);       expect(error).toBeInstanceOf(BrowserDpopKeyError);      expect(error.operation).toBe("generate");      expect(error.cause).toBe(cause);      expect(error.message).not.toContain(cause.message);      generateKey.mockRestore();    }),  );});