@youidian/pay-sdk 2.0.1 → 3.0.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.
package/dist/login.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hosted-modal.ts","../src/login.ts"],"sourcesContent":["export interface HostedFrameMessage {\n\ttype: string\n\theight?: number\n}\n\ntype HostedFrameOptions<TData extends HostedFrameMessage> = {\n\tallowedOrigin?: string\n\theight?: string\n\tonCloseButton?: () => void\n\tonMessage: (data: TData, container: HTMLDivElement) => void\n\twidth?: string\n}\n\nconst SDK_SUPPORTED_LOCALES = [\n\t\"en\",\n\t\"zh-CN\",\n\t\"zh-Hant\",\n\t\"fr\",\n\t\"de\",\n\t\"ja\",\n\t\"es\",\n\t\"ko\",\n\t\"nl\",\n\t\"it\",\n\t\"pt\",\n] as const\n\nconst SDK_DEFAULT_LOCALE = \"zh-CN\"\n\nconst SDK_LOCALE_ALIASES: Record<string, string> = {\n\ten: \"en\",\n\t\"en-us\": \"en\",\n\t\"en-gb\": \"en\",\n\tzh: \"zh-CN\",\n\t\"zh-cn\": \"zh-CN\",\n\t\"zh-tw\": \"zh-Hant\",\n\t\"zh-hk\": \"zh-Hant\",\n\t\"zh-hant\": \"zh-Hant\",\n\t\"zh-hans\": \"zh-CN\",\n\tfr: \"fr\",\n\tde: \"de\",\n\tja: \"ja\",\n\tes: \"es\",\n\tko: \"ko\",\n\tnl: \"nl\",\n\tit: \"it\",\n\tpt: \"pt\",\n}\n\nfunction matchSupportedBaseLocale(locale: string): string | undefined {\n\tconst base = locale.toLowerCase().split(\"-\")[0]\n\treturn SDK_SUPPORTED_LOCALES.find((item) => item.toLowerCase() === base)\n}\n\nexport function normalizeSdkLocale(locale?: string): string | undefined {\n\tif (!locale) return\n\n\tconst sanitized = locale.trim().replace(/_/g, \"-\")\n\tif (!sanitized) return\n\n\tconst lower = sanitized.toLowerCase()\n\tif (SDK_LOCALE_ALIASES[lower]) {\n\t\treturn SDK_LOCALE_ALIASES[lower]\n\t}\n\n\tlet canonical: string | undefined\n\ttry {\n\t\tcanonical = Intl.getCanonicalLocales(sanitized)[0]\n\t} catch {\n\t\tcanonical = undefined\n\t}\n\n\tconst candidates = [canonical, sanitized].filter(Boolean) as string[]\n\n\tfor (const candidate of candidates) {\n\t\tconst candidateLower = candidate.toLowerCase()\n\n\t\tif (SDK_LOCALE_ALIASES[candidateLower]) {\n\t\t\treturn SDK_LOCALE_ALIASES[candidateLower]\n\t\t}\n\t\tif ((SDK_SUPPORTED_LOCALES as readonly string[]).includes(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\n\t\tconst baseMatch = matchSupportedBaseLocale(candidate)\n\t\tif (baseMatch) {\n\t\t\treturn baseMatch\n\t\t}\n\t}\n}\n\nexport function getBrowserLocale(): string | undefined {\n\tif (typeof navigator === \"undefined\") return\n\n\tfor (const candidate of navigator.languages || []) {\n\t\tconst normalized = normalizeSdkLocale(candidate)\n\t\tif (normalized) return normalized\n\t}\n\n\treturn normalizeSdkLocale(navigator.language)\n}\n\nexport function getAutoResolvedLocale(locale?: string): string {\n\treturn normalizeSdkLocale(locale) || getBrowserLocale() || SDK_DEFAULT_LOCALE\n}\n\nexport function applyLocaleToUrl(urlValue: string, locale?: string): string {\n\tif (!locale) return urlValue\n\n\ttry {\n\t\tconst url = new URL(urlValue)\n\t\tconst localePrefix = `/${locale}`\n\t\tif (\n\t\t\t!url.pathname.startsWith(`${localePrefix}/`) &&\n\t\t\turl.pathname !== localePrefix\n\t\t) {\n\t\t\turl.pathname = `${localePrefix}${url.pathname}`\n\t\t}\n\t\treturn url.toString()\n\t} catch (_error) {\n\t\tconst localePrefix = `/${locale}`\n\t\tif (!urlValue.startsWith(`${localePrefix}/`) && urlValue !== localePrefix) {\n\t\t\treturn `${localePrefix}${urlValue.startsWith(\"/\") ? \"\" : \"/\"}${urlValue}`\n\t\t}\n\t\treturn urlValue\n\t}\n}\n\nexport class HostedFrameModal<TData extends HostedFrameMessage> {\n\tprotected iframe: HTMLIFrameElement | null = null\n\tprotected modal: HTMLDivElement | null = null\n\tprotected messageHandler: ((event: MessageEvent) => void) | null = null\n\n\tprotected openHostedFrame(\n\t\turl: string,\n\t\toptions: HostedFrameOptions<TData>,\n\t): void {\n\t\tif (typeof document === \"undefined\") return\n\t\tif (this.modal) return\n\n\t\tthis.modal = document.createElement(\"div\")\n\t\tObject.assign(this.modal.style, {\n\t\t\tposition: \"fixed\",\n\t\t\ttop: \"0\",\n\t\t\tleft: \"0\",\n\t\t\twidth: \"100%\",\n\t\t\theight: \"100%\",\n\t\t\tbackgroundColor: \"rgba(15,23,42,0.52)\",\n\t\t\tdisplay: \"flex\",\n\t\t\talignItems: \"center\",\n\t\t\tjustifyContent: \"center\",\n\t\t\tzIndex: \"9999\",\n\t\t\ttransition: \"opacity 0.3s ease\",\n\t\t\tbackdropFilter: \"blur(14px)\",\n\t\t\tpadding: \"16px\",\n\t\t})\n\n\t\tconst container = document.createElement(\"div\")\n\t\tObject.assign(container.style, {\n\t\t\twidth: options.width || \"450px\",\n\t\t\theight: options.height || \"min(600px, 90vh)\",\n\t\t\tbackgroundColor: \"transparent\",\n\t\t\tborderRadius: \"28px\",\n\t\t\toverflow: \"visible\",\n\t\t\tposition: \"relative\",\n\t\t\tboxShadow: \"none\",\n\t\t\tmaxWidth: \"calc(100vw - 32px)\",\n\t\t\tmaxHeight: \"calc(100vh - 32px)\",\n\t\t\ttransition: \"height 180ms ease\",\n\t\t\twillChange: \"height\",\n\t\t})\n\n\t\tconst closeBtn = document.createElement(\"button\")\n\t\tcloseBtn.innerHTML = \"×\"\n\t\tObject.assign(closeBtn.style, {\n\t\t\tposition: \"absolute\",\n\t\t\tright: \"-14px\",\n\t\t\ttop: \"-14px\",\n\t\t\tfontSize: \"20px\",\n\t\t\twidth: \"36px\",\n\t\t\theight: \"36px\",\n\t\t\tborderRadius: \"9999px\",\n\t\t\tborder: \"1px solid rgba(255,255,255,0.5)\",\n\t\t\tbackground: \"rgba(255,255,255,0.9)\",\n\t\t\tcursor: \"pointer\",\n\t\t\tcolor: \"#475569\",\n\t\t\tzIndex: \"2\",\n\t\t\tboxShadow: \"0 10px 30px rgba(15,23,42,0.16)\",\n\t\t})\n\t\tcloseBtn.onclick = () => {\n\t\t\tthis.close()\n\t\t\toptions.onCloseButton?.()\n\t\t}\n\n\t\tthis.iframe = document.createElement(\"iframe\")\n\t\tthis.iframe.src = url\n\t\tObject.assign(this.iframe.style, {\n\t\t\twidth: \"100%\",\n\t\t\theight: \"100%\",\n\t\t\tborder: \"none\",\n\t\t\tborderRadius: \"28px\",\n\t\t\tbackground: \"transparent\",\n\t\t\tdisplay: \"block\",\n\t\t})\n\n\t\tcontainer.appendChild(closeBtn)\n\t\tcontainer.appendChild(this.iframe)\n\t\tthis.modal.appendChild(container)\n\t\tdocument.body.appendChild(this.modal)\n\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tif (options.allowedOrigin && options.allowedOrigin !== \"*\") {\n\t\t\t\tif (event.origin !== options.allowedOrigin) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst data = event.data as TData\n\t\t\tif (!data || typeof data !== \"object\" || !data.type) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\toptions.onMessage(data, container)\n\t\t}\n\n\t\twindow.addEventListener(\"message\", this.messageHandler)\n\t}\n\n\tclose() {\n\t\tif (typeof window === \"undefined\") return\n\n\t\tif (this.messageHandler) {\n\t\t\twindow.removeEventListener(\"message\", this.messageHandler)\n\t\t\tthis.messageHandler = null\n\t\t}\n\n\t\tif (this.modal?.parentNode) {\n\t\t\tthis.modal.parentNode.removeChild(this.modal)\n\t\t}\n\n\t\tthis.modal = null\n\t\tthis.iframe = null\n\t}\n}\n","import {\n\tapplyLocaleToUrl,\n\tgetAutoResolvedLocale,\n\ttype HostedFrameMessage,\n} from \"./hosted-modal\"\n\nexport type LoginEventType =\n\t| \"LOGIN_SUCCESS\"\n\t| \"LOGIN_CANCELLED\"\n\t| \"LOGIN_CLOSE\"\n\t| \"LOGIN_RESIZE\"\n\t| \"LOGIN_ERROR\"\n\nexport interface LoginResult {\n\tavatar?: string | null\n\tchannel?: string\n\temail?: string | null\n\texpiresAt?: string\n\tlegacyCasdoorId?: string\n\tloginToken?: string\n\tname?: string | null\n\tuserId?: string\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n}\n\nexport interface LoginEventData extends HostedFrameMessage, LoginResult {\n\ttype: LoginEventType\n\terror?: string\n\tmessage?: string\n}\n\nexport interface LoginUIOptions {\n\t/** Origin to validate postMessage (defaults to '*', set for security) */\n\tallowedOrigin?: string\n\tonCancel?: (result?: LoginResult) => void\n\tonClose?: () => void\n\tonError?: (message?: string, data?: LoginEventData) => void\n\tonSuccess?: (result: LoginResult) => void\n}\n\nexport interface LoginParams {\n\tappId: string\n\n\t/**\n\t * @deprecated Use loginUrl instead\n\t * Defaults to https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * Hosted login page base URL (e.g. https://pay.youidian.com)\n\t * Defaults to https://pay.imgto.link\n\t */\n\tloginUrl?: string\n\n\t/**\n\t * Optional locale prefix to prepend to the hosted login path.\n\t * If omitted, the SDK automatically resolves the locale from the browser language.\n\t */\n\tlocale?: string\n\n\t/** Preferred provider/channel code */\n\tpreferredChannel?: string\n}\n\nfunction getOrigin(value?: string): string | null {\n\tif (!value) return null\n\ttry {\n\t\treturn new URL(value).origin\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction buildLoginUrl(params: LoginParams): string {\n\tconst { appId, baseUrl = \"https://pay.imgto.link\", loginUrl } = params\n\tconst rawBase = (loginUrl || baseUrl).replace(/\\/$/, \"\")\n\tconst parentOrigin =\n\t\ttypeof window !== \"undefined\" ? window.location.origin : undefined\n\n\tlet finalUrl: string\n\ttry {\n\t\tconst url = new URL(rawBase)\n\t\tif (!/\\/auth\\/connect\\/[^/]+$/.test(url.pathname)) {\n\t\t\turl.pathname =\n\t\t\t\t`${url.pathname.replace(/\\/$/, \"\")}/auth/connect/${appId}`.replace(\n\t\t\t\t\t/\\/{2,}/g,\n\t\t\t\t\t\"/\",\n\t\t\t\t)\n\t\t}\n\t\tfinalUrl = url.toString()\n\t} catch (_error) {\n\t\tif (/\\/auth\\/connect\\/[^/]+$/.test(rawBase)) {\n\t\t\tfinalUrl = rawBase\n\t\t} else {\n\t\t\tfinalUrl = `${rawBase}/auth/connect/${appId}`.replace(/\\/{2,}/g, \"/\")\n\t\t}\n\t}\n\n\tfinalUrl = applyLocaleToUrl(finalUrl, getAutoResolvedLocale(params.locale))\n\n\ttry {\n\t\tconst url = new URL(finalUrl)\n\t\tif (params.preferredChannel) {\n\t\t\turl.searchParams.set(\"preferredChannel\", params.preferredChannel)\n\t\t}\n\t\tif (parentOrigin) {\n\t\t\turl.searchParams.set(\"origin\", parentOrigin)\n\t\t}\n\t\treturn url.toString()\n\t} catch (_error) {\n\t\tconst searchParams = new URLSearchParams()\n\t\tif (params.preferredChannel) {\n\t\t\tsearchParams.set(\"preferredChannel\", params.preferredChannel)\n\t\t}\n\t\tif (parentOrigin) {\n\t\t\tsearchParams.set(\"origin\", parentOrigin)\n\t\t}\n\t\tconst query = searchParams.toString()\n\t\tif (!query) {\n\t\t\treturn finalUrl\n\t\t}\n\t\tconst separator = finalUrl.includes(\"?\") ? \"&\" : \"?\"\n\t\treturn `${finalUrl}${separator}${query}`\n\t}\n}\n\nfunction extractLoginResult(data: LoginEventData): LoginResult {\n\treturn {\n\t\tavatar: data.avatar,\n\t\tchannel: data.channel,\n\t\temail: data.email,\n\t\texpiresAt: data.expiresAt,\n\t\tlegacyCasdoorId: data.legacyCasdoorId,\n\t\tloginToken: data.loginToken,\n\t\tname: data.name,\n\t\tuserId: data.userId,\n\t\twechatOpenId: data.wechatOpenId,\n\t\twechatUnionId: data.wechatUnionId,\n\t}\n}\n\nexport class LoginUI {\n\tprivate popup: Window | null = null\n\tprivate messageHandler: ((event: MessageEvent) => void) | null = null\n\tprivate closeMonitor: number | null = null\n\tprivate completed = false\n\n\tprivate cleanup() {\n\t\tif (typeof window !== \"undefined\" && this.messageHandler) {\n\t\t\twindow.removeEventListener(\"message\", this.messageHandler)\n\t\t}\n\t\tif (typeof window !== \"undefined\" && this.closeMonitor) {\n\t\t\twindow.clearInterval(this.closeMonitor)\n\t\t}\n\t\tthis.messageHandler = null\n\t\tthis.closeMonitor = null\n\t\tthis.popup = null\n\t\tthis.completed = false\n\t}\n\n\tclose() {\n\t\tif (this.popup && !this.popup.closed) {\n\t\t\tthis.popup.close()\n\t\t}\n\t\tthis.cleanup()\n\t}\n\n\topenLogin(params: LoginParams, options?: LoginUIOptions) {\n\t\tif (typeof window === \"undefined\") return\n\t\tif (this.popup && !this.popup.closed) return\n\n\t\tconst finalUrl = buildLoginUrl(params)\n\t\tconst popupWidth = 520\n\t\tconst popupHeight = 720\n\t\tconst left = Math.max(\n\t\t\t0,\n\t\t\tMath.round(window.screenX + (window.outerWidth - popupWidth) / 2),\n\t\t)\n\t\tconst top = Math.max(\n\t\t\t0,\n\t\t\tMath.round(window.screenY + (window.outerHeight - popupHeight) / 2),\n\t\t)\n\t\tconst features = [\n\t\t\t\"popup=yes\",\n\t\t\t`width=${popupWidth}`,\n\t\t\t`height=${popupHeight}`,\n\t\t\t`left=${left}`,\n\t\t\t`top=${top}`,\n\t\t\t\"resizable=yes\",\n\t\t\t\"scrollbars=yes\",\n\t\t].join(\",\")\n\n\t\tconst popup = window.open(finalUrl, \"youidian-login\", features)\n\t\tif (!popup) {\n\t\t\toptions?.onError?.(\"Login popup was blocked\")\n\t\t\treturn\n\t\t}\n\n\t\tthis.popup = popup\n\t\tthis.completed = false\n\n\t\tconst allowedOrigin = options?.allowedOrigin\n\t\tconst popupOrigin = getOrigin(finalUrl)\n\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tif (\n\t\t\t\tallowedOrigin &&\n\t\t\t\tallowedOrigin !== \"*\" &&\n\t\t\t\tevent.origin !== allowedOrigin\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!allowedOrigin && popupOrigin && event.origin !== popupOrigin) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst data = event.data as LoginEventData\n\t\t\tif (!data || typeof data !== \"object\" || !data.type) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tswitch (data.type) {\n\t\t\t\tcase \"LOGIN_SUCCESS\":\n\t\t\t\t\tthis.completed = true\n\t\t\t\t\toptions?.onSuccess?.(extractLoginResult(data))\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_CANCELLED\":\n\t\t\t\t\toptions?.onCancel?.(extractLoginResult(data))\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_RESIZE\":\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_ERROR\":\n\t\t\t\t\toptions?.onError?.(data.message || data.error, data)\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_CLOSE\":\n\t\t\t\t\tthis.close()\n\t\t\t\t\toptions?.onClose?.()\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\twindow.addEventListener(\"message\", this.messageHandler)\n\n\t\tthis.closeMonitor = window.setInterval(() => {\n\t\t\tif (!this.popup || this.popup.closed) {\n\t\t\t\tconst shouldTreatAsCancel = !this.completed\n\t\t\t\tthis.cleanup()\n\t\t\t\tif (shouldTreatAsCancel) {\n\t\t\t\t\toptions?.onCancel?.()\n\t\t\t\t}\n\t\t\t\toptions?.onClose?.()\n\t\t\t}\n\t\t}, 500)\n\t}\n}\n\nexport function createLoginUI(): LoginUI {\n\treturn new LoginUI()\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hosted-modal.ts","../src/login.ts"],"sourcesContent":["export interface HostedFrameMessage {\n\ttype: string\n\theight?: number\n}\n\ntype HostedFrameOptions<TData extends HostedFrameMessage> = {\n\tallowedOrigin?: string\n\theight?: string\n\tonCloseButton?: () => void\n\tonMessage: (data: TData, container: HTMLDivElement) => void\n\twidth?: string\n}\n\nconst SDK_SUPPORTED_LOCALES = [\n\t\"en\",\n\t\"zh-CN\",\n\t\"zh-Hant\",\n\t\"fr\",\n\t\"de\",\n\t\"ja\",\n\t\"es\",\n\t\"ko\",\n\t\"nl\",\n\t\"it\",\n\t\"pt\",\n] as const\n\nconst SDK_DEFAULT_LOCALE = \"zh-CN\"\n\nconst SDK_LOCALE_ALIASES: Record<string, string> = {\n\ten: \"en\",\n\t\"en-us\": \"en\",\n\t\"en-gb\": \"en\",\n\tzh: \"zh-CN\",\n\t\"zh-cn\": \"zh-CN\",\n\t\"zh-tw\": \"zh-Hant\",\n\t\"zh-hk\": \"zh-Hant\",\n\t\"zh-hant\": \"zh-Hant\",\n\t\"zh-hans\": \"zh-CN\",\n\tfr: \"fr\",\n\tde: \"de\",\n\tja: \"ja\",\n\tes: \"es\",\n\tko: \"ko\",\n\tnl: \"nl\",\n\tit: \"it\",\n\tpt: \"pt\",\n}\n\nfunction matchSupportedBaseLocale(locale: string): string | undefined {\n\tconst base = locale.toLowerCase().split(\"-\")[0]\n\treturn SDK_SUPPORTED_LOCALES.find((item) => item.toLowerCase() === base)\n}\n\nexport function normalizeSdkLocale(locale?: string): string | undefined {\n\tif (!locale) return\n\n\tconst sanitized = locale.trim().replace(/_/g, \"-\")\n\tif (!sanitized) return\n\n\tconst lower = sanitized.toLowerCase()\n\tif (SDK_LOCALE_ALIASES[lower]) {\n\t\treturn SDK_LOCALE_ALIASES[lower]\n\t}\n\n\tlet canonical: string | undefined\n\ttry {\n\t\tcanonical = Intl.getCanonicalLocales(sanitized)[0]\n\t} catch {\n\t\tcanonical = undefined\n\t}\n\n\tconst candidates = [canonical, sanitized].filter(Boolean) as string[]\n\n\tfor (const candidate of candidates) {\n\t\tconst candidateLower = candidate.toLowerCase()\n\n\t\tif (SDK_LOCALE_ALIASES[candidateLower]) {\n\t\t\treturn SDK_LOCALE_ALIASES[candidateLower]\n\t\t}\n\t\tif ((SDK_SUPPORTED_LOCALES as readonly string[]).includes(candidate)) {\n\t\t\treturn candidate\n\t\t}\n\n\t\tconst baseMatch = matchSupportedBaseLocale(candidate)\n\t\tif (baseMatch) {\n\t\t\treturn baseMatch\n\t\t}\n\t}\n}\n\nexport function getBrowserLocale(): string | undefined {\n\tif (typeof navigator === \"undefined\") return\n\n\tfor (const candidate of navigator.languages || []) {\n\t\tconst normalized = normalizeSdkLocale(candidate)\n\t\tif (normalized) return normalized\n\t}\n\n\treturn normalizeSdkLocale(navigator.language)\n}\n\nexport function getAutoResolvedLocale(locale?: string): string {\n\treturn normalizeSdkLocale(locale) || getBrowserLocale() || SDK_DEFAULT_LOCALE\n}\n\nexport function applyLocaleToUrl(urlValue: string, locale?: string): string {\n\tif (!locale) return urlValue\n\n\ttry {\n\t\tconst url = new URL(urlValue)\n\t\tconst localePrefix = `/${locale}`\n\t\tif (\n\t\t\t!url.pathname.startsWith(`${localePrefix}/`) &&\n\t\t\turl.pathname !== localePrefix\n\t\t) {\n\t\t\turl.pathname = `${localePrefix}${url.pathname}`\n\t\t}\n\t\treturn url.toString()\n\t} catch (_error) {\n\t\tconst localePrefix = `/${locale}`\n\t\tif (!urlValue.startsWith(`${localePrefix}/`) && urlValue !== localePrefix) {\n\t\t\treturn `${localePrefix}${urlValue.startsWith(\"/\") ? \"\" : \"/\"}${urlValue}`\n\t\t}\n\t\treturn urlValue\n\t}\n}\n\nexport class HostedFrameModal<TData extends HostedFrameMessage> {\n\tprotected iframe: HTMLIFrameElement | null = null\n\tprotected modal: HTMLDivElement | null = null\n\tprotected messageHandler: ((event: MessageEvent) => void) | null = null\n\n\tprotected openHostedFrame(\n\t\turl: string,\n\t\toptions: HostedFrameOptions<TData>,\n\t): void {\n\t\tif (typeof document === \"undefined\") return\n\t\tif (this.modal) return\n\n\t\tthis.modal = document.createElement(\"div\")\n\t\tObject.assign(this.modal.style, {\n\t\t\tposition: \"fixed\",\n\t\t\ttop: \"0\",\n\t\t\tleft: \"0\",\n\t\t\twidth: \"100%\",\n\t\t\theight: \"100%\",\n\t\t\tbackgroundColor: \"rgba(15,23,42,0.52)\",\n\t\t\tdisplay: \"flex\",\n\t\t\talignItems: \"center\",\n\t\t\tjustifyContent: \"center\",\n\t\t\tzIndex: \"9999\",\n\t\t\ttransition: \"opacity 0.3s ease\",\n\t\t\tbackdropFilter: \"blur(14px)\",\n\t\t\tpadding: \"16px\",\n\t\t})\n\n\t\tconst container = document.createElement(\"div\")\n\t\tObject.assign(container.style, {\n\t\t\twidth: options.width || \"450px\",\n\t\t\theight: options.height || \"min(600px, 90vh)\",\n\t\t\tbackgroundColor: \"transparent\",\n\t\t\tborderRadius: \"28px\",\n\t\t\toverflow: \"visible\",\n\t\t\tposition: \"relative\",\n\t\t\tboxShadow: \"none\",\n\t\t\tmaxWidth: \"calc(100vw - 32px)\",\n\t\t\tmaxHeight: \"calc(100vh - 32px)\",\n\t\t\ttransition: \"height 180ms ease\",\n\t\t\twillChange: \"height\",\n\t\t})\n\n\t\tconst closeBtn = document.createElement(\"button\")\n\t\tcloseBtn.innerHTML = \"×\"\n\t\tObject.assign(closeBtn.style, {\n\t\t\tposition: \"absolute\",\n\t\t\tright: \"-14px\",\n\t\t\ttop: \"-14px\",\n\t\t\tfontSize: \"20px\",\n\t\t\twidth: \"36px\",\n\t\t\theight: \"36px\",\n\t\t\tborderRadius: \"9999px\",\n\t\t\tborder: \"1px solid rgba(255,255,255,0.5)\",\n\t\t\tbackground: \"rgba(255,255,255,0.9)\",\n\t\t\tcursor: \"pointer\",\n\t\t\tcolor: \"#475569\",\n\t\t\tzIndex: \"2\",\n\t\t\tboxShadow: \"0 10px 30px rgba(15,23,42,0.16)\",\n\t\t})\n\t\tcloseBtn.onclick = () => {\n\t\t\tthis.close()\n\t\t\toptions.onCloseButton?.()\n\t\t}\n\n\t\tthis.iframe = document.createElement(\"iframe\")\n\t\tthis.iframe.src = url\n\t\tObject.assign(this.iframe.style, {\n\t\t\twidth: \"100%\",\n\t\t\theight: \"100%\",\n\t\t\tborder: \"none\",\n\t\t\tborderRadius: \"28px\",\n\t\t\tbackground: \"transparent\",\n\t\t\tdisplay: \"block\",\n\t\t})\n\n\t\tcontainer.appendChild(closeBtn)\n\t\tcontainer.appendChild(this.iframe)\n\t\tthis.modal.appendChild(container)\n\t\tdocument.body.appendChild(this.modal)\n\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tif (options.allowedOrigin && options.allowedOrigin !== \"*\") {\n\t\t\t\tif (event.origin !== options.allowedOrigin) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst data = event.data as TData\n\t\t\tif (!data || typeof data !== \"object\" || !data.type) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\toptions.onMessage(data, container)\n\t\t}\n\n\t\twindow.addEventListener(\"message\", this.messageHandler)\n\t}\n\n\tclose() {\n\t\tif (typeof window === \"undefined\") return\n\n\t\tif (this.messageHandler) {\n\t\t\twindow.removeEventListener(\"message\", this.messageHandler)\n\t\t\tthis.messageHandler = null\n\t\t}\n\n\t\tif (this.modal?.parentNode) {\n\t\t\tthis.modal.parentNode.removeChild(this.modal)\n\t\t}\n\n\t\tthis.modal = null\n\t\tthis.iframe = null\n\t}\n}\n","import {\n\tapplyLocaleToUrl,\n\tgetAutoResolvedLocale,\n\ttype HostedFrameMessage,\n} from \"./hosted-modal\"\n\nexport type LoginEventType =\n\t| \"LOGIN_SUCCESS\"\n\t| \"LOGIN_CANCELLED\"\n\t| \"LOGIN_CLOSE\"\n\t| \"LOGIN_RESIZE\"\n\t| \"LOGIN_ERROR\"\n\nexport interface LoginResult {\n\tavatar?: string | null\n\tchannel?: string\n\temail?: string | null\n\texpiresAt?: string\n\tlegacyCasdoorId?: string\n\tloginToken?: string\n\tname?: string | null\n\tuserId?: string\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n}\n\nexport interface LoginEventData extends HostedFrameMessage, LoginResult {\n\ttype: LoginEventType\n\terror?: string\n\tmessage?: string\n}\n\nexport interface LoginUIOptions {\n\t/** Origin to validate postMessage (defaults to '*', set for security) */\n\tallowedOrigin?: string\n\t/** Whether the SDK should close the popup after receiving LOGIN_CLOSE. Defaults to true. */\n\tautoClose?: boolean\n\tonCancel?: () => void\n\tonClose?: () => void\n\tonError?: (message?: string, data?: LoginEventData) => void\n\tonSuccess?: (result: LoginResult) => void\n}\n\nexport interface LoginParams {\n\tappId: string\n\n\t/**\n\t * @deprecated Use loginUrl instead\n\t * Defaults to https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * Hosted login page base URL (e.g. https://pay.youidian.com)\n\t * Defaults to https://pay.imgto.link\n\t */\n\tloginUrl?: string\n\n\t/**\n\t * Optional locale prefix to prepend to the hosted login path.\n\t * If omitted, the SDK automatically resolves the locale from the browser language.\n\t */\n\tlocale?: string\n\n\t/** Preferred provider/channel code */\n\tpreferredChannel?: string\n}\n\nfunction getOrigin(value?: string): string | null {\n\tif (!value) return null\n\ttry {\n\t\treturn new URL(value).origin\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction buildLoginUrl(params: LoginParams): string {\n\tconst { appId, baseUrl = \"https://pay.imgto.link\", loginUrl } = params\n\tconst rawBase = (loginUrl || baseUrl).replace(/\\/$/, \"\")\n\tconst parentOrigin =\n\t\ttypeof window !== \"undefined\" ? window.location.origin : undefined\n\n\tlet finalUrl: string\n\ttry {\n\t\tconst url = new URL(rawBase)\n\t\tif (!/\\/auth\\/connect\\/[^/]+$/.test(url.pathname)) {\n\t\t\turl.pathname =\n\t\t\t\t`${url.pathname.replace(/\\/$/, \"\")}/auth/connect/${appId}`.replace(\n\t\t\t\t\t/\\/{2,}/g,\n\t\t\t\t\t\"/\",\n\t\t\t\t)\n\t\t}\n\t\tfinalUrl = url.toString()\n\t} catch (_error) {\n\t\tif (/\\/auth\\/connect\\/[^/]+$/.test(rawBase)) {\n\t\t\tfinalUrl = rawBase\n\t\t} else {\n\t\t\tfinalUrl = `${rawBase}/auth/connect/${appId}`.replace(/\\/{2,}/g, \"/\")\n\t\t}\n\t}\n\n\tfinalUrl = applyLocaleToUrl(finalUrl, getAutoResolvedLocale(params.locale))\n\n\ttry {\n\t\tconst url = new URL(finalUrl)\n\t\tif (params.preferredChannel) {\n\t\t\turl.searchParams.set(\"preferredChannel\", params.preferredChannel)\n\t\t}\n\t\tif (parentOrigin) {\n\t\t\turl.searchParams.set(\"origin\", parentOrigin)\n\t\t}\n\t\treturn url.toString()\n\t} catch (_error) {\n\t\tconst searchParams = new URLSearchParams()\n\t\tif (params.preferredChannel) {\n\t\t\tsearchParams.set(\"preferredChannel\", params.preferredChannel)\n\t\t}\n\t\tif (parentOrigin) {\n\t\t\tsearchParams.set(\"origin\", parentOrigin)\n\t\t}\n\t\tconst query = searchParams.toString()\n\t\tif (!query) {\n\t\t\treturn finalUrl\n\t\t}\n\t\tconst separator = finalUrl.includes(\"?\") ? \"&\" : \"?\"\n\t\treturn `${finalUrl}${separator}${query}`\n\t}\n}\n\nfunction extractLoginResult(data: LoginEventData): LoginResult {\n\treturn {\n\t\tavatar: data.avatar,\n\t\tchannel: data.channel,\n\t\temail: data.email,\n\t\texpiresAt: data.expiresAt,\n\t\tlegacyCasdoorId: data.legacyCasdoorId,\n\t\tloginToken: data.loginToken,\n\t\tname: data.name,\n\t\tuserId: data.userId,\n\t\twechatOpenId: data.wechatOpenId,\n\t\twechatUnionId: data.wechatUnionId,\n\t}\n}\n\nconst LOGIN_UI_LOG_PREFIX = \"[Youidian LoginUI]\"\n\nfunction logLoginDebug(message: string, details?: Record<string, unknown>) {\n\tif (typeof console === \"undefined\") return\n\tconsole.debug(LOGIN_UI_LOG_PREFIX, message, details || {})\n}\n\nfunction logLoginInfo(message: string, details?: Record<string, unknown>) {\n\tif (typeof console === \"undefined\") return\n\tconsole.info(LOGIN_UI_LOG_PREFIX, message, details || {})\n}\n\nfunction logLoginWarn(message: string, details?: Record<string, unknown>) {\n\tif (typeof console === \"undefined\") return\n\tconsole.warn(LOGIN_UI_LOG_PREFIX, message, details || {})\n}\n\nexport class LoginUI {\n\tprivate popup: Window | null = null\n\tprivate messageHandler: ((event: MessageEvent) => void) | null = null\n\tprivate closeMonitor: number | null = null\n\tprivate completed = false\n\n\tprivate cleanup() {\n\t\tif (typeof window !== \"undefined\" && this.messageHandler) {\n\t\t\twindow.removeEventListener(\"message\", this.messageHandler)\n\t\t}\n\t\tif (typeof window !== \"undefined\" && this.closeMonitor) {\n\t\t\twindow.clearInterval(this.closeMonitor)\n\t\t}\n\t\tthis.messageHandler = null\n\t\tthis.closeMonitor = null\n\t\tthis.popup = null\n\t\tthis.completed = false\n\t}\n\n\tclose() {\n\t\tlogLoginInfo(\"close() called\", {\n\t\t\thasPopup: Boolean(this.popup),\n\t\t\tpopupClosed: this.popup?.closed ?? true,\n\t\t})\n\t\tif (this.popup && !this.popup.closed) {\n\t\t\tthis.popup.close()\n\t\t}\n\t\tthis.cleanup()\n\t}\n\n\topenLogin(params: LoginParams, options?: LoginUIOptions) {\n\t\tif (typeof window === \"undefined\") return\n\t\tif (this.popup && !this.popup.closed) return\n\n\t\tconst finalUrl = buildLoginUrl(params)\n\t\tlogLoginInfo(\"Opening hosted login popup\", {\n\t\t\tappId: params.appId,\n\t\t\tloginUrl: finalUrl,\n\t\t\tallowedOrigin: options?.allowedOrigin || null,\n\t\t\tautoClose: options?.autoClose ?? true,\n\t\t})\n\t\tconst popupWidth = 520\n\t\tconst popupHeight = 720\n\t\tconst left = Math.max(\n\t\t\t0,\n\t\t\tMath.round(window.screenX + (window.outerWidth - popupWidth) / 2),\n\t\t)\n\t\tconst top = Math.max(\n\t\t\t0,\n\t\t\tMath.round(window.screenY + (window.outerHeight - popupHeight) / 2),\n\t\t)\n\t\tconst features = [\n\t\t\t\"popup=yes\",\n\t\t\t`width=${popupWidth}`,\n\t\t\t`height=${popupHeight}`,\n\t\t\t`left=${left}`,\n\t\t\t`top=${top}`,\n\t\t\t\"resizable=yes\",\n\t\t\t\"scrollbars=yes\",\n\t\t].join(\",\")\n\n\t\tconst popup = window.open(finalUrl, \"youidian-login\", features)\n\t\tif (!popup) {\n\t\t\tlogLoginWarn(\"Popup blocked by the browser\")\n\t\t\toptions?.onError?.(\"Login popup was blocked\")\n\t\t\treturn\n\t\t}\n\n\t\tthis.popup = popup\n\t\tthis.completed = false\n\n\t\tconst allowedOrigin = options?.allowedOrigin\n\t\tconst popupOrigin = getOrigin(finalUrl)\n\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tif (\n\t\t\t\tallowedOrigin &&\n\t\t\t\tallowedOrigin !== \"*\" &&\n\t\t\t\tevent.origin !== allowedOrigin\n\t\t\t) {\n\t\t\t\tlogLoginWarn(\"Ignored message due to allowedOrigin mismatch\", {\n\t\t\t\t\tallowedOrigin,\n\t\t\t\t\teventOrigin: event.origin,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!allowedOrigin && popupOrigin && event.origin !== popupOrigin) {\n\t\t\t\tlogLoginWarn(\"Ignored message due to popup origin mismatch\", {\n\t\t\t\t\texpectedOrigin: popupOrigin,\n\t\t\t\t\teventOrigin: event.origin,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst data = event.data as LoginEventData\n\t\t\tif (!data || typeof data !== \"object\" || !data.type) {\n\t\t\t\tlogLoginDebug(\"Ignored non-login postMessage payload\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlogLoginInfo(\"Received login event\", {\n\t\t\t\ttype: data.type,\n\t\t\t\teventOrigin: event.origin,\n\t\t\t})\n\n\t\t\tswitch (data.type) {\n\t\t\t\tcase \"LOGIN_SUCCESS\":\n\t\t\t\t\tthis.completed = true\n\t\t\t\t\tlogLoginInfo(\"Login succeeded\", {\n\t\t\t\t\t\tchannel: data.channel || null,\n\t\t\t\t\t\tuserId: data.userId || null,\n\t\t\t\t\t})\n\t\t\t\t\toptions?.onSuccess?.(extractLoginResult(data))\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_CANCELLED\":\n\t\t\t\t\tlogLoginInfo(\"Login cancelled\")\n\t\t\t\t\toptions?.onCancel?.()\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_RESIZE\":\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_ERROR\":\n\t\t\t\t\tlogLoginWarn(\"Login flow reported an error\", {\n\t\t\t\t\t\tmessage: data.message || data.error || null,\n\t\t\t\t\t})\n\t\t\t\t\toptions?.onError?.(data.message || data.error, data)\n\t\t\t\t\tbreak\n\t\t\t\tcase \"LOGIN_CLOSE\":\n\t\t\t\t\tif (options?.autoClose === false) {\n\t\t\t\t\t\tlogLoginInfo(\"Login popup requested close; autoClose disabled, keeping popup open\",)\n\t\t\t\t\t\toptions?.onClose?.()\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tlogLoginInfo(\"Login popup requested close; autoClose enabled\")\n\t\t\t\t\tthis.close()\n\t\t\t\t\toptions?.onClose?.()\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\twindow.addEventListener(\"message\", this.messageHandler)\n\n\t\tthis.closeMonitor = window.setInterval(() => {\n\t\t\tif (!this.popup || this.popup.closed) {\n\t\t\t\tconst shouldTreatAsCancel = !this.completed\n\t\t\t\tlogLoginInfo(\"Detected popup closed\", {\n\t\t\t\t\tshouldTreatAsCancel,\n\t\t\t\t})\n\t\t\t\tthis.cleanup()\n\t\t\t\tif (shouldTreatAsCancel) {\n\t\t\t\t\toptions?.onCancel?.()\n\t\t\t\t}\n\t\t\t\toptions?.onClose?.()\n\t\t\t}\n\t\t}, 500)\n\t}\n}\n\nexport function createLoginUI(): LoginUI {\n\treturn new LoginUI()\n}\n"],"mappings":"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","names":[]}
package/dist/server.cjs CHANGED
@@ -185,12 +185,27 @@ var PaymentClient = class {
185
185
  return this.request("GET", path);
186
186
  }
187
187
  /**
188
- * Get all entitlements for a user
188
+ * Get user entitlements in the legacy flat shape.
189
189
  * @param userId - User ID
190
190
  */
191
191
  async getEntitlements(userId) {
192
192
  return this.request("GET", `/users/${userId}/entitlements`);
193
193
  }
194
+ /**
195
+ * Get user entitlements with full details (type, expiry, reset config, source)
196
+ * @param userId - User ID
197
+ */
198
+ async getEntitlementsDetail(userId) {
199
+ return this.request("GET", `/users/${userId}/entitlements/detail`);
200
+ }
201
+ /**
202
+ * Ensure user exists and auto-assign trial product if new user
203
+ * This should be called when user first logs in or registers
204
+ * @param userId - User ID
205
+ */
206
+ async ensureUserWithTrial(userId) {
207
+ return this.request("POST", `/users/${userId}/entitlements/bootstrap`, {});
208
+ }
194
209
  /**
195
210
  * Get a single entitlement value
196
211
  * @param userId - User ID
@@ -198,7 +213,7 @@ var PaymentClient = class {
198
213
  */
199
214
  async getEntitlementValue(userId, key) {
200
215
  const entitlements = await this.getEntitlements(userId);
201
- return entitlements[key];
216
+ return entitlements[key] ?? null;
202
217
  }
203
218
  /**
204
219
  * Consume numeric entitlement
@@ -206,10 +221,11 @@ var PaymentClient = class {
206
221
  * @param key - Entitlement key
207
222
  * @param amount - Amount to consume
208
223
  */
209
- async consumeEntitlement(userId, key, amount) {
224
+ async consumeEntitlement(userId, key, amount, options) {
210
225
  return this.request("POST", `/users/${userId}/entitlements/consume`, {
211
226
  key,
212
- amount
227
+ amount,
228
+ ...options
213
229
  });
214
230
  }
215
231
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Youidian Payment SDK - Server Module\n * 用于服务端集成,包含签名、订单创建、回调解密等功能\n */\n\nimport crypto from \"crypto\"\n\n/**\n * Order status response\n */\nexport interface OrderStatus {\n\torderId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tpaidAt?: string\n\tchannelTransactionId?: string\n}\n\n/**\n * Order details response (full order information)\n */\nexport interface OrderDetails {\n\torderId: string\n\tinternalId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tdescription?: string\n\tpaidAt?: string\n\tcreatedAt: string\n\tchannel?: string\n\tproduct?: {\n\t\tcode: string\n\t\ttype: string\n\t\tname: string\n\t\tdescription?: string\n\t\tentitlements: ProductEntitlements\n\t}\n}\n\n/**\n * Product Entitlements\n */\nexport interface ProductEntitlements {\n\t[key: string]: any\n}\n\n/**\n * Product Price\n */\nexport interface ProductPrice {\n\tid: string\n\tcurrency: string\n\tamount: number\n\tdisplayAmount: string\n\tlocale: string | null\n\tisDefault: boolean\n}\n\n/**\n * Product Data\n */\nexport interface Product {\n\tid: string\n\tcode: string\n\ttype: string\n\tname: string\n\tdescription?: string\n\tentitlements: ProductEntitlements\n\tprices: ProductPrice[]\n}\n\n/**\n * WeChat JSAPI Payment Parameters (for wx.requestPayment)\n */\nexport interface WechatJsapiPayParams {\n\tappId: string\n\ttimeStamp: string\n\tnonceStr: string\n\tpackage: string\n\tsignType: \"RSA\"\n\tpaySign: string\n}\n\n/**\n * Verified hosted login token payload.\n */\nexport interface VerifiedLoginToken {\n\tappId: string\n\tuserId: string\n\tlegacyCasdoorId?: string | null\n\tchannel: string\n\temail?: string | null\n\tname?: string | null\n\tavatar?: string | null\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n\texpiresAt: string\n}\n\n/**\n * SDK Client Options\n */\nexport interface PaymentClientOptions {\n\t/** Application ID (Required) */\n\tappId: string\n\t/** Application Secret (Required for server-side operations) */\n\tappSecret: string\n\n\t/**\n\t * @deprecated Use apiUrl and checkoutUrl instead\n\t * API Base URL (e.g. https://pay.youidian.com)\n\t * If apiUrl or checkoutUrl is not provided, this will be used as fallback\n\t * Default: https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * API server URL for backend requests (e.g. https://api.youidian.com)\n\t * Default: https://pay-api.imgto.link\n\t */\n\tapiUrl?: string\n\n\t/**\n\t * Checkout page URL for client-side payment (e.g. https://pay.youidian.com)\n\t * Default: https://pay.imgto.link\n\t */\n\tcheckoutUrl?: string\n}\n\n/**\n * Create Order Parameters\n */\nexport interface CreateOrderParams {\n\tproductId?: string\n\tpriceId?: string\n\tchannel?: string\n\tuserId: string\n\treturnUrl?: string\n\tmetadata?: Record<string, any>\n\tmerchantOrderId?: string\n\topenid?: string\n\tlocale?: string\n}\n\n/**\n * Create WeChat Mini Program Order Parameters\n */\nexport type CreateMiniProgramOrderParams = {\n\tuserId: string\n\topenid: string\n\tmerchantOrderId?: string\n\tpriceId: string\n}\n\n/**\n * Create Order Response\n */\nexport interface CreateOrderResponse {\n\torderId: string\n\tinternalId: string\n\tamount: number\n\tcurrency: string\n\tpayParams: any\n}\n\n/**\n * Payment Callback Notification\n */\nexport interface PaymentNotification {\n\tiv: string\n\tencryptedData: string\n\tauthTag: string\n}\n\n/**\n * Decrypted Payment Callback Data\n */\nexport interface PaymentCallbackData {\n\torderId: string\n\tmerchantOrderId?: string\n\tstatus: \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tpaidAt: string\n\tchannelTransactionId?: string\n\tmetadata?: Record<string, any>\n}\n\n/**\n * Get Orders Parameters\n */\nexport interface GetOrdersParams {\n\tpage?: number\n\tpageSize?: number\n\tuserId?: string\n\tstatus?: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tstartDate?: string\n\tendDate?: string\n}\n\n/**\n * Order List Item\n */\nexport interface OrderListItem {\n\torderId: string\n\tinternalId: string\n\tmerchantUserId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tchannel?: string\n\tpaidAt?: string\n\tcreatedAt: string\n}\n\n/**\n * Get Orders Response\n */\nexport interface GetOrdersResponse {\n\torders: OrderListItem[]\n\tpagination: {\n\t\ttotal: number\n\t\tpage: number\n\t\tpageSize: number\n\t\ttotalPages: number\n\t}\n}\n\n/**\n * Server-side Payment Client\n * 服务端支付客户端,用于创建订单、查询状态、解密回调\n */\nexport class PaymentClient {\n\tprivate readonly appId: string\n\tprivate readonly appSecret: string\n\tprivate readonly apiUrl: string // 用于 API 调用\n\tprivate readonly checkoutUrl: string // 用于生成 checkout URL\n\n\tconstructor(options: PaymentClientOptions) {\n\t\tif (!options.appId) throw new Error(\"appId is required\")\n\t\tif (!options.appSecret) throw new Error(\"appSecret is required\")\n\n\t\tthis.appId = options.appId\n\t\tthis.appSecret = options.appSecret\n\n\t\t// apiUrl: 优先使用 apiUrl,其次 baseUrl,默认 https://pay-api.imgto.link\n\t\tconst apiUrl =\n\t\t\toptions.apiUrl || options.baseUrl || \"https://pay-api.imgto.link\"\n\t\tthis.apiUrl = apiUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\n\t\t// checkoutUrl: 优先使用 checkoutUrl,其次 baseUrl,默认 https://pay.imgto.link\n\t\tconst checkoutUrl =\n\t\t\toptions.checkoutUrl || options.baseUrl || \"https://pay.imgto.link\"\n\t\tthis.checkoutUrl = checkoutUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\t}\n\n\t/**\n\t * Generate SHA256 signature for the request\n\t * Logic: SHA256(appId + appSecret + timestamp)\n\t */\n\tprivate generateSignature(timestamp: number): string {\n\t\tconst str = `${this.appId}${this.appSecret}${timestamp}`\n\t\treturn crypto.createHash(\"sha256\").update(str).digest(\"hex\")\n\t}\n\n\t/**\n\t * Internal request helper for Gateway API\n\t */\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: any,\n\t): Promise<T> {\n\t\tconst timestamp = Date.now()\n\t\tconst signature = this.generateSignature(timestamp)\n\n\t\tconst url = `${this.apiUrl}/api/v1/gateway/${this.appId}${path}`\n\n\t\tconst headers: HeadersInit = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-Pay-Timestamp\": timestamp.toString(),\n\t\t\t\"X-Pay-Sign\": signature,\n\t\t}\n\n\t\tconst options: RequestInit = {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t}\n\n\t\tconst response = await fetch(url, options)\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text()\n\t\t\tthrow new Error(`Payment SDK Error (${response.status}): ${errorText}`)\n\t\t}\n\n\t\tconst json = await response.json()\n\t\tif (json.error) {\n\t\t\tthrow new Error(`Payment API Error: ${json.error}`)\n\t\t}\n\n\t\treturn json.data as T\n\t}\n\n\t/**\n\t * Decrypts the callback notification payload using AES-256-GCM.\n\t * @param notification - The encrypted notification from payment webhook\n\t * @returns Decrypted payment callback data\n\t */\n\tdecryptCallback(notification: PaymentNotification): PaymentCallbackData {\n\t\ttry {\n\t\t\tconst { iv, encryptedData, authTag } = notification\n\t\t\tconst key = crypto.createHash(\"sha256\").update(this.appSecret).digest()\n\t\t\tconst decipher = crypto.createDecipheriv(\n\t\t\t\t\"aes-256-gcm\",\n\t\t\t\tkey,\n\t\t\t\tBuffer.from(iv, \"hex\"),\n\t\t\t)\n\n\t\t\tdecipher.setAuthTag(Buffer.from(authTag, \"hex\"))\n\n\t\t\tlet decrypted = decipher.update(encryptedData, \"hex\", \"utf8\")\n\t\t\tdecrypted += decipher.final(\"utf8\")\n\n\t\t\treturn JSON.parse(decrypted)\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Failed to decrypt payment callback: Invalid secret or tampered data.\",\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Fetch products for the configured app.\n\t */\n\tasync getProducts(options?: {\n\t\tlocale?: string\n\t\tcurrency?: string\n\t}): Promise<Product[]> {\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.locale) params.append(\"locale\", options.locale)\n\t\tif (options?.currency) params.append(\"currency\", options.currency)\n\n\t\tconst path = params.toString()\n\t\t\t? `/products?${params.toString()}`\n\t\t\t: \"/products\"\n\t\treturn this.request(\"GET\", path)\n\t}\n\n\t/**\n\t * Create a new order\n\t * @param params - Order creation parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createOrder(params: CreateOrderParams): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", \"/orders\", params)\n\t}\n\n\t/**\n\t * Create a WeChat Mini Program order (channel fixed to WECHAT_MINI)\n\t * @param params - Mini program order parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createMiniProgramOrder(\n\t\tparams: CreateMiniProgramOrderParams,\n\t): Promise<CreateOrderResponse> {\n\t\tconst { openid, ...rest } = params\n\t\treturn this.request(\"POST\", \"/orders\", {\n\t\t\t...rest,\n\t\t\tchannel: \"WECHAT_MINI\",\n\t\t\topenid,\n\t\t\tmetadata: { openid },\n\t\t} satisfies CreateOrderParams)\n\t}\n\n\t/**\n\t * Pay for an existing order\n\t * @param orderId - The order ID to pay\n\t * @param params - Payment parameters including channel\n\t */\n\tasync payOrder(\n\t\torderId: string,\n\t\tparams: {\n\t\t\tchannel: string\n\t\t\treturnUrl?: string\n\t\t\topenid?: string\n\t\t\t[key: string]: any\n\t\t},\n\t): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", `/orders/${orderId}/pay`, params)\n\t}\n\n\t/**\n\t * Query order status\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderStatus(orderId: string): Promise<OrderStatus> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}`)\n\t}\n\n\t/**\n\t * Get order details (full order information)\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderDetails(orderId: string): Promise<OrderDetails> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}/details`)\n\t}\n\n\t/**\n\t * Get orders list with pagination\n\t * @param params - Query parameters (pagination, filters)\n\t * @returns Orders list and pagination info\n\t */\n\tasync getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse> {\n\t\tconst queryParams = new URLSearchParams()\n\t\tif (params?.page) queryParams.append(\"page\", params.page.toString())\n\t\tif (params?.pageSize)\n\t\t\tqueryParams.append(\"pageSize\", params.pageSize.toString())\n\t\tif (params?.userId) queryParams.append(\"userId\", params.userId)\n\t\tif (params?.status) queryParams.append(\"status\", params.status)\n\t\tif (params?.startDate) queryParams.append(\"startDate\", params.startDate)\n\t\tif (params?.endDate) queryParams.append(\"endDate\", params.endDate)\n\n\t\tconst path = queryParams.toString()\n\t\t\t? `/orders?${queryParams.toString()}`\n\t\t\t: \"/orders\"\n\t\treturn this.request<GetOrdersResponse>(\"GET\", path)\n\t}\n\n\t/**\n\t * Get all entitlements for a user\n\t * @param userId - User ID\n\t */\n\tasync getEntitlements(userId: string): Promise<Record<string, any>> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements`)\n\t}\n\n\t/**\n\t * Get a single entitlement value\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t */\n\tasync getEntitlementValue(userId: string, key: string): Promise<any> {\n\t\tconst entitlements = await this.getEntitlements(userId)\n\t\treturn entitlements[key]\n\t}\n\n\t/**\n\t * Consume numeric entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to consume\n\t */\n\tasync consumeEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/consume`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Add numeric entitlement (e.g. refund)\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to add\n\t */\n\tasync addEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/add`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Toggle boolean entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param enabled - Whether to enable\n\t */\n\tasync toggleEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tenabled: boolean,\n\t): Promise<{ isEnabled: boolean }> {\n\t\t// Toggle endpoint expects POST with enabled flag\n\t\t// However, looking at list_dir, we have toggle/route.ts\n\t\t// I should verify its contract, but assuming standard toggle pattern:\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/toggle`, {\n\t\t\tkey,\n\t\t\tenabled,\n\t\t})\n\t}\n\n\t/**\n\t * Generate checkout URL for client-side payment\n\t * @param productId - Product ID\n\t * @param priceId - Price ID\n\t * @returns Checkout page URL\n\t */\n\tgetCheckoutUrl(productId: string, priceId: string): string {\n\t\treturn `${this.checkoutUrl}/checkout/${this.appId}/${productId}/${priceId}`\n\t}\n\n\t/**\n\t * Verify a hosted login token and return the normalized login profile.\n\t * This request is signed with your app credentials and routed through the worker API.\n\t */\n\tasync verifyLoginToken(token: string): Promise<VerifiedLoginToken> {\n\t\tif (!token?.trim()) {\n\t\t\tthrow new Error(\"login token is required\")\n\t\t}\n\t\treturn this.request(\"POST\", \"/login/tokens/verify\", { token: token.trim() })\n\t}\n}\n"],"mappings":"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","names":["crypto"]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Youidian Payment SDK - Server Module\n * 用于服务端集成,包含签名、订单创建、回调解密等功能\n */\n\nimport crypto from \"crypto\"\n\n/**\n * Order status response\n */\nexport interface OrderStatus {\n\torderId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tpaidAt?: string\n\tchannelTransactionId?: string\n}\n\n/**\n * Order details response (full order information)\n */\nexport interface OrderDetails {\n\torderId: string\n\tinternalId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tdescription?: string\n\tpaidAt?: string\n\tcreatedAt: string\n\tchannel?: string\n\tproduct?: {\n\t\tcode: string\n\t\ttype: string\n\t\tname: string\n\t\tdescription?: string\n\t\tentitlements: ProductEntitlements\n\t\tmetadata?: ProductMetadata | null\n\t}\n}\n\n/**\n * Product Entitlements\n */\nexport interface ProductEntitlements {\n\t[key: string]: any\n}\n\n/**\n * Product Price\n */\nexport interface ProductPrice {\n\tid: string\n\tcurrency: string\n\tamount: number\n\tdisplayAmount: string\n\tlocale: string | null\n\tisDefault: boolean\n}\n\nexport interface ProductSubscriptionPeriod {\n\tvalue: number\n\tunit: \"days\" | \"months\" | \"years\"\n}\n\nexport interface ProductResetRule {\n\tresetInterval: \"month\"\n}\n\nexport interface ProductMetadata {\n\tsubscriptionPeriod?: ProductSubscriptionPeriod\n\texpiringEntitlements?: string[]\n\tresetEntitlements?: Record<string, ProductResetRule>\n\tautoAssignOnNewUser?: boolean\n\ttrialDurationDays?: number\n}\n\n/**\n * Product Data\n */\nexport interface Product {\n\tid: string\n\tcode: string\n\ttype: string\n\tname: string\n\tdescription?: string\n\tentitlements: ProductEntitlements\n\tprices: ProductPrice[]\n\tmetadata?: ProductMetadata | null\n}\n\n/**\n * WeChat JSAPI Payment Parameters (for wx.requestPayment)\n */\nexport interface WechatJsapiPayParams {\n\tappId: string\n\ttimeStamp: string\n\tnonceStr: string\n\tpackage: string\n\tsignType: \"RSA\"\n\tpaySign: string\n}\n\n/**\n * Verified hosted login token payload.\n */\nexport interface VerifiedLoginToken {\n\tappId: string\n\tuserId: string\n\tlegacyCasdoorId?: string | null\n\tchannel: string\n\temail?: string | null\n\tname?: string | null\n\tavatar?: string | null\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n\texpiresAt: string\n}\n\n/**\n * SDK Client Options\n */\nexport interface PaymentClientOptions {\n\t/** Application ID (Required) */\n\tappId: string\n\t/** Application Secret (Required for server-side operations) */\n\tappSecret: string\n\n\t/**\n\t * @deprecated Use apiUrl and checkoutUrl instead\n\t * API Base URL (e.g. https://pay.youidian.com)\n\t * If apiUrl or checkoutUrl is not provided, this will be used as fallback\n\t * Default: https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * API server URL for backend requests (e.g. https://api.youidian.com)\n\t * Default: https://pay-api.imgto.link\n\t */\n\tapiUrl?: string\n\n\t/**\n\t * Checkout page URL for client-side payment (e.g. https://pay.youidian.com)\n\t * Default: https://pay.imgto.link\n\t */\n\tcheckoutUrl?: string\n}\n\n/**\n * Create Order Parameters\n */\nexport interface CreateOrderParams {\n\tproductId?: string\n\tpriceId?: string\n\tchannel?: string\n\tuserId: string\n\treturnUrl?: string\n\tmetadata?: Record<string, any>\n\tmerchantOrderId?: string\n\topenid?: string\n\tlocale?: string\n}\n\n/**\n * Create WeChat Mini Program Order Parameters\n */\nexport type CreateMiniProgramOrderParams = {\n\tuserId: string\n\topenid: string\n\tmerchantOrderId?: string\n\tpriceId: string\n}\n\n/**\n * Create Order Response\n */\nexport interface CreateOrderResponse {\n\torderId: string\n\tinternalId: string\n\tamount: number\n\tcurrency: string\n\tpayParams: any\n}\n\n/**\n * Payment Callback Notification\n */\nexport interface PaymentNotification {\n\tiv: string\n\tencryptedData: string\n\tauthTag: string\n}\n\n/**\n * Decrypted Payment Callback Data\n */\nexport interface PaymentCallbackData {\n\torderId: string\n\tmerchantOrderId?: string\n\tstatus: \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tpaidAt: string\n\tchannelTransactionId?: string\n\tmetadata?: Record<string, any>\n}\n\n/**\n * Get Orders Parameters\n */\nexport interface GetOrdersParams {\n\tpage?: number\n\tpageSize?: number\n\tuserId?: string\n\tstatus?: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tstartDate?: string\n\tendDate?: string\n}\n\n/**\n * Order List Item\n */\nexport interface OrderListItem {\n\torderId: string\n\tinternalId: string\n\tmerchantUserId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tchannel?: string\n\tpaidAt?: string\n\tcreatedAt: string\n}\n\n/**\n * Get Orders Response\n */\nexport interface GetOrdersResponse {\n\torders: OrderListItem[]\n\tpagination: {\n\t\ttotal: number\n\t\tpage: number\n\t\tpageSize: number\n\t\ttotalPages: number\n\t}\n}\n\n/**\n * Entitlement Detail Item\n */\nexport interface EntitlementDetailItem {\n\ttype: string\n\tcurrent: number | boolean\n\tlimit?: number\n\texpiresAt?: string | null\n\tresetInterval?: string | null\n\tnextResetAt?: string | null\n\tsourceKind?: string | null\n}\n\n/**\n * Entitlement Detail - returned by getEntitlementsDetail\n */\nexport type EntitlementDetail = Record<string, EntitlementDetailItem>\n\n/**\n * Ensure User With Trial Response\n */\nexport interface EnsureUserWithTrialResponse {\n\tisNew: boolean\n\ttrialAssigned: boolean\n\ttrialProductCode?: string\n\tentitlements: EntitlementDetail\n}\n\n/**\n * Server-side Payment Client\n * 服务端支付客户端,用于创建订单、查询状态、解密回调\n */\nexport class PaymentClient {\n\tprivate readonly appId: string\n\tprivate readonly appSecret: string\n\tprivate readonly apiUrl: string // 用于 API 调用\n\tprivate readonly checkoutUrl: string // 用于生成 checkout URL\n\n\tconstructor(options: PaymentClientOptions) {\n\t\tif (!options.appId) throw new Error(\"appId is required\")\n\t\tif (!options.appSecret) throw new Error(\"appSecret is required\")\n\n\t\tthis.appId = options.appId\n\t\tthis.appSecret = options.appSecret\n\n\t\t// apiUrl: 优先使用 apiUrl,其次 baseUrl,默认 https://pay-api.imgto.link\n\t\tconst apiUrl =\n\t\t\toptions.apiUrl || options.baseUrl || \"https://pay-api.imgto.link\"\n\t\tthis.apiUrl = apiUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\n\t\t// checkoutUrl: 优先使用 checkoutUrl,其次 baseUrl,默认 https://pay.imgto.link\n\t\tconst checkoutUrl =\n\t\t\toptions.checkoutUrl || options.baseUrl || \"https://pay.imgto.link\"\n\t\tthis.checkoutUrl = checkoutUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\t}\n\n\t/**\n\t * Generate SHA256 signature for the request\n\t * Logic: SHA256(appId + appSecret + timestamp)\n\t */\n\tprivate generateSignature(timestamp: number): string {\n\t\tconst str = `${this.appId}${this.appSecret}${timestamp}`\n\t\treturn crypto.createHash(\"sha256\").update(str).digest(\"hex\")\n\t}\n\n\t/**\n\t * Internal request helper for Gateway API\n\t */\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: any,\n\t): Promise<T> {\n\t\tconst timestamp = Date.now()\n\t\tconst signature = this.generateSignature(timestamp)\n\n\t\tconst url = `${this.apiUrl}/api/v1/gateway/${this.appId}${path}`\n\n\t\tconst headers: HeadersInit = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-Pay-Timestamp\": timestamp.toString(),\n\t\t\t\"X-Pay-Sign\": signature,\n\t\t}\n\n\t\tconst options: RequestInit = {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t}\n\n\t\tconst response = await fetch(url, options)\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text()\n\t\t\tthrow new Error(`Payment SDK Error (${response.status}): ${errorText}`)\n\t\t}\n\n\t\tconst json = await response.json()\n\t\tif (json.error) {\n\t\t\tthrow new Error(`Payment API Error: ${json.error}`)\n\t\t}\n\n\t\treturn json.data as T\n\t}\n\n\t/**\n\t * Decrypts the callback notification payload using AES-256-GCM.\n\t * @param notification - The encrypted notification from payment webhook\n\t * @returns Decrypted payment callback data\n\t */\n\tdecryptCallback(notification: PaymentNotification): PaymentCallbackData {\n\t\ttry {\n\t\t\tconst { iv, encryptedData, authTag } = notification\n\t\t\tconst key = crypto.createHash(\"sha256\").update(this.appSecret).digest()\n\t\t\tconst decipher = crypto.createDecipheriv(\n\t\t\t\t\"aes-256-gcm\",\n\t\t\t\tkey,\n\t\t\t\tBuffer.from(iv, \"hex\"),\n\t\t\t)\n\n\t\t\tdecipher.setAuthTag(Buffer.from(authTag, \"hex\"))\n\n\t\t\tlet decrypted = decipher.update(encryptedData, \"hex\", \"utf8\")\n\t\t\tdecrypted += decipher.final(\"utf8\")\n\n\t\t\treturn JSON.parse(decrypted)\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Failed to decrypt payment callback: Invalid secret or tampered data.\",\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Fetch products for the configured app.\n\t */\n\tasync getProducts(options?: {\n\t\tlocale?: string\n\t\tcurrency?: string\n\t}): Promise<Product[]> {\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.locale) params.append(\"locale\", options.locale)\n\t\tif (options?.currency) params.append(\"currency\", options.currency)\n\n\t\tconst path = params.toString()\n\t\t\t? `/products?${params.toString()}`\n\t\t\t: \"/products\"\n\t\treturn this.request(\"GET\", path)\n\t}\n\n\t/**\n\t * Create a new order\n\t * @param params - Order creation parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createOrder(params: CreateOrderParams): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", \"/orders\", params)\n\t}\n\n\t/**\n\t * Create a WeChat Mini Program order (channel fixed to WECHAT_MINI)\n\t * @param params - Mini program order parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createMiniProgramOrder(\n\t\tparams: CreateMiniProgramOrderParams,\n\t): Promise<CreateOrderResponse> {\n\t\tconst { openid, ...rest } = params\n\t\treturn this.request(\"POST\", \"/orders\", {\n\t\t\t...rest,\n\t\t\tchannel: \"WECHAT_MINI\",\n\t\t\topenid,\n\t\t\tmetadata: { openid },\n\t\t} satisfies CreateOrderParams)\n\t}\n\n\t/**\n\t * Pay for an existing order\n\t * @param orderId - The order ID to pay\n\t * @param params - Payment parameters including channel\n\t */\n\tasync payOrder(\n\t\torderId: string,\n\t\tparams: {\n\t\t\tchannel: string\n\t\t\treturnUrl?: string\n\t\t\topenid?: string\n\t\t\t[key: string]: any\n\t\t},\n\t): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", `/orders/${orderId}/pay`, params)\n\t}\n\n\t/**\n\t * Query order status\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderStatus(orderId: string): Promise<OrderStatus> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}`)\n\t}\n\n\t/**\n\t * Get order details (full order information)\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderDetails(orderId: string): Promise<OrderDetails> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}/details`)\n\t}\n\n\t/**\n\t * Get orders list with pagination\n\t * @param params - Query parameters (pagination, filters)\n\t * @returns Orders list and pagination info\n\t */\n\tasync getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse> {\n\t\tconst queryParams = new URLSearchParams()\n\t\tif (params?.page) queryParams.append(\"page\", params.page.toString())\n\t\tif (params?.pageSize)\n\t\t\tqueryParams.append(\"pageSize\", params.pageSize.toString())\n\t\tif (params?.userId) queryParams.append(\"userId\", params.userId)\n\t\tif (params?.status) queryParams.append(\"status\", params.status)\n\t\tif (params?.startDate) queryParams.append(\"startDate\", params.startDate)\n\t\tif (params?.endDate) queryParams.append(\"endDate\", params.endDate)\n\n\t\tconst path = queryParams.toString()\n\t\t\t? `/orders?${queryParams.toString()}`\n\t\t\t: \"/orders\"\n\t\treturn this.request<GetOrdersResponse>(\"GET\", path)\n\t}\n\n\t/**\n\t * Get user entitlements in the legacy flat shape.\n\t * @param userId - User ID\n\t */\n\tasync getEntitlements(userId: string): Promise<Record<string, any>> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements`)\n\t}\n\n\t/**\n\t * Get user entitlements with full details (type, expiry, reset config, source)\n\t * @param userId - User ID\n\t */\n\tasync getEntitlementsDetail(userId: string): Promise<EntitlementDetail> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements/detail`)\n\t}\n\n\t/**\n\t * Ensure user exists and auto-assign trial product if new user\n\t * This should be called when user first logs in or registers\n\t * @param userId - User ID\n\t */\n\tasync ensureUserWithTrial(\n\t\tuserId: string,\n\t): Promise<EnsureUserWithTrialResponse> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/bootstrap`, {})\n\t}\n\n\t/**\n\t * Get a single entitlement value\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t */\n\tasync getEntitlementValue(userId: string, key: string): Promise<any> {\n\t\tconst entitlements = await this.getEntitlements(userId)\n\t\treturn entitlements[key] ?? null\n\t}\n\n\t/**\n\t * Consume numeric entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to consume\n\t */\n\tasync consumeEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t\toptions?: {\n\t\t\tidempotencyKey?: string\n\t\t\tmetadata?: Record<string, any>\n\t\t},\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/consume`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t\t...options,\n\t\t})\n\t}\n\n\t/**\n\t * Add numeric entitlement (e.g. refund)\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to add\n\t */\n\tasync addEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/add`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Toggle boolean entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param enabled - Whether to enable\n\t */\n\tasync toggleEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tenabled: boolean,\n\t): Promise<{ isEnabled: boolean }> {\n\t\t// Toggle endpoint expects POST with enabled flag\n\t\t// However, looking at list_dir, we have toggle/route.ts\n\t\t// I should verify its contract, but assuming standard toggle pattern:\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/toggle`, {\n\t\t\tkey,\n\t\t\tenabled,\n\t\t})\n\t}\n\n\t/**\n\t * Generate checkout URL for client-side payment\n\t * @param productId - Product ID\n\t * @param priceId - Price ID\n\t * @returns Checkout page URL\n\t */\n\tgetCheckoutUrl(productId: string, priceId: string): string {\n\t\treturn `${this.checkoutUrl}/checkout/${this.appId}/${productId}/${priceId}`\n\t}\n\n\t/**\n\t * Verify a hosted login token and return the normalized login profile.\n\t * This request is signed with your app credentials and routed through the worker API.\n\t */\n\tasync verifyLoginToken(token: string): Promise<VerifiedLoginToken> {\n\t\tif (!token?.trim()) {\n\t\t\tthrow new Error(\"login token is required\")\n\t\t}\n\t\treturn this.request(\"POST\", \"/login/tokens/verify\", { token: token.trim() })\n\t}\n}\n"],"mappings":"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","names":["crypto"]}
package/dist/server.d.cts CHANGED
@@ -30,6 +30,7 @@ interface OrderDetails {
30
30
  name: string;
31
31
  description?: string;
32
32
  entitlements: ProductEntitlements;
33
+ metadata?: ProductMetadata | null;
33
34
  };
34
35
  }
35
36
  /**
@@ -49,6 +50,20 @@ interface ProductPrice {
49
50
  locale: string | null;
50
51
  isDefault: boolean;
51
52
  }
53
+ interface ProductSubscriptionPeriod {
54
+ value: number;
55
+ unit: "days" | "months" | "years";
56
+ }
57
+ interface ProductResetRule {
58
+ resetInterval: "month";
59
+ }
60
+ interface ProductMetadata {
61
+ subscriptionPeriod?: ProductSubscriptionPeriod;
62
+ expiringEntitlements?: string[];
63
+ resetEntitlements?: Record<string, ProductResetRule>;
64
+ autoAssignOnNewUser?: boolean;
65
+ trialDurationDays?: number;
66
+ }
52
67
  /**
53
68
  * Product Data
54
69
  */
@@ -60,6 +75,7 @@ interface Product {
60
75
  description?: string;
61
76
  entitlements: ProductEntitlements;
62
77
  prices: ProductPrice[];
78
+ metadata?: ProductMetadata | null;
63
79
  }
64
80
  /**
65
81
  * WeChat JSAPI Payment Parameters (for wx.requestPayment)
@@ -204,6 +220,31 @@ interface GetOrdersResponse {
204
220
  totalPages: number;
205
221
  };
206
222
  }
223
+ /**
224
+ * Entitlement Detail Item
225
+ */
226
+ interface EntitlementDetailItem {
227
+ type: string;
228
+ current: number | boolean;
229
+ limit?: number;
230
+ expiresAt?: string | null;
231
+ resetInterval?: string | null;
232
+ nextResetAt?: string | null;
233
+ sourceKind?: string | null;
234
+ }
235
+ /**
236
+ * Entitlement Detail - returned by getEntitlementsDetail
237
+ */
238
+ type EntitlementDetail = Record<string, EntitlementDetailItem>;
239
+ /**
240
+ * Ensure User With Trial Response
241
+ */
242
+ interface EnsureUserWithTrialResponse {
243
+ isNew: boolean;
244
+ trialAssigned: boolean;
245
+ trialProductCode?: string;
246
+ entitlements: EntitlementDetail;
247
+ }
207
248
  /**
208
249
  * Server-side Payment Client
209
250
  * 服务端支付客户端,用于创建订单、查询状态、解密回调
@@ -276,10 +317,21 @@ declare class PaymentClient {
276
317
  */
277
318
  getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse>;
278
319
  /**
279
- * Get all entitlements for a user
320
+ * Get user entitlements in the legacy flat shape.
280
321
  * @param userId - User ID
281
322
  */
282
323
  getEntitlements(userId: string): Promise<Record<string, any>>;
324
+ /**
325
+ * Get user entitlements with full details (type, expiry, reset config, source)
326
+ * @param userId - User ID
327
+ */
328
+ getEntitlementsDetail(userId: string): Promise<EntitlementDetail>;
329
+ /**
330
+ * Ensure user exists and auto-assign trial product if new user
331
+ * This should be called when user first logs in or registers
332
+ * @param userId - User ID
333
+ */
334
+ ensureUserWithTrial(userId: string): Promise<EnsureUserWithTrialResponse>;
283
335
  /**
284
336
  * Get a single entitlement value
285
337
  * @param userId - User ID
@@ -292,7 +344,10 @@ declare class PaymentClient {
292
344
  * @param key - Entitlement key
293
345
  * @param amount - Amount to consume
294
346
  */
295
- consumeEntitlement(userId: string, key: string, amount: number): Promise<{
347
+ consumeEntitlement(userId: string, key: string, amount: number, options?: {
348
+ idempotencyKey?: string;
349
+ metadata?: Record<string, any>;
350
+ }): Promise<{
296
351
  balance: number;
297
352
  }>;
298
353
  /**
@@ -327,4 +382,4 @@ declare class PaymentClient {
327
382
  verifyLoginToken(token: string): Promise<VerifiedLoginToken>;
328
383
  }
329
384
 
330
- export { type CreateMiniProgramOrderParams, type CreateOrderParams, type CreateOrderResponse, type GetOrdersParams, type GetOrdersResponse, type OrderDetails, type OrderListItem, type OrderStatus, type PaymentCallbackData, PaymentClient, type PaymentClientOptions, type PaymentNotification, type Product, type ProductEntitlements, type ProductPrice, type VerifiedLoginToken, type WechatJsapiPayParams };
385
+ export { type CreateMiniProgramOrderParams, type CreateOrderParams, type CreateOrderResponse, type EnsureUserWithTrialResponse, type EntitlementDetail, type EntitlementDetailItem, type GetOrdersParams, type GetOrdersResponse, type OrderDetails, type OrderListItem, type OrderStatus, type PaymentCallbackData, PaymentClient, type PaymentClientOptions, type PaymentNotification, type Product, type ProductEntitlements, type ProductMetadata, type ProductPrice, type ProductResetRule, type ProductSubscriptionPeriod, type VerifiedLoginToken, type WechatJsapiPayParams };
package/dist/server.d.ts CHANGED
@@ -30,6 +30,7 @@ interface OrderDetails {
30
30
  name: string;
31
31
  description?: string;
32
32
  entitlements: ProductEntitlements;
33
+ metadata?: ProductMetadata | null;
33
34
  };
34
35
  }
35
36
  /**
@@ -49,6 +50,20 @@ interface ProductPrice {
49
50
  locale: string | null;
50
51
  isDefault: boolean;
51
52
  }
53
+ interface ProductSubscriptionPeriod {
54
+ value: number;
55
+ unit: "days" | "months" | "years";
56
+ }
57
+ interface ProductResetRule {
58
+ resetInterval: "month";
59
+ }
60
+ interface ProductMetadata {
61
+ subscriptionPeriod?: ProductSubscriptionPeriod;
62
+ expiringEntitlements?: string[];
63
+ resetEntitlements?: Record<string, ProductResetRule>;
64
+ autoAssignOnNewUser?: boolean;
65
+ trialDurationDays?: number;
66
+ }
52
67
  /**
53
68
  * Product Data
54
69
  */
@@ -60,6 +75,7 @@ interface Product {
60
75
  description?: string;
61
76
  entitlements: ProductEntitlements;
62
77
  prices: ProductPrice[];
78
+ metadata?: ProductMetadata | null;
63
79
  }
64
80
  /**
65
81
  * WeChat JSAPI Payment Parameters (for wx.requestPayment)
@@ -204,6 +220,31 @@ interface GetOrdersResponse {
204
220
  totalPages: number;
205
221
  };
206
222
  }
223
+ /**
224
+ * Entitlement Detail Item
225
+ */
226
+ interface EntitlementDetailItem {
227
+ type: string;
228
+ current: number | boolean;
229
+ limit?: number;
230
+ expiresAt?: string | null;
231
+ resetInterval?: string | null;
232
+ nextResetAt?: string | null;
233
+ sourceKind?: string | null;
234
+ }
235
+ /**
236
+ * Entitlement Detail - returned by getEntitlementsDetail
237
+ */
238
+ type EntitlementDetail = Record<string, EntitlementDetailItem>;
239
+ /**
240
+ * Ensure User With Trial Response
241
+ */
242
+ interface EnsureUserWithTrialResponse {
243
+ isNew: boolean;
244
+ trialAssigned: boolean;
245
+ trialProductCode?: string;
246
+ entitlements: EntitlementDetail;
247
+ }
207
248
  /**
208
249
  * Server-side Payment Client
209
250
  * 服务端支付客户端,用于创建订单、查询状态、解密回调
@@ -276,10 +317,21 @@ declare class PaymentClient {
276
317
  */
277
318
  getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse>;
278
319
  /**
279
- * Get all entitlements for a user
320
+ * Get user entitlements in the legacy flat shape.
280
321
  * @param userId - User ID
281
322
  */
282
323
  getEntitlements(userId: string): Promise<Record<string, any>>;
324
+ /**
325
+ * Get user entitlements with full details (type, expiry, reset config, source)
326
+ * @param userId - User ID
327
+ */
328
+ getEntitlementsDetail(userId: string): Promise<EntitlementDetail>;
329
+ /**
330
+ * Ensure user exists and auto-assign trial product if new user
331
+ * This should be called when user first logs in or registers
332
+ * @param userId - User ID
333
+ */
334
+ ensureUserWithTrial(userId: string): Promise<EnsureUserWithTrialResponse>;
283
335
  /**
284
336
  * Get a single entitlement value
285
337
  * @param userId - User ID
@@ -292,7 +344,10 @@ declare class PaymentClient {
292
344
  * @param key - Entitlement key
293
345
  * @param amount - Amount to consume
294
346
  */
295
- consumeEntitlement(userId: string, key: string, amount: number): Promise<{
347
+ consumeEntitlement(userId: string, key: string, amount: number, options?: {
348
+ idempotencyKey?: string;
349
+ metadata?: Record<string, any>;
350
+ }): Promise<{
296
351
  balance: number;
297
352
  }>;
298
353
  /**
@@ -327,4 +382,4 @@ declare class PaymentClient {
327
382
  verifyLoginToken(token: string): Promise<VerifiedLoginToken>;
328
383
  }
329
384
 
330
- export { type CreateMiniProgramOrderParams, type CreateOrderParams, type CreateOrderResponse, type GetOrdersParams, type GetOrdersResponse, type OrderDetails, type OrderListItem, type OrderStatus, type PaymentCallbackData, PaymentClient, type PaymentClientOptions, type PaymentNotification, type Product, type ProductEntitlements, type ProductPrice, type VerifiedLoginToken, type WechatJsapiPayParams };
385
+ export { type CreateMiniProgramOrderParams, type CreateOrderParams, type CreateOrderResponse, type EnsureUserWithTrialResponse, type EntitlementDetail, type EntitlementDetailItem, type GetOrdersParams, type GetOrdersResponse, type OrderDetails, type OrderListItem, type OrderStatus, type PaymentCallbackData, PaymentClient, type PaymentClientOptions, type PaymentNotification, type Product, type ProductEntitlements, type ProductMetadata, type ProductPrice, type ProductResetRule, type ProductSubscriptionPeriod, type VerifiedLoginToken, type WechatJsapiPayParams };
package/dist/server.js CHANGED
@@ -153,12 +153,27 @@ var PaymentClient = class {
153
153
  return this.request("GET", path);
154
154
  }
155
155
  /**
156
- * Get all entitlements for a user
156
+ * Get user entitlements in the legacy flat shape.
157
157
  * @param userId - User ID
158
158
  */
159
159
  async getEntitlements(userId) {
160
160
  return this.request("GET", `/users/${userId}/entitlements`);
161
161
  }
162
+ /**
163
+ * Get user entitlements with full details (type, expiry, reset config, source)
164
+ * @param userId - User ID
165
+ */
166
+ async getEntitlementsDetail(userId) {
167
+ return this.request("GET", `/users/${userId}/entitlements/detail`);
168
+ }
169
+ /**
170
+ * Ensure user exists and auto-assign trial product if new user
171
+ * This should be called when user first logs in or registers
172
+ * @param userId - User ID
173
+ */
174
+ async ensureUserWithTrial(userId) {
175
+ return this.request("POST", `/users/${userId}/entitlements/bootstrap`, {});
176
+ }
162
177
  /**
163
178
  * Get a single entitlement value
164
179
  * @param userId - User ID
@@ -166,7 +181,7 @@ var PaymentClient = class {
166
181
  */
167
182
  async getEntitlementValue(userId, key) {
168
183
  const entitlements = await this.getEntitlements(userId);
169
- return entitlements[key];
184
+ return entitlements[key] ?? null;
170
185
  }
171
186
  /**
172
187
  * Consume numeric entitlement
@@ -174,10 +189,11 @@ var PaymentClient = class {
174
189
  * @param key - Entitlement key
175
190
  * @param amount - Amount to consume
176
191
  */
177
- async consumeEntitlement(userId, key, amount) {
192
+ async consumeEntitlement(userId, key, amount, options) {
178
193
  return this.request("POST", `/users/${userId}/entitlements/consume`, {
179
194
  key,
180
- amount
195
+ amount,
196
+ ...options
181
197
  });
182
198
  }
183
199
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Youidian Payment SDK - Server Module\n * 用于服务端集成,包含签名、订单创建、回调解密等功能\n */\n\nimport crypto from \"crypto\"\n\n/**\n * Order status response\n */\nexport interface OrderStatus {\n\torderId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tpaidAt?: string\n\tchannelTransactionId?: string\n}\n\n/**\n * Order details response (full order information)\n */\nexport interface OrderDetails {\n\torderId: string\n\tinternalId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tdescription?: string\n\tpaidAt?: string\n\tcreatedAt: string\n\tchannel?: string\n\tproduct?: {\n\t\tcode: string\n\t\ttype: string\n\t\tname: string\n\t\tdescription?: string\n\t\tentitlements: ProductEntitlements\n\t}\n}\n\n/**\n * Product Entitlements\n */\nexport interface ProductEntitlements {\n\t[key: string]: any\n}\n\n/**\n * Product Price\n */\nexport interface ProductPrice {\n\tid: string\n\tcurrency: string\n\tamount: number\n\tdisplayAmount: string\n\tlocale: string | null\n\tisDefault: boolean\n}\n\n/**\n * Product Data\n */\nexport interface Product {\n\tid: string\n\tcode: string\n\ttype: string\n\tname: string\n\tdescription?: string\n\tentitlements: ProductEntitlements\n\tprices: ProductPrice[]\n}\n\n/**\n * WeChat JSAPI Payment Parameters (for wx.requestPayment)\n */\nexport interface WechatJsapiPayParams {\n\tappId: string\n\ttimeStamp: string\n\tnonceStr: string\n\tpackage: string\n\tsignType: \"RSA\"\n\tpaySign: string\n}\n\n/**\n * Verified hosted login token payload.\n */\nexport interface VerifiedLoginToken {\n\tappId: string\n\tuserId: string\n\tlegacyCasdoorId?: string | null\n\tchannel: string\n\temail?: string | null\n\tname?: string | null\n\tavatar?: string | null\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n\texpiresAt: string\n}\n\n/**\n * SDK Client Options\n */\nexport interface PaymentClientOptions {\n\t/** Application ID (Required) */\n\tappId: string\n\t/** Application Secret (Required for server-side operations) */\n\tappSecret: string\n\n\t/**\n\t * @deprecated Use apiUrl and checkoutUrl instead\n\t * API Base URL (e.g. https://pay.youidian.com)\n\t * If apiUrl or checkoutUrl is not provided, this will be used as fallback\n\t * Default: https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * API server URL for backend requests (e.g. https://api.youidian.com)\n\t * Default: https://pay-api.imgto.link\n\t */\n\tapiUrl?: string\n\n\t/**\n\t * Checkout page URL for client-side payment (e.g. https://pay.youidian.com)\n\t * Default: https://pay.imgto.link\n\t */\n\tcheckoutUrl?: string\n}\n\n/**\n * Create Order Parameters\n */\nexport interface CreateOrderParams {\n\tproductId?: string\n\tpriceId?: string\n\tchannel?: string\n\tuserId: string\n\treturnUrl?: string\n\tmetadata?: Record<string, any>\n\tmerchantOrderId?: string\n\topenid?: string\n\tlocale?: string\n}\n\n/**\n * Create WeChat Mini Program Order Parameters\n */\nexport type CreateMiniProgramOrderParams = {\n\tuserId: string\n\topenid: string\n\tmerchantOrderId?: string\n\tpriceId: string\n}\n\n/**\n * Create Order Response\n */\nexport interface CreateOrderResponse {\n\torderId: string\n\tinternalId: string\n\tamount: number\n\tcurrency: string\n\tpayParams: any\n}\n\n/**\n * Payment Callback Notification\n */\nexport interface PaymentNotification {\n\tiv: string\n\tencryptedData: string\n\tauthTag: string\n}\n\n/**\n * Decrypted Payment Callback Data\n */\nexport interface PaymentCallbackData {\n\torderId: string\n\tmerchantOrderId?: string\n\tstatus: \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tpaidAt: string\n\tchannelTransactionId?: string\n\tmetadata?: Record<string, any>\n}\n\n/**\n * Get Orders Parameters\n */\nexport interface GetOrdersParams {\n\tpage?: number\n\tpageSize?: number\n\tuserId?: string\n\tstatus?: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tstartDate?: string\n\tendDate?: string\n}\n\n/**\n * Order List Item\n */\nexport interface OrderListItem {\n\torderId: string\n\tinternalId: string\n\tmerchantUserId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tchannel?: string\n\tpaidAt?: string\n\tcreatedAt: string\n}\n\n/**\n * Get Orders Response\n */\nexport interface GetOrdersResponse {\n\torders: OrderListItem[]\n\tpagination: {\n\t\ttotal: number\n\t\tpage: number\n\t\tpageSize: number\n\t\ttotalPages: number\n\t}\n}\n\n/**\n * Server-side Payment Client\n * 服务端支付客户端,用于创建订单、查询状态、解密回调\n */\nexport class PaymentClient {\n\tprivate readonly appId: string\n\tprivate readonly appSecret: string\n\tprivate readonly apiUrl: string // 用于 API 调用\n\tprivate readonly checkoutUrl: string // 用于生成 checkout URL\n\n\tconstructor(options: PaymentClientOptions) {\n\t\tif (!options.appId) throw new Error(\"appId is required\")\n\t\tif (!options.appSecret) throw new Error(\"appSecret is required\")\n\n\t\tthis.appId = options.appId\n\t\tthis.appSecret = options.appSecret\n\n\t\t// apiUrl: 优先使用 apiUrl,其次 baseUrl,默认 https://pay-api.imgto.link\n\t\tconst apiUrl =\n\t\t\toptions.apiUrl || options.baseUrl || \"https://pay-api.imgto.link\"\n\t\tthis.apiUrl = apiUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\n\t\t// checkoutUrl: 优先使用 checkoutUrl,其次 baseUrl,默认 https://pay.imgto.link\n\t\tconst checkoutUrl =\n\t\t\toptions.checkoutUrl || options.baseUrl || \"https://pay.imgto.link\"\n\t\tthis.checkoutUrl = checkoutUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\t}\n\n\t/**\n\t * Generate SHA256 signature for the request\n\t * Logic: SHA256(appId + appSecret + timestamp)\n\t */\n\tprivate generateSignature(timestamp: number): string {\n\t\tconst str = `${this.appId}${this.appSecret}${timestamp}`\n\t\treturn crypto.createHash(\"sha256\").update(str).digest(\"hex\")\n\t}\n\n\t/**\n\t * Internal request helper for Gateway API\n\t */\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: any,\n\t): Promise<T> {\n\t\tconst timestamp = Date.now()\n\t\tconst signature = this.generateSignature(timestamp)\n\n\t\tconst url = `${this.apiUrl}/api/v1/gateway/${this.appId}${path}`\n\n\t\tconst headers: HeadersInit = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-Pay-Timestamp\": timestamp.toString(),\n\t\t\t\"X-Pay-Sign\": signature,\n\t\t}\n\n\t\tconst options: RequestInit = {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t}\n\n\t\tconst response = await fetch(url, options)\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text()\n\t\t\tthrow new Error(`Payment SDK Error (${response.status}): ${errorText}`)\n\t\t}\n\n\t\tconst json = await response.json()\n\t\tif (json.error) {\n\t\t\tthrow new Error(`Payment API Error: ${json.error}`)\n\t\t}\n\n\t\treturn json.data as T\n\t}\n\n\t/**\n\t * Decrypts the callback notification payload using AES-256-GCM.\n\t * @param notification - The encrypted notification from payment webhook\n\t * @returns Decrypted payment callback data\n\t */\n\tdecryptCallback(notification: PaymentNotification): PaymentCallbackData {\n\t\ttry {\n\t\t\tconst { iv, encryptedData, authTag } = notification\n\t\t\tconst key = crypto.createHash(\"sha256\").update(this.appSecret).digest()\n\t\t\tconst decipher = crypto.createDecipheriv(\n\t\t\t\t\"aes-256-gcm\",\n\t\t\t\tkey,\n\t\t\t\tBuffer.from(iv, \"hex\"),\n\t\t\t)\n\n\t\t\tdecipher.setAuthTag(Buffer.from(authTag, \"hex\"))\n\n\t\t\tlet decrypted = decipher.update(encryptedData, \"hex\", \"utf8\")\n\t\t\tdecrypted += decipher.final(\"utf8\")\n\n\t\t\treturn JSON.parse(decrypted)\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Failed to decrypt payment callback: Invalid secret or tampered data.\",\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Fetch products for the configured app.\n\t */\n\tasync getProducts(options?: {\n\t\tlocale?: string\n\t\tcurrency?: string\n\t}): Promise<Product[]> {\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.locale) params.append(\"locale\", options.locale)\n\t\tif (options?.currency) params.append(\"currency\", options.currency)\n\n\t\tconst path = params.toString()\n\t\t\t? `/products?${params.toString()}`\n\t\t\t: \"/products\"\n\t\treturn this.request(\"GET\", path)\n\t}\n\n\t/**\n\t * Create a new order\n\t * @param params - Order creation parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createOrder(params: CreateOrderParams): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", \"/orders\", params)\n\t}\n\n\t/**\n\t * Create a WeChat Mini Program order (channel fixed to WECHAT_MINI)\n\t * @param params - Mini program order parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createMiniProgramOrder(\n\t\tparams: CreateMiniProgramOrderParams,\n\t): Promise<CreateOrderResponse> {\n\t\tconst { openid, ...rest } = params\n\t\treturn this.request(\"POST\", \"/orders\", {\n\t\t\t...rest,\n\t\t\tchannel: \"WECHAT_MINI\",\n\t\t\topenid,\n\t\t\tmetadata: { openid },\n\t\t} satisfies CreateOrderParams)\n\t}\n\n\t/**\n\t * Pay for an existing order\n\t * @param orderId - The order ID to pay\n\t * @param params - Payment parameters including channel\n\t */\n\tasync payOrder(\n\t\torderId: string,\n\t\tparams: {\n\t\t\tchannel: string\n\t\t\treturnUrl?: string\n\t\t\topenid?: string\n\t\t\t[key: string]: any\n\t\t},\n\t): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", `/orders/${orderId}/pay`, params)\n\t}\n\n\t/**\n\t * Query order status\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderStatus(orderId: string): Promise<OrderStatus> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}`)\n\t}\n\n\t/**\n\t * Get order details (full order information)\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderDetails(orderId: string): Promise<OrderDetails> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}/details`)\n\t}\n\n\t/**\n\t * Get orders list with pagination\n\t * @param params - Query parameters (pagination, filters)\n\t * @returns Orders list and pagination info\n\t */\n\tasync getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse> {\n\t\tconst queryParams = new URLSearchParams()\n\t\tif (params?.page) queryParams.append(\"page\", params.page.toString())\n\t\tif (params?.pageSize)\n\t\t\tqueryParams.append(\"pageSize\", params.pageSize.toString())\n\t\tif (params?.userId) queryParams.append(\"userId\", params.userId)\n\t\tif (params?.status) queryParams.append(\"status\", params.status)\n\t\tif (params?.startDate) queryParams.append(\"startDate\", params.startDate)\n\t\tif (params?.endDate) queryParams.append(\"endDate\", params.endDate)\n\n\t\tconst path = queryParams.toString()\n\t\t\t? `/orders?${queryParams.toString()}`\n\t\t\t: \"/orders\"\n\t\treturn this.request<GetOrdersResponse>(\"GET\", path)\n\t}\n\n\t/**\n\t * Get all entitlements for a user\n\t * @param userId - User ID\n\t */\n\tasync getEntitlements(userId: string): Promise<Record<string, any>> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements`)\n\t}\n\n\t/**\n\t * Get a single entitlement value\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t */\n\tasync getEntitlementValue(userId: string, key: string): Promise<any> {\n\t\tconst entitlements = await this.getEntitlements(userId)\n\t\treturn entitlements[key]\n\t}\n\n\t/**\n\t * Consume numeric entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to consume\n\t */\n\tasync consumeEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/consume`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Add numeric entitlement (e.g. refund)\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to add\n\t */\n\tasync addEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/add`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Toggle boolean entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param enabled - Whether to enable\n\t */\n\tasync toggleEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tenabled: boolean,\n\t): Promise<{ isEnabled: boolean }> {\n\t\t// Toggle endpoint expects POST with enabled flag\n\t\t// However, looking at list_dir, we have toggle/route.ts\n\t\t// I should verify its contract, but assuming standard toggle pattern:\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/toggle`, {\n\t\t\tkey,\n\t\t\tenabled,\n\t\t})\n\t}\n\n\t/**\n\t * Generate checkout URL for client-side payment\n\t * @param productId - Product ID\n\t * @param priceId - Price ID\n\t * @returns Checkout page URL\n\t */\n\tgetCheckoutUrl(productId: string, priceId: string): string {\n\t\treturn `${this.checkoutUrl}/checkout/${this.appId}/${productId}/${priceId}`\n\t}\n\n\t/**\n\t * Verify a hosted login token and return the normalized login profile.\n\t * This request is signed with your app credentials and routed through the worker API.\n\t */\n\tasync verifyLoginToken(token: string): Promise<VerifiedLoginToken> {\n\t\tif (!token?.trim()) {\n\t\t\tthrow new Error(\"login token is required\")\n\t\t}\n\t\treturn this.request(\"POST\", \"/login/tokens/verify\", { token: token.trim() })\n\t}\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Youidian Payment SDK - Server Module\n * 用于服务端集成,包含签名、订单创建、回调解密等功能\n */\n\nimport crypto from \"crypto\"\n\n/**\n * Order status response\n */\nexport interface OrderStatus {\n\torderId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tpaidAt?: string\n\tchannelTransactionId?: string\n}\n\n/**\n * Order details response (full order information)\n */\nexport interface OrderDetails {\n\torderId: string\n\tinternalId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tdescription?: string\n\tpaidAt?: string\n\tcreatedAt: string\n\tchannel?: string\n\tproduct?: {\n\t\tcode: string\n\t\ttype: string\n\t\tname: string\n\t\tdescription?: string\n\t\tentitlements: ProductEntitlements\n\t\tmetadata?: ProductMetadata | null\n\t}\n}\n\n/**\n * Product Entitlements\n */\nexport interface ProductEntitlements {\n\t[key: string]: any\n}\n\n/**\n * Product Price\n */\nexport interface ProductPrice {\n\tid: string\n\tcurrency: string\n\tamount: number\n\tdisplayAmount: string\n\tlocale: string | null\n\tisDefault: boolean\n}\n\nexport interface ProductSubscriptionPeriod {\n\tvalue: number\n\tunit: \"days\" | \"months\" | \"years\"\n}\n\nexport interface ProductResetRule {\n\tresetInterval: \"month\"\n}\n\nexport interface ProductMetadata {\n\tsubscriptionPeriod?: ProductSubscriptionPeriod\n\texpiringEntitlements?: string[]\n\tresetEntitlements?: Record<string, ProductResetRule>\n\tautoAssignOnNewUser?: boolean\n\ttrialDurationDays?: number\n}\n\n/**\n * Product Data\n */\nexport interface Product {\n\tid: string\n\tcode: string\n\ttype: string\n\tname: string\n\tdescription?: string\n\tentitlements: ProductEntitlements\n\tprices: ProductPrice[]\n\tmetadata?: ProductMetadata | null\n}\n\n/**\n * WeChat JSAPI Payment Parameters (for wx.requestPayment)\n */\nexport interface WechatJsapiPayParams {\n\tappId: string\n\ttimeStamp: string\n\tnonceStr: string\n\tpackage: string\n\tsignType: \"RSA\"\n\tpaySign: string\n}\n\n/**\n * Verified hosted login token payload.\n */\nexport interface VerifiedLoginToken {\n\tappId: string\n\tuserId: string\n\tlegacyCasdoorId?: string | null\n\tchannel: string\n\temail?: string | null\n\tname?: string | null\n\tavatar?: string | null\n\twechatOpenId?: string | null\n\twechatUnionId?: string | null\n\texpiresAt: string\n}\n\n/**\n * SDK Client Options\n */\nexport interface PaymentClientOptions {\n\t/** Application ID (Required) */\n\tappId: string\n\t/** Application Secret (Required for server-side operations) */\n\tappSecret: string\n\n\t/**\n\t * @deprecated Use apiUrl and checkoutUrl instead\n\t * API Base URL (e.g. https://pay.youidian.com)\n\t * If apiUrl or checkoutUrl is not provided, this will be used as fallback\n\t * Default: https://pay.imgto.link\n\t */\n\tbaseUrl?: string\n\n\t/**\n\t * API server URL for backend requests (e.g. https://api.youidian.com)\n\t * Default: https://pay-api.imgto.link\n\t */\n\tapiUrl?: string\n\n\t/**\n\t * Checkout page URL for client-side payment (e.g. https://pay.youidian.com)\n\t * Default: https://pay.imgto.link\n\t */\n\tcheckoutUrl?: string\n}\n\n/**\n * Create Order Parameters\n */\nexport interface CreateOrderParams {\n\tproductId?: string\n\tpriceId?: string\n\tchannel?: string\n\tuserId: string\n\treturnUrl?: string\n\tmetadata?: Record<string, any>\n\tmerchantOrderId?: string\n\topenid?: string\n\tlocale?: string\n}\n\n/**\n * Create WeChat Mini Program Order Parameters\n */\nexport type CreateMiniProgramOrderParams = {\n\tuserId: string\n\topenid: string\n\tmerchantOrderId?: string\n\tpriceId: string\n}\n\n/**\n * Create Order Response\n */\nexport interface CreateOrderResponse {\n\torderId: string\n\tinternalId: string\n\tamount: number\n\tcurrency: string\n\tpayParams: any\n}\n\n/**\n * Payment Callback Notification\n */\nexport interface PaymentNotification {\n\tiv: string\n\tencryptedData: string\n\tauthTag: string\n}\n\n/**\n * Decrypted Payment Callback Data\n */\nexport interface PaymentCallbackData {\n\torderId: string\n\tmerchantOrderId?: string\n\tstatus: \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tpaidAt: string\n\tchannelTransactionId?: string\n\tmetadata?: Record<string, any>\n}\n\n/**\n * Get Orders Parameters\n */\nexport interface GetOrdersParams {\n\tpage?: number\n\tpageSize?: number\n\tuserId?: string\n\tstatus?: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tstartDate?: string\n\tendDate?: string\n}\n\n/**\n * Order List Item\n */\nexport interface OrderListItem {\n\torderId: string\n\tinternalId: string\n\tmerchantUserId: string\n\tstatus: \"PENDING\" | \"PAID\" | \"CANCELLED\" | \"REFUNDED\" | \"FAILED\"\n\tamount: number\n\tcurrency: string\n\tchannel?: string\n\tpaidAt?: string\n\tcreatedAt: string\n}\n\n/**\n * Get Orders Response\n */\nexport interface GetOrdersResponse {\n\torders: OrderListItem[]\n\tpagination: {\n\t\ttotal: number\n\t\tpage: number\n\t\tpageSize: number\n\t\ttotalPages: number\n\t}\n}\n\n/**\n * Entitlement Detail Item\n */\nexport interface EntitlementDetailItem {\n\ttype: string\n\tcurrent: number | boolean\n\tlimit?: number\n\texpiresAt?: string | null\n\tresetInterval?: string | null\n\tnextResetAt?: string | null\n\tsourceKind?: string | null\n}\n\n/**\n * Entitlement Detail - returned by getEntitlementsDetail\n */\nexport type EntitlementDetail = Record<string, EntitlementDetailItem>\n\n/**\n * Ensure User With Trial Response\n */\nexport interface EnsureUserWithTrialResponse {\n\tisNew: boolean\n\ttrialAssigned: boolean\n\ttrialProductCode?: string\n\tentitlements: EntitlementDetail\n}\n\n/**\n * Server-side Payment Client\n * 服务端支付客户端,用于创建订单、查询状态、解密回调\n */\nexport class PaymentClient {\n\tprivate readonly appId: string\n\tprivate readonly appSecret: string\n\tprivate readonly apiUrl: string // 用于 API 调用\n\tprivate readonly checkoutUrl: string // 用于生成 checkout URL\n\n\tconstructor(options: PaymentClientOptions) {\n\t\tif (!options.appId) throw new Error(\"appId is required\")\n\t\tif (!options.appSecret) throw new Error(\"appSecret is required\")\n\n\t\tthis.appId = options.appId\n\t\tthis.appSecret = options.appSecret\n\n\t\t// apiUrl: 优先使用 apiUrl,其次 baseUrl,默认 https://pay-api.imgto.link\n\t\tconst apiUrl =\n\t\t\toptions.apiUrl || options.baseUrl || \"https://pay-api.imgto.link\"\n\t\tthis.apiUrl = apiUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\n\t\t// checkoutUrl: 优先使用 checkoutUrl,其次 baseUrl,默认 https://pay.imgto.link\n\t\tconst checkoutUrl =\n\t\t\toptions.checkoutUrl || options.baseUrl || \"https://pay.imgto.link\"\n\t\tthis.checkoutUrl = checkoutUrl.replace(/\\/$/, \"\") // Remove trailing slash\n\t}\n\n\t/**\n\t * Generate SHA256 signature for the request\n\t * Logic: SHA256(appId + appSecret + timestamp)\n\t */\n\tprivate generateSignature(timestamp: number): string {\n\t\tconst str = `${this.appId}${this.appSecret}${timestamp}`\n\t\treturn crypto.createHash(\"sha256\").update(str).digest(\"hex\")\n\t}\n\n\t/**\n\t * Internal request helper for Gateway API\n\t */\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: any,\n\t): Promise<T> {\n\t\tconst timestamp = Date.now()\n\t\tconst signature = this.generateSignature(timestamp)\n\n\t\tconst url = `${this.apiUrl}/api/v1/gateway/${this.appId}${path}`\n\n\t\tconst headers: HeadersInit = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-Pay-Timestamp\": timestamp.toString(),\n\t\t\t\"X-Pay-Sign\": signature,\n\t\t}\n\n\t\tconst options: RequestInit = {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t}\n\n\t\tconst response = await fetch(url, options)\n\n\t\tif (!response.ok) {\n\t\t\tconst errorText = await response.text()\n\t\t\tthrow new Error(`Payment SDK Error (${response.status}): ${errorText}`)\n\t\t}\n\n\t\tconst json = await response.json()\n\t\tif (json.error) {\n\t\t\tthrow new Error(`Payment API Error: ${json.error}`)\n\t\t}\n\n\t\treturn json.data as T\n\t}\n\n\t/**\n\t * Decrypts the callback notification payload using AES-256-GCM.\n\t * @param notification - The encrypted notification from payment webhook\n\t * @returns Decrypted payment callback data\n\t */\n\tdecryptCallback(notification: PaymentNotification): PaymentCallbackData {\n\t\ttry {\n\t\t\tconst { iv, encryptedData, authTag } = notification\n\t\t\tconst key = crypto.createHash(\"sha256\").update(this.appSecret).digest()\n\t\t\tconst decipher = crypto.createDecipheriv(\n\t\t\t\t\"aes-256-gcm\",\n\t\t\t\tkey,\n\t\t\t\tBuffer.from(iv, \"hex\"),\n\t\t\t)\n\n\t\t\tdecipher.setAuthTag(Buffer.from(authTag, \"hex\"))\n\n\t\t\tlet decrypted = decipher.update(encryptedData, \"hex\", \"utf8\")\n\t\t\tdecrypted += decipher.final(\"utf8\")\n\n\t\t\treturn JSON.parse(decrypted)\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Failed to decrypt payment callback: Invalid secret or tampered data.\",\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Fetch products for the configured app.\n\t */\n\tasync getProducts(options?: {\n\t\tlocale?: string\n\t\tcurrency?: string\n\t}): Promise<Product[]> {\n\t\tconst params = new URLSearchParams()\n\t\tif (options?.locale) params.append(\"locale\", options.locale)\n\t\tif (options?.currency) params.append(\"currency\", options.currency)\n\n\t\tconst path = params.toString()\n\t\t\t? `/products?${params.toString()}`\n\t\t\t: \"/products\"\n\t\treturn this.request(\"GET\", path)\n\t}\n\n\t/**\n\t * Create a new order\n\t * @param params - Order creation parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createOrder(params: CreateOrderParams): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", \"/orders\", params)\n\t}\n\n\t/**\n\t * Create a WeChat Mini Program order (channel fixed to WECHAT_MINI)\n\t * @param params - Mini program order parameters\n\t * @returns Order details with payment parameters\n\t */\n\tasync createMiniProgramOrder(\n\t\tparams: CreateMiniProgramOrderParams,\n\t): Promise<CreateOrderResponse> {\n\t\tconst { openid, ...rest } = params\n\t\treturn this.request(\"POST\", \"/orders\", {\n\t\t\t...rest,\n\t\t\tchannel: \"WECHAT_MINI\",\n\t\t\topenid,\n\t\t\tmetadata: { openid },\n\t\t} satisfies CreateOrderParams)\n\t}\n\n\t/**\n\t * Pay for an existing order\n\t * @param orderId - The order ID to pay\n\t * @param params - Payment parameters including channel\n\t */\n\tasync payOrder(\n\t\torderId: string,\n\t\tparams: {\n\t\t\tchannel: string\n\t\t\treturnUrl?: string\n\t\t\topenid?: string\n\t\t\t[key: string]: any\n\t\t},\n\t): Promise<CreateOrderResponse> {\n\t\treturn this.request(\"POST\", `/orders/${orderId}/pay`, params)\n\t}\n\n\t/**\n\t * Query order status\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderStatus(orderId: string): Promise<OrderStatus> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}`)\n\t}\n\n\t/**\n\t * Get order details (full order information)\n\t * @param orderId - The order ID to query\n\t */\n\tasync getOrderDetails(orderId: string): Promise<OrderDetails> {\n\t\treturn this.request(\"GET\", `/orders/${orderId}/details`)\n\t}\n\n\t/**\n\t * Get orders list with pagination\n\t * @param params - Query parameters (pagination, filters)\n\t * @returns Orders list and pagination info\n\t */\n\tasync getOrders(params?: GetOrdersParams): Promise<GetOrdersResponse> {\n\t\tconst queryParams = new URLSearchParams()\n\t\tif (params?.page) queryParams.append(\"page\", params.page.toString())\n\t\tif (params?.pageSize)\n\t\t\tqueryParams.append(\"pageSize\", params.pageSize.toString())\n\t\tif (params?.userId) queryParams.append(\"userId\", params.userId)\n\t\tif (params?.status) queryParams.append(\"status\", params.status)\n\t\tif (params?.startDate) queryParams.append(\"startDate\", params.startDate)\n\t\tif (params?.endDate) queryParams.append(\"endDate\", params.endDate)\n\n\t\tconst path = queryParams.toString()\n\t\t\t? `/orders?${queryParams.toString()}`\n\t\t\t: \"/orders\"\n\t\treturn this.request<GetOrdersResponse>(\"GET\", path)\n\t}\n\n\t/**\n\t * Get user entitlements in the legacy flat shape.\n\t * @param userId - User ID\n\t */\n\tasync getEntitlements(userId: string): Promise<Record<string, any>> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements`)\n\t}\n\n\t/**\n\t * Get user entitlements with full details (type, expiry, reset config, source)\n\t * @param userId - User ID\n\t */\n\tasync getEntitlementsDetail(userId: string): Promise<EntitlementDetail> {\n\t\treturn this.request(\"GET\", `/users/${userId}/entitlements/detail`)\n\t}\n\n\t/**\n\t * Ensure user exists and auto-assign trial product if new user\n\t * This should be called when user first logs in or registers\n\t * @param userId - User ID\n\t */\n\tasync ensureUserWithTrial(\n\t\tuserId: string,\n\t): Promise<EnsureUserWithTrialResponse> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/bootstrap`, {})\n\t}\n\n\t/**\n\t * Get a single entitlement value\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t */\n\tasync getEntitlementValue(userId: string, key: string): Promise<any> {\n\t\tconst entitlements = await this.getEntitlements(userId)\n\t\treturn entitlements[key] ?? null\n\t}\n\n\t/**\n\t * Consume numeric entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to consume\n\t */\n\tasync consumeEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t\toptions?: {\n\t\t\tidempotencyKey?: string\n\t\t\tmetadata?: Record<string, any>\n\t\t},\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/consume`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t\t...options,\n\t\t})\n\t}\n\n\t/**\n\t * Add numeric entitlement (e.g. refund)\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param amount - Amount to add\n\t */\n\tasync addEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tamount: number,\n\t): Promise<{ balance: number }> {\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/add`, {\n\t\t\tkey,\n\t\t\tamount,\n\t\t})\n\t}\n\n\t/**\n\t * Toggle boolean entitlement\n\t * @param userId - User ID\n\t * @param key - Entitlement key\n\t * @param enabled - Whether to enable\n\t */\n\tasync toggleEntitlement(\n\t\tuserId: string,\n\t\tkey: string,\n\t\tenabled: boolean,\n\t): Promise<{ isEnabled: boolean }> {\n\t\t// Toggle endpoint expects POST with enabled flag\n\t\t// However, looking at list_dir, we have toggle/route.ts\n\t\t// I should verify its contract, but assuming standard toggle pattern:\n\t\treturn this.request(\"POST\", `/users/${userId}/entitlements/toggle`, {\n\t\t\tkey,\n\t\t\tenabled,\n\t\t})\n\t}\n\n\t/**\n\t * Generate checkout URL for client-side payment\n\t * @param productId - Product ID\n\t * @param priceId - Price ID\n\t * @returns Checkout page URL\n\t */\n\tgetCheckoutUrl(productId: string, priceId: string): string {\n\t\treturn `${this.checkoutUrl}/checkout/${this.appId}/${productId}/${priceId}`\n\t}\n\n\t/**\n\t * Verify a hosted login token and return the normalized login profile.\n\t * This request is signed with your app credentials and routed through the worker API.\n\t */\n\tasync verifyLoginToken(token: string): Promise<VerifiedLoginToken> {\n\t\tif (!token?.trim()) {\n\t\t\tthrow new Error(\"login token is required\")\n\t\t}\n\t\treturn this.request(\"POST\", \"/login/tokens/verify\", { token: token.trim() })\n\t}\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youidian/pay-sdk",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Youidian Payment SDK - 统一支付与登录集成 SDK",
5
5
  "author": "Youidian",
6
6
  "license": "MIT",