@stacknet/userutils 0.5.6 → 0.6.0

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,5 +1,6 @@
1
- import { S as Session } from '../auth-DR2aYcor.js';
2
- import { e as ServerConfig } from '../config-_ZjAzNkJ.js';
1
+ import { S as Session } from '../auth-c1d7Eji2.js';
2
+ import { e as ServerConfig, I as IPExtractorConfig } from '../config-xNca5ufB.js';
3
+ export { f as decodeJWTPayload, g as extractIP, h as generateToken, m as maybeRefreshJWT, s as signJWT, v as verifyJWT, i as verifyJWTSignature } from '../config-xNca5ufB.js';
3
4
 
4
5
  /**
5
6
  * Server-only session type that includes the JWT.
@@ -75,6 +76,10 @@ interface OTPHandlerConfig extends Pick<ServerConfig, 'authSecret' | 'secureCook
75
76
  otpSecret: string;
76
77
  /** Rate limiter (default: 5 attempts per 5 min per IP) */
77
78
  rateLimiter?: RateLimiter;
79
+ /** How to extract the real client IP for rate-limit keys. Defaults to
80
+ * `{ trustedProxyCount: 1 }`. Set `trustedProxyCount: 0` if the handler
81
+ * is exposed directly (no proxy in front). */
82
+ ipConfig?: IPExtractorConfig;
78
83
  }
79
84
  /**
80
85
  * Factory: POST handler for OTP verification.
@@ -111,6 +116,21 @@ declare function createGoogleOneTapHandler(config: ServerConfig, opts?: GoogleOn
111
116
  interface BillingProxyConfig extends Pick<ServerConfig, 'authSecret' | 'stacknetUrl' | 'stackId' | 'stacknetJwtSecret' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge' | 'jwtExpiry'> {
112
117
  /** Rate limiter for mutations (default: 20/min per user) */
113
118
  rateLimiter?: RateLimiter;
119
+ /**
120
+ * Canonical absolute origin (e.g. "https://app.example.com") used when
121
+ * constructing Stripe success/cancel URLs. STRONGLY RECOMMENDED.
122
+ *
123
+ * Without this, the origin is derived from the request URL — which is
124
+ * populated from the `Host` header. If the app is deployed behind a
125
+ * proxy that does not validate / rewrite `Host`, an attacker can send
126
+ * `Host: evil.example` and the post-checkout redirect will point there.
127
+ * Stripe's dashboard allowlist catches most cases, but we should not
128
+ * depend on that alone.
129
+ *
130
+ * Must be a full http(s) origin with no path. Factory throws on
131
+ * malformed input so misconfigurations surface at boot, not at runtime.
132
+ */
133
+ canonicalOrigin?: string;
114
134
  }
115
135
  type Handler = (request: Request) => Promise<Response>;
116
136
  /**
@@ -221,24 +241,6 @@ declare function nextSecurityHeaders(): Array<{
221
241
  value: string;
222
242
  }>;
223
243
 
224
- /** Decode JWT payload without verification (server-side helper) */
225
- declare function decodeJWTPayload(jwt: string): Record<string, any> | null;
226
- /** Sign a JWT with HMAC-SHA256 */
227
- declare function signJWT(payload: Record<string, any>, secret: string): string;
228
- /** Verify a JWT signature with HMAC-SHA256 (constant-time comparison) */
229
- declare function verifyJWTSignature(jwt: string, secret: string): boolean;
230
- /** Verify JWT and return payload if valid (checks signature + expiry) */
231
- declare function verifyJWT(jwt: string, secret: string): Record<string, any> | null;
232
- /**
233
- * Check if JWT needs refresh and return a new one if so.
234
- * Returns null if no refresh needed or JWT is invalid.
235
- */
236
- declare function maybeRefreshJWT(jwt: string, secret: string, expirySeconds?: number, refreshWindowSeconds?: number): string | null;
237
- /** Generate a cryptographically secure random token */
238
- declare function generateToken(bytes?: number): string;
239
- /** Extract IP address from request headers */
240
- declare function extractIP(request: Request): string;
241
-
242
244
  /**
243
245
  * Re-sign a JWT using StackNet's HMAC-SHA256 scheme.
244
246
  *
@@ -261,4 +263,97 @@ declare function buildStackNetHeaders(jwt: string, stacknetJwtSecret: string): R
261
263
  */
262
264
  declare function extractJwt(request: Request): string | null;
263
265
 
264
- export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type GoogleOneTapHandlerConfig, type OAuthHandlerConfig, type OTPHandlerConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createGoogleOneTapHandler, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOAuthHandlers, createOTPHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, maybeRefreshJWT, nextSecurityHeaders, resignForStackNet, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
266
+ /**
267
+ * Server-side proxy helpers for StackNet preview codes.
268
+ *
269
+ * Preview codes are admin-minted 6-digit access credentials with a
270
+ * per-code token budget. Only the pinned admin global id (enforced
271
+ * by StackNet's state machine) can mint / list / revoke codes.
272
+ *
273
+ * These helpers wrap `fetch` against the StackNet HTTP layer with
274
+ * re-signed JWT cookies (same pattern as `buildStackNetHeaders`).
275
+ * Admin-console API routes are expected to call them; the raw
276
+ * endpoints are NOT exposed in the client bundle.
277
+ */
278
+ interface PreviewCode {
279
+ code: string;
280
+ createdBy: string;
281
+ tokenBudget: number;
282
+ tokensUsed: number;
283
+ tokensRemaining: number;
284
+ createdAt: number;
285
+ expiresAt: number | null;
286
+ revoked: boolean;
287
+ /** Optional human-readable label (e.g. "Tester: Alice"). Absent on
288
+ * codes minted before the name field shipped. */
289
+ name?: string | null;
290
+ }
291
+ interface MintPreviewCodeOptions {
292
+ /** Token budget for the new code. Must be > 0. */
293
+ tokenBudget: number;
294
+ /** Optional explicit 6-digit code string. Server generates one
295
+ * if omitted. */
296
+ code?: string;
297
+ /** Optional Unix-ms expiry. */
298
+ expiresAt?: number;
299
+ /** Optional human-readable label for the code. Shown in the admin
300
+ * list so the operator can tell codes apart. */
301
+ name?: string;
302
+ }
303
+ interface PreviewCodesProxyConfig {
304
+ /** StackNet base URL (no trailing slash). */
305
+ stacknetBaseUrl: string;
306
+ /** Shared HMAC secret for re-signing the caller's JWT. */
307
+ stacknetJwtSecret: string;
308
+ /** Caller's StackAuth JWT (user identity). */
309
+ jwt: string;
310
+ }
311
+ /** Admin-only: mint a new preview code. Returns the new code row. */
312
+ declare function mintPreviewCode(cfg: PreviewCodesProxyConfig, options: MintPreviewCodeOptions): Promise<{
313
+ minted: boolean;
314
+ code: PreviewCode;
315
+ } | {
316
+ error: string;
317
+ status: number;
318
+ }>;
319
+ /** Admin-only: list every preview code in the system. */
320
+ declare function listPreviewCodes(cfg: PreviewCodesProxyConfig): Promise<PreviewCode[] | {
321
+ error: string;
322
+ status: number;
323
+ }>;
324
+ /** Public: read a code's balance + status. Used by auth middleware. */
325
+ declare function getPreviewCode(stacknetBaseUrl: string, code: string): Promise<PreviewCode | null>;
326
+ /** Admin-only: revoke a preview code. */
327
+ declare function revokePreviewCode(cfg: PreviewCodesProxyConfig, code: string): Promise<{
328
+ revoked: boolean;
329
+ code: PreviewCode;
330
+ } | {
331
+ error: string;
332
+ status: number;
333
+ }>;
334
+ /** Internal: debit tokens from a preview code. Called by the metering
335
+ * layer after inference completes. */
336
+ declare function redeemPreviewCode(stacknetBaseUrl: string, code: string, tokens: number): Promise<{
337
+ redeemed: boolean;
338
+ code: string;
339
+ tokensUsed: number;
340
+ tokensRemaining: number;
341
+ } | {
342
+ error: string;
343
+ status: number;
344
+ }>;
345
+ /** Allowlist of admin global ids that can mint / revoke preview
346
+ * codes. Mirrors PREVIEW_CODE_ADMIN_GLOBAL_IDS in the Rust state
347
+ * machine — must stay in sync. Admin-console UI and API route guards
348
+ * should call `isPreviewCodeAdmin(currentUser.userId)` instead of
349
+ * comparing against a single constant so every entry is accepted. */
350
+ declare const PREVIEW_CODE_ADMIN_GLOBAL_IDS: readonly string[];
351
+ /** Returns true if the given global id is in the preview-code admin
352
+ * allowlist. */
353
+ declare function isPreviewCodeAdmin(globalId: string | null | undefined): boolean;
354
+ /** Back-compat alias for callers that only need a single canonical
355
+ * admin id for display/logging. Don't use for gating — use
356
+ * `isPreviewCodeAdmin()` to accept every entry in the allowlist. */
357
+ declare const PREVIEW_CODE_ADMIN_GLOBAL_ID: string;
358
+
359
+ export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type GoogleOneTapHandlerConfig, IPExtractorConfig, type MintPreviewCodeOptions, type OAuthHandlerConfig, type OTPHandlerConfig, PREVIEW_CODE_ADMIN_GLOBAL_ID, PREVIEW_CODE_ADMIN_GLOBAL_IDS, type PreviewCode, type PreviewCodesProxyConfig, type RateLimiter, type ReplayStore, ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createGoogleOneTapHandler, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOAuthHandlers, createOTPHandler, createSessionHandler, createWebhookHandler, extractJwt, getPreviewCode, isPreviewCodeAdmin, listPreviewCodes, mintPreviewCode, nextSecurityHeaders, redeemPreviewCode, resignForStackNet, revokePreviewCode, securityHeaders, withSecurityHeaders };
@@ -1,2 +1,2 @@
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
+ import {createHmac,timingSafeEqual,randomBytes,createHash}from'crypto';function Z(e){return Buffer.from(e).toString("base64url")}function se(e){return Buffer.from(e,"base64url").toString()}function N(e){try{let r=e.split(".");return r.length!==3?null:JSON.parse(se(r[1]))}catch{return null}}function j(e,r){let t=Z(JSON.stringify({alg:"HS256",typ:"JWT"})),o=Z(JSON.stringify(e)),n=createHmac("sha256",r).update(`${t}.${o}`).digest("base64url");return `${t}.${o}.${n}`}function X(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[o,n,a]=t,c=createHmac("sha256",r).update(`${o}.${n}`).digest("base64url"),g=Buffer.from(a),f=Buffer.from(c);return g.length!==f.length?!1:timingSafeEqual(g,f)}catch{return false}}function C(e,r){if(!X(e,r))return null;let t=N(e);return !t||t.exp&&t.exp<Math.floor(Date.now()/1e3)?null:t}function D(e,r,t=900,o=300){let n=C(e,r);return !n?.exp||n.exp*1e3-Date.now()>o*1e3?null:j({...n,exp:Math.floor(Date.now()/1e3)+t},r)}function F(e=32){return randomBytes(e).toString("hex")}function O(e,r={}){let t=r.trustedProxyCount??1,o=r.trustRealIpHeader===true;if(r.customExtractor){let n=r.customExtractor(e);if(n)return n}if(t>0){let n=e.headers.get("x-forwarded-for");if(n){let a=n.split(",").map(g=>g.trim()).filter(Boolean),c=a.length-t;if(c>=0&&c<a.length)return a[c]}}if(o){let n=e.headers.get("x-real-ip");if(n)return n.trim()}return "unknown"}var ne="__csrf",ae="x-csrf-token",ie=/^[A-Za-z_$][A-Za-z0-9_$-]{0,63}$/,ce=/^[A-Za-z][A-Za-z0-9-]{0,63}$/;function v(e={}){let r=e.cookieName||ne,t=e.headerName||ae,o=e.tokenLength||32,n=e.secure!==false;if(!ie.test(r))throw new Error(`createCSRFProtection: invalid cookieName "${r}"`);if(!ce.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 c=F(o),g=[`${r}=${c}`,"Path=/","SameSite=Lax"];return n&&g.push("Secure"),a.append("Set-Cookie",g.join("; ")),c},validateRequest(a){let c=a.headers.get("cookie");if(!c)return {valid:false,error:"No cookies present"};let g=c.split(";").map(m=>m.trim()).find(m=>m.startsWith(`${r}=`))?.slice(r.length+1);if(!g)return {valid:false,error:"CSRF cookie missing"};let f=a.headers.get(t);if(!f)return {valid:false,error:"CSRF header missing"};try{let m=Buffer.from(g),p=Buffer.from(f);return m.length!==p.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(m,p)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function $(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 ue(){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 le(e,r){let t=r?.rateLimiter||$({maxRequests:10,windowMs:6e4}),o=v({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;return async function(f){let m=O(f,e.ipConfig),p=await t.check(`auth:${m}`);if(!p.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||60)}});let y;try{y=await f.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:u,message:d,signature:s,publicKey:i,otp:l,code:h,redirectUrl:k,stackId:x}=y,S=x||e.stackId,w;if(u&&d&&s){let ee={"Content-Type":"application/json"},L=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(S)}/auth/web3/verify`,{method:"POST",headers:ee,body:JSON.stringify({chain:u,message:d,signature:s,public_key:i}),signal:AbortSignal.timeout(1e4)});if(!L.ok){let q=await L.json().catch(()=>({})),W=q?.error?.message||q?.message||q?.error||`StackNet returned ${L.status}`;return console.error(`[auth-callback] Verify failed: ${L.status}`,W),Response.json({error:"Wallet verification failed",detail:typeof W=="string"?W:void 0},{status:401})}let U=await L.json();w=U.data?.session||U.session||U.data||U,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(w||{}).join(", ")}`);}else return l||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(!w?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let R=C(w.jwt,c);if(!R)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let P=Math.floor(Date.now()/1e3),A={...R,exp:P+n,iat:P},T=j(A,e.authSecret),I={userId:R.sub||R.user_id||R.session_id||R.global_id||"",address:w.address||R.address,chain:w.chain||u,expiresAt:Date.now()+a*1e3,authMethod:u?`web3:${u}`:"otp"},b=new Headers({"Content-Type":"application/json"}),H=e.secureCookies!==false?"; Secure":"",_=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";b.append("Set-Cookie",`stackauth_jwt=${T}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${H}${_}`);let B=Buffer.from(JSON.stringify(I)).toString("base64url");return b.append("Set-Cookie",`stackauth_session=${B}; Path=/; SameSite=Lax; Max-Age=${a}${H}${_}`),o.generateToken(b),new Response(JSON.stringify({user:I}),{status:200,headers:b})}}function Q(e,r){if(!r)return null;try{let t=N(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 de=/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;function Y(e){return typeof e!="string"||e.length===0||e.length>8192?null:de.test(e)?e:null}function J(e,r){let t=Q(e,r),o=t&&Y(t);if(o)return {Cookie:`stackauth_jwt=${o}`};let n=Y(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 fe(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 g=C(o,e.authSecret),f=g?.session_id||g?.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}`:"",c=new Headers({"Content-Type":"application/json"});return c.append("Set-Cookie",`stackauth_jwt=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${n}${a}`),c.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${n}${a}`),c.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${n}${a}`),new Response(JSON.stringify({success:true}),{status:200,headers:c})}}function me(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 c=C(a,e.authSecret);if(!c)return Response.json({session:null},{status:200});let f={userId:c.sub||c.user_id||c.session_id||c.global_id||"",address:c.address,chain:c.chain,expiresAt:c.session_expires_at||(c.exp?c.exp*1e3:Date.now()+t*1e3),planId:c.plan_id,authMethod:c.auth_method},m=new Headers({"Content-Type":"application/json"}),p=D(a,e.authSecret,r,300);if(p){let y=e.secureCookies!==false?"; Secure":"",u=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";m.append("Set-Cookie",`stackauth_jwt=${p}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${y}${u}`);}return new Response(JSON.stringify({session:f}),{status:200,headers:m})}}function ye(e,r){if(e.length!==r.length)return false;try{return timingSafeEqual(Buffer.from(e),Buffer.from(r))}catch{return false}}function we(e){let r=e.rateLimiter||$({maxRequests:5,windowMs:3e5}),t=v({secure:e.secureCookies!==false}),o=e.jwtExpiry||900,n=e.sessionMaxAge||604800;return async function(c){let g=O(c,e.ipConfig),f=await r.check(`otp:${g}`);if(!f.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(f.retryAfter||300)}});let m;try{m=await c.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:p}=m;if(!p||typeof p!="string"||p.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!ye(p,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let y=Math.floor(Date.now()/1e3),d={sub:`preview:otp:${createHash("sha256").update(`otp:${p}:${Date.now()}`).digest("hex").slice(0,32)}`,scope:"preview",auth_method:"otp",credentials:["otp"],iat:y,exp:y+o},s=j(d,e.authSecret),i={userId:d.sub,expiresAt:Date.now()+n*1e3,authMethod:"otp",scope:"preview"},l=new Headers({"Content-Type":"application/json"}),h=e.secureCookies!==false?"; Secure":"",k=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";l.append("Set-Cookie",`stackauth_jwt=${s}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${n}${h}${k}`);let x=Buffer.from(JSON.stringify(i)).toString("base64url");return l.append("Set-Cookie",`stackauth_session=${x}; Path=/; SameSite=Lax; Max-Age=${n}${h}${k}`),t.generateToken(l),new Response(JSON.stringify({success:true,data:{user:i}}),{status:200,headers:l})}}function Se(e,r){let t=r?.rateLimiter||$({maxRequests:10,windowMs:6e4}),o=v({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;async function g(m){let p=new URL(m.url),y=p.searchParams.get("provider"),u=p.searchParams.get("redirectUri")||p.searchParams.get("redirect_uri"),d=p.searchParams.get("stackId")||e.stackId;if(!y)return Response.json({error:"Missing provider parameter"},{status:400});if(!u)return Response.json({error:"Missing redirectUri parameter"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(y))return Response.json({error:"Invalid provider name"},{status:400});if(!d||!/^[a-zA-Z0-9_-]{1,64}$/.test(d))return Response.json({error:"Invalid stackId"},{status:400});try{let s=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(d)}/auth/oauth/${encodeURIComponent(y)}/initiate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({redirectUrl:u}),signal:AbortSignal.timeout(1e4)});if(!s.ok){let h=await s.json().catch(()=>({}));return Response.json({error:h.error?.message||`Failed to start OAuth flow: ${s.statusText}`},{status:s.status})}let i=await s.json(),l=i.data||i;return Response.json({redirect_url:l.url,state:l.state})}catch(s){return Response.json({error:s.message||"Failed to start OAuth flow"},{status:500})}}async function f(m){let p=O(m,e.ipConfig),y=await t.check(`oauth:${p}`);if(!y.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(y.retryAfter||60)}});let u;try{u=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{provider:d,code:s,state:i,stackId:l}=u,h=l||e.stackId;if(!d||!s||!i)return Response.json({error:"Missing provider, code, or state"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(d))return Response.json({error:"Invalid provider name"},{status:400});if(!h||!/^[a-zA-Z0-9_-]{1,64}$/.test(h))return Response.json({error:"Invalid stackId"},{status:400});try{let k=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(h)}/auth/oauth/${encodeURIComponent(d)}/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:s,state:i}),signal:AbortSignal.timeout(1e4)});if(!k.ok){let _=await k.json().catch(()=>({}));return Response.json({error:_.error?.message||`OAuth verification failed: ${k.statusText}`},{status:401})}let x=await k.json(),S=x.data?.session||x.session||x.data||x;if(!S?.jwt)return Response.json({error:"OAuth authentication failed \u2014 no session returned"},{status:401});let w=C(S.jwt,c);if(!w)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let R=Math.floor(Date.now()/1e3),P=j({...w,exp:R+n,iat:R},e.authSecret),T={userId:w.sub||w.user_id||w.session_id||w.global_id||"",address:S.address||w.address,chain:void 0,expiresAt:Date.now()+a*1e3,authMethod:`oauth:${d}`},E=new Headers({"Content-Type":"application/json"}),I=e.secureCookies!==!1?"; Secure":"",b=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";E.append("Set-Cookie",`stackauth_jwt=${P}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${I}${b}`);let H=Buffer.from(JSON.stringify(T)).toString("base64url");return E.append("Set-Cookie",`stackauth_session=${H}; Path=/; SameSite=Lax; Max-Age=${a}${I}${b}`),o.generateToken(E),new Response(JSON.stringify({user:T}),{status:200,headers:E})}catch(k){return Response.json({error:k.message||"OAuth callback failed"},{status:500})}}return {startFlow:g,handleCallback:f}}function ke(e,r){let t=r?.rateLimiter||$({maxRequests:10,windowMs:6e4}),o=v({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,a=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;return async function(f){let m=O(f,e.ipConfig),p=await t.check(`google-onetap:${m}`);if(!p.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(p.retryAfter||60)}});let y;try{y=await f.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{credential:u,stackId:d}=y,s=d||e.stackId;if(!u)return Response.json({error:"Missing credential"},{status:400});if(u.split(".").length!==3)return Response.json({error:"Invalid credential format"},{status:400});let i;try{let S=await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(u)}`,{signal:AbortSignal.timeout(1e4)});if(!S.ok)return Response.json({error:"Google credential verification failed"},{status:401});i=await S.json();}catch{return Response.json({error:"Failed to verify Google credential"},{status:500})}if(!i.sub||!i.email)return Response.json({error:"Invalid Google token \u2014 missing user info"},{status:401});if(i.iss!=="https://accounts.google.com"&&i.iss!=="accounts.google.com")return Response.json({error:"Invalid Google token issuer"},{status:401});let l=typeof i.exp=="string"?parseInt(i.exp,10):Number(i.exp);if(!Number.isFinite(l)||l<Math.floor(Date.now()/1e3))return Response.json({error:"Google token expired"},{status:401});let h=e.googleClientIds||(e.googleClientId?[e.googleClientId]:[]);if(h.length===0)return Response.json({error:"Google One Tap not configured \u2014 set ServerConfig.googleClientId(s)"},{status:500});if(!i.aud||!h.includes(i.aud))return Response.json({error:"Invalid Google token audience"},{status:401});if(!(i.email_verified===true||i.email_verified==="true"))return Response.json({error:"Google email is not verified"},{status:401});let x={sub:i.sub,email:i.email,name:i.name,picture:i.picture};try{let S=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(s)}/auth/oauth/google/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:u,google_id:x.sub,email:x.email,name:x.name,picture:x.picture,one_tap:!0}),signal:AbortSignal.timeout(1e4)});if(!S.ok){let P=await S.json().catch(()=>({}));return Response.json({error:P?.error?.message||"Google sign-in failed"},{status:S.status})}let w=await S.json(),R=w.data?.session||w.session||w.data||w;if(R?.jwt){let P=C(R.jwt,c);if(!P)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let A=Math.floor(Date.now()/1e3),T=j({...P,exp:A+n,iat:A},e.authSecret),I={userId:P.sub||P.user_id||x.sub,address:x.email||R.address,chain:void 0,expiresAt:Date.now()+a*1e3,authMethod:"oauth:google"},b=new Headers({"Content-Type":"application/json"}),H=e.secureCookies!==!1?"; Secure":"",_=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";b.append("Set-Cookie",`stackauth_jwt=${T}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${H}${_}`);let B=Buffer.from(JSON.stringify(I)).toString("base64url");return b.append("Set-Cookie",`stackauth_session=${B}; Path=/; SameSite=Lax; Max-Age=${a}${H}${_}`),o.generateToken(b),new Response(JSON.stringify({user:I}),{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 xe(e){let r=v({secure:e.secureCookies!==false}),t=e.rateLimiter||$({maxRequests:20,windowMs:6e4}),o=e.stacknetJwtSecret||e.authSecret,n=e.jwtExpiry||900,a=e.sessionMaxAge||604800,c=null;if(e.canonicalOrigin){let s;try{s=new URL(e.canonicalOrigin);}catch{throw new Error(`createBillingProxy: canonicalOrigin "${e.canonicalOrigin}" is not a valid URL`)}if(s.protocol!=="http:"&&s.protocol!=="https:")throw new Error(`createBillingProxy: canonicalOrigin must be http or https (got "${s.protocol}")`);if(s.pathname!=="/"&&s.pathname!=="")throw new Error(`createBillingProxy: canonicalOrigin must have no path (got "${s.pathname}")`);c=s.origin;}let g=false;function f(s){if(c)return c;g||(g=true,console.warn("[userutils] createBillingProxy: canonicalOrigin not set \u2014 falling back to request origin. Set canonicalOrigin to the public URL of this app to prevent Host-header spoofing of Stripe success URLs."));try{let i=new URL(s.url);return i.protocol!=="http:"&&i.protocol!=="https:"?null:i.origin}catch{return null}}function m(s){let i=M(s);if(!i)return null;let l=C(i,e.authSecret);return l?{jwt:i,payload:l}:null}function p(s,i){let l=D(s,e.authSecret,n,300);if(l){let h=e.secureCookies!==false?"; Secure":"",k=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";i.append("Set-Cookie",`stackauth_jwt=${l}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${a}${h}${k}`);}}async function y(s,i){let l=m(s);if(!l)return Response.json({error:"Unauthorized"},{status:401});let h=J(l.jwt,o),k=await fetch(`${e.stacknetUrl}${i}`,{headers:h,signal:AbortSignal.timeout(15e3)}),x=await k.json().catch(()=>({})),S=new Headers({"Content-Type":"application/json"});return p(l.jwt,S),new Response(JSON.stringify(x),{status:k.status,headers:S})}async function u(s,i,l){let h=m(s);if(!h)return Response.json({error:"Unauthorized"},{status:401});let k=r.validateRequest(s);if(!k.valid)return Response.json({error:k.error||"CSRF validation failed"},{status:403});let x=h.payload.sub||h.payload.user_id||"unknown";if(!(await t.check(`billing:${x}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let w=await s.json().catch(()=>({})),R=J(h.jwt,o);R["Content-Type"]="application/json";let P=await fetch(`${e.stacknetUrl}${i}`,{method:"POST",headers:R,body:JSON.stringify({...w,...l}),signal:AbortSignal.timeout(15e3)}),A=await P.json().catch(()=>({})),T=new Headers({"Content-Type":"application/json"});return p(h.jwt,T),new Response(JSON.stringify(A),{status:P.status,headers:T})}let d=`/api/v2/stacks/${encodeURIComponent(e.stackId)}`;return {plans:{GET:async s=>{let i=await fetch(`${e.stacknetUrl}${d}/plans`,{signal:AbortSignal.timeout(1e4)}),l=await i.json().catch(()=>({}));return Response.json(l,{status:i.status})}},subscription:{GET:(s=>y(s,`${d}/subscription`))},subscribe:{POST:(s=>{let i=f(s);return i?u(s,`${d}/subscribe`,{successUrl:`${i}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${i}/pricing`}):Promise.resolve(Response.json({error:"Invalid request origin for checkout"},{status:400}))})},cancel:{POST:(s=>u(s,`${d}/cancel-subscription`))},usage:{GET:(s=>y(s,"/v1/account/usage"))},history:{GET:(s=>y(s,`${d}/billing`))},prepaid:{POST:(s=>{let i=f(s);return i?u(s,`${d}/prepaid`,{successUrl:`${i}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${i}/pricing/prepaid`}):Promise.resolve(Response.json({error:"Invalid request origin for checkout"},{status:400}))})},verifyPrepaid:{POST:(s=>u(s,`${d}/verify-prepaid`))},verifySession:{POST:(s=>u(s,`${d}/verify-session`))},subscribeSol:{POST:(s=>u(s,`${d}/subscribe-sol`))},prepaidSol:{POST:(s=>u(s,`${d}/prepaid-sol`))},topup:{POST:(s=>u(s,"/v1/account/topup"))}}}function Re(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)}),c=await a.json().catch(()=>({received:!0}));return Response.json(c,{status:a.status})}catch{return Response.json({error:"Webhook processing failed"},{status:502})}}}function G(){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 Pe(e){return async r=>{let t=await e(r),o=G(),n=new Headers(t.headers);for(let[a,c]of Object.entries(o))n.set(a,c);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:n})}}function Ce(){return Object.entries(G()).map(([e,r])=>({key:e,value:r}))}function z(e){return {"Content-Type":"application/json",...J(e.jwt,e.stacknetJwtSecret)}}async function be(e,r){let t=await fetch(`${e.stacknetBaseUrl}/v1/preview-codes`,{method:"POST",headers:z(e),body:JSON.stringify({token_budget:r.tokenBudget,code:r.code,expires_at:r.expiresAt,name:r.name})});return t.ok?await t.json():{error:await t.text().catch(()=>"")||`HTTP ${t.status}`,status:t.status}}async function ve(e){let r=await fetch(`${e.stacknetBaseUrl}/v1/preview-codes`,{method:"GET",headers:z(e)});return r.ok?(await r.json()).codes:{error:await r.text().catch(()=>"")||`HTTP ${r.status}`,status:r.status}}async function $e(e,r){let t=await fetch(`${e}/v1/preview-codes/${encodeURIComponent(r)}`,{method:"GET"});return t.status===404||!t.ok?null:await t.json()}async function je(e,r){let t=await fetch(`${e.stacknetBaseUrl}/v1/preview-codes/${encodeURIComponent(r)}`,{method:"DELETE",headers:z(e)});return t.ok?await t.json():{error:await t.text().catch(()=>"")||`HTTP ${t.status}`,status:t.status}}async function Te(e,r,t){let o=await fetch(`${e}/v1/preview-codes/${encodeURIComponent(r)}/redeem`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tokens:t})});return o.ok?await o.json():{error:await o.text().catch(()=>"")||`HTTP ${o.status}`,status:o.status}}var V=["127924fc17182f69cb463d9977348c482d2e784dfdd16f9e6eecc4db07fb04c3","HxLBLBjKbSrJSFCE7LGs9FxMA6M5bztmpNoLuB43HTFs"];function Oe(e){return e?V.includes(e):false}var Ie=V[0];
2
+ export{Ie as PREVIEW_CODE_ADMIN_GLOBAL_ID,V as PREVIEW_CODE_ADMIN_GLOBAL_IDS,J as buildStackNetHeaders,le as createAuthCallback,xe as createBillingProxy,v as createCSRFProtection,ke as createGoogleOneTapHandler,$ as createInMemoryRateLimiter,ue as createInMemoryReplayStore,fe as createLogoutHandler,Se as createOAuthHandlers,we as createOTPHandler,me as createSessionHandler,Re as createWebhookHandler,N as decodeJWTPayload,O as extractIP,M as extractJwt,F as generateToken,$e as getPreviewCode,Oe as isPreviewCodeAdmin,ve as listPreviewCodes,D as maybeRefreshJWT,be as mintPreviewCode,Ce as nextSecurityHeaders,Te as redeemPreviewCode,Q as resignForStackNet,je as revokePreviewCode,G as securityHeaders,j as signJWT,C as verifyJWT,X as verifyJWTSignature,Pe as withSecurityHeaders};
@@ -1,5 +1,5 @@
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';
1
+ export { A as APIResponse, M as MPCNode, N as NetworkStatus, P as PublicSession, S as Session, W as Web3Chain } from '../auth-c1d7Eji2.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-CLzVWDrU.cjs';
3
3
 
4
4
  /** User profile — global (network-wide) or stack-scoped */
5
5
  interface UserProfile {
@@ -1,5 +1,5 @@
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';
1
+ export { A as APIResponse, M as MPCNode, N as NetworkStatus, P as PublicSession, S as Session, W as Web3Chain } from '../auth-c1d7Eji2.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-xNca5ufB.js';
3
3
 
4
4
  /** User profile — global (network-wide) or stack-scoped */
5
5
  interface UserProfile {
@@ -1,4 +1,4 @@
1
- import { P as PublicSession, W as Web3Chain } from './auth-DR2aYcor.cjs';
1
+ import { P as PublicSession, W as Web3Chain } from './auth-c1d7Eji2.cjs';
2
2
 
3
3
  interface AuthTransport {
4
4
  /** Store credentials after successful login */
@@ -1,4 +1,4 @@
1
- import { P as PublicSession, W as Web3Chain } from './auth-DR2aYcor.js';
1
+ import { P as PublicSession, W as Web3Chain } from './auth-c1d7Eji2.js';
2
2
 
3
3
  interface AuthTransport {
4
4
  /** Store credentials after successful login */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacknet/userutils",
3
- "version": "0.5.6",
3
+ "version": "0.6.0",
4
4
  "description": "Reusable auth, billing, and security utilities for StackNet stacks and applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1,123 +0,0 @@
1
- import { P as PublicSession } from './auth-DR2aYcor.cjs';
2
-
3
- interface BillingPlan {
4
- id: string;
5
- name: string;
6
- price_cents: number;
7
- token_allocation: number;
8
- features: string;
9
- sort_order?: number;
10
- is_active?: boolean;
11
- }
12
- interface Subscription {
13
- plan?: {
14
- id: string;
15
- name: string;
16
- priceCents: number;
17
- tokenAllocation: number;
18
- };
19
- planId?: string;
20
- planName?: string;
21
- status: string;
22
- tokensUsed?: number;
23
- tokensRemaining?: number;
24
- usagePercent?: number;
25
- periodStart?: number;
26
- periodEnd?: number;
27
- cancelAtPeriodEnd?: boolean;
28
- stripeCustomerId?: string;
29
- }
30
- interface UsageSummary {
31
- ownerId: string;
32
- planAllocation: number;
33
- planId: string | null;
34
- planName: string;
35
- inferenceUsed: number;
36
- ledgerSpent: number;
37
- totalUsed: number;
38
- remaining: number;
39
- percent: number;
40
- exceeded: boolean;
41
- breakdown: {
42
- subscription: number;
43
- prepaidCredit: number;
44
- tokenGrants: number;
45
- inference: number;
46
- skillRegistrations: number;
47
- skillRegistrationCount: number;
48
- };
49
- }
50
- interface PrepaidCheckoutResult {
51
- url: string;
52
- sessionId?: string;
53
- }
54
- interface PrepaidVerifyResult {
55
- alreadyCredited: boolean;
56
- tokensCredited?: number;
57
- amountCents?: number;
58
- paymentRef?: string;
59
- }
60
- interface BillingRecord {
61
- id: string;
62
- recorded_at?: number;
63
- date?: string;
64
- reason?: string;
65
- description?: string;
66
- amount: number;
67
- direction?: string;
68
- source?: string;
69
- status?: string;
70
- payment_ref?: string;
71
- }
72
-
73
- /** Client-side configuration for UserUtilsProvider */
74
- interface UserUtilsConfig {
75
- /** Base URL for API calls (empty string = same origin) */
76
- apiBaseUrl: string;
77
- /** StackNet stack identifier */
78
- stackId?: string;
79
- /** Direct StackNet URL for client-side API calls (e.g. challenges) */
80
- stacknetUrl?: string;
81
- /** Theme preference */
82
- theme?: 'light' | 'dark' | 'system';
83
- }
84
- /** Callbacks for auth and billing events */
85
- interface UserUtilsCallbacks {
86
- onAuthSuccess?: (session: PublicSession) => void;
87
- onAuthError?: (error: Error) => void;
88
- onLogout?: () => void;
89
- onSubscriptionChange?: (subscription: Subscription) => void;
90
- }
91
- /** Server-side configuration for handler factories */
92
- interface ServerConfig {
93
- /** HMAC-SHA256 secret for signing JWTs */
94
- authSecret: string;
95
- /** StackNet backend URL (always https://stacknet.magma-rpc.com) */
96
- stacknetUrl: string;
97
- /** StackNet stack identifier */
98
- stackId: string;
99
- /** JWT secret for re-signing to StackNet (defaults to authSecret) */
100
- stacknetJwtSecret?: string;
101
- /** Cookie domain for subdomain sharing (e.g. '.geoff.ai') */
102
- cookieDomain?: string;
103
- /** Use Secure flag on cookies (default: true) */
104
- secureCookies?: boolean;
105
- /** Session max age in seconds (default: 604800 = 7 days) */
106
- sessionMaxAge?: number;
107
- /** JWT expiry in seconds (default: 900 = 15 minutes) */
108
- jwtExpiry?: number;
109
- /**
110
- * Google OAuth client ID (single). Used by createGoogleOneTapHandler to
111
- * validate the `aud` claim on incoming Google ID tokens. Required for
112
- * Google One Tap — without it, an ID token issued to any other Google
113
- * application could be replayed against this endpoint.
114
- */
115
- googleClientId?: string;
116
- /**
117
- * Google OAuth client IDs (multiple). Use when the stack accepts tokens
118
- * from more than one Google client (e.g. web + native).
119
- */
120
- googleClientIds?: string[];
121
- }
122
-
123
- export type { BillingPlan as B, PrepaidCheckoutResult as P, Subscription as S, UsageSummary as U, BillingRecord as a, PrepaidVerifyResult as b, UserUtilsCallbacks as c, UserUtilsConfig as d, ServerConfig as e };
@@ -1,123 +0,0 @@
1
- import { P as PublicSession } from './auth-DR2aYcor.js';
2
-
3
- interface BillingPlan {
4
- id: string;
5
- name: string;
6
- price_cents: number;
7
- token_allocation: number;
8
- features: string;
9
- sort_order?: number;
10
- is_active?: boolean;
11
- }
12
- interface Subscription {
13
- plan?: {
14
- id: string;
15
- name: string;
16
- priceCents: number;
17
- tokenAllocation: number;
18
- };
19
- planId?: string;
20
- planName?: string;
21
- status: string;
22
- tokensUsed?: number;
23
- tokensRemaining?: number;
24
- usagePercent?: number;
25
- periodStart?: number;
26
- periodEnd?: number;
27
- cancelAtPeriodEnd?: boolean;
28
- stripeCustomerId?: string;
29
- }
30
- interface UsageSummary {
31
- ownerId: string;
32
- planAllocation: number;
33
- planId: string | null;
34
- planName: string;
35
- inferenceUsed: number;
36
- ledgerSpent: number;
37
- totalUsed: number;
38
- remaining: number;
39
- percent: number;
40
- exceeded: boolean;
41
- breakdown: {
42
- subscription: number;
43
- prepaidCredit: number;
44
- tokenGrants: number;
45
- inference: number;
46
- skillRegistrations: number;
47
- skillRegistrationCount: number;
48
- };
49
- }
50
- interface PrepaidCheckoutResult {
51
- url: string;
52
- sessionId?: string;
53
- }
54
- interface PrepaidVerifyResult {
55
- alreadyCredited: boolean;
56
- tokensCredited?: number;
57
- amountCents?: number;
58
- paymentRef?: string;
59
- }
60
- interface BillingRecord {
61
- id: string;
62
- recorded_at?: number;
63
- date?: string;
64
- reason?: string;
65
- description?: string;
66
- amount: number;
67
- direction?: string;
68
- source?: string;
69
- status?: string;
70
- payment_ref?: string;
71
- }
72
-
73
- /** Client-side configuration for UserUtilsProvider */
74
- interface UserUtilsConfig {
75
- /** Base URL for API calls (empty string = same origin) */
76
- apiBaseUrl: string;
77
- /** StackNet stack identifier */
78
- stackId?: string;
79
- /** Direct StackNet URL for client-side API calls (e.g. challenges) */
80
- stacknetUrl?: string;
81
- /** Theme preference */
82
- theme?: 'light' | 'dark' | 'system';
83
- }
84
- /** Callbacks for auth and billing events */
85
- interface UserUtilsCallbacks {
86
- onAuthSuccess?: (session: PublicSession) => void;
87
- onAuthError?: (error: Error) => void;
88
- onLogout?: () => void;
89
- onSubscriptionChange?: (subscription: Subscription) => void;
90
- }
91
- /** Server-side configuration for handler factories */
92
- interface ServerConfig {
93
- /** HMAC-SHA256 secret for signing JWTs */
94
- authSecret: string;
95
- /** StackNet backend URL (always https://stacknet.magma-rpc.com) */
96
- stacknetUrl: string;
97
- /** StackNet stack identifier */
98
- stackId: string;
99
- /** JWT secret for re-signing to StackNet (defaults to authSecret) */
100
- stacknetJwtSecret?: string;
101
- /** Cookie domain for subdomain sharing (e.g. '.geoff.ai') */
102
- cookieDomain?: string;
103
- /** Use Secure flag on cookies (default: true) */
104
- secureCookies?: boolean;
105
- /** Session max age in seconds (default: 604800 = 7 days) */
106
- sessionMaxAge?: number;
107
- /** JWT expiry in seconds (default: 900 = 15 minutes) */
108
- jwtExpiry?: number;
109
- /**
110
- * Google OAuth client ID (single). Used by createGoogleOneTapHandler to
111
- * validate the `aud` claim on incoming Google ID tokens. Required for
112
- * Google One Tap — without it, an ID token issued to any other Google
113
- * application could be replayed against this endpoint.
114
- */
115
- googleClientId?: string;
116
- /**
117
- * Google OAuth client IDs (multiple). Use when the stack accepts tokens
118
- * from more than one Google client (e.g. web + native).
119
- */
120
- googleClientIds?: string[];
121
- }
122
-
123
- export type { BillingPlan as B, PrepaidCheckoutResult as P, Subscription as S, UsageSummary as U, BillingRecord as a, PrepaidVerifyResult as b, UserUtilsCallbacks as c, UserUtilsConfig as d, ServerConfig as e };