refactor: 只保留 Web,移除 Capacitor 原生壳
删除 android/、ios/ 两个 Capacitor 原生工程与 capacitor.config.ts、 ionic.config.json,以及 scripts/android、scripts/ios.sh 构建脚本; 根与 apps/web 的 @capacitor/* 依赖和相关 npm scripts 一并移除。 Web 侧去掉原生分支:登录只走云乐坊容器授权面板或顶层重定向, 暗色模式不再改原生状态栏,移除 Android 物理返回键处理。 保留 @nuxtjs/ionic(Web UI 框架)、云乐坊 window.ylf 桥接与 PWA。 决策记录见 docs/decisions/0004-web-only.md。
This commit is contained in:
@@ -2,23 +2,15 @@ import type {
|
||||
AcquisitionContext,
|
||||
SsoAuthorizationResult,
|
||||
} from '@yunlefun/sso'
|
||||
import type {
|
||||
NativeSsoTransaction,
|
||||
NativeSsoTransactionStore,
|
||||
} from '~/utils/nativeSso'
|
||||
import type {
|
||||
YunlefunAccount,
|
||||
YunlefunAuthorizationAdapter,
|
||||
YunlefunAuthorizationResult,
|
||||
YunlefunIdentityAdapter,
|
||||
} from '~/utils/yunlefunAuth'
|
||||
import type { YunlefunSsoConfig } from '~/utils/yunlefunSsoConfig'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import {
|
||||
adoptSsoCode,
|
||||
consumeSsoRedirect,
|
||||
sanitizeAcquisitionContext,
|
||||
startSsoRedirect,
|
||||
} from '@yunlefun/sso'
|
||||
import {
|
||||
@@ -26,13 +18,6 @@ import {
|
||||
SsoIdentityAdoptionError,
|
||||
} from '@yunlefun/sso/browser'
|
||||
import { readCookAcquisition } from '~/utils/acquisition'
|
||||
import {
|
||||
consumeNativeSsoCallback,
|
||||
isNativeSsoCallbackUrl,
|
||||
openNativeSsoRedirect,
|
||||
} from '~/utils/nativeSso'
|
||||
|
||||
const NATIVE_TRANSACTION_KEY = 'cook:sso:v3:native-transaction'
|
||||
|
||||
interface CloudbaseUser {
|
||||
avatar_url?: string
|
||||
@@ -94,11 +79,6 @@ interface CloudbaseModule {
|
||||
}) => CloudbaseApp
|
||||
}
|
||||
|
||||
export interface NativeYunlefunAuthorizationAdapter extends YunlefunAuthorizationAdapter {
|
||||
cancel: () => Promise<void>
|
||||
consumeUrl: (url: string) => Promise<YunlefunAuthorizationResult | null>
|
||||
}
|
||||
|
||||
export interface CloudbaseIdentityDependencies {
|
||||
getAuth?: (env: string) => Promise<CloudbaseAuthClient>
|
||||
}
|
||||
@@ -151,43 +131,6 @@ export function createWebYunlefunAuthorizationAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
export function createNativeYunlefunAuthorizationAdapter(
|
||||
config: YunlefunSsoConfig,
|
||||
initialUrl: () => Promise<string | null>,
|
||||
readAcquisition: () => AcquisitionContext | null = readCookAcquisition,
|
||||
): NativeYunlefunAuthorizationAdapter {
|
||||
const store = createNativeTransactionStore()
|
||||
|
||||
async function consumeUrl(url: string): Promise<YunlefunAuthorizationResult | null> {
|
||||
if (!isNativeSsoCallbackUrl(url, config.redirect.redirectUri))
|
||||
return null
|
||||
|
||||
const result = await consumeNativeSsoCallback(url, store)
|
||||
await Browser.close().catch(() => undefined)
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
cancel: async () => store.clear(),
|
||||
consumeInitial: async () => {
|
||||
const url = await initialUrl()
|
||||
return url ? consumeUrl(url) : null
|
||||
},
|
||||
consumeUrl,
|
||||
start: async () => {
|
||||
const acquisition = readAcquisition()
|
||||
await openNativeSsoRedirect({
|
||||
...config.redirect,
|
||||
prompt: 'consent',
|
||||
...(acquisition ? { acquisition } : {}),
|
||||
}, store, async (url) => {
|
||||
await Browser.open({ url })
|
||||
})
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createCloudbaseIdentityAdapter(
|
||||
config: YunlefunSsoConfig,
|
||||
dependencies: CloudbaseIdentityDependencies = {},
|
||||
@@ -212,32 +155,6 @@ export function createCloudbaseIdentityAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
function createNativeTransactionStore(): NativeSsoTransactionStore {
|
||||
return {
|
||||
clear: async () => {
|
||||
await Preferences.remove({ key: NATIVE_TRANSACTION_KEY })
|
||||
},
|
||||
load: async () => {
|
||||
const { value } = await Preferences.get({ key: NATIVE_TRANSACTION_KEY })
|
||||
if (!value)
|
||||
return null
|
||||
|
||||
try {
|
||||
return readNativeTransaction(JSON.parse(value) as unknown)
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
save: async (transaction) => {
|
||||
await Preferences.set({
|
||||
key: NATIVE_TRANSACTION_KEY,
|
||||
value: JSON.stringify(transaction),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function getCloudbaseApp(env: string): Promise<CloudbaseApp> {
|
||||
if (cachedCloudbaseApp) {
|
||||
if (cachedCloudbaseEnv !== env)
|
||||
@@ -339,33 +256,3 @@ function normalizeAccount(user: CloudbaseUser | null | undefined): YunlefunAccou
|
||||
function firstString(...values: unknown[]): string | undefined {
|
||||
return values.find(value => typeof value === 'string' && value.trim())?.toString().trim()
|
||||
}
|
||||
|
||||
function readNativeTransaction(value: unknown): NativeSsoTransaction | null {
|
||||
if (!isRecord(value)
|
||||
|| typeof value.clientId !== 'string'
|
||||
|| typeof value.codeVerifier !== 'string'
|
||||
|| typeof value.createdAt !== 'number'
|
||||
|| typeof value.issuer !== 'string'
|
||||
|| typeof value.nonce !== 'string'
|
||||
|| typeof value.redirectUri !== 'string'
|
||||
|| !Array.isArray(value.scope)
|
||||
|| !value.scope.every(scope => typeof scope === 'string')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const acquisition = sanitizeAcquisitionContext(value.acquisition)
|
||||
return {
|
||||
clientId: value.clientId,
|
||||
codeVerifier: value.codeVerifier,
|
||||
createdAt: value.createdAt,
|
||||
issuer: value.issuer,
|
||||
nonce: value.nonce,
|
||||
redirectUri: value.redirectUri,
|
||||
scope: value.scope,
|
||||
...(acquisition ? { acquisition } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { StatusBar, Style } from '@capacitor/status-bar'
|
||||
import { ionDarkClass } from '~/constants'
|
||||
|
||||
/**
|
||||
@@ -13,24 +11,14 @@ export const useDarkMode = createSharedComposable(() => {
|
||||
const color = useColorMode()
|
||||
const isDark = computed(() => color.value === 'dark')
|
||||
|
||||
async function setLightMode() {
|
||||
function setLightMode() {
|
||||
document.documentElement.classList.remove(ionDarkClass)
|
||||
syncThemeColor('#f2f2f6')
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await StatusBar.setStyle({ style: Style.Light })
|
||||
await StatusBar.setBackgroundColor({ color: '#f2f2f6ff' })
|
||||
}
|
||||
}
|
||||
|
||||
async function setDarkMode() {
|
||||
function setDarkMode() {
|
||||
document.documentElement.classList.add(ionDarkClass)
|
||||
syncThemeColor('#000000')
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await StatusBar.setStyle({ style: Style.Dark })
|
||||
await StatusBar.setBackgroundColor({ color: '#ff000000' })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -39,7 +27,7 @@ export const useDarkMode = createSharedComposable(() => {
|
||||
setLightMode,
|
||||
setDarkMode,
|
||||
|
||||
async toggleDark() {
|
||||
toggleDark() {
|
||||
const targetMode = color.value === 'dark' ? 'light' : 'dark'
|
||||
color.preference = targetMode
|
||||
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import type { NativeYunlefunAuthorizationAdapter } from '~/adapters/yunlefunAuth'
|
||||
import type {
|
||||
YunlefunAuthController,
|
||||
YunlefunAuthSnapshot,
|
||||
} from '~/utils/yunlefunAuth'
|
||||
import { App } from '@capacitor/app'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import {
|
||||
callCloudbaseFunction,
|
||||
createCloudbaseIdentityAdapter,
|
||||
createNativeYunlefunAuthorizationAdapter,
|
||||
createWebYunlefunAuthorizationAdapter,
|
||||
} from '~/adapters/yunlefunAuth'
|
||||
import { stripNativeSsoCallbackHash } from '~/utils/nativeSso'
|
||||
import { createYunlefunAuthController } from '~/utils/yunlefunAuth'
|
||||
import { resolveYunlefunSsoConfig } from '~/utils/yunlefunSsoConfig'
|
||||
|
||||
@@ -21,8 +15,6 @@ interface YunlefunRuntimeConfig {
|
||||
yunlefunCloudbaseEnv?: string
|
||||
yunlefunSsoClientId?: string
|
||||
yunlefunSsoExchangeUrl?: string
|
||||
yunlefunSsoNativeClientId?: string
|
||||
yunlefunSsoNativeRedirectUri?: string
|
||||
yunlefunSsoOrigin?: string
|
||||
yunlefunSsoRedirectUri?: string
|
||||
yunlefunSsoRedirectUris?: string
|
||||
@@ -32,10 +24,7 @@ interface YunlefunRuntimeConfig {
|
||||
|
||||
let controller: YunlefunAuthController | undefined
|
||||
let cloudbaseEnv = ''
|
||||
let nativeAdapter: NativeYunlefunAuthorizationAdapter | undefined
|
||||
let initialization: Promise<void> | undefined
|
||||
let nativeUrlListenerInstalled = false
|
||||
let handlingNativeCallback = false
|
||||
|
||||
export function useYunlefunAuth() {
|
||||
const runtimeConfig = useRuntimeConfig() as unknown as YunlefunRuntimeConfig
|
||||
@@ -98,35 +87,16 @@ async function initializeOnce(
|
||||
if (!import.meta.client)
|
||||
return
|
||||
|
||||
const platform = Capacitor.isNativePlatform() ? 'native' : 'web'
|
||||
if (platform === 'web'
|
||||
&& window.location.pathname === '/auth/callback'
|
||||
&& new URLSearchParams(window.location.search).get('platform') === 'native') {
|
||||
const sanitizedPath = stripNativeSsoCallbackHash(window.location.href)
|
||||
if (sanitizedPath)
|
||||
window.history.replaceState(window.history.state, '', sanitizedPath)
|
||||
configurationMessage.value = '未能自动返回 Cook,请确认已安装最新版应用后重新授权'
|
||||
snapshot.value = {
|
||||
account: null,
|
||||
errorMessage: '',
|
||||
status: 'signed-out',
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const result = resolveYunlefunSsoConfig({
|
||||
clientId: runtimeConfig.public.yunlefunSsoClientId,
|
||||
cloudbaseEnv: runtimeConfig.public.yunlefunCloudbaseEnv,
|
||||
exchangeUrl: runtimeConfig.public.yunlefunSsoExchangeUrl,
|
||||
nativeClientId: runtimeConfig.public.yunlefunSsoNativeClientId,
|
||||
nativeRedirectUri: runtimeConfig.public.yunlefunSsoNativeRedirectUri,
|
||||
redirectUri: runtimeConfig.public.yunlefunSsoRedirectUri,
|
||||
redirectUris: runtimeConfig.public.yunlefunSsoRedirectUris,
|
||||
scope: runtimeConfig.public.yunlefunSsoScope,
|
||||
ssoOrigin: runtimeConfig.public.yunlefunSsoOrigin,
|
||||
}, {
|
||||
currentOrigin: window.location.origin,
|
||||
platform,
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
@@ -140,59 +110,14 @@ async function initializeOnce(
|
||||
}
|
||||
|
||||
cloudbaseEnv = result.config.cloudbaseEnv
|
||||
const web = createWebYunlefunAuthorizationAdapter(result.config)
|
||||
nativeAdapter = createNativeYunlefunAuthorizationAdapter(result.config, async () => {
|
||||
const launch = await App.getLaunchUrl()
|
||||
return launch?.url ?? null
|
||||
})
|
||||
controller = createYunlefunAuthController({
|
||||
identity: createCloudbaseIdentityAdapter(result.config),
|
||||
native: nativeAdapter,
|
||||
platform,
|
||||
web,
|
||||
web: createWebYunlefunAuthorizationAdapter(result.config),
|
||||
})
|
||||
|
||||
if (platform === 'native' && !nativeUrlListenerInstalled) {
|
||||
nativeUrlListenerInstalled = true
|
||||
await App.addListener('appUrlOpen', async ({ url }) => {
|
||||
if (!controller || !nativeAdapter)
|
||||
return
|
||||
|
||||
handlingNativeCallback = true
|
||||
try {
|
||||
const authorization = await nativeAdapter.consumeUrl(url)
|
||||
if (!authorization)
|
||||
return
|
||||
|
||||
await controller.handleAuthorization(authorization)
|
||||
publishSnapshot(snapshot)
|
||||
if (controller.snapshot().status === 'signed-in')
|
||||
await navigateTo('/my', { replace: true })
|
||||
}
|
||||
finally {
|
||||
handlingNativeCallback = false
|
||||
}
|
||||
})
|
||||
await Browser.addListener('browserFinished', async () => {
|
||||
if (!controller
|
||||
|| handlingNativeCallback
|
||||
|| controller.snapshot().status !== 'signing-in') {
|
||||
return
|
||||
}
|
||||
|
||||
await nativeAdapter?.cancel().catch(() => undefined)
|
||||
await controller.handleAuthorization({
|
||||
ok: false,
|
||||
reason: 'access_denied',
|
||||
})
|
||||
publishSnapshot(snapshot)
|
||||
})
|
||||
}
|
||||
|
||||
await controller.initialize()
|
||||
publishSnapshot(snapshot)
|
||||
if (platform === 'web'
|
||||
&& window.location.pathname === '/auth/callback'
|
||||
if (window.location.pathname === '/auth/callback'
|
||||
&& controller.snapshot().status === 'signed-in') {
|
||||
await navigateTo('/my', { replace: true })
|
||||
}
|
||||
|
||||
@@ -101,12 +101,6 @@ export const acknowledgements: Acknowledgement[] = [
|
||||
href: 'https://ionicframework.com',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
type: 'website',
|
||||
label: 'Capacitor',
|
||||
href: 'https://capacitorjs.com',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
type: 'website',
|
||||
label: 'VueUse',
|
||||
|
||||
@@ -2,7 +2,7 @@ export const appName = '食用手册'
|
||||
export const appDescription = '好的,今天我们来做菜!'
|
||||
|
||||
export const namespace = 'cook'
|
||||
export const lastDbUpdated = '2026-10-20 11:02:43'
|
||||
export const lastDbUpdated = '2026-10-21 12:26:30'
|
||||
|
||||
export const icp = '苏ICP备17038157号'
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import { App } from '@capacitor/app'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { useBackButton } from '@ionic/vue'
|
||||
import { tabRootPaths } from '../config'
|
||||
|
||||
useHead({
|
||||
@@ -9,7 +6,6 @@ useHead({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const ionRouter = useIonRouter()
|
||||
|
||||
function isTabRootPath(path: string) {
|
||||
return tabRootPaths.includes(path)
|
||||
@@ -29,19 +25,6 @@ function onTabClick(tab: string) {
|
||||
const content = tabsRef.value?.$el.querySelector<HTMLIonContentElement>('.ion-page:not(.ion-page-hidden) ion-content')
|
||||
void content?.scrollToTop(reducedMotion.value ? 0 : 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
useBackButton(10, () => {
|
||||
if (isTabRootPath(router.currentRoute.value.path)) {
|
||||
App.minimizeApp()
|
||||
}
|
||||
else {
|
||||
ionRouter.back()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { actionSheetController, alertController } from '@ionic/vue'
|
||||
import { yunleHaptic } from './yunleHost'
|
||||
|
||||
/**
|
||||
* iOS 风格的弹窗:替代 @capacitor/dialog(Web 上会退化成 window.alert / confirm / prompt)
|
||||
* iOS 风格的弹窗:统一走 Ionic 的 alert / actionSheet
|
||||
*/
|
||||
|
||||
export async function showAlert(header: string, message: string): Promise<void> {
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import type {
|
||||
AcquisitionContext,
|
||||
RedirectSsoOptions,
|
||||
SsoAuthorizationResult,
|
||||
SsoRedirectResult,
|
||||
} from '@yunlefun/sso'
|
||||
import {
|
||||
buildSsoRequestUrl,
|
||||
deriveSsoPkceChallenge,
|
||||
isSsoPkceVerifier,
|
||||
parseSsoRedirectResult,
|
||||
readSsoClientId,
|
||||
readSsoRedirectUri,
|
||||
readSsoScope,
|
||||
sanitizeAcquisitionContext,
|
||||
YLF_DEFAULT_SSO_ORIGIN,
|
||||
} from '@yunlefun/sso'
|
||||
|
||||
const TRANSACTION_TTL_MS = 10 * 60 * 1000
|
||||
const NONCE_RE = /^[\w-]{32,128}$/
|
||||
const BASE64_PADDING_RE = /=+$/
|
||||
|
||||
export interface NativeSsoTransaction {
|
||||
clientId: string
|
||||
codeVerifier: string
|
||||
createdAt: number
|
||||
issuer: string
|
||||
nonce: string
|
||||
redirectUri: string
|
||||
scope: string[]
|
||||
acquisition?: AcquisitionContext
|
||||
}
|
||||
|
||||
export interface NativeSsoTransactionStore {
|
||||
clear: () => Promise<void>
|
||||
load: () => Promise<NativeSsoTransaction | null>
|
||||
save: (transaction: NativeSsoTransaction) => Promise<void>
|
||||
}
|
||||
|
||||
export interface NativeSsoDependencies {
|
||||
now?: () => number
|
||||
randomBase64Url?: () => string
|
||||
}
|
||||
|
||||
export async function beginNativeSsoRedirect(
|
||||
options: RedirectSsoOptions,
|
||||
store: NativeSsoTransactionStore,
|
||||
dependencies: NativeSsoDependencies = {},
|
||||
): Promise<string> {
|
||||
const issuer = new URL(options.ssoOrigin ?? YLF_DEFAULT_SSO_ORIGIN).origin
|
||||
const random = dependencies.randomBase64Url ?? (() => randomBase64Url(32))
|
||||
const nonce = random()
|
||||
const codeVerifier = random()
|
||||
const codeChallenge = await deriveSsoPkceChallenge(codeVerifier)
|
||||
const acquisition = sanitizeAcquisitionContext(options.acquisition)
|
||||
const authorizationUrl = buildSsoRequestUrl({
|
||||
clientId: options.clientId,
|
||||
codeChallenge,
|
||||
nonce,
|
||||
redirectUri: options.redirectUri,
|
||||
scope: options.scope,
|
||||
ssoOrigin: issuer,
|
||||
...(options.prompt ? { prompt: options.prompt } : {}),
|
||||
...(acquisition ? { acquisition } : {}),
|
||||
})
|
||||
|
||||
await store.save({
|
||||
clientId: options.clientId,
|
||||
codeVerifier,
|
||||
createdAt: (dependencies.now ?? Date.now)(),
|
||||
issuer,
|
||||
nonce,
|
||||
redirectUri: options.redirectUri,
|
||||
scope: [...options.scope],
|
||||
...(acquisition ? { acquisition } : {}),
|
||||
})
|
||||
|
||||
return authorizationUrl
|
||||
}
|
||||
|
||||
export async function openNativeSsoRedirect(
|
||||
options: RedirectSsoOptions,
|
||||
store: NativeSsoTransactionStore,
|
||||
open: (url: string) => Promise<void>,
|
||||
dependencies: NativeSsoDependencies = {},
|
||||
): Promise<void> {
|
||||
const url = await beginNativeSsoRedirect(options, store, dependencies)
|
||||
try {
|
||||
await open(url)
|
||||
}
|
||||
catch (error) {
|
||||
await store.clear()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function consumeNativeSsoCallback(
|
||||
callbackUrl: string,
|
||||
store: NativeSsoTransactionStore,
|
||||
dependencies: Pick<NativeSsoDependencies, 'now'> = {},
|
||||
): Promise<SsoAuthorizationResult | Extract<SsoRedirectResult, { ok: false }> | null> {
|
||||
const stored = await store.load()
|
||||
await store.clear()
|
||||
|
||||
const transaction = normalizeTransaction(stored)
|
||||
const now = (dependencies.now ?? Date.now)()
|
||||
if (!transaction
|
||||
|| transaction.createdAt + TRANSACTION_TTL_MS <= now
|
||||
|| !callbackMatchesRedirect(callbackUrl, transaction.redirectUri)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = parseSsoRedirectResult({
|
||||
expectedIssuer: transaction.issuer,
|
||||
expectedNonce: transaction.nonce,
|
||||
hash: new URL(callbackUrl).hash,
|
||||
})
|
||||
if (!result?.ok)
|
||||
return result
|
||||
|
||||
return Object.freeze({
|
||||
...result,
|
||||
clientId: transaction.clientId,
|
||||
codeVerifier: transaction.codeVerifier,
|
||||
redirectUri: transaction.redirectUri,
|
||||
scope: Object.freeze([...transaction.scope]),
|
||||
...(transaction.acquisition ? { acquisition: transaction.acquisition } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function isNativeSsoCallbackUrl(callbackUrl: string, redirectUri: string): boolean {
|
||||
return callbackMatchesRedirect(callbackUrl, redirectUri)
|
||||
}
|
||||
|
||||
export function stripNativeSsoCallbackHash(callbackUrl: string): string {
|
||||
try {
|
||||
const url = new URL(callbackUrl)
|
||||
return `${url.pathname}${url.search}`
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function callbackMatchesRedirect(callbackUrl: string, redirectUri: string): boolean {
|
||||
try {
|
||||
const callback = new URL(callbackUrl)
|
||||
const callbackWithoutHash = `${callback.origin}${callback.pathname}${callback.search}`
|
||||
return callback.protocol === 'https:'
|
||||
&& !callback.username
|
||||
&& !callback.password
|
||||
&& Boolean(callback.hash)
|
||||
&& callbackWithoutHash === redirectUri
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTransaction(value: NativeSsoTransaction | null): NativeSsoTransaction | null {
|
||||
if (!value)
|
||||
return null
|
||||
|
||||
const clientId = readSsoClientId(value.clientId)
|
||||
const redirectUri = readSsoRedirectUri(value.redirectUri)
|
||||
const scope = readSsoScope(value.scope.join(' '))
|
||||
let issuer = ''
|
||||
try {
|
||||
const issuerUrl = new URL(value.issuer)
|
||||
issuer = issuerUrl.protocol === 'https:' && issuerUrl.origin === value.issuer
|
||||
? issuerUrl.origin
|
||||
: ''
|
||||
}
|
||||
catch {
|
||||
issuer = ''
|
||||
}
|
||||
|
||||
if (!clientId
|
||||
|| !redirectUri
|
||||
|| scope.length === 0
|
||||
|| !issuer
|
||||
|| !NONCE_RE.test(value.nonce)
|
||||
|| !isSsoPkceVerifier(value.codeVerifier)
|
||||
|| !Number.isSafeInteger(value.createdAt)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const acquisition = sanitizeAcquisitionContext(value.acquisition)
|
||||
return {
|
||||
clientId,
|
||||
codeVerifier: value.codeVerifier,
|
||||
createdAt: value.createdAt,
|
||||
issuer,
|
||||
nonce: value.nonce,
|
||||
redirectUri,
|
||||
scope,
|
||||
...(acquisition ? { acquisition } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function randomBase64Url(byteLength: number): string {
|
||||
const bytes = new Uint8Array(byteLength)
|
||||
globalThis.crypto.getRandomValues(bytes)
|
||||
let binary = ''
|
||||
for (const byte of bytes)
|
||||
binary += String.fromCharCode(byte)
|
||||
return globalThis.btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(BASE64_PADDING_RE, '')
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 云乐坊 App 内嵌容器(apps.yunle.fun)注入的 `window.ylf` JSAPI。
|
||||
* 普通浏览器与 Cook 自己的 Capacitor 外壳中不存在,所有调用都要特性检测。
|
||||
* 普通浏览器中不存在,所有调用都要特性检测。
|
||||
*
|
||||
* @see https://github.com/YunLeFun/apps.yunle.fun/blob/main/docs/guide/jsapi.md
|
||||
*/
|
||||
|
||||
@@ -48,15 +48,13 @@ export interface YunlefunAuthController {
|
||||
|
||||
interface YunlefunAuthControllerOptions {
|
||||
identity: YunlefunIdentityAdapter
|
||||
native: YunlefunAuthorizationAdapter
|
||||
platform: 'native' | 'web'
|
||||
web: YunlefunAuthorizationAdapter
|
||||
}
|
||||
|
||||
export function createYunlefunAuthController(
|
||||
options: YunlefunAuthControllerOptions,
|
||||
): YunlefunAuthController {
|
||||
const authorization = options.platform === 'native' ? options.native : options.web
|
||||
const authorization = options.web
|
||||
let initialized = false
|
||||
let state: YunlefunAuthSnapshot = {
|
||||
account: null,
|
||||
|
||||
@@ -9,8 +9,6 @@ export interface YunlefunSsoConfigInput {
|
||||
clientId?: string
|
||||
cloudbaseEnv?: string
|
||||
exchangeUrl?: string
|
||||
nativeClientId?: string
|
||||
nativeRedirectUri?: string
|
||||
redirectUri?: string
|
||||
redirectUris?: string | string[]
|
||||
scope?: string
|
||||
@@ -19,7 +17,6 @@ export interface YunlefunSsoConfigInput {
|
||||
|
||||
export interface YunlefunSsoConfigContext {
|
||||
currentOrigin: string
|
||||
platform: 'native' | 'web'
|
||||
}
|
||||
|
||||
export interface YunlefunSsoConfig {
|
||||
@@ -39,22 +36,18 @@ export function resolveYunlefunSsoConfig(
|
||||
input: YunlefunSsoConfigInput,
|
||||
context: YunlefunSsoConfigContext,
|
||||
): YunlefunSsoConfigResult {
|
||||
const clientId = readSsoClientId(
|
||||
context.platform === 'native' ? input.nativeClientId : input.clientId,
|
||||
)
|
||||
const clientId = readSsoClientId(input.clientId)
|
||||
const cloudbaseEnv = normalizeValue(input.cloudbaseEnv)
|
||||
const exchangeUrl = readSsoRedirectUri(input.exchangeUrl)
|
||||
const webRedirectUris = readWebRedirectUris(input.redirectUris ?? input.redirectUri)
|
||||
const redirectUri = context.platform === 'native'
|
||||
? readSsoRedirectUri(input.nativeRedirectUri)
|
||||
: webRedirectUris.find(uri => new URL(uri).origin === context.currentOrigin)
|
||||
const redirectUri = webRedirectUris.find(uri => new URL(uri).origin === context.currentOrigin)
|
||||
const scope = readSsoScope(input.scope)
|
||||
const ssoOrigin = normalizeHttpsOrigin(input.ssoOrigin)
|
||||
|
||||
if (!clientId
|
||||
|| !CLOUDBASE_ENV_RE.test(cloudbaseEnv)
|
||||
|| !exchangeUrl
|
||||
|| (context.platform === 'native' ? !redirectUri : webRedirectUris.length === 0)
|
||||
|| webRedirectUris.length === 0
|
||||
|| scope.length === 0
|
||||
|| !ssoOrigin) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user