@payez/next-mvp 4.0.47 → 4.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.
@@ -128,14 +128,16 @@ function createStatsHandler(config) {
128
128
  if (adminCheck.error)
129
129
  return adminCheck.error;
130
130
  try {
131
- // Fetch from 3 sources in parallel
132
- const [usersResult, sessionCount, auditResult] = await Promise.allSettled([
133
- // 1. Users + tier breakdown via HMAC proxy (Vibe collection query)
131
+ // Fetch from 4 sources in parallel
132
+ const [usersResult, tierDistributionResult, sessionCount, auditResult] = await Promise.allSettled([
133
+ // 1. Users count via HMAC proxy (Vibe collection query)
134
134
  vibeServiceRequest('/v1/collections/vibe_app/tables/users/query', {
135
135
  method: 'POST',
136
136
  body: { page: 1, pageSize: 500, orderBy: 'created_at', orderDirection: 'desc' },
137
137
  }),
138
- // 2. Active sessions from Redis
138
+ // 2. Tier distribution from analytics endpoint (uses purchases table)
139
+ vibeServiceRequest('/v1/analytics/tier-distribution?includeTrend=false', { method: 'GET' }),
140
+ // 3. Active sessions from Redis
139
141
  (async () => {
140
142
  const redis = (0, redis_1.getRedis)();
141
143
  const sessionPrefix = getSessionPrefix();
@@ -148,12 +150,11 @@ function createStatsHandler(config) {
148
150
  } while (cursor !== '0');
149
151
  return sessionKeys.length;
150
152
  })(),
151
- // 3. Recent audit activity via HMAC proxy
153
+ // 4. Recent audit activity via HMAC proxy
152
154
  vibeServiceRequest('/v1/audit?pageSize=10&sortDir=desc', { method: 'GET' }),
153
155
  ]);
154
156
  // Parse users — deduplicate by user_id
155
157
  let totalUsers = 0;
156
- let tierBreakdown = {};
157
158
  if (usersResult.status === 'fulfilled' && usersResult.value.ok && usersResult.value.data) {
158
159
  const data = usersResult.value.data;
159
160
  const rawUsers = data.data || data.documents || data.users || [];
@@ -166,17 +167,26 @@ function createStatsHandler(config) {
166
167
  userMap.set(uid, u);
167
168
  }
168
169
  }
169
- const uniqueUsers = Array.from(userMap.values());
170
- totalUsers = uniqueUsers.length;
171
- // Build tier breakdown from deduplicated users (unless API provides one)
172
- tierBreakdown = data.tierBreakdown || data.tiers || {};
173
- if (Object.keys(tierBreakdown).length === 0) {
174
- for (const user of uniqueUsers) {
175
- const tier = user.tier || 'free';
176
- tierBreakdown[tier] = (tierBreakdown[tier] || 0) + 1;
170
+ totalUsers = userMap.size;
171
+ }
172
+ // Parse tier distribution from analytics endpoint (uses purchases table)
173
+ let tierBreakdown = {};
174
+ if (tierDistributionResult.status === 'fulfilled' && tierDistributionResult.value.ok && tierDistributionResult.value.data) {
175
+ const data = tierDistributionResult.value.data;
176
+ // Handle response shape: { distribution: [{ tierKey, userCount }, ...] }
177
+ const distribution = data.distribution || data.data || data.tiers || [];
178
+ if (Array.isArray(distribution)) {
179
+ for (const item of distribution) {
180
+ const tierKey = item.tierKey || item.tier || item.name || 'free';
181
+ const count = item.userCount || item.count || item.users || 0;
182
+ tierBreakdown[tierKey] = (tierBreakdown[tierKey] || 0) + count;
177
183
  }
178
184
  }
179
185
  }
186
+ // Fallback: if no tier data from analytics, show all as free
187
+ if (Object.keys(tierBreakdown).length === 0) {
188
+ tierBreakdown = { free: totalUsers };
189
+ }
180
190
  // Parse active sessions count
181
191
  let activeSessions = 0;
182
192
  if (sessionCount.status === 'fulfilled') {
@@ -9,6 +9,7 @@
9
9
  * @see BETTER-AUTH-MIGRATION-SPEC.md
10
10
  */
11
11
  import 'server-only';
12
+ import { type MagicLinkOptions } from 'better-auth/plugins/magic-link';
12
13
  import type { IDPClientConfig } from '../lib/idp-client-config';
13
14
  /**
14
15
  * Better Auth social provider config shape.
@@ -22,13 +23,25 @@ export interface BetterAuthSocialProvider {
22
23
  * Build Better Auth social providers from IDP config.
23
24
  */
24
25
  export declare function buildBetterAuthProviders(config: IDPClientConfig): Record<string, BetterAuthSocialProvider>;
26
+ /**
27
+ * Optional configuration for `createBetterAuthInstance`.
28
+ *
29
+ * - `magicLink`: if provided, registers Better Auth's magic-link plugin.
30
+ * The host app supplies its own `sendMagicLink` callback — typically a
31
+ * fetch to its email service (e.g. ACP's `/v1/auth/magic-link/email`).
32
+ * Omit the `magicLink` key entirely to skip the plugin; the consuming
33
+ * app will not have a magic-link flow.
34
+ */
35
+ export interface CreateBetterAuthInstanceOptions {
36
+ magicLink?: MagicLinkOptions;
37
+ }
25
38
  /**
26
39
  * Create Better Auth instance from IDP config.
27
40
  *
28
41
  * No database — runs in stateless mode with JWE cookie cache.
29
42
  * Call after getIDPClientConfig() resolves.
30
43
  */
31
- export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): import("better-auth").Auth<{
44
+ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig, opts?: CreateBetterAuthInstanceOptions): import("better-auth").Auth<{
32
45
  baseURL: string;
33
46
  secret: string;
34
47
  socialProviders: Record<string, BetterAuthSocialProvider>;
@@ -65,7 +78,102 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): im
65
78
  handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
66
79
  }[];
67
80
  };
68
- }];
81
+ }, ...{
82
+ id: "magic-link";
83
+ endpoints: {
84
+ signInMagicLink: import("better-auth").StrictEndpoint<"/sign-in/magic-link", {
85
+ method: "POST";
86
+ requireHeaders: true;
87
+ body: import("better-auth").ZodObject<{
88
+ email: import("better-auth").ZodEmail;
89
+ name: import("better-auth").ZodOptional<import("better-auth").ZodString>;
90
+ callbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
91
+ newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
92
+ errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
93
+ metadata: import("better-auth").ZodOptional<import("better-auth").ZodRecord<import("better-auth").ZodString, import("better-auth").ZodAny>>;
94
+ }, import("better-auth").$strip>;
95
+ metadata: {
96
+ openapi: {
97
+ operationId: string;
98
+ description: string;
99
+ responses: {
100
+ 200: {
101
+ description: string;
102
+ content: {
103
+ "application/json": {
104
+ schema: {
105
+ type: "object";
106
+ properties: {
107
+ status: {
108
+ type: string;
109
+ };
110
+ };
111
+ };
112
+ };
113
+ };
114
+ };
115
+ };
116
+ };
117
+ };
118
+ }, {
119
+ status: boolean;
120
+ }>;
121
+ magicLinkVerify: import("better-auth").StrictEndpoint<"/magic-link/verify", {
122
+ method: "GET";
123
+ query: import("better-auth").ZodObject<{
124
+ token: import("better-auth").ZodString;
125
+ callbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
126
+ errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
127
+ newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
128
+ }, import("better-auth").$strip>;
129
+ use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>)[];
130
+ requireHeaders: true;
131
+ metadata: {
132
+ openapi: {
133
+ operationId: string;
134
+ description: string;
135
+ responses: {
136
+ 200: {
137
+ description: string;
138
+ content: {
139
+ "application/json": {
140
+ schema: {
141
+ type: "object";
142
+ properties: {
143
+ session: {
144
+ $ref: string;
145
+ };
146
+ user: {
147
+ $ref: string;
148
+ };
149
+ };
150
+ };
151
+ };
152
+ };
153
+ };
154
+ };
155
+ };
156
+ };
157
+ }, {
158
+ token: string;
159
+ user: {
160
+ id: string;
161
+ createdAt: Date;
162
+ updatedAt: Date;
163
+ email: string;
164
+ emailVerified: boolean;
165
+ name: string;
166
+ image?: string | null | undefined;
167
+ };
168
+ }>;
169
+ };
170
+ rateLimit: {
171
+ pathMatcher(path: string): boolean;
172
+ window: number;
173
+ max: number;
174
+ }[];
175
+ options: MagicLinkOptions;
176
+ }[]];
69
177
  }>;
70
178
  /**
71
179
  * Better Auth is always enabled (NextAuth removed in 4.0).
@@ -77,6 +185,18 @@ export declare function isBetterAuthEnabled(): boolean;
77
185
  */
78
186
  declare let cachedInstance: any;
79
187
  export { cachedInstance as __betterAuthInstance };
188
+ /**
189
+ * Configure Better Auth instance options for this process.
190
+ *
191
+ * Must be called before the first auth request — before
192
+ * `getBetterAuthInstance()` caches an instance. Typically called once at
193
+ * app startup, e.g. from Next.js `instrumentation.ts` or an equivalent
194
+ * server bootstrap hook.
195
+ *
196
+ * Throws if called after the instance has already been resolved: options
197
+ * cannot be applied retroactively.
198
+ */
199
+ export declare function configureBetterAuth(opts: CreateBetterAuthInstanceOptions): void;
80
200
  export declare function getBetterAuthInstance(): Promise<any>;
81
201
  /**
82
202
  * Get flag-gated auth handler for Next.js route.
@@ -47,6 +47,7 @@ exports.__betterAuthInstance = void 0;
47
47
  exports.buildBetterAuthProviders = buildBetterAuthProviders;
48
48
  exports.createBetterAuthInstance = createBetterAuthInstance;
49
49
  exports.isBetterAuthEnabled = isBetterAuthEnabled;
50
+ exports.configureBetterAuth = configureBetterAuth;
50
51
  exports.getBetterAuthInstance = getBetterAuthInstance;
51
52
  exports.getBetterAuthHandler = getBetterAuthHandler;
52
53
  exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
@@ -55,6 +56,7 @@ require("server-only");
55
56
  const better_auth_1 = require("better-auth");
56
57
  const next_js_1 = require("better-auth/next-js");
57
58
  const next_js_2 = require("better-auth/next-js");
59
+ const magic_link_1 = require("better-auth/plugins/magic-link");
58
60
  const idp_client_config_1 = require("../lib/idp-client-config");
59
61
  const app_slug_1 = require("../lib/app-slug");
60
62
  const redis_1 = require("../lib/redis");
@@ -81,7 +83,7 @@ function buildBetterAuthProviders(config) {
81
83
  * No database — runs in stateless mode with JWE cookie cache.
82
84
  * Call after getIDPClientConfig() resolves.
83
85
  */
84
- function createBetterAuthInstance(idpConfig) {
86
+ function createBetterAuthInstance(idpConfig, opts = {}) {
85
87
  const appSlug = idpConfig.clientSlug || (0, app_slug_1.getAppSlug)();
86
88
  // Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
87
89
  // Must include /api/auth since that's where the catch-all route is mounted
@@ -149,6 +151,7 @@ function createBetterAuthInstance(idpConfig) {
149
151
  },
150
152
  plugins: [
151
153
  (0, next_js_1.nextCookies)(),
154
+ ...(opts.magicLink ? [(0, magic_link_1.magicLink)(opts.magicLink)] : []),
152
155
  ],
153
156
  });
154
157
  }
@@ -167,12 +170,31 @@ let cachedInstance = null;
167
170
  exports.__betterAuthInstance = cachedInstance;
168
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
172
  let initPromise = null;
173
+ let configuredOpts = {};
174
+ /**
175
+ * Configure Better Auth instance options for this process.
176
+ *
177
+ * Must be called before the first auth request — before
178
+ * `getBetterAuthInstance()` caches an instance. Typically called once at
179
+ * app startup, e.g. from Next.js `instrumentation.ts` or an equivalent
180
+ * server bootstrap hook.
181
+ *
182
+ * Throws if called after the instance has already been resolved: options
183
+ * cannot be applied retroactively.
184
+ */
185
+ function configureBetterAuth(opts) {
186
+ if (cachedInstance) {
187
+ throw new Error('[BETTER_AUTH] configureBetterAuth() must run before the instance is first resolved. ' +
188
+ 'Call it in Next.js instrumentation.ts or an equivalent startup hook.');
189
+ }
190
+ configuredOpts = opts;
191
+ }
170
192
  async function getBetterAuthInstance() {
171
193
  if (cachedInstance)
172
194
  return cachedInstance;
173
195
  if (!initPromise) {
174
196
  initPromise = (0, idp_client_config_1.getIDPClientConfig)(true).then(config => {
175
- const instance = createBetterAuthInstance(config);
197
+ const instance = createBetterAuthInstance(config, configuredOpts);
176
198
  exports.__betterAuthInstance = cachedInstance = instance;
177
199
  console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
178
200
  return instance;