diff --git a/app/features/accounts/account-repository.ts b/app/features/accounts/account-repository.ts index e0ecbe97e..7a7eae95c 100644 --- a/app/features/accounts/account-repository.ts +++ b/app/features/accounts/account-repository.ts @@ -18,6 +18,7 @@ import { mintKeysQuery, seedQuery, } from '../shared/cashu'; +import { cashuAuthStore } from '../shared/cashu-mint-authentication'; import { useEncryption } from '../shared/encryption'; import type { Account } from './account'; @@ -208,12 +209,25 @@ export class AccountRepository { ); } + const getClearAuthToken = async () => { + const token = await cashuAuthStore + .getState() + .getClearAuthTokenWithRefresh(mintUrl); + if (!token) { + throw new Error(`No clear auth token available for mint ${mintUrl}`); + } + return token; + }; + const wallet = getExtendedCashuWallet(mintUrl, { unit: getCashuUnit(currency), bip39seed: seed ?? undefined, mintInfo, keys: activeKeysForUnit, keysets: unitKeysets, + getClearAuthToken, + getBlindAuthToken: async () => + cashuAuthStore.getState().getAndConsumeBlindAuthToken(mintUrl), }); // The constructor does not set the keysetId, so we need to set it manually diff --git a/app/features/receive/receive-cashu-token-hooks.tsx b/app/features/receive/receive-cashu-token-hooks.tsx index 3a8e1be58..f529a6879 100644 --- a/app/features/receive/receive-cashu-token-hooks.tsx +++ b/app/features/receive/receive-cashu-token-hooks.tsx @@ -171,6 +171,10 @@ export function useCashuTokenWithClaimableProofs({ cashuPubKey ? [cashuPubKey] : [], ); + // TODO: to determine if we can claim the token, we will need to check the auth requirements. + // Check if the swapping, or any melt operations require auth + // Then when receiving the token we will need to redirect the user to login to the mint if they don't have an auth session + return claimableProofs ? { claimableToken: { ...token, proofs: claimableProofs }, diff --git a/app/features/receive/receive-cashu.tsx b/app/features/receive/receive-cashu.tsx index 7675872ff..528077bc8 100644 --- a/app/features/receive/receive-cashu.tsx +++ b/app/features/receive/receive-cashu.tsx @@ -44,6 +44,10 @@ function MintQuoteCarouselItem({ error, } = useCreateCashuReceiveQuote(); + // TODO: we should not allow the user to create a quote if any minting endpoints require auth. + // For example, if creating a quote does not require auth, but minting does, we should not allow the user to create a quote + // because then they will pay the quote but be unable to mint. + const { quote, status: quotePaymentStatus } = useCashuReceiveQuote({ quoteId: createdQuote?.id, onPaid: onPaid, diff --git a/app/features/settings/accounts/add-mint-form.tsx b/app/features/settings/accounts/add-mint-form.tsx index 3d5d99115..d93b98e02 100644 --- a/app/features/settings/accounts/add-mint-form.tsx +++ b/app/features/settings/accounts/add-mint-form.tsx @@ -17,6 +17,7 @@ import { cashuMintValidator, mintInfoQuery, } from '~/features/shared/cashu'; +import { cashuAuthStore } from '~/features/shared/cashu-mint-authentication'; import { useUser } from '~/features/user/user-hooks'; import { useToast } from '~/hooks/use-toast'; import { getCashuProtocolUnit } from '~/lib/cashu'; @@ -54,6 +55,7 @@ export function AddMintForm() { const defaultCurrency = useUser((u) => u.defaultCurrency); const location = useLocation(); const queryClient = useQueryClient(); + const { startOidcFlow, sessions: clearAuthSessions } = cashuAuthStore(); const { register, @@ -68,6 +70,16 @@ export function AddMintForm() { }); const onSubmit = async (data: FormValues) => { + const mintInfo = await queryClient.fetchQuery(mintInfoQuery(data.mintUrl)); + if (mintInfo.isSupported(21)) { + if (!clearAuthSessions[data.mintUrl]) { + // TODO: we should prompt the user before starting the oidc flow + // TODO: we should also maintain the form values so when we redirect back to here we can auto-submit the form + startOidcFlow(data.mintUrl); + return; + } + } + try { await addAccount({ name: data.name, diff --git a/app/features/shared/cashu-mint-authentication.ts b/app/features/shared/cashu-mint-authentication.ts new file mode 100644 index 000000000..b13585439 --- /dev/null +++ b/app/features/shared/cashu-mint-authentication.ts @@ -0,0 +1,233 @@ +import { + CashuAuthMint, + CashuAuthWallet, + type Proof, + getEncodedAuthToken, +} from '@cashu/cashu-ts'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { + completeOidcFlow, + prepareOidcFlow, + refreshOidcSession, +} from '~/lib/oidc-client'; +import { getQueryClient } from '~/query-client'; +import { mintInfoQuery } from './cashu'; + +/** + * Clear Authentication Session for NUT-21 + * Contains OAuth 2.0 tokens and metadata from an OIDC provider + */ +export type ClearAuthSession = { + /** The access token (JWT) */ + accessToken: string; + /** The refresh token for obtaining new access tokens */ + refreshToken?: string; + /** When the access token expires (timestamp in seconds) */ + expiresAt?: number; + /** The mint URL this token is associated with */ + mintUrl: string; + /** OIDC authority that issued the token */ + authority: string; + /** Client ID used for authentication */ + clientId: string; +}; + +type CashuAuthStoreState = { + /** + * A map of mint URLs to their associated authentication sessions. + * The key is the mint URL, and the value is the authentication session. + */ + sessions: Record; + blindAuthTokens: Record; +}; + +type CashuAuthStoreActions = { + startOidcFlow: (mintUrl: string) => Promise; + completeOidcFlow: (callbackUrl: string) => Promise; + getClearAuthTokenWithRefresh: ( + mintUrl: string, + ) => Promise; + getAndConsumeBlindAuthToken: (mintUrl: string) => Promise; + topUpBlindAuthTokens: (mintUrl: string) => Promise; +}; + +export type CashuAuthStore = CashuAuthStoreState & CashuAuthStoreActions; + +export const cashuAuthStore = create()( + persist( + (set, get) => ({ + sessions: {}, + blindAuthTokens: {}, + startOidcFlow: async (mintUrl) => { + // TODO: casn we inject the query client here? + const mintInfo = await getQueryClient().fetchQuery( + mintInfoQuery(mintUrl), + ); + + const nut21 = mintInfo.isSupported(21); + if (!nut21.supported) { + throw new Error( + 'Mint does not support clear authentication via NUT-21', + ); + } + + const alreadyHasSession = mintUrl in get().sessions; + if (alreadyHasSession) { + throw new Error('Session already exists for this mint'); + } + + const authUrl = await prepareOidcFlow({ + openIdDiscoveryUrl: nut21.openid_discovery, + clientId: nut21.client_id, + redirectUri: `${window.location.origin}/oidc-callback`, + customStateData: { + mintUrl, + }, + }); + + // Redirect to the authorization URL + // TODO: should we instead just return the authUrl and handle redirect in the component? + window.location.href = authUrl.toString(); + }, + completeOidcFlow: async (callbackUrl) => { + const session = await completeOidcFlow(callbackUrl); + + const mintUrl = session.customStateData?.mintUrl; + if (!mintUrl || typeof mintUrl !== 'string') { + throw new Error('Mint URL not found in callback URL'); + } + + // Store the session with the mint URL + set((state) => ({ + sessions: { + ...state.sessions, + [mintUrl]: { + accessToken: session.accessToken, + refreshToken: session.refreshToken, + expiresAt: session.expiresAt, + authority: session.authority, + clientId: session.clientId, + mintUrl, + }, + }, + })); + }, + getClearAuthTokenWithRefresh: async (mintUrl) => { + const session = get().sessions[mintUrl]; + if (!session) { + // TODO: we should ask the user before starting the oidc flow + await get().startOidcFlow(mintUrl); + return undefined; + } + + if (!isSessionExpired(session)) { + return session.accessToken; + } + + if (!session.refreshToken) { + throw new Error( + `Clear auth session for mint ${mintUrl} is expired and no refresh token is available`, + ); + } + + try { + // TODO: what if this fails? It could fail if the user's auth session has been revoked completely. + const refreshedSession = await refreshOidcSession({ + authority: session.authority, + clientId: session.clientId, + refreshToken: session.refreshToken, + }); + + set((state) => ({ + sessions: { + ...state.sessions, + [mintUrl]: { + refreshToken: refreshedSession.refreshToken, + accessToken: refreshedSession.accessToken, + expiresAt: refreshedSession.expiresAt, + authority: session.authority, + clientId: session.clientId, + mintUrl, + }, + }, + })); + + return refreshedSession.accessToken; + } catch (error) { + console.error('Error refreshing clear auth token', error); + //clear session + set((state) => { + delete state.sessions[mintUrl]; + return state; + }); + throw error; + } + }, + topUpBlindAuthTokens: async (mintUrl) => { + const clearAuthToken = + await get().getClearAuthTokenWithRefresh(mintUrl); + if (!clearAuthToken) { + throw new Error(`No clear auth token available for mint ${mintUrl}`); + } + + const authWallet = new CashuAuthWallet(new CashuAuthMint(mintUrl)); + + // TODO: read bat_max_mint from mint info + const NUM_TOKENS_TO_MINT = 30; + const blindAuthTokens = await authWallet.mintProofs( + NUM_TOKENS_TO_MINT, + clearAuthToken, + ); + + set((state) => ({ + blindAuthTokens: { + ...state.blindAuthTokens, + [mintUrl]: blindAuthTokens, + }, + })); + }, + getAndConsumeBlindAuthToken: async (mintUrl) => { + let blindAuthTokens = get().blindAuthTokens[mintUrl] || []; + if (blindAuthTokens.length === 0) { + await get().topUpBlindAuthTokens(mintUrl); + // Get fresh state after topping up + blindAuthTokens = get().blindAuthTokens[mintUrl] || []; + } + + if (blindAuthTokens.length === 0) { + throw new Error(`No blind auth tokens available for mint ${mintUrl}`); + } + + // Get the token before removing it + const token = blindAuthTokens[0]; + + // Remove the consumed token from the store + set((state) => ({ + blindAuthTokens: { + ...state.blindAuthTokens, + [mintUrl]: blindAuthTokens.slice(1), + }, + })); + + return getEncodedAuthToken(token); + }, + }), + { + name: 'cashu-auth-store', + partialize: (state) => ({ + sessions: state.sessions, + blindAuthTokens: state.blindAuthTokens, + }), + }, + ), +); + +const isSessionExpired = (session: ClearAuthSession) => { + if (!session.expiresAt) { + return false; + } + const now = Date.now() / 1000; // current time in seconds + const bufferTime = 5; // 5 seconds buffer time + return session.expiresAt < now + bufferTime; +}; diff --git a/app/lib/cashu/utils.ts b/app/lib/cashu/utils.ts index 7b20f3d9c..5f4eeabe1 100644 --- a/app/lib/cashu/utils.ts +++ b/app/lib/cashu/utils.ts @@ -201,17 +201,22 @@ export const getExtendedCashuWallet = ( > & { unit?: CurrencyUnit; bip39seed: Uint8Array; + getClearAuthToken?: () => Promise; + getBlindAuthToken?: () => Promise; }, ) => { - const { unit, ...rest } = options; + const { unit, getClearAuthToken, getBlindAuthToken, ...rest } = options; // Cashu calls the unit 'usd' even though the amount is in cents. // To avoid this confusion we use 'cent' everywhere and then here we switch the value to 'usd' before creating the Cashu wallet. const cashuUnit = unit === 'cent' ? 'usd' : unit; - return new ExtendedCashuWallet(new CashuMint(mintUrl), { - ...rest, - unit: cashuUnit, - bip39seed: options.bip39seed, - }); + return new ExtendedCashuWallet( + new CashuMint(mintUrl, undefined, getBlindAuthToken, getClearAuthToken), + { + ...rest, + unit: cashuUnit, + bip39seed: options.bip39seed, + }, + ); }; export const getCashuWallet = ( diff --git a/app/lib/oidc-client.ts b/app/lib/oidc-client.ts new file mode 100644 index 000000000..7bdcbe684 --- /dev/null +++ b/app/lib/oidc-client.ts @@ -0,0 +1,333 @@ +import { jwtDecode } from 'jwt-decode'; +import ky from 'ky'; + +/** + * OIDC Discovery Configuration Response + */ +type OidcConfiguration = { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint?: string; + jwks_uri?: string; + scopes_supported?: string[]; + response_types_supported?: string[]; + grant_types_supported?: string[]; + code_challenge_methods_supported?: string[]; + [key: string]: unknown; +}; + +/** + * OAuth 2.0 Token Response + */ +type TokenResponse = { + access_token: string; + token_type: string; + expires_in?: number; + refresh_token?: string; + scope?: string; + [key: string]: unknown; +}; + +/** + * Build authorization URL and store session data + * @returns the authorization URL to redirect to + */ +export async function prepareOidcFlow({ + openIdDiscoveryUrl, + clientId, + redirectUri, + scope = 'openid profile', + customStateData, +}: { + openIdDiscoveryUrl: string; + clientId: string; + redirectUri: string; + scope?: string; + customStateData?: Record; +}): Promise { + const oidcConfig = await ky.get(openIdDiscoveryUrl).json(); + + if (!oidcConfig.authorization_endpoint) { + throw new Error('Authorization endpoint not found in OIDC configuration'); + } + + // Generate security parameters + const securityState = generateRandomString(); + const { codeVerifier, codeChallenge } = await generatePKCE(); + + const stateData = { + securityState, + ...customStateData, + }; + const encodedState = btoa(JSON.stringify(stateData)); + + // Store only the security parameters that need to persist + sessionStorage.setItem('oidc_security_state', securityState); + sessionStorage.setItem('oidc_code_verifier', codeVerifier); + + // Store the OIDC configuration for later use when completing the flow + sessionStorage.setItem('oidc_authority', oidcConfig.issuer); + sessionStorage.setItem('oidc_client_id', clientId); + sessionStorage.setItem('oidc_redirect_uri', redirectUri); + + // Build authorization URL + const authUrl = new URL(oidcConfig.authorization_endpoint); + authUrl.searchParams.set('response_type', 'code'); + authUrl.searchParams.set('client_id', clientId); + authUrl.searchParams.set('redirect_uri', redirectUri); + authUrl.searchParams.set('scope', scope); + authUrl.searchParams.set('state', encodedState); + authUrl.searchParams.set('code_challenge', codeChallenge); + authUrl.searchParams.set('code_challenge_method', 'S256'); + + return authUrl; +} + +/** + * Complete the authorization flow by exchanging code for tokens + * @param callbackUrl - The current URL after the OIDC callback + */ +export async function completeOidcFlow(callbackUrl: string): Promise<{ + accessToken: string; + authority: string; + clientId: string; + refreshToken?: string; + expiresAt?: number; + customStateData?: Record; +}> { + // Retrieve the security parameters from storage + const storedSecurityState = sessionStorage.getItem('oidc_security_state'); + const codeVerifier = sessionStorage.getItem('oidc_code_verifier'); + + // Retrieve the OIDC configuration + const authority = sessionStorage.getItem('oidc_authority'); + const clientId = sessionStorage.getItem('oidc_client_id'); + const redirectUri = sessionStorage.getItem('oidc_redirect_uri'); + + try { + if ( + !storedSecurityState || + !codeVerifier || + !authority || + !clientId || + !redirectUri + ) { + throw new Error('OIDC security parameters missing from storage'); + } + + // Parse callback URL parameters + const url = new URL(callbackUrl); + const urlParams = url.searchParams; + const code = urlParams.get('code'); + const encodedCallbackState = urlParams.get('state'); + const error = urlParams.get('error'); + const errorDescription = urlParams.get('error_description'); + + // Handle authorization errors + if (error) { + const message = errorDescription || error; + throw new Error(`OIDC authorization error: ${message}`); + } + + if (!code) { + throw new Error('Authorization code not found in callback URL'); + } + + // Decode and verify state parameter (CSRF protection) + if (!encodedCallbackState) { + throw new Error('State parameter missing from callback'); + } + + let decodedState: { securityState: string; [key: string]: unknown }; + try { + decodedState = JSON.parse(atob(encodedCallbackState)); + } catch { + throw new Error('Invalid state parameter format'); + } + + if (decodedState.securityState !== storedSecurityState) { + throw new Error('State parameter mismatch - possible CSRF attack'); + } + + // Extract custom state data (excluding the security state) + const { securityState, ...customStateData } = decodedState; + + const tokenEndpoint = await getTokenEndpoint(authority); + + // Exchange authorization code for tokens + const tokenRequestBody = new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + redirect_uri: redirectUri, + client_id: clientId, + code_verifier: codeVerifier, + }); + + const tokenData = await ky + .post(tokenEndpoint, { + body: tokenRequestBody, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .json(); + + if (!tokenData.access_token) { + throw new Error('Access token not received from token endpoint'); + } + + // Calculate expiration time + let expiresAt: number | undefined; + + // Keycloak is returning expires_in as 0 even the the JWT itslef has an expiration + // TODO: make sure correct expiration is used + if (tokenData.expires_in && tokenData.expires_in > 0) { + expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in; + } else { + const jwtExpiration = jwtDecode(tokenData.access_token).exp; + expiresAt = jwtExpiration || undefined; + } + + // Clean up session storage + clearSessionData(); + + return { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + authority, + clientId, + expiresAt, + customStateData, + }; + } catch (error) { + clearSessionData(); + throw error; + } +} + +/** + * Refresh an access token using a refresh token + * Returns updated token information + */ +export async function refreshOidcSession({ + authority, + clientId, + refreshToken: refreshTokenValue, +}: { + authority: string; + clientId: string; + refreshToken: string; +}): Promise<{ + accessToken: string; + refreshToken?: string; + expiresAt?: number; +}> { + if (!refreshTokenValue) { + throw new Error('No refresh token provided'); + } + + const tokenEndpoint = await getTokenEndpoint(authority); + + // Prepare refresh token request + const tokenRequestBody = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshTokenValue, + client_id: clientId, + }); + + const tokenData = await ky + .post(tokenEndpoint, { + body: tokenRequestBody, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .json(); + + if (!tokenData.access_token) { + throw new Error('Access token not received from token endpoint'); + } + + // Calculate expiration time + let expiresAt: number | undefined; + + // Keycloak is returning expires_in as 0 even the the JWT itslef has an expiration + // TODO: make sure correct expiration is used + if (tokenData.expires_in && tokenData.expires_in > 0) { + expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in; + } else { + const jwtExpiration = jwtDecode(tokenData.access_token).exp; + expiresAt = jwtExpiration || undefined; + } + + return { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresAt, + }; +} + +/** + * Generate a cryptographically secure random string + */ +function generateRandomString(length = 32): string { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join( + '', + ); +} + +/** + * Generate base64url-encoded string from array buffer + */ +function base64urlEncode(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let str = ''; + for (const byte of bytes) { + str += String.fromCharCode(byte); + } + return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +/** + * Generate PKCE code verifier and challenge for enhanced security + * According to RFC 7636, code verifier must be 43-128 characters long + * and use unreserved characters: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + */ +async function generatePKCE(): Promise<{ + codeVerifier: string; + codeChallenge: string; +}> { + // Generate 32 random bytes and encode as base64url (43 characters) + const array = new Uint8Array(32); + crypto.getRandomValues(array); + const codeVerifier = base64urlEncode(array.buffer); + + // Create code challenge using SHA256 + const encoder = new TextEncoder(); + const data = encoder.encode(codeVerifier); + const digest = await crypto.subtle.digest('SHA-256', data); + const codeChallenge = base64urlEncode(digest); + + return { codeVerifier, codeChallenge }; +} + +async function getTokenEndpoint(authority: string): Promise { + const oidcConfig = await ky + .get(`${authority}/.well-known/openid-configuration`) + .json(); + return oidcConfig.token_endpoint; +} + +/** + * Clear OIDC session data from storage + */ +function clearSessionData(): void { + sessionStorage.removeItem('oidc_security_state'); + sessionStorage.removeItem('oidc_code_verifier'); + sessionStorage.removeItem('oidc_authority'); + sessionStorage.removeItem('oidc_client_id'); + sessionStorage.removeItem('oidc_redirect_uri'); +} diff --git a/app/routes/_protected.oidc-callback.tsx b/app/routes/_protected.oidc-callback.tsx new file mode 100644 index 000000000..a5a879604 --- /dev/null +++ b/app/routes/_protected.oidc-callback.tsx @@ -0,0 +1,32 @@ +import { type ClientLoaderFunctionArgs, redirect } from 'react-router'; +import { LoadingScreen } from '~/features/loading/LoadingScreen'; +import { cashuAuthStore } from '~/features/shared/cashu-mint-authentication'; + +/** + * Client loader that handles OIDC callback after the user has authenticated with the auth provider. + * It completes the auth flow by exchanging the code for tokens, stores the tokens, then + * redirects to the home page. + */ +export async function clientLoader({ request }: ClientLoaderFunctionArgs) { + const url = new URL(request.url); + const callbackUrl = url.href; + + const { completeOidcFlow } = cashuAuthStore.getState(); + await completeOidcFlow(callbackUrl); + console.log('oidc-callback completed successfully'); + + // TODO: starting the oidc flow should save the user's current location and redirect back to that page after the oidc flow is complete + + // Redirect to home page after successful authentication + throw redirect('/'); +} + +clientLoader.hydrate = true as const; + +export function HydrateFallback() { + return ; +} + +export default function OidcCallback() { + return null; +} diff --git a/bun.lock b/bun.lock index 76bf8da31..36e357f67 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,7 @@ "": { "name": "agicash", "dependencies": { - "@cashu/cashu-ts": "2.6.0", + "@cashu/cashu-ts": "github:gudnuf/cashu-ts#improve-auth-support-with-build-artifacts-3", "@cashu/crypto": "0.3.4", "@jbojcic/bc-ur": "https://github.com/jbojcic1/bc-ur", "@noble/ciphers": "1.3.0", @@ -171,7 +171,7 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@cashu/cashu-ts": ["@cashu/cashu-ts@2.6.0", "", { "dependencies": { "@noble/curves": "^1.9.5", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "buffer": "^6.0.3" } }, "sha512-yDX5hCje8VeKnByVxeqSHlTpKi1sxviFZ+JJva4mCmV8pA4/t4/W1RPYRbL89z8hySXFHdQWRZd4+2SHZM8iIw=="], + "@cashu/cashu-ts": ["@cashu/cashu-ts@github:gudnuf/cashu-ts#6e0a28a", { "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "buffer": "^6.0.3" } }, "gudnuf-cashu-ts-6e0a28a"], "@cashu/crypto": ["@cashu/crypto@0.3.4", "", { "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA=="], diff --git a/devenv b/devenv new file mode 100644 index 000000000..e69de29bb diff --git a/devenv.nix b/devenv.nix index 8cd3a129f..482f5f552 100644 --- a/devenv.nix +++ b/devenv.nix @@ -3,7 +3,7 @@ { # CDK repository configuration env.CDK_REPO = "https://github.com/cashubtc/cdk.git"; - env.CDK_REF = "aa624d3afd739a82aa31dfde2632480934004fc2"; # Can be branch, tag, or commit hash + env.CDK_REF = "a4aaef705ed1f0cda1f2fe970b4600d203ffd29b"; # Can be branch, tag, or commit hash # https://devenv.sh/packages/ packages = [ diff --git a/package.json b/package.json index 68e940408..b591f2442 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "start": "We are running the build in node because atm we are using node as the build target." }, "dependencies": { - "@cashu/cashu-ts": "2.6.0", + "@cashu/cashu-ts": "github:gudnuf/cashu-ts#improve-auth-support-with-build-artifacts-3", "@cashu/crypto": "0.3.4", "@jbojcic/bc-ur": "https://github.com/jbojcic1/bc-ur", "@noble/ciphers": "1.3.0", diff --git a/tools/devenv/cdk/cdk-mint.config.toml b/tools/devenv/cdk/cdk-mint.config.toml index b8ac353fb..4f2503038 100644 --- a/tools/devenv/cdk/cdk-mint.config.toml +++ b/tools/devenv/cdk/cdk-mint.config.toml @@ -49,14 +49,14 @@ min_delay_time = 1 max_delay_time = 3 [auth] -auth_enabled = false +auth_enabled = true openid_discovery = "http://127.0.0.1:8080/realms/cdk-test-realm/.well-known/openid-configuration" openid_client_id = "cashu-client" mint_max_bat=50 # Authentication settings for endpoints # Options: "clear", "blind", "none" (none = disabled) -mint = "blind" +mint = "clear" get_mint_quote = "none" check_mint_quote = "none"