@stacknet/userutils 0.6.3 → 0.6.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.
@@ -0,0 +1,499 @@
1
+ type Web3Chain = 'ethereum' | 'solana' | 'polygon' | 'arbitrum' | 'base';
2
+ /**
3
+ * Client-safe session metadata. Does NOT contain the JWT.
4
+ * Use ServerSession (from @stacknet/userutils/server) for server-side code that needs the JWT.
5
+ */
6
+ interface Session {
7
+ id: string;
8
+ app_identity_id: string;
9
+ global_identity_commitment: string;
10
+ expires_at: number;
11
+ created_at: number;
12
+ stack_id?: string;
13
+ signed_by: string[];
14
+ user_id?: string;
15
+ address?: string;
16
+ chain?: Web3Chain;
17
+ auth_method?: string;
18
+ }
19
+
20
+ /**
21
+ * Server-only session type that includes the JWT.
22
+ * NEVER export this from the client bundle.
23
+ */
24
+ interface ServerSession extends Session {
25
+ jwt: string;
26
+ }
27
+
28
+ /** Decode JWT payload without verification (server-side helper) */
29
+ declare function decodeJWTPayload(jwt: string): Record<string, any> | null;
30
+ /** Sign a JWT with HMAC-SHA256 */
31
+ declare function signJWT(payload: Record<string, any>, secret: string): string;
32
+ /** Verify a JWT signature with HMAC-SHA256 (constant-time comparison) */
33
+ declare function verifyJWTSignature(jwt: string, secret: string): boolean;
34
+ /** Verify JWT and return payload if valid (checks signature + expiry) */
35
+ declare function verifyJWT(jwt: string, secret: string): Record<string, any> | null;
36
+ /**
37
+ * Check if JWT needs refresh and return a new one if so.
38
+ * Returns null if no refresh needed or JWT is invalid.
39
+ */
40
+ declare function maybeRefreshJWT(jwt: string, secret: string, expirySeconds?: number, refreshWindowSeconds?: number): string | null;
41
+ /** Generate a cryptographically secure random token */
42
+ declare function generateToken(bytes?: number): string;
43
+ /** Options controlling how the real client IP is extracted from a Request.
44
+ *
45
+ * The previous implementation trusted `X-Forwarded-For[0]` unconditionally,
46
+ * which lets any direct caller pin arbitrary values and bypass per-IP rate
47
+ * limits. The correct read depends on how many reverse proxies sit between
48
+ * the app and the real client: with N trusted hops, the real client IP is
49
+ * at position `(length - N)` in the XFF list, because each trusted hop
50
+ * appends its peer IP and the attacker-controlled prefix is pushed left. */
51
+ interface IPExtractorConfig {
52
+ /** Number of trusted reverse-proxy hops between this app and the real client.
53
+ * - `0`: do NOT trust X-Forwarded-For (app is directly internet-exposed).
54
+ * - `1` (default): one trusted proxy (e.g. single LB, Vercel edge, Nginx).
55
+ * - `N`: N trusted hops; the real client IP is `XFF[length - N]`.
56
+ * If XFF has fewer entries than expected, the chain is misconfigured or
57
+ * spoofed and the extractor falls through to the next source. */
58
+ trustedProxyCount?: number;
59
+ /** Trust the `X-Real-IP` header. Only enable if your proxy sets it AND
60
+ * strips any inbound `X-Real-IP` from clients. Default: `false`. */
61
+ trustRealIpHeader?: boolean;
62
+ /** Override entirely — return the real client IP from a request however
63
+ * you know best (e.g. a platform-specific header like `cf-connecting-ip`
64
+ * or `x-vercel-forwarded-for`). If this returns a non-empty string it
65
+ * wins; if it returns null/empty the other strategies run. */
66
+ customExtractor?: (request: Request) => string | null | undefined;
67
+ }
68
+ /** Extract the real client IP address from a request.
69
+ *
70
+ * Default behavior (`trustedProxyCount: 1`) is safe for the common case of
71
+ * one trusted reverse proxy. Consumers with no proxy must pass `0`, and
72
+ * consumers behind multiple proxies must pass the exact hop count. */
73
+ declare function extractIP(request: Request, config?: IPExtractorConfig): string;
74
+
75
+ /** Server-side configuration for handler factories */
76
+ interface ServerConfig {
77
+ /** HMAC-SHA256 secret for signing JWTs */
78
+ authSecret: string;
79
+ /** StackNet backend URL (always https://stacknet.magma-rpc.com) */
80
+ stacknetUrl: string;
81
+ /** StackNet stack identifier */
82
+ stackId: string;
83
+ /** JWT secret for re-signing to StackNet (defaults to authSecret) */
84
+ stacknetJwtSecret?: string;
85
+ /** Cookie domain for subdomain sharing (e.g. '.geoff.ai') */
86
+ cookieDomain?: string;
87
+ /** Use Secure flag on cookies (default: true) */
88
+ secureCookies?: boolean;
89
+ /** Session max age in seconds (default: 604800 = 7 days) */
90
+ sessionMaxAge?: number;
91
+ /** JWT expiry in seconds (default: 900 = 15 minutes) */
92
+ jwtExpiry?: number;
93
+ /**
94
+ * Google OAuth client ID (single). Used by createGoogleOneTapHandler to
95
+ * validate the `aud` claim on incoming Google ID tokens. Required for
96
+ * Google One Tap — without it, an ID token issued to any other Google
97
+ * application could be replayed against this endpoint.
98
+ */
99
+ googleClientId?: string;
100
+ /**
101
+ * Google OAuth client IDs (multiple). Use when the stack accepts tokens
102
+ * from more than one Google client (e.g. web + native).
103
+ */
104
+ googleClientIds?: string[];
105
+ /**
106
+ * How the handlers should derive the real client IP for rate-limit keys.
107
+ * Defaults to `{ trustedProxyCount: 1 }`. Set `trustedProxyCount: 0` if
108
+ * the app is exposed directly (no proxy) — otherwise any caller can pin
109
+ * their X-Forwarded-For and bypass rate limiting.
110
+ */
111
+ ipConfig?: IPExtractorConfig;
112
+ }
113
+
114
+ /** Rate limiter interface — implement with Redis, Upstash, or use the in-memory default */
115
+ interface RateLimiter {
116
+ check(key: string): Promise<{
117
+ allowed: boolean;
118
+ remaining: number;
119
+ retryAfter?: number;
120
+ }>;
121
+ }
122
+ /** Replay store interface — for preventing JWT re-sign replay attacks */
123
+ interface ReplayStore {
124
+ has(key: string): Promise<boolean>;
125
+ set(key: string, ttlSeconds: number): Promise<void>;
126
+ }
127
+ /**
128
+ * In-memory rate limiter with sliding window.
129
+ *
130
+ * Safe default for single-process deployments. For multi-process or
131
+ * distributed environments, provide a Redis-backed RateLimiter instead.
132
+ */
133
+ declare function createInMemoryRateLimiter(opts: {
134
+ maxRequests: number;
135
+ windowMs: number;
136
+ }): RateLimiter;
137
+ /** In-memory replay store (single-process, entries auto-expire) */
138
+ declare function createInMemoryReplayStore(): ReplayStore;
139
+
140
+ interface AuthCallbackOptions {
141
+ rateLimiter?: RateLimiter;
142
+ }
143
+ /**
144
+ * Factory: POST handler for auth callback (login completion).
145
+ *
146
+ * Accepts wallet signature verification or OTP results from client,
147
+ * validates with StackNet, sets HttpOnly JWT cookie + public session cookie + CSRF cookie.
148
+ */
149
+ declare function createAuthCallback(config: ServerConfig, opts?: AuthCallbackOptions): (request: Request) => Promise<Response>;
150
+
151
+ /**
152
+ * Factory: POST handler for logout.
153
+ * Revokes session with StackNet and clears all auth cookies.
154
+ */
155
+ declare function createLogoutHandler(config: Pick<ServerConfig, 'stacknetUrl' | 'secureCookies' | 'cookieDomain'> & {
156
+ /**
157
+ * HMAC secret used to verify the JWT signature before extracting the
158
+ * sessionId for upstream revocation. STRONGLY RECOMMENDED.
159
+ *
160
+ * Without this, the handler skips upstream revocation entirely and only
161
+ * clears cookies — because trusting an unverified JWT for the sessionId
162
+ * would let an attacker plant a forged cookie and trigger DELETE on
163
+ * another user's session.
164
+ */
165
+ authSecret?: string;
166
+ }): (request: Request) => Promise<Response>;
167
+
168
+ /**
169
+ * Factory: GET handler for session validation.
170
+ * Reads HttpOnly JWT cookie, validates, returns public session info.
171
+ * Transparently refreshes JWT if close to expiry.
172
+ */
173
+ declare function createSessionHandler(config: Pick<ServerConfig, 'authSecret' | 'jwtExpiry' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge'>): (request: Request) => Promise<Response>;
174
+
175
+ interface OTPHandlerConfig extends Pick<ServerConfig, 'authSecret' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge' | 'jwtExpiry'> {
176
+ /** The OTP secret to validate against */
177
+ otpSecret: string;
178
+ /** Rate limiter (default: 5 attempts per 5 min per IP) */
179
+ rateLimiter?: RateLimiter;
180
+ /** How to extract the real client IP for rate-limit keys. Defaults to
181
+ * `{ trustedProxyCount: 1 }`. Set `trustedProxyCount: 0` if the handler
182
+ * is exposed directly (no proxy in front). */
183
+ ipConfig?: IPExtractorConfig;
184
+ }
185
+ /**
186
+ * Factory: POST handler for OTP verification.
187
+ * Validates OTP code, creates session JWT, sets HttpOnly cookie.
188
+ */
189
+ declare function createOTPHandler(config: OTPHandlerConfig): (request: Request) => Promise<Response>;
190
+
191
+ interface OAuthHandlerConfig {
192
+ rateLimiter?: RateLimiter;
193
+ }
194
+ /**
195
+ * Factory: OAuth flow handlers.
196
+ *
197
+ * Returns two handlers:
198
+ * - GET /api/auth/oauth/[provider] — Starts OAuth flow, returns redirect URL
199
+ * - POST /api/auth/oauth/[provider]/callback — Handles OAuth callback, sets cookies
200
+ */
201
+ declare function createOAuthHandlers(config: ServerConfig, opts?: OAuthHandlerConfig): {
202
+ startFlow: (request: Request) => Promise<Response>;
203
+ handleCallback: (request: Request) => Promise<Response>;
204
+ };
205
+
206
+ interface GoogleOneTapHandlerConfig {
207
+ rateLimiter?: RateLimiter;
208
+ }
209
+ /**
210
+ * Factory: POST handler for Google One Tap credential verification.
211
+ *
212
+ * Receives the Google JWT credential from the client, verifies it with
213
+ * Google's tokeninfo endpoint, then creates a StackNet session.
214
+ */
215
+ declare function createGoogleOneTapHandler(config: ServerConfig, opts?: GoogleOneTapHandlerConfig): (request: Request) => Promise<Response>;
216
+
217
+ interface PreviewCodeHandlerOptions {
218
+ rateLimiter?: RateLimiter;
219
+ /** Maximum session length in seconds (default 3600 = 1h). Preview
220
+ * codes are meant for short-lived demo access — keep this
221
+ * meaningfully shorter than a normal wallet/OAuth session. */
222
+ sessionMaxAge?: number;
223
+ }
224
+ /**
225
+ * Factory: POST handler for preview-code login.
226
+ *
227
+ * Flow:
228
+ * 1. Client POSTs `{code: "692232"}` to this route.
229
+ * 2. Handler validates the code against
230
+ * `${config.stacknetUrl}/v1/preview-codes/:code` (public read —
231
+ * the 6-digit code is itself the bearer credential).
232
+ * 3. Rejects on not-found / revoked / expired / exhausted.
233
+ * 4. Mints a local JWT (signed with `config.authSecret`) with
234
+ * `sub = global_id = "preview_<code>"` and `auth_method =
235
+ * "preview_code"`. Sets it as the `stackauth_jwt` HttpOnly
236
+ * cookie so `useSession` recognizes the user as authenticated.
237
+ * 5. Also sets the public `stackauth_session` cookie + CSRF cookie
238
+ * to match the normal wallet / OAuth callback shape.
239
+ *
240
+ * The preview-code bearer (`pc_<code>`) is expected to already be in
241
+ * `localStorage['stacknet-api-key']` — the widget sets that
242
+ * client-side after a successful POST here. Together, the cookie
243
+ * drives same-origin `useSession` / server-rendered pages, and the
244
+ * bearer drives direct stacknet API calls from the browser.
245
+ *
246
+ * Security:
247
+ * - Rate-limited per IP (10 attempts / minute by default).
248
+ * - Format guard: only 6-digit numeric codes reach the upstream
249
+ * validator so nothing weird hits stacknet.
250
+ * - No upstream JWT is trusted — the stacknet response is a
251
+ * read-only row and the session JWT is freshly signed locally
252
+ * with the app's own `authSecret`.
253
+ */
254
+ declare function createPreviewCodeHandler(config: ServerConfig, opts?: PreviewCodeHandlerOptions): (request: Request) => Promise<Response>;
255
+
256
+ interface BillingProxyConfig extends Pick<ServerConfig, 'authSecret' | 'stacknetUrl' | 'stackId' | 'stacknetJwtSecret' | 'secureCookies' | 'cookieDomain' | 'sessionMaxAge' | 'jwtExpiry'> {
257
+ /** Rate limiter for mutations (default: 20/min per user) */
258
+ rateLimiter?: RateLimiter;
259
+ /**
260
+ * Canonical absolute origin (e.g. "https://app.example.com") used when
261
+ * constructing Stripe success/cancel URLs. STRONGLY RECOMMENDED.
262
+ *
263
+ * Without this, the origin is derived from the request URL — which is
264
+ * populated from the `Host` header. If the app is deployed behind a
265
+ * proxy that does not validate / rewrite `Host`, an attacker can send
266
+ * `Host: evil.example` and the post-checkout redirect will point there.
267
+ * Stripe's dashboard allowlist catches most cases, but we should not
268
+ * depend on that alone.
269
+ *
270
+ * Must be a full http(s) origin with no path. Factory throws on
271
+ * malformed input so misconfigurations surface at boot, not at runtime.
272
+ */
273
+ canonicalOrigin?: string;
274
+ }
275
+ type Handler = (request: Request) => Promise<Response>;
276
+ /**
277
+ * Factory: creates all billing route handlers that proxy to StackNet.
278
+ *
279
+ * All POST handlers validate CSRF tokens. All handlers validate the JWT cookie
280
+ * and transparently refresh if close to expiry.
281
+ */
282
+ declare function createBillingProxy(config: BillingProxyConfig): {
283
+ plans: {
284
+ GET: Handler;
285
+ };
286
+ subscription: {
287
+ GET: Handler;
288
+ };
289
+ subscribe: {
290
+ POST: Handler;
291
+ };
292
+ cancel: {
293
+ POST: Handler;
294
+ };
295
+ usage: {
296
+ GET: Handler;
297
+ };
298
+ history: {
299
+ GET: Handler;
300
+ };
301
+ prepaid: {
302
+ POST: Handler;
303
+ };
304
+ verifyPrepaid: {
305
+ POST: Handler;
306
+ };
307
+ verifySession: {
308
+ POST: Handler;
309
+ };
310
+ subscribeSol: {
311
+ POST: Handler;
312
+ };
313
+ prepaidSol: {
314
+ POST: Handler;
315
+ };
316
+ topup: {
317
+ POST: Handler;
318
+ };
319
+ };
320
+
321
+ /**
322
+ * Factory: POST handler for Stripe webhooks.
323
+ * Forwards raw body + stripe-signature to StackNet for verification and processing.
324
+ */
325
+ declare function createWebhookHandler(config: Pick<ServerConfig, 'stacknetUrl' | 'stackId'>): (request: Request) => Promise<Response>;
326
+
327
+ interface CSRFConfig {
328
+ /** Cookie name (default: '__csrf') */
329
+ cookieName?: string;
330
+ /** Header name (default: 'x-csrf-token') */
331
+ headerName?: string;
332
+ /** Token length in bytes (default: 32) */
333
+ tokenLength?: number;
334
+ /** Use Secure flag on cookie (default: true) */
335
+ secure?: boolean;
336
+ }
337
+ /**
338
+ * Create CSRF protection using the double-submit cookie pattern.
339
+ *
340
+ * 1. Server sets a non-HttpOnly cookie with a random token
341
+ * 2. Client reads the cookie and sends the token in a header on mutations
342
+ * 3. Server validates cookie === header (attacker can't read cookie cross-origin)
343
+ */
344
+ declare function createCSRFProtection(config?: CSRFConfig): {
345
+ /**
346
+ * Generate a CSRF token and add Set-Cookie header to a response.
347
+ * Call this on auth callback (login) to establish the CSRF cookie.
348
+ */
349
+ generateToken(headers: Headers): string;
350
+ /**
351
+ * Validate a request's CSRF token (cookie vs header).
352
+ * Returns true if valid, false if not.
353
+ */
354
+ validateRequest(request: Request): {
355
+ valid: boolean;
356
+ error?: string;
357
+ };
358
+ /** Cookie name for client-side reading */
359
+ cookieName: string;
360
+ /** Header name for client-side sending */
361
+ headerName: string;
362
+ };
363
+
364
+ /** Standard security response headers */
365
+ declare function securityHeaders(): Record<string, string>;
366
+ /**
367
+ * Wrap a request handler to add security headers to the response.
368
+ */
369
+ declare function withSecurityHeaders(handler: (request: Request) => Promise<Response> | Response): (request: Request) => Promise<Response>;
370
+ /**
371
+ * Generate security headers config for Next.js next.config.ts.
372
+ *
373
+ * Usage in next.config.ts:
374
+ * ```ts
375
+ * import { nextSecurityHeaders } from '@stacknet/userutils/server';
376
+ * export default { headers: () => [{ source: '/(.*)', headers: nextSecurityHeaders() }] };
377
+ * ```
378
+ */
379
+ declare function nextSecurityHeaders(): Array<{
380
+ key: string;
381
+ value: string;
382
+ }>;
383
+
384
+ /**
385
+ * Re-sign a JWT using StackNet's HMAC-SHA256 scheme.
386
+ *
387
+ * Geoff apps sign JWTs with AUTH_SECRET. StackNet validates with JWT_SECRET.
388
+ * This function re-signs using the StackNet secret so the backend resolves
389
+ * the correct per-user identity.
390
+ */
391
+ declare function resignForStackNet(jwt: string, stacknetJwtSecret: string): string | null;
392
+ /**
393
+ * Build headers for proxying a user-scoped request to StackNet.
394
+ * Re-signs the JWT so StackNet recognises the user's identity.
395
+ *
396
+ * Refuses to interpolate any value that doesn't match the compact JWT
397
+ * format (header.body.signature, base64url segments only) to prevent
398
+ * header injection.
399
+ */
400
+ declare function buildStackNetHeaders(jwt: string, stacknetJwtSecret: string): Record<string, string>;
401
+ /**
402
+ * Extract JWT from a request's cookies or Authorization header.
403
+ */
404
+ declare function extractJwt(request: Request): string | null;
405
+
406
+ /**
407
+ * Server-side proxy helpers for StackNet preview codes.
408
+ *
409
+ * Preview codes are admin-minted 6-digit access credentials with a
410
+ * per-code token budget. Only the pinned admin global id (enforced
411
+ * by StackNet's state machine) can mint / list / revoke codes.
412
+ *
413
+ * These helpers wrap `fetch` against the StackNet HTTP layer with
414
+ * re-signed JWT cookies (same pattern as `buildStackNetHeaders`).
415
+ * Admin-console API routes are expected to call them; the raw
416
+ * endpoints are NOT exposed in the client bundle.
417
+ */
418
+ interface PreviewCode {
419
+ code: string;
420
+ createdBy: string;
421
+ tokenBudget: number;
422
+ tokensUsed: number;
423
+ tokensRemaining: number;
424
+ createdAt: number;
425
+ expiresAt: number | null;
426
+ revoked: boolean;
427
+ /** Optional human-readable label (e.g. "Tester: Alice"). Absent on
428
+ * codes minted before the name field shipped. */
429
+ name?: string | null;
430
+ }
431
+ interface MintPreviewCodeOptions {
432
+ /** Token budget for the new code. Must be > 0. */
433
+ tokenBudget: number;
434
+ /** Optional explicit 6-digit code string. Server generates one
435
+ * if omitted. */
436
+ code?: string;
437
+ /** Optional Unix-ms expiry. */
438
+ expiresAt?: number;
439
+ /** Optional human-readable label for the code. Shown in the admin
440
+ * list so the operator can tell codes apart. */
441
+ name?: string;
442
+ }
443
+ interface PreviewCodesProxyConfig {
444
+ /** StackNet base URL (no trailing slash). */
445
+ stacknetBaseUrl: string;
446
+ /** Shared HMAC secret for re-signing the caller's JWT. */
447
+ stacknetJwtSecret: string;
448
+ /** Caller's StackAuth JWT (user identity). */
449
+ jwt: string;
450
+ }
451
+ /** Admin-only: mint a new preview code. Returns the new code row. */
452
+ declare function mintPreviewCode(cfg: PreviewCodesProxyConfig, options: MintPreviewCodeOptions): Promise<{
453
+ minted: boolean;
454
+ code: PreviewCode;
455
+ } | {
456
+ error: string;
457
+ status: number;
458
+ }>;
459
+ /** Admin-only: list every preview code in the system. */
460
+ declare function listPreviewCodes(cfg: PreviewCodesProxyConfig): Promise<PreviewCode[] | {
461
+ error: string;
462
+ status: number;
463
+ }>;
464
+ /** Public: read a code's balance + status. Used by auth middleware. */
465
+ declare function getPreviewCode(stacknetBaseUrl: string, code: string): Promise<PreviewCode | null>;
466
+ /** Admin-only: revoke a preview code. */
467
+ declare function revokePreviewCode(cfg: PreviewCodesProxyConfig, code: string): Promise<{
468
+ revoked: boolean;
469
+ code: PreviewCode;
470
+ } | {
471
+ error: string;
472
+ status: number;
473
+ }>;
474
+ /** Internal: debit tokens from a preview code. Called by the metering
475
+ * layer after inference completes. */
476
+ declare function redeemPreviewCode(stacknetBaseUrl: string, code: string, tokens: number): Promise<{
477
+ redeemed: boolean;
478
+ code: string;
479
+ tokensUsed: number;
480
+ tokensRemaining: number;
481
+ } | {
482
+ error: string;
483
+ status: number;
484
+ }>;
485
+ /** Allowlist of admin global ids that can mint / revoke preview
486
+ * codes. Mirrors PREVIEW_CODE_ADMIN_GLOBAL_IDS in the Rust state
487
+ * machine — must stay in sync. Admin-console UI and API route guards
488
+ * should call `isPreviewCodeAdmin(currentUser.userId)` instead of
489
+ * comparing against a single constant so every entry is accepted. */
490
+ declare const PREVIEW_CODE_ADMIN_GLOBAL_IDS: readonly string[];
491
+ /** Returns true if the given global id is in the preview-code admin
492
+ * allowlist. */
493
+ declare function isPreviewCodeAdmin(globalId: string | null | undefined): boolean;
494
+ /** Back-compat alias for callers that only need a single canonical
495
+ * admin id for display/logging. Don't use for gating — use
496
+ * `isPreviewCodeAdmin()` to accept every entry in the allowlist. */
497
+ declare const PREVIEW_CODE_ADMIN_GLOBAL_ID: string;
498
+
499
+ export { type AuthCallbackOptions, type BillingProxyConfig, type CSRFConfig, type GoogleOneTapHandlerConfig, type IPExtractorConfig, type MintPreviewCodeOptions, type OAuthHandlerConfig, type OTPHandlerConfig, PREVIEW_CODE_ADMIN_GLOBAL_ID, PREVIEW_CODE_ADMIN_GLOBAL_IDS, type PreviewCode, type PreviewCodeHandlerOptions, type PreviewCodesProxyConfig, type RateLimiter, type ReplayStore, type ServerConfig, type ServerSession, buildStackNetHeaders, createAuthCallback, createBillingProxy, createCSRFProtection, createGoogleOneTapHandler, createInMemoryRateLimiter, createInMemoryReplayStore, createLogoutHandler, createOAuthHandlers, createOTPHandler, createPreviewCodeHandler, createSessionHandler, createWebhookHandler, decodeJWTPayload, extractIP, extractJwt, generateToken, getPreviewCode, isPreviewCodeAdmin, listPreviewCodes, maybeRefreshJWT, mintPreviewCode, nextSecurityHeaders, redeemPreviewCode, resignForStackNet, revokePreviewCode, securityHeaders, signJWT, verifyJWT, verifyJWTSignature, withSecurityHeaders };
@@ -1,2 +1,2 @@
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
+ 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"})),n=Z(JSON.stringify(e)),s=createHmac("sha256",r).update(`${t}.${n}`).digest("base64url");return `${t}.${n}.${s}`}function X(e,r){try{let t=e.split(".");if(t.length!==3)return !1;let[n,s,i]=t,c=createHmac("sha256",r).update(`${n}.${s}`).digest("base64url"),w=Buffer.from(i),m=Buffer.from(c);return w.length!==m.length?!1:timingSafeEqual(w,m)}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 E(e,r,t=900,n=300){let s=C(e,r);return !s?.exp||s.exp*1e3-Date.now()>n*1e3?null:j({...s,exp:Math.floor(Date.now()/1e3)+t},r)}function F(e=32){return randomBytes(e).toString("hex")}function T(e,r={}){let t=r.trustedProxyCount??1,n=r.trustRealIpHeader===true;if(r.customExtractor){let s=r.customExtractor(e);if(s)return s}if(t>0){let s=e.headers.get("x-forwarded-for");if(s){let i=s.split(",").map(w=>w.trim()).filter(Boolean),c=i.length-t;if(c>=0&&c<i.length)return i[c]}}if(n){let s=e.headers.get("x-real-ip");if(s)return s.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 b(e={}){let r=e.cookieName||ne,t=e.headerName||ae,n=e.tokenLength||32,s=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(n<16||n>128)throw new Error("createCSRFProtection: tokenLength must be between 16 and 128 bytes");return {generateToken(i){let c=F(n),w=[`${r}=${c}`,"Path=/","SameSite=Lax"];return s&&w.push("Secure"),i.append("Set-Cookie",w.join("; ")),c},validateRequest(i){let c=i.headers.get("cookie");if(!c)return {valid:false,error:"No cookies present"};let w=c.split(";").map(g=>g.trim()).find(g=>g.startsWith(`${r}=`))?.slice(r.length+1);if(!w)return {valid:false,error:"CSRF cookie missing"};let m=i.headers.get(t);if(!m)return {valid:false,error:"CSRF header missing"};try{let g=Buffer.from(w),u=Buffer.from(m);return g.length!==u.length?{valid:!1,error:"CSRF token mismatch"}:timingSafeEqual(g,u)?{valid:!0}:{valid:!1,error:"CSRF token mismatch"}}catch{return {valid:false,error:"CSRF validation failed"}}},cookieName:r,headerName:t}}function v(e){let r=new Map,t=setInterval(()=>{let n=Date.now();for(let[s,i]of r)n>=i.resetAt&&r.delete(s);},6e4);return typeof t=="object"&&"unref"in t&&t.unref(),{async check(n){let s=Date.now(),i=r.get(n);return (!i||s>=i.resetAt)&&(i={count:0,resetAt:s+e.windowMs},r.set(n,i)),i.count++,i.count>e.maxRequests?{allowed:false,remaining:0,retryAfter:Math.ceil((i.resetAt-s)/1e3)}:{allowed:true,remaining:e.maxRequests-i.count}}}}function ue(){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 pe(e,r){let t=r?.rateLimiter||v({maxRequests:10,windowMs:6e4}),n=b({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,i=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;return async function(m){let g=T(m,e.ipConfig),u=await t.check(`auth:${g}`);if(!u.allowed)return Response.json({error:"Too many login attempts. Please wait."},{status:429,headers:{"Retry-After":String(u.retryAfter||60)}});let d;try{d=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{chain:l,message:f,signature:o,publicKey:a,otp:p,code:h,redirectUrl:S,stackId:k}=d,y=k||e.stackId,x;if(l&&f&&o){let ee={"Content-Type":"application/json"},J=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(y)}/auth/web3/verify`,{method:"POST",headers:ee,body:JSON.stringify({chain:l,message:f,signature:o,public_key:a}),signal:AbortSignal.timeout(1e4)});if(!J.ok){let B=await J.json().catch(()=>({})),W=B?.error?.message||B?.message||B?.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 U=await J.json();x=U.data?.session||U.session||U.data||U,console.log(`[auth-callback] Verify OK, sessionData keys: ${Object.keys(x||{}).join(", ")}`);}else return p||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(!x?.jwt)return Response.json({error:"Authentication failed \u2014 no session returned"},{status:401});let R=C(x.jwt,c);if(!R)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let P=Math.floor(Date.now()/1e3),I={...R,exp:P+s,iat:P},A=j(I,e.authSecret),O={userId:R.sub||R.user_id||R.session_id||R.global_id||"",address:x.address||R.address,chain:x.chain||l,expiresAt:Date.now()+i*1e3,authMethod:l?`web3:${l}`:"otp"},$=new Headers({"Content-Type":"application/json"}),H=e.secureCookies!==false?"; Secure":"",_=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";$.append("Set-Cookie",`stackauth_jwt=${A}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${i}${H}${_}`);let q=Buffer.from(JSON.stringify(O)).toString("base64url");return $.append("Set-Cookie",`stackauth_session=${q}; Path=/; SameSite=Lax; Max-Age=${i}${H}${_}`),n.generateToken($),new Response(JSON.stringify({user:O}),{status:200,headers:$})}}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 n=Buffer.from(JSON.stringify({alg:"HS256",typ:"JWT"})).toString("base64url"),s=Buffer.from(JSON.stringify(t)).toString("base64url"),i=createHmac("sha256",r).update(`${n}.${s}`).digest("base64url");return `${n}.${s}.${i}`}catch{return null}}var le=/^[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:le.test(e)?e:null}function D(e,r){let t=Q(e,r),n=t&&Y(t);if(n)return {Cookie:`stackauth_jwt=${n}`};let s=Y(e);return s?{Cookie:`stackauth_jwt=${s}`}:{}}function M(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 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 n=M(t);if(n&&e.authSecret){let w=C(n,e.authSecret),m=w?.session_id||w?.sub;if(m&&typeof m=="string")try{await fetch(`${e.stacknetUrl}/api/v2/sessions/${encodeURIComponent(m)}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});}catch{}}let s=e.secureCookies!==false?"; Secure":"",i=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${s}${i}`),c.append("Set-Cookie",`stackauth_session=; Path=/; SameSite=Lax; Max-Age=0${s}${i}`),c.append("Set-Cookie",`__csrf=; Path=/; SameSite=Lax; Max-Age=0${s}${i}`),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(s){let i=M(s);if(!i)return Response.json({session:null},{status:200});let c=C(i,e.authSecret);if(!c)return Response.json({session:null},{status:200});let m={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},g=new Headers({"Content-Type":"application/json"}),u=E(i,e.authSecret,r,300);if(u){let d=e.secureCookies!==false?"; Secure":"",l=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";g.append("Set-Cookie",`stackauth_jwt=${u}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${t}${d}${l}`);}return new Response(JSON.stringify({session:m}),{status:200,headers:g})}}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||v({maxRequests:5,windowMs:3e5}),t=b({secure:e.secureCookies!==false}),n=e.jwtExpiry||900,s=e.sessionMaxAge||604800;return async function(c){let w=T(c,e.ipConfig),m=await r.check(`otp:${w}`);if(!m.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(m.retryAfter||300)}});let g;try{g=await c.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{code:u}=g;if(!u||typeof u!="string"||u.length!==6)return Response.json({error:"Invalid code format"},{status:400});if(!ye(u,e.otpSecret))return Response.json({error:"Invalid code"},{status:401});let d=Math.floor(Date.now()/1e3),f={sub:`preview:otp:${createHash("sha256").update(`otp:${u}:${Date.now()}`).digest("hex").slice(0,32)}`,scope:"preview",auth_method:"otp",credentials:["otp"],iat:d,exp:d+n},o=j(f,e.authSecret),a={userId:f.sub,expiresAt:Date.now()+s*1e3,authMethod:"otp",scope:"preview"},p=new Headers({"Content-Type":"application/json"}),h=e.secureCookies!==false?"; Secure":"",S=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";p.append("Set-Cookie",`stackauth_jwt=${o}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${h}${S}`);let k=Buffer.from(JSON.stringify(a)).toString("base64url");return p.append("Set-Cookie",`stackauth_session=${k}; Path=/; SameSite=Lax; Max-Age=${s}${h}${S}`),t.generateToken(p),new Response(JSON.stringify({success:true,data:{user:a}}),{status:200,headers:p})}}function Se(e,r){let t=r?.rateLimiter||v({maxRequests:10,windowMs:6e4}),n=b({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,i=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;async function w(g){let u=new URL(g.url),d=u.searchParams.get("provider"),l=u.searchParams.get("redirectUri")||u.searchParams.get("redirect_uri"),f=u.searchParams.get("stackId")||e.stackId;if(!d)return Response.json({error:"Missing provider parameter"},{status:400});if(!l)return Response.json({error:"Missing redirectUri parameter"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(d))return Response.json({error:"Invalid provider name"},{status:400});if(!f||!/^[a-zA-Z0-9_-]{1,64}$/.test(f))return Response.json({error:"Invalid stackId"},{status:400});try{let o=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(f)}/auth/oauth/${encodeURIComponent(d)}/initiate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({redirectUrl:l}),signal:AbortSignal.timeout(1e4)});if(!o.ok){let h=await o.json().catch(()=>({}));return Response.json({error:h.error?.message||`Failed to start OAuth flow: ${o.statusText}`},{status:o.status})}let a=await o.json(),p=a.data||a;return Response.json({redirect_url:p.url,state:p.state})}catch(o){return Response.json({error:o.message||"Failed to start OAuth flow"},{status:500})}}async function m(g){let u=T(g,e.ipConfig),d=await t.check(`oauth:${u}`);if(!d.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(d.retryAfter||60)}});let l;try{l=await g.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{provider:f,code:o,state:a,stackId:p}=l,h=p||e.stackId;if(!f||!o||!a)return Response.json({error:"Missing provider, code, or state"},{status:400});if(!/^[a-z][a-z0-9_-]{0,32}$/.test(f))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 S=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(h)}/auth/oauth/${encodeURIComponent(f)}/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:o,state:a}),signal:AbortSignal.timeout(1e4)});if(!S.ok){let _=await S.json().catch(()=>({}));return Response.json({error:_.error?.message||`OAuth verification failed: ${S.statusText}`},{status:401})}let k=await S.json(),y=k.data?.session||k.session||k.data||k;if(!y?.jwt)return Response.json({error:"OAuth authentication failed \u2014 no session returned"},{status:401});let x=C(y.jwt,c);if(!x)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let R=Math.floor(Date.now()/1e3),P=j({...x,exp:R+s,iat:R},e.authSecret),A={userId:x.sub||x.user_id||x.session_id||x.global_id||"",address:y.address||x.address,chain:void 0,expiresAt:Date.now()+i*1e3,authMethod:`oauth:${f}`},L=new Headers({"Content-Type":"application/json"}),O=e.secureCookies!==!1?"; Secure":"",$=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";L.append("Set-Cookie",`stackauth_jwt=${P}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${i}${O}${$}`);let H=Buffer.from(JSON.stringify(A)).toString("base64url");return L.append("Set-Cookie",`stackauth_session=${H}; Path=/; SameSite=Lax; Max-Age=${i}${O}${$}`),n.generateToken(L),new Response(JSON.stringify({user:A}),{status:200,headers:L})}catch(S){return Response.json({error:S.message||"OAuth callback failed"},{status:500})}}return {startFlow:w,handleCallback:m}}function ke(e,r){let t=r?.rateLimiter||v({maxRequests:10,windowMs:6e4}),n=b({secure:e.secureCookies!==false}),s=e.jwtExpiry||900,i=e.sessionMaxAge||604800,c=e.stacknetJwtSecret||e.authSecret;return async function(m){let g=T(m,e.ipConfig),u=await t.check(`google-onetap:${g}`);if(!u.allowed)return Response.json({error:"Too many attempts. Please wait."},{status:429,headers:{"Retry-After":String(u.retryAfter||60)}});let d;try{d=await m.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let{credential:l,stackId:f}=d,o=f||e.stackId;if(!l)return Response.json({error:"Missing credential"},{status:400});if(l.split(".").length!==3)return Response.json({error:"Invalid credential format"},{status:400});let a;try{let y=await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(l)}`,{signal:AbortSignal.timeout(1e4)});if(!y.ok)return Response.json({error:"Google credential verification failed"},{status:401});a=await y.json();}catch{return Response.json({error:"Failed to verify Google credential"},{status:500})}if(!a.sub||!a.email)return Response.json({error:"Invalid Google token \u2014 missing user info"},{status:401});if(a.iss!=="https://accounts.google.com"&&a.iss!=="accounts.google.com")return Response.json({error:"Invalid Google token issuer"},{status:401});let p=typeof a.exp=="string"?parseInt(a.exp,10):Number(a.exp);if(!Number.isFinite(p)||p<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(!a.aud||!h.includes(a.aud))return Response.json({error:"Invalid Google token audience"},{status:401});if(!(a.email_verified===true||a.email_verified==="true"))return Response.json({error:"Google email is not verified"},{status:401});let k={sub:a.sub,email:a.email,name:a.name,picture:a.picture};try{let y=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(o)}/auth/oauth/google/callback`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({credential:l,google_id:k.sub,email:k.email,name:k.name,picture:k.picture,one_tap:!0}),signal:AbortSignal.timeout(1e4)});if(!y.ok){let P=await y.json().catch(()=>({}));return Response.json({error:P?.error?.message||"Google sign-in failed"},{status:y.status})}let x=await y.json(),R=x.data?.session||x.session||x.data||x;if(R?.jwt){let P=C(R.jwt,c);if(!P)return Response.json({error:"Upstream session JWT failed verification"},{status:502});let I=Math.floor(Date.now()/1e3),A=j({...P,exp:I+s,iat:I},e.authSecret),O={userId:P.sub||P.user_id||k.sub,address:k.email||R.address,chain:void 0,expiresAt:Date.now()+i*1e3,authMethod:"oauth:google"},$=new Headers({"Content-Type":"application/json"}),H=e.secureCookies!==!1?"; Secure":"",_=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";$.append("Set-Cookie",`stackauth_jwt=${A}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${i}${H}${_}`);let q=Buffer.from(JSON.stringify(O)).toString("base64url");return $.append("Set-Cookie",`stackauth_session=${q}; Path=/; SameSite=Lax; Max-Age=${i}${H}${_}`),n.generateToken($),new Response(JSON.stringify({user:O}),{status:200,headers:$})}return Response.json({error:"No session returned"},{status:401})}catch(y){return Response.json({error:y.message||"Google One Tap authentication failed"},{status:500})}}}function xe(e,r){let t=r?.rateLimiter||v({maxRequests:10,windowMs:6e4}),n=b({secure:e.secureCookies!==false}),s=r?.sessionMaxAge??3600;return async function(c){let w=T(c,e.ipConfig),m=await t.check(`preview-code:${w}`);if(!m.allowed)return Response.json({error:"Too many preview-code attempts. Please wait."},{status:429,headers:{"Retry-After":String(m.retryAfter||60)}});let g;try{g=await c.json();}catch{return Response.json({error:"Invalid request body"},{status:400})}let u=typeof g?.code=="string"?g.code.trim():"";if(!/^\d{6}$/.test(u))return Response.json({error:"Code must be 6 digits"},{status:400});let d;try{let y=await fetch(`${e.stacknetUrl.replace(/\/$/,"")}/v1/preview-codes/${encodeURIComponent(u)}`,{method:"GET",headers:{Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(y.status===404)return Response.json({error:"Preview code not found"},{status:404});if(!y.ok)return Response.json({error:`Upstream returned ${y.status}`},{status:502});d=await y.json();}catch{return Response.json({error:"Upstream validation failed"},{status:502})}if(!d||d.code!==u)return Response.json({error:"Preview code not found"},{status:404});if(d.revoked===true)return Response.json({error:"Preview code has been revoked"},{status:401});if(typeof d.expiresAt=="number"&&d.expiresAt<=Date.now())return Response.json({error:"Preview code has expired"},{status:401});if(typeof d.tokensRemaining=="number"&&d.tokensRemaining<=0)return Response.json({error:"Preview code is exhausted"},{status:402});let l=Math.floor(Date.now()/1e3),f=`preview_${u}`,o=j({sub:f,global_id:f,auth_method:"preview_code",preview_code:u,exp:l+s,iat:l},e.authSecret),a={userId:f,expiresAt:Date.now()+s*1e3,authMethod:"preview_code"},p=new Headers({"Content-Type":"application/json"}),h=e.secureCookies!==false?"; Secure":"",S=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";p.append("Set-Cookie",`stackauth_jwt=${o}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${s}${h}${S}`);let k=Buffer.from(JSON.stringify(a)).toString("base64url");return p.append("Set-Cookie",`stackauth_session=${k}; Path=/; SameSite=Lax; Max-Age=${s}${h}${S}`),n.generateToken(p),new Response(JSON.stringify({user:a,name:d.name??null}),{status:200,headers:p})}}function Re(e){let r=b({secure:e.secureCookies!==false}),t=e.rateLimiter||v({maxRequests:20,windowMs:6e4}),n=e.stacknetJwtSecret||e.authSecret,s=e.jwtExpiry||900,i=e.sessionMaxAge||604800,c=null;if(e.canonicalOrigin){let o;try{o=new URL(e.canonicalOrigin);}catch{throw new Error(`createBillingProxy: canonicalOrigin "${e.canonicalOrigin}" is not a valid URL`)}if(o.protocol!=="http:"&&o.protocol!=="https:")throw new Error(`createBillingProxy: canonicalOrigin must be http or https (got "${o.protocol}")`);if(o.pathname!=="/"&&o.pathname!=="")throw new Error(`createBillingProxy: canonicalOrigin must have no path (got "${o.pathname}")`);c=o.origin;}let w=false;function m(o){if(c)return c;w||(w=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 a=new URL(o.url);return a.protocol!=="http:"&&a.protocol!=="https:"?null:a.origin}catch{return null}}function g(o){let a=M(o);if(!a)return null;let p=C(a,e.authSecret);return p?{jwt:a,payload:p}:null}function u(o,a){let p=E(o,e.authSecret,s,300);if(p){let h=e.secureCookies!==false?"; Secure":"",S=e.cookieDomain?`; Domain=${e.cookieDomain}`:"";a.append("Set-Cookie",`stackauth_jwt=${p}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${i}${h}${S}`);}}async function d(o,a){let p=g(o);if(!p)return Response.json({error:"Unauthorized"},{status:401});let h=D(p.jwt,n),S=await fetch(`${e.stacknetUrl}${a}`,{headers:h,signal:AbortSignal.timeout(15e3)}),k=await S.json().catch(()=>({})),y=new Headers({"Content-Type":"application/json"});return u(p.jwt,y),new Response(JSON.stringify(k),{status:S.status,headers:y})}async function l(o,a,p){let h=g(o);if(!h)return Response.json({error:"Unauthorized"},{status:401});let S=r.validateRequest(o);if(!S.valid)return Response.json({error:S.error||"CSRF validation failed"},{status:403});let k=h.payload.sub||h.payload.user_id||"unknown";if(!(await t.check(`billing:${k}`)).allowed)return Response.json({error:"Too many requests"},{status:429});let x=await o.json().catch(()=>({})),R=D(h.jwt,n);R["Content-Type"]="application/json";let P=await fetch(`${e.stacknetUrl}${a}`,{method:"POST",headers:R,body:JSON.stringify({...x,...p}),signal:AbortSignal.timeout(15e3)}),I=await P.json().catch(()=>({})),A=new Headers({"Content-Type":"application/json"});return u(h.jwt,A),new Response(JSON.stringify(I),{status:P.status,headers:A})}let f=`/api/v2/stacks/${encodeURIComponent(e.stackId)}`;return {plans:{GET:async o=>{let a=await fetch(`${e.stacknetUrl}${f}/plans`,{signal:AbortSignal.timeout(1e4)}),p=await a.json().catch(()=>({}));return Response.json(p,{status:a.status})}},subscription:{GET:(o=>d(o,`${f}/subscription`))},subscribe:{POST:(o=>{let a=m(o);return a?l(o,`${f}/subscribe`,{successUrl:`${a}/billing/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${a}/pricing`}):Promise.resolve(Response.json({error:"Invalid request origin for checkout"},{status:400}))})},cancel:{POST:(o=>l(o,`${f}/cancel-subscription`))},usage:{GET:(o=>d(o,"/v1/account/usage"))},history:{GET:(o=>d(o,`${f}/billing`))},prepaid:{POST:(o=>{let a=m(o);return a?l(o,`${f}/prepaid`,{successUrl:`${a}/pricing/prepaid/success?session_id={CHECKOUT_SESSION_ID}`,cancelUrl:`${a}/pricing/prepaid`}):Promise.resolve(Response.json({error:"Invalid request origin for checkout"},{status:400}))})},verifyPrepaid:{POST:(o=>l(o,`${f}/verify-prepaid`))},verifySession:{POST:(o=>l(o,`${f}/verify-session`))},subscribeSol:{POST:(o=>l(o,`${f}/subscribe-sol`))},prepaidSol:{POST:(o=>l(o,`${f}/prepaid-sol`))},topup:{POST:(o=>l(o,"/v1/account/topup"))}}}function Pe(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(),i=await fetch(`${e.stacknetUrl}/api/v2/stacks/${encodeURIComponent(e.stackId)}/webhook/stripe`,{method:"POST",headers:{"Content-Type":"application/json","stripe-signature":n},body:s,signal:AbortSignal.timeout(1e4)}),c=await i.json().catch(()=>({received:!0}));return Response.json(c,{status:i.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 Ce(e){return async r=>{let t=await e(r),n=G(),s=new Headers(t.headers);for(let[i,c]of Object.entries(n))s.set(i,c);return new Response(t.body,{status:t.status,statusText:t.statusText,headers:s})}}function be(){return Object.entries(G()).map(([e,r])=>({key:e,value:r}))}function z(e){return {"Content-Type":"application/json",...D(e.jwt,e.stacknetJwtSecret)}}async function ve(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 $e(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 je(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 Te(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 Ae(e,r,t){let n=await fetch(`${e}/v1/preview-codes/${encodeURIComponent(r)}/redeem`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tokens:t})});return n.ok?await n.json():{error:await n.text().catch(()=>"")||`HTTP ${n.status}`,status:n.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,D as buildStackNetHeaders,pe as createAuthCallback,Re as createBillingProxy,b as createCSRFProtection,ke as createGoogleOneTapHandler,v as createInMemoryRateLimiter,ue as createInMemoryReplayStore,fe as createLogoutHandler,Se as createOAuthHandlers,we as createOTPHandler,xe as createPreviewCodeHandler,me as createSessionHandler,Pe as createWebhookHandler,N as decodeJWTPayload,T as extractIP,M as extractJwt,F as generateToken,je as getPreviewCode,Oe as isPreviewCodeAdmin,$e as listPreviewCodes,E as maybeRefreshJWT,ve as mintPreviewCode,be as nextSecurityHeaders,Ae as redeemPreviewCode,Q as resignForStackNet,Te as revokePreviewCode,G as securityHeaders,j as signJWT,C as verifyJWT,X as verifyJWTSignature,Ce as withSecurityHeaders};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacknet/userutils",
3
- "version": "0.6.3",
3
+ "version": "0.6.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",