whop-kit 0.1.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/index.d.ts +85 -0
  3. package/dist/auth/index.js +3 -0
  4. package/dist/auth/index.js.map +1 -0
  5. package/dist/chunk-BLQ7T7NQ.js +18 -0
  6. package/dist/chunk-BLQ7T7NQ.js.map +1 -0
  7. package/dist/chunk-EYK3S5U2.js +158 -0
  8. package/dist/chunk-EYK3S5U2.js.map +1 -0
  9. package/dist/chunk-GXQLG4HV.js +87 -0
  10. package/dist/chunk-GXQLG4HV.js.map +1 -0
  11. package/dist/chunk-KQQGVBBH.js +86 -0
  12. package/dist/chunk-KQQGVBBH.js.map +1 -0
  13. package/dist/chunk-NS32METI.js +72 -0
  14. package/dist/chunk-NS32METI.js.map +1 -0
  15. package/dist/chunk-PLZUFPEG.js +53 -0
  16. package/dist/chunk-PLZUFPEG.js.map +1 -0
  17. package/dist/chunk-ZQ44KZRC.js +30 -0
  18. package/dist/chunk-ZQ44KZRC.js.map +1 -0
  19. package/dist/config/index.d.ts +42 -0
  20. package/dist/config/index.js +3 -0
  21. package/dist/config/index.js.map +1 -0
  22. package/dist/core/index.d.ts +57 -0
  23. package/dist/core/index.js +3 -0
  24. package/dist/core/index.js.map +1 -0
  25. package/dist/email/index.d.ts +43 -0
  26. package/dist/email/index.js +3 -0
  27. package/dist/email/index.js.map +1 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +9 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/subscriptions/index.d.ts +76 -0
  32. package/dist/subscriptions/index.js +3 -0
  33. package/dist/subscriptions/index.js.map +1 -0
  34. package/dist/utils/index.d.ts +8 -0
  35. package/dist/utils/index.js +3 -0
  36. package/dist/utils/index.js.map +1 -0
  37. package/dist/whop/index.d.ts +102 -0
  38. package/dist/whop/index.js +3 -0
  39. package/dist/whop/index.js.map +1 -0
  40. package/package.json +70 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colin McDermott
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ /** The session payload stored in the JWT and used throughout the app */
2
+ interface Session {
3
+ userId: string;
4
+ whopUserId: string;
5
+ email: string | null;
6
+ name: string | null;
7
+ profileImageUrl: string | null;
8
+ plan: string;
9
+ cancelAtPeriodEnd: boolean;
10
+ isAdmin: boolean;
11
+ }
12
+ /** Options for session token creation */
13
+ interface SessionTokenOptions {
14
+ /** JWT max age in seconds. Defaults to 7 days. */
15
+ maxAge?: number;
16
+ }
17
+ /** Cookie adapter interface — implement per framework */
18
+ interface CookieAdapter {
19
+ get(name: string): string | undefined | Promise<string | undefined>;
20
+ set(name: string, value: string, options: CookieOptions): void | Promise<void>;
21
+ delete(name: string): void | Promise<void>;
22
+ }
23
+ interface CookieOptions {
24
+ httpOnly: boolean;
25
+ secure: boolean;
26
+ sameSite: "lax" | "strict" | "none";
27
+ maxAge: number;
28
+ path: string;
29
+ }
30
+ /**
31
+ * Create a signed JWT session token.
32
+ *
33
+ * @param session - The session payload
34
+ * @param secret - The signing secret (Uint8Array)
35
+ * @param options - Optional token settings
36
+ */
37
+ declare function createSessionToken(session: Session, secret: Uint8Array, options?: SessionTokenOptions): Promise<string>;
38
+ /**
39
+ * Verify and decode a JWT session token.
40
+ *
41
+ * @param token - The JWT string
42
+ * @param secret - The signing secret (Uint8Array)
43
+ * @param validPlanKeys - Array of valid plan keys (to validate the plan field)
44
+ * @param defaultPlan - Fallback plan key if the JWT's plan is invalid
45
+ */
46
+ declare function verifySessionToken(token: string, secret: Uint8Array, validPlanKeys: string[], defaultPlan: string): Promise<Session | null>;
47
+ /**
48
+ * Set the session cookie (and a non-httpOnly login indicator).
49
+ *
50
+ * @param session - The session payload
51
+ * @param secret - JWT signing secret
52
+ * @param cookies - Framework-specific cookie adapter
53
+ * @param isProduction - Whether to set Secure flag
54
+ */
55
+ declare function setSessionCookie(session: Session, secret: Uint8Array, cookies: CookieAdapter, isProduction?: boolean): Promise<void>;
56
+ /**
57
+ * Clear the session cookie.
58
+ *
59
+ * @param cookies - Framework-specific cookie adapter
60
+ * @param isProduction - Whether to set Secure flag
61
+ */
62
+ declare function clearSessionCookie(cookies: CookieAdapter, isProduction?: boolean): Promise<void>;
63
+ /**
64
+ * Read and verify the session from cookies, optionally refreshing the plan
65
+ * from the database.
66
+ *
67
+ * @param cookies - Framework-specific cookie adapter
68
+ * @param secret - JWT signing secret
69
+ * @param validPlanKeys - Array of valid plan keys
70
+ * @param defaultPlan - Fallback plan key
71
+ * @param refreshPlan - Optional async function to fetch fresh plan from DB
72
+ */
73
+ declare function getSessionFromCookie(cookies: CookieAdapter, secret: Uint8Array, validPlanKeys: string[], defaultPlan: string, refreshPlan?: (userId: string) => Promise<{
74
+ plan: string;
75
+ cancelAtPeriodEnd: boolean;
76
+ } | null>): Promise<Session | null>;
77
+ /**
78
+ * Generate a cryptographically secure secret string (hex-encoded).
79
+ * Useful for auto-generating SESSION_SECRET on first run.
80
+ */
81
+ declare function generateSecret(byteLength?: number): string;
82
+ /** Encode a secret string to Uint8Array (for use with jose) */
83
+ declare function encodeSecret(secret: string): Uint8Array;
84
+
85
+ export { type CookieAdapter, type CookieOptions, type Session, type SessionTokenOptions, clearSessionCookie, createSessionToken, encodeSecret, generateSecret, getSessionFromCookie, setSessionCookie, verifySessionToken };
@@ -0,0 +1,3 @@
1
+ export { clearSessionCookie, createSessionToken, encodeSecret, generateSecret, getSessionFromCookie, setSessionCookie, verifySessionToken } from '../chunk-GXQLG4HV.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,18 @@
1
+ // src/utils/index.ts
2
+ function cn(...classes) {
3
+ return classes.filter(Boolean).join(" ");
4
+ }
5
+ function monthlyEquivalent(yearlyTotal) {
6
+ return Math.round(yearlyTotal / 12 * 100) / 100;
7
+ }
8
+ function formatDate(date) {
9
+ return new Intl.DateTimeFormat("en-US", {
10
+ month: "short",
11
+ day: "numeric",
12
+ year: "numeric"
13
+ }).format(new Date(date));
14
+ }
15
+
16
+ export { cn, formatDate, monthlyEquivalent };
17
+ //# sourceMappingURL=chunk-BLQ7T7NQ.js.map
18
+ //# sourceMappingURL=chunk-BLQ7T7NQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/index.ts"],"names":[],"mappings":";AAKO,SAAS,MAAM,OAAA,EAAwD;AAC5E,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AACzC;AAGO,SAAS,kBAAkB,WAAA,EAA6B;AAC7D,EAAA,OAAO,IAAA,CAAK,KAAA,CAAO,WAAA,GAAc,EAAA,GAAM,GAAG,CAAA,GAAI,GAAA;AAChD;AAGO,SAAS,WAAW,IAAA,EAA6B;AACtD,EAAA,OAAO,IAAI,IAAA,CAAK,cAAA,CAAe,OAAA,EAAS;AAAA,IACtC,KAAA,EAAO,OAAA;AAAA,IACP,GAAA,EAAK,SAAA;AAAA,IACL,IAAA,EAAM;AAAA,GACP,CAAA,CAAE,MAAA,CAAO,IAAI,IAAA,CAAK,IAAI,CAAC,CAAA;AAC1B","file":"chunk-BLQ7T7NQ.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Utilities — pure helper functions\n// ---------------------------------------------------------------------------\n\n/** Merge class names, filtering out falsy values */\nexport function cn(...classes: (string | false | null | undefined)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\n/** Calculate the monthly-equivalent price from a yearly total */\nexport function monthlyEquivalent(yearlyTotal: number): number {\n return Math.round((yearlyTotal / 12) * 100) / 100;\n}\n\n/** Format a date for display (e.g. \"Jan 1, 2026\") */\nexport function formatDate(date: Date | string): string {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n }).format(new Date(date));\n}\n"]}
@@ -0,0 +1,158 @@
1
+ // src/whop/index.ts
2
+ var WHOP_API_BASE = "https://api.whop.com";
3
+ function base64url(bytes) {
4
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5
+ }
6
+ function randomString(len) {
7
+ return base64url(crypto.getRandomValues(new Uint8Array(len)));
8
+ }
9
+ async function sha256(str) {
10
+ const hash = await crypto.subtle.digest(
11
+ "SHA-256",
12
+ new TextEncoder().encode(str)
13
+ );
14
+ return base64url(new Uint8Array(hash));
15
+ }
16
+ async function buildAuthorizationUrl(redirectUri, clientId) {
17
+ const codeVerifier = randomString(32);
18
+ const codeChallenge = await sha256(codeVerifier);
19
+ const state = randomString(16);
20
+ const nonce = randomString(16);
21
+ const params = new URLSearchParams({
22
+ response_type: "code",
23
+ client_id: clientId,
24
+ redirect_uri: redirectUri,
25
+ scope: "openid profile email",
26
+ state,
27
+ nonce,
28
+ code_challenge: codeChallenge,
29
+ code_challenge_method: "S256"
30
+ });
31
+ return {
32
+ url: `${WHOP_API_BASE}/oauth/authorize?${params}`,
33
+ codeVerifier,
34
+ state,
35
+ nonce
36
+ };
37
+ }
38
+ async function exchangeCodeForTokens(code, codeVerifier, redirectUri, clientId) {
39
+ const res = await fetch(`${WHOP_API_BASE}/oauth/token`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({
43
+ grant_type: "authorization_code",
44
+ code,
45
+ redirect_uri: redirectUri,
46
+ client_id: clientId,
47
+ code_verifier: codeVerifier
48
+ })
49
+ });
50
+ if (!res.ok) {
51
+ const error = await res.text().catch(() => "Unknown error");
52
+ throw new Error(`Token exchange failed (${res.status}): ${error}`);
53
+ }
54
+ return res.json();
55
+ }
56
+ async function getWhopUser(accessToken) {
57
+ const res = await fetch(`${WHOP_API_BASE}/oauth/userinfo`, {
58
+ headers: { Authorization: `Bearer ${accessToken}` },
59
+ cache: "no-store"
60
+ });
61
+ if (!res.ok) {
62
+ throw new Error(`Failed to fetch user (${res.status})`);
63
+ }
64
+ return res.json();
65
+ }
66
+ async function checkWhopAccess(whopUserId, resourceId, apiKey) {
67
+ const res = await fetch(
68
+ `${WHOP_API_BASE}/api/v1/users/${whopUserId}/access/${resourceId}`,
69
+ {
70
+ headers: { Authorization: `Bearer ${apiKey}` },
71
+ cache: "no-store"
72
+ }
73
+ );
74
+ if (res.ok) {
75
+ const data = await res.json();
76
+ return {
77
+ hasAccess: data.has_access ?? false,
78
+ accessLevel: data.access_level ?? "no_access"
79
+ };
80
+ }
81
+ if (res.status === 403 || res.status === 404) {
82
+ return { hasAccess: false, accessLevel: "no_access" };
83
+ }
84
+ console.error(
85
+ `[whop-kit] Access check failed (${res.status}) for user ${whopUserId}`
86
+ );
87
+ return { hasAccess: false, accessLevel: "no_access" };
88
+ }
89
+ async function fetchWhopPlanDetails(planId, apiKey) {
90
+ try {
91
+ const res = await fetch(`${WHOP_API_BASE}/api/v1/plans/${planId}`, {
92
+ headers: { Authorization: `Bearer ${apiKey}` },
93
+ cache: "no-store"
94
+ });
95
+ if (!res.ok) {
96
+ console.error(
97
+ `[whop-kit] Failed to fetch plan ${planId} (${res.status})`
98
+ );
99
+ return null;
100
+ }
101
+ return res.json();
102
+ } catch (err) {
103
+ console.error(`[whop-kit] Error fetching plan ${planId}:`, err);
104
+ return null;
105
+ }
106
+ }
107
+ function getEffectivePrice(details) {
108
+ return details.plan_type === "renewal" ? details.renewal_price ?? 0 : details.initial_price ?? 0;
109
+ }
110
+ async function uncancelMembership(membershipId, apiKey) {
111
+ const res = await fetch(
112
+ `${WHOP_API_BASE}/api/v1/memberships/${membershipId}/uncancel`,
113
+ {
114
+ method: "POST",
115
+ headers: { Authorization: `Bearer ${apiKey}` }
116
+ }
117
+ );
118
+ return res.ok;
119
+ }
120
+ async function verifyWebhookSignature(body, headers, webhookSecret) {
121
+ const msgId = headers["webhook-id"];
122
+ const signature = headers["webhook-signature"];
123
+ const timestamp = headers["webhook-timestamp"];
124
+ if (!msgId || !signature || !timestamp) return false;
125
+ const now = Math.floor(Date.now() / 1e3);
126
+ const webhookTimestamp = parseInt(timestamp, 10);
127
+ if (Math.abs(now - webhookTimestamp) > 300) return false;
128
+ const secretBytes = new TextEncoder().encode(webhookSecret);
129
+ const toSign = `${msgId}.${timestamp}.${body}`;
130
+ const key = await crypto.subtle.importKey(
131
+ "raw",
132
+ secretBytes,
133
+ { name: "HMAC", hash: "SHA-256" },
134
+ false,
135
+ ["sign"]
136
+ );
137
+ const signatureBytes = await crypto.subtle.sign(
138
+ "HMAC",
139
+ key,
140
+ new TextEncoder().encode(toSign)
141
+ );
142
+ const expectedSignature = `v1,${base64url(new Uint8Array(signatureBytes))}`;
143
+ const providedSignatures = signature.split(" ");
144
+ const expectedBytes = new TextEncoder().encode(expectedSignature);
145
+ return providedSignatures.some((sig) => {
146
+ const sigBytes = new TextEncoder().encode(sig);
147
+ if (sigBytes.length !== expectedBytes.length) return false;
148
+ let diff = 0;
149
+ for (let i = 0; i < sigBytes.length; i++) {
150
+ diff |= sigBytes[i] ^ expectedBytes[i];
151
+ }
152
+ return diff === 0;
153
+ });
154
+ }
155
+
156
+ export { buildAuthorizationUrl, checkWhopAccess, exchangeCodeForTokens, fetchWhopPlanDetails, getEffectivePrice, getWhopUser, randomString, sha256, uncancelMembership, verifyWebhookSignature };
157
+ //# sourceMappingURL=chunk-EYK3S5U2.js.map
158
+ //# sourceMappingURL=chunk-EYK3S5U2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/whop/index.ts"],"names":[],"mappings":"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","file":"chunk-EYK3S5U2.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Whop API helpers — framework-agnostic\n// ---------------------------------------------------------------------------\n// Direct fetch calls to Whop's API. No SDK dependency — you can see\n// exactly what's happening. Works in any JS runtime with fetch + crypto.\n// ---------------------------------------------------------------------------\n\nconst WHOP_API_BASE = \"https://api.whop.com\";\n\n// ---------------------------------------------------------------------------\n// Crypto utilities\n// ---------------------------------------------------------------------------\n\nfunction base64url(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport function randomString(len: number): string {\n return base64url(crypto.getRandomValues(new Uint8Array(len)));\n}\n\nexport async function sha256(str: string): Promise<string> {\n const hash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(str),\n );\n return base64url(new Uint8Array(hash));\n}\n\n// ---------------------------------------------------------------------------\n// OAuth helpers\n// ---------------------------------------------------------------------------\n\nexport interface AuthorizationUrlResult {\n url: string;\n codeVerifier: string;\n state: string;\n nonce: string;\n}\n\n/**\n * Build the Whop OAuth authorization URL with PKCE.\n *\n * @param redirectUri - Your OAuth callback URL\n * @param clientId - Whop App ID\n */\nexport async function buildAuthorizationUrl(\n redirectUri: string,\n clientId: string,\n): Promise<AuthorizationUrlResult> {\n const codeVerifier = randomString(32);\n const codeChallenge = await sha256(codeVerifier);\n const state = randomString(16);\n const nonce = randomString(16);\n\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: \"openid profile email\",\n state,\n nonce,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n });\n\n return {\n url: `${WHOP_API_BASE}/oauth/authorize?${params}`,\n codeVerifier,\n state,\n nonce,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Token exchange\n// ---------------------------------------------------------------------------\n\nexport interface TokenResponse {\n access_token: string;\n refresh_token: string;\n token_type: string;\n expires_in: number;\n id_token?: string;\n}\n\n/**\n * Exchange an authorization code for tokens using PKCE.\n */\nexport async function exchangeCodeForTokens(\n code: string,\n codeVerifier: string,\n redirectUri: string,\n clientId: string,\n): Promise<TokenResponse> {\n const res = await fetch(`${WHOP_API_BASE}/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!res.ok) {\n const error = await res.text().catch(() => \"Unknown error\");\n throw new Error(`Token exchange failed (${res.status}): ${error}`);\n }\n\n return res.json() as Promise<TokenResponse>;\n}\n\n// ---------------------------------------------------------------------------\n// User info\n// ---------------------------------------------------------------------------\n\n/**\n * OIDC UserInfo response from Whop.\n * Fields depend on the scopes granted: openid, profile, email.\n */\nexport interface WhopUser {\n /** User ID (e.g. \"user_xxxxx\") */\n sub: string;\n /** Requires \"profile\" scope */\n name?: string;\n /** Requires \"profile\" scope */\n preferred_username?: string;\n /** Requires \"profile\" scope */\n picture?: string;\n /** Requires \"email\" scope */\n email?: string;\n /** Requires \"email\" scope */\n email_verified?: boolean;\n}\n\n/**\n * Fetch the authenticated user's profile from Whop's OIDC userinfo endpoint.\n */\nexport async function getWhopUser(accessToken: string): Promise<WhopUser> {\n const res = await fetch(`${WHOP_API_BASE}/oauth/userinfo`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n cache: \"no-store\",\n });\n\n if (!res.ok) {\n throw new Error(`Failed to fetch user (${res.status})`);\n }\n\n return res.json() as Promise<WhopUser>;\n}\n\n// ---------------------------------------------------------------------------\n// Access verification\n// ---------------------------------------------------------------------------\n\nexport interface AccessCheckResult {\n hasAccess: boolean;\n accessLevel: string;\n}\n\n/**\n * Check if a user has access to a specific Whop resource (product/experience).\n * Uses the Whop API directly for authoritative, real-time access checks.\n *\n * @param whopUserId - The user's Whop ID\n * @param resourceId - The product or experience ID to check\n * @param apiKey - Your Whop API key\n */\nexport async function checkWhopAccess(\n whopUserId: string,\n resourceId: string,\n apiKey: string,\n): Promise<AccessCheckResult> {\n const res = await fetch(\n `${WHOP_API_BASE}/api/v1/users/${whopUserId}/access/${resourceId}`,\n {\n headers: { Authorization: `Bearer ${apiKey}` },\n cache: \"no-store\",\n },\n );\n\n if (res.ok) {\n const data = (await res.json()) as Record<string, unknown>;\n return {\n hasAccess: (data.has_access as boolean) ?? false,\n accessLevel: (data.access_level as string) ?? \"no_access\",\n };\n }\n\n if (res.status === 403 || res.status === 404) {\n return { hasAccess: false, accessLevel: \"no_access\" };\n }\n\n console.error(\n `[whop-kit] Access check failed (${res.status}) for user ${whopUserId}`,\n );\n return { hasAccess: false, accessLevel: \"no_access\" };\n}\n\n// ---------------------------------------------------------------------------\n// Plan details\n// ---------------------------------------------------------------------------\n\nexport interface WhopPlanDetails {\n id: string;\n initial_price: number | null;\n renewal_price: number | null;\n billing_period: number | null;\n currency: string;\n plan_type: string;\n trial_period_days: number | null;\n}\n\n/**\n * Fetch plan details from the Whop API.\n */\nexport async function fetchWhopPlanDetails(\n planId: string,\n apiKey: string,\n): Promise<WhopPlanDetails | null> {\n try {\n const res = await fetch(`${WHOP_API_BASE}/api/v1/plans/${planId}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n cache: \"no-store\",\n });\n\n if (!res.ok) {\n console.error(\n `[whop-kit] Failed to fetch plan ${planId} (${res.status})`,\n );\n return null;\n }\n\n return res.json() as Promise<WhopPlanDetails>;\n } catch (err) {\n console.error(`[whop-kit] Error fetching plan ${planId}:`, err);\n return null;\n }\n}\n\n/**\n * Get the effective price from a Whop plan's details.\n * Renewal plans use renewal_price; others use initial_price.\n */\nexport function getEffectivePrice(details: WhopPlanDetails): number {\n return details.plan_type === \"renewal\"\n ? (details.renewal_price ?? 0)\n : (details.initial_price ?? 0);\n}\n\n// ---------------------------------------------------------------------------\n// Membership management\n// ---------------------------------------------------------------------------\n\n/**\n * Uncancel a membership via the Whop API.\n *\n * @param membershipId - The Whop membership ID\n * @param apiKey - Your Whop API key\n */\nexport async function uncancelMembership(\n membershipId: string,\n apiKey: string,\n): Promise<boolean> {\n const res = await fetch(\n `${WHOP_API_BASE}/api/v1/memberships/${membershipId}/uncancel`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${apiKey}` },\n },\n );\n return res.ok;\n}\n\n// ---------------------------------------------------------------------------\n// Webhook verification\n// ---------------------------------------------------------------------------\n\nexport interface WebhookHeaders {\n \"webhook-id\"?: string | null;\n \"webhook-signature\"?: string | null;\n \"webhook-timestamp\"?: string | null;\n}\n\n/**\n * Verify a Whop webhook signature.\n * Whop uses the standardwebhooks format: HMAC-SHA256 of \"{msg_id}.{timestamp}.{body}\".\n *\n * @param body - The raw request body string\n * @param headers - The webhook headers\n * @param webhookSecret - Your webhook signing secret\n */\nexport async function verifyWebhookSignature(\n body: string,\n headers: WebhookHeaders,\n webhookSecret: string,\n): Promise<boolean> {\n const msgId = headers[\"webhook-id\"];\n const signature = headers[\"webhook-signature\"];\n const timestamp = headers[\"webhook-timestamp\"];\n\n if (!msgId || !signature || !timestamp) return false;\n\n // Check timestamp to prevent replay attacks (5 minute tolerance)\n const now = Math.floor(Date.now() / 1000);\n const webhookTimestamp = parseInt(timestamp, 10);\n if (Math.abs(now - webhookTimestamp) > 300) return false;\n\n const secretBytes = new TextEncoder().encode(webhookSecret);\n const toSign = `${msgId}.${timestamp}.${body}`;\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n secretBytes,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n\n const signatureBytes = await crypto.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(toSign),\n );\n\n const expectedSignature = `v1,${base64url(new Uint8Array(signatureBytes))}`;\n\n // Check against all provided signatures (space-separated)\n // Use constant-time comparison to prevent timing attacks\n const providedSignatures = signature.split(\" \");\n const expectedBytes = new TextEncoder().encode(expectedSignature);\n\n return providedSignatures.some((sig) => {\n const sigBytes = new TextEncoder().encode(sig);\n if (sigBytes.length !== expectedBytes.length) return false;\n let diff = 0;\n for (let i = 0; i < sigBytes.length; i++) {\n diff |= sigBytes[i] ^ expectedBytes[i];\n }\n return diff === 0;\n });\n}\n"]}
@@ -0,0 +1,87 @@
1
+ import { SignJWT, jwtVerify } from 'jose';
2
+
3
+ // src/auth/index.ts
4
+ var DEFAULT_MAX_AGE = 60 * 60 * 24 * 7;
5
+ var SESSION_COOKIE = "session";
6
+ var LOGGED_IN_COOKIE = "logged_in";
7
+ async function createSessionToken(session, secret, options) {
8
+ const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE;
9
+ return new SignJWT({ ...session }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${maxAge}s`).sign(secret);
10
+ }
11
+ async function verifySessionToken(token, secret, validPlanKeys, defaultPlan) {
12
+ try {
13
+ const { payload } = await jwtVerify(token, secret);
14
+ if (typeof payload.userId !== "string" || typeof payload.whopUserId !== "string" || typeof payload.plan !== "string") {
15
+ return null;
16
+ }
17
+ const plan = validPlanKeys.includes(payload.plan) ? payload.plan : defaultPlan;
18
+ return {
19
+ userId: payload.userId,
20
+ whopUserId: payload.whopUserId,
21
+ email: payload.email ?? null,
22
+ name: payload.name ?? null,
23
+ profileImageUrl: payload.profileImageUrl ?? null,
24
+ plan,
25
+ cancelAtPeriodEnd: payload.cancelAtPeriodEnd ?? false,
26
+ isAdmin: payload.isAdmin ?? false
27
+ };
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function setSessionCookie(session, secret, cookies, isProduction = false) {
33
+ const token = await createSessionToken(session, secret);
34
+ const opts = {
35
+ httpOnly: true,
36
+ secure: isProduction,
37
+ sameSite: "lax",
38
+ maxAge: DEFAULT_MAX_AGE,
39
+ path: "/"
40
+ };
41
+ await cookies.set(SESSION_COOKIE, token, opts);
42
+ await cookies.set(LOGGED_IN_COOKIE, "1", {
43
+ ...opts,
44
+ httpOnly: false
45
+ });
46
+ }
47
+ async function clearSessionCookie(cookies, isProduction = false) {
48
+ const opts = {
49
+ httpOnly: true,
50
+ secure: isProduction,
51
+ sameSite: "lax",
52
+ maxAge: 0,
53
+ path: "/"
54
+ };
55
+ await cookies.set(SESSION_COOKIE, "", opts);
56
+ await cookies.set(LOGGED_IN_COOKIE, "", { ...opts, httpOnly: false });
57
+ }
58
+ async function getSessionFromCookie(cookies, secret, validPlanKeys, defaultPlan, refreshPlan) {
59
+ const token = await cookies.get(SESSION_COOKIE);
60
+ if (!token) return null;
61
+ const session = await verifySessionToken(
62
+ token,
63
+ secret,
64
+ validPlanKeys,
65
+ defaultPlan
66
+ );
67
+ if (!session) return null;
68
+ if (refreshPlan) {
69
+ const fresh = await refreshPlan(session.userId);
70
+ if (!fresh) return null;
71
+ const plan = validPlanKeys.includes(fresh.plan) ? fresh.plan : defaultPlan;
72
+ return { ...session, plan, cancelAtPeriodEnd: fresh.cancelAtPeriodEnd };
73
+ }
74
+ return session;
75
+ }
76
+ function generateSecret(byteLength = 32) {
77
+ const bytes = new Uint8Array(byteLength);
78
+ crypto.getRandomValues(bytes);
79
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
80
+ }
81
+ function encodeSecret(secret) {
82
+ return new TextEncoder().encode(secret);
83
+ }
84
+
85
+ export { clearSessionCookie, createSessionToken, encodeSecret, generateSecret, getSessionFromCookie, setSessionCookie, verifySessionToken };
86
+ //# sourceMappingURL=chunk-GXQLG4HV.js.map
87
+ //# sourceMappingURL=chunk-GXQLG4HV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth/index.ts"],"names":[],"mappings":"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","file":"chunk-GXQLG4HV.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Auth — JWT session management (framework-agnostic)\n// ---------------------------------------------------------------------------\n// Pure functions that take their dependencies as arguments.\n// Framework-specific wrappers (Next.js cookies(), Astro.cookies, etc.)\n// live in the template, not the kit.\n// ---------------------------------------------------------------------------\n\nimport { SignJWT, jwtVerify } from \"jose\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The session payload stored in the JWT and used throughout the app */\nexport interface Session {\n userId: string;\n whopUserId: string;\n email: string | null;\n name: string | null;\n profileImageUrl: string | null;\n plan: string;\n cancelAtPeriodEnd: boolean;\n isAdmin: boolean;\n}\n\n/** Options for session token creation */\nexport interface SessionTokenOptions {\n /** JWT max age in seconds. Defaults to 7 days. */\n maxAge?: number;\n}\n\n/** Cookie adapter interface — implement per framework */\nexport interface CookieAdapter {\n get(name: string): string | undefined | Promise<string | undefined>;\n set(\n name: string,\n value: string,\n options: CookieOptions,\n ): void | Promise<void>;\n delete(name: string): void | Promise<void>;\n}\n\nexport interface CookieOptions {\n httpOnly: boolean;\n secure: boolean;\n sameSite: \"lax\" | \"strict\" | \"none\";\n maxAge: number;\n path: string;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_MAX_AGE = 60 * 60 * 24 * 7; // 7 days\nconst SESSION_COOKIE = \"session\";\nconst LOGGED_IN_COOKIE = \"logged_in\";\n\n// ---------------------------------------------------------------------------\n// JWT helpers (pure functions)\n// ---------------------------------------------------------------------------\n\n/**\n * Create a signed JWT session token.\n *\n * @param session - The session payload\n * @param secret - The signing secret (Uint8Array)\n * @param options - Optional token settings\n */\nexport async function createSessionToken(\n session: Session,\n secret: Uint8Array,\n options?: SessionTokenOptions,\n): Promise<string> {\n const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE;\n return new SignJWT({ ...session })\n .setProtectedHeader({ alg: \"HS256\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAge}s`)\n .sign(secret);\n}\n\n/**\n * Verify and decode a JWT session token.\n *\n * @param token - The JWT string\n * @param secret - The signing secret (Uint8Array)\n * @param validPlanKeys - Array of valid plan keys (to validate the plan field)\n * @param defaultPlan - Fallback plan key if the JWT's plan is invalid\n */\nexport async function verifySessionToken(\n token: string,\n secret: Uint8Array,\n validPlanKeys: string[],\n defaultPlan: string,\n): Promise<Session | null> {\n try {\n const { payload } = await jwtVerify(token, secret);\n\n if (\n typeof payload.userId !== \"string\" ||\n typeof payload.whopUserId !== \"string\" ||\n typeof payload.plan !== \"string\"\n ) {\n return null;\n }\n\n const plan = validPlanKeys.includes(payload.plan)\n ? payload.plan\n : defaultPlan;\n\n return {\n userId: payload.userId,\n whopUserId: payload.whopUserId,\n email: (payload.email as string) ?? null,\n name: (payload.name as string) ?? null,\n profileImageUrl: (payload.profileImageUrl as string) ?? null,\n plan,\n cancelAtPeriodEnd: (payload.cancelAtPeriodEnd as boolean) ?? false,\n isAdmin: (payload.isAdmin as boolean) ?? false,\n };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cookie-based session helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Set the session cookie (and a non-httpOnly login indicator).\n *\n * @param session - The session payload\n * @param secret - JWT signing secret\n * @param cookies - Framework-specific cookie adapter\n * @param isProduction - Whether to set Secure flag\n */\nexport async function setSessionCookie(\n session: Session,\n secret: Uint8Array,\n cookies: CookieAdapter,\n isProduction = false,\n): Promise<void> {\n const token = await createSessionToken(session, secret);\n const opts: CookieOptions = {\n httpOnly: true,\n secure: isProduction,\n sameSite: \"lax\",\n maxAge: DEFAULT_MAX_AGE,\n path: \"/\",\n };\n await cookies.set(SESSION_COOKIE, token, opts);\n await cookies.set(LOGGED_IN_COOKIE, \"1\", {\n ...opts,\n httpOnly: false,\n });\n}\n\n/**\n * Clear the session cookie.\n *\n * @param cookies - Framework-specific cookie adapter\n * @param isProduction - Whether to set Secure flag\n */\nexport async function clearSessionCookie(\n cookies: CookieAdapter,\n isProduction = false,\n): Promise<void> {\n const opts: CookieOptions = {\n httpOnly: true,\n secure: isProduction,\n sameSite: \"lax\",\n maxAge: 0,\n path: \"/\",\n };\n await cookies.set(SESSION_COOKIE, \"\", opts);\n await cookies.set(LOGGED_IN_COOKIE, \"\", { ...opts, httpOnly: false });\n}\n\n/**\n * Read and verify the session from cookies, optionally refreshing the plan\n * from the database.\n *\n * @param cookies - Framework-specific cookie adapter\n * @param secret - JWT signing secret\n * @param validPlanKeys - Array of valid plan keys\n * @param defaultPlan - Fallback plan key\n * @param refreshPlan - Optional async function to fetch fresh plan from DB\n */\nexport async function getSessionFromCookie(\n cookies: CookieAdapter,\n secret: Uint8Array,\n validPlanKeys: string[],\n defaultPlan: string,\n refreshPlan?: (\n userId: string,\n ) => Promise<{ plan: string; cancelAtPeriodEnd: boolean } | null>,\n): Promise<Session | null> {\n const token = await cookies.get(SESSION_COOKIE);\n if (!token) return null;\n\n const session = await verifySessionToken(\n token,\n secret,\n validPlanKeys,\n defaultPlan,\n );\n if (!session) return null;\n\n if (refreshPlan) {\n const fresh = await refreshPlan(session.userId);\n if (!fresh) return null; // user deleted\n const plan = validPlanKeys.includes(fresh.plan)\n ? fresh.plan\n : defaultPlan;\n return { ...session, plan, cancelAtPeriodEnd: fresh.cancelAtPeriodEnd };\n }\n\n return session;\n}\n\n// ---------------------------------------------------------------------------\n// Secret helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a cryptographically secure secret string (hex-encoded).\n * Useful for auto-generating SESSION_SECRET on first run.\n */\nexport function generateSecret(byteLength = 32): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Encode a secret string to Uint8Array (for use with jose) */\nexport function encodeSecret(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n"]}
@@ -0,0 +1,86 @@
1
+ // src/email/index.ts
2
+ async function sendEmail(config, options) {
3
+ try {
4
+ switch (config.provider) {
5
+ case "resend":
6
+ return await sendViaResend(config.apiKey, options);
7
+ case "sendgrid":
8
+ return await sendViaSendGrid(config.apiKey, options);
9
+ default:
10
+ return {
11
+ success: false,
12
+ error: `Unknown email provider: ${config.provider}`
13
+ };
14
+ }
15
+ } catch (err) {
16
+ const message = err instanceof Error ? err.message : "Unknown error";
17
+ console.error("[whop-kit] Email send failed:", message);
18
+ return { success: false, error: message };
19
+ }
20
+ }
21
+ async function sendViaResend(apiKey, options) {
22
+ const res = await fetch("https://api.resend.com/emails", {
23
+ method: "POST",
24
+ headers: {
25
+ Authorization: `Bearer ${apiKey}`,
26
+ "Content-Type": "application/json"
27
+ },
28
+ body: JSON.stringify({
29
+ from: options.from,
30
+ to: [options.to],
31
+ subject: options.subject,
32
+ html: options.html
33
+ })
34
+ });
35
+ if (res.ok) return { success: true };
36
+ const data = await res.json().catch(() => ({}));
37
+ return {
38
+ success: false,
39
+ error: data.message || `Resend error (${res.status})`
40
+ };
41
+ }
42
+ async function sendViaSendGrid(apiKey, options) {
43
+ const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
44
+ method: "POST",
45
+ headers: {
46
+ Authorization: `Bearer ${apiKey}`,
47
+ "Content-Type": "application/json"
48
+ },
49
+ body: JSON.stringify({
50
+ personalizations: [{ to: [{ email: options.to }] }],
51
+ from: { email: options.from },
52
+ subject: options.subject,
53
+ content: [{ type: "text/html", value: options.html }]
54
+ })
55
+ });
56
+ if (res.ok || res.status === 202) return { success: true };
57
+ const data = await res.json().catch(() => ({}));
58
+ const errors = data.errors;
59
+ return {
60
+ success: false,
61
+ error: errors?.[0]?.message || `SendGrid error (${res.status})`
62
+ };
63
+ }
64
+ function escapeHtml(text) {
65
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
66
+ }
67
+ function emailWrapper(body, footerText) {
68
+ return `<!DOCTYPE html>
69
+ <html>
70
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
71
+ <body style="margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
72
+ <table width="100%" cellpadding="0" cellspacing="0" style="padding:40px 16px">
73
+ <tr><td align="center">
74
+ <table width="100%" style="max-width:480px;background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:32px">
75
+ <tr><td>${body}</td></tr>
76
+ </table>
77
+ <p style="margin-top:24px;font-size:12px;color:#a1a1aa">${escapeHtml(footerText)}</p>
78
+ </td></tr>
79
+ </table>
80
+ </body>
81
+ </html>`;
82
+ }
83
+
84
+ export { emailWrapper, escapeHtml, sendEmail };
85
+ //# sourceMappingURL=chunk-KQQGVBBH.js.map
86
+ //# sourceMappingURL=chunk-KQQGVBBH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/email/index.ts"],"names":[],"mappings":";AAiDA,eAAsB,SAAA,CACpB,QACA,OAAA,EAC0B;AAC1B,EAAA,IAAI;AACF,IAAA,QAAQ,OAAO,QAAA;AAAU,MACvB,KAAK,QAAA;AACH,QAAA,OAAO,MAAM,aAAA,CAAc,MAAA,CAAO,MAAA,EAAQ,OAAO,CAAA;AAAA,MACnD,KAAK,UAAA;AACH,QAAA,OAAO,MAAM,eAAA,CAAgB,MAAA,CAAO,MAAA,EAAQ,OAAO,CAAA;AAAA,MACrD;AACE,QAAA,OAAO;AAAA,UACL,OAAA,EAAS,KAAA;AAAA,UACT,KAAA,EAAO,CAAA,wBAAA,EAA2B,MAAA,CAAO,QAAQ,CAAA;AAAA,SACnD;AAAA;AACJ,EACF,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,OAAA,GAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,eAAA;AACrD,IAAA,OAAA,CAAQ,KAAA,CAAM,iCAAiC,OAAO,CAAA;AACtD,IAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,OAAA,EAAQ;AAAA,EAC1C;AACF;AAEA,eAAe,aAAA,CACb,QACA,OAAA,EAC0B;AAC1B,EAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,+BAAA,EAAiC;AAAA,IACvD,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,aAAA,EAAe,UAAU,MAAM,CAAA,CAAA;AAAA,MAC/B,cAAA,EAAgB;AAAA,KAClB;AAAA,IACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACnB,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,EAAA,EAAI,CAAC,OAAA,CAAQ,EAAE,CAAA;AAAA,MACf,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB,MAAM,OAAA,CAAQ;AAAA,KACf;AAAA,GACF,CAAA;AAED,EAAA,IAAI,GAAA,CAAI,EAAA,EAAI,OAAO,EAAE,SAAS,IAAA,EAAK;AAEnC,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,GAAO,KAAA,CAAM,OAAO,EAAC,CAAE,CAAA;AAC/C,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,KAAA,EAAO,IAAA,CAAK,OAAA,IAAW,CAAA,cAAA,EAAiB,IAAI,MAAM,CAAA,CAAA;AAAA,GACpD;AACF;AAEA,eAAe,eAAA,CACb,QACA,OAAA,EAC0B;AAC1B,EAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,uCAAA,EAAyC;AAAA,IAC/D,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,aAAA,EAAe,UAAU,MAAM,CAAA,CAAA;AAAA,MAC/B,cAAA,EAAgB;AAAA,KAClB;AAAA,IACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACnB,gBAAA,EAAkB,CAAC,EAAE,EAAA,EAAI,CAAC,EAAE,KAAA,EAAO,OAAA,CAAQ,EAAA,EAAI,CAAA,EAAG,CAAA;AAAA,MAClD,IAAA,EAAM,EAAE,KAAA,EAAO,OAAA,CAAQ,IAAA,EAAK;AAAA,MAC5B,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,aAAa,KAAA,EAAO,OAAA,CAAQ,MAAM;AAAA,KACrD;AAAA,GACF,CAAA;AAED,EAAA,IAAI,GAAA,CAAI,MAAM,GAAA,CAAI,MAAA,KAAW,KAAK,OAAO,EAAE,SAAS,IAAA,EAAK;AAEzD,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,GAAO,KAAA,CAAM,OAAO,EAAC,CAAE,CAAA;AAC/C,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA;AACpB,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,OAAO,MAAA,GAAS,CAAC,GAAG,OAAA,IAAW,CAAA,gBAAA,EAAmB,IAAI,MAAM,CAAA,CAAA;AAAA,GAC9D;AACF;AAOO,SAAS,WAAW,IAAA,EAAsB;AAC/C,EAAA,OAAO,KACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CACrB,OAAA,CAAQ,MAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,EACpB,OAAA,CAAQ,IAAA,EAAM,QAAQ,CAAA,CACtB,OAAA,CAAQ,MAAM,OAAO,CAAA;AAC1B;AASO,SAAS,YAAA,CAAa,MAAc,UAAA,EAA4B;AACrE,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAA,EAOS,IAAI,CAAA;AAAA;AAAA,8DAAA,EAE0C,UAAA,CAAW,UAAU,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA,OAAA,CAAA;AAKtF","file":"chunk-KQQGVBBH.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Email — provider-agnostic email sending\n// ---------------------------------------------------------------------------\n// Supports Resend and SendGrid out of the box via direct fetch calls.\n// No SDK dependencies. Add new providers by extending the switch.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type EmailProvider = \"resend\" | \"sendgrid\";\n\nexport interface SendEmailOptions {\n to: string;\n subject: string;\n html: string;\n from: string;\n}\n\nexport interface SendEmailResult {\n success: boolean;\n error?: string;\n}\n\nexport interface EmailConfig {\n provider: EmailProvider;\n apiKey: string;\n}\n\n// ---------------------------------------------------------------------------\n// Sending\n// ---------------------------------------------------------------------------\n\n/**\n * Send an email via a configured provider (Resend or SendGrid).\n * Uses direct fetch — no SDK dependency.\n *\n * @example\n * const result = await sendEmail(\n * { provider: \"resend\", apiKey: \"re_xxxxx\" },\n * {\n * to: \"user@example.com\",\n * from: \"noreply@myapp.com\",\n * subject: \"Welcome!\",\n * html: \"<h1>Hello</h1>\",\n * },\n * );\n */\nexport async function sendEmail(\n config: EmailConfig,\n options: SendEmailOptions,\n): Promise<SendEmailResult> {\n try {\n switch (config.provider) {\n case \"resend\":\n return await sendViaResend(config.apiKey, options);\n case \"sendgrid\":\n return await sendViaSendGrid(config.apiKey, options);\n default:\n return {\n success: false,\n error: `Unknown email provider: ${config.provider}`,\n };\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Unknown error\";\n console.error(\"[whop-kit] Email send failed:\", message);\n return { success: false, error: message };\n }\n}\n\nasync function sendViaResend(\n apiKey: string,\n options: SendEmailOptions,\n): Promise<SendEmailResult> {\n const res = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: options.from,\n to: [options.to],\n subject: options.subject,\n html: options.html,\n }),\n });\n\n if (res.ok) return { success: true };\n\n const data = (await res.json().catch(() => ({}))) as Record<string, string>;\n return {\n success: false,\n error: data.message || `Resend error (${res.status})`,\n };\n}\n\nasync function sendViaSendGrid(\n apiKey: string,\n options: SendEmailOptions,\n): Promise<SendEmailResult> {\n const res = await fetch(\"https://api.sendgrid.com/v3/mail/send\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n personalizations: [{ to: [{ email: options.to }] }],\n from: { email: options.from },\n subject: options.subject,\n content: [{ type: \"text/html\", value: options.html }],\n }),\n });\n\n if (res.ok || res.status === 202) return { success: true };\n\n const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;\n const errors = data.errors as Array<{ message?: string }> | undefined;\n return {\n success: false,\n error: errors?.[0]?.message || `SendGrid error (${res.status})`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Template helpers\n// ---------------------------------------------------------------------------\n\n/** Escape HTML special characters to prevent XSS in email templates */\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\n/**\n * Wrap email body content in a standard HTML email layout.\n * Inline CSS for maximum email client compatibility.\n *\n * @param body - The inner HTML content\n * @param footerText - Text shown in the footer (e.g. app name)\n */\nexport function emailWrapper(body: string, footerText: string): string {\n return `<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head>\n<body style=\"margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding:40px 16px\">\n <tr><td align=\"center\">\n <table width=\"100%\" style=\"max-width:480px;background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:32px\">\n <tr><td>${body}</td></tr>\n </table>\n <p style=\"margin-top:24px;font-size:12px;color:#a1a1aa\">${escapeHtml(footerText)}</p>\n </td></tr>\n </table>\n</body>\n</html>`;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ // src/subscriptions/index.ts
2
+ function createSubscriptionHelpers(db, defaultPlan, validPlanKeys) {
3
+ async function getSubscriptionDetails(userId) {
4
+ try {
5
+ const user = await db.findUserById(userId);
6
+ if (!user) {
7
+ return { hasSubscription: false, error: "User not found" };
8
+ }
9
+ const plan = validPlanKeys.includes(user.plan) ? user.plan : defaultPlan;
10
+ const isPaid = plan !== defaultPlan;
11
+ if (!isPaid) {
12
+ return { hasSubscription: false };
13
+ }
14
+ return {
15
+ hasSubscription: true,
16
+ subscription: {
17
+ plan,
18
+ whopMembershipId: user.whopMembershipId,
19
+ cancelAtPeriodEnd: user.cancelAtPeriodEnd,
20
+ status: user.cancelAtPeriodEnd ? "canceling" : "active"
21
+ }
22
+ };
23
+ } catch (error) {
24
+ console.error("[whop-kit] Failed to get subscription details:", error);
25
+ return { hasSubscription: false, error: "Database error" };
26
+ }
27
+ }
28
+ async function isUserSubscribed(userId) {
29
+ const result = await getSubscriptionDetails(userId);
30
+ return result.hasSubscription && result.subscription?.status === "active";
31
+ }
32
+ async function getUserSubscriptionStatus(userId) {
33
+ const result = await getSubscriptionDetails(userId);
34
+ if (!result.hasSubscription || !result.subscription) return "free";
35
+ return result.subscription.status;
36
+ }
37
+ async function getUserCreatedAt(userId) {
38
+ return db.getUserCreatedAt(userId);
39
+ }
40
+ async function getUserForNotification(whopUserId) {
41
+ const user = await db.findUserByWhopId(whopUserId);
42
+ if (!user?.email) return null;
43
+ return { email: user.email, name: user.name };
44
+ }
45
+ async function activateMembership(whopUserId, plan, membershipId) {
46
+ await db.upsertMembership(whopUserId, plan, membershipId);
47
+ }
48
+ async function deactivateMembership(whopUserId) {
49
+ await db.deactivateMembership(whopUserId, defaultPlan);
50
+ }
51
+ async function updateCancelAtPeriodEnd(whopUserId, cancelAtPeriodEnd) {
52
+ await db.updateCancelAtPeriodEnd(whopUserId, cancelAtPeriodEnd);
53
+ }
54
+ async function uncancelSubscription(userId) {
55
+ await db.uncancelSubscription(userId);
56
+ }
57
+ return {
58
+ getSubscriptionDetails,
59
+ isUserSubscribed,
60
+ getUserSubscriptionStatus,
61
+ getUserCreatedAt,
62
+ getUserForNotification,
63
+ activateMembership,
64
+ deactivateMembership,
65
+ updateCancelAtPeriodEnd,
66
+ uncancelSubscription
67
+ };
68
+ }
69
+
70
+ export { createSubscriptionHelpers };
71
+ //# sourceMappingURL=chunk-NS32METI.js.map
72
+ //# sourceMappingURL=chunk-NS32METI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/subscriptions/index.ts"],"names":[],"mappings":"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","file":"chunk-NS32METI.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Subscription helpers — framework-agnostic\n// ---------------------------------------------------------------------------\n// All database operations go through the DbAdapter interface.\n// Templates provide their own adapter (Prisma, Drizzle, raw SQL, etc.).\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Minimal user record for subscription operations */\nexport interface UserRecord {\n id: string;\n whopUserId: string;\n email: string | null;\n name: string | null;\n plan: string;\n whopMembershipId: string | null;\n cancelAtPeriodEnd: boolean;\n createdAt: Date;\n}\n\nexport type SubscriptionStatus = \"active\" | \"canceling\" | \"free\";\n\nexport interface SubscriptionDetails {\n plan: string;\n whopMembershipId: string | null;\n cancelAtPeriodEnd: boolean;\n status: SubscriptionStatus;\n}\n\nexport interface SubscriptionDetailsResult {\n hasSubscription: boolean;\n subscription?: SubscriptionDetails;\n error?: string;\n}\n\n/** Database adapter — implement per ORM/driver */\nexport interface DbAdapter {\n /** Find a user by internal ID */\n findUserById(\n id: string,\n ): Promise<Pick<\n UserRecord,\n \"plan\" | \"whopMembershipId\" | \"cancelAtPeriodEnd\"\n > | null>;\n\n /** Find a user by Whop user ID */\n findUserByWhopId(\n whopUserId: string,\n ): Promise<Pick<UserRecord, \"email\" | \"name\"> | null>;\n\n /** Get a user's creation date */\n getUserCreatedAt(id: string): Promise<Date | null>;\n\n /** Create or update a user on membership activation */\n upsertMembership(\n whopUserId: string,\n plan: string,\n membershipId: string | null,\n ): Promise<void>;\n\n /** Downgrade a user to the default plan */\n deactivateMembership(whopUserId: string, defaultPlan: string): Promise<void>;\n\n /** Update the cancel-at-period-end flag */\n updateCancelAtPeriodEnd(\n whopUserId: string,\n cancelAtPeriodEnd: boolean,\n ): Promise<void>;\n\n /** Reverse a pending cancellation by user ID */\n uncancelSubscription(userId: string): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport interface SubscriptionHelpers {\n getSubscriptionDetails(userId: string): Promise<SubscriptionDetailsResult>;\n isUserSubscribed(userId: string): Promise<boolean>;\n getUserSubscriptionStatus(userId: string): Promise<SubscriptionStatus>;\n getUserCreatedAt(userId: string): Promise<Date | null>;\n getUserForNotification(\n whopUserId: string,\n ): Promise<{ email: string; name: string | null } | null>;\n activateMembership(\n whopUserId: string,\n plan: string,\n membershipId: string | null,\n ): Promise<void>;\n deactivateMembership(whopUserId: string): Promise<void>;\n updateCancelAtPeriodEnd(\n whopUserId: string,\n cancelAtPeriodEnd: boolean,\n ): Promise<void>;\n uncancelSubscription(userId: string): Promise<void>;\n}\n\n/**\n * Create subscription helpers bound to a database adapter.\n *\n * @param db - Your database adapter implementation\n * @param defaultPlan - The default/free plan key\n * @param validPlanKeys - Array of all valid plan keys\n *\n * @example\n * import { createSubscriptionHelpers } from 'whop-kit/subscriptions'\n * import { prismaDbAdapter } from './adapters/prisma'\n *\n * const subs = createSubscriptionHelpers(\n * prismaDbAdapter(prisma),\n * plans.defaultPlan,\n * plans.keys,\n * );\n *\n * const result = await subs.getSubscriptionDetails(userId);\n */\nexport function createSubscriptionHelpers(\n db: DbAdapter,\n defaultPlan: string,\n validPlanKeys: string[],\n): SubscriptionHelpers {\n async function getSubscriptionDetails(\n userId: string,\n ): Promise<SubscriptionDetailsResult> {\n try {\n const user = await db.findUserById(userId);\n if (!user) {\n return { hasSubscription: false, error: \"User not found\" };\n }\n\n const plan = validPlanKeys.includes(user.plan)\n ? user.plan\n : defaultPlan;\n const isPaid = plan !== defaultPlan;\n\n if (!isPaid) {\n return { hasSubscription: false };\n }\n\n return {\n hasSubscription: true,\n subscription: {\n plan,\n whopMembershipId: user.whopMembershipId,\n cancelAtPeriodEnd: user.cancelAtPeriodEnd,\n status: user.cancelAtPeriodEnd ? \"canceling\" : \"active\",\n },\n };\n } catch (error) {\n console.error(\"[whop-kit] Failed to get subscription details:\", error);\n return { hasSubscription: false, error: \"Database error\" };\n }\n }\n\n async function isUserSubscribed(userId: string): Promise<boolean> {\n const result = await getSubscriptionDetails(userId);\n return (\n result.hasSubscription && result.subscription?.status === \"active\"\n );\n }\n\n async function getUserSubscriptionStatus(\n userId: string,\n ): Promise<SubscriptionStatus> {\n const result = await getSubscriptionDetails(userId);\n if (!result.hasSubscription || !result.subscription) return \"free\";\n return result.subscription.status;\n }\n\n async function getUserCreatedAt(userId: string): Promise<Date | null> {\n return db.getUserCreatedAt(userId);\n }\n\n async function getUserForNotification(\n whopUserId: string,\n ): Promise<{ email: string; name: string | null } | null> {\n const user = await db.findUserByWhopId(whopUserId);\n if (!user?.email) return null;\n return { email: user.email, name: user.name };\n }\n\n async function activateMembership(\n whopUserId: string,\n plan: string,\n membershipId: string | null,\n ): Promise<void> {\n await db.upsertMembership(whopUserId, plan, membershipId);\n }\n\n async function deactivateMembership(whopUserId: string): Promise<void> {\n await db.deactivateMembership(whopUserId, defaultPlan);\n }\n\n async function updateCancelAtPeriodEnd(\n whopUserId: string,\n cancelAtPeriodEnd: boolean,\n ): Promise<void> {\n await db.updateCancelAtPeriodEnd(whopUserId, cancelAtPeriodEnd);\n }\n\n async function uncancelSubscription(userId: string): Promise<void> {\n await db.uncancelSubscription(userId);\n }\n\n return {\n getSubscriptionDetails,\n isUserSubscribed,\n getUserSubscriptionStatus,\n getUserCreatedAt,\n getUserForNotification,\n activateMembership,\n deactivateMembership,\n updateCancelAtPeriodEnd,\n uncancelSubscription,\n };\n}\n"]}
@@ -0,0 +1,53 @@
1
+ // src/config/index.ts
2
+ function createConfigManager(options) {
3
+ const { store, envMap = {}, cacheTtlMs = 3e4 } = options;
4
+ const getEnv = options.getEnv ?? ((name) => process.env[name]);
5
+ const cache = /* @__PURE__ */ new Map();
6
+ async function get(key) {
7
+ const cached = cache.get(key);
8
+ if (cached !== void 0 && Date.now() < cached.expiresAt) {
9
+ return cached.value;
10
+ }
11
+ if (cached !== void 0) cache.delete(key);
12
+ const envKey = envMap[key];
13
+ if (envKey) {
14
+ const envVal = getEnv(envKey);
15
+ if (envVal) {
16
+ cache.set(key, {
17
+ value: envVal,
18
+ expiresAt: Date.now() + cacheTtlMs
19
+ });
20
+ return envVal;
21
+ }
22
+ }
23
+ try {
24
+ const value = await store.get(key);
25
+ if (value !== null) {
26
+ cache.set(key, {
27
+ value,
28
+ expiresAt: Date.now() + cacheTtlMs
29
+ });
30
+ return value;
31
+ }
32
+ } catch {
33
+ }
34
+ return null;
35
+ }
36
+ async function set(key, value) {
37
+ await store.set(key, value);
38
+ cache.set(key, { value, expiresAt: Date.now() + cacheTtlMs });
39
+ }
40
+ async function setMany(configs) {
41
+ await Promise.all(
42
+ Object.entries(configs).filter(([, value]) => !!value).map(([key, value]) => set(key, value))
43
+ );
44
+ }
45
+ function clearCache() {
46
+ cache.clear();
47
+ }
48
+ return { get, set, setMany, clearCache };
49
+ }
50
+
51
+ export { createConfigManager };
52
+ //# sourceMappingURL=chunk-PLZUFPEG.js.map
53
+ //# sourceMappingURL=chunk-PLZUFPEG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/index.ts"],"names":[],"mappings":";AA0DO,SAAS,oBACd,OAAA,EACe;AACf,EAAA,MAAM,EAAE,KAAA,EAAO,MAAA,GAAS,EAAC,EAAG,UAAA,GAAa,KAAO,GAAI,OAAA;AACpD,EAAA,MAAM,SACJ,OAAA,CAAQ,MAAA,KAAW,CAAC,IAAA,KAAiB,OAAA,CAAQ,IAAI,IAAI,CAAA,CAAA;AAEvD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAkD;AAEpE,EAAA,eAAe,IAAI,GAAA,EAAqC;AAEtD,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC5B,IAAA,IAAI,WAAW,MAAA,IAAa,IAAA,CAAK,GAAA,EAAI,GAAI,OAAO,SAAA,EAAW;AACzD,MAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IAChB;AACA,IAAA,IAAI,MAAA,KAAW,MAAA,EAAW,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA;AAG1C,IAAA,MAAM,MAAA,GAAS,OAAO,GAAG,CAAA;AACzB,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,MAAM,MAAA,GAAS,OAAO,MAAM,CAAA;AAC5B,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,KAAA,CAAM,IAAI,GAAA,EAAK;AAAA,UACb,KAAA,EAAO,MAAA;AAAA,UACP,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,SACzB,CAAA;AACD,QAAA,OAAO,MAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AACjC,MAAA,IAAI,UAAU,IAAA,EAAM;AAClB,QAAA,KAAA,CAAM,IAAI,GAAA,EAAK;AAAA,UACb,KAAA;AAAA,UACA,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,SACzB,CAAA;AACD,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,eAAe,GAAA,CAAI,KAAa,KAAA,EAA8B;AAC5D,IAAA,MAAM,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAC1B,IAAA,KAAA,CAAM,GAAA,CAAI,KAAK,EAAE,KAAA,EAAO,WAAW,IAAA,CAAK,GAAA,EAAI,GAAI,UAAA,EAAY,CAAA;AAAA,EAC9D;AAEA,EAAA,eAAe,QAAQ,OAAA,EAAgD;AACrE,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,MACZ,MAAA,CAAO,QAAQ,OAAO,CAAA,CACnB,OAAO,CAAC,GAAG,KAAK,CAAA,KAAM,CAAC,CAAC,KAAK,CAAA,CAC7B,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,KAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAC;AAAA,KAC1C;AAAA,EACF;AAEA,EAAA,SAAS,UAAA,GAAmB;AAC1B,IAAA,KAAA,CAAM,KAAA,EAAM;AAAA,EACd;AAEA,EAAA,OAAO,EAAE,GAAA,EAAK,GAAA,EAAK,OAAA,EAAS,UAAA,EAAW;AACzC","file":"chunk-PLZUFPEG.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Config system — framework-agnostic key-value store with caching\n// ---------------------------------------------------------------------------\n// Provides a ConfigStore interface that templates implement (e.g. with\n// Prisma, Drizzle, KV store, etc.). The config manager adds an in-memory\n// cache layer on top, plus optional env var fallback.\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Persistent config store — implement per database/backend */\nexport interface ConfigStore {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n}\n\nexport interface ConfigManagerOptions {\n /** The persistent store (database, KV, file, etc.) */\n store: ConfigStore;\n /** Map of config key → env var name for fallback lookups */\n envMap?: Record<string, string>;\n /** Cache TTL in milliseconds. Defaults to 30000 (30s). */\n cacheTtlMs?: number;\n /** Function to read env vars. Defaults to process.env lookup. */\n getEnv?: (name: string) => string | undefined;\n}\n\nexport interface ConfigManager {\n /** Read a config value (cache → env → store) */\n get(key: string): Promise<string | null>;\n /** Write a config value (store + cache) */\n set(key: string, value: string): Promise<void>;\n /** Bulk set config values */\n setMany(configs: Record<string, string>): Promise<void>;\n /** Clear the in-memory cache (useful for testing) */\n clearCache(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Create a config manager with in-memory caching and optional env var fallback.\n *\n * @example\n * const config = createConfigManager({\n * store: prismaConfigStore(prisma),\n * envMap: {\n * whop_app_id: \"WHOP_APP_ID\",\n * whop_api_key: \"WHOP_API_KEY\",\n * },\n * });\n *\n * const appId = await config.get(\"whop_app_id\");\n */\nexport function createConfigManager(\n options: ConfigManagerOptions,\n): ConfigManager {\n const { store, envMap = {}, cacheTtlMs = 30_000 } = options;\n const getEnv =\n options.getEnv ?? ((name: string) => process.env[name]);\n\n const cache = new Map<string, { value: string; expiresAt: number }>();\n\n async function get(key: string): Promise<string | null> {\n // 1. In-memory cache (with TTL)\n const cached = cache.get(key);\n if (cached !== undefined && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n if (cached !== undefined) cache.delete(key);\n\n // 2. Env var fallback\n const envKey = envMap[key];\n if (envKey) {\n const envVal = getEnv(envKey);\n if (envVal) {\n cache.set(key, {\n value: envVal,\n expiresAt: Date.now() + cacheTtlMs,\n });\n return envVal;\n }\n }\n\n // 3. Persistent store\n try {\n const value = await store.get(key);\n if (value !== null) {\n cache.set(key, {\n value,\n expiresAt: Date.now() + cacheTtlMs,\n });\n return value;\n }\n } catch {\n // Store might not be ready (e.g. during first build)\n }\n\n return null;\n }\n\n async function set(key: string, value: string): Promise<void> {\n await store.set(key, value);\n cache.set(key, { value, expiresAt: Date.now() + cacheTtlMs });\n }\n\n async function setMany(configs: Record<string, string>): Promise<void> {\n await Promise.all(\n Object.entries(configs)\n .filter(([, value]) => !!value)\n .map(([key, value]) => set(key, value)),\n );\n }\n\n function clearCache(): void {\n cache.clear();\n }\n\n return { get, set, setMany, clearCache };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ // src/core/index.ts
2
+ function definePlans(metadata) {
3
+ const keys = Object.keys(metadata);
4
+ const ranks = Object.fromEntries(
5
+ keys.map((key, index) => [key, index])
6
+ );
7
+ const defaultPlan = keys[0];
8
+ return {
9
+ metadata,
10
+ keys,
11
+ ranks,
12
+ defaultPlan,
13
+ hasMinimum(userPlan, minimumPlan) {
14
+ return (ranks[userPlan] ?? 0) >= (ranks[minimumPlan] ?? 0);
15
+ },
16
+ getBillingIntervals(key) {
17
+ const meta = metadata[key];
18
+ return [...meta.billingIntervals ?? ["monthly", "yearly"]];
19
+ },
20
+ configKey: (key) => `whop_${key}_plan_id`,
21
+ configKeyYearly: (key) => `whop_${key}_plan_id_yearly`,
22
+ priceConfigKey: (key) => `whop_${key}_price_monthly`,
23
+ priceConfigKeyYearly: (key) => `whop_${key}_price_yearly`,
24
+ nameConfigKey: (key) => `plan_${key}_name`
25
+ };
26
+ }
27
+
28
+ export { definePlans };
29
+ //# sourceMappingURL=chunk-ZQ44KZRC.js.map
30
+ //# sourceMappingURL=chunk-ZQ44KZRC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/index.ts"],"names":[],"mappings":";AAgEO,SAAS,YACd,QAAA,EACe;AACf,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AACjC,EAAA,MAAM,QAAQ,MAAA,CAAO,WAAA;AAAA,IACnB,IAAA,CAAK,IAAI,CAAC,GAAA,EAAK,UAAU,CAAC,GAAA,EAAK,KAAK,CAAC;AAAA,GACvC;AACA,EAAA,MAAM,WAAA,GAAc,KAAK,CAAC,CAAA;AAE1B,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,IAAA;AAAA,IACA,KAAA;AAAA,IACA,WAAA;AAAA,IAEA,UAAA,CAAW,UAAa,WAAA,EAAyB;AAC/C,MAAA,OAAA,CAAQ,MAAM,QAAQ,CAAA,IAAK,CAAA,MAAO,KAAA,CAAM,WAAW,CAAA,IAAK,CAAA,CAAA;AAAA,IAC1D,CAAA;AAAA,IAEA,oBAAoB,GAAA,EAA2B;AAC7C,MAAA,MAAM,IAAA,GAAO,SAAS,GAAG,CAAA;AACzB,MAAA,OAAO,CAAC,GAAI,IAAA,CAAK,oBAAoB,CAAC,SAAA,EAAW,QAAQ,CAAE,CAAA;AAAA,IAC7D,CAAA;AAAA,IAEA,SAAA,EAAW,CAAC,GAAA,KAAW,CAAA,KAAA,EAAQ,GAAG,CAAA,QAAA,CAAA;AAAA,IAClC,eAAA,EAAiB,CAAC,GAAA,KAAW,CAAA,KAAA,EAAQ,GAAG,CAAA,eAAA,CAAA;AAAA,IACxC,cAAA,EAAgB,CAAC,GAAA,KAAW,CAAA,KAAA,EAAQ,GAAG,CAAA,cAAA,CAAA;AAAA,IACvC,oBAAA,EAAsB,CAAC,GAAA,KAAW,CAAA,KAAA,EAAQ,GAAG,CAAA,aAAA,CAAA;AAAA,IAC7C,aAAA,EAAe,CAAC,GAAA,KAAW,CAAA,KAAA,EAAQ,GAAG,CAAA,KAAA;AAAA,GACxC;AACF","file":"chunk-ZQ44KZRC.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Core plan system — framework-agnostic, parameterized\n// ---------------------------------------------------------------------------\n// Users define their own plans via definePlans(). The kit never hardcodes\n// plan names, tiers, or pricing. Everything is derived from the definition.\n// ---------------------------------------------------------------------------\n\nexport type BillingInterval = \"monthly\" | \"yearly\";\n\n/** Shape of each plan entry passed to definePlans() */\nexport interface PlanMetadataEntry {\n name: string;\n description: string;\n priceMonthly: number;\n priceYearly: number;\n features: readonly string[];\n highlighted: boolean;\n /** Display-only free trial length (configure the actual trial in Whop) */\n trialDays?: number;\n /** Which billing intervals to offer. Defaults to [\"monthly\", \"yearly\"]. */\n billingIntervals?: readonly BillingInterval[];\n}\n\n/** The resolved plan system returned by definePlans() */\nexport interface PlanSystem<K extends string = string> {\n /** The full metadata object as passed in */\n metadata: Record<K, PlanMetadataEntry>;\n /** Ordered array of plan keys (insertion order = hierarchy) */\n keys: K[];\n /** Numeric rank for each plan (used for comparisons) */\n ranks: Record<K, number>;\n /** The lowest-tier plan key (first in metadata) */\n defaultPlan: K;\n /** Check if a user's plan meets or exceeds a minimum plan level */\n hasMinimum: (userPlan: K, minimumPlan: K) => boolean;\n /** Get the billing intervals a plan supports */\n getBillingIntervals: (key: K) => BillingInterval[];\n /** Config key for a plan's Whop plan ID (monthly) */\n configKey: (key: K) => string;\n /** Config key for a plan's Whop plan ID (yearly) */\n configKeyYearly: (key: K) => string;\n /** Config key for a plan's cached price (monthly) */\n priceConfigKey: (key: K) => string;\n /** Config key for a plan's cached price (yearly) */\n priceConfigKeyYearly: (key: K) => string;\n /** Config key for a plan's admin-customized name */\n nameConfigKey: (key: K) => string;\n}\n\n/**\n * Define your app's plan system. Key order defines the hierarchy\n * (first = lowest/free, last = highest).\n *\n * @example\n * const plans = definePlans({\n * free: { name: \"Free\", description: \"Get started\", ... },\n * starter: { name: \"Starter\", description: \"For teams\", ... },\n * pro: { name: \"Pro\", description: \"For power users\", ... },\n * });\n *\n * plans.hasMinimum(\"starter\", \"free\") // true\n * plans.hasMinimum(\"free\", \"pro\") // false\n * plans.defaultPlan // \"free\"\n */\nexport function definePlans<K extends string>(\n metadata: Record<K, PlanMetadataEntry>,\n): PlanSystem<K> {\n const keys = Object.keys(metadata) as K[];\n const ranks = Object.fromEntries(\n keys.map((key, index) => [key, index]),\n ) as Record<K, number>;\n const defaultPlan = keys[0];\n\n return {\n metadata,\n keys,\n ranks,\n defaultPlan,\n\n hasMinimum(userPlan: K, minimumPlan: K): boolean {\n return (ranks[userPlan] ?? 0) >= (ranks[minimumPlan] ?? 0);\n },\n\n getBillingIntervals(key: K): BillingInterval[] {\n const meta = metadata[key];\n return [...(meta.billingIntervals ?? [\"monthly\", \"yearly\"])];\n },\n\n configKey: (key: K) => `whop_${key}_plan_id`,\n configKeyYearly: (key: K) => `whop_${key}_plan_id_yearly`,\n priceConfigKey: (key: K) => `whop_${key}_price_monthly`,\n priceConfigKeyYearly: (key: K) => `whop_${key}_price_yearly`,\n nameConfigKey: (key: K) => `plan_${key}_name`,\n };\n}\n"]}
@@ -0,0 +1,42 @@
1
+ /** Persistent config store — implement per database/backend */
2
+ interface ConfigStore {
3
+ get(key: string): Promise<string | null>;
4
+ set(key: string, value: string): Promise<void>;
5
+ }
6
+ interface ConfigManagerOptions {
7
+ /** The persistent store (database, KV, file, etc.) */
8
+ store: ConfigStore;
9
+ /** Map of config key → env var name for fallback lookups */
10
+ envMap?: Record<string, string>;
11
+ /** Cache TTL in milliseconds. Defaults to 30000 (30s). */
12
+ cacheTtlMs?: number;
13
+ /** Function to read env vars. Defaults to process.env lookup. */
14
+ getEnv?: (name: string) => string | undefined;
15
+ }
16
+ interface ConfigManager {
17
+ /** Read a config value (cache → env → store) */
18
+ get(key: string): Promise<string | null>;
19
+ /** Write a config value (store + cache) */
20
+ set(key: string, value: string): Promise<void>;
21
+ /** Bulk set config values */
22
+ setMany(configs: Record<string, string>): Promise<void>;
23
+ /** Clear the in-memory cache (useful for testing) */
24
+ clearCache(): void;
25
+ }
26
+ /**
27
+ * Create a config manager with in-memory caching and optional env var fallback.
28
+ *
29
+ * @example
30
+ * const config = createConfigManager({
31
+ * store: prismaConfigStore(prisma),
32
+ * envMap: {
33
+ * whop_app_id: "WHOP_APP_ID",
34
+ * whop_api_key: "WHOP_API_KEY",
35
+ * },
36
+ * });
37
+ *
38
+ * const appId = await config.get("whop_app_id");
39
+ */
40
+ declare function createConfigManager(options: ConfigManagerOptions): ConfigManager;
41
+
42
+ export { type ConfigManager, type ConfigManagerOptions, type ConfigStore, createConfigManager };
@@ -0,0 +1,3 @@
1
+ export { createConfigManager } from '../chunk-PLZUFPEG.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,57 @@
1
+ type BillingInterval = "monthly" | "yearly";
2
+ /** Shape of each plan entry passed to definePlans() */
3
+ interface PlanMetadataEntry {
4
+ name: string;
5
+ description: string;
6
+ priceMonthly: number;
7
+ priceYearly: number;
8
+ features: readonly string[];
9
+ highlighted: boolean;
10
+ /** Display-only free trial length (configure the actual trial in Whop) */
11
+ trialDays?: number;
12
+ /** Which billing intervals to offer. Defaults to ["monthly", "yearly"]. */
13
+ billingIntervals?: readonly BillingInterval[];
14
+ }
15
+ /** The resolved plan system returned by definePlans() */
16
+ interface PlanSystem<K extends string = string> {
17
+ /** The full metadata object as passed in */
18
+ metadata: Record<K, PlanMetadataEntry>;
19
+ /** Ordered array of plan keys (insertion order = hierarchy) */
20
+ keys: K[];
21
+ /** Numeric rank for each plan (used for comparisons) */
22
+ ranks: Record<K, number>;
23
+ /** The lowest-tier plan key (first in metadata) */
24
+ defaultPlan: K;
25
+ /** Check if a user's plan meets or exceeds a minimum plan level */
26
+ hasMinimum: (userPlan: K, minimumPlan: K) => boolean;
27
+ /** Get the billing intervals a plan supports */
28
+ getBillingIntervals: (key: K) => BillingInterval[];
29
+ /** Config key for a plan's Whop plan ID (monthly) */
30
+ configKey: (key: K) => string;
31
+ /** Config key for a plan's Whop plan ID (yearly) */
32
+ configKeyYearly: (key: K) => string;
33
+ /** Config key for a plan's cached price (monthly) */
34
+ priceConfigKey: (key: K) => string;
35
+ /** Config key for a plan's cached price (yearly) */
36
+ priceConfigKeyYearly: (key: K) => string;
37
+ /** Config key for a plan's admin-customized name */
38
+ nameConfigKey: (key: K) => string;
39
+ }
40
+ /**
41
+ * Define your app's plan system. Key order defines the hierarchy
42
+ * (first = lowest/free, last = highest).
43
+ *
44
+ * @example
45
+ * const plans = definePlans({
46
+ * free: { name: "Free", description: "Get started", ... },
47
+ * starter: { name: "Starter", description: "For teams", ... },
48
+ * pro: { name: "Pro", description: "For power users", ... },
49
+ * });
50
+ *
51
+ * plans.hasMinimum("starter", "free") // true
52
+ * plans.hasMinimum("free", "pro") // false
53
+ * plans.defaultPlan // "free"
54
+ */
55
+ declare function definePlans<K extends string>(metadata: Record<K, PlanMetadataEntry>): PlanSystem<K>;
56
+
57
+ export { type BillingInterval, type PlanMetadataEntry, type PlanSystem, definePlans };
@@ -0,0 +1,3 @@
1
+ export { definePlans } from '../chunk-ZQ44KZRC.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,43 @@
1
+ type EmailProvider = "resend" | "sendgrid";
2
+ interface SendEmailOptions {
3
+ to: string;
4
+ subject: string;
5
+ html: string;
6
+ from: string;
7
+ }
8
+ interface SendEmailResult {
9
+ success: boolean;
10
+ error?: string;
11
+ }
12
+ interface EmailConfig {
13
+ provider: EmailProvider;
14
+ apiKey: string;
15
+ }
16
+ /**
17
+ * Send an email via a configured provider (Resend or SendGrid).
18
+ * Uses direct fetch — no SDK dependency.
19
+ *
20
+ * @example
21
+ * const result = await sendEmail(
22
+ * { provider: "resend", apiKey: "re_xxxxx" },
23
+ * {
24
+ * to: "user@example.com",
25
+ * from: "noreply@myapp.com",
26
+ * subject: "Welcome!",
27
+ * html: "<h1>Hello</h1>",
28
+ * },
29
+ * );
30
+ */
31
+ declare function sendEmail(config: EmailConfig, options: SendEmailOptions): Promise<SendEmailResult>;
32
+ /** Escape HTML special characters to prevent XSS in email templates */
33
+ declare function escapeHtml(text: string): string;
34
+ /**
35
+ * Wrap email body content in a standard HTML email layout.
36
+ * Inline CSS for maximum email client compatibility.
37
+ *
38
+ * @param body - The inner HTML content
39
+ * @param footerText - Text shown in the footer (e.g. app name)
40
+ */
41
+ declare function emailWrapper(body: string, footerText: string): string;
42
+
43
+ export { type EmailConfig, type EmailProvider, type SendEmailOptions, type SendEmailResult, emailWrapper, escapeHtml, sendEmail };
@@ -0,0 +1,3 @@
1
+ export { emailWrapper, escapeHtml, sendEmail } from '../chunk-KQQGVBBH.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,7 @@
1
+ export { BillingInterval, PlanMetadataEntry, PlanSystem, definePlans } from './core/index.js';
2
+ export { CookieAdapter, CookieOptions, Session, SessionTokenOptions, clearSessionCookie, createSessionToken, encodeSecret, generateSecret, getSessionFromCookie, setSessionCookie, verifySessionToken } from './auth/index.js';
3
+ export { AccessCheckResult, AuthorizationUrlResult, TokenResponse, WebhookHeaders, WhopPlanDetails, WhopUser, buildAuthorizationUrl, checkWhopAccess, exchangeCodeForTokens, fetchWhopPlanDetails, getEffectivePrice, getWhopUser, randomString, sha256, uncancelMembership, verifyWebhookSignature } from './whop/index.js';
4
+ export { ConfigManager, ConfigManagerOptions, ConfigStore, createConfigManager } from './config/index.js';
5
+ export { DbAdapter, SubscriptionDetails, SubscriptionDetailsResult, SubscriptionHelpers, SubscriptionStatus, UserRecord, createSubscriptionHelpers } from './subscriptions/index.js';
6
+ export { EmailConfig, EmailProvider, SendEmailOptions, SendEmailResult, emailWrapper, escapeHtml, sendEmail } from './email/index.js';
7
+ export { cn, formatDate, monthlyEquivalent } from './utils/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { clearSessionCookie, createSessionToken, encodeSecret, generateSecret, getSessionFromCookie, setSessionCookie, verifySessionToken } from './chunk-GXQLG4HV.js';
2
+ export { createConfigManager } from './chunk-PLZUFPEG.js';
3
+ export { definePlans } from './chunk-ZQ44KZRC.js';
4
+ export { emailWrapper, escapeHtml, sendEmail } from './chunk-KQQGVBBH.js';
5
+ export { createSubscriptionHelpers } from './chunk-NS32METI.js';
6
+ export { cn, formatDate, monthlyEquivalent } from './chunk-BLQ7T7NQ.js';
7
+ export { buildAuthorizationUrl, checkWhopAccess, exchangeCodeForTokens, fetchWhopPlanDetails, getEffectivePrice, getWhopUser, randomString, sha256, uncancelMembership, verifyWebhookSignature } from './chunk-EYK3S5U2.js';
8
+ //# sourceMappingURL=index.js.map
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,76 @@
1
+ /** Minimal user record for subscription operations */
2
+ interface UserRecord {
3
+ id: string;
4
+ whopUserId: string;
5
+ email: string | null;
6
+ name: string | null;
7
+ plan: string;
8
+ whopMembershipId: string | null;
9
+ cancelAtPeriodEnd: boolean;
10
+ createdAt: Date;
11
+ }
12
+ type SubscriptionStatus = "active" | "canceling" | "free";
13
+ interface SubscriptionDetails {
14
+ plan: string;
15
+ whopMembershipId: string | null;
16
+ cancelAtPeriodEnd: boolean;
17
+ status: SubscriptionStatus;
18
+ }
19
+ interface SubscriptionDetailsResult {
20
+ hasSubscription: boolean;
21
+ subscription?: SubscriptionDetails;
22
+ error?: string;
23
+ }
24
+ /** Database adapter — implement per ORM/driver */
25
+ interface DbAdapter {
26
+ /** Find a user by internal ID */
27
+ findUserById(id: string): Promise<Pick<UserRecord, "plan" | "whopMembershipId" | "cancelAtPeriodEnd"> | null>;
28
+ /** Find a user by Whop user ID */
29
+ findUserByWhopId(whopUserId: string): Promise<Pick<UserRecord, "email" | "name"> | null>;
30
+ /** Get a user's creation date */
31
+ getUserCreatedAt(id: string): Promise<Date | null>;
32
+ /** Create or update a user on membership activation */
33
+ upsertMembership(whopUserId: string, plan: string, membershipId: string | null): Promise<void>;
34
+ /** Downgrade a user to the default plan */
35
+ deactivateMembership(whopUserId: string, defaultPlan: string): Promise<void>;
36
+ /** Update the cancel-at-period-end flag */
37
+ updateCancelAtPeriodEnd(whopUserId: string, cancelAtPeriodEnd: boolean): Promise<void>;
38
+ /** Reverse a pending cancellation by user ID */
39
+ uncancelSubscription(userId: string): Promise<void>;
40
+ }
41
+ interface SubscriptionHelpers {
42
+ getSubscriptionDetails(userId: string): Promise<SubscriptionDetailsResult>;
43
+ isUserSubscribed(userId: string): Promise<boolean>;
44
+ getUserSubscriptionStatus(userId: string): Promise<SubscriptionStatus>;
45
+ getUserCreatedAt(userId: string): Promise<Date | null>;
46
+ getUserForNotification(whopUserId: string): Promise<{
47
+ email: string;
48
+ name: string | null;
49
+ } | null>;
50
+ activateMembership(whopUserId: string, plan: string, membershipId: string | null): Promise<void>;
51
+ deactivateMembership(whopUserId: string): Promise<void>;
52
+ updateCancelAtPeriodEnd(whopUserId: string, cancelAtPeriodEnd: boolean): Promise<void>;
53
+ uncancelSubscription(userId: string): Promise<void>;
54
+ }
55
+ /**
56
+ * Create subscription helpers bound to a database adapter.
57
+ *
58
+ * @param db - Your database adapter implementation
59
+ * @param defaultPlan - The default/free plan key
60
+ * @param validPlanKeys - Array of all valid plan keys
61
+ *
62
+ * @example
63
+ * import { createSubscriptionHelpers } from 'whop-kit/subscriptions'
64
+ * import { prismaDbAdapter } from './adapters/prisma'
65
+ *
66
+ * const subs = createSubscriptionHelpers(
67
+ * prismaDbAdapter(prisma),
68
+ * plans.defaultPlan,
69
+ * plans.keys,
70
+ * );
71
+ *
72
+ * const result = await subs.getSubscriptionDetails(userId);
73
+ */
74
+ declare function createSubscriptionHelpers(db: DbAdapter, defaultPlan: string, validPlanKeys: string[]): SubscriptionHelpers;
75
+
76
+ export { type DbAdapter, type SubscriptionDetails, type SubscriptionDetailsResult, type SubscriptionHelpers, type SubscriptionStatus, type UserRecord, createSubscriptionHelpers };
@@ -0,0 +1,3 @@
1
+ export { createSubscriptionHelpers } from '../chunk-NS32METI.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,8 @@
1
+ /** Merge class names, filtering out falsy values */
2
+ declare function cn(...classes: (string | false | null | undefined)[]): string;
3
+ /** Calculate the monthly-equivalent price from a yearly total */
4
+ declare function monthlyEquivalent(yearlyTotal: number): number;
5
+ /** Format a date for display (e.g. "Jan 1, 2026") */
6
+ declare function formatDate(date: Date | string): string;
7
+
8
+ export { cn, formatDate, monthlyEquivalent };
@@ -0,0 +1,3 @@
1
+ export { cn, formatDate, monthlyEquivalent } from '../chunk-BLQ7T7NQ.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,102 @@
1
+ declare function randomString(len: number): string;
2
+ declare function sha256(str: string): Promise<string>;
3
+ interface AuthorizationUrlResult {
4
+ url: string;
5
+ codeVerifier: string;
6
+ state: string;
7
+ nonce: string;
8
+ }
9
+ /**
10
+ * Build the Whop OAuth authorization URL with PKCE.
11
+ *
12
+ * @param redirectUri - Your OAuth callback URL
13
+ * @param clientId - Whop App ID
14
+ */
15
+ declare function buildAuthorizationUrl(redirectUri: string, clientId: string): Promise<AuthorizationUrlResult>;
16
+ interface TokenResponse {
17
+ access_token: string;
18
+ refresh_token: string;
19
+ token_type: string;
20
+ expires_in: number;
21
+ id_token?: string;
22
+ }
23
+ /**
24
+ * Exchange an authorization code for tokens using PKCE.
25
+ */
26
+ declare function exchangeCodeForTokens(code: string, codeVerifier: string, redirectUri: string, clientId: string): Promise<TokenResponse>;
27
+ /**
28
+ * OIDC UserInfo response from Whop.
29
+ * Fields depend on the scopes granted: openid, profile, email.
30
+ */
31
+ interface WhopUser {
32
+ /** User ID (e.g. "user_xxxxx") */
33
+ sub: string;
34
+ /** Requires "profile" scope */
35
+ name?: string;
36
+ /** Requires "profile" scope */
37
+ preferred_username?: string;
38
+ /** Requires "profile" scope */
39
+ picture?: string;
40
+ /** Requires "email" scope */
41
+ email?: string;
42
+ /** Requires "email" scope */
43
+ email_verified?: boolean;
44
+ }
45
+ /**
46
+ * Fetch the authenticated user's profile from Whop's OIDC userinfo endpoint.
47
+ */
48
+ declare function getWhopUser(accessToken: string): Promise<WhopUser>;
49
+ interface AccessCheckResult {
50
+ hasAccess: boolean;
51
+ accessLevel: string;
52
+ }
53
+ /**
54
+ * Check if a user has access to a specific Whop resource (product/experience).
55
+ * Uses the Whop API directly for authoritative, real-time access checks.
56
+ *
57
+ * @param whopUserId - The user's Whop ID
58
+ * @param resourceId - The product or experience ID to check
59
+ * @param apiKey - Your Whop API key
60
+ */
61
+ declare function checkWhopAccess(whopUserId: string, resourceId: string, apiKey: string): Promise<AccessCheckResult>;
62
+ interface WhopPlanDetails {
63
+ id: string;
64
+ initial_price: number | null;
65
+ renewal_price: number | null;
66
+ billing_period: number | null;
67
+ currency: string;
68
+ plan_type: string;
69
+ trial_period_days: number | null;
70
+ }
71
+ /**
72
+ * Fetch plan details from the Whop API.
73
+ */
74
+ declare function fetchWhopPlanDetails(planId: string, apiKey: string): Promise<WhopPlanDetails | null>;
75
+ /**
76
+ * Get the effective price from a Whop plan's details.
77
+ * Renewal plans use renewal_price; others use initial_price.
78
+ */
79
+ declare function getEffectivePrice(details: WhopPlanDetails): number;
80
+ /**
81
+ * Uncancel a membership via the Whop API.
82
+ *
83
+ * @param membershipId - The Whop membership ID
84
+ * @param apiKey - Your Whop API key
85
+ */
86
+ declare function uncancelMembership(membershipId: string, apiKey: string): Promise<boolean>;
87
+ interface WebhookHeaders {
88
+ "webhook-id"?: string | null;
89
+ "webhook-signature"?: string | null;
90
+ "webhook-timestamp"?: string | null;
91
+ }
92
+ /**
93
+ * Verify a Whop webhook signature.
94
+ * Whop uses the standardwebhooks format: HMAC-SHA256 of "{msg_id}.{timestamp}.{body}".
95
+ *
96
+ * @param body - The raw request body string
97
+ * @param headers - The webhook headers
98
+ * @param webhookSecret - Your webhook signing secret
99
+ */
100
+ declare function verifyWebhookSignature(body: string, headers: WebhookHeaders, webhookSecret: string): Promise<boolean>;
101
+
102
+ export { type AccessCheckResult, type AuthorizationUrlResult, type TokenResponse, type WebhookHeaders, type WhopPlanDetails, type WhopUser, buildAuthorizationUrl, checkWhopAccess, exchangeCodeForTokens, fetchWhopPlanDetails, getEffectivePrice, getWhopUser, randomString, sha256, uncancelMembership, verifyWebhookSignature };
@@ -0,0 +1,3 @@
1
+ export { buildAuthorizationUrl, checkWhopAccess, exchangeCodeForTokens, fetchWhopPlanDetails, getEffectivePrice, getWhopUser, randomString, sha256, uncancelMembership, verifyWebhookSignature } from '../chunk-EYK3S5U2.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "whop-kit",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic toolkit for building apps with Whop authentication, payments, and memberships",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Colin McDermott",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/colinmcd/whop-kit"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./core": {
18
+ "import": "./dist/core/index.js",
19
+ "types": "./dist/core/index.d.ts"
20
+ },
21
+ "./auth": {
22
+ "import": "./dist/auth/index.js",
23
+ "types": "./dist/auth/index.d.ts"
24
+ },
25
+ "./whop": {
26
+ "import": "./dist/whop/index.js",
27
+ "types": "./dist/whop/index.d.ts"
28
+ },
29
+ "./config": {
30
+ "import": "./dist/config/index.js",
31
+ "types": "./dist/config/index.d.ts"
32
+ },
33
+ "./subscriptions": {
34
+ "import": "./dist/subscriptions/index.js",
35
+ "types": "./dist/subscriptions/index.d.ts"
36
+ },
37
+ "./email": {
38
+ "import": "./dist/email/index.js",
39
+ "types": "./dist/email/index.d.ts"
40
+ },
41
+ "./utils": {
42
+ "import": "./dist/utils/index.js",
43
+ "types": "./dist/utils/index.d.ts"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "typecheck": "tsc --noEmit",
55
+ "prepublishOnly": "npm run build"
56
+ },
57
+ "devDependencies": {
58
+ "jose": "^6.2.2",
59
+ "tsup": "^8.4.0",
60
+ "typescript": "^5.9.3"
61
+ },
62
+ "peerDependencies": {
63
+ "jose": "^6.0.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "jose": {
67
+ "optional": false
68
+ }
69
+ }
70
+ }