From 48c3f7c68649d91da59bf58574c17360dbafc627 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 16:17:46 +0000 Subject: [PATCH 1/3] Use schema decoders for JSON parsing Co-authored-by: Julius Marminge --- apps/server/src/vcs/VcsProjectConfig.test.ts | 20 +++ apps/server/src/vcs/VcsProjectConfig.ts | 41 +++--- packages/shared/src/dpop.test.ts | 130 ++++++++++--------- packages/shared/src/dpop.ts | 99 ++++++-------- 4 files changed, 145 insertions(+), 145 deletions(-) diff --git a/apps/server/src/vcs/VcsProjectConfig.test.ts b/apps/server/src/vcs/VcsProjectConfig.test.ts index aac4beb7e32..a79f2e9d769 100644 --- a/apps/server/src/vcs/VcsProjectConfig.test.ts +++ b/apps/server/src/vcs/VcsProjectConfig.test.ts @@ -67,4 +67,24 @@ describe("VcsProjectConfig", () => { }), ); }); + + it.layer(TestLayer)("falls back to auto for invalid config JSON", (it) => { + it.effect("returns auto", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-vcs-config-test-", + }); + const configDir = path.join(root, ".t3code"); + yield* fileSystem.makeDirectory(configDir, { recursive: true }); + yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{"); + + const config = yield* VcsProjectConfig.VcsProjectConfig; + const kind = yield* config.resolveKind({ cwd: root }); + + assert.equal(kind, "auto"); + }), + ); + }); }); diff --git a/apps/server/src/vcs/VcsProjectConfig.ts b/apps/server/src/vcs/VcsProjectConfig.ts index 3e5ee2347ce..e47e42f48e9 100644 --- a/apps/server/src/vcs/VcsProjectConfig.ts +++ b/apps/server/src/vcs/VcsProjectConfig.ts @@ -2,6 +2,7 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; @@ -15,16 +16,10 @@ const ProjectVcsConfig = Schema.Struct({ ), vcsKind: Schema.optional(VcsDriverKind), }); -const isProjectVcsConfig = Schema.is(ProjectVcsConfig); +const ProjectVcsConfigJson = Schema.fromJsonString(ProjectVcsConfig); +const decodeProjectVcsConfigJson = Schema.decodeUnknownOption(ProjectVcsConfigJson); -interface ProjectVcsConfigFile { - readonly vcs?: - | { - readonly kind?: VcsDriverKindType | undefined; - } - | undefined; - readonly vcsKind?: VcsDriverKindType | undefined; -} +type ProjectVcsConfigFile = typeof ProjectVcsConfig.Type; export interface VcsProjectConfigResolveInput { readonly cwd: string; @@ -45,14 +40,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto return config.vcs?.kind ?? config.vcsKind ?? "auto"; } -function parseConfig(raw: string): ProjectVcsConfigFile | null { - try { - const parsed = JSON.parse(raw) as unknown; - return isProjectVcsConfig(parsed) ? parsed : null; - } catch { - return null; - } -} +const parseConfig = decodeProjectVcsConfigJson; export const make = Effect.fn("makeVcsProjectConfig")(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -63,12 +51,12 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { while (true) { const candidate = path.join(current, ".t3code", "vcs.json"); if (yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false))) { - return candidate; + return Option.some(candidate); } const parent = path.dirname(current); if (parent === current) { - return null; + return Option.none(); } current = parent; } @@ -78,26 +66,27 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { configPath: string, ) { const raw = yield* fileSystem.readFileString(configPath).pipe( + Effect.map(Option.some), Effect.catch((error) => Effect.logWarning("failed to read VCS project config", { configPath, error, - }).pipe(Effect.as(null)), + }).pipe(Effect.as(Option.none())), ), ); - if (raw === null) { + if (Option.isNone(raw)) { return "auto" as const; } - const parsed = parseConfig(raw); - if (parsed === null) { + const parsed = parseConfig(raw.value); + if (Option.isNone(parsed)) { yield* Effect.logWarning("invalid VCS project config", { configPath, }); return "auto" as const; } - return configuredKind(parsed); + return configuredKind(parsed.value); }); const resolveKind: VcsProjectConfigShape["resolveKind"] = Effect.fn( @@ -108,11 +97,11 @@ export const make = Effect.fn("makeVcsProjectConfig")(function* () { } const configPath = yield* findConfigPath(input.cwd); - if (configPath === null) { + if (Option.isNone(configPath)) { return "auto"; } - return yield* readConfiguredKind(configPath); + return yield* readConfiguredKind(configPath.value); }); return VcsProjectConfig.of({ diff --git a/packages/shared/src/dpop.test.ts b/packages/shared/src/dpop.test.ts index 58bd161e2a3..c4b17f323ad 100644 --- a/packages/shared/src/dpop.test.ts +++ b/packages/shared/src/dpop.test.ts @@ -1,6 +1,6 @@ import * as NodeCrypto from "node:crypto"; -import { describe, expect, it } from "@effect/vitest"; +import { assert, describe, it } from "@effect/vitest"; import { computeDpopAccessTokenHash, @@ -56,59 +56,61 @@ describe("verifyDpopProof", () => { it("verifies an ES256 DPoP proof and returns the RFC 7638 thumbprint", () => { const thumbprint = computeDpopJwkThumbprint(publicJwk); - expect( - verifyDpopProof({ - proof, - method: "POST", - url: "https://example.com/oauth/token", - nowEpochSeconds: 101, - expectedThumbprint: thumbprint, - }), - ).toMatchObject({ - ok: true, - thumbprint, - jti: "proof-1", + const result = verifyDpopProof({ + proof, + method: "POST", + url: "https://example.com/oauth/token", + nowEpochSeconds: 101, + expectedThumbprint: thumbprint, }); + + assert.equal(result.ok, true); + assert.equal(result.thumbprint, thumbprint); + assert.equal(result.jti, "proof-1"); }); it("rejects method, URL, thumbprint, and time-window mismatches", () => { const thumbprint = computeDpopJwkThumbprint(publicJwk); - expect( + assert.equal( verifyDpopProof({ proof, method: "GET", url: "https://example.com/oauth/token", nowEpochSeconds: 101, expectedThumbprint: thumbprint, - }), - ).toMatchObject({ ok: false }); - expect( + }).ok, + false, + ); + assert.equal( verifyDpopProof({ proof, method: "POST", url: "https://example.com/other", nowEpochSeconds: 101, expectedThumbprint: thumbprint, - }), - ).toMatchObject({ ok: false }); - expect( + }).ok, + false, + ); + assert.equal( verifyDpopProof({ proof, method: "POST", url: "https://example.com/oauth/token", nowEpochSeconds: 101, expectedThumbprint: "other-thumbprint", - }), - ).toMatchObject({ ok: false }); - expect( + }).ok, + false, + ); + assert.equal( verifyDpopProof({ proof, method: "POST", url: "https://example.com/oauth/token", nowEpochSeconds: 1_000, expectedThumbprint: thumbprint, - }), - ).toMatchObject({ ok: false }); + }).ok, + false, + ); }); it("requires the RFC 9449 access token hash when an access token is expected", () => { @@ -122,7 +124,7 @@ describe("verifyDpopProof", () => { accessToken: "clerk-access-token", }); - expect( + assert.equal( verifyDpopProof({ proof: accessTokenProof, method: "POST", @@ -130,32 +132,36 @@ describe("verifyDpopProof", () => { nowEpochSeconds: 101, expectedThumbprint: thumbprint, expectedAccessToken: "clerk-access-token", - }), - ).toMatchObject({ ok: true }); - expect( - verifyDpopProof({ - proof, - method: "POST", - url: "https://example.com/oauth/token", - nowEpochSeconds: 101, - expectedThumbprint: thumbprint, - expectedAccessToken: "clerk-access-token", - }), - ).toMatchObject({ ok: false, reason: "DPoP access token hash mismatch." }); - expect( - verifyDpopProof({ - proof: accessTokenProof, - method: "POST", - url: "https://example.com/v1/environments/env/connect", - nowEpochSeconds: 101, - expectedThumbprint: thumbprint, - expectedAccessToken: "other-access-token", - }), - ).toMatchObject({ ok: false, reason: "DPoP access token hash mismatch." }); + }).ok, + true, + ); + + const missingHash = verifyDpopProof({ + proof, + method: "POST", + url: "https://example.com/oauth/token", + nowEpochSeconds: 101, + expectedThumbprint: thumbprint, + expectedAccessToken: "clerk-access-token", + }); + assert.equal(missingHash.ok, false); + assert.equal(missingHash.reason, "DPoP access token hash mismatch."); + + const mismatchedHash = verifyDpopProof({ + proof: accessTokenProof, + method: "POST", + url: "https://example.com/v1/environments/env/connect", + nowEpochSeconds: 101, + expectedThumbprint: thumbprint, + expectedAccessToken: "other-access-token", + }); + assert.equal(mismatchedHash.ok, false); + assert.equal(mismatchedHash.reason, "DPoP access token hash mismatch."); }); it("normalizes htu by excluding query and fragment components per RFC 9449", () => { - expect(normalizeDpopHtu("https://example.com/v1/environments/env/connect?foo=bar#frag")).toBe( + assert.equal( + normalizeDpopHtu("https://example.com/v1/environments/env/connect?foo=bar#frag"), "https://example.com/v1/environments/env/connect", ); @@ -168,15 +174,16 @@ describe("verifyDpopProof", () => { publicJwk, }); - expect( + assert.equal( verifyDpopProof({ proof: queryProof, method: "POST", url: "https://example.com/v1/environments/env/connect?foo=bar#frag", nowEpochSeconds: 101, expectedThumbprint: thumbprint, - }), - ).toMatchObject({ ok: true }); + }).ok, + true, + ); }); it("rejects DPoP public JWK headers that expose private key material", () => { @@ -192,14 +199,15 @@ describe("verifyDpopProof", () => { publicJwk: privateJwk, }); - expect( - verifyDpopProof({ - proof: proofWithPrivateJwk, - method: "POST", - url: "https://example.com/oauth/token", - nowEpochSeconds: 101, - expectedThumbprint: thumbprint, - }), - ).toMatchObject({ ok: false, reason: "Invalid DPoP JWT header." }); + const result = verifyDpopProof({ + proof: proofWithPrivateJwk, + method: "POST", + url: "https://example.com/oauth/token", + nowEpochSeconds: 101, + expectedThumbprint: thumbprint, + }); + + assert.equal(result.ok, false); + assert.equal(result.reason, "Invalid DPoP JWT header."); }); }); diff --git a/packages/shared/src/dpop.ts b/packages/shared/src/dpop.ts index 34210679007..a734ac092ba 100644 --- a/packages/shared/src/dpop.ts +++ b/packages/shared/src/dpop.ts @@ -1,6 +1,7 @@ import { p256 } from "@noble/curves/nist"; import { sha256 } from "@noble/hashes/sha2"; import * as Encoding from "effect/Encoding"; +import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; @@ -17,21 +18,31 @@ export const DpopPublicJwk = Schema.Struct({ y: Schema.String.check(Schema.isNonEmpty()), }); export type DpopPublicJwk = typeof DpopPublicJwk.Type; -const isDpopPublicJwk = Schema.is(DpopPublicJwk); -interface DpopJwtHeader { - readonly typ: string; - readonly alg: string; - readonly jwk: DpopPublicJwk; -} +const DpopJwtHeaderPublicJwk = Schema.Struct({ + ...DpopPublicJwk.fields, + d: Schema.optionalKey(Schema.Never), +}); -interface DpopJwtPayload { - readonly htm: string; - readonly htu: string; - readonly jti: string; - readonly iat: number; - readonly ath?: string; -} +const DpopJwtHeaderJson = Schema.fromJsonString( + Schema.Struct({ + typ: Schema.Literal(DPOP_TYP), + alg: Schema.Literal(DPOP_ALG), + jwk: DpopJwtHeaderPublicJwk, + }), +); +const decodeDpopJwtHeaderJson = Schema.decodeUnknownOption(DpopJwtHeaderJson); + +const DpopJwtPayloadJson = Schema.fromJsonString( + Schema.Struct({ + htm: Schema.String.check(Schema.isNonEmpty()), + htu: Schema.String.check(Schema.isNonEmpty()), + jti: Schema.String.check(Schema.isNonEmpty()), + iat: Schema.Int, + ath: Schema.optionalKey(Schema.String), + }), +); +const decodeDpopJwtPayloadJson = Schema.decodeUnknownOption(DpopJwtPayloadJson); export type DpopVerificationResult = | { @@ -49,40 +60,12 @@ function base64UrlToBytes(value: string): Uint8Array { return Result.getOrThrow(Encoding.decodeBase64Url(value)); } -function decodeBase64UrlJson(value: string): unknown { - return JSON.parse(Result.getOrThrow(Encoding.decodeBase64UrlString(value))) as unknown; +function decodeBase64UrlDpopJwtHeader(value: string) { + return decodeDpopJwtHeaderJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value))); } -function isDpopJwtHeader(value: unknown): value is DpopJwtHeader { - if (typeof value !== "object" || value === null) { - return false; - } - const record = value as Record; - return ( - record.typ === DPOP_TYP && - record.alg === DPOP_ALG && - typeof record.jwk === "object" && - record.jwk !== null && - !("d" in record.jwk) && - isDpopPublicJwk(record.jwk) - ); -} - -function isDpopJwtPayload(value: unknown): value is DpopJwtPayload { - if (typeof value !== "object" || value === null) { - return false; - } - const record = value as Record; - return ( - typeof record.htm === "string" && - record.htm.length > 0 && - typeof record.htu === "string" && - record.htu.length > 0 && - typeof record.jti === "string" && - record.jti.length > 0 && - typeof record.iat === "number" && - Number.isInteger(record.iat) - ); +function decodeBase64UrlDpopJwtPayload(value: string) { + return decodeDpopJwtPayloadJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value))); } function dpopThumbprintInput(jwk: DpopPublicJwk): string { @@ -145,44 +128,44 @@ export function verifyDpopProof(input: { } try { - const header = decodeBase64UrlJson(parts[0]); - const payload = decodeBase64UrlJson(parts[1]); - if (!isDpopJwtHeader(header)) { + const header = decodeBase64UrlDpopJwtHeader(parts[0]); + const payload = decodeBase64UrlDpopJwtPayload(parts[1]); + if (Option.isNone(header)) { return { ok: false, reason: "Invalid DPoP JWT header." }; } - if (!isDpopJwtPayload(payload)) { + if (Option.isNone(payload)) { return { ok: false, reason: "Invalid DPoP JWT payload." }; } - const thumbprint = computeDpopJwkThumbprint(header.jwk); + const thumbprint = computeDpopJwkThumbprint(header.value.jwk); if (input.expectedThumbprint && thumbprint !== input.expectedThumbprint) { return { ok: false, reason: "DPoP key thumbprint mismatch." }; } - if (payload.htm.toUpperCase() !== input.method.toUpperCase()) { + if (payload.value.htm.toUpperCase() !== input.method.toUpperCase()) { return { ok: false, reason: "DPoP method mismatch." }; } const normalizedHtu = normalizeDpopHtu(input.url); - if (normalizedHtu === null || payload.htu !== normalizedHtu) { + if (normalizedHtu === null || payload.value.htu !== normalizedHtu) { return { ok: false, reason: "DPoP URL mismatch." }; } if (input.expectedAccessToken) { const expectedAth = computeDpopAccessTokenHash(input.expectedAccessToken); - if (payload.ath !== expectedAth) { + if (payload.value.ath !== expectedAth) { return { ok: false, reason: "DPoP access token hash mismatch." }; } } const maxAgeSeconds = input.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS; if ( - payload.iat > input.nowEpochSeconds + 5 || - input.nowEpochSeconds - payload.iat > maxAgeSeconds + payload.value.iat > input.nowEpochSeconds + 5 || + input.nowEpochSeconds - payload.value.iat > maxAgeSeconds ) { return { ok: false, reason: "DPoP proof is outside the allowed time window." }; } const signature = base64UrlToBytes(parts[2]); const signatureInputHash = sha256(new TextEncoder().encode(`${parts[0]}.${parts[1]}`)); - const verified = p256.verify(signature, signatureInputHash, publicKeyBytesFromJwk(header.jwk), { + const verified = p256.verify(signature, signatureInputHash, publicKeyBytesFromJwk(header.value.jwk), { prehash: false, format: "compact", }); @@ -190,8 +173,8 @@ export function verifyDpopProof(input: { ? { ok: true, thumbprint, - jti: payload.jti, - iat: payload.iat, + jti: payload.value.jti, + iat: payload.value.iat, } : { ok: false, reason: "Invalid DPoP signature." }; } catch { From 3ed4f22a8a2418963abadc4c2d3607173e739dea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 16:19:04 +0000 Subject: [PATCH 2/3] Format DPoP schema parsing Co-authored-by: Julius Marminge --- packages/shared/src/dpop.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/dpop.ts b/packages/shared/src/dpop.ts index a734ac092ba..88dcf8e3090 100644 --- a/packages/shared/src/dpop.ts +++ b/packages/shared/src/dpop.ts @@ -165,10 +165,15 @@ export function verifyDpopProof(input: { const signature = base64UrlToBytes(parts[2]); const signatureInputHash = sha256(new TextEncoder().encode(`${parts[0]}.${parts[1]}`)); - const verified = p256.verify(signature, signatureInputHash, publicKeyBytesFromJwk(header.value.jwk), { - prehash: false, - format: "compact", - }); + const verified = p256.verify( + signature, + signatureInputHash, + publicKeyBytesFromJwk(header.value.jwk), + { + prehash: false, + format: "compact", + }, + ); return verified ? { ok: true, From be977957c5da42ba093a93021f6c5ccfe0dad961 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 16:19:51 +0000 Subject: [PATCH 3/3] Narrow DPoP test assertions Co-authored-by: Julius Marminge --- packages/shared/src/dpop.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/dpop.test.ts b/packages/shared/src/dpop.test.ts index c4b17f323ad..8c5ce4cd6bf 100644 --- a/packages/shared/src/dpop.test.ts +++ b/packages/shared/src/dpop.test.ts @@ -64,7 +64,9 @@ describe("verifyDpopProof", () => { expectedThumbprint: thumbprint, }); - assert.equal(result.ok, true); + if (!result.ok) { + assert.fail(result.reason); + } assert.equal(result.thumbprint, thumbprint); assert.equal(result.jti, "proof-1"); }); @@ -144,7 +146,9 @@ describe("verifyDpopProof", () => { expectedThumbprint: thumbprint, expectedAccessToken: "clerk-access-token", }); - assert.equal(missingHash.ok, false); + if (missingHash.ok) { + assert.fail("Expected DPoP proof without an access token hash to fail."); + } assert.equal(missingHash.reason, "DPoP access token hash mismatch."); const mismatchedHash = verifyDpopProof({ @@ -155,7 +159,9 @@ describe("verifyDpopProof", () => { expectedThumbprint: thumbprint, expectedAccessToken: "other-access-token", }); - assert.equal(mismatchedHash.ok, false); + if (mismatchedHash.ok) { + assert.fail("Expected DPoP proof with a mismatched access token hash to fail."); + } assert.equal(mismatchedHash.reason, "DPoP access token hash mismatch."); }); @@ -207,7 +213,9 @@ describe("verifyDpopProof", () => { expectedThumbprint: thumbprint, }); - assert.equal(result.ok, false); + if (result.ok) { + assert.fail("Expected DPoP proof with private JWK material to fail."); + } assert.equal(result.reason, "Invalid DPoP JWT header."); }); });