From a13f3687413711af57a69e674de5e777d83c0c3d Mon Sep 17 00:00:00 2001 From: Jeremie Charrier Date: Wed, 29 Jul 2026 18:16:09 +0200 Subject: [PATCH] [WALLET-177] Move credential offer fetch to backend Co-Authored-By: Claude Opus 4.8 (1M context) --- app/_components/use_cases/issuance-case.tsx | 42 +++++++++------------ app/api/credential-offer/route.ts | 35 +++++++++++++++++ app/lib/environments.ts | 5 +++ 3 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 app/api/credential-offer/route.ts diff --git a/app/_components/use_cases/issuance-case.tsx b/app/_components/use_cases/issuance-case.tsx index cb1a906..c1d659d 100644 --- a/app/_components/use_cases/issuance-case.tsx +++ b/app/_components/use_cases/issuance-case.tsx @@ -10,9 +10,8 @@ import { settingsToQuery, useDemoSettings, } from "../../_components/demo-settings-context"; -import { apiBaseUrl } from "../../lib/environments"; +import { credentialOfferUrl } from "../../lib/environments"; -const OFFER_ID = "00000000-0000-0000-0000-000000000001"; const OID4VCI_PROTOCOL = "openid4vci-v1"; const CHROME_FLAG = "chrome://flags/#web-identity-digital-credentials-creation"; const FLAG_HINT = `The Digital Credentials API for credential issuance is not enabled in this browser. Enable ${CHROME_FLAG} and reload the page.`; @@ -32,35 +31,28 @@ const getDcApiSnapshot = (): DcApiStatus => "DigitalCredential" in window ? "supported" : "unsupported"; /** - * Parses an OID4VCI Credential Offer URI and extracts the JSON credential offer. - * - * Handles two formats: - * - Inline: `openid-credential-offer://?credential_offer={...json...}` - * - Reference: `openid-credential-offer://?credential_offer_uri=https://...` (fetches the JSON) + * Fetches the JSON credential offer via the backend proxy route, which + * requests it server-side to avoid the browser's cross-origin restriction. */ -async function parseCredentialOfferUri(credentialOfferUri: string): Promise { - const url = new URL(credentialOfferUri); - const inlineOffer = url.searchParams.get("credential_offer"); - if (inlineOffer) { - return JSON.parse(inlineOffer); - } - const offerUri = url.searchParams.get("credential_offer_uri"); - if (offerUri) { - const response = await fetch(offerUri); - if (!response.ok) { - throw new Error(`Failed to fetch credential offer from ${offerUri}: ${response.status}`); - } - return response.json(); - } - throw new Error( - "Credential offer URI has neither credential_offer nor credential_offer_uri parameter", +async function fetchCredentialOffer(env: string): Promise { + const response = await fetch( + `/api/credential-offer?environmentKey=${encodeURIComponent(env)}`, ); + const json = await response.json(); + if (!response.ok) { + throw new Error( + typeof json?.error === "string" + ? json.error + : `Failed to fetch credential offer: ${response.status}`, + ); + } + return json; } export function IssuanceCase() { const { env, responseMode, authzMethod } = useDemoSettings(); - const offerUrl = `${apiBaseUrl(env)}/verifiable-credentials/v1/issuance/credential-offers/${OFFER_ID}`; + const offerUrl = credentialOfferUrl(env); const offerUri = `openid-credential-offer://?credential_offer_uri=${encodeURIComponent(offerUrl)}`; const dcApiStatus = useSyncExternalStore( @@ -78,7 +70,7 @@ export function IssuanceCase() { setStatus(null); setIssuing(true); try { - const credentialOfferData = await parseCredentialOfferUri(offerUri); + const credentialOfferData = await fetchCredentialOffer(env); const creator = navigator.credentials as unknown as DigitalCredentialsCreator; await creator.create({ digital: { diff --git a/app/api/credential-offer/route.ts b/app/api/credential-offer/route.ts new file mode 100644 index 0000000..dab899f --- /dev/null +++ b/app/api/credential-offer/route.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from "next/server"; +import { + ENVIRONMENTS, + credentialOfferUrl, + type EnvironmentKey, +} from "@/app/lib/environments"; + +export const runtime = "nodejs"; + +export async function GET(request: NextRequest) { + const environmentKey = request.nextUrl.searchParams.get( + "environmentKey", + ) as EnvironmentKey | null; + + if (!environmentKey || !ENVIRONMENTS[environmentKey]) { + return Response.json({ error: "invalid environment" }, { status: 400 }); + } + + const offerUrl = credentialOfferUrl(environmentKey); + + try { + const response = await fetch(offerUrl); + if (!response.ok) { + return Response.json( + { + error: `Failed to fetch credential offer from ${offerUrl}: ${response.status}`, + }, + { status: 502 }, + ); + } + return Response.json(await response.json()); + } catch (error) { + return Response.json({ error: String(error) }, { status: 502 }); + } +} diff --git a/app/lib/environments.ts b/app/lib/environments.ts index 4d76f34..699b399 100644 --- a/app/lib/environments.ts +++ b/app/lib/environments.ts @@ -65,6 +65,11 @@ export const API_HOSTS: Record = { export const apiBaseUrl = (environmentKey: EnvironmentKey): string => API_HOSTS[ENVIRONMENTS[environmentKey].environment]; +export const CREDENTIAL_OFFER_ID = "00000000-0000-0000-0000-000000000001"; + +export const credentialOfferUrl = (environmentKey: EnvironmentKey): string => + `${apiBaseUrl(environmentKey)}/verifiable-credentials/v1/issuance/credential-offers/${CREDENTIAL_OFFER_ID}`; + const FALLBACK_ORIGIN = "https://demo.next.proof.com"; export const callbackURI = (