@cogito.ai/cli 0.4.3 → 0.4.4

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 (45) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +8 -18
  3. package/dist/templates/web-nextjs/apps/docs/.source/dynamic.ts +5 -11
  4. package/dist/templates/web-nextjs/apps/docs/.source/server.ts +17 -37
  5. package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
  6. package/dist/templates/web-nextjs/apps/web/messages/en.json +70 -0
  7. package/dist/templates/web-nextjs/apps/web/messages/zh.json +71 -1
  8. package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
  9. package/dist/templates/web-nextjs/apps/web/package.json +4 -0
  10. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +4 -10
  11. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
  12. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
  13. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
  14. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +48 -1
  15. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
  16. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +1 -1
  17. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
  18. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
  19. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
  20. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
  21. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
  22. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
  23. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
  24. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
  25. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
  26. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
  27. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
  28. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
  29. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
  30. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
  31. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
  32. package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
  33. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
  34. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
  35. package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
  36. package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
  37. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
  38. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
  39. package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
  40. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
  41. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
  42. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
  43. package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
  44. package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
  45. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ 'use client'
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4
+ import type { Payment, SubscriptionPlan, UserSubscription } from './types'
5
+
6
+ function createSupabaseClient() {
7
+ return import('@supabase/ssr').then(({ createBrowserClient }) =>
8
+ createBrowserClient(
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
10
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11
+ ),
12
+ )
13
+ }
14
+
15
+ export function useSubscriptionPlans() {
16
+ return useQuery({
17
+ queryKey: ['subscription-plans'],
18
+ queryFn: async (): Promise<SubscriptionPlan[]> => {
19
+ const supabase = await createSupabaseClient()
20
+ const { data, error } = await supabase
21
+ .from('subscription_plans')
22
+ .select('*')
23
+ .eq('is_active', true)
24
+ .order('display_order', { ascending: true })
25
+
26
+ if (error) throw error
27
+ return (data as SubscriptionPlan[]) ?? []
28
+ },
29
+ })
30
+ }
31
+
32
+ export function useSubscriptionPlan(planCode: string) {
33
+ return useQuery({
34
+ queryKey: ['subscription-plan', planCode],
35
+ queryFn: async (): Promise<SubscriptionPlan | null> => {
36
+ const supabase = await createSupabaseClient()
37
+ const { data, error } = await supabase
38
+ .from('subscription_plans')
39
+ .select('*')
40
+ .eq('plan_code', planCode)
41
+ .single()
42
+
43
+ if (error && error.code !== 'PGRST116') throw error
44
+ return (data as SubscriptionPlan) ?? null
45
+ },
46
+ })
47
+ }
48
+
49
+ export function useUserSubscription(userId: string) {
50
+ return useQuery({
51
+ queryKey: ['user-subscription', userId],
52
+ queryFn: async (): Promise<UserSubscription | null> => {
53
+ const supabase = await createSupabaseClient()
54
+ const { data, error } = await supabase
55
+ .from('user_subscriptions')
56
+ .select('*')
57
+ .eq('user_id', userId)
58
+ .in('status', ['trial', 'active'])
59
+ .order('created_at', { ascending: false })
60
+ .limit(1)
61
+ .single()
62
+
63
+ if (error && error.code !== 'PGRST116') throw error
64
+ return (data as UserSubscription) ?? null
65
+ },
66
+ staleTime: 2 * 60 * 1000, // 2 minutes
67
+ enabled: !!userId,
68
+ })
69
+ }
70
+
71
+ export function useUserPayments(userId: string) {
72
+ return useQuery({
73
+ queryKey: ['user-payments', userId],
74
+ queryFn: async (): Promise<Payment[]> => {
75
+ const supabase = await createSupabaseClient()
76
+ const { data, error } = await supabase
77
+ .from('payments')
78
+ .select('*')
79
+ .eq('user_id', userId)
80
+ .order('created_at', { ascending: false })
81
+
82
+ if (error) throw error
83
+ return (data as Payment[]) ?? []
84
+ },
85
+ enabled: !!userId,
86
+ })
87
+ }
88
+
89
+ interface PaymentStatusResult {
90
+ status: string
91
+ }
92
+
93
+ export function usePaymentStatus(
94
+ orderNo: string,
95
+ paymentMethod: string,
96
+ options?: { enabled?: boolean },
97
+ ) {
98
+ return useQuery({
99
+ queryKey: ['payment-status', orderNo],
100
+ queryFn: async (): Promise<PaymentStatusResult> => {
101
+ const endpoint =
102
+ paymentMethod === 'alipay' ? '/api/payments/alipay/query' : '/api/payments/wechat/query'
103
+ const res = await fetch(endpoint, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ outTradeNo: orderNo }),
107
+ })
108
+ if (!res.ok) throw new Error('Failed to query payment status')
109
+ return res.json() as Promise<PaymentStatusResult>
110
+ },
111
+ refetchInterval: 3000,
112
+ enabled: options?.enabled ?? !!orderNo,
113
+ })
114
+ }
115
+
116
+ interface CreatePaymentParams {
117
+ userId: string
118
+ planId: number
119
+ amount: number
120
+ paymentMethod: string
121
+ billingCycle: string
122
+ }
123
+
124
+ export function useCreatePayment() {
125
+ const queryClient = useQueryClient()
126
+
127
+ return useMutation({
128
+ mutationFn: async (params: CreatePaymentParams) => {
129
+ const res = await fetch('/api/payments/create', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify(params),
133
+ })
134
+ if (!res.ok) throw new Error('Failed to create payment')
135
+ return res.json()
136
+ },
137
+ onSuccess: () => {
138
+ queryClient.invalidateQueries({ queryKey: ['user-payments'] })
139
+ },
140
+ })
141
+ }
@@ -0,0 +1,27 @@
1
+ import crypto from 'crypto'
2
+
3
+ /**
4
+ * Generates a unique order number in the format WN{timestamp}{4-digit-random}
5
+ * WN = WebNextjs prefix to avoid conflicts with other projects
6
+ */
7
+ export function generateOrderNumber(): string {
8
+ const timestamp = Date.now()
9
+ const random = Math.floor(Math.random() * 10000)
10
+ .toString()
11
+ .padStart(4, '0')
12
+ return `WN${timestamp}${random}`
13
+ }
14
+
15
+ /**
16
+ * Formats a price in cents to a human-readable currency string (CNY)
17
+ */
18
+ export function formatPriceInCents(cents: number): string {
19
+ return `¥${(cents / 100).toFixed(2)}`
20
+ }
21
+
22
+ /**
23
+ * Checks if a subscription is currently active
24
+ */
25
+ export function isSubscriptionActive(status: string): boolean {
26
+ return status === 'trial' || status === 'active'
27
+ }
@@ -0,0 +1,56 @@
1
+ import { getServerClient } from '@/infra/db/client'
2
+ import type { Payment, CreatePaymentParams } from './types'
3
+
4
+ export async function createPaymentRecord(params: CreatePaymentParams): Promise<Payment> {
5
+ const supabase = await getServerClient()
6
+ const { data, error } = await supabase
7
+ .from('payments')
8
+ .insert({
9
+ user_id: params.user_id,
10
+ subscription_id: params.subscription_id ?? null,
11
+ order_no: params.order_no,
12
+ amount: params.amount,
13
+ currency: params.currency ?? 'CNY',
14
+ payment_method: params.payment_method,
15
+ payment_channel: params.payment_channel ?? null,
16
+ status: 'pending',
17
+ metadata: params.metadata ?? {},
18
+ })
19
+ .select()
20
+ .single()
21
+
22
+ if (error) throw error
23
+ return data as Payment
24
+ }
25
+
26
+ export async function getPaymentByOrderNo(orderNo: string): Promise<Payment | null> {
27
+ const supabase = await getServerClient()
28
+ const { data, error } = await supabase
29
+ .from('payments')
30
+ .select('*')
31
+ .eq('order_no', orderNo)
32
+ .single()
33
+
34
+ if (error && error.code !== 'PGRST116') throw error
35
+ return (data as Payment) ?? null
36
+ }
37
+
38
+ export async function getPaymentById(id: number): Promise<Payment | null> {
39
+ const supabase = await getServerClient()
40
+ const { data, error } = await supabase.from('payments').select('*').eq('id', id).single()
41
+
42
+ if (error && error.code !== 'PGRST116') throw error
43
+ return (data as Payment) ?? null
44
+ }
45
+
46
+ export async function getUserPayments(userId: string): Promise<Payment[]> {
47
+ const supabase = await getServerClient()
48
+ const { data, error } = await supabase
49
+ .from('payments')
50
+ .select('*')
51
+ .eq('user_id', userId)
52
+ .order('created_at', { ascending: false })
53
+
54
+ if (error) throw error
55
+ return (data as Payment[]) ?? []
56
+ }
@@ -0,0 +1,55 @@
1
+ import { getBrowserClient, getServerClient } from '@/infra/db/client'
2
+ import type { SubscriptionPlan, UserSubscription } from './types'
3
+
4
+ export async function getActiveSubscriptionPlans(): Promise<SubscriptionPlan[]> {
5
+ const supabase = await getServerClient()
6
+ const { data, error } = await supabase
7
+ .from('subscription_plans')
8
+ .select('*')
9
+ .eq('is_active', true)
10
+ .order('display_order', { ascending: true })
11
+
12
+ if (error) throw error
13
+ return (data as SubscriptionPlan[]) ?? []
14
+ }
15
+
16
+ export async function getSubscriptionPlanByCode(
17
+ planCode: string,
18
+ ): Promise<SubscriptionPlan | null> {
19
+ const supabase = await getServerClient()
20
+ const { data, error } = await supabase
21
+ .from('subscription_plans')
22
+ .select('*')
23
+ .eq('plan_code', planCode)
24
+ .single()
25
+
26
+ if (error && error.code !== 'PGRST116') throw error
27
+ return (data as SubscriptionPlan) ?? null
28
+ }
29
+
30
+ export async function getUserActiveSubscription(userId: string): Promise<UserSubscription | null> {
31
+ const supabase = await getServerClient()
32
+ const { data, error } = await supabase
33
+ .from('user_subscriptions')
34
+ .select('*')
35
+ .eq('user_id', userId)
36
+ .in('status', ['trial', 'active'])
37
+ .order('created_at', { ascending: false })
38
+ .limit(1)
39
+ .single()
40
+
41
+ if (error && error.code !== 'PGRST116') throw error
42
+ return (data as UserSubscription) ?? null
43
+ }
44
+
45
+ export async function getUserSubscriptionHistory(userId: string): Promise<UserSubscription[]> {
46
+ const supabase = await getServerClient()
47
+ const { data, error } = await supabase
48
+ .from('user_subscriptions')
49
+ .select('*')
50
+ .eq('user_id', userId)
51
+ .order('created_at', { ascending: false })
52
+
53
+ if (error) throw error
54
+ return (data as UserSubscription[]) ?? []
55
+ }
@@ -0,0 +1,73 @@
1
+ export type PlanCode = 'free' | 'pro' | 'enterprise'
2
+
3
+ export type BillingCycle = 'monthly' | 'yearly'
4
+
5
+ export type PaymentMethod = 'alipay' | 'wechat' | 'stripe'
6
+
7
+ export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled'
8
+
9
+ export type SubscriptionStatus = 'trial' | 'active' | 'expired' | 'cancelled' | 'pending'
10
+
11
+ export type PaymentChannel = 'alipay_page' | 'alipay_wap' | 'wechat_native' | 'wechat_h5'
12
+
13
+ export interface SubscriptionPlan {
14
+ id: number
15
+ plan_code: PlanCode
16
+ plan_name: string
17
+ description: string | null
18
+ price_monthly: number
19
+ price_yearly: number
20
+ features: Record<string, unknown>
21
+ display_order: number
22
+ is_active: boolean
23
+ is_featured: boolean
24
+ created_at: string
25
+ updated_at: string
26
+ }
27
+
28
+ export interface UserSubscription {
29
+ id: number
30
+ user_id: string
31
+ plan_id: number
32
+ status: SubscriptionStatus
33
+ billing_cycle: BillingCycle
34
+ current_period_start: string | null
35
+ current_period_end: string | null
36
+ cancelled_at: string | null
37
+ cancel_at_period_end: boolean
38
+ last_payment_id: number | null
39
+ next_billing_date: string | null
40
+ metadata: Record<string, unknown> | null
41
+ created_at: string
42
+ updated_at: string
43
+ }
44
+
45
+ export interface Payment {
46
+ id: number
47
+ user_id: string
48
+ subscription_id: number | null
49
+ order_no: string
50
+ amount: number
51
+ currency: string
52
+ payment_method: PaymentMethod
53
+ payment_channel: PaymentChannel | null
54
+ status: PaymentStatus
55
+ provider_trade_no: string | null
56
+ provider_response: Record<string, unknown> | null
57
+ paid_at: string | null
58
+ refunded_at: string | null
59
+ metadata: Record<string, unknown> | null
60
+ created_at: string
61
+ updated_at: string
62
+ }
63
+
64
+ export interface CreatePaymentParams {
65
+ user_id: string
66
+ subscription_id?: number
67
+ order_no: string
68
+ amount: number
69
+ currency?: string
70
+ payment_method: PaymentMethod
71
+ payment_channel?: PaymentChannel
72
+ metadata?: Record<string, unknown>
73
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ interface WechatPayResult {
4
+ type: 'qrcode' | 'redirect'
5
+ codeUrl?: string
6
+ h5Url?: string
7
+ }
8
+
9
+ export async function initiateWechatPayment(
10
+ paymentId: number,
11
+ userId: string,
12
+ ): Promise<WechatPayResult> {
13
+ const res = await fetch('/api/payments/wechat/create', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ paymentId, userId }),
17
+ })
18
+
19
+ if (!res.ok) {
20
+ throw new Error('Failed to initiate WeChat payment')
21
+ }
22
+
23
+ const data = (await res.json()) as WechatPayResult
24
+
25
+ if (data.type === 'qrcode' && data.codeUrl) {
26
+ return { type: 'qrcode', codeUrl: data.codeUrl }
27
+ } else if (data.type === 'redirect' && data.h5Url) {
28
+ window.location.href = data.h5Url
29
+ return { type: 'redirect', h5Url: data.h5Url }
30
+ }
31
+
32
+ throw new Error('Unexpected WeChat payment response')
33
+ }
@@ -0,0 +1,147 @@
1
+ import { wxpayRequest } from '@/lib/wechat-pay/client'
2
+ import type { WechatPayConfig } from '@/lib/wechat-pay/types'
3
+ import { decryptResource, verifySignature } from '@/lib/wechat-pay/crypto'
4
+
5
+ export function getWechatPayConfig(): WechatPayConfig {
6
+ const appId = process.env.WECHAT_PAY_APP_ID
7
+ const mchId = process.env.WECHAT_PAY_MCH_ID
8
+ const privateKey = process.env.WECHAT_PAY_PRIVATE_KEY
9
+ const serialNo = process.env.WECHAT_PAY_SERIAL_NO
10
+ const apiV3Key = process.env.WECHAT_PAY_API_V3_KEY
11
+ const platformPublicKey = process.env.WECHAT_PAY_PLATFORM_PUBLIC_KEY
12
+ const baseUrl = process.env.WECHAT_PAY_BASE_URL || 'https://api.mch.weixin.qq.com'
13
+ const notifyUrl = process.env.WECHAT_PAY_NOTIFY_URL
14
+
15
+ if (
16
+ !appId ||
17
+ !mchId ||
18
+ !privateKey ||
19
+ !serialNo ||
20
+ !apiV3Key ||
21
+ !platformPublicKey ||
22
+ !notifyUrl
23
+ ) {
24
+ throw new Error(
25
+ 'Missing WeChat Pay credentials. Ensure all WECHAT_PAY_* environment variables are set.',
26
+ )
27
+ }
28
+
29
+ return {
30
+ appId,
31
+ mchId,
32
+ privateKey,
33
+ serialNo,
34
+ apiV3Key,
35
+ platformPublicKey,
36
+ baseUrl,
37
+ notifyUrl,
38
+ }
39
+ }
40
+
41
+ interface CreateWechatPayParams {
42
+ description: string
43
+ outTradeNo: string
44
+ notifyUrl: string
45
+ amount: { total: number; currency?: string }
46
+ sceneInfo?: object
47
+ }
48
+
49
+ export async function createWechatNativePay(params: CreateWechatPayParams) {
50
+ const config = getWechatPayConfig()
51
+ const body = {
52
+ appid: config.appId,
53
+ mchid: config.mchId,
54
+ description: params.description,
55
+ out_trade_no: params.outTradeNo,
56
+ notify_url: params.notifyUrl,
57
+ amount: params.amount,
58
+ }
59
+
60
+ return wxpayRequest<{ code_url: string }>({
61
+ config,
62
+ method: 'post',
63
+ url: '/v3/pay/transactions/native',
64
+ body,
65
+ })
66
+ }
67
+
68
+ export async function createWechatH5Pay(params: CreateWechatPayParams) {
69
+ const config = getWechatPayConfig()
70
+ const body = {
71
+ appid: config.appId,
72
+ mchid: config.mchId,
73
+ description: params.description,
74
+ out_trade_no: params.outTradeNo,
75
+ notify_url: params.notifyUrl,
76
+ amount: params.amount,
77
+ scene_info: params.sceneInfo,
78
+ }
79
+
80
+ return wxpayRequest<{ h5_url: string }>({
81
+ config,
82
+ method: 'post',
83
+ url: '/v3/pay/transactions/h5',
84
+ body,
85
+ })
86
+ }
87
+
88
+ export async function queryWechatOrder(outTradeNo: string) {
89
+ const config = getWechatPayConfig()
90
+ return wxpayRequest<{ trade_state: string }>({
91
+ config,
92
+ method: 'get',
93
+ url: `/v3/pay/transactions/out-trade-no/${outTradeNo}`,
94
+ })
95
+ }
96
+
97
+ export interface VerifyAndDecryptNotifyResult {
98
+ outTradeNo: string
99
+ tradeState: string
100
+ transactionId: string
101
+ amount: number
102
+ currency: string
103
+ appId: string
104
+ mchId: string
105
+ }
106
+
107
+ export async function verifyAndDecryptNotify(
108
+ headers: Record<string, string | undefined>,
109
+ rawBody: string,
110
+ ): Promise<VerifyAndDecryptNotifyResult> {
111
+ const config = getWechatPayConfig()
112
+ const timestamp = headers['wechatpay-timestamp'] || headers['Wechatpay-Timestamp'] || ''
113
+ const nonce = headers['wechatpay-nonce'] || headers['Wechatpay-Nonce'] || ''
114
+ const signature = headers['wechatpay-signature'] || headers['Wechatpay-Signature'] || ''
115
+
116
+ const isValid = verifySignature({
117
+ timestamp,
118
+ nonce,
119
+ body: rawBody,
120
+ signature,
121
+ platformPublicKey: config.platformPublicKey,
122
+ })
123
+
124
+ if (!isValid) {
125
+ throw new Error('Invalid WeChat Pay signature')
126
+ }
127
+
128
+ const bodyData = JSON.parse(rawBody)
129
+ const resource = bodyData.resource
130
+ const decrypted = decryptResource({
131
+ ciphertext: resource.ciphertext,
132
+ associatedData: resource.associated_data,
133
+ nonce: resource.nonce,
134
+ apiV3Key: config.apiV3Key,
135
+ })
136
+
137
+ const transaction = JSON.parse(decrypted)
138
+ return {
139
+ outTradeNo: transaction.out_trade_no,
140
+ tradeState: transaction.trade_state,
141
+ transactionId: transaction.transaction_id,
142
+ amount: transaction.amount?.total,
143
+ currency: transaction.amount?.currency,
144
+ appId: transaction.appid,
145
+ mchId: transaction.mchid,
146
+ }
147
+ }
@@ -0,0 +1,23 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+
3
+ /**
4
+ * Creates a Supabase admin client using the service role key.
5
+ *
6
+ * ⚠️ WARNING: This client bypasses Row Level Security (RLS).
7
+ * Use ONLY in server-side contexts (Route Handlers, Server Actions).
8
+ * NEVER expose this client to the browser.
9
+ */
10
+ export function createAdminClient() {
11
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL
12
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY
13
+
14
+ if (!url || !key) {
15
+ throw new Error(
16
+ 'Missing Supabase admin credentials. Ensure NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set.',
17
+ )
18
+ }
19
+
20
+ return createClient(url, key, {
21
+ auth: { autoRefreshToken: false, persistSession: false },
22
+ })
23
+ }
@@ -0,0 +1,66 @@
1
+ import { buildAuthorizationHeader, generateNonce, signRequest } from './crypto'
2
+ import type { WechatPayConfig } from './types'
3
+
4
+ interface WxpayRequestParams<T = unknown> {
5
+ config: WechatPayConfig
6
+ method: string
7
+ url: string
8
+ body?: object
9
+ }
10
+
11
+ export async function wxpayRequest<T>(params: WxpayRequestParams): Promise<T> {
12
+ const { config, method, url, body } = params
13
+ const timestamp = Math.floor(Date.now() / 1000).toString()
14
+ const nonce = generateNonce()
15
+ const bodyStr = body ? JSON.stringify(body) : ''
16
+
17
+ const signature = signRequest({
18
+ method,
19
+ url,
20
+ timestamp,
21
+ nonce,
22
+ body: bodyStr,
23
+ privateKey: config.privateKey,
24
+ })
25
+
26
+ const authorization = buildAuthorizationHeader({
27
+ mchId: config.mchId,
28
+ serialNo: config.serialNo,
29
+ timestamp,
30
+ nonce,
31
+ signature,
32
+ })
33
+
34
+ const headers: Record<string, string> = {
35
+ Accept: 'application/json',
36
+ Authorization: authorization,
37
+ 'Content-Type': 'application/json',
38
+ 'User-Agent': 'Mozilla/5.0 (compatible; WebNextjs/1.0)',
39
+ }
40
+
41
+ const res = await fetch(`${config.baseUrl}${url}`, {
42
+ method: method.toUpperCase(),
43
+ headers,
44
+ body: bodyStr || null,
45
+ })
46
+
47
+ const data = (await res.json()) as T
48
+
49
+ // Check for WeChat Pay API errors
50
+ if (!res.ok) {
51
+ const errorData = data as Record<string, unknown>
52
+ const errorMessage =
53
+ typeof errorData.message === 'string'
54
+ ? errorData.message
55
+ : `WeChat Pay API error: ${res.status}`
56
+ const error = new Error(errorMessage)
57
+ ;(error as Error & { code?: string; status?: number; raw?: unknown }).code = String(
58
+ errorData.code || '',
59
+ )
60
+ ;(error as Error & { code?: string; status?: number; raw?: unknown }).status = res.status
61
+ ;(error as Error & { code?: string; status?: number; raw?: unknown }).raw = errorData
62
+ throw error
63
+ }
64
+
65
+ return data
66
+ }