@linkfox-web/api-user-center 0.3.2 → 0.3.5

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/README.md CHANGED
@@ -20,7 +20,7 @@ pnpm add @linkfox-web/http @linkfox-web/api-user-center
20
20
 
21
21
  登录函数的第二个参数可传 `{ sourceUrl }`,默认取 `location.href`。`source`、`systemId`、`clientChannel` 可通过 `configure({ services: { 'user-center': { … } } })` 覆盖。
22
22
 
23
- ## 第三方应用 SSO(iframe)
23
+ ## 第三方应用 SSO(iframe / 整页跳转)
24
24
 
25
25
  这套 token 属于第三方应用服务,与上面的主站登录会话、刷新及团队同步相互独立。SDK 不依赖 React 或 Next.js。
26
26
 
@@ -56,12 +56,29 @@ const session = await startThirdPartySso({
56
56
  // 退出仅清除当前应用会话:clearThirdPartySsoSession(clientId)。
57
57
  ```
58
58
 
59
- 登录成功后 SDK 用 `@linkfox-web/http` 的 `setSession()` 保存会话(附带 `ssoClientId`,有效期取 JWT 的 `exp`),`@linkfox-web/ai-tools` 等服务会自动把 accessToken 放进 `Authorization`。这会替换当前站点的 http 会话,在主站域名下使用时请先 `configure({ cookieKey: 'LINKFOX_SSO_TOKEN' })` 之类的独立键,避免覆盖主站登录。`readThirdPartySsoSession(clientId)` 只认同一 clientId 写入的会话,过期或损坏时会清掉;`clearThirdPartySsoSession(clientId)` 不动其他来源的会话。无法保存或 token 缺少有效 exp 时登录失败。解码 exp 只用于本地过期判断,不代替服务端验签;前端 cookie 不具备 HttpOnly。
59
+ 登录成功后 SDK 用 `@linkfox-web/http` 的 `setSession()` 保存会话(附带 `ssoClientId`,有效期取 JWT 的 `exp`),`@linkfox-web/ai-tools` 等服务会自动把 accessToken 放进 `Authorization`。调用任一第三方 SSO 会话接口时,SDK 会把 http 会话切到 `` configure({ cookieKey: `LINKFOX_SSO_${clientId}_TOKEN`, storage: 'local' }) ``:每个应用存在自己的 localStorage 键,不写共享根域 cookie,同一域名下部署多个应用互不覆盖,也不会覆盖主站登录;因此同一页面不要混用主站登录与第三方 SSO。`readThirdPartySsoSession(clientId)` 只认同一 clientId 写入的会话,过期或损坏时会清掉;`clearThirdPartySsoSession(clientId)` 不动其他来源的会话。无法保存或 token 缺少有效 exp 时登录失败。解码 exp 只用于本地过期判断,不代替服务端验签;前端 cookie 不具备 HttpOnly。
60
60
 
61
61
  `getThirdPartyApplication(clientId, { redirectUri, signal })` 和 `loginThirdPartyApplication(params, { signal })` 可独立调用;后者会保存应用会话。SSO 接口本身均为匿名请求,不向 SSO 服务发送已有 token,不调用主站刷新接口;HTTP 200 中的 `{ error: { code, message } }` 也会作为异常抛出。
62
62
 
63
63
  `startThirdPartySso()` 会生成本次授权的随机 state,保留响应中的 PKCE 参数,校验回调 origin、路径、state 和 iframe 来源,最多消费一次 code;默认 5 分钟超时。回调必须与应用同源,根页面不能在回调处理前重定向丢掉查询参数。授权站点必须允许 iframe 嵌入,用户仍需自行完成其身份验证。
64
64
 
65
+ ### 整页跳转登录
66
+
67
+ 不想用 iframe 时,点击登录直接跳到授权页,登录完回到 `redirectUri`,再由应用回到登录前页面:
68
+
69
+ ```ts
70
+ import { completeThirdPartySsoRedirect, redirectToThirdPartySso } from '@linkfox-web/api-user-center'
71
+
72
+ // 应用启动时先处理回调;非回调页返回 null。
73
+ const result = await completeThirdPartySsoRedirect(clientId)
74
+ if (result && result.returnTo !== location.href) location.replace(result.returnTo)
75
+
76
+ // 登录按钮:跳走前不会 resolve 出会话,页面保持“正在跳转”即可。
77
+ await redirectToThirdPartySso({ clientId, redirectUri: window.location.origin })
78
+ ```
79
+
80
+ `redirectToThirdPartySso()` 使用与 iframe 流程相同的 redirectUri、HTTPS 与 PKCE 校验,生成随机 state,把 PKCE 参数与 `returnTo`(默认当前页面,非同源时回退为 redirectUri)存进当前标签页的 `sessionStorage`。`completeThirdPartySsoRedirect()` 只在地址同时带 `state` 和 `code`/`error` 时处理:先清掉回调参数并删除事务(code 只消费一次),无事务、state 不符、回调地址不符、超过 10 分钟或授权被拒都会抛错且不换 token。`sessionStorage` 不可用时直接报错。授权站点需允许顶层跳转回 redirectUri。
81
+
65
82
  本地接口仿真使用用户提供的契约及真实 `/get` 响应编写(CLI 未登录,未查询远端维护者 mock):
66
83
 
67
84
  ```bash
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var Pe=Object.create;var x=Object.defineProperty;var xe=Object.getOwnPropertyDescriptor;var Ce=Object.getOwnPropertyNames;var Ue=Object.getPrototypeOf,Ie=Object.prototype.hasOwnProperty;var be=(e,r)=>{for(var t in r)x(e,t,{get:r[t],enumerable:!0})},B=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Ce(r))!Ie.call(e,s)&&s!==t&&x(e,s,{get:()=>r[s],enumerable:!(n=xe(r,s))||n.enumerable});return e};var C=(e,r,t)=>(t=e!=null?Pe(Ue(e)):{},B(r||!e||!e.__esModule?x(t,"default",{value:e,enumerable:!0}):t,e)),ke=e=>B(x({},"__esModule",{value:!0}),e);var Oe={};be(Oe,{clearThirdPartySsoSession:()=>ge,getImageVerifyCode:()=>ie,getPersonalKey:()=>_,getThirdPartyApplication:()=>z,getUserInfo:()=>y,getVerifyCode:()=>ae,getWechatQrCode:()=>ue,getWechatQrCodeStatus:()=>de,handleThirdPartySsoCallback:()=>ye,loginByCode:()=>ce,loginByPassword:()=>se,loginByWechat:()=>fe,loginThirdPartyApplication:()=>F,logout:()=>G,readThirdPartySsoSession:()=>M,resolveCurrentTeam:()=>w,startThirdPartySso:()=>we,switchTeam:()=>J,syncApiKey:()=>Y,syncCurrentTeam:()=>m});module.exports=ke(Oe);var S=require("@linkfox-web/http");var I=require("@linkfox-web/http");var H={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var Ee=new Set(["30003","30009","30011","30015","30017","30019"]),j="LINKFOX_CLIENT_ID";function U(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function O(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let r=Math.random()*16|0;return(e==="x"?r:r&3|8).toString(16)})}function Ae(){try{let e=localStorage.getItem(j);return e||(e=O(),localStorage.setItem(j,e)),e}catch{return""}}function Le(e){try{return btoa(JSON.stringify({a_id:Ae(),d_id:e}))}catch{return""}}var c=(0,I.defineService)({name:"user-center",defaults:H,baseHeaders:e=>({source:e.config.source,uid:Le(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,r)=>e.status===401||U(r)&&r.success!==!0&&Ee.has(String(r.code??"")),refreshOnUnauthorized:!1,unwrap:(e,r)=>{if(U(r)&&(r.code==="OK"&&r.success!==!1||r.code===200&&r.success!==!1||r.success===!0)&&e.ok)return r.data;throw new I.LinkfoxHttpError({service:"user-center",status:e.status,code:U(r)?r.code:void 0,message:U(r)&&(r.message||r.msg)||e.statusText||`HTTP ${e.status}`,body:r})}});var p=require("@linkfox-web/http");var l=require("@linkfox-web/http");async function _(e){return(await c.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await ve()}}))?.credentials??[]}async function ve(){let e=(0,l.getCurrentTeam)()?.teamId;if(e)return e;let{teamList:r}=await y(),t=(w(r)??r.find(n=>n.isSelect===1)??r[0])?.teamId;if(!t)throw new l.LinkfoxHttpError({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return t}async function Y(e){let r=await _(e),t=r.find(n=>n.credentialType==="USER_PRIMARY"&&n.credential)??r.find(n=>n.credential);if(!t)throw new l.LinkfoxHttpError({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return N(t.credential),t.credential}function N(e){(0,l.configure)({services:{"ai-tools":{apiKey:e}}})}function y(){return $(!1)}function $(e){return c.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function w(e){if(!e?.length)return;let r=e.find(n=>n.isSelect===1);if(r?.agentMemberId)return r;let t=e.find(n=>n.teamType===1);return t?.agentMemberId?t:e.find(n=>n.agentMemberId)}function X(e){return e.aiTeamId?{id:e.aiTeamId,teamId:e.teamId,name:e.teamName,type:e.teamType,logo:e.logo??"",ownerId:e.ownerId}:null}async function m(e){let r=await $(e?.skipRecovery??!1),t=w(r.teamList),n=t?X(t):null;return n?((0,p.setTeam)(n),!0):!1}async function J(e){await c.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let r=await y(),t=r.teamList.find(s=>s.teamId===e),n=t?X(t):null;return n?(0,p.setTeam)(n):(0,p.clearTeam)(),r}function G(){(0,p.clearSession)(),N(void 0)}async function Re(e){let r=await c.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!r?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");(0,S.updateSession)({accessToken:r.accessToken,refreshToken:r.refreshToken,expireAt:r.expireAt})}function Z(){(0,S.registerCapability)("refresh",Re),(0,S.registerCapability)("resolveTeam",()=>m({skipRecovery:!0}))}var b=require("@linkfox-web/http");var ee=C(require("crypto-js/aes"),1),re=C(require("crypto-js/enc-utf8"),1),te=C(require("crypto-js/mode-ecb"),1),ne=C(require("crypto-js/pad-zeropadding"),1);function oe(e,r){return ee.default.encrypt(e,re.default.parse(r),{mode:te.default,padding:ne.default}).toString()}function D(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function W(e){if(!e?.accessToken)throw new b.LinkfoxHttpError({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});(0,b.setSession)(e);try{await m()}catch{}return e}function k(e,r){return c.request({method:"POST",path:e,body:r,auth:"none"})}async function ie(){let e=O(),r=await c.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),t=await new Promise((n,s)=>{let a=new FileReader;a.onloadend=()=>n(String(a.result??"")),a.onerror=()=>s(a.error),a.readAsDataURL(r)});return{uuid:e,imageBase64:t}}async function se(e,r){let t=c.config(),n=await k("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:D(r),data:{authPhone:e.authPhone,auth:oe(e.password,t.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:t.systemId}});return W(n)}async function ae(e){await k("/user/v3/web/login",{type:"sms",method:"getVerifyCode",data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",...e.uuid?{uuid:e.uuid}:{},...e.imageCode?{imageCode:e.imageCode}:{},clientChannel:c.config().clientChannel}})}async function ce(e,r){let t=await k("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:D(r),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return W(t)}function ue(){return c.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function de(e){return c.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function fe(e,r){let t=await k("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:D(r),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return W(t)}var E=require("@linkfox-web/http"),A=(0,E.defineService)({name:"third-party-sso",defaults:{baseURL:""},refreshOnUnauthorized:!1,unwrap:(e,r)=>{let t=r&&typeof r=="object"?r:void 0,n=t?.error&&typeof t.error=="object"?t.error:void 0;if(e.ok&&t&&!t.error)return t;throw new E.LinkfoxHttpError({service:"third-party-sso",status:e.status,code:typeof n?.code=="string"||typeof n?.code=="number"?n.code:e.status,message:typeof n?.message=="string"?n.message:`SSO 请求失败(HTTP ${e.status})`})}});var g=require("@linkfox-web/http");function K(e){if(!e.trim())throw new Error("缺少 SSO clientId")}function ge(e){K(e),(0,g.getSession)()?.ssoClientId===e&&(0,g.clearSession)()}function M(e){K(e);let r=(0,g.getSession)();if(!r||r.ssoClientId!==e)return null;try{if(typeof r.sub!="string"||!r.sub||typeof r.id!="string"||!r.id||typeof r.name!="string"||typeof r.expireAt!="number"||r.expireAt<=Date.now()||le(r.accessToken)!==r.expireAt)throw new Error("无效会话")}catch{return(0,g.clearSession)(),null}return{accessToken:r.accessToken,sub:r.sub,id:r.id,name:r.name,...typeof r.picture=="string"?{picture:r.picture}:{},expireAt:r.expireAt}}function le(e){try{let r=(e.split(".")[1]??"").replace(/-/g,"+").replace(/_/g,"/"),{exp:t}=JSON.parse(atob(r.padEnd(Math.ceil(r.length/4)*4,"=")));if(typeof t=="number"&&Number.isFinite(t)&&t*1e3>Date.now()&&t*1e3<=864e13)return t*1e3}catch{}throw new Error("SSO token 无效或已过期,请重新登录")}function pe(e,r){K(e);let t=r;if(!t||typeof t.accessToken!="string"||!t.accessToken||typeof t.sub!="string"||!t.sub||typeof t.id!="string"||!t.id||typeof t.name!="string")throw new Error("SSO 登录响应缺少用户信息或 accessToken");let n={accessToken:t.accessToken,sub:t.sub,id:t.id,name:t.name,...typeof t.picture=="string"?{picture:t.picture}:{},expireAt:le(t.accessToken)};if((0,g.setSession)({...n,userId:n.sub,ssoClientId:e}),M(e)?.accessToken!==n.accessToken)throw new Error("浏览器禁止保存登录信息,请允许本站 cookie 与本地存储后重试");return n}var he="linkfox:sso:callback";function me(e){if(!e.trim())throw new Error("缺少 SSO clientId");if(!A.config().baseURL)throw new Error("请配置 third-party-sso 的 baseURL")}async function z(e,r={}){me(e);let t=await A.request({method:"POST",path:"/thirdPartyApplication/get",body:{clientId:e,...r.redirectUri?{redirectUri:r.redirectUri}:{}},auth:"none",signal:r.signal});if(!["redirectUri","authorizationUrl","codeVerifier","appName"].every(n=>typeof t[n]=="string"&&t[n]))throw new Error("SSO 应用配置响应不完整");return t}async function F(e,r={}){if(me(e.clientId),![e.code,e.codeVerifier,e.redirectUri,e.appName].every(o=>typeof o=="string"&&o.trim()))throw new Error("SSO 换取 token 的参数不完整");r.signal?.throwIfAborted();let{clientId:t,redirectUri:n,codeVerifier:s,appName:a,code:T}=e,u=await A.request({method:"POST",path:"/thirdPartyApplication/login",body:{clientId:t,redirectUri:n,codeVerifier:s,appName:a,code:T},auth:"none",signal:r.signal});return r.signal?.throwIfAborted(),pe(t,u)}function ye(){if(typeof window>"u"||window.parent===window)return!1;let e=new URL(window.location.href);if(!e.searchParams.has("code")&&!e.searchParams.has("error"))return!1;window.parent.postMessage({type:he,url:e.href},e.origin);for(let r of["code","state","error","error_description"])e.searchParams.delete(r);return window.history.replaceState(null,"",e.href),!0}function we(e){if(typeof window>"u")return Promise.reject(new Error("SSO iframe 登录只能在浏览器中启动"));let r=new AbortController,t=r.signal;return new Promise((n,s)=>{let a=!1,T=!1,u,o,L="",P=e.timeoutMs??3e5;if(!Number.isFinite(P)||P<=0||P>2147483647){s(new Error("SSO 超时配置无效"));return}let V=()=>{clearTimeout(Se),window.removeEventListener("message",q),e.signal?.removeEventListener("abort",v),u=void 0},h=f=>{a||(a=!0,V(),r.abort(),s(f))},v=()=>h(new DOMException("登录已取消","AbortError")),q=f=>{if(a||T||!u||!o||f.origin!==o.origin||!f.source||f.source!==e.getFrameWindow())return;let i=f.data;if(!(!i||i.type!==he||typeof i.url!="string"))try{let d=new URL(i.url);if(d.origin!==o.origin||d.pathname!==o.pathname)throw new Error("SSO 回调地址与 redirectUri 不一致");for(let[R,Te]of o.searchParams)if(d.searchParams.get(R)!==Te)throw new Error("SSO 回调参数与 redirectUri 不一致");if(d.searchParams.getAll("state").length!==1||d.searchParams.get("state")!==L)throw new Error("SSO 回调 state 不匹配,请重新登录");if(d.searchParams.has("error"))throw new Error("授权未完成或已被拒绝,请重新登录");let Q=d.searchParams.get("code");if(!Q||d.searchParams.getAll("code").length!==1)throw new Error("SSO 回调缺少唯一 code");T=!0,F({clientId:e.clientId,redirectUri:u.redirectUri,codeVerifier:u.codeVerifier,appName:u.appName,code:Q},{signal:t}).then(R=>{a||(a=!0,V(),n(R))},h)}catch(d){h(d)}},Se=setTimeout(()=>h(new Error("登录超时,请重试;若授权页无法显示,请检查其 iframe 嵌入策略")),P);if(e.signal?.addEventListener("abort",v,{once:!0}),e.signal?.aborted){v();return}window.addEventListener("message",q),z(e.clientId,{redirectUri:e.redirectUri??window.location.origin,signal:t}).then(f=>{if(!a)try{if(u=f,o=new URL(u.redirectUri),o.origin!==window.location.origin||o.username||o.password||o.hash||!["http:","https:"].includes(o.protocol))throw new Error(`SSO redirectUri 为 ${o.origin}${o.pathname},请在该地址打开应用或修改服务端注册配置`);if(o.href!==new URL(e.redirectUri??window.location.origin).href)throw new Error("SSO 返回的 redirectUri 与请求不一致");let i=new URL(u.authorizationUrl);if(i.protocol!=="https:"||i.username||i.password)throw new Error("SSO authorizationUrl 必须是 HTTPS 地址");if(i.searchParams.get("redirect_uri")!==u.redirectUri||i.searchParams.get("code_challenge_method")!=="S256"||!i.searchParams.get("code_challenge"))throw new Error("SSO 授权地址缺少匹配的 redirect_uri 或 PKCE S256 参数");L=crypto.randomUUID(),i.searchParams.set("state",L),e.onAuthorization({authorizationUrl:i.href,appName:u.appName})}catch(i){h(i)}},h)})}Z();
1
+ "use strict";var Ee=Object.create;var x=Object.defineProperty;var Ae=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var be=Object.getPrototypeOf,ke=Object.prototype.hasOwnProperty;var Le=(e,r)=>{for(var t in r)x(e,t,{get:r[t],enumerable:!0})},q=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of Re(r))!ke.call(e,i)&&i!==t&&x(e,i,{get:()=>r[i],enumerable:!(n=Ae(r,i))||n.enumerable});return e};var C=(e,r,t)=>(t=e!=null?Ee(be(e)):{},q(r||!e||!e.__esModule?x(t,"default",{value:e,enumerable:!0}):t,e)),ve=e=>q(x({},"__esModule",{value:!0}),e);var We={};Le(We,{clearThirdPartySsoSession:()=>ue,completeThirdPartySsoRedirect:()=>Ce,getImageVerifyCode:()=>ne,getPersonalKey:()=>N,getThirdPartyApplication:()=>k,getUserInfo:()=>w,getVerifyCode:()=>oe,getWechatQrCode:()=>ae,getWechatQrCodeStatus:()=>ce,handleThirdPartySsoCallback:()=>we,loginByCode:()=>se,loginByPassword:()=>ie,loginByWechat:()=>de,loginThirdPartyApplication:()=>L,logout:()=>$,readThirdPartySsoSession:()=>M,redirectToThirdPartySso:()=>xe,resolveCurrentTeam:()=>S,startThirdPartySso:()=>Se,switchTeam:()=>Y,syncApiKey:()=>H,syncCurrentTeam:()=>y});module.exports=ve(We);var T=require("@linkfox-web/http");var I=require("@linkfox-web/http");var B={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var Oe=new Set(["30003","30009","30011","30015","30017","30019"]),Q="LINKFOX_CLIENT_ID";function U(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function _(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let r=Math.random()*16|0;return(e==="x"?r:r&3|8).toString(16)})}function _e(){try{let e=localStorage.getItem(Q);return e||(e=_(),localStorage.setItem(Q,e)),e}catch{return""}}function Ne(e){try{return btoa(JSON.stringify({a_id:_e(),d_id:e}))}catch{return""}}var s=(0,I.defineService)({name:"user-center",defaults:B,baseHeaders:e=>({source:e.config.source,uid:Ne(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,r)=>e.status===401||U(r)&&r.success!==!0&&Oe.has(String(r.code??"")),refreshOnUnauthorized:!1,unwrap:(e,r)=>{if(U(r)&&(r.code==="OK"&&r.success!==!1||r.code===200&&r.success!==!1||r.success===!0)&&e.ok)return r.data;throw new I.LinkfoxHttpError({service:"user-center",status:e.status,code:U(r)?r.code:void 0,message:U(r)&&(r.message||r.msg)||e.statusText||`HTTP ${e.status}`,body:r})}});var p=require("@linkfox-web/http");var l=require("@linkfox-web/http");async function N(e){return(await s.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await De()}}))?.credentials??[]}async function De(){let e=(0,l.getCurrentTeam)()?.teamId;if(e)return e;let{teamList:r}=await w(),t=(S(r)??r.find(n=>n.isSelect===1)??r[0])?.teamId;if(!t)throw new l.LinkfoxHttpError({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return t}async function H(e){let r=await N(e),t=r.find(n=>n.credentialType==="USER_PRIMARY"&&n.credential)??r.find(n=>n.credential);if(!t)throw new l.LinkfoxHttpError({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return D(t.credential),t.credential}function D(e){(0,l.configure)({services:{"ai-tools":{apiKey:e}}})}function w(){return j(!1)}function j(e){return s.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function S(e){if(!e?.length)return;let r=e.find(n=>n.isSelect===1);if(r?.agentMemberId)return r;let t=e.find(n=>n.teamType===1);return t?.agentMemberId?t:e.find(n=>n.agentMemberId)}function X(e){return e.aiTeamId?{id:e.aiTeamId,teamId:e.teamId,name:e.teamName,type:e.teamType,logo:e.logo??"",ownerId:e.ownerId}:null}async function y(e){let r=await j(e?.skipRecovery??!1),t=S(r.teamList),n=t?X(t):null;return n?((0,p.setTeam)(n),!0):!1}async function Y(e){await s.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let r=await w(),t=r.teamList.find(i=>i.teamId===e),n=t?X(t):null;return n?(0,p.setTeam)(n):(0,p.clearTeam)(),r}function $(){(0,p.clearSession)(),D(void 0)}async function Ke(e){let r=await s.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!r?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");(0,T.updateSession)({accessToken:r.accessToken,refreshToken:r.refreshToken,expireAt:r.expireAt})}function J(){(0,T.registerCapability)("refresh",Ke),(0,T.registerCapability)("resolveTeam",()=>y({skipRecovery:!0}))}var E=require("@linkfox-web/http");var G=C(require("crypto-js/aes"),1),Z=C(require("crypto-js/enc-utf8"),1),ee=C(require("crypto-js/mode-ecb"),1),re=C(require("crypto-js/pad-zeropadding"),1);function te(e,r){return G.default.encrypt(e,Z.default.parse(r),{mode:ee.default,padding:re.default}).toString()}function K(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function V(e){if(!e?.accessToken)throw new E.LinkfoxHttpError({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});(0,E.setSession)(e);try{await y()}catch{}return e}function A(e,r){return s.request({method:"POST",path:e,body:r,auth:"none"})}async function ne(){let e=_(),r=await s.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),t=await new Promise((n,i)=>{let o=new FileReader;o.onloadend=()=>n(String(o.result??"")),o.onerror=()=>i(o.error),o.readAsDataURL(r)});return{uuid:e,imageBase64:t}}async function ie(e,r){let t=s.config(),n=await A("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:K(r),data:{authPhone:e.authPhone,auth:te(e.password,t.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:t.systemId}});return V(n)}async function oe(e){await A("/user/v3/web/login",{type:"sms",method:"getVerifyCode",data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",...e.uuid?{uuid:e.uuid}:{},...e.imageCode?{imageCode:e.imageCode}:{},clientChannel:s.config().clientChannel}})}async function se(e,r){let t=await A("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:K(r),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return V(t)}function ae(){return s.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function ce(e){return s.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function de(e,r){let t=await A("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:K(r),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return V(t)}var R=require("@linkfox-web/http"),b=(0,R.defineService)({name:"third-party-sso",defaults:{baseURL:""},refreshOnUnauthorized:!1,unwrap:(e,r)=>{let t=r&&typeof r=="object"?r:void 0,n=t?.error&&typeof t.error=="object"?t.error:void 0;if(e.ok&&t&&!t.error)return t;throw new R.LinkfoxHttpError({service:"third-party-sso",status:e.status,code:typeof n?.code=="string"||typeof n?.code=="number"?n.code:e.status,message:typeof n?.message=="string"?n.message:`SSO 请求失败(HTTP ${e.status})`})}});var c=require("@linkfox-web/http");function z(e){if(!e.trim())throw new Error("缺少 SSO clientId");(0,c.configure)({cookieKey:`LINKFOX_SSO_${e}_TOKEN`,storage:"local"})}function ue(e){z(e),(0,c.getSession)()?.ssoClientId===e&&(0,c.clearSession)()}function M(e){z(e);let r=(0,c.getSession)();if(!r||r.ssoClientId!==e)return null;try{if(typeof r.sub!="string"||!r.sub||typeof r.id!="string"||!r.id||typeof r.name!="string"||typeof r.expireAt!="number"||r.expireAt<=Date.now()||fe(r.accessToken)!==r.expireAt)throw new Error("无效会话")}catch{return(0,c.clearSession)(),null}return{accessToken:r.accessToken,sub:r.sub,id:r.id,name:r.name,...typeof r.picture=="string"?{picture:r.picture}:{},expireAt:r.expireAt}}function fe(e){try{let r=(e.split(".")[1]??"").replace(/-/g,"+").replace(/_/g,"/"),{exp:t}=JSON.parse(atob(r.padEnd(Math.ceil(r.length/4)*4,"=")));if(typeof t=="number"&&Number.isFinite(t)&&t*1e3>Date.now()&&t*1e3<=864e13)return t*1e3}catch{}throw new Error("SSO token 无效或已过期,请重新登录")}function ge(e,r){z(e);let t=r;if(!t||typeof t.accessToken!="string"||!t.accessToken||typeof t.sub!="string"||!t.sub||typeof t.id!="string"||!t.id||typeof t.name!="string")throw new Error("SSO 登录响应缺少用户信息或 accessToken");let n={accessToken:t.accessToken,sub:t.sub,id:t.id,name:t.name,...typeof t.picture=="string"?{picture:t.picture}:{},expireAt:fe(t.accessToken)};if((0,c.setSession)({...n,userId:n.sub,ssoClientId:e}),M(e)?.accessToken!==n.accessToken)throw new Error("浏览器禁止保存登录信息,请允许本站 cookie 与本地存储后重试");return n}var le="linkfox:sso:callback",Ve=["code","state","error","error_description"],pe="linkfox:sso:redirect:",ze=6e5;function he(e){if(!e.trim())throw new Error("缺少 SSO clientId");if(!b.config().baseURL)throw new Error("请配置 third-party-sso 的 baseURL")}async function k(e,r={}){he(e);let t=await b.request({method:"POST",path:"/thirdPartyApplication/get",body:{clientId:e,...r.redirectUri?{redirectUri:r.redirectUri}:{}},auth:"none",signal:r.signal});if(!["redirectUri","authorizationUrl","codeVerifier","appName"].every(n=>typeof t[n]=="string"&&t[n]))throw new Error("SSO 应用配置响应不完整");return t}async function L(e,r={}){if(he(e.clientId),![e.code,e.codeVerifier,e.redirectUri,e.appName].every(g=>typeof g=="string"&&g.trim()))throw new Error("SSO 换取 token 的参数不完整");r.signal?.throwIfAborted();let{clientId:t,redirectUri:n,codeVerifier:i,appName:o,code:f}=e,a=await b.request({method:"POST",path:"/thirdPartyApplication/login",body:{clientId:t,redirectUri:n,codeVerifier:i,appName:o,code:f},auth:"none",signal:r.signal});return r.signal?.throwIfAborted(),ge(t,a)}function me(e,r){let t=new URL(e.redirectUri);if(t.origin!==window.location.origin||t.username||t.password||t.hash||!["http:","https:"].includes(t.protocol))throw new Error(`SSO redirectUri 为 ${t.origin}${t.pathname},请在该地址打开应用或修改服务端注册配置`);if(t.href!==new URL(r).href)throw new Error("SSO 返回的 redirectUri 与请求不一致");let n=new URL(e.authorizationUrl);if(n.protocol!=="https:"||n.username||n.password)throw new Error("SSO authorizationUrl 必须是 HTTPS 地址");if(n.searchParams.get("redirect_uri")!==e.redirectUri||n.searchParams.get("code_challenge_method")!=="S256"||!n.searchParams.get("code_challenge"))throw new Error("SSO 授权地址缺少匹配的 redirect_uri 或 PKCE S256 参数");return{redirect:t,authorization:n}}function ye(e,r,t){if(e.origin!==r.origin||e.pathname!==r.pathname)throw new Error("SSO 回调地址与 redirectUri 不一致");for(let[i,o]of r.searchParams)if(e.searchParams.get(i)!==o)throw new Error("SSO 回调参数与 redirectUri 不一致");if(e.searchParams.getAll("state").length!==1||e.searchParams.get("state")!==t)throw new Error("SSO 回调 state 不匹配,请重新登录");if(e.searchParams.has("error"))throw new Error("授权未完成或已被拒绝,请重新登录");let n=e.searchParams.get("code");if(!n||e.searchParams.getAll("code").length!==1)throw new Error("SSO 回调缺少唯一 code");return n}function we(){if(typeof window>"u"||window.parent===window)return!1;let e=new URL(window.location.href);if(!e.searchParams.has("code")&&!e.searchParams.has("error"))return!1;window.parent.postMessage({type:le,url:e.href},e.origin);for(let r of["code","state","error","error_description"])e.searchParams.delete(r);return window.history.replaceState(null,"",e.href),!0}function Se(e){if(typeof window>"u")return Promise.reject(new Error("SSO iframe 登录只能在浏览器中启动"));let r=new AbortController,t=r.signal;return new Promise((n,i)=>{let o=!1,f=!1,a,g,v="",P=e.timeoutMs??3e5;if(!Number.isFinite(P)||P<=0||P>2147483647){i(new Error("SSO 超时配置无效"));return}let W=()=>{clearTimeout(Ue),window.removeEventListener("message",F),e.signal?.removeEventListener("abort",O),a=void 0},h=u=>{o||(o=!0,W(),r.abort(),i(u))},O=()=>h(new DOMException("登录已取消","AbortError")),F=u=>{if(o||f||!a||!g||u.origin!==g.origin||!u.source||u.source!==e.getFrameWindow())return;let d=u.data;if(!(!d||d.type!==le||typeof d.url!="string"))try{let m=ye(new URL(d.url),g,v);f=!0,L({clientId:e.clientId,redirectUri:a.redirectUri,codeVerifier:a.codeVerifier,appName:a.appName,code:m},{signal:t}).then(Ie=>{o||(o=!0,W(),n(Ie))},h)}catch(m){h(m)}},Ue=setTimeout(()=>h(new Error("登录超时,请重试;若授权页无法显示,请检查其 iframe 嵌入策略")),P);if(e.signal?.addEventListener("abort",O,{once:!0}),e.signal?.aborted){O();return}window.addEventListener("message",F),k(e.clientId,{redirectUri:e.redirectUri??window.location.origin,signal:t}).then(u=>{if(!o)try{a=u;let d=me(a,e.redirectUri??window.location.origin);g=d.redirect;let m=d.authorization;v=crypto.randomUUID(),m.searchParams.set("state",v),e.onAuthorization({authorizationUrl:m.href,appName:a.appName})}catch(d){h(d)}},h)})}function Te(){try{return window.sessionStorage}catch{throw new Error("浏览器禁止本地存储,无法跳转登录,请允许本站存储后重试")}}function Pe(e){if(e)try{let r=new URL(e,window.location.href);return r.origin===window.location.origin?r.href:void 0}catch{return}}function Me(e){let r=Te(),t=pe+e,n=r.getItem(t);if(r.removeItem(t),!n)return null;try{let i=JSON.parse(n);return!["state","codeVerifier","redirectUri","appName","returnTo"].every(o=>typeof i[o]=="string"&&i[o])||typeof i.createdAt!="number"?null:i}catch{return null}}async function xe(e){if(typeof window>"u")throw new Error("SSO 跳转登录只能在浏览器中启动");let r=Te(),t=e.redirectUri??window.location.origin,n=await k(e.clientId,{redirectUri:t,signal:e.signal}),{authorization:i}=me(n,t),o=crypto.randomUUID();i.searchParams.set("state",o);let f={state:o,codeVerifier:n.codeVerifier,redirectUri:n.redirectUri,appName:n.appName,returnTo:Pe(e.returnTo??window.location.href)??n.redirectUri,createdAt:Date.now()};e.signal?.throwIfAborted();try{r.setItem(pe+e.clientId,JSON.stringify(f))}catch{throw new Error("浏览器禁止本地存储,无法跳转登录,请允许本站存储后重试")}window.location.assign(i.href)}async function Ce(e,r={}){if(typeof window>"u")return null;let t=new URL(window.location.href);if(!t.searchParams.has("state")||!t.searchParams.has("code")&&!t.searchParams.has("error"))return null;let n=new URL(t.href);for(let a of Ve)n.searchParams.delete(a);window.history.replaceState(window.history.state,"",n.href);let i=Me(e);if(!i)throw new Error("登录请求已失效,请重新登录");if(Date.now()-i.createdAt>ze)throw new Error("登录超时,请重新登录");let o=ye(t,new URL(i.redirectUri),i.state);return{session:await L({clientId:e,redirectUri:i.redirectUri,codeVerifier:i.codeVerifier,appName:i.appName,code:o},r),returnTo:Pe(i.returnTo)??i.redirectUri}}J();
package/dist/index.d.cts CHANGED
@@ -188,6 +188,19 @@ interface ThirdPartySsoOptions {
188
188
  /** 整个授权事务的超时,默认 5 分钟。 */
189
189
  timeoutMs?: number;
190
190
  }
191
+ interface ThirdPartySsoRedirectOptions {
192
+ clientId: string;
193
+ /** 默认当前站点 origin,也可指定本站的回调路径。 */
194
+ redirectUri?: string;
195
+ /** 登录完成后回到的本站地址,默认当前页面;非同源地址回退为 redirectUri。 */
196
+ returnTo?: string;
197
+ signal?: AbortSignal;
198
+ }
199
+ interface ThirdPartySsoRedirectResult {
200
+ session: ThirdPartySsoSession;
201
+ /** 已校验同源,调用方按需跳回。 */
202
+ returnTo: string;
203
+ }
191
204
  declare function getThirdPartyApplication(clientId: string, options?: {
192
205
  redirectUri?: string;
193
206
  signal?: AbortSignal;
@@ -199,6 +212,15 @@ declare function loginThirdPartyApplication(params: ThirdPartyLoginParams, optio
199
212
  declare function handleThirdPartySsoCallback(): boolean;
200
213
  /** 每个调用方应取消旧事务后再启动新事务;SDK 不依赖任何 UI 框架。 */
201
214
  declare function startThirdPartySso(options: ThirdPartySsoOptions): Promise<ThirdPartySsoSession>;
215
+ /** 整页跳转到授权页;PKCE 与 state 存在当前标签页的 sessionStorage,回到 redirectUri 后由 completeThirdPartySsoRedirect 完成登录。 */
216
+ declare function redirectToThirdPartySso(options: ThirdPartySsoRedirectOptions): Promise<void>;
217
+ /**
218
+ * 应用启动时调用:当前地址不是跳转登录的回调时返回 null;是则清掉回调参数、校验并换取 token,返回会话与登录前页面。
219
+ * 无对应事务、state 不符或已过期一律抛错,不换 token。
220
+ */
221
+ declare function completeThirdPartySsoRedirect(clientId: string, options?: {
222
+ signal?: AbortSignal;
223
+ }): Promise<ThirdPartySsoRedirectResult | null>;
202
224
 
203
225
  interface ThirdPartySsoConfig {
204
226
  baseURL: string;
@@ -209,4 +231,4 @@ declare module '@linkfox-web/http' {
209
231
  }
210
232
  }
211
233
 
212
- export { type ApiCredential, type ImageVerifyCode, type LoginOptions, type LoginResult, type ThirdPartyApplication, type ThirdPartyLoginParams, type ThirdPartySsoConfig, type ThirdPartySsoOptions, type ThirdPartySsoSession, type UserCenterConfig, type UserInfo, type UserInfoTeam, type WechatQrCode, type WechatQrCodeStatus, clearThirdPartySsoSession, getImageVerifyCode, getPersonalKey, getThirdPartyApplication, getUserInfo, getVerifyCode, getWechatQrCode, getWechatQrCodeStatus, handleThirdPartySsoCallback, loginByCode, loginByPassword, loginByWechat, loginThirdPartyApplication, logout, readThirdPartySsoSession, resolveCurrentTeam, startThirdPartySso, switchTeam, syncApiKey, syncCurrentTeam };
234
+ export { type ApiCredential, type ImageVerifyCode, type LoginOptions, type LoginResult, type ThirdPartyApplication, type ThirdPartyLoginParams, type ThirdPartySsoConfig, type ThirdPartySsoOptions, type ThirdPartySsoRedirectOptions, type ThirdPartySsoRedirectResult, type ThirdPartySsoSession, type UserCenterConfig, type UserInfo, type UserInfoTeam, type WechatQrCode, type WechatQrCodeStatus, clearThirdPartySsoSession, completeThirdPartySsoRedirect, getImageVerifyCode, getPersonalKey, getThirdPartyApplication, getUserInfo, getVerifyCode, getWechatQrCode, getWechatQrCodeStatus, handleThirdPartySsoCallback, loginByCode, loginByPassword, loginByWechat, loginThirdPartyApplication, logout, readThirdPartySsoSession, redirectToThirdPartySso, resolveCurrentTeam, startThirdPartySso, switchTeam, syncApiKey, syncCurrentTeam };
package/dist/index.d.ts CHANGED
@@ -188,6 +188,19 @@ interface ThirdPartySsoOptions {
188
188
  /** 整个授权事务的超时,默认 5 分钟。 */
189
189
  timeoutMs?: number;
190
190
  }
191
+ interface ThirdPartySsoRedirectOptions {
192
+ clientId: string;
193
+ /** 默认当前站点 origin,也可指定本站的回调路径。 */
194
+ redirectUri?: string;
195
+ /** 登录完成后回到的本站地址,默认当前页面;非同源地址回退为 redirectUri。 */
196
+ returnTo?: string;
197
+ signal?: AbortSignal;
198
+ }
199
+ interface ThirdPartySsoRedirectResult {
200
+ session: ThirdPartySsoSession;
201
+ /** 已校验同源,调用方按需跳回。 */
202
+ returnTo: string;
203
+ }
191
204
  declare function getThirdPartyApplication(clientId: string, options?: {
192
205
  redirectUri?: string;
193
206
  signal?: AbortSignal;
@@ -199,6 +212,15 @@ declare function loginThirdPartyApplication(params: ThirdPartyLoginParams, optio
199
212
  declare function handleThirdPartySsoCallback(): boolean;
200
213
  /** 每个调用方应取消旧事务后再启动新事务;SDK 不依赖任何 UI 框架。 */
201
214
  declare function startThirdPartySso(options: ThirdPartySsoOptions): Promise<ThirdPartySsoSession>;
215
+ /** 整页跳转到授权页;PKCE 与 state 存在当前标签页的 sessionStorage,回到 redirectUri 后由 completeThirdPartySsoRedirect 完成登录。 */
216
+ declare function redirectToThirdPartySso(options: ThirdPartySsoRedirectOptions): Promise<void>;
217
+ /**
218
+ * 应用启动时调用:当前地址不是跳转登录的回调时返回 null;是则清掉回调参数、校验并换取 token,返回会话与登录前页面。
219
+ * 无对应事务、state 不符或已过期一律抛错,不换 token。
220
+ */
221
+ declare function completeThirdPartySsoRedirect(clientId: string, options?: {
222
+ signal?: AbortSignal;
223
+ }): Promise<ThirdPartySsoRedirectResult | null>;
202
224
 
203
225
  interface ThirdPartySsoConfig {
204
226
  baseURL: string;
@@ -209,4 +231,4 @@ declare module '@linkfox-web/http' {
209
231
  }
210
232
  }
211
233
 
212
- export { type ApiCredential, type ImageVerifyCode, type LoginOptions, type LoginResult, type ThirdPartyApplication, type ThirdPartyLoginParams, type ThirdPartySsoConfig, type ThirdPartySsoOptions, type ThirdPartySsoSession, type UserCenterConfig, type UserInfo, type UserInfoTeam, type WechatQrCode, type WechatQrCodeStatus, clearThirdPartySsoSession, getImageVerifyCode, getPersonalKey, getThirdPartyApplication, getUserInfo, getVerifyCode, getWechatQrCode, getWechatQrCodeStatus, handleThirdPartySsoCallback, loginByCode, loginByPassword, loginByWechat, loginThirdPartyApplication, logout, readThirdPartySsoSession, resolveCurrentTeam, startThirdPartySso, switchTeam, syncApiKey, syncCurrentTeam };
234
+ export { type ApiCredential, type ImageVerifyCode, type LoginOptions, type LoginResult, type ThirdPartyApplication, type ThirdPartyLoginParams, type ThirdPartySsoConfig, type ThirdPartySsoOptions, type ThirdPartySsoRedirectOptions, type ThirdPartySsoRedirectResult, type ThirdPartySsoSession, type UserCenterConfig, type UserInfo, type UserInfoTeam, type WechatQrCode, type WechatQrCodeStatus, clearThirdPartySsoSession, completeThirdPartySsoRedirect, getImageVerifyCode, getPersonalKey, getThirdPartyApplication, getUserInfo, getVerifyCode, getWechatQrCode, getWechatQrCodeStatus, handleThirdPartySsoCallback, loginByCode, loginByPassword, loginByWechat, loginThirdPartyApplication, logout, readThirdPartySsoSession, redirectToThirdPartySso, resolveCurrentTeam, startThirdPartySso, switchTeam, syncApiKey, syncCurrentTeam };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{registerCapability as M,updateSession as ge}from"@linkfox-web/http";import{defineService as Z,LinkfoxHttpError as ee}from"@linkfox-web/http";var R={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var re=new Set(["30003","30009","30011","30015","30017","30019"]),O="LINKFOX_CLIENT_ID";function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function U(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let r=Math.random()*16|0;return(e==="x"?r:r&3|8).toString(16)})}function te(){try{let e=localStorage.getItem(O);return e||(e=U(),localStorage.setItem(O,e)),e}catch{return""}}function ne(e){try{return btoa(JSON.stringify({a_id:te(),d_id:e}))}catch{return""}}var a=Z({name:"user-center",defaults:R,baseHeaders:e=>({source:e.config.source,uid:ne(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,r)=>e.status===401||m(r)&&r.success!==!0&&re.has(String(r.code??"")),refreshOnUnauthorized:!1,unwrap:(e,r)=>{if(m(r)&&(r.code==="OK"&&r.success!==!1||r.code===200&&r.success!==!1||r.success===!0)&&e.ok)return r.data;throw new ee({service:"user-center",status:e.status,code:m(r)?r.code:void 0,message:m(r)&&(r.message||r.msg)||e.statusText||`HTTP ${e.status}`,body:r})}});import{clearSession as ce,clearTeam as ue,setTeam as D}from"@linkfox-web/http";import{configure as oe,getCurrentTeam as ie,LinkfoxHttpError as _}from"@linkfox-web/http";async function N(e){return(await a.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await se()}}))?.credentials??[]}async function se(){let e=ie()?.teamId;if(e)return e;let{teamList:r}=await y(),t=(w(r)??r.find(n=>n.isSelect===1)??r[0])?.teamId;if(!t)throw new _({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return t}async function ae(e){let r=await N(e),t=r.find(n=>n.credentialType==="USER_PRIMARY"&&n.credential)??r.find(n=>n.credential);if(!t)throw new _({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return I(t.credential),t.credential}function I(e){oe({services:{"ai-tools":{apiKey:e}}})}function y(){return W(!1)}function W(e){return a.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function w(e){if(!e?.length)return;let r=e.find(n=>n.isSelect===1);if(r?.agentMemberId)return r;let t=e.find(n=>n.teamType===1);return t?.agentMemberId?t:e.find(n=>n.agentMemberId)}function K(e){return e.aiTeamId?{id:e.aiTeamId,teamId:e.teamId,name:e.teamName,type:e.teamType,logo:e.logo??"",ownerId:e.ownerId}:null}async function l(e){let r=await W(e?.skipRecovery??!1),t=w(r.teamList),n=t?K(t):null;return n?(D(n),!0):!1}async function de(e){await a.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let r=await y(),t=r.teamList.find(d=>d.teamId===e),n=t?K(t):null;return n?D(n):ue(),r}function fe(){ce(),I(void 0)}async function le(e){let r=await a.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!r?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");ge({accessToken:r.accessToken,refreshToken:r.refreshToken,expireAt:r.expireAt})}function z(){M("refresh",le),M("resolveTeam",()=>l({skipRecovery:!0}))}import{LinkfoxHttpError as we,setSession as Se}from"@linkfox-web/http";import pe from"crypto-js/aes";import he from"crypto-js/enc-utf8";import me from"crypto-js/mode-ecb";import ye from"crypto-js/pad-zeropadding";function F(e,r){return pe.encrypt(e,he.parse(r),{mode:me,padding:ye}).toString()}function b(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function k(e){if(!e?.accessToken)throw new we({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});Se(e);try{await l()}catch{}return e}function S(e,r){return a.request({method:"POST",path:e,body:r,auth:"none"})}async function Te(){let e=U(),r=await a.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),t=await new Promise((n,d)=>{let s=new FileReader;s.onloadend=()=>n(String(s.result??"")),s.onerror=()=>d(s.error),s.readAsDataURL(r)});return{uuid:e,imageBase64:t}}async function Pe(e,r){let t=a.config(),n=await S("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:b(r),data:{authPhone:e.authPhone,auth:F(e.password,t.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:t.systemId}});return k(n)}async function xe(e){await S("/user/v3/web/login",{type:"sms",method:"getVerifyCode",data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",...e.uuid?{uuid:e.uuid}:{},...e.imageCode?{imageCode:e.imageCode}:{},clientChannel:a.config().clientChannel}})}async function Ce(e,r){let t=await S("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:b(r),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return k(t)}function Ue(){return a.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function Ie(e){return a.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function be(e,r){let t=await S("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:b(r),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return k(t)}import{defineService as ke,LinkfoxHttpError as Ee}from"@linkfox-web/http";var T=ke({name:"third-party-sso",defaults:{baseURL:""},refreshOnUnauthorized:!1,unwrap:(e,r)=>{let t=r&&typeof r=="object"?r:void 0,n=t?.error&&typeof t.error=="object"?t.error:void 0;if(e.ok&&t&&!t.error)return t;throw new Ee({service:"third-party-sso",status:e.status,code:typeof n?.code=="string"||typeof n?.code=="number"?n.code:e.status,message:typeof n?.message=="string"?n.message:`SSO 请求失败(HTTP ${e.status})`})}});import{clearSession as V,getSession as q,setSession as Ae}from"@linkfox-web/http";function E(e){if(!e.trim())throw new Error("缺少 SSO clientId")}function Le(e){E(e),q()?.ssoClientId===e&&V()}function Q(e){E(e);let r=q();if(!r||r.ssoClientId!==e)return null;try{if(typeof r.sub!="string"||!r.sub||typeof r.id!="string"||!r.id||typeof r.name!="string"||typeof r.expireAt!="number"||r.expireAt<=Date.now()||B(r.accessToken)!==r.expireAt)throw new Error("无效会话")}catch{return V(),null}return{accessToken:r.accessToken,sub:r.sub,id:r.id,name:r.name,...typeof r.picture=="string"?{picture:r.picture}:{},expireAt:r.expireAt}}function B(e){try{let r=(e.split(".")[1]??"").replace(/-/g,"+").replace(/_/g,"/"),{exp:t}=JSON.parse(atob(r.padEnd(Math.ceil(r.length/4)*4,"=")));if(typeof t=="number"&&Number.isFinite(t)&&t*1e3>Date.now()&&t*1e3<=864e13)return t*1e3}catch{}throw new Error("SSO token 无效或已过期,请重新登录")}function H(e,r){E(e);let t=r;if(!t||typeof t.accessToken!="string"||!t.accessToken||typeof t.sub!="string"||!t.sub||typeof t.id!="string"||!t.id||typeof t.name!="string")throw new Error("SSO 登录响应缺少用户信息或 accessToken");let n={accessToken:t.accessToken,sub:t.sub,id:t.id,name:t.name,...typeof t.picture=="string"?{picture:t.picture}:{},expireAt:B(t.accessToken)};if(Ae({...n,userId:n.sub,ssoClientId:e}),Q(e)?.accessToken!==n.accessToken)throw new Error("浏览器禁止保存登录信息,请允许本站 cookie 与本地存储后重试");return n}var j="linkfox:sso:callback";function Y(e){if(!e.trim())throw new Error("缺少 SSO clientId");if(!T.config().baseURL)throw new Error("请配置 third-party-sso 的 baseURL")}async function $(e,r={}){Y(e);let t=await T.request({method:"POST",path:"/thirdPartyApplication/get",body:{clientId:e,...r.redirectUri?{redirectUri:r.redirectUri}:{}},auth:"none",signal:r.signal});if(!["redirectUri","authorizationUrl","codeVerifier","appName"].every(n=>typeof t[n]=="string"&&t[n]))throw new Error("SSO 应用配置响应不完整");return t}async function X(e,r={}){if(Y(e.clientId),![e.code,e.codeVerifier,e.redirectUri,e.appName].every(o=>typeof o=="string"&&o.trim()))throw new Error("SSO 换取 token 的参数不完整");r.signal?.throwIfAborted();let{clientId:t,redirectUri:n,codeVerifier:d,appName:s,code:p}=e,c=await T.request({method:"POST",path:"/thirdPartyApplication/login",body:{clientId:t,redirectUri:n,codeVerifier:d,appName:s,code:p},auth:"none",signal:r.signal});return r.signal?.throwIfAborted(),H(t,c)}function ve(){if(typeof window>"u"||window.parent===window)return!1;let e=new URL(window.location.href);if(!e.searchParams.has("code")&&!e.searchParams.has("error"))return!1;window.parent.postMessage({type:j,url:e.href},e.origin);for(let r of["code","state","error","error_description"])e.searchParams.delete(r);return window.history.replaceState(null,"",e.href),!0}function Re(e){if(typeof window>"u")return Promise.reject(new Error("SSO iframe 登录只能在浏览器中启动"));let r=new AbortController,t=r.signal;return new Promise((n,d)=>{let s=!1,p=!1,c,o,P="",h=e.timeoutMs??3e5;if(!Number.isFinite(h)||h<=0||h>2147483647){d(new Error("SSO 超时配置无效"));return}let A=()=>{clearTimeout(J),window.removeEventListener("message",L),e.signal?.removeEventListener("abort",x),c=void 0},g=f=>{s||(s=!0,A(),r.abort(),d(f))},x=()=>g(new DOMException("登录已取消","AbortError")),L=f=>{if(s||p||!c||!o||f.origin!==o.origin||!f.source||f.source!==e.getFrameWindow())return;let i=f.data;if(!(!i||i.type!==j||typeof i.url!="string"))try{let u=new URL(i.url);if(u.origin!==o.origin||u.pathname!==o.pathname)throw new Error("SSO 回调地址与 redirectUri 不一致");for(let[C,G]of o.searchParams)if(u.searchParams.get(C)!==G)throw new Error("SSO 回调参数与 redirectUri 不一致");if(u.searchParams.getAll("state").length!==1||u.searchParams.get("state")!==P)throw new Error("SSO 回调 state 不匹配,请重新登录");if(u.searchParams.has("error"))throw new Error("授权未完成或已被拒绝,请重新登录");let v=u.searchParams.get("code");if(!v||u.searchParams.getAll("code").length!==1)throw new Error("SSO 回调缺少唯一 code");p=!0,X({clientId:e.clientId,redirectUri:c.redirectUri,codeVerifier:c.codeVerifier,appName:c.appName,code:v},{signal:t}).then(C=>{s||(s=!0,A(),n(C))},g)}catch(u){g(u)}},J=setTimeout(()=>g(new Error("登录超时,请重试;若授权页无法显示,请检查其 iframe 嵌入策略")),h);if(e.signal?.addEventListener("abort",x,{once:!0}),e.signal?.aborted){x();return}window.addEventListener("message",L),$(e.clientId,{redirectUri:e.redirectUri??window.location.origin,signal:t}).then(f=>{if(!s)try{if(c=f,o=new URL(c.redirectUri),o.origin!==window.location.origin||o.username||o.password||o.hash||!["http:","https:"].includes(o.protocol))throw new Error(`SSO redirectUri 为 ${o.origin}${o.pathname},请在该地址打开应用或修改服务端注册配置`);if(o.href!==new URL(e.redirectUri??window.location.origin).href)throw new Error("SSO 返回的 redirectUri 与请求不一致");let i=new URL(c.authorizationUrl);if(i.protocol!=="https:"||i.username||i.password)throw new Error("SSO authorizationUrl 必须是 HTTPS 地址");if(i.searchParams.get("redirect_uri")!==c.redirectUri||i.searchParams.get("code_challenge_method")!=="S256"||!i.searchParams.get("code_challenge"))throw new Error("SSO 授权地址缺少匹配的 redirect_uri 或 PKCE S256 参数");P=crypto.randomUUID(),i.searchParams.set("state",P),e.onAuthorization({authorizationUrl:i.href,appName:c.appName})}catch(i){g(i)}},g)})}z();export{Le as clearThirdPartySsoSession,Te as getImageVerifyCode,N as getPersonalKey,$ as getThirdPartyApplication,y as getUserInfo,xe as getVerifyCode,Ue as getWechatQrCode,Ie as getWechatQrCodeStatus,ve as handleThirdPartySsoCallback,Ce as loginByCode,Pe as loginByPassword,be as loginByWechat,X as loginThirdPartyApplication,fe as logout,Q as readThirdPartySsoSession,w as resolveCurrentTeam,Re as startThirdPartySso,de as switchTeam,ae as syncApiKey,l as syncCurrentTeam};
1
+ import{registerCapability as z,updateSession as he}from"@linkfox-web/http";import{defineService as te,LinkfoxHttpError as ne}from"@linkfox-web/http";var v={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var ie=new Set(["30003","30009","30011","30015","30017","30019"]),O="LINKFOX_CLIENT_ID";function m(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function C(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let r=Math.random()*16|0;return(e==="x"?r:r&3|8).toString(16)})}function oe(){try{let e=localStorage.getItem(O);return e||(e=C(),localStorage.setItem(O,e)),e}catch{return""}}function se(e){try{return btoa(JSON.stringify({a_id:oe(),d_id:e}))}catch{return""}}var s=te({name:"user-center",defaults:v,baseHeaders:e=>({source:e.config.source,uid:se(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,r)=>e.status===401||m(r)&&r.success!==!0&&ie.has(String(r.code??"")),refreshOnUnauthorized:!1,unwrap:(e,r)=>{if(m(r)&&(r.code==="OK"&&r.success!==!1||r.code===200&&r.success!==!1||r.success===!0)&&e.ok)return r.data;throw new ne({service:"user-center",status:e.status,code:m(r)?r.code:void 0,message:m(r)&&(r.message||r.msg)||e.statusText||`HTTP ${e.status}`,body:r})}});import{clearSession as fe,clearTeam as ge,setTeam as D}from"@linkfox-web/http";import{configure as ae,getCurrentTeam as ce,LinkfoxHttpError as _}from"@linkfox-web/http";async function N(e){return(await s.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await de()}}))?.credentials??[]}async function de(){let e=ce()?.teamId;if(e)return e;let{teamList:r}=await y(),t=(w(r)??r.find(n=>n.isSelect===1)??r[0])?.teamId;if(!t)throw new _({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return t}async function ue(e){let r=await N(e),t=r.find(n=>n.credentialType==="USER_PRIMARY"&&n.credential)??r.find(n=>n.credential);if(!t)throw new _({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return U(t.credential),t.credential}function U(e){ae({services:{"ai-tools":{apiKey:e}}})}function y(){return K(!1)}function K(e){return s.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function w(e){if(!e?.length)return;let r=e.find(n=>n.isSelect===1);if(r?.agentMemberId)return r;let t=e.find(n=>n.teamType===1);return t?.agentMemberId?t:e.find(n=>n.agentMemberId)}function V(e){return e.aiTeamId?{id:e.aiTeamId,teamId:e.teamId,name:e.teamName,type:e.teamType,logo:e.logo??"",ownerId:e.ownerId}:null}async function p(e){let r=await K(e?.skipRecovery??!1),t=w(r.teamList),n=t?V(t):null;return n?(D(n),!0):!1}async function le(e){await s.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let r=await y(),t=r.teamList.find(i=>i.teamId===e),n=t?V(t):null;return n?D(n):ge(),r}function pe(){fe(),U(void 0)}async function me(e){let r=await s.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!r?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");he({accessToken:r.accessToken,refreshToken:r.refreshToken,expireAt:r.expireAt})}function M(){z("refresh",me),z("resolveTeam",()=>p({skipRecovery:!0}))}import{LinkfoxHttpError as Pe,setSession as xe}from"@linkfox-web/http";import ye from"crypto-js/aes";import we from"crypto-js/enc-utf8";import Se from"crypto-js/mode-ecb";import Te from"crypto-js/pad-zeropadding";function W(e,r){return ye.encrypt(e,we.parse(r),{mode:Se,padding:Te}).toString()}function I(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function E(e){if(!e?.accessToken)throw new Pe({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});xe(e);try{await p()}catch{}return e}function S(e,r){return s.request({method:"POST",path:e,body:r,auth:"none"})}async function Ce(){let e=C(),r=await s.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),t=await new Promise((n,i)=>{let o=new FileReader;o.onloadend=()=>n(String(o.result??"")),o.onerror=()=>i(o.error),o.readAsDataURL(r)});return{uuid:e,imageBase64:t}}async function Ue(e,r){let t=s.config(),n=await S("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:I(r),data:{authPhone:e.authPhone,auth:W(e.password,t.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:t.systemId}});return E(n)}async function Ie(e){await S("/user/v3/web/login",{type:"sms",method:"getVerifyCode",data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",...e.uuid?{uuid:e.uuid}:{},...e.imageCode?{imageCode:e.imageCode}:{},clientChannel:s.config().clientChannel}})}async function Ee(e,r){let t=await S("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:I(r),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return E(t)}function Ae(){return s.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function Re(e){return s.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function be(e,r){let t=await S("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:I(r),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return E(t)}import{defineService as ke,LinkfoxHttpError as Le}from"@linkfox-web/http";var T=ke({name:"third-party-sso",defaults:{baseURL:""},refreshOnUnauthorized:!1,unwrap:(e,r)=>{let t=r&&typeof r=="object"?r:void 0,n=t?.error&&typeof t.error=="object"?t.error:void 0;if(e.ok&&t&&!t.error)return t;throw new Le({service:"third-party-sso",status:e.status,code:typeof n?.code=="string"||typeof n?.code=="number"?n.code:e.status,message:typeof n?.message=="string"?n.message:`SSO 请求失败(HTTP ${e.status})`})}});import{clearSession as F,configure as ve,getSession as q,setSession as Oe}from"@linkfox-web/http";function A(e){if(!e.trim())throw new Error("缺少 SSO clientId");ve({cookieKey:`LINKFOX_SSO_${e}_TOKEN`,storage:"local"})}function _e(e){A(e),q()?.ssoClientId===e&&F()}function B(e){A(e);let r=q();if(!r||r.ssoClientId!==e)return null;try{if(typeof r.sub!="string"||!r.sub||typeof r.id!="string"||!r.id||typeof r.name!="string"||typeof r.expireAt!="number"||r.expireAt<=Date.now()||Q(r.accessToken)!==r.expireAt)throw new Error("无效会话")}catch{return F(),null}return{accessToken:r.accessToken,sub:r.sub,id:r.id,name:r.name,...typeof r.picture=="string"?{picture:r.picture}:{},expireAt:r.expireAt}}function Q(e){try{let r=(e.split(".")[1]??"").replace(/-/g,"+").replace(/_/g,"/"),{exp:t}=JSON.parse(atob(r.padEnd(Math.ceil(r.length/4)*4,"=")));if(typeof t=="number"&&Number.isFinite(t)&&t*1e3>Date.now()&&t*1e3<=864e13)return t*1e3}catch{}throw new Error("SSO token 无效或已过期,请重新登录")}function H(e,r){A(e);let t=r;if(!t||typeof t.accessToken!="string"||!t.accessToken||typeof t.sub!="string"||!t.sub||typeof t.id!="string"||!t.id||typeof t.name!="string")throw new Error("SSO 登录响应缺少用户信息或 accessToken");let n={accessToken:t.accessToken,sub:t.sub,id:t.id,name:t.name,...typeof t.picture=="string"?{picture:t.picture}:{},expireAt:Q(t.accessToken)};if(Oe({...n,userId:n.sub,ssoClientId:e}),B(e)?.accessToken!==n.accessToken)throw new Error("浏览器禁止保存登录信息,请允许本站 cookie 与本地存储后重试");return n}var j="linkfox:sso:callback",Ne=["code","state","error","error_description"],X="linkfox:sso:redirect:",De=6e5;function Y(e){if(!e.trim())throw new Error("缺少 SSO clientId");if(!T.config().baseURL)throw new Error("请配置 third-party-sso 的 baseURL")}async function R(e,r={}){Y(e);let t=await T.request({method:"POST",path:"/thirdPartyApplication/get",body:{clientId:e,...r.redirectUri?{redirectUri:r.redirectUri}:{}},auth:"none",signal:r.signal});if(!["redirectUri","authorizationUrl","codeVerifier","appName"].every(n=>typeof t[n]=="string"&&t[n]))throw new Error("SSO 应用配置响应不完整");return t}async function b(e,r={}){if(Y(e.clientId),![e.code,e.codeVerifier,e.redirectUri,e.appName].every(f=>typeof f=="string"&&f.trim()))throw new Error("SSO 换取 token 的参数不完整");r.signal?.throwIfAborted();let{clientId:t,redirectUri:n,codeVerifier:i,appName:o,code:u}=e,a=await T.request({method:"POST",path:"/thirdPartyApplication/login",body:{clientId:t,redirectUri:n,codeVerifier:i,appName:o,code:u},auth:"none",signal:r.signal});return r.signal?.throwIfAborted(),H(t,a)}function $(e,r){let t=new URL(e.redirectUri);if(t.origin!==window.location.origin||t.username||t.password||t.hash||!["http:","https:"].includes(t.protocol))throw new Error(`SSO redirectUri 为 ${t.origin}${t.pathname},请在该地址打开应用或修改服务端注册配置`);if(t.href!==new URL(r).href)throw new Error("SSO 返回的 redirectUri 与请求不一致");let n=new URL(e.authorizationUrl);if(n.protocol!=="https:"||n.username||n.password)throw new Error("SSO authorizationUrl 必须是 HTTPS 地址");if(n.searchParams.get("redirect_uri")!==e.redirectUri||n.searchParams.get("code_challenge_method")!=="S256"||!n.searchParams.get("code_challenge"))throw new Error("SSO 授权地址缺少匹配的 redirect_uri 或 PKCE S256 参数");return{redirect:t,authorization:n}}function J(e,r,t){if(e.origin!==r.origin||e.pathname!==r.pathname)throw new Error("SSO 回调地址与 redirectUri 不一致");for(let[i,o]of r.searchParams)if(e.searchParams.get(i)!==o)throw new Error("SSO 回调参数与 redirectUri 不一致");if(e.searchParams.getAll("state").length!==1||e.searchParams.get("state")!==t)throw new Error("SSO 回调 state 不匹配,请重新登录");if(e.searchParams.has("error"))throw new Error("授权未完成或已被拒绝,请重新登录");let n=e.searchParams.get("code");if(!n||e.searchParams.getAll("code").length!==1)throw new Error("SSO 回调缺少唯一 code");return n}function Ke(){if(typeof window>"u"||window.parent===window)return!1;let e=new URL(window.location.href);if(!e.searchParams.has("code")&&!e.searchParams.has("error"))return!1;window.parent.postMessage({type:j,url:e.href},e.origin);for(let r of["code","state","error","error_description"])e.searchParams.delete(r);return window.history.replaceState(null,"",e.href),!0}function Ve(e){if(typeof window>"u")return Promise.reject(new Error("SSO iframe 登录只能在浏览器中启动"));let r=new AbortController,t=r.signal;return new Promise((n,i)=>{let o=!1,u=!1,a,f,P="",h=e.timeoutMs??3e5;if(!Number.isFinite(h)||h<=0||h>2147483647){i(new Error("SSO 超时配置无效"));return}let k=()=>{clearTimeout(ee),window.removeEventListener("message",L),e.signal?.removeEventListener("abort",x),a=void 0},g=d=>{o||(o=!0,k(),r.abort(),i(d))},x=()=>g(new DOMException("登录已取消","AbortError")),L=d=>{if(o||u||!a||!f||d.origin!==f.origin||!d.source||d.source!==e.getFrameWindow())return;let c=d.data;if(!(!c||c.type!==j||typeof c.url!="string"))try{let l=J(new URL(c.url),f,P);u=!0,b({clientId:e.clientId,redirectUri:a.redirectUri,codeVerifier:a.codeVerifier,appName:a.appName,code:l},{signal:t}).then(re=>{o||(o=!0,k(),n(re))},g)}catch(l){g(l)}},ee=setTimeout(()=>g(new Error("登录超时,请重试;若授权页无法显示,请检查其 iframe 嵌入策略")),h);if(e.signal?.addEventListener("abort",x,{once:!0}),e.signal?.aborted){x();return}window.addEventListener("message",L),R(e.clientId,{redirectUri:e.redirectUri??window.location.origin,signal:t}).then(d=>{if(!o)try{a=d;let c=$(a,e.redirectUri??window.location.origin);f=c.redirect;let l=c.authorization;P=crypto.randomUUID(),l.searchParams.set("state",P),e.onAuthorization({authorizationUrl:l.href,appName:a.appName})}catch(c){g(c)}},g)})}function G(){try{return window.sessionStorage}catch{throw new Error("浏览器禁止本地存储,无法跳转登录,请允许本站存储后重试")}}function Z(e){if(e)try{let r=new URL(e,window.location.href);return r.origin===window.location.origin?r.href:void 0}catch{return}}function ze(e){let r=G(),t=X+e,n=r.getItem(t);if(r.removeItem(t),!n)return null;try{let i=JSON.parse(n);return!["state","codeVerifier","redirectUri","appName","returnTo"].every(o=>typeof i[o]=="string"&&i[o])||typeof i.createdAt!="number"?null:i}catch{return null}}async function Me(e){if(typeof window>"u")throw new Error("SSO 跳转登录只能在浏览器中启动");let r=G(),t=e.redirectUri??window.location.origin,n=await R(e.clientId,{redirectUri:t,signal:e.signal}),{authorization:i}=$(n,t),o=crypto.randomUUID();i.searchParams.set("state",o);let u={state:o,codeVerifier:n.codeVerifier,redirectUri:n.redirectUri,appName:n.appName,returnTo:Z(e.returnTo??window.location.href)??n.redirectUri,createdAt:Date.now()};e.signal?.throwIfAborted();try{r.setItem(X+e.clientId,JSON.stringify(u))}catch{throw new Error("浏览器禁止本地存储,无法跳转登录,请允许本站存储后重试")}window.location.assign(i.href)}async function We(e,r={}){if(typeof window>"u")return null;let t=new URL(window.location.href);if(!t.searchParams.has("state")||!t.searchParams.has("code")&&!t.searchParams.has("error"))return null;let n=new URL(t.href);for(let a of Ne)n.searchParams.delete(a);window.history.replaceState(window.history.state,"",n.href);let i=ze(e);if(!i)throw new Error("登录请求已失效,请重新登录");if(Date.now()-i.createdAt>De)throw new Error("登录超时,请重新登录");let o=J(t,new URL(i.redirectUri),i.state);return{session:await b({clientId:e,redirectUri:i.redirectUri,codeVerifier:i.codeVerifier,appName:i.appName,code:o},r),returnTo:Z(i.returnTo)??i.redirectUri}}M();export{_e as clearThirdPartySsoSession,We as completeThirdPartySsoRedirect,Ce as getImageVerifyCode,N as getPersonalKey,R as getThirdPartyApplication,y as getUserInfo,Ie as getVerifyCode,Ae as getWechatQrCode,Re as getWechatQrCodeStatus,Ke as handleThirdPartySsoCallback,Ee as loginByCode,Ue as loginByPassword,be as loginByWechat,b as loginThirdPartyApplication,pe as logout,B as readThirdPartySsoSession,Me as redirectToThirdPartySso,w as resolveCurrentTeam,Ve as startThirdPartySso,le as switchTeam,ue as syncApiKey,p as syncCurrentTeam};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkfox-web/api-user-center",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "LinkFox 用户中心接口:登录、刷新、用户信息、切换团队",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,14 +31,14 @@
31
31
  "./dist/index.cjs"
32
32
  ],
33
33
  "peerDependencies": {
34
- "@linkfox-web/http": "^0.3.2"
34
+ "@linkfox-web/http": "^0.3.5"
35
35
  },
36
36
  "dependencies": {
37
37
  "crypto-js": "4.2.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/crypto-js": "4.2.2",
41
- "@linkfox-web/http": "0.3.2"
41
+ "@linkfox-web/http": "0.3.5"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"