@linkfox-web/api-user-center 0.2.0 → 0.3.1
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 +53 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,3 +19,56 @@ pnpm add @linkfox-web/http @linkfox-web/api-user-center
|
|
|
19
19
|
| `logout()` | 清除本地会话,并清掉已注入的 API Key |
|
|
20
20
|
|
|
21
21
|
登录函数的第二个参数可传 `{ sourceUrl }`,默认取 `location.href`。`source`、`systemId`、`clientChannel` 可通过 `configure({ services: { 'user-center': { … } } })` 覆盖。
|
|
22
|
+
|
|
23
|
+
## 第三方应用 SSO(iframe)
|
|
24
|
+
|
|
25
|
+
这套 token 属于第三方应用服务,与上面的主站登录会话、刷新及团队同步相互独立。SDK 不依赖 React 或 Next.js。
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { configure } from '@linkfox-web/http'
|
|
29
|
+
import {
|
|
30
|
+
handleThirdPartySsoCallback, startThirdPartySso,
|
|
31
|
+
readThirdPartySsoSession, clearThirdPartySsoSession,
|
|
32
|
+
} from '@linkfox-web/api-user-center'
|
|
33
|
+
|
|
34
|
+
configure({ services: { 'third-party-sso': { baseURL: 'https://agent-studio-api.linkfox.vip' } } })
|
|
35
|
+
const clientId = 'your-application-client-id'
|
|
36
|
+
|
|
37
|
+
// 应用启动时先处理 iframe 回调;返回 true 时仅显示“正在完成登录”,不要跳转或加载业务页。
|
|
38
|
+
const isCallback = handleThirdPartySsoCallback()
|
|
39
|
+
if (!isCallback) {
|
|
40
|
+
const existing = readThirdPartySsoSession(clientId)
|
|
41
|
+
// existing 非空可恢复页面用户状态;否则在用户点击登录时调用下方示例。
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 以下代码放在登录按钮事件中,回调页不得执行:
|
|
45
|
+
const controller = new AbortController()
|
|
46
|
+
const session = await startThirdPartySso({
|
|
47
|
+
clientId,
|
|
48
|
+
redirectUri: window.location.origin, // /get 支持传回调地址,默认也是当前 origin
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
getFrameWindow: () => document.querySelector('iframe')?.contentWindow ?? null,
|
|
51
|
+
onAuthorization: ({ authorizationUrl, appName }) => {
|
|
52
|
+
// 使用自己的组件展示 iframe 并加载 authorizationUrl;仅向 UI 提供 URL 和应用名。
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
// 用户取消/组件销毁时 controller.abort();成功后关闭 iframe 并使用 session.name 等用户信息。
|
|
56
|
+
// 退出仅清除当前应用会话:clearThirdPartySsoSession(clientId)。
|
|
57
|
+
```
|
|
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。
|
|
60
|
+
|
|
61
|
+
`getThirdPartyApplication(clientId, { redirectUri, signal })` 和 `loginThirdPartyApplication(params, { signal })` 可独立调用;后者会保存应用会话。SSO 接口本身均为匿名请求,不向 SSO 服务发送已有 token,不调用主站刷新接口;HTTP 200 中的 `{ error: { code, message } }` 也会作为异常抛出。
|
|
62
|
+
|
|
63
|
+
`startThirdPartySso()` 会生成本次授权的随机 state,保留响应中的 PKCE 参数,校验回调 origin、路径、state 和 iframe 来源,最多消费一次 code;默认 5 分钟超时。回调必须与应用同源,根页面不能在回调处理前重定向丢掉查询参数。授权站点必须允许 iframe 嵌入,用户仍需自行完成其身份验证。
|
|
64
|
+
|
|
65
|
+
本地接口仿真使用用户提供的契约及真实 `/get` 响应编写(CLI 未登录,未查询远端维护者 mock):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
postman simulation run postman/simulations/third-party-sso.sim.yaml --port 4901 --output ndjson
|
|
69
|
+
# 在仓库根目录的另一个终端:
|
|
70
|
+
pnpm build
|
|
71
|
+
node scripts/check-sso-simulation.mjs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
将 SDK 的 `third-party-sso.baseURL` 指向 `http://127.0.0.1:4901/sso`;模拟授权码为 `simulation-code`,模拟 token 仅用于验证流程,不能调用真实服务。
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var V=Object.create;var l=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var G=(e,t)=>{for(var n in t)l(e,n,{get:t[n],enumerable:!0})},U=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of j(t))!Y.call(e,i)&&i!==n&&l(e,i,{get:()=>t[i],enumerable:!(r=z(t,i))||r.enumerable});return e};var m=(e,t,n)=>(n=e!=null?V(X(e)):{},U(t||!e||!e.__esModule?l(n,"default",{value:e,enumerable:!0}):n,e)),J=e=>U(l({},"__esModule",{value:!0}),e);var re={};G(re,{getImageVerifyCode:()=>D,getPersonalKey:()=>I,getUserInfo:()=>d,getVerifyCode:()=>q,getWechatQrCode:()=>F,getWechatQrCodeStatus:()=>H,loginByCode:()=>B,loginByPassword:()=>Q,loginByWechat:()=>M,logout:()=>L,resolveCurrentTeam:()=>g,switchTeam:()=>b,syncApiKey:()=>E,syncCurrentTeam:()=>u});module.exports=J(re);var f=require("@linkfox-web/http");var h=require("@linkfox-web/http");var S={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var Z=new Set(["30003","30009","30011","30015","30017","30019"]),k="LINKFOX_CLIENT_ID";function p(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function y(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function $(){try{let e=localStorage.getItem(k);return e||(e=y(),localStorage.setItem(k,e)),e}catch{return""}}function ee(e){try{return btoa(JSON.stringify({a_id:$(),d_id:e}))}catch{return""}}var o=(0,h.defineService)({name:"user-center",defaults:S,baseHeaders:e=>({source:e.config.source,uid:ee(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,t)=>e.status===401||p(t)&&t.success!==!0&&Z.has(String(t.code??"")),refreshOnUnauthorized:!1,unwrap:(e,t)=>{if(p(t)&&(t.code==="OK"&&t.success!==!1||t.code===200&&t.success!==!1||t.success===!0)&&e.ok)return t.data;throw new h.LinkfoxHttpError({service:"user-center",status:e.status,code:p(t)?t.code:void 0,message:p(t)&&(t.message||t.msg)||e.statusText||`HTTP ${e.status}`,body:t})}});var a=require("@linkfox-web/http");var s=require("@linkfox-web/http");async function I(e){return(await o.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await te()}}))?.credentials??[]}async function te(){let e=(0,s.getCurrentTeam)()?.teamId;if(e)return e;let{teamList:t}=await d(),n=(g(t)??t.find(r=>r.isSelect===1)??t[0])?.teamId;if(!n)throw new s.LinkfoxHttpError({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return n}async function E(e){let t=await I(e),n=t.find(r=>r.credentialType==="USER_PRIMARY"&&r.credential)??t.find(r=>r.credential);if(!n)throw new s.LinkfoxHttpError({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return T(n.credential),n.credential}function T(e){(0,s.configure)({services:{"ai-tools":{apiKey:e}}})}function d(){return R(!1)}function R(e){return o.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function g(e){if(!e?.length)return;let t=e.find(r=>r.isSelect===1);if(t?.agentMemberId)return t;let n=e.find(r=>r.teamType===1);return n?.agentMemberId?n:e.find(r=>r.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 u(e){let t=await R(e?.skipRecovery??!1),n=g(t.teamList),r=n?v(n):null;return r?((0,a.setTeam)(r),!0):!1}async function b(e){await o.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let t=await d(),n=t.teamList.find(i=>i.teamId===e),r=n?v(n):null;return r?(0,a.setTeam)(r):(0,a.clearTeam)(),t}function L(){(0,a.clearSession)(),T(void 0)}async function ne(e){let t=await o.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!t?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");(0,f.updateSession)({accessToken:t.accessToken,refreshToken:t.refreshToken,expireAt:t.expireAt})}function A(){(0,f.registerCapability)("refresh",ne),(0,f.registerCapability)("resolveTeam",()=>u({skipRecovery:!0}))}var C=require("@linkfox-web/http");var O=m(require("crypto-js/aes"),1),_=m(require("crypto-js/enc-utf8"),1),N=m(require("crypto-js/mode-ecb"),1),K=m(require("crypto-js/pad-zeropadding"),1);function W(e,t){return O.default.encrypt(e,_.default.parse(t),{mode:N.default,padding:K.default}).toString()}function w(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function P(e){if(!e?.accessToken)throw new C.LinkfoxHttpError({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});(0,C.setSession)(e);try{await u()}catch{}return e}function x(e,t){return o.request({method:"POST",path:e,body:t,auth:"none"})}async function D(){let e=y(),t=await o.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),n=await new Promise((r,i)=>{let c=new FileReader;c.onloadend=()=>r(String(c.result??"")),c.onerror=()=>i(c.error),c.readAsDataURL(t)});return{uuid:e,imageBase64:n}}async function Q(e,t){let n=o.config(),r=await x("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:w(t),data:{authPhone:e.authPhone,auth:W(e.password,n.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:n.systemId}});return P(r)}async function q(e){await x("/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:o.config().clientChannel}})}async function B(e,t){let n=await x("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:w(t),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return P(n)}function F(){return o.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function H(e){return o.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function M(e,t){let n=await x("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:w(t),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return P(n)}A();
|
|
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();
|
package/dist/index.d.cts
CHANGED
|
@@ -146,4 +146,67 @@ declare module '@linkfox-web/http' {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
/** 存在 http 会话里的 SSO 应用会话,accessToken 由 http 管道按各服务的 authorize 注入。 */
|
|
150
|
+
interface ThirdPartySsoSession {
|
|
151
|
+
accessToken: string;
|
|
152
|
+
sub: string;
|
|
153
|
+
id: string;
|
|
154
|
+
name: string;
|
|
155
|
+
picture?: string;
|
|
156
|
+
/** token 的 exp 换算为毫秒;仅用于本地过期判断,不代表验签。 */
|
|
157
|
+
expireAt: number;
|
|
158
|
+
}
|
|
159
|
+
/** 只清当前应用写入的会话,不动主站或其他应用的登录态。 */
|
|
160
|
+
declare function clearThirdPartySsoSession(clientId: string): void;
|
|
161
|
+
declare function readThirdPartySsoSession(clientId: string): ThirdPartySsoSession | null;
|
|
162
|
+
|
|
163
|
+
interface ThirdPartyApplication {
|
|
164
|
+
redirectUri: string;
|
|
165
|
+
authorizationUrl: string;
|
|
166
|
+
codeVerifier: string;
|
|
167
|
+
appName: string;
|
|
168
|
+
}
|
|
169
|
+
interface ThirdPartyLoginParams {
|
|
170
|
+
clientId: string;
|
|
171
|
+
redirectUri: string;
|
|
172
|
+
codeVerifier: string;
|
|
173
|
+
appName: string;
|
|
174
|
+
code: string;
|
|
175
|
+
}
|
|
176
|
+
interface ThirdPartySsoOptions {
|
|
177
|
+
clientId: string;
|
|
178
|
+
/** 默认当前站点 origin,也可指定本站的回调路径。 */
|
|
179
|
+
redirectUri?: string;
|
|
180
|
+
/** 弹窗内 iframe 的 window;收到消息时必须仍指向这一个 iframe。 */
|
|
181
|
+
getFrameWindow(): Window | null;
|
|
182
|
+
/** 仅传递展示所需的信息,不暴露 codeVerifier。 */
|
|
183
|
+
onAuthorization(application: {
|
|
184
|
+
authorizationUrl: string;
|
|
185
|
+
appName: string;
|
|
186
|
+
}): void;
|
|
187
|
+
signal?: AbortSignal;
|
|
188
|
+
/** 整个授权事务的超时,默认 5 分钟。 */
|
|
189
|
+
timeoutMs?: number;
|
|
190
|
+
}
|
|
191
|
+
declare function getThirdPartyApplication(clientId: string, options?: {
|
|
192
|
+
redirectUri?: string;
|
|
193
|
+
signal?: AbortSignal;
|
|
194
|
+
}): Promise<ThirdPartyApplication>;
|
|
195
|
+
declare function loginThirdPartyApplication(params: ThirdPartyLoginParams, options?: {
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
}): Promise<ThirdPartySsoSession>;
|
|
198
|
+
/** 回调页面启动时调用;返回 true 时只展示完成提示,不初始化业务内容。 */
|
|
199
|
+
declare function handleThirdPartySsoCallback(): boolean;
|
|
200
|
+
/** 每个调用方应取消旧事务后再启动新事务;SDK 不依赖任何 UI 框架。 */
|
|
201
|
+
declare function startThirdPartySso(options: ThirdPartySsoOptions): Promise<ThirdPartySsoSession>;
|
|
202
|
+
|
|
203
|
+
interface ThirdPartySsoConfig {
|
|
204
|
+
baseURL: string;
|
|
205
|
+
}
|
|
206
|
+
declare module '@linkfox-web/http' {
|
|
207
|
+
interface ServiceConfigMap {
|
|
208
|
+
'third-party-sso': ThirdPartySsoConfig;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -146,4 +146,67 @@ declare module '@linkfox-web/http' {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
/** 存在 http 会话里的 SSO 应用会话,accessToken 由 http 管道按各服务的 authorize 注入。 */
|
|
150
|
+
interface ThirdPartySsoSession {
|
|
151
|
+
accessToken: string;
|
|
152
|
+
sub: string;
|
|
153
|
+
id: string;
|
|
154
|
+
name: string;
|
|
155
|
+
picture?: string;
|
|
156
|
+
/** token 的 exp 换算为毫秒;仅用于本地过期判断,不代表验签。 */
|
|
157
|
+
expireAt: number;
|
|
158
|
+
}
|
|
159
|
+
/** 只清当前应用写入的会话,不动主站或其他应用的登录态。 */
|
|
160
|
+
declare function clearThirdPartySsoSession(clientId: string): void;
|
|
161
|
+
declare function readThirdPartySsoSession(clientId: string): ThirdPartySsoSession | null;
|
|
162
|
+
|
|
163
|
+
interface ThirdPartyApplication {
|
|
164
|
+
redirectUri: string;
|
|
165
|
+
authorizationUrl: string;
|
|
166
|
+
codeVerifier: string;
|
|
167
|
+
appName: string;
|
|
168
|
+
}
|
|
169
|
+
interface ThirdPartyLoginParams {
|
|
170
|
+
clientId: string;
|
|
171
|
+
redirectUri: string;
|
|
172
|
+
codeVerifier: string;
|
|
173
|
+
appName: string;
|
|
174
|
+
code: string;
|
|
175
|
+
}
|
|
176
|
+
interface ThirdPartySsoOptions {
|
|
177
|
+
clientId: string;
|
|
178
|
+
/** 默认当前站点 origin,也可指定本站的回调路径。 */
|
|
179
|
+
redirectUri?: string;
|
|
180
|
+
/** 弹窗内 iframe 的 window;收到消息时必须仍指向这一个 iframe。 */
|
|
181
|
+
getFrameWindow(): Window | null;
|
|
182
|
+
/** 仅传递展示所需的信息,不暴露 codeVerifier。 */
|
|
183
|
+
onAuthorization(application: {
|
|
184
|
+
authorizationUrl: string;
|
|
185
|
+
appName: string;
|
|
186
|
+
}): void;
|
|
187
|
+
signal?: AbortSignal;
|
|
188
|
+
/** 整个授权事务的超时,默认 5 分钟。 */
|
|
189
|
+
timeoutMs?: number;
|
|
190
|
+
}
|
|
191
|
+
declare function getThirdPartyApplication(clientId: string, options?: {
|
|
192
|
+
redirectUri?: string;
|
|
193
|
+
signal?: AbortSignal;
|
|
194
|
+
}): Promise<ThirdPartyApplication>;
|
|
195
|
+
declare function loginThirdPartyApplication(params: ThirdPartyLoginParams, options?: {
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
}): Promise<ThirdPartySsoSession>;
|
|
198
|
+
/** 回调页面启动时调用;返回 true 时只展示完成提示,不初始化业务内容。 */
|
|
199
|
+
declare function handleThirdPartySsoCallback(): boolean;
|
|
200
|
+
/** 每个调用方应取消旧事务后再启动新事务;SDK 不依赖任何 UI 框架。 */
|
|
201
|
+
declare function startThirdPartySso(options: ThirdPartySsoOptions): Promise<ThirdPartySsoSession>;
|
|
202
|
+
|
|
203
|
+
interface ThirdPartySsoConfig {
|
|
204
|
+
baseURL: string;
|
|
205
|
+
}
|
|
206
|
+
declare module '@linkfox-web/http' {
|
|
207
|
+
interface ServiceConfigMap {
|
|
208
|
+
'third-party-sso': ThirdPartySsoConfig;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{registerCapability as P,updateSession as Q}from"@linkfox-web/http";import{defineService as k,LinkfoxHttpError as E}from"@linkfox-web/http";var h={baseURL:"https://api.linkfox.com",aesKey:"AR7x7zfje4nkim4t",source:"ai-linkfox-web",systemId:"LinkFoxAgent",clientChannel:"LinkFoxAI"};var R=new Set(["30003","30009","30011","30015","30017","30019"]),C="LINKFOX_CLIENT_ID";function a(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function f(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function v(){try{let e=localStorage.getItem(C);return e||(e=f(),localStorage.setItem(C,e)),e}catch{return""}}function b(e){try{return btoa(JSON.stringify({a_id:v(),d_id:e}))}catch{return""}}var o=k({name:"user-center",defaults:h,baseHeaders:e=>({source:e.config.source,uid:b(e.session?.userId??"")}),authorize:e=>e.session?{authorization:e.session.accessToken}:{},isUnauthorized:(e,t)=>e.status===401||a(t)&&t.success!==!0&&R.has(String(t.code??"")),refreshOnUnauthorized:!1,unwrap:(e,t)=>{if(a(t)&&(t.code==="OK"&&t.success!==!1||t.code===200&&t.success!==!1||t.success===!0)&&e.ok)return t.data;throw new E({service:"user-center",status:e.status,code:a(t)?t.code:void 0,message:a(t)&&(t.message||t.msg)||e.statusText||`HTTP ${e.status}`,body:t})}});import{clearSession as N,clearTeam as K,setTeam as I}from"@linkfox-web/http";import{configure as L,getCurrentTeam as A,LinkfoxHttpError as x}from"@linkfox-web/http";async function y(e){return(await o.request({method:"POST",path:"/linkFoxApp/api/userCenter/api-credential/getPersonalKey",body:{teamId:e||await O()}}))?.credentials??[]}async function O(){let e=A()?.teamId;if(e)return e;let{teamList:t}=await u(),n=(c(t)??t.find(r=>r.isSelect===1)??t[0])?.teamId;if(!n)throw new x({service:"user-center",status:200,code:"TEAM_REQUIRED",message:"当前账号没有可用团队"});return n}async function _(e){let t=await y(e),n=t.find(r=>r.credentialType==="USER_PRIMARY"&&r.credential)??t.find(r=>r.credential);if(!n)throw new x({service:"user-center",status:200,code:"API_KEY_NOT_FOUND",message:"当前账号没有可用的 API Key"});return l(n.credential),n.credential}function l(e){L({services:{"ai-tools":{apiKey:e}}})}function u(){return T(!1)}function T(e){return o.request({method:"POST",path:"/linkFoxApp/api/userCenter/userInfo",body:{},skipRecovery:e})}function c(e){if(!e?.length)return;let t=e.find(r=>r.isSelect===1);if(t?.agentMemberId)return t;let n=e.find(r=>r.teamType===1);return n?.agentMemberId?n:e.find(r=>r.agentMemberId)}function w(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 s(e){let t=await T(e?.skipRecovery??!1),n=c(t.teamList),r=n?w(n):null;return r?(I(r),!0):!1}async function W(e){await o.request({method:"POST",path:"/linkFoxApp/api/userCenter/team/switchTeam",body:{targetTeamId:e}});let t=await u(),n=t.teamList.find(g=>g.teamId===e),r=n?w(n):null;return r?I(r):K(),t}function D(){N(),l(void 0)}async function q(e){let t=await o.request({method:"POST",path:"/cp/v1/user/refresh",auth:"none",skipRecovery:!0,headers:{authorization:e.refreshToken??""}});if(!t?.accessToken)throw new Error("REFRESH_TOKEN_EXPIRED");Q({accessToken:t.accessToken,refreshToken:t.refreshToken,expireAt:t.expireAt})}function U(){P("refresh",q),P("resolveTeam",()=>s({skipRecovery:!0}))}import{LinkfoxHttpError as V,setSession as z}from"@linkfox-web/http";import B from"crypto-js/aes";import F from"crypto-js/enc-utf8";import H from"crypto-js/mode-ecb";import M from"crypto-js/pad-zeropadding";function S(e,t){return B.encrypt(e,F.parse(t),{mode:H,padding:M}).toString()}function m(e){return e?.sourceUrl??(typeof location<"u"?location.href:"")}async function p(e){if(!e?.accessToken)throw new V({service:"user-center",status:200,code:"INVALID_LOGIN_RESPONSE",message:"登录响应缺少 accessToken",body:e});z(e);try{await s()}catch{}return e}function d(e,t){return o.request({method:"POST",path:e,body:t,auth:"none"})}async function j(){let e=f(),t=await o.request({path:"/user/v1/sendMessageCode.jpg",query:{uuid:e},auth:"none",responseType:"blob"}),n=await new Promise((r,g)=>{let i=new FileReader;i.onloadend=()=>r(String(i.result??"")),i.onerror=()=>g(i.error),i.readAsDataURL(t)});return{uuid:e,imageBase64:n}}async function X(e,t){let n=o.config(),r=await d("/user/v2/web/login",{type:"smsPwd",method:"login",sourceUrl:m(t),data:{authPhone:e.authPhone,auth:S(e.password,n.aesKey),uuid:e.uuid,imageCode:e.imageCode,systemId:n.systemId}});return p(r)}async function Y(e){await d("/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:o.config().clientChannel}})}async function G(e,t){let n=await d("/user/v3/web/login",{type:"sms",method:"login",sourceUrl:m(t),data:{authPhone:e.authPhone,areaCode:e.areaCode??"+86",authCode:e.authCode,sourceChannel:e.sourceChannel??""}});return p(n)}function J(){return o.request({method:"POST",path:"/user/v1/web/login/wechat/qr_url",body:{type:"1"},auth:"none"})}function Z(e){return o.request({path:"/user/v1/web/login/wechat",query:{sessionId:e},auth:"none"})}async function $(e,t){let n=await d("/user/v2/web/login",{type:"wechatQR",method:"login",sourceUrl:m(t),data:{sessionId:e.sessionId,...e.authPhone?{authPhone:e.authPhone}:{},...e.authCode?{authCode:e.authCode}:{},sourceChannel:e.sourceChannel??""}});return p(n)}U();export{j as getImageVerifyCode,y as getPersonalKey,u as getUserInfo,Y as getVerifyCode,J as getWechatQrCode,Z as getWechatQrCodeStatus,G as loginByCode,X as loginByPassword,$ as loginByWechat,D as logout,c as resolveCurrentTeam,W as switchTeam,_ as syncApiKey,s as syncCurrentTeam};
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkfox-web/api-user-center",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
34
|
+
"@linkfox-web/http": "^0.3.1"
|
|
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.
|
|
41
|
+
"@linkfox-web/http": "0.3.1"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|