@stacknet/userutils 0.3.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import {createHmac,timingSafeEqual,randomBytes,createHash}from'crypto';function G(e){return Buffer.from(e).toString("base64url")}function ee(e){return Buffer.from(e,"base64url").toString()}function v(e){try{let r=e.split(".");return r.length!==3?null:JSON.parse(ee(r[1]))}catch{return null}}function $(e,r){let t=G(JSON.stringify({alg:"HS256",typ:"JWT"})),n=G(JSON.stringify(e)),o=createHmac("sha256",r).update(`${t}.${n}`).digest("base64url");return `${t}.${n}.${o}`}function K(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[n,o,s]=t,i=createHmac("sha256",r).update(`${n}.${o}`).digest("base64url"),y=Buffer.from(s),m=Buffer.from(i);return y.length!==m.length?!1:timingSafeEqual(y,m)}catch{return false}}function M(e,r){if(!K(e,r))return null;let t=v(e);return !t||t.exp&&t.exp<Math.floor(Date.now()/1e3)?null:t}function D(e,r,t=900,n=300){let o=M(e,r);return !o?.exp||o.exp*1e3-Date.now()>n*1e3?null:$({...o,exp:Math.floor(Date.now()/1e3)+t},r)}function W(e=32){return randomBytes(e).toString("hex")}function O(e){return e.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||e.headers.get("x-real-ip")||"unknown"}var re="__csrf",se="x-csrf-token";function j(e={}){let r=e.cookieName||re,t=e.headerName||se,n=e.tokenLength||32,o=e.secure!==false;return {generateToken(s){let i=W(n),y=[`${r}=${i}`,"Path=/","SameSite=Lax"];return o&&y.push("Secure"),s.append("Set-Cookie",y.join("; ")),i},validateRequest(s){let i=s.headers.get("cookie");if(!i)return {valid:false,error:"No cookies present"};let y=i.split(";").map(d=>d.trim()).find(d=>d.startsWith(`${r}=`))?.slice(r.length+1);if(!y)return {valid:false,error:"CSRF cookie missing"};let m=s.headers.get(t);if(!m)return {valid:false,error:"CSRF header missing"};try{let d=Buffer.from(y),l=Buffer.from(m);return d.length!==l.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(d,l)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function C(e){let r=new Map,t=setInterval(()=>{let n=Date.now();for(let[o,s]of r)n>=s.resetAt&&r.delete(o);},6e4);return typeof t=="object"&&"unref"in t&&t.unref(),{async check(n){let o=Date.now(),s=r.get(n);return (!s||o>=s.resetAt)&&(s={count:0,resetAt:o+e.windowMs},r.set(n,s)),s.count++,s.count>e.maxRequests?{allowed:false,remaining:0,retryAfter:Math.ceil((s.resetAt-o)/1e3)}:{allowed:true,remaining:e.maxRequests-s.count}}}}function oe(){let e=new Map,r=setInterval(()=>{let t=Date.now();for(let[n,o]of e)t>=o&&e.delete(n);},6e4);return typeof r=="object"&&"unref"in r&&r.unref(),{async has(t){let n=e.get(t);return n?Date.now()>=n?(e.delete(t),false):true:false},async set(t,n){e.set(t,Date.now()+n*1e3);}}}function ne(e,r){let t=r?.rateLimiter||C({maxRequests:10,windowMs:6e4}),n=j({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,s=e.sessionMaxAge||604800;e.stacknetJwtSecret||e.authSecret;return async function(m){let d=O(m),l=await t.check(`auth:${d}`);if(!l.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(l.retryAfter||60)}});let a;try{a=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:c,message:u,signature:p,publicKey:f,otp:h,code:g,redirectUrl:k,stackId:w}=a,x=w||e.stackId,R;if(c&&u&&p){let Y={"Content-Type":"application/json"},_=await fetch(`${e.stacknetUrl}/api/v2/stacks/${x}/auth/web3/verify`,{method:"POST",headers:Y,body:JSON.stringify({chain:c,message:u,signature:p,public_key:f}),signal:AbortSignal.timeout(1e4)});if(!_.ok){let q=await _.json().catch(()=>({})),U=q?.error?.message||q?.message||q?.error||`StackNet returned ${_.status}`;return console.error(`[auth-callback] Verify failed: ${_.status}`,U),Response.json({error:"Wallet verification failed",detail:typeof U=="string"?U:void 0},{status:401})}let N=await _.json();R=N.data?.session||N.session||N.data||N,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(R||{}).join(", ")}`);}else return h||g?Response.json({error:"Use /api/auth/otp for OTP verification"},{status:400}):Response.json({error:"Provide wallet signature or OTP code"},{status:400});if(!R?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let S=JSON.parse(Buffer.from(R.jwt.split(".")[1],"base64url").toString()),b=Math.floor(Date.now()/1e3),T={...S,exp:b+o,iat:b},P=$(T,e.authSecret),J={userId:S.sub||S.user_id||S.session_id||S.global_id||"",address:R.address||S.address,chain:R.chain||c,expiresAt:Date.now()+s*1e3,authMethod:c?`web3:${c}`:"otp"},H=new Headers({"Content-Type":"application/json"}),L=e.secureCookies!==false?"; Secure":"",F=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";H.append("Set-Cookie",`stackauth_jwt=${P}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${L}${F}`);let z=Buffer.from(JSON.stringify(J)).toString("base64url");return H.append("Set-Cookie",`stackauth_session=${z}; Path=/; SameSite=Lax; Max-Age=${s}${L}${F}`),n.generateToken(H),new Response(JSON.stringify({user:J}),{status:200,headers:H})}}function X(e,r){if(!r)return null;try{let t=v(e);if(!t||t.exp&&t.exp<Math.floor(Date.now()/1e3))return null;let n=Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString("base64url"),o=Buffer.from(JSON.stringify(t)).toString("base64url"),s=createHmac("sha256",r).update(`${n}.${o}`).digest("base64url");return `${n}.${o}.${s}`}catch{return null}}function E(e,r){let t=X(e,r);return t?{Cookie:`stackauth_jwt=${t}`}:{Cookie:`stackauth_jwt=${e}`}}function A(e){let r=e.headers.get("cookie");if(r){let n=r.split(";").map(o=>o.trim()).find(o=>o.startsWith("stackauth_jwt="));if(n)return n.slice(14)}let t=e.headers.get("authorization");return t?.startsWith("Bearer ")?t.slice(7):null}function ie(e){return async function(t){let n=A(t);if(n){let y=v(n),m=y?.session_id||y?.sub;if(m)try{await fetch(`${e.stacknetUrl}/api/v2/sessions/${m}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});}catch{}}let o=e.secureCookies!==false?"; Secure":"",s=e.cookieDomain?`; Domain=${e.cookieDomain}`:"",i=new Headers({"Content-Type":"application/json"});return i.append("Set-Cookie",`stackauth_jwt=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${o}${s}`),i.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${o}${s}`),i.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${o}${s}`),new Response(JSON.stringify({success:true}),{status:200,headers:i})}}function ce(e){let r=e.jwtExpiry||900,t=e.sessionMaxAge||604800;return async function(o){let s=A(o);if(!s)return Response.json({session:null},{status:200});let i=M(s,e.authSecret);if(!i)return Response.json({session:null},{status:200});let m={userId:i.sub||i.user_id||i.session_id||i.global_id||"",address:i.address,chain:i.chain,expiresAt:i.session_expires_at||(i.exp?i.exp*1e3:Date.now()+t*1e3),planId:i.plan_id,authMethod:i.auth_method},d=new Headers({"Content-Type":"application/json"}),l=D(s,e.authSecret,r,300);if(l){let a=e.secureCookies!==false?"; Secure":"",c=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";d.append("Set-Cookie",`stackauth_jwt=${l}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${a}${c}`);}return new Response(JSON.stringify({session:m}),{status:200,headers:d})}}function pe(e,r){if(e.length!==r.length)return false;try{return timingSafeEqual(Buffer.from(e),Buffer.from(r))}catch{return false}}function de(e){let r=e.rateLimiter||C({maxRequests:5,windowMs:3e5}),t=j({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,o=e.sessionMaxAge||604800;return async function(i){let y=O(i),m=await r.check(`otp:${y}`);if(!m.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(m.retryAfter||300)}});let d;try{d=await i.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:l}=d;if(!l||typeof l!="string"||l.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!pe(l,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let a=Math.floor(Date.now()/1e3),u={sub:`otp:${createHash("sha256").update(`otp:${l}:${Date.now()}`).digest("hex").slice(0,32)}`,auth_method:"otp",iat:a,exp:a+n},p=$(u,e.authSecret),f={userId:u.sub,expiresAt:Date.now()+o*1e3,authMethod:"otp"},h=new Headers({"Content-Type":"application/json"}),g=e.secureCookies!==false?"; Secure":"",k=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";h.append("Set-Cookie",`stackauth_jwt=${p}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${o}${g}${k}`);let w=Buffer.from(JSON.stringify(f)).toString("base64url");return h.append("Set-Cookie",`stackauth_session=${w}; Path=/; SameSite=Lax; Max-Age=${o}${g}${k}`),t.generateToken(h),new Response(JSON.stringify({success:true,data:{user:f}}),{status:200,headers:h})}}function fe(e,r){let t=r?.rateLimiter||C({maxRequests:10,windowMs:6e4}),n=j({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,s=e.sessionMaxAge||604800;async function i(m){let d=new URL(m.url),l=d.searchParams.get("provider"),a=d.searchParams.get("redirectUri")||d.searchParams.get("redirect_uri"),c=d.searchParams.get("stackId")||e.stackId;if(!l)return Response.json({error:"Missing provider parameter"},{status:400});if(!a)return Response.json({error:"Missing redirectUri parameter"},{status:400});try{let u=await fetch(`${e.stacknetUrl}/api/v2/stacks/${c}/auth/oauth/${l}/initiate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({redirectUrl:a}),signal:AbortSignal.timeout(1e4)});if(!u.ok){let h=await u.json().catch(()=>({}));return Response.json({error:h.error?.message||`Failed to start OAuth flow: ${u.statusText}`},{status:u.status})}let p=await u.json(),f=p.data||p;return Response.json({redirect_url:f.url,state:f.state})}catch(u){return Response.json({error:u.message||"Failed to start OAuth flow"},{status:500})}}async function y(m){let d=O(m),l=await t.check(`oauth:${d}`);if(!l.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(l.retryAfter||60)}});let a;try{a=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{provider:c,code:u,state:p,stackId:f}=a,h=f||e.stackId;if(!c||!u||!p)return Response.json({error:"Missing provider, code, or state"},{status:400});try{let g=await fetch(`${e.stacknetUrl}/api/v2/stacks/${h}/auth/oauth/${c}/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:u,state:p}),signal:AbortSignal.timeout(1e4)});if(!g.ok){let L=await g.json().catch(()=>({}));return Response.json({error:L.error?.message||`OAuth verification failed: ${g.statusText}`},{status:401})}let k=await g.json(),w=k.data?.session||k.session||k.data||k;if(!w?.jwt)return Response.json({error:"OAuth authentication failed \u2014 no session returned"},{status:401});let x=JSON.parse(Buffer.from(w.jwt.split(".")[1],"base64url").toString()),R=Math.floor(Date.now()/1e3),S=$({...x,exp:R+o,iat:R},e.authSecret),T={userId:x.sub||x.user_id||x.session_id||x.global_id||"",address:w.address||x.address,chain:void 0,expiresAt:Date.now()+s*1e3,authMethod:`oauth:${c}`},P=new Headers({"Content-Type":"application/json"}),I=e.secureCookies!==!1?"; Secure":"",J=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";P.append("Set-Cookie",`stackauth_jwt=${S}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${I}${J}`);let H=Buffer.from(JSON.stringify(T)).toString("base64url");return P.append("Set-Cookie",`stackauth_session=${H}; Path=/; SameSite=Lax; Max-Age=${s}${I}${J}`),n.generateToken(P),new Response(JSON.stringify({user:T}),{status:200,headers:P})}catch(g){return Response.json({error:g.message||"OAuth callback failed"},{status:500})}}return {startFlow:i,handleCallback:y}}function me(e,r){let t=r?.rateLimiter||C({maxRequests:10,windowMs:6e4}),n=j({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,s=e.sessionMaxAge||604800;return async function(y){let m=O(y),d=await t.check(`google-onetap:${m}`);if(!d.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(d.retryAfter||60)}});let l;try{l=await y.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{credential:a,stackId:c}=l,u=c||e.stackId;if(!a)return Response.json({error:"Missing credential"},{status:400});let p;try{let f=a.split(".");if(f.length!==3)throw new Error("Invalid JWT");p=JSON.parse(Buffer.from(f[1],"base64url").toString());}catch{return Response.json({error:"Invalid credential format"},{status:400})}try{let f=await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(a)}`,{signal:AbortSignal.timeout(1e4)});if(!f.ok)return Response.json({error:"Google credential verification failed"},{status:401});let h=await f.json();if(!h.sub||!h.email)return Response.json({error:"Invalid Google token \u2014 missing user info"},{status:401})}catch{return Response.json({error:"Failed to verify Google credential"},{status:500})}try{let f=await fetch(`${e.stacknetUrl}/api/v2/stacks/${u}/auth/oauth/google/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:a,google_id:p.sub,email:p.email,name:p.name,picture:p.picture,one_tap:!0}),signal:AbortSignal.timeout(1e4)});if(!f.ok){let k=Math.floor(Date.now()/1e3),w=p.sub,x=$({sub:w,global_id:`google:${w}`,stack_id:u,chain:"google",email:p.email,credentials:["oauth:google"],iat:k,exp:k+o,iss:"stackauth.network",signed_by:["local"]},e.authSecret),R={userId:w,address:p.email,chain:void 0,expiresAt:Date.now()+s*1e3,authMethod:"oauth:google"},S=new Headers({"Content-Type":"application/json"}),b=e.secureCookies!==!1?"; Secure":"",T=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";S.append("Set-Cookie",`stackauth_jwt=${x}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${b}${T}`);let P=Buffer.from(JSON.stringify(R)).toString("base64url");return S.append("Set-Cookie",`stackauth_session=${P}; Path=/; SameSite=Lax; Max-Age=${s}${b}${T}`),n.generateToken(S),new Response(JSON.stringify({user:R}),{status:200,headers:S})}let h=await f.json(),g=h.data?.session||h.session||h.data||h;if(g?.jwt){let k=JSON.parse(Buffer.from(g.jwt.split(".")[1],"base64url").toString()),w=Math.floor(Date.now()/1e3),x=$({...k,exp:w+o,iat:w},e.authSecret),S={userId:k.sub||k.user_id||p.sub,address:p.email||g.address,chain:void 0,expiresAt:Date.now()+s*1e3,authMethod:"oauth:google"},b=new Headers({"Content-Type":"application/json"}),T=e.secureCookies!==!1?"; Secure":"",P=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";b.append("Set-Cookie",`stackauth_jwt=${x}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${T}${P}`);let I=Buffer.from(JSON.stringify(S)).toString("base64url");return b.append("Set-Cookie",`stackauth_session=${I}; Path=/; SameSite=Lax; Max-Age=${s}${T}${P}`),n.generateToken(b),new Response(JSON.stringify({user:S}),{status:200,headers:b})}return Response.json({error:"No session returned"},{status:401})}catch(f){return Response.json({error:f.message||"Google One Tap authentication failed"},{status:500})}}}function ye(e){let r=j({secure:e.secureCookies!==false}),t=e.rateLimiter||C({maxRequests:20,windowMs:6e4}),n=e.stacknetJwtSecret||e.authSecret,o=e.jwtExpiry||900,s=e.sessionMaxAge||604800;function i(a){let c=A(a);if(!c)return null;let u=M(c,e.authSecret);return u?{jwt:c,payload:u}:null}function y(a,c){let u=D(a,e.authSecret,o,300);if(u){let p=e.secureCookies!==false?"; Secure":"",f=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";c.append("Set-Cookie",`stackauth_jwt=${u}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${p}${f}`);}}async function m(a,c){let u=i(a);if(!u)return Response.json({error:"Unauthorized"},{status:401});let p=E(u.jwt,n),f=await fetch(`${e.stacknetUrl}${c}`,{headers:p,signal:AbortSignal.timeout(15e3)}),h=await f.json().catch(()=>({})),g=new Headers({"Content-Type":"application/json"});return y(u.jwt,g),new Response(JSON.stringify(h),{status:f.status,headers:g})}async function d(a,c,u){let p=i(a);if(!p)return Response.json({error:"Unauthorized"},{status:401});let f=r.validateRequest(a);if(!f.valid)return Response.json({error:f.error||"CSRF validation failed"},{status:403});let h=p.payload.sub||p.payload.user_id||"unknown";if(!(await t.check(`billing:${h}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let k=await a.json().catch(()=>({})),w=E(p.jwt,n);w["Content-Type"]="application/json";let x=await fetch(`${e.stacknetUrl}${c}`,{method:"POST",headers:w,body:JSON.stringify({...k,...u}),signal:AbortSignal.timeout(15e3)}),R=await x.json().catch(()=>({})),S=new Headers({"Content-Type":"application/json"});return y(p.jwt,S),new Response(JSON.stringify(R),{status:x.status,headers:S})}let l=`/api/v2/stacks/${e.stackId}`;return {plans:{GET:async a=>{let c=await fetch(`${e.stacknetUrl}${l}/plans`,{signal:AbortSignal.timeout(1e4)}),u=await c.json().catch(()=>({}));return Response.json(u,{status:c.status})}},subscription:{GET:(a=>m(a,`${l}/subscription`))},subscribe:{POST:(a=>{let c=new URL(a.url).origin;return d(a,`${l}/subscribe`,{successUrl:`${c}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing`})})},cancel:{POST:(a=>d(a,`${l}/cancel-subscription`))},usage:{GET:(a=>m(a,"/v1/account/usage"))},history:{GET:(a=>m(a,`${l}/billing`))},prepaid:{POST:(a=>{let c=new URL(a.url).origin;return d(a,`${l}/prepaid`,{successUrl:`${c}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing/prepaid`})})},verifyPrepaid:{POST:(a=>d(a,`${l}/verify-prepaid`))},verifySession:{POST:(a=>d(a,`${l}/verify-session`))}}}function he(e){return async function(t){let n=t.headers.get("stripe-signature");if(!n)return Response.json({error:"Missing Stripe signature"},{status:400});try{let o=await t.text(),s=await fetch(`${e.stacknetUrl}/api/v2/stacks/${e.stackId}/webhook/stripe`,{method:"POST",headers:{"Content-Type":"application/json","stripe-signature":n},body:o,signal:AbortSignal.timeout(1e4)}),i=await s.json().catch(()=>({received:!0}));return Response.json(i,{status:s.status})}catch{return Response.json({error:"Webhook processing failed"},{status:502})}}}function B(){return {"Strict-Transport-Security":"max-age=63072000; includeSubDomains; preload","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"0","Referrer-Policy":"strict-origin-when-cross-origin","Permissions-Policy":"camera=(), microphone=(), geolocation=()"}}function ge(e){return async r=>{let t=await e(r),n=B(),o=new Headers(t.headers);for(let[s,i]of Object.entries(n))o.set(s,i);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:o})}}function Se(){return Object.entries(B()).map(([e,r])=>({key:e,value:r}))}
2
- export{E as buildStackNetHeaders,ne as createAuthCallback,ye as createBillingProxy,j as createCSRFProtection,me as createGoogleOneTapHandler,C as createInMemoryRateLimiter,oe as createInMemoryReplayStore,ie as createLogoutHandler,fe as createOAuthHandlers,de as createOTPHandler,ce as createSessionHandler,he as createWebhookHandler,v as decodeJWTPayload,O as extractIP,A as extractJwt,W as generateToken,D as maybeRefreshJWT,Se as nextSecurityHeaders,X as resignForStackNet,B as securityHeaders,$ as signJWT,M as verifyJWT,K as verifyJWTSignature,ge as withSecurityHeaders};
1
+ import {createHmac,timingSafeEqual,randomBytes,createHash}from'crypto';function G(e){return Buffer.from(e).toString("base64url")}function te(e){return Buffer.from(e,"base64url").toString()}function E(e){try{let r=e.split(".");return r.length!==3?null:JSON.parse(te(r[1]))}catch{return null}}function P(e,r){let t=G(JSON.stringify({alg:"HS256",typ:"JWT"})),o=G(JSON.stringify(e)),n=createHmac("sha256",r).update(`${t}.${o}`).digest("base64url");return `${t}.${o}.${n}`}function V(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[o,n,a]=t,i=createHmac("sha256",r).update(`${o}.${n}`).digest("base64url"),m=Buffer.from(a),f=Buffer.from(i);return m.length!==f.length?!1:timingSafeEqual(m,f)}catch{return false}}function _(e,r){if(!V(e,r))return null;let t=E(e);return !t||t.exp&&t.exp<Math.floor(Date.now()/1e3)?null:t}function D(e,r,t=900,o=300){let n=_(e,r);return !n?.exp||n.exp*1e3-Date.now()>o*1e3?null:P({...n,exp:Math.floor(Date.now()/1e3)+t},r)}function B(e=32){return randomBytes(e).toString("hex")}function I(e){return e.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||e.headers.get("x-real-ip")||"unknown"}var se="__csrf",oe="x-csrf-token",ne=/^[A-Za-z_$][A-Za-z0-9_$-]{0,63}$/,ae=/^[A-Za-z][A-Za-z0-9-]{0,63}$/;function j(e={}){let r=e.cookieName||se,t=e.headerName||oe,o=e.tokenLength||32,n=e.secure!==false;if(!ne.test(r))throw new Error(`createCSRFProtection: invalid cookieName "${r}"`);if(!ae.test(t))throw new Error(`createCSRFProtection: invalid headerName "${t}"`);if(o<16||o>128)throw new Error("createCSRFProtection: tokenLength must be between 16 and 128 bytes");return {generateToken(a){let i=B(o),m=[`${r}=${i}`,"Path=/","SameSite=Lax"];return n&&m.push("Secure"),a.append("Set-Cookie",m.join("; ")),i},validateRequest(a){let i=a.headers.get("cookie");if(!i)return {valid:false,error:"No cookies present"};let m=i.split(";").map(p=>p.trim()).find(p=>p.startsWith(`${r}=`))?.slice(r.length+1);if(!m)return {valid:false,error:"CSRF cookie missing"};let f=a.headers.get(t);if(!f)return {valid:false,error:"CSRF header missing"};try{let p=Buffer.from(m),l=Buffer.from(f);return p.length!==l.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(p,l)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function T(e){let r=new Map,t=setInterval(()=>{let o=Date.now();for(let[n,a]of r)o>=a.resetAt&&r.delete(n);},6e4);return typeof t=="object"&&"unref"in t&&t.unref(),{async check(o){let n=Date.now(),a=r.get(o);return (!a||n>=a.resetAt)&&(a={count:0,resetAt:n+e.windowMs},r.set(o,a)),a.count++,a.count>e.maxRequests?{allowed:false,remaining:0,retryAfter:Math.ceil((a.resetAt-n)/1e3)}:{allowed:true,remaining:e.maxRequests-a.count}}}}function ie(){let e=new Map,r=setInterval(()=>{let t=Date.now();for(let[o,n]of e)t>=n&&e.delete(o);},6e4);return typeof r=="object"&&"unref"in r&&r.unref(),{async has(t){let o=e.get(t);return o?Date.now()>=o?(e.delete(t),false):true:false},async set(t,o){e.set(t,Date.now()+o*1e3);}}}function ce(e,r){let t=r?.rateLimiter||T({maxRequests:10,windowMs:6e4}),o=j({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800;e.stacknetJwtSecret||e.authSecret;return async function(f){let p=I(f),l=await t.check(`auth:${p}`);if(!l.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(l.retryAfter||60)}});let s;try{s=await f.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:c,message:d,signature:u,publicKey:g,otp:y,code:w,redirectUrl:h,stackId:S}=s,R=S||e.stackId,x;if(c&&d&&u){let Y={"Content-Type":"application/json"},J=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(R)}/auth/web3/verify`,{method:"POST",headers:Y,body:JSON.stringify({chain:c,message:d,signature:u,public_key:g}),signal:AbortSignal.timeout(1e4)});if(!J.ok){let q=await J.json().catch(()=>({})),W=q?.error?.message||q?.message||q?.error||`StackNet returned ${J.status}`;return console.error(`[auth-callback] Verify failed: ${J.status}`,W),Response.json({error:"Wallet verification failed",detail:typeof W=="string"?W:void 0},{status:401})}let N=await J.json();x=N.data?.session||N.session||N.data||N,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(x||{}).join(", ")}`);}else return y||w?Response.json({error:"Use /api/auth/otp for OTP verification"},{status:400}):Response.json({error:"Provide wallet signature or OTP code"},{status:400});if(!x?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let k=JSON.parse(Buffer.from(x.jwt.split(".")[1],"base64url").toString()),A=Math.floor(Date.now()/1e3),H={...k,exp:A+n,iat:A},v=P(H,e.authSecret),b={userId:k.sub||k.user_id||k.session_id||k.global_id||"",address:x.address||k.address,chain:x.chain||c,expiresAt:Date.now()+a*1e3,authMethod:c?`web3:${c}`:"otp"},$=new Headers({"Content-Type":"application/json"}),O=e.secureCookies!==false?"; Secure":"",L=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";$.append("Set-Cookie",`stackauth_jwt=${v}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${O}${L}`);let X=Buffer.from(JSON.stringify(b)).toString("base64url");return $.append("Set-Cookie",`stackauth_session=${X}; Path=/; SameSite=Lax; Max-Age=${a}${O}${L}`),o.generateToken($),new Response(JSON.stringify({user:b}),{status:200,headers:$})}}function K(e,r){if(!r)return null;try{let t=E(e);if(!t||t.exp&&t.exp<Math.floor(Date.now()/1e3))return null;let o=Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString("base64url"),n=Buffer.from(JSON.stringify(t)).toString("base64url"),a=createHmac("sha256",r).update(`${o}.${n}`).digest("base64url");return `${o}.${n}.${a}`}catch{return null}}var le=/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;function Z(e){return typeof e!="string"||e.length===0||e.length>8192?null:le.test(e)?e:null}function U(e,r){let t=K(e,r),o=t&&Z(t);if(o)return {Cookie:`stackauth_jwt=${o}`};let n=Z(e);return n?{Cookie:`stackauth_jwt=${n}`}:{}}function M(e){let r=e.headers.get("cookie");if(r){let o=r.split(";").map(n=>n.trim()).find(n=>n.startsWith("stackauth_jwt="));if(o)return o.slice(14)}let t=e.headers.get("authorization");return t?.startsWith("Bearer ")?t.slice(7):null}function pe(e){return !e.authSecret&&typeof console<"u"&&console.warn("[userutils] createLogoutHandler called without authSecret \u2014 upstream session revocation is disabled. Pass authSecret to enable it safely."),async function(t){let o=M(t);if(o&&e.authSecret){let m=_(o,e.authSecret),f=m?.session_id||m?.sub;if(f&&typeof f=="string")try{await fetch(`${e.stacknetUrl}/api/v2/sessions/${encodeURIComponent(f)}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});}catch{}}let n=e.secureCookies!==false?"; Secure":"",a=e.cookieDomain?`; Domain=${e.cookieDomain}`:"",i=new Headers({"Content-Type":"application/json"});return i.append("Set-Cookie",`stackauth_jwt=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${n}${a}`),i.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${n}${a}`),i.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${n}${a}`),new Response(JSON.stringify({success:true}),{status:200,headers:i})}}function de(e){let r=e.jwtExpiry||900,t=e.sessionMaxAge||604800;return async function(n){let a=M(n);if(!a)return Response.json({session:null},{status:200});let i=_(a,e.authSecret);if(!i)return Response.json({session:null},{status:200});let f={userId:i.sub||i.user_id||i.session_id||i.global_id||"",address:i.address,chain:i.chain,expiresAt:i.session_expires_at||(i.exp?i.exp*1e3:Date.now()+t*1e3),planId:i.plan_id,authMethod:i.auth_method},p=new Headers({"Content-Type":"application/json"}),l=D(a,e.authSecret,r,300);if(l){let s=e.secureCookies!==false?"; Secure":"",c=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";p.append("Set-Cookie",`stackauth_jwt=${l}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${s}${c}`);}return new Response(JSON.stringify({session:f}),{status:200,headers:p})}}function ge(e,r){if(e.length!==r.length)return false;try{return timingSafeEqual(Buffer.from(e),Buffer.from(r))}catch{return false}}function he(e){let r=e.rateLimiter||T({maxRequests:5,windowMs:3e5}),t=j({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,n=e.sessionMaxAge||604800;return async function(i){let m=I(i),f=await r.check(`otp:${m}`);if(!f.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(f.retryAfter||300)}});let p;try{p=await i.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:l}=p;if(!l||typeof l!="string"||l.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!ge(l,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let s=Math.floor(Date.now()/1e3),d={sub:`otp:${createHash("sha256").update(`otp:${l}:${Date.now()}`).digest("hex").slice(0,32)}`,auth_method:"otp",iat:s,exp:s+o},u=P(d,e.authSecret),g={userId:d.sub,expiresAt:Date.now()+n*1e3,authMethod:"otp"},y=new Headers({"Content-Type":"application/json"}),w=e.secureCookies!==false?"; Secure":"",h=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";y.append("Set-Cookie",`stackauth_jwt=${u}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${n}${w}${h}`);let S=Buffer.from(JSON.stringify(g)).toString("base64url");return y.append("Set-Cookie",`stackauth_session=${S}; Path=/; SameSite=Lax; Max-Age=${n}${w}${h}`),t.generateToken(y),new Response(JSON.stringify({success:true,data:{user:g}}),{status:200,headers:y})}}function ye(e,r){let t=r?.rateLimiter||T({maxRequests:10,windowMs:6e4}),o=j({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800;async function i(f){let p=new URL(f.url),l=p.searchParams.get("provider"),s=p.searchParams.get("redirectUri")||p.searchParams.get("redirect_uri"),c=p.searchParams.get("stackId")||e.stackId;if(!l)return Response.json({error:"Missing provider parameter"},{status:400});if(!s)return Response.json({error:"Missing redirectUri parameter"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(l))return Response.json({error:"Invalid provider name"},{status:400});if(!c||!/^[a-zA-Z0-9_-]{1,64}$/.test(c))return Response.json({error:"Invalid stackId"},{status:400});try{let d=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(c)}/auth/oauth/${encodeURIComponent(l)}/initiate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({redirectUrl:s}),signal:AbortSignal.timeout(1e4)});if(!d.ok){let y=await d.json().catch(()=>({}));return Response.json({error:y.error?.message||`Failed to start OAuth flow: ${d.statusText}`},{status:d.status})}let u=await d.json(),g=u.data||u;return Response.json({redirect_url:g.url,state:g.state})}catch(d){return Response.json({error:d.message||"Failed to start OAuth flow"},{status:500})}}async function m(f){let p=I(f),l=await t.check(`oauth:${p}`);if(!l.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(l.retryAfter||60)}});let s;try{s=await f.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{provider:c,code:d,state:u,stackId:g}=s,y=g||e.stackId;if(!c||!d||!u)return Response.json({error:"Missing provider, code, or state"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(c))return Response.json({error:"Invalid provider name"},{status:400});if(!y||!/^[a-zA-Z0-9_-]{1,64}$/.test(y))return Response.json({error:"Invalid stackId"},{status:400});try{let w=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(y)}/auth/oauth/${encodeURIComponent(c)}/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:d,state:u}),signal:AbortSignal.timeout(1e4)});if(!w.ok){let O=await w.json().catch(()=>({}));return Response.json({error:O.error?.message||`OAuth verification failed: ${w.statusText}`},{status:401})}let h=await w.json(),S=h.data?.session||h.session||h.data||h;if(!S?.jwt)return Response.json({error:"OAuth authentication failed \u2014 no session returned"},{status:401});let R=JSON.parse(Buffer.from(S.jwt.split(".")[1],"base64url").toString()),x=Math.floor(Date.now()/1e3),k=P({...R,exp:x+n,iat:x},e.authSecret),H={userId:R.sub||R.user_id||R.session_id||R.global_id||"",address:S.address||R.address,chain:void 0,expiresAt:Date.now()+a*1e3,authMethod:`oauth:${c}`},v=new Headers({"Content-Type":"application/json"}),C=e.secureCookies!==!1?"; Secure":"",b=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";v.append("Set-Cookie",`stackauth_jwt=${k}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${C}${b}`);let $=Buffer.from(JSON.stringify(H)).toString("base64url");return v.append("Set-Cookie",`stackauth_session=${$}; Path=/; SameSite=Lax; Max-Age=${a}${C}${b}`),o.generateToken(v),new Response(JSON.stringify({user:H}),{status:200,headers:v})}catch(w){return Response.json({error:w.message||"OAuth callback failed"},{status:500})}}return {startFlow:i,handleCallback:m}}function Se(e,r){let t=r?.rateLimiter||T({maxRequests:10,windowMs:6e4}),o=j({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800;return async function(m){let f=I(m),p=await t.check(`google-onetap:${f}`);if(!p.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||60)}});let l;try{l=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{credential:s,stackId:c}=l,d=c||e.stackId;if(!s)return Response.json({error:"Missing credential"},{status:400});if(s.split(".").length!==3)return Response.json({error:"Invalid credential format"},{status:400});let u;try{let S=await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(s)}`,{signal:AbortSignal.timeout(1e4)});if(!S.ok)return Response.json({error:"Google credential verification failed"},{status:401});u=await S.json();}catch{return Response.json({error:"Failed to verify Google credential"},{status:500})}if(!u.sub||!u.email)return Response.json({error:"Invalid Google token \u2014 missing user info"},{status:401});if(u.iss!=="https://accounts.google.com"&&u.iss!=="accounts.google.com")return Response.json({error:"Invalid Google token issuer"},{status:401});let g=typeof u.exp=="string"?parseInt(u.exp,10):Number(u.exp);if(!Number.isFinite(g)||g<Math.floor(Date.now()/1e3))return Response.json({error:"Google token expired"},{status:401});let y=e.googleClientIds||(e.googleClientId?[e.googleClientId]:[]);if(y.length===0)return Response.json({error:"Google One Tap not configured \u2014 set ServerConfig.googleClientId(s)"},{status:500});if(!u.aud||!y.includes(u.aud))return Response.json({error:"Invalid Google token audience"},{status:401});if(!(u.email_verified===true||u.email_verified==="true"))return Response.json({error:"Google email is not verified"},{status:401});let h={sub:u.sub,email:u.email,name:u.name,picture:u.picture};try{let S=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(d)}/auth/oauth/google/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:s,google_id:h.sub,email:h.email,name:h.name,picture:h.picture,one_tap:!0}),signal:AbortSignal.timeout(1e4)});if(!S.ok){let k=Math.floor(Date.now()/1e3),A=h.sub,H=P({sub:A,global_id:`google:${A}`,stack_id:d,chain:"google",email:h.email,credentials:["oauth:google"],iat:k,exp:k+n,iss:"stackauth.network",signed_by:["local"]},e.authSecret),v={userId:A,address:h.email,chain:void 0,expiresAt:Date.now()+a*1e3,authMethod:"oauth:google"},C=new Headers({"Content-Type":"application/json"}),b=e.secureCookies!==!1?"; Secure":"",$=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";C.append("Set-Cookie",`stackauth_jwt=${H}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${b}${$}`);let O=Buffer.from(JSON.stringify(v)).toString("base64url");return C.append("Set-Cookie",`stackauth_session=${O}; Path=/; SameSite=Lax; Max-Age=${a}${b}${$}`),o.generateToken(C),new Response(JSON.stringify({user:v}),{status:200,headers:C})}let R=await S.json(),x=R.data?.session||R.session||R.data||R;if(x?.jwt){let k=JSON.parse(Buffer.from(x.jwt.split(".")[1],"base64url").toString()),A=Math.floor(Date.now()/1e3),H=P({...k,exp:A+n,iat:A},e.authSecret),C={userId:k.sub||k.user_id||h.sub,address:h.email||x.address,chain:void 0,expiresAt:Date.now()+a*1e3,authMethod:"oauth:google"},b=new Headers({"Content-Type":"application/json"}),$=e.secureCookies!==!1?"; Secure":"",O=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";b.append("Set-Cookie",`stackauth_jwt=${H}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${$}${O}`);let L=Buffer.from(JSON.stringify(C)).toString("base64url");return b.append("Set-Cookie",`stackauth_session=${L}; Path=/; SameSite=Lax; Max-Age=${a}${$}${O}`),o.generateToken(b),new Response(JSON.stringify({user:C}),{status:200,headers:b})}return Response.json({error:"No session returned"},{status:401})}catch(S){return Response.json({error:S.message||"Google One Tap authentication failed"},{status:500})}}}function ke(e){let r=j({secure:e.secureCookies!==false}),t=e.rateLimiter||T({maxRequests:20,windowMs:6e4}),o=e.stacknetJwtSecret||e.authSecret,n=e.jwtExpiry||900,a=e.sessionMaxAge||604800;function i(s){let c=M(s);if(!c)return null;let d=_(c,e.authSecret);return d?{jwt:c,payload:d}:null}function m(s,c){let d=D(s,e.authSecret,n,300);if(d){let u=e.secureCookies!==false?"; Secure":"",g=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";c.append("Set-Cookie",`stackauth_jwt=${d}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${u}${g}`);}}async function f(s,c){let d=i(s);if(!d)return Response.json({error:"Unauthorized"},{status:401});let u=U(d.jwt,o),g=await fetch(`${e.stacknetUrl}${c}`,{headers:u,signal:AbortSignal.timeout(15e3)}),y=await g.json().catch(()=>({})),w=new Headers({"Content-Type":"application/json"});return m(d.jwt,w),new Response(JSON.stringify(y),{status:g.status,headers:w})}async function p(s,c,d){let u=i(s);if(!u)return Response.json({error:"Unauthorized"},{status:401});let g=r.validateRequest(s);if(!g.valid)return Response.json({error:g.error||"CSRF validation failed"},{status:403});let y=u.payload.sub||u.payload.user_id||"unknown";if(!(await t.check(`billing:${y}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let h=await s.json().catch(()=>({})),S=U(u.jwt,o);S["Content-Type"]="application/json";let R=await fetch(`${e.stacknetUrl}${c}`,{method:"POST",headers:S,body:JSON.stringify({...h,...d}),signal:AbortSignal.timeout(15e3)}),x=await R.json().catch(()=>({})),k=new Headers({"Content-Type":"application/json"});return m(u.jwt,k),new Response(JSON.stringify(x),{status:R.status,headers:k})}let l=`/api/v2/stacks/${encodeURIComponent(e.stackId)}`;return {plans:{GET:async s=>{let c=await fetch(`${e.stacknetUrl}${l}/plans`,{signal:AbortSignal.timeout(1e4)}),d=await c.json().catch(()=>({}));return Response.json(d,{status:c.status})}},subscription:{GET:(s=>f(s,`${l}/subscription`))},subscribe:{POST:(s=>{let c=new URL(s.url).origin;return p(s,`${l}/subscribe`,{successUrl:`${c}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing`})})},cancel:{POST:(s=>p(s,`${l}/cancel-subscription`))},usage:{GET:(s=>f(s,"/v1/account/usage"))},history:{GET:(s=>f(s,`${l}/billing`))},prepaid:{POST:(s=>{let c=new URL(s.url).origin;return p(s,`${l}/prepaid`,{successUrl:`${c}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing/prepaid`})})},verifyPrepaid:{POST:(s=>p(s,`${l}/verify-prepaid`))},verifySession:{POST:(s=>p(s,`${l}/verify-session`))},subscribeSol:{POST:(s=>p(s,`${l}/subscribe-sol`))},prepaidSol:{POST:(s=>{new URL(s.url).origin;return p(s,`${l}/prepaid-sol`)})},topup:{POST:(s=>p(s,"/v1/account/topup"))}}}function we(e){return async function(t){let o=t.headers.get("stripe-signature");if(!o)return Response.json({error:"Missing Stripe signature"},{status:400});try{let n=await t.text(),a=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(e.stackId)}/webhook/stripe`,{method:"POST",headers:{"Content-Type":"application/json","stripe-signature":o},body:n,signal:AbortSignal.timeout(1e4)}),i=await a.json().catch(()=>({received:!0}));return Response.json(i,{status:a.status})}catch{return Response.json({error:"Webhook processing failed"},{status:502})}}}function F(){return {"Strict-Transport-Security":"max-age=63072000; includeSubDomains; preload","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"0","Referrer-Policy":"strict-origin-when-cross-origin","Permissions-Policy":"camera=(), microphone=(), geolocation=()"}}function Re(e){return async r=>{let t=await e(r),o=F(),n=new Headers(t.headers);for(let[a,i]of Object.entries(o))n.set(a,i);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:n})}}function xe(){return Object.entries(F()).map(([e,r])=>({key:e,value:r}))}
2
+ export{U as buildStackNetHeaders,ce as createAuthCallback,ke as createBillingProxy,j as createCSRFProtection,Se as createGoogleOneTapHandler,T as createInMemoryRateLimiter,ie as createInMemoryReplayStore,pe as createLogoutHandler,ye as createOAuthHandlers,he as createOTPHandler,de as createSessionHandler,we as createWebhookHandler,E as decodeJWTPayload,I as extractIP,M as extractJwt,B as generateToken,D as maybeRefreshJWT,xe as nextSecurityHeaders,K as resignForStackNet,F as securityHeaders,P as signJWT,_ as verifyJWT,V as verifyJWTSignature,Re as withSecurityHeaders};
@@ -1,4 +1,5 @@
1
- export { A as APIResponse, B as BillingPlan, a as BillingRecord, M as MPCNode, N as NetworkStatus, P as PrepaidCheckoutResult, b as PrepaidVerifyResult, c as PublicSession, S as Session, d as Subscription, U as UsageSummary, e as UserUtilsCallbacks, f as UserUtilsConfig, W as Web3Chain } from '../config-BrziGArs.cjs';
1
+ export { A as APIResponse, M as MPCNode, N as NetworkStatus, P as PublicSession, S as Session, W as Web3Chain } from '../auth-DR2aYcor.cjs';
2
+ export { B as BillingPlan, a as BillingRecord, P as PrepaidCheckoutResult, b as PrepaidVerifyResult, S as Subscription, U as UsageSummary, c as UserUtilsCallbacks, d as UserUtilsConfig } from '../config-Bjh8PEhY.cjs';
2
3
 
3
4
  /** User profile — global (network-wide) or stack-scoped */
4
5
  interface UserProfile {
@@ -6,6 +7,12 @@ interface UserProfile {
6
7
  username: string;
7
8
  avatarUrl?: string;
8
9
  bio?: string;
10
+ /**
11
+ * Stack-scoped payout address (e.g. Solana wallet for Paper / USDC payouts).
12
+ * Only meaningful on stack-scoped profiles. The backend stores this as
13
+ * `payment_address` (snake_case).
14
+ */
15
+ paymentAddress?: string;
9
16
  createdAt?: number;
10
17
  updatedAt?: number;
11
18
  }
@@ -14,6 +21,12 @@ interface ProfileUpdateInput {
14
21
  username?: string;
15
22
  avatarUrl?: string;
16
23
  bio?: string;
24
+ /**
25
+ * Stack-scoped payout address. Pass through `useProfile().updateProfile`
26
+ * with a stack-scoped scope to set the payment_address on the user's
27
+ * profile for that stack.
28
+ */
29
+ paymentAddress?: string;
17
30
  }
18
31
  /** Scope for profile operations */
19
32
  type ProfileScope = 'global' | {
@@ -1,4 +1,5 @@
1
- export { A as APIResponse, B as BillingPlan, a as BillingRecord, M as MPCNode, N as NetworkStatus, P as PrepaidCheckoutResult, b as PrepaidVerifyResult, c as PublicSession, S as Session, d as Subscription, U as UsageSummary, e as UserUtilsCallbacks, f as UserUtilsConfig, W as Web3Chain } from '../config-BrziGArs.js';
1
+ export { A as APIResponse, M as MPCNode, N as NetworkStatus, P as PublicSession, S as Session, W as Web3Chain } from '../auth-DR2aYcor.js';
2
+ export { B as BillingPlan, a as BillingRecord, P as PrepaidCheckoutResult, b as PrepaidVerifyResult, S as Subscription, U as UsageSummary, c as UserUtilsCallbacks, d as UserUtilsConfig } from '../config-_ZjAzNkJ.js';
2
3
 
3
4
  /** User profile — global (network-wide) or stack-scoped */
4
5
  interface UserProfile {
@@ -6,6 +7,12 @@ interface UserProfile {
6
7
  username: string;
7
8
  avatarUrl?: string;
8
9
  bio?: string;
10
+ /**
11
+ * Stack-scoped payout address (e.g. Solana wallet for Paper / USDC payouts).
12
+ * Only meaningful on stack-scoped profiles. The backend stores this as
13
+ * `payment_address` (snake_case).
14
+ */
15
+ paymentAddress?: string;
9
16
  createdAt?: number;
10
17
  updatedAt?: number;
11
18
  }
@@ -14,6 +21,12 @@ interface ProfileUpdateInput {
14
21
  username?: string;
15
22
  avatarUrl?: string;
16
23
  bio?: string;
24
+ /**
25
+ * Stack-scoped payout address. Pass through `useProfile().updateProfile`
26
+ * with a stack-scoped scope to set the payment_address on the user's
27
+ * profile for that stack.
28
+ */
29
+ paymentAddress?: string;
17
30
  }
18
31
  /** Scope for profile operations */
19
32
  type ProfileScope = 'global' | {
@@ -0,0 +1,60 @@
1
+ import { P as PublicSession, W as Web3Chain } from './auth-DR2aYcor.cjs';
2
+
3
+ interface AuthTransport {
4
+ /** Store credentials after successful login */
5
+ storeCredentials(token: string, session: PublicSession): Promise<void>;
6
+ /** Retrieve the stored auth token (null if not authenticated) */
7
+ getToken(): Promise<string | null>;
8
+ /** Get headers to attach to authenticated API requests */
9
+ getHeaders(): Promise<Record<string, string>>;
10
+ /** Read the stored session without a network call */
11
+ getStoredSession(): Promise<PublicSession | null>;
12
+ /** Clear all stored auth data */
13
+ clear(): Promise<void>;
14
+ }
15
+ interface AuthClientConfig {
16
+ /** Base URL for app API routes (e.g. http://localhost:3000) */
17
+ apiBaseUrl: string;
18
+ /** Direct StackNet URL for challenges (defaults to https://stacknet.magma-rpc.com) */
19
+ stacknetUrl?: string;
20
+ /** Stack identifier */
21
+ stackId?: string;
22
+ /** Auth transport (web cookies or native secure storage) */
23
+ transport: AuthTransport;
24
+ /** Optional fallback token for dev (e.g. service key) */
25
+ serviceKey?: string;
26
+ /** Callbacks */
27
+ onAuthSuccess?: (session: PublicSession) => void;
28
+ onAuthError?: (error: Error) => void;
29
+ onLogout?: () => void;
30
+ }
31
+ interface AuthClient {
32
+ /** Email + password login */
33
+ login(email: string, password: string): Promise<PublicSession>;
34
+ /** Web3 wallet login (challenge must be obtained first) */
35
+ loginWeb3(params: {
36
+ chain: Web3Chain;
37
+ address: string;
38
+ message: string;
39
+ signature: string;
40
+ }): Promise<PublicSession>;
41
+ /** OTP code login */
42
+ loginOTP(code: string): Promise<PublicSession>;
43
+ /** Get current session (from storage, then validate with server) */
44
+ getSession(): Promise<PublicSession | null>;
45
+ /** Check if token is valid with the server */
46
+ checkSession(): Promise<boolean>;
47
+ /** Get a web3 challenge for wallet signing */
48
+ getChallenge(chain: Web3Chain, address: string): Promise<{
49
+ message: string;
50
+ nonce: string;
51
+ }>;
52
+ /** Sign out and clear stored credentials */
53
+ logout(): Promise<void>;
54
+ /** Get auth headers for API requests */
55
+ getHeaders(): Promise<Record<string, string>>;
56
+ /** Get stored token directly */
57
+ getToken(): Promise<string | null>;
58
+ }
59
+
60
+ export type { AuthClient as A, AuthClientConfig as a, AuthTransport as b };
@@ -0,0 +1,60 @@
1
+ import { P as PublicSession, W as Web3Chain } from './auth-DR2aYcor.js';
2
+
3
+ interface AuthTransport {
4
+ /** Store credentials after successful login */
5
+ storeCredentials(token: string, session: PublicSession): Promise<void>;
6
+ /** Retrieve the stored auth token (null if not authenticated) */
7
+ getToken(): Promise<string | null>;
8
+ /** Get headers to attach to authenticated API requests */
9
+ getHeaders(): Promise<Record<string, string>>;
10
+ /** Read the stored session without a network call */
11
+ getStoredSession(): Promise<PublicSession | null>;
12
+ /** Clear all stored auth data */
13
+ clear(): Promise<void>;
14
+ }
15
+ interface AuthClientConfig {
16
+ /** Base URL for app API routes (e.g. http://localhost:3000) */
17
+ apiBaseUrl: string;
18
+ /** Direct StackNet URL for challenges (defaults to https://stacknet.magma-rpc.com) */
19
+ stacknetUrl?: string;
20
+ /** Stack identifier */
21
+ stackId?: string;
22
+ /** Auth transport (web cookies or native secure storage) */
23
+ transport: AuthTransport;
24
+ /** Optional fallback token for dev (e.g. service key) */
25
+ serviceKey?: string;
26
+ /** Callbacks */
27
+ onAuthSuccess?: (session: PublicSession) => void;
28
+ onAuthError?: (error: Error) => void;
29
+ onLogout?: () => void;
30
+ }
31
+ interface AuthClient {
32
+ /** Email + password login */
33
+ login(email: string, password: string): Promise<PublicSession>;
34
+ /** Web3 wallet login (challenge must be obtained first) */
35
+ loginWeb3(params: {
36
+ chain: Web3Chain;
37
+ address: string;
38
+ message: string;
39
+ signature: string;
40
+ }): Promise<PublicSession>;
41
+ /** OTP code login */
42
+ loginOTP(code: string): Promise<PublicSession>;
43
+ /** Get current session (from storage, then validate with server) */
44
+ getSession(): Promise<PublicSession | null>;
45
+ /** Check if token is valid with the server */
46
+ checkSession(): Promise<boolean>;
47
+ /** Get a web3 challenge for wallet signing */
48
+ getChallenge(chain: Web3Chain, address: string): Promise<{
49
+ message: string;
50
+ nonce: string;
51
+ }>;
52
+ /** Sign out and clear stored credentials */
53
+ logout(): Promise<void>;
54
+ /** Get auth headers for API requests */
55
+ getHeaders(): Promise<Record<string, string>>;
56
+ /** Get stored token directly */
57
+ getToken(): Promise<string | null>;
58
+ }
59
+
60
+ export type { AuthClient as A, AuthClientConfig as a, AuthTransport as b };
@@ -1,2 +1,2 @@
1
- 'use strict';var clsx=require('clsx'),tailwindMerge=require('tailwind-merge');function i(...t){return tailwindMerge.twMerge(clsx.clsx(t))}function s(t){return t>=1e12?`${(t/1e12).toFixed(t%1e12===0?0:1)}T`:t>=1e9?`${(t/1e9).toFixed(t%1e9===0?0:1)}B`:t>=1e6?`${(t/1e6).toFixed(t%1e6===0?0:1)}M`:t>=1e3?`${(t/1e3).toFixed(0)}K`:t.toLocaleString()}function a(t,r){if(!t)return "/";if(t.startsWith("/")&&!t.startsWith("//"))return t;try{let e=new URL(t,r);return e.origin!==r?"/":e.pathname+e.search+e.hash}catch{return "/"}}function u(t){try{let r=t.split(".");if(r.length!==3)return null;let e=atob(r[1].replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(e)}catch{return null}}function c(){if(typeof document>"u")return null;try{let t=document.cookie.split(";").map(e=>e.trim()).find(e=>e.startsWith("stackauth_session="));if(!t)return null;let r=t.slice(18);return JSON.parse(atob(r.replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function l(t="__csrf"){if(typeof document>"u")return null;let r=document.cookie.split(";").map(e=>e.trim()).find(e=>e.startsWith(`${t}=`));return r?r.slice(t.length+1):null}
2
- exports.cn=i;exports.decodeJwtPayloadClient=u;exports.formatTokens=s;exports.readCSRFCookie=l;exports.readSessionCookie=c;exports.validateRedirectUrl=a;
1
+ 'use strict';var clsx=require('clsx'),tailwindMerge=require('tailwind-merge');function s(...t){return tailwindMerge.twMerge(clsx.clsx(t))}function a(t){return t>=1e12?`${(t/1e12).toFixed(t%1e12===0?0:1)}T`:t>=1e9?`${(t/1e9).toFixed(t%1e9===0?0:1)}B`:t>=1e6?`${(t/1e6).toFixed(t%1e6===0?0:1)}M`:t>=1e3?`${(t/1e3).toFixed(0)}K`:t.toLocaleString()}function u(t,e){if(!t)return "/";if(t.startsWith("/")&&!t.startsWith("//"))return t;try{let r=new URL(t,e);return r.origin!==e?"/":r.pathname+r.search+r.hash}catch{return "/"}}function c(t,e="#"){if(!t||typeof t!="string")return e;let r=t.trim();if(r===""||r==="#")return e;if(r.startsWith("/")||r.startsWith("./")||r.startsWith("../"))return r;try{let n=new URL(r);if(n.protocol==="http:"||n.protocol==="https:")return n.toString()}catch{}return e}function l(t){try{let e=t.split(".");if(e.length!==3)return null;let r=atob(e[1].replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(r)}catch{return null}}function d(){if(typeof document>"u")return null;try{let t=document.cookie.split(";").map(r=>r.trim()).find(r=>r.startsWith("stackauth_session="));if(!t)return null;let e=t.slice(18);return JSON.parse(atob(e.replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function f(t="__csrf"){if(typeof document>"u")return null;let e=document.cookie.split(";").map(r=>r.trim()).find(r=>r.startsWith(`${t}=`));return e?e.slice(t.length+1):null}
2
+ exports.cn=s;exports.decodeJwtPayloadClient=l;exports.formatTokens=a;exports.readCSRFCookie=f;exports.readSessionCookie=d;exports.safeUrl=c;exports.validateRedirectUrl=u;
@@ -11,6 +11,21 @@ declare function formatTokens(n: number): string;
11
11
  */
12
12
  declare function validateRedirectUrl(url: string, allowedOrigin: string): string;
13
13
 
14
+ /**
15
+ * Returns a safe href value for an anchor or image source.
16
+ *
17
+ * Allows: relative paths (starting with /, ./, ../), and absolute http(s) URLs.
18
+ * Blocks: javascript:, data:, file:, vbscript:, mailto:, etc.
19
+ *
20
+ * React only sanitizes `javascript:` in href attributes by default; this
21
+ * helper closes the gap for `data:` (which can carry HTML/JS) and any other
22
+ * non-network protocol that a misconfigured or malicious config could supply.
23
+ *
24
+ * Returns the supplied `fallback` (default '#') for any input that fails
25
+ * validation, so the markup never renders an unsafe URL.
26
+ */
27
+ declare function safeUrl(url: string | undefined | null, fallback?: string): string;
28
+
14
29
  /**
15
30
  * Client-side JWT payload decode — NO VERIFICATION.
16
31
  * For display purposes only. Never trust the result for auth decisions.
@@ -33,4 +48,4 @@ declare function readSessionCookie(): {
33
48
  */
34
49
  declare function readCSRFCookie(cookieName?: string): string | null;
35
50
 
36
- export { cn, decodeJwtPayloadClient, formatTokens, readCSRFCookie, readSessionCookie, validateRedirectUrl };
51
+ export { cn, decodeJwtPayloadClient, formatTokens, readCSRFCookie, readSessionCookie, safeUrl, validateRedirectUrl };
@@ -11,6 +11,21 @@ declare function formatTokens(n: number): string;
11
11
  */
12
12
  declare function validateRedirectUrl(url: string, allowedOrigin: string): string;
13
13
 
14
+ /**
15
+ * Returns a safe href value for an anchor or image source.
16
+ *
17
+ * Allows: relative paths (starting with /, ./, ../), and absolute http(s) URLs.
18
+ * Blocks: javascript:, data:, file:, vbscript:, mailto:, etc.
19
+ *
20
+ * React only sanitizes `javascript:` in href attributes by default; this
21
+ * helper closes the gap for `data:` (which can carry HTML/JS) and any other
22
+ * non-network protocol that a misconfigured or malicious config could supply.
23
+ *
24
+ * Returns the supplied `fallback` (default '#') for any input that fails
25
+ * validation, so the markup never renders an unsafe URL.
26
+ */
27
+ declare function safeUrl(url: string | undefined | null, fallback?: string): string;
28
+
14
29
  /**
15
30
  * Client-side JWT payload decode — NO VERIFICATION.
16
31
  * For display purposes only. Never trust the result for auth decisions.
@@ -33,4 +48,4 @@ declare function readSessionCookie(): {
33
48
  */
34
49
  declare function readCSRFCookie(cookieName?: string): string | null;
35
50
 
36
- export { cn, decodeJwtPayloadClient, formatTokens, readCSRFCookie, readSessionCookie, validateRedirectUrl };
51
+ export { cn, decodeJwtPayloadClient, formatTokens, readCSRFCookie, readSessionCookie, safeUrl, validateRedirectUrl };
@@ -1,2 +1,2 @@
1
- import {clsx}from'clsx';import {twMerge}from'tailwind-merge';function i(...t){return twMerge(clsx(t))}function s(t){return t>=1e12?`${(t/1e12).toFixed(t%1e12===0?0:1)}T`:t>=1e9?`${(t/1e9).toFixed(t%1e9===0?0:1)}B`:t>=1e6?`${(t/1e6).toFixed(t%1e6===0?0:1)}M`:t>=1e3?`${(t/1e3).toFixed(0)}K`:t.toLocaleString()}function a(t,r){if(!t)return "/";if(t.startsWith("/")&&!t.startsWith("//"))return t;try{let e=new URL(t,r);return e.origin!==r?"/":e.pathname+e.search+e.hash}catch{return "/"}}function u(t){try{let r=t.split(".");if(r.length!==3)return null;let e=atob(r[1].replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(e)}catch{return null}}function c(){if(typeof document>"u")return null;try{let t=document.cookie.split(";").map(e=>e.trim()).find(e=>e.startsWith("stackauth_session="));if(!t)return null;let r=t.slice(18);return JSON.parse(atob(r.replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function l(t="__csrf"){if(typeof document>"u")return null;let r=document.cookie.split(";").map(e=>e.trim()).find(e=>e.startsWith(`${t}=`));return r?r.slice(t.length+1):null}
2
- export{i as cn,u as decodeJwtPayloadClient,s as formatTokens,l as readCSRFCookie,c as readSessionCookie,a as validateRedirectUrl};
1
+ import {clsx}from'clsx';import {twMerge}from'tailwind-merge';function s(...t){return twMerge(clsx(t))}function a(t){return t>=1e12?`${(t/1e12).toFixed(t%1e12===0?0:1)}T`:t>=1e9?`${(t/1e9).toFixed(t%1e9===0?0:1)}B`:t>=1e6?`${(t/1e6).toFixed(t%1e6===0?0:1)}M`:t>=1e3?`${(t/1e3).toFixed(0)}K`:t.toLocaleString()}function u(t,e){if(!t)return "/";if(t.startsWith("/")&&!t.startsWith("//"))return t;try{let r=new URL(t,e);return r.origin!==e?"/":r.pathname+r.search+r.hash}catch{return "/"}}function c(t,e="#"){if(!t||typeof t!="string")return e;let r=t.trim();if(r===""||r==="#")return e;if(r.startsWith("/")||r.startsWith("./")||r.startsWith("../"))return r;try{let n=new URL(r);if(n.protocol==="http:"||n.protocol==="https:")return n.toString()}catch{}return e}function l(t){try{let e=t.split(".");if(e.length!==3)return null;let r=atob(e[1].replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(r)}catch{return null}}function d(){if(typeof document>"u")return null;try{let t=document.cookie.split(";").map(r=>r.trim()).find(r=>r.startsWith("stackauth_session="));if(!t)return null;let e=t.slice(18);return JSON.parse(atob(e.replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function f(t="__csrf"){if(typeof document>"u")return null;let e=document.cookie.split(";").map(r=>r.trim()).find(r=>r.startsWith(`${t}=`));return e?e.slice(t.length+1):null}
2
+ export{s as cn,l as decodeJwtPayloadClient,a as formatTokens,f as readCSRFCookie,d as readSessionCookie,c as safeUrl,u as validateRedirectUrl};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacknet/userutils",
3
- "version": "0.3.6",
3
+ "version": "0.5.1",
4
4
  "description": "Reusable auth, billing, and security utilities for StackNet stacks and applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -36,6 +36,16 @@
36
36
  "types": "./dist/server/index.d.ts",
37
37
  "import": "./dist/server/index.js",
38
38
  "require": "./dist/server/index.cjs"
39
+ },
40
+ "./core": {
41
+ "types": "./dist/core/index.d.ts",
42
+ "import": "./dist/core/index.js",
43
+ "require": "./dist/core/index.cjs"
44
+ },
45
+ "./adapters": {
46
+ "types": "./dist/adapters/index.d.ts",
47
+ "import": "./dist/adapters/index.js",
48
+ "require": "./dist/adapters/index.cjs"
39
49
  }
40
50
  },
41
51
  "files": [