Files

82 lines
5.5 KiB
JavaScript
Raw Normal View History

2026-09-10 18:39:46 +08:00
const SANSHENG_AUTH_CONFIG={
apiBase:'https://api.sanshengai.net',
pcBase:'https://www.sanshengai.net',
clientId:'sansheng-product-analysis-extension',
scope:'product.analysis',
callbackPath:'/extension/authorize'
};
const AUTH_ACCESS_KEY='sansheng_extension_access';
const AUTH_REFRESH_KEY='sansheng_extension_refresh';
const AUTH_PENDING_KEY='sansheng_extension_auth_pending';
const AUTH_LAST_CONTEXT_KEY='sansheng_extension_auth_last_context';
function authBase64Url(bytes){return btoa(String.fromCharCode(...bytes)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')}
function authRandom(size=32){const bytes=new Uint8Array(size);crypto.getRandomValues(bytes);return authBase64Url(bytes)}
async function authChallenge(verifier){const digest=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(verifier));return authBase64Url(new Uint8Array(digest))}
function authCallbackUrl(){return SANSHENG_AUTH_CONFIG.pcBase+SANSHENG_AUTH_CONFIG.callbackPath}
async function getExtensionAuth(){
const session=await chrome.storage.session.get(AUTH_ACCESS_KEY);
const local=await chrome.storage.local.get(AUTH_REFRESH_KEY);
return {access:session[AUTH_ACCESS_KEY]||null,refresh:local[AUTH_REFRESH_KEY]||null};
}
async function startExtensionLogin(){
const verifier=authRandom(48),state=authRandom(24),challenge=await authChallenge(verifier);
const tabs=await chrome.tabs.query({active:true,currentWindow:true});
await chrome.storage.local.set({[AUTH_PENDING_KEY]:{verifier,state,createdAt:Date.now(),tabId:tabs?.[0]?.id||0,windowId:tabs?.[0]?.windowId||0}});
const query=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,redirect_uri:authCallbackUrl(),scope:SANSHENG_AUTH_CONFIG.scope,state,code_challenge:challenge,code_challenge_method:'S256'});
return chrome.tabs.create({url:`${SANSHENG_AUTH_CONFIG.pcBase}${SANSHENG_AUTH_CONFIG.callbackPath}?${query}`,active:true});
}
async function exchangeExtensionCode(callbackUrl){
const url=new URL(callbackUrl),code=url.searchParams.get('code'),state=url.searchParams.get('state');
if(!code||!state)return false;
const pending=(await chrome.storage.local.get(AUTH_PENDING_KEY))[AUTH_PENDING_KEY];
if(!pending||pending.state!==state||Date.now()-pending.createdAt>10*60*1000)throw new Error('扩展授权请求已过期,请重新登录');
const body=new URLSearchParams({grant_type:'authorization_code',client_id:SANSHENG_AUTH_CONFIG.clientId,code,redirect_uri:authCallbackUrl(),code_verifier:pending.verifier});
const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body});
const token=await response.json();
if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录失败');
await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}});
await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}});
await chrome.storage.local.set({[AUTH_LAST_CONTEXT_KEY]:{tabId:pending.tabId||0,windowId:pending.windowId||0,updatedAt:Date.now()}});
await chrome.storage.local.remove(AUTH_PENDING_KEY);
return true;
}
async function refreshExtensionToken(){
const auth=await getExtensionAuth();
if(!auth.refresh?.token)throw new Error('扩展登录已过期');
const body=new URLSearchParams({grant_type:'refresh_token',client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token});
const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body});
const token=await response.json();
if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录已过期');
await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}});
await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}});
return token.access_token;
}
async function extensionApiFetch(path,options={}){
let auth=await getExtensionAuth();
if(!auth.access?.token||auth.access.expiresAt-Date.now()<60_000){await refreshExtensionToken();auth=await getExtensionAuth()}
const send=()=>fetch(SANSHENG_AUTH_CONFIG.apiBase+path,{...options,headers:{...(options.headers||{}),Authorization:`Sansheng ${auth.access.token}`,'Sansheng-Source':'chrome-extension'}});
let response=await send();
if(response.status===401){await refreshExtensionToken();auth=await getExtensionAuth();response=await send()}
return response;
}
async function logoutExtension(){
const auth=await getExtensionAuth();
if(auth.refresh?.token){
const body=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token,access_token:auth.access?.token||''});
await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Revoke`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}).catch(()=>{});
}
await chrome.storage.session.remove(AUTH_ACCESS_KEY);
await chrome.storage.local.remove([AUTH_REFRESH_KEY,AUTH_PENDING_KEY,AUTH_LAST_CONTEXT_KEY]);
}
self.sanshengExtensionAuth={config:SANSHENG_AUTH_CONFIG,get:getExtensionAuth,start:startExtensionLogin,exchange:exchangeExtensionCode,fetch:extensionApiFetch,logout:logoutExtension};