@stacknet/userutils 0.2.55 → 0.3.5

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.
@@ -70,6 +70,21 @@ interface OTPHandlerConfig extends Pick<ServerConfig, 'authSecret' | 'secureCook
70
70
  */
71
71
  declare function createOTPHandler(config: OTPHandlerConfig): (request: Request) => Promise<Response>;
72
72
 
73
+ interface OAuthHandlerConfig {
74
+ rateLimiter?: RateLimiter;
75
+ }
76
+ /**
77
+ * Factory: OAuth flow handlers.
78
+ *
79
+ * Returns two handlers:
80
+ * - GET /api/auth/oauth/[provider] — Starts OAuth flow, returns redirect URL
81
+ * - POST /api/auth/oauth/[provider]/callback — Handles OAuth callback, sets cookies
82
+ */
83
+ declare function createOAuthHandlers(config: ServerConfig, opts?: OAuthHandlerConfig): {
84
+ startFlow: (request: Request) => Promise<Response>;
85
+ handleCallback: (request: Request) => Promise<Response>;
86
+ };
87
+
73
88
  interface BillingProxyConfig extends Pick<ServerConfig, 'authSecret' | 'stacknetUrl' | 'stackId' | 'stacknetJwtSecret' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge' | 'jwtExpiry'> {
74
89
  /** Rate limiter for mutations (default: 20/min per user) */
75
90
  rateLimiter?: RateLimiter;
@@ -210,4 +225,4 @@ declare function buildStackNetHeaders(jwt: string, stacknetJwtSecret: string): R
210
225
  */
211
226
  declare function extractJwt(request: Request): string | null;
212
227
 
213
- export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type OTPHandlerConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOTPHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, maybeRefreshJWT, nextSecurityHeaders, resignForStackNet, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
228
+ export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type OAuthHandlerConfig, type OTPHandlerConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOAuthHandlers, createOTPHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, maybeRefreshJWT, nextSecurityHeaders, resignForStackNet, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
@@ -70,6 +70,21 @@ interface OTPHandlerConfig extends Pick<ServerConfig, 'authSecret' | 'secureCook
70
70
  */
71
71
  declare function createOTPHandler(config: OTPHandlerConfig): (request: Request) => Promise<Response>;
72
72
 
73
+ interface OAuthHandlerConfig {
74
+ rateLimiter?: RateLimiter;
75
+ }
76
+ /**
77
+ * Factory: OAuth flow handlers.
78
+ *
79
+ * Returns two handlers:
80
+ * - GET /api/auth/oauth/[provider] — Starts OAuth flow, returns redirect URL
81
+ * - POST /api/auth/oauth/[provider]/callback — Handles OAuth callback, sets cookies
82
+ */
83
+ declare function createOAuthHandlers(config: ServerConfig, opts?: OAuthHandlerConfig): {
84
+ startFlow: (request: Request) => Promise<Response>;
85
+ handleCallback: (request: Request) => Promise<Response>;
86
+ };
87
+
73
88
  interface BillingProxyConfig extends Pick<ServerConfig, 'authSecret' | 'stacknetUrl' | 'stackId' | 'stacknetJwtSecret' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge' | 'jwtExpiry'> {
74
89
  /** Rate limiter for mutations (default: 20/min per user) */
75
90
  rateLimiter?: RateLimiter;
@@ -210,4 +225,4 @@ declare function buildStackNetHeaders(jwt: string, stacknetJwtSecret: string): R
210
225
  */
211
226
  declare function extractJwt(request: Request): string | null;
212
227
 
213
- export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type OTPHandlerConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOTPHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, maybeRefreshJWT, nextSecurityHeaders, resignForStackNet, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
228
+ export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type OAuthHandlerConfig, type OTPHandlerConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOAuthHandlers, createOTPHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, maybeRefreshJWT, nextSecurityHeaders, resignForStackNet, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
@@ -1 +1 @@
1
- import {createHmac,timingSafeEqual,randomBytes,createHash}from'crypto';function B(e){return Buffer.from(e).toString("base64url")}function Z(e){return Buffer.from(e,"base64url").toString()}function P(e){try{let r=e.split(".");return r.length!==3?null:JSON.parse(Z(r[1]))}catch{return null}}function $(e,r){let t=B(JSON.stringify({alg:"HS256",typ:"JWT"})),n=B(JSON.stringify(e)),s=createHmac("sha256",r).update(`${t}.${n}`).digest("base64url");return `${t}.${n}.${s}`}function F(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[n,s,o]=t,a=createHmac("sha256",r).update(`${n}.${s}`).digest("base64url"),f=Buffer.from(o),p=Buffer.from(a);return f.length!==p.length?!1:timingSafeEqual(f,p)}catch{return false}}function C(e,r){if(!F(e,r))return null;let t=P(e);return !t||t.exp&&t.exp<Math.floor(Date.now()/1e3)?null:t}function H(e,r,t=900,n=300){let s=C(e,r);return !s?.exp||s.exp*1e3-Date.now()>n*1e3?null:$({...s,exp:Math.floor(Date.now()/1e3)+t},r)}function I(e=32){return randomBytes(e).toString("hex")}function v(e){return e.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||e.headers.get("x-real-ip")||"unknown"}var te="__csrf",re="x-csrf-token";function x(e={}){let r=e.cookieName||te,t=e.headerName||re,n=e.tokenLength||32,s=e.secure!==false;return {generateToken(o){let a=I(n),f=[`${r}=${a}`,"Path=/","SameSite=Lax"];return s&&f.push("Secure"),o.append("Set-Cookie",f.join("; ")),a},validateRequest(o){let a=o.headers.get("cookie");if(!a)return {valid:false,error:"No cookies present"};let f=a.split(";").map(l=>l.trim()).find(l=>l.startsWith(`${r}=`))?.slice(r.length+1);if(!f)return {valid:false,error:"CSRF cookie missing"};let p=o.headers.get(t);if(!p)return {valid:false,error:"CSRF header missing"};try{let l=Buffer.from(f),u=Buffer.from(p);return l.length!==u.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(l,u)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function w(e){let r=new Map,t=setInterval(()=>{let n=Date.now();for(let[s,o]of r)n>=o.resetAt&&r.delete(s);},6e4);return typeof t=="object"&&"unref"in t&&t.unref(),{async check(n){let s=Date.now(),o=r.get(n);return (!o||s>=o.resetAt)&&(o={count:0,resetAt:s+e.windowMs},r.set(n,o)),o.count++,o.count>e.maxRequests?{allowed:false,remaining:0,retryAfter:Math.ceil((o.resetAt-s)/1e3)}:{allowed:true,remaining:e.maxRequests-o.count}}}}function se(){let e=new Map,r=setInterval(()=>{let t=Date.now();for(let[n,s]of e)t>=s&&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||w({maxRequests:10,windowMs:6e4}),n=x({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,o=e.sessionMaxAge||604800;e.stacknetJwtSecret||e.authSecret;return async function(p){let l=v(p),u=await t.check(`auth:${l}`);if(!u.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(u.retryAfter||60)}});let i;try{i=await p.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:c,message:d,signature:m,publicKey:y,otp:g,code:S,redirectUrl:T,stackId:b}=i,O=b||e.stackId,k;if(c&&d&&m){let z={"Content-Type":"application/json"},j=await fetch(`${e.stacknetUrl}/api/v2/stacks/${O}/auth/web3/verify`,{method:"POST",headers:z,body:JSON.stringify({chain:c,message:d,signature:m,public_key:y}),signal:AbortSignal.timeout(1e4)});if(!j.ok){let D=await j.json().catch(()=>({})),_=D?.error?.message||D?.message||D?.error||`StackNet returned ${j.status}`;return console.error(`[auth-callback] Verify failed: ${j.status}`,_),Response.json({error:"Wallet verification failed",detail:typeof _=="string"?_:void 0},{status:401})}let J=await j.json();k=J.data?.session||J.session||J.data||J,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(k||{}).join(", ")}`);}else return g||S?Response.json({error:"Use /api/auth/otp for OTP verification"},{status:400}):Response.json({error:"Provide wallet signature or OTP code"},{status:400});if(!k?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let h=JSON.parse(Buffer.from(k.jwt.split(".")[1],"base64url").toString()),N=Math.floor(Date.now()/1e3),K={...h,exp:N+s,iat:N},V=$(K,e.authSecret),E={userId:h.sub||h.user_id||h.session_id||h.global_id||"",address:k.address||h.address,chain:k.chain||c,expiresAt:Date.now()+o*1e3,authMethod:c?`web3:${c}`:"otp"},A=new Headers({"Content-Type":"application/json"}),q=e.secureCookies!==false?"; Secure":"",W=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";A.append("Set-Cookie",`stackauth_jwt=${V}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${o}${q}${W}`);let X=Buffer.from(JSON.stringify(E)).toString("base64url");return A.append("Set-Cookie",`stackauth_session=${X}; Path=/; SameSite=Lax; Max-Age=${o}${q}${W}`),n.generateToken(A),new Response(JSON.stringify({user:E}),{status:200,headers:A})}}function G(e,r){if(!r)return null;try{let t=P(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"),s=Buffer.from(JSON.stringify(t)).toString("base64url"),o=createHmac("sha256",r).update(`${n}.${s}`).digest("base64url");return `${n}.${s}.${o}`}catch{return null}}function M(e,r){let t=G(e,r);return t?{Cookie:`stackauth_jwt=${t}`}:{Cookie:`stackauth_jwt=${e}`}}function R(e){let r=e.headers.get("cookie");if(r){let n=r.split(";").map(s=>s.trim()).find(s=>s.startsWith("stackauth_jwt="));if(n)return n.slice(14)}let t=e.headers.get("authorization");return t?.startsWith("Bearer ")?t.slice(7):null}function ae(e){return async function(t){let n=R(t);if(n){let f=P(n),p=f?.session_id||f?.sub;if(p)try{await fetch(`${e.stacknetUrl}/api/v2/sessions/${p}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});}catch{}}let s=e.secureCookies!==false?"; Secure":"",o=e.cookieDomain?`; Domain=${e.cookieDomain}`:"",a=new Headers({"Content-Type":"application/json"});return a.append("Set-Cookie",`stackauth_jwt=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${s}${o}`),a.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${s}${o}`),a.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${s}${o}`),new Response(JSON.stringify({success:true}),{status:200,headers:a})}}function ie(e){let r=e.jwtExpiry||900,t=e.sessionMaxAge||604800;return async function(s){let o=R(s);if(!o)return Response.json({session:null},{status:200});let a=C(o,e.authSecret);if(!a)return Response.json({session:null},{status:200});let p={userId:a.sub||a.user_id||a.session_id||a.global_id||"",address:a.address,chain:a.chain,expiresAt:a.session_expires_at||(a.exp?a.exp*1e3:Date.now()+t*1e3),planId:a.plan_id,authMethod:a.auth_method},l=new Headers({"Content-Type":"application/json"}),u=H(o,e.authSecret,r,300);if(u){let i=e.secureCookies!==false?"; Secure":"",c=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";l.append("Set-Cookie",`stackauth_jwt=${u}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${i}${c}`);}return new Response(JSON.stringify({session:p}),{status:200,headers:l})}}function pe(e,r){if(e.length!==r.length)return false;try{return timingSafeEqual(Buffer.from(e),Buffer.from(r))}catch{return false}}function le(e){let r=e.rateLimiter||w({maxRequests:5,windowMs:3e5}),t=x({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,s=e.sessionMaxAge||604800;return async function(a){let f=v(a),p=await r.check(`otp:${f}`);if(!p.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||300)}});let l;try{l=await a.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:u}=l;if(!u||typeof u!="string"||u.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!pe(u,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let i=Math.floor(Date.now()/1e3),d={sub:`otp:${createHash("sha256").update(`otp:${u}:${Date.now()}`).digest("hex").slice(0,32)}`,auth_method:"otp",iat:i,exp:i+n},m=$(d,e.authSecret),y={userId:d.sub,expiresAt:Date.now()+s*1e3,authMethod:"otp"},g=new Headers({"Content-Type":"application/json"}),S=e.secureCookies!==false?"; Secure":"",T=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";g.append("Set-Cookie",`stackauth_jwt=${m}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${S}${T}`);let b=Buffer.from(JSON.stringify(y)).toString("base64url");return g.append("Set-Cookie",`stackauth_session=${b}; Path=/; SameSite=Lax; Max-Age=${s}${S}${T}`),t.generateToken(g),new Response(JSON.stringify({success:true,data:{user:y}}),{status:200,headers:g})}}function de(e){let r=x({secure:e.secureCookies!==false}),t=e.rateLimiter||w({maxRequests:20,windowMs:6e4}),n=e.stacknetJwtSecret||e.authSecret,s=e.jwtExpiry||900,o=e.sessionMaxAge||604800;function a(i){let c=R(i);if(!c)return null;let d=C(c,e.authSecret);return d?{jwt:c,payload:d}:null}function f(i,c){let d=H(i,e.authSecret,s,300);if(d){let m=e.secureCookies!==false?"; Secure":"",y=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";c.append("Set-Cookie",`stackauth_jwt=${d}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${o}${m}${y}`);}}async function p(i,c){let d=a(i);if(!d)return Response.json({error:"Unauthorized"},{status:401});let m=M(d.jwt,n),y=await fetch(`${e.stacknetUrl}${c}`,{headers:m,signal:AbortSignal.timeout(15e3)}),g=await y.json().catch(()=>({})),S=new Headers({"Content-Type":"application/json"});return f(d.jwt,S),new Response(JSON.stringify(g),{status:y.status,headers:S})}async function l(i,c,d){let m=a(i);if(!m)return Response.json({error:"Unauthorized"},{status:401});let y=r.validateRequest(i);if(!y.valid)return Response.json({error:y.error||"CSRF validation failed"},{status:403});let g=m.payload.sub||m.payload.user_id||"unknown";if(!(await t.check(`billing:${g}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let T=await i.json().catch(()=>({})),b=M(m.jwt,n);b["Content-Type"]="application/json";let O=await fetch(`${e.stacknetUrl}${c}`,{method:"POST",headers:b,body:JSON.stringify({...T,...d}),signal:AbortSignal.timeout(15e3)}),k=await O.json().catch(()=>({})),h=new Headers({"Content-Type":"application/json"});return f(m.jwt,h),new Response(JSON.stringify(k),{status:O.status,headers:h})}let u=`/api/v2/stacks/${e.stackId}`;return {plans:{GET:async i=>{let c=await fetch(`${e.stacknetUrl}${u}/plans`,{signal:AbortSignal.timeout(1e4)}),d=await c.json().catch(()=>({}));return Response.json(d,{status:c.status})}},subscription:{GET:(i=>p(i,`${u}/subscription`))},subscribe:{POST:(i=>{let c=new URL(i.url).origin;return l(i,`${u}/subscribe`,{successUrl:`${c}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing`})})},cancel:{POST:(i=>l(i,`${u}/cancel-subscription`))},usage:{GET:(i=>p(i,"/v1/account/usage"))},history:{GET:(i=>p(i,`${u}/billing`))},prepaid:{POST:(i=>{let c=new URL(i.url).origin;return l(i,`${u}/prepaid`,{successUrl:`${c}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing/prepaid`})})},verifyPrepaid:{POST:(i=>l(i,`${u}/verify-prepaid`))},verifySession:{POST:(i=>l(i,`${u}/verify-session`))}}}function fe(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 s=await t.text(),o=await fetch(`${e.stacknetUrl}/api/v2/stacks/${e.stackId}/webhook/stripe`,{method:"POST",headers:{"Content-Type":"application/json","stripe-signature":n},body:s,signal:AbortSignal.timeout(1e4)}),a=await o.json().catch(()=>({received:!0}));return Response.json(a,{status:o.status})}catch{return Response.json({error:"Webhook processing failed"},{status:502})}}}function L(){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 me(e){return async r=>{let t=await e(r),n=L(),s=new Headers(t.headers);for(let[o,a]of Object.entries(n))s.set(o,a);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:s})}}function ye(){return Object.entries(L()).map(([e,r])=>({key:e,value:r}))}export{M as buildStackNetHeaders,ne as createAuthCallback,de as createBillingProxy,x as createCSRFProtection,w as createInMemoryRateLimiter,se as createInMemoryReplayStore,ae as createLogoutHandler,le as createOTPHandler,ie as createSessionHandler,fe as createWebhookHandler,P as decodeJWTPayload,v as extractIP,R as extractJwt,I as generateToken,H as maybeRefreshJWT,ye as nextSecurityHeaders,G as resignForStackNet,L as securityHeaders,$ as signJWT,C as verifyJWT,F as verifyJWTSignature,me as withSecurityHeaders};
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 A(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"})),o=G(JSON.stringify(e)),s=createHmac("sha256",r).update(`${t}.${o}`).digest("base64url");return `${t}.${o}.${s}`}function V(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[o,s,n]=t,i=createHmac("sha256",r).update(`${o}.${s}`).digest("base64url"),f=Buffer.from(n),d=Buffer.from(i);return f.length!==d.length?!1:timingSafeEqual(f,d)}catch{return false}}function H(e,r){if(!V(e,r))return null;let t=A(e);return !t||t.exp&&t.exp<Math.floor(Date.now()/1e3)?null:t}function M(e,r,t=900,o=300){let s=H(e,r);return !s?.exp||s.exp*1e3-Date.now()>o*1e3?null:$({...s,exp:Math.floor(Date.now()/1e3)+t},r)}function W(e=32){return randomBytes(e).toString("hex")}function j(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 b(e={}){let r=e.cookieName||re,t=e.headerName||se,o=e.tokenLength||32,s=e.secure!==false;return {generateToken(n){let i=W(o),f=[`${r}=${i}`,"Path=/","SameSite=Lax"];return s&&f.push("Secure"),n.append("Set-Cookie",f.join("; ")),i},validateRequest(n){let i=n.headers.get("cookie");if(!i)return {valid:false,error:"No cookies present"};let f=i.split(";").map(l=>l.trim()).find(l=>l.startsWith(`${r}=`))?.slice(r.length+1);if(!f)return {valid:false,error:"CSRF cookie missing"};let d=n.headers.get(t);if(!d)return {valid:false,error:"CSRF header missing"};try{let l=Buffer.from(f),p=Buffer.from(d);return l.length!==p.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(l,p)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function P(e){let r=new Map,t=setInterval(()=>{let o=Date.now();for(let[s,n]of r)o>=n.resetAt&&r.delete(s);},6e4);return typeof t=="object"&&"unref"in t&&t.unref(),{async check(o){let s=Date.now(),n=r.get(o);return (!n||s>=n.resetAt)&&(n={count:0,resetAt:s+e.windowMs},r.set(o,n)),n.count++,n.count>e.maxRequests?{allowed:false,remaining:0,retryAfter:Math.ceil((n.resetAt-s)/1e3)}:{allowed:true,remaining:e.maxRequests-n.count}}}}function oe(){let e=new Map,r=setInterval(()=>{let t=Date.now();for(let[o,s]of e)t>=s&&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 ne(e,r){let t=r?.rateLimiter||P({maxRequests:10,windowMs:6e4}),o=b({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,n=e.sessionMaxAge||604800;e.stacknetJwtSecret||e.authSecret;return async function(d){let l=j(d),p=await t.check(`auth:${l}`);if(!p.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||60)}});let a;try{a=await d.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:c,message:u,signature:m,publicKey:y,otp:g,code:h,redirectUrl:x,stackId:R}=a,S=R||e.stackId,k;if(c&&u&&m){let Y={"Content-Type":"application/json"},J=await fetch(`${e.stacknetUrl}/api/v2/stacks/${S}/auth/web3/verify`,{method:"POST",headers:Y,body:JSON.stringify({chain:c,message:u,signature:m,public_key:y}),signal:AbortSignal.timeout(1e4)});if(!J.ok){let q=await J.json().catch(()=>({})),U=q?.error?.message||q?.message||q?.error||`StackNet returned ${J.status}`;return console.error(`[auth-callback] Verify failed: ${J.status}`,U),Response.json({error:"Wallet verification failed",detail:typeof U=="string"?U:void 0},{status:401})}let _=await J.json();k=_.data?.session||_.session||_.data||_,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(k||{}).join(", ")}`);}else return g||h?Response.json({error:"Use /api/auth/otp for OTP verification"},{status:400}):Response.json({error:"Provide wallet signature or OTP code"},{status:400});if(!k?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let w=JSON.parse(Buffer.from(k.jwt.split(".")[1],"base64url").toString()),N=Math.floor(Date.now()/1e3),I={...w,exp:N+s,iat:N},T=$(I,e.authSecret),v={userId:w.sub||w.user_id||w.session_id||w.global_id||"",address:k.address||w.address,chain:k.chain||c,expiresAt:Date.now()+n*1e3,authMethod:c?`web3:${c}`:"otp"},O=new Headers({"Content-Type":"application/json"}),D=e.secureCookies!==false?"; Secure":"",F=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";O.append("Set-Cookie",`stackauth_jwt=${T}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${n}${D}${F}`);let z=Buffer.from(JSON.stringify(v)).toString("base64url");return O.append("Set-Cookie",`stackauth_session=${z}; Path=/; SameSite=Lax; Max-Age=${n}${D}${F}`),o.generateToken(O),new Response(JSON.stringify({user:v}),{status:200,headers:O})}}function X(e,r){if(!r)return null;try{let t=A(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"),s=Buffer.from(JSON.stringify(t)).toString("base64url"),n=createHmac("sha256",r).update(`${o}.${s}`).digest("base64url");return `${o}.${s}.${n}`}catch{return null}}function L(e,r){let t=X(e,r);return t?{Cookie:`stackauth_jwt=${t}`}:{Cookie:`stackauth_jwt=${e}`}}function C(e){let r=e.headers.get("cookie");if(r){let o=r.split(";").map(s=>s.trim()).find(s=>s.startsWith("stackauth_jwt="));if(o)return o.slice(14)}let t=e.headers.get("authorization");return t?.startsWith("Bearer ")?t.slice(7):null}function ie(e){return async function(t){let o=C(t);if(o){let f=A(o),d=f?.session_id||f?.sub;if(d)try{await fetch(`${e.stacknetUrl}/api/v2/sessions/${d}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});}catch{}}let s=e.secureCookies!==false?"; Secure":"",n=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${s}${n}`),i.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${s}${n}`),i.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${s}${n}`),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(s){let n=C(s);if(!n)return Response.json({session:null},{status:200});let i=H(n,e.authSecret);if(!i)return Response.json({session:null},{status:200});let d={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},l=new Headers({"Content-Type":"application/json"}),p=M(n,e.authSecret,r,300);if(p){let a=e.secureCookies!==false?"; Secure":"",c=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";l.append("Set-Cookie",`stackauth_jwt=${p}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${a}${c}`);}return new Response(JSON.stringify({session:d}),{status:200,headers:l})}}function le(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||P({maxRequests:5,windowMs:3e5}),t=b({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,s=e.sessionMaxAge||604800;return async function(i){let f=j(i),d=await r.check(`otp:${f}`);if(!d.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(d.retryAfter||300)}});let l;try{l=await i.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:p}=l;if(!p||typeof p!="string"||p.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!le(p,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let a=Math.floor(Date.now()/1e3),u={sub:`otp:${createHash("sha256").update(`otp:${p}:${Date.now()}`).digest("hex").slice(0,32)}`,auth_method:"otp",iat:a,exp:a+o},m=$(u,e.authSecret),y={userId:u.sub,expiresAt:Date.now()+s*1e3,authMethod:"otp"},g=new Headers({"Content-Type":"application/json"}),h=e.secureCookies!==false?"; Secure":"",x=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";g.append("Set-Cookie",`stackauth_jwt=${m}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${h}${x}`);let R=Buffer.from(JSON.stringify(y)).toString("base64url");return g.append("Set-Cookie",`stackauth_session=${R}; Path=/; SameSite=Lax; Max-Age=${s}${h}${x}`),t.generateToken(g),new Response(JSON.stringify({success:true,data:{user:y}}),{status:200,headers:g})}}function fe(e,r){let t=r?.rateLimiter||P({maxRequests:10,windowMs:6e4}),o=b({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,n=e.sessionMaxAge||604800;async function i(d){let l=new URL(d.url),p=l.searchParams.get("provider"),a=l.searchParams.get("redirectUri")||l.searchParams.get("redirect_uri"),c=l.searchParams.get("stackId")||e.stackId;if(!p)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/${p}/initiate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({redirectUrl:a}),signal:AbortSignal.timeout(1e4)});if(!u.ok){let g=await u.json().catch(()=>({}));return Response.json({error:g.error?.message||`Failed to start OAuth flow: ${u.statusText}`},{status:u.status})}let m=await u.json(),y=m.data||m;return Response.json({redirect_url:y.url,state:y.state})}catch(u){return Response.json({error:u.message||"Failed to start OAuth flow"},{status:500})}}async function f(d){let l=j(d),p=await t.check(`oauth:${l}`);if(!p.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||60)}});let a;try{a=await d.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{provider:c,code:u,state:m,stackId:y}=a,g=y||e.stackId;if(!c||!u||!m)return Response.json({error:"Missing provider, code, or state"},{status:400});try{let h=await fetch(`${e.stacknetUrl}/api/v2/stacks/${g}/auth/oauth/${c}/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:u,state:m}),signal:AbortSignal.timeout(1e4)});if(!h.ok){let D=await h.json().catch(()=>({}));return Response.json({error:D.error?.message||`OAuth verification failed: ${h.statusText}`},{status:401})}let x=await h.json(),R=x.data?.session||x.session||x.data||x;if(!R?.jwt)return Response.json({error:"OAuth authentication failed \u2014 no session returned"},{status:401});let S=JSON.parse(Buffer.from(R.jwt.split(".")[1],"base64url").toString()),k=Math.floor(Date.now()/1e3),w=$({...S,exp:k+s,iat:k},e.authSecret),I={userId:S.sub||S.user_id||S.session_id||S.global_id||"",address:R.address||S.address,chain:void 0,expiresAt:Date.now()+n*1e3,authMethod:`oauth:${c}`},T=new Headers({"Content-Type":"application/json"}),E=e.secureCookies!==!1?"; Secure":"",v=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";T.append("Set-Cookie",`stackauth_jwt=${w}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${n}${E}${v}`);let O=Buffer.from(JSON.stringify(I)).toString("base64url");return T.append("Set-Cookie",`stackauth_session=${O}; Path=/; SameSite=Lax; Max-Age=${n}${E}${v}`),o.generateToken(T),new Response(JSON.stringify({user:I}),{status:200,headers:T})}catch(h){return Response.json({error:h.message||"OAuth callback failed"},{status:500})}}return {startFlow:i,handleCallback:f}}function me(e){let r=b({secure:e.secureCookies!==false}),t=e.rateLimiter||P({maxRequests:20,windowMs:6e4}),o=e.stacknetJwtSecret||e.authSecret,s=e.jwtExpiry||900,n=e.sessionMaxAge||604800;function i(a){let c=C(a);if(!c)return null;let u=H(c,e.authSecret);return u?{jwt:c,payload:u}:null}function f(a,c){let u=M(a,e.authSecret,s,300);if(u){let m=e.secureCookies!==false?"; Secure":"",y=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";c.append("Set-Cookie",`stackauth_jwt=${u}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${n}${m}${y}`);}}async function d(a,c){let u=i(a);if(!u)return Response.json({error:"Unauthorized"},{status:401});let m=L(u.jwt,o),y=await fetch(`${e.stacknetUrl}${c}`,{headers:m,signal:AbortSignal.timeout(15e3)}),g=await y.json().catch(()=>({})),h=new Headers({"Content-Type":"application/json"});return f(u.jwt,h),new Response(JSON.stringify(g),{status:y.status,headers:h})}async function l(a,c,u){let m=i(a);if(!m)return Response.json({error:"Unauthorized"},{status:401});let y=r.validateRequest(a);if(!y.valid)return Response.json({error:y.error||"CSRF validation failed"},{status:403});let g=m.payload.sub||m.payload.user_id||"unknown";if(!(await t.check(`billing:${g}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let x=await a.json().catch(()=>({})),R=L(m.jwt,o);R["Content-Type"]="application/json";let S=await fetch(`${e.stacknetUrl}${c}`,{method:"POST",headers:R,body:JSON.stringify({...x,...u}),signal:AbortSignal.timeout(15e3)}),k=await S.json().catch(()=>({})),w=new Headers({"Content-Type":"application/json"});return f(m.jwt,w),new Response(JSON.stringify(k),{status:S.status,headers:w})}let p=`/api/v2/stacks/${e.stackId}`;return {plans:{GET:async a=>{let c=await fetch(`${e.stacknetUrl}${p}/plans`,{signal:AbortSignal.timeout(1e4)}),u=await c.json().catch(()=>({}));return Response.json(u,{status:c.status})}},subscription:{GET:(a=>d(a,`${p}/subscription`))},subscribe:{POST:(a=>{let c=new URL(a.url).origin;return l(a,`${p}/subscribe`,{successUrl:`${c}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing`})})},cancel:{POST:(a=>l(a,`${p}/cancel-subscription`))},usage:{GET:(a=>d(a,"/v1/account/usage"))},history:{GET:(a=>d(a,`${p}/billing`))},prepaid:{POST:(a=>{let c=new URL(a.url).origin;return l(a,`${p}/prepaid`,{successUrl:`${c}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${c}/pricing/prepaid`})})},verifyPrepaid:{POST:(a=>l(a,`${p}/verify-prepaid`))},verifySession:{POST:(a=>l(a,`${p}/verify-session`))}}}function ye(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 s=await t.text(),n=await fetch(`${e.stacknetUrl}/api/v2/stacks/${e.stackId}/webhook/stripe`,{method:"POST",headers:{"Content-Type":"application/json","stripe-signature":o},body:s,signal:AbortSignal.timeout(1e4)}),i=await n.json().catch(()=>({received:!0}));return Response.json(i,{status:n.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 he(e){return async r=>{let t=await e(r),o=B(),s=new Headers(t.headers);for(let[n,i]of Object.entries(o))s.set(n,i);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:s})}}function ge(){return Object.entries(B()).map(([e,r])=>({key:e,value:r}))}export{L as buildStackNetHeaders,ne as createAuthCallback,me as createBillingProxy,b as createCSRFProtection,P as createInMemoryRateLimiter,oe as createInMemoryReplayStore,ie as createLogoutHandler,fe as createOAuthHandlers,de as createOTPHandler,ce as createSessionHandler,ye as createWebhookHandler,A as decodeJWTPayload,j as extractIP,C as extractJwt,W as generateToken,M as maybeRefreshJWT,ge as nextSecurityHeaders,X as resignForStackNet,B as securityHeaders,$ as signJWT,H as verifyJWT,V as verifyJWTSignature,he as withSecurityHeaders};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacknet/userutils",
3
- "version": "0.2.55",
3
+ "version": "0.3.5",
4
4
  "description": "Reusable auth, billing, and security utilities for StackNet stacks and applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",