@ascendkit/nextjs 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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client/hooks.d.ts +109 -0
  3. package/dist/client/hooks.d.ts.map +1 -0
  4. package/dist/client/hooks.js +372 -0
  5. package/dist/client/index.d.ts +4 -0
  6. package/dist/client/index.d.ts.map +1 -0
  7. package/dist/client/index.js +3 -0
  8. package/dist/client/provider.d.ts +66 -0
  9. package/dist/client/provider.d.ts.map +1 -0
  10. package/dist/client/provider.js +284 -0
  11. package/dist/client/use-analytics.d.ts +27 -0
  12. package/dist/client/use-analytics.d.ts.map +1 -0
  13. package/dist/client/use-analytics.js +133 -0
  14. package/dist/components/auth-card.d.ts +20 -0
  15. package/dist/components/auth-card.d.ts.map +1 -0
  16. package/dist/components/auth-card.js +128 -0
  17. package/dist/components/auth-modal.d.ts +9 -0
  18. package/dist/components/auth-modal.d.ts.map +1 -0
  19. package/dist/components/auth-modal.js +110 -0
  20. package/dist/components/branding-badge.d.ts +2 -0
  21. package/dist/components/branding-badge.d.ts.map +1 -0
  22. package/dist/components/branding-badge.js +9 -0
  23. package/dist/components/email-verification.d.ts +2 -0
  24. package/dist/components/email-verification.d.ts.map +1 -0
  25. package/dist/components/email-verification.js +48 -0
  26. package/dist/components/forgot-password.d.ts +6 -0
  27. package/dist/components/forgot-password.d.ts.map +1 -0
  28. package/dist/components/forgot-password.js +37 -0
  29. package/dist/components/index.d.ts +13 -0
  30. package/dist/components/index.d.ts.map +1 -0
  31. package/dist/components/index.js +12 -0
  32. package/dist/components/login.d.ts +9 -0
  33. package/dist/components/login.d.ts.map +1 -0
  34. package/dist/components/login.js +48 -0
  35. package/dist/components/reset-password.d.ts +6 -0
  36. package/dist/components/reset-password.d.ts.map +1 -0
  37. package/dist/components/reset-password.js +47 -0
  38. package/dist/components/sign-in-button.d.ts +19 -0
  39. package/dist/components/sign-in-button.d.ts.map +1 -0
  40. package/dist/components/sign-in-button.js +27 -0
  41. package/dist/components/sign-up-button.d.ts +19 -0
  42. package/dist/components/sign-up-button.d.ts.map +1 -0
  43. package/dist/components/sign-up-button.js +27 -0
  44. package/dist/components/signup.d.ts +9 -0
  45. package/dist/components/signup.d.ts.map +1 -0
  46. package/dist/components/signup.js +60 -0
  47. package/dist/components/social-button.d.ts +8 -0
  48. package/dist/components/social-button.d.ts.map +1 -0
  49. package/dist/components/social-button.js +10 -0
  50. package/dist/components/user-button.d.ts +11 -0
  51. package/dist/components/user-button.d.ts.map +1 -0
  52. package/dist/components/user-button.js +14 -0
  53. package/dist/index.d.ts +6 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +8 -0
  56. package/dist/server/access-token.d.ts +39 -0
  57. package/dist/server/access-token.d.ts.map +1 -0
  58. package/dist/server/access-token.js +74 -0
  59. package/dist/server/adapter.d.ts +6 -0
  60. package/dist/server/adapter.d.ts.map +1 -0
  61. package/dist/server/adapter.js +57 -0
  62. package/dist/server/analytics.d.ts +61 -0
  63. package/dist/server/analytics.d.ts.map +1 -0
  64. package/dist/server/analytics.js +117 -0
  65. package/dist/server/ascendkit-auth.d.ts +122 -0
  66. package/dist/server/ascendkit-auth.d.ts.map +1 -0
  67. package/dist/server/ascendkit-auth.js +146 -0
  68. package/dist/server/auth-runtime.d.ts +12 -0
  69. package/dist/server/auth-runtime.d.ts.map +1 -0
  70. package/dist/server/auth-runtime.js +123 -0
  71. package/dist/server/config-fetcher.d.ts +22 -0
  72. package/dist/server/config-fetcher.d.ts.map +1 -0
  73. package/dist/server/config-fetcher.js +26 -0
  74. package/dist/server/email-sender.d.ts +29 -0
  75. package/dist/server/email-sender.d.ts.map +1 -0
  76. package/dist/server/email-sender.js +58 -0
  77. package/dist/server/index.d.ts +10 -0
  78. package/dist/server/index.d.ts.map +1 -0
  79. package/dist/server/index.js +7 -0
  80. package/dist/server/oauth-proxy-plugin.d.ts +10 -0
  81. package/dist/server/oauth-proxy-plugin.d.ts.map +1 -0
  82. package/dist/server/oauth-proxy-plugin.js +156 -0
  83. package/dist/server/social-providers.d.ts +12 -0
  84. package/dist/server/social-providers.d.ts.map +1 -0
  85. package/dist/server/social-providers.js +15 -0
  86. package/dist/server/webhooks.d.ts +43 -0
  87. package/dist/server/webhooks.d.ts.map +1 -0
  88. package/dist/server/webhooks.js +83 -0
  89. package/dist/shared/http-client.d.ts +17 -0
  90. package/dist/shared/http-client.d.ts.map +1 -0
  91. package/dist/shared/http-client.js +52 -0
  92. package/dist/shared/types.d.ts +49 -0
  93. package/dist/shared/types.d.ts.map +1 -0
  94. package/dist/shared/types.js +1 -0
  95. package/package.json +49 -0
@@ -0,0 +1,123 @@
1
+ import { buildAscendKitAuthFromConfig, createDisabledAuth } from "./ascendkit-auth";
2
+ import { fetchAuthConfigConditional } from "./config-fetcher";
3
+ function isCallbackPath(request) {
4
+ try {
5
+ return new URL(request.url).pathname.includes("/callback/");
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ async function shouldRetryWithPrevious(response) {
12
+ if (response.status === 404) {
13
+ return true;
14
+ }
15
+ if (response.status !== 400 && response.status !== 403) {
16
+ return false;
17
+ }
18
+ const contentType = (response.headers.get("content-type") || "").toLowerCase();
19
+ if (!contentType.includes("application/json")) {
20
+ return false;
21
+ }
22
+ const body = await response.clone().json().catch(() => null);
23
+ const detail = String(body?.error || body?.detail || body?.message || "").toLowerCase();
24
+ return detail.includes("provider") || detail.includes("oauth") || detail.includes("social");
25
+ }
26
+ export function createAscendKitAuthRuntime(options = {}) {
27
+ const publicKey = options.publicKey || process.env.ASCENDKIT_ENV_KEY || "";
28
+ const secretKey = options.secretKey || process.env.ASCENDKIT_SECRET_KEY || "";
29
+ const apiUrl = options.apiUrl || process.env.ASCENDKIT_API_URL || "https://api.ascendkit.com";
30
+ const callbackGraceMs = 5 * 60 * 1000;
31
+ const minRefreshIntervalMs = 10 * 1000;
32
+ let currentAuth = null;
33
+ let currentEtag;
34
+ let previousAuth = null;
35
+ let refreshInFlight = null;
36
+ let nextRefreshCheckAt = 0;
37
+ async function refreshAuth() {
38
+ const now = Date.now();
39
+ if (currentAuth && now < nextRefreshCheckAt) {
40
+ return;
41
+ }
42
+ if (refreshInFlight) {
43
+ return refreshInFlight;
44
+ }
45
+ const refreshTask = (async () => {
46
+ try {
47
+ const result = await fetchAuthConfigConditional(publicKey, secretKey, apiUrl, currentEtag);
48
+ if (result.notModified) {
49
+ if (result.etag) {
50
+ currentEtag = result.etag;
51
+ }
52
+ console.info("[AscendKit runtime] config_refresh_outcome=hit_304");
53
+ if (!currentAuth) {
54
+ currentAuth = createDisabledAuth(publicKey, apiUrl);
55
+ }
56
+ return;
57
+ }
58
+ if (!result.config) {
59
+ console.warn("[AscendKit runtime] config_refresh_outcome=cold_fail reason=missing_config");
60
+ if (!currentAuth) {
61
+ currentAuth = createDisabledAuth(publicKey, apiUrl);
62
+ }
63
+ return;
64
+ }
65
+ const nextAuth = await buildAscendKitAuthFromConfig({
66
+ publicKey,
67
+ apiUrl,
68
+ config: result.config,
69
+ waitlistRedirectPath: options.waitlistRedirectPath,
70
+ rejectedRedirectPath: options.rejectedRedirectPath,
71
+ });
72
+ if (currentAuth) {
73
+ previousAuth = {
74
+ auth: currentAuth,
75
+ expiresAt: Date.now() + callbackGraceMs,
76
+ };
77
+ }
78
+ currentAuth = nextAuth;
79
+ currentEtag = result.etag ?? currentEtag;
80
+ console.info("[AscendKit runtime] config_refresh_outcome=updated_200");
81
+ }
82
+ catch (err) {
83
+ console.warn("[AscendKit runtime] config_refresh_outcome=fallback_last_known_good", err instanceof Error ? err.message : err);
84
+ if (!currentAuth) {
85
+ currentAuth = createDisabledAuth(publicKey, apiUrl);
86
+ }
87
+ }
88
+ })();
89
+ refreshInFlight = refreshTask.finally(() => {
90
+ refreshInFlight = null;
91
+ nextRefreshCheckAt = Date.now() + minRefreshIntervalMs;
92
+ });
93
+ return refreshInFlight;
94
+ }
95
+ async function getAuth() {
96
+ await refreshAuth();
97
+ if (!currentAuth) {
98
+ currentAuth = createDisabledAuth(publicKey, apiUrl);
99
+ }
100
+ return currentAuth;
101
+ }
102
+ async function handler(request) {
103
+ const activeAuth = await getAuth();
104
+ const callbackRequest = isCallbackPath(request) ? request.clone() : null;
105
+ const response = await activeAuth.handler(request);
106
+ if (callbackRequest
107
+ && previousAuth
108
+ && previousAuth.expiresAt > Date.now()
109
+ && await shouldRetryWithPrevious(response)) {
110
+ return previousAuth.auth.handler(callbackRequest);
111
+ }
112
+ return response;
113
+ }
114
+ async function getSession(headers) {
115
+ const activeAuth = await getAuth();
116
+ return await activeAuth.api.getSession({ headers });
117
+ }
118
+ return {
119
+ handler,
120
+ getSession,
121
+ getAuth,
122
+ };
123
+ }
@@ -0,0 +1,22 @@
1
+ import type { AuthConfig } from "../shared/types";
2
+ export interface ConditionalAuthConfigResult {
3
+ notModified: boolean;
4
+ config?: AuthConfig;
5
+ etag?: string;
6
+ }
7
+ /**
8
+ * Fetches the full auth configuration from the AscendKit backend.
9
+ * Returns enabled providers, OAuth credentials (with secrets), and feature flags.
10
+ *
11
+ * Uses the server-config endpoint authenticated via secret key.
12
+ * Used by AscendKitAuth to configure Better Auth dynamically.
13
+ */
14
+ export declare function fetchAuthConfig(publicKey: string, secretKey: string, apiUrl: string): Promise<AuthConfig>;
15
+ /**
16
+ * Fetches auth configuration with conditional ETag support.
17
+ *
18
+ * - Sends If-None-Match when an ETag is provided
19
+ * - Returns notModified=true for 304 responses
20
+ */
21
+ export declare function fetchAuthConfigConditional(publicKey: string, secretKey: string, apiUrl: string, etag?: string): Promise<ConditionalAuthConfigResult>;
22
+ //# sourceMappingURL=config-fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-fetcher.d.ts","sourceRoot":"","sources":["../../src/server/config-fetcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAM/G;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,2BAA2B,CAAC,CAItC"}
@@ -0,0 +1,26 @@
1
+ import { createHttpClient } from "../shared/http-client";
2
+ /**
3
+ * Fetches the full auth configuration from the AscendKit backend.
4
+ * Returns enabled providers, OAuth credentials (with secrets), and feature flags.
5
+ *
6
+ * Uses the server-config endpoint authenticated via secret key.
7
+ * Used by AscendKitAuth to configure Better Auth dynamically.
8
+ */
9
+ export async function fetchAuthConfig(publicKey, secretKey, apiUrl) {
10
+ const result = await fetchAuthConfigConditional(publicKey, secretKey, apiUrl);
11
+ if (!result.config) {
12
+ throw new Error("Server returned no auth config payload");
13
+ }
14
+ return result.config;
15
+ }
16
+ /**
17
+ * Fetches auth configuration with conditional ETag support.
18
+ *
19
+ * - Sends If-None-Match when an ETag is provided
20
+ * - Returns notModified=true for 304 responses
21
+ */
22
+ export async function fetchAuthConfigConditional(publicKey, secretKey, apiUrl, etag) {
23
+ const http = createHttpClient({ publicKey, apiUrl, secretKey });
24
+ const result = await http.getConditional("/api/auth/settings/server-config", etag);
25
+ return { notModified: result.notModified, config: result.data, etag: result.etag };
26
+ }
@@ -0,0 +1,29 @@
1
+ interface EmailSenderOptions {
2
+ waitlistEnabled?: boolean;
3
+ }
4
+ /**
5
+ * Creates email callback functions that route Better Auth's email events
6
+ * through the AscendKit backend's content service + SES delivery pipeline.
7
+ */
8
+ export declare function createEmailSender(publicKey: string, apiUrl: string, options?: EmailSenderOptions): {
9
+ sendVerificationEmail({ user, url }: {
10
+ user: {
11
+ name: string;
12
+ email: string;
13
+ };
14
+ url: string;
15
+ }): Promise<void>;
16
+ sendResetPasswordEmail({ user, url }: {
17
+ user: {
18
+ name: string;
19
+ email: string;
20
+ };
21
+ url: string;
22
+ }): Promise<void>;
23
+ sendMagicLinkEmail({ email, url }: {
24
+ email: string;
25
+ url: string;
26
+ }): Promise<void>;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=email-sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-sender.d.ts","sourceRoot":"","sources":["../../src/server/email-sender.ts"],"names":[],"mappings":"AAqBA,UAAU,kBAAkB;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;yCAItD;QAAE,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;0CAWrD;QAAE,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;uCAWzD;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;EAU1E"}
@@ -0,0 +1,58 @@
1
+ import { createHttpClient } from "../shared/http-client";
2
+ /**
3
+ * Append `verified=true` (and optionally `waitlisted=true`) to the
4
+ * callbackURL embedded in Better Auth's verification link so the provider
5
+ * can detect a successful verification on redirect and show the right
6
+ * in-modal confirmation.
7
+ */
8
+ function tagVerificationCallback(verificationUrl, waitlistEnabled) {
9
+ try {
10
+ const url = new URL(verificationUrl);
11
+ const callback = url.searchParams.get("callbackURL") || "/";
12
+ const sep = callback.includes("?") ? "&" : "?";
13
+ const tags = waitlistEnabled ? "verified=true&waitlisted=true" : "verified=true";
14
+ url.searchParams.set("callbackURL", `${callback}${sep}${tags}`);
15
+ return url.toString();
16
+ }
17
+ catch {
18
+ return verificationUrl;
19
+ }
20
+ }
21
+ /**
22
+ * Creates email callback functions that route Better Auth's email events
23
+ * through the AscendKit backend's content service + SES delivery pipeline.
24
+ */
25
+ export function createEmailSender(publicKey, apiUrl, options = {}) {
26
+ const http = createHttpClient({ publicKey, apiUrl });
27
+ return {
28
+ async sendVerificationEmail({ user, url }) {
29
+ await http.post("/api/auth/email/send", {
30
+ type: "email-verification",
31
+ to: user.email,
32
+ variables: {
33
+ userName: user.name || "",
34
+ verificationUrl: tagVerificationCallback(url, options.waitlistEnabled ?? false),
35
+ },
36
+ });
37
+ },
38
+ async sendResetPasswordEmail({ user, url }) {
39
+ await http.post("/api/auth/email/send", {
40
+ type: "password-reset",
41
+ to: user.email,
42
+ variables: {
43
+ userName: user.name || "",
44
+ resetUrl: url,
45
+ },
46
+ });
47
+ },
48
+ async sendMagicLinkEmail({ email, url }) {
49
+ await http.post("/api/auth/email/send", {
50
+ type: "magic-link",
51
+ to: email,
52
+ variables: {
53
+ magicLinkUrl: url,
54
+ },
55
+ });
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,10 @@
1
+ export { AscendKitAuth } from "./ascendkit-auth";
2
+ export { createAscendKitAuthRuntime } from "./auth-runtime";
3
+ export { createAscendKitAdapter } from "./adapter";
4
+ export { createAccessTokenHandler } from "./access-token";
5
+ export { Analytics } from "./analytics";
6
+ export { verifyWebhookSignature } from "./webhooks";
7
+ export { toNextJsHandler } from "better-auth/next-js";
8
+ export type { AscendKitAuthOptions } from "../shared/types";
9
+ export type { AscendKitAuthRuntime } from "./auth-runtime";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { AscendKitAuth } from "./ascendkit-auth";
2
+ export { createAscendKitAuthRuntime } from "./auth-runtime";
3
+ export { createAscendKitAdapter } from "./adapter";
4
+ export { createAccessTokenHandler } from "./access-token";
5
+ export { Analytics } from "./analytics";
6
+ export { verifyWebhookSignature } from "./webhooks";
7
+ export { toNextJsHandler } from "better-auth/next-js";
@@ -0,0 +1,10 @@
1
+ import type { BetterAuthPlugin } from "better-auth";
2
+ export interface AscendKitOAuthProxyPluginOptions {
3
+ publicKey: string;
4
+ apiUrl: string;
5
+ proxyProviders: string[];
6
+ waitlistRedirectPath?: string;
7
+ rejectedRedirectPath?: string;
8
+ }
9
+ export declare function ascendkitOAuthProxyPlugin(options: AscendKitOAuthProxyPluginOptions): BetterAuthPlugin;
10
+ //# sourceMappingURL=oauth-proxy-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-proxy-plugin.d.ts","sourceRoot":"","sources":["../../src/server/oauth-proxy-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAoED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,gBAAgB,CAqIlB"}
@@ -0,0 +1,156 @@
1
+ import { createAuthEndpoint, createAuthMiddleware } from "better-auth/plugins";
2
+ import { APIError } from "better-call";
3
+ import * as z from "zod";
4
+ const proxyCallbackQuerySchema = z.object({
5
+ ticket: z.string().optional(),
6
+ callback_url: z.string().optional(),
7
+ error: z.string().optional(),
8
+ });
9
+ function stripTrailingSlash(url) {
10
+ return url.replace(/\/+$/, "");
11
+ }
12
+ function resolveCurrentOrigin(ctx) {
13
+ const requestUrl = ctx.request?.url;
14
+ if (typeof requestUrl === "string") {
15
+ return new URL(requestUrl).origin;
16
+ }
17
+ if (requestUrl instanceof URL) {
18
+ return requestUrl.origin;
19
+ }
20
+ if (ctx.context.baseURL) {
21
+ return new URL(ctx.context.baseURL).origin;
22
+ }
23
+ return "http://localhost:3000";
24
+ }
25
+ function sanitizeFinalCallback(callbackURL, currentOrigin) {
26
+ if (!callbackURL) {
27
+ return "/";
28
+ }
29
+ if (callbackURL.startsWith("/") && !callbackURL.startsWith("//")) {
30
+ return callbackURL;
31
+ }
32
+ try {
33
+ const parsed = new URL(callbackURL);
34
+ if (parsed.origin !== currentOrigin) {
35
+ return "/";
36
+ }
37
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
38
+ }
39
+ catch {
40
+ return "/";
41
+ }
42
+ }
43
+ function appendQuery(path, key, value) {
44
+ const parsed = new URL(path, "http://ascendkit.local");
45
+ parsed.searchParams.set(key, value);
46
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
47
+ }
48
+ function normalizeProxyError(rawError) {
49
+ const trimmed = rawError.trim();
50
+ const lowered = trimmed.toLowerCase();
51
+ if (lowered === "account is pending approval") {
52
+ return "waitlist_pending";
53
+ }
54
+ if (lowered === "account has been rejected") {
55
+ return "waitlist_rejected";
56
+ }
57
+ return trimmed;
58
+ }
59
+ export function ascendkitOAuthProxyPlugin(options) {
60
+ const proxyProviders = new Set(options.proxyProviders);
61
+ const apiUrl = stripTrailingSlash(options.apiUrl);
62
+ return {
63
+ id: "ascendkit-oauth-proxy",
64
+ endpoints: {
65
+ ascendkitOAuthProxyCallback: createAuthEndpoint("/callback/ascendkit-proxy", {
66
+ method: "GET",
67
+ query: proxyCallbackQuerySchema,
68
+ }, async (ctx) => {
69
+ const currentOrigin = resolveCurrentOrigin(ctx);
70
+ const finalCallback = sanitizeFinalCallback(ctx.query.callback_url, currentOrigin);
71
+ if (ctx.query.error) {
72
+ const normalizedError = normalizeProxyError(ctx.query.error);
73
+ if (normalizedError === "waitlist_pending" && options.waitlistRedirectPath) {
74
+ const waitlistRedirect = sanitizeFinalCallback(options.waitlistRedirectPath, currentOrigin);
75
+ throw ctx.redirect(waitlistRedirect);
76
+ }
77
+ if (normalizedError === "waitlist_rejected" && options.rejectedRedirectPath) {
78
+ const rejectedRedirect = sanitizeFinalCallback(options.rejectedRedirectPath, currentOrigin);
79
+ throw ctx.redirect(rejectedRedirect);
80
+ }
81
+ throw ctx.redirect(appendQuery(finalCallback, "error", normalizedError));
82
+ }
83
+ if (!ctx.query.ticket) {
84
+ throw ctx.redirect(appendQuery(finalCallback, "error", "missing_ticket"));
85
+ }
86
+ let sessionToken;
87
+ try {
88
+ const res = await fetch(`${apiUrl}/api/oauth/ticket/exchange`, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "X-AscendKit-Public-Key": options.publicKey,
93
+ },
94
+ body: JSON.stringify({ ticket: ctx.query.ticket }),
95
+ });
96
+ const json = await res.json().catch(() => ({}));
97
+ if (!res.ok || !json.data?.sessionToken) {
98
+ sessionToken = undefined;
99
+ }
100
+ else {
101
+ sessionToken = json.data.sessionToken;
102
+ }
103
+ }
104
+ catch {
105
+ throw ctx.redirect(appendQuery(finalCallback, "error", "ticket_exchange_failed"));
106
+ }
107
+ if (!sessionToken) {
108
+ throw ctx.redirect(appendQuery(finalCallback, "error", "ticket_exchange_failed"));
109
+ }
110
+ const sessionCookie = ctx.context.authCookies.sessionToken;
111
+ await ctx.setSignedCookie(sessionCookie.name, sessionToken, ctx.context.secret, sessionCookie.attributes);
112
+ // Clear cache cookie so Better Auth rehydrates session from adapter.
113
+ const sessionDataCookie = ctx.context.authCookies.sessionData;
114
+ ctx.setCookie(sessionDataCookie.name, "", {
115
+ ...sessionDataCookie.attributes,
116
+ maxAge: 0,
117
+ });
118
+ throw ctx.redirect(finalCallback);
119
+ }),
120
+ },
121
+ hooks: {
122
+ before: [
123
+ {
124
+ matcher(context) {
125
+ return !!context.path?.startsWith("/sign-in/social");
126
+ },
127
+ handler: createAuthMiddleware(async (ctx) => {
128
+ const provider = ctx.body?.provider;
129
+ if (!provider || !proxyProviders.has(provider)) {
130
+ return;
131
+ }
132
+ const callbackURL = typeof ctx.body.callbackURL === "string"
133
+ ? ctx.body.callbackURL
134
+ : "/";
135
+ const currentOrigin = resolveCurrentOrigin(ctx);
136
+ const basePath = ctx.context.options.basePath || "/api/auth";
137
+ const proxyCallbackURL = `${stripTrailingSlash(currentOrigin)}${basePath}/callback/ascendkit-proxy`;
138
+ const authorizeURL = new URL(`${apiUrl}/api/oauth/authorize/${provider}`);
139
+ authorizeURL.searchParams.set("pk", options.publicKey);
140
+ authorizeURL.searchParams.set("callback_url", proxyCallbackURL);
141
+ authorizeURL.searchParams.set("app_callback_url", callbackURL);
142
+ const url = authorizeURL.toString();
143
+ // Returning a plain object from a "before" hook can cause request headers
144
+ // (including Content-Length) to leak into the response in Better Auth's
145
+ // toAuthEndpoints bridge, which truncates JSON bodies. Throw a 200 APIError
146
+ // payload instead so Better Auth client still receives { url, redirect }.
147
+ throw new APIError(200, {
148
+ url,
149
+ redirect: !ctx.body.disableRedirect,
150
+ });
151
+ }),
152
+ },
153
+ ],
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuthConfig } from "../shared/types";
2
+ /**
3
+ * Converts AscendKit auth config into Better Auth's socialProviders format.
4
+ *
5
+ * Better Auth expects: { google: { clientId, clientSecret }, github: { ... } }
6
+ * Our config returns the same shape from resolve_credentials() on the backend.
7
+ */
8
+ export declare function buildSocialProviders(config: AuthConfig): Record<string, {
9
+ clientId: string;
10
+ clientSecret: string;
11
+ }>;
12
+ //# sourceMappingURL=social-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social-providers.d.ts","sourceRoot":"","sources":["../../src/server/social-providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,GACjB,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ5D"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Converts AscendKit auth config into Better Auth's socialProviders format.
3
+ *
4
+ * Better Auth expects: { google: { clientId, clientSecret }, github: { ... } }
5
+ * Our config returns the same shape from resolve_credentials() on the backend.
6
+ */
7
+ export function buildSocialProviders(config) {
8
+ const providers = {};
9
+ for (const [name, creds] of Object.entries(config.oauth)) {
10
+ if (creds.flowType !== "proxy" && creds.clientId && creds.clientSecret) {
11
+ providers[name] = { clientId: creds.clientId, clientSecret: creds.clientSecret };
12
+ }
13
+ }
14
+ return providers;
15
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Verify an AscendKit webhook signature.
3
+ *
4
+ * AscendKit signs every webhook request with an HMAC-SHA256 signature. The
5
+ * signature header contains a timestamp and one or more versioned signatures
6
+ * in the format: `t=<unix_seconds>,v1=<hex_hmac>`.
7
+ *
8
+ * The signed content is `<timestamp>.<raw_body>`, ensuring both freshness
9
+ * and integrity.
10
+ *
11
+ * @param secret - Your webhook signing secret.
12
+ * @param signatureHeader - The value of the `X-AscendKit-Signature` header.
13
+ * @param payload - The raw request body as a string.
14
+ * @param tolerance - Maximum allowed age of the timestamp in seconds (default 300).
15
+ * @returns `true` if the signature is valid and the timestamp is within tolerance.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // app/api/webhooks/ascendkit/route.ts
20
+ * import { verifyWebhookSignature } from "@ascendkit/nextjs/server";
21
+ *
22
+ * export async function POST(req: Request) {
23
+ * const body = await req.text();
24
+ * const signature = req.headers.get("x-ascendkit-signature") ?? "";
25
+ *
26
+ * const isValid = verifyWebhookSignature(
27
+ * process.env.ASCENDKIT_WEBHOOK_SECRET!,
28
+ * signature,
29
+ * body,
30
+ * );
31
+ *
32
+ * if (!isValid) {
33
+ * return new Response("Invalid signature", { status: 401 });
34
+ * }
35
+ *
36
+ * const event = JSON.parse(body);
37
+ * // Handle the event...
38
+ * return new Response("OK", { status: 200 });
39
+ * }
40
+ * ```
41
+ */
42
+ export declare function verifyWebhookSignature(secret: string, signatureHeader: string, payload: string, tolerance?: number): boolean;
43
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/server/webhooks.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,MAAkC,GAC5C,OAAO,CA8CT"}
@@ -0,0 +1,83 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /** Default tolerance for timestamp validation (5 minutes). */
3
+ const DEFAULT_TOLERANCE_SECONDS = 300;
4
+ /**
5
+ * Verify an AscendKit webhook signature.
6
+ *
7
+ * AscendKit signs every webhook request with an HMAC-SHA256 signature. The
8
+ * signature header contains a timestamp and one or more versioned signatures
9
+ * in the format: `t=<unix_seconds>,v1=<hex_hmac>`.
10
+ *
11
+ * The signed content is `<timestamp>.<raw_body>`, ensuring both freshness
12
+ * and integrity.
13
+ *
14
+ * @param secret - Your webhook signing secret.
15
+ * @param signatureHeader - The value of the `X-AscendKit-Signature` header.
16
+ * @param payload - The raw request body as a string.
17
+ * @param tolerance - Maximum allowed age of the timestamp in seconds (default 300).
18
+ * @returns `true` if the signature is valid and the timestamp is within tolerance.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // app/api/webhooks/ascendkit/route.ts
23
+ * import { verifyWebhookSignature } from "@ascendkit/nextjs/server";
24
+ *
25
+ * export async function POST(req: Request) {
26
+ * const body = await req.text();
27
+ * const signature = req.headers.get("x-ascendkit-signature") ?? "";
28
+ *
29
+ * const isValid = verifyWebhookSignature(
30
+ * process.env.ASCENDKIT_WEBHOOK_SECRET!,
31
+ * signature,
32
+ * body,
33
+ * );
34
+ *
35
+ * if (!isValid) {
36
+ * return new Response("Invalid signature", { status: 401 });
37
+ * }
38
+ *
39
+ * const event = JSON.parse(body);
40
+ * // Handle the event...
41
+ * return new Response("OK", { status: 200 });
42
+ * }
43
+ * ```
44
+ */
45
+ export function verifyWebhookSignature(secret, signatureHeader, payload, tolerance = DEFAULT_TOLERANCE_SECONDS) {
46
+ // Parse header: t=<timestamp>,v1=<hmac>
47
+ const parts = signatureHeader.split(",");
48
+ let timestamp;
49
+ let signatureHex;
50
+ for (const part of parts) {
51
+ const [key, value] = part.split("=", 2);
52
+ if (key === "t") {
53
+ timestamp = value;
54
+ }
55
+ else if (key === "v1") {
56
+ signatureHex = value;
57
+ }
58
+ }
59
+ if (!timestamp || !signatureHex) {
60
+ return false;
61
+ }
62
+ // Validate timestamp freshness
63
+ const timestampSeconds = parseInt(timestamp, 10);
64
+ if (Number.isNaN(timestampSeconds)) {
65
+ return false;
66
+ }
67
+ const now = Math.floor(Date.now() / 1000);
68
+ if (Math.abs(now - timestampSeconds) > tolerance) {
69
+ return false;
70
+ }
71
+ // Compute expected signature: HMAC-SHA256 of "<timestamp>.<payload>"
72
+ const signedContent = `${timestamp}.${payload}`;
73
+ const expectedHmac = createHmac("sha256", secret)
74
+ .update(signedContent)
75
+ .digest("hex");
76
+ // Constant-time comparison to prevent timing attacks
77
+ const expectedBuffer = Buffer.from(expectedHmac, "hex");
78
+ const receivedBuffer = Buffer.from(signatureHex, "hex");
79
+ if (expectedBuffer.length !== receivedBuffer.length) {
80
+ return false;
81
+ }
82
+ return timingSafeEqual(expectedBuffer, receivedBuffer);
83
+ }
@@ -0,0 +1,17 @@
1
+ export interface HttpClientOptions {
2
+ publicKey: string;
3
+ apiUrl: string;
4
+ secretKey?: string;
5
+ }
6
+ export interface ConditionalGetResult<T> {
7
+ notModified: boolean;
8
+ data?: T;
9
+ etag?: string;
10
+ }
11
+ export declare function createHttpClient({ publicKey, apiUrl, secretKey }: HttpClientOptions): {
12
+ post: <T = any>(path: string, body: unknown) => Promise<T>;
13
+ get: <T = any>(path: string) => Promise<T>;
14
+ getConditional: <T = any>(path: string, etag?: string) => Promise<ConditionalGetResult<T>>;
15
+ };
16
+ export type HttpClient = ReturnType<typeof createHttpClient>;
17
+ //# sourceMappingURL=http-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../../src/shared/http-client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,gBAAgB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,iBAAiB;WAgB9D,CAAC,cAAc,MAAM,QAAQ,OAAO,KAAG,OAAO,CAAC,CAAC,CAAC;UASlD,CAAC,cAAc,MAAM,KAAG,OAAO,CAAC,CAAC,CAAC;qBAOvB,CAAC,cAAc,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;EA0BtG;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC"}