@huaqiu/dsh-auth 0.5.1 → 0.5.3
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/lib/client.js +767 -34
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/client.ts +63 -0
- package/src/client/i18n.ts +37 -0
- package/src/client/index.tsx +15 -0
- package/src/client/lib.ts +111 -0
- package/src/client/ui/sidebar-action.tsx +477 -20
- package/src/client/ui/subscription-dialog.ts +183 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-auth",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.0",
|
|
35
|
-
"@huaqiu/dsh-plugin-log": "0.5.
|
|
35
|
+
"@huaqiu/dsh-plugin-log": "0.5.3"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"lib",
|
package/src/client/client.ts
CHANGED
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
import {
|
|
6
6
|
AUTH_ORIGIN,
|
|
7
7
|
buildLoginUrl,
|
|
8
|
+
buildSubscriptionUrl,
|
|
9
|
+
fetchSubscriptionQuota,
|
|
8
10
|
handleAuthMessage,
|
|
11
|
+
SUBSCRIPTION_PREFERRED_SIZE,
|
|
9
12
|
type AuthMessageEventLike,
|
|
10
13
|
type AuthTokenPayload,
|
|
11
14
|
type LoginOptions,
|
|
15
|
+
type SubscriptionQuota,
|
|
12
16
|
} from './lib.js'
|
|
13
17
|
import { closeLoginDialog, isLoginDialogOpen, openLoginDialog } from './ui/login-dialog.js'
|
|
18
|
+
import { closeSubscriptionDialog, isSubscriptionDialogOpen, openSubscriptionDialog } from './ui/subscription-dialog.js'
|
|
14
19
|
import type { AuthStorage } from './storage.js'
|
|
15
20
|
import type { AuthTransport } from './transport.js'
|
|
16
21
|
|
|
@@ -22,6 +27,19 @@ export interface AuthClientDeps {
|
|
|
22
27
|
loginUrl?: string
|
|
23
28
|
windowLike: Pick<Window, 'addEventListener' | 'removeEventListener'>
|
|
24
29
|
documentLike: Pick<Document, 'createElement' | 'body'>
|
|
30
|
+
/**
|
|
31
|
+
* Host-side native dialog opener (wired by the client entry from the lazy
|
|
32
|
+
* `hqEdge` service). In host mode the subscription dialog must be opened by
|
|
33
|
+
* the EDA host through DialogService.OpenUrl (hqEdge.openUrl) — a browser
|
|
34
|
+
* iframe cannot be shown over the host UI. Absent/undefined = the host has
|
|
35
|
+
* no dialog service; `openSubscriptionDialog()` then no-ops and the caller
|
|
36
|
+
* degrades gracefully.
|
|
37
|
+
*/
|
|
38
|
+
openHostDialog?: (url: string, options: {
|
|
39
|
+
title?: string
|
|
40
|
+
size?: { width?: number; height?: number }
|
|
41
|
+
key?: string
|
|
42
|
+
}) => Promise<void>
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
export interface AuthClient {
|
|
@@ -46,6 +64,22 @@ export interface AuthClient {
|
|
|
46
64
|
* the UI keeps showing the authenticated state.
|
|
47
65
|
*/
|
|
48
66
|
logout(): Promise<void>
|
|
67
|
+
/**
|
|
68
|
+
* Active subscription quota for one eda.cn product, fetched DIRECTLY from
|
|
69
|
+
* eda.cn (`/sub-api/subscriptions/active`) — no hq-edge hop, so the
|
|
70
|
+
* sidebar can show live quota even in standalone DSH. Null on fetch
|
|
71
|
+
* failure / no credential (callers decide fail-open vs fail-closed).
|
|
72
|
+
*/
|
|
73
|
+
getSubscriptionQuota(productCode: string): Promise<SubscriptionQuota | null>
|
|
74
|
+
/**
|
|
75
|
+
* Open the subscription landing page (`gen.eda.cn/subscription` with the
|
|
76
|
+
* token). Host mode: ask the EDA host to open its NATIVE dialog through
|
|
77
|
+
* DialogService.OpenUrl (preferred size 1400×740). Standalone DSH: open
|
|
78
|
+
* the in-app iframe dialog at the same preferred size. No-op when the
|
|
79
|
+
* credential is missing; host mode without a dialog service no-ops too
|
|
80
|
+
* (the UI falls back to its own subscribe button + error copy).
|
|
81
|
+
*/
|
|
82
|
+
openSubscriptionDialog(): Promise<void>
|
|
49
83
|
onAuthStateChanged(listener: (info: AuthTokenPayload | null) => void): () => void
|
|
50
84
|
}
|
|
51
85
|
/** Route window 'message' events here. Exposed for direct testing. */
|
|
@@ -195,6 +229,34 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
|
|
|
195
229
|
emit(null)
|
|
196
230
|
closeIframe()
|
|
197
231
|
},
|
|
232
|
+
getSubscriptionQuota: async (productCode: string): Promise<SubscriptionQuota | null> => {
|
|
233
|
+
if (hostMode && !hostSessionLoaded) await resolveHost()
|
|
234
|
+
const token = await auth.getAccessToken()
|
|
235
|
+
const id = hostMode
|
|
236
|
+
? (hostSession?.id ?? null)
|
|
237
|
+
: (hostSession?.id ?? storage.get()?.id ?? null)
|
|
238
|
+
if (!token || !id) return null
|
|
239
|
+
return fetchSubscriptionQuota({ userId: id, token, productCode })
|
|
240
|
+
},
|
|
241
|
+
openSubscriptionDialog: async (): Promise<void> => {
|
|
242
|
+
if (hostMode && !hostSessionLoaded) await resolveHost()
|
|
243
|
+
const token = await auth.getAccessToken()
|
|
244
|
+
if (!token) return
|
|
245
|
+
const url = buildSubscriptionUrl({ token })
|
|
246
|
+
if (hostMode) {
|
|
247
|
+
// The EDA host owns the UI — open its native dialog (fire-and-forget).
|
|
248
|
+
// No dialog service → no-op; the caller keeps its subscribe button.
|
|
249
|
+
if (!deps.openHostDialog) return
|
|
250
|
+
await deps.openHostDialog(url, {
|
|
251
|
+
title: 'Huaqiu EDA subscription',
|
|
252
|
+
size: { width: SUBSCRIPTION_PREFERRED_SIZE.width, height: SUBSCRIPTION_PREFERRED_SIZE.height },
|
|
253
|
+
key: 'hq-subscription-dialog',
|
|
254
|
+
})
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (isSubscriptionDialogOpen()) return
|
|
258
|
+
openSubscriptionDialog(url, { size: SUBSCRIPTION_PREFERRED_SIZE })
|
|
259
|
+
},
|
|
198
260
|
onAuthStateChanged: (listener: (info: AuthTokenPayload | null) => void): (() => void) => {
|
|
199
261
|
listeners.add(listener)
|
|
200
262
|
return () => listeners.delete(listener)
|
|
@@ -293,6 +355,7 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
|
|
|
293
355
|
dispose() {
|
|
294
356
|
deps.windowLike.removeEventListener('message', onWindowMessage)
|
|
295
357
|
closeIframe()
|
|
358
|
+
if (isSubscriptionDialogOpen()) closeSubscriptionDialog()
|
|
296
359
|
hostMode = false
|
|
297
360
|
hostSession = null
|
|
298
361
|
hostSessionLoaded = false
|
package/src/client/i18n.ts
CHANGED
|
@@ -26,6 +26,25 @@ const zh = {
|
|
|
26
26
|
'menu.profile': '个人中心',
|
|
27
27
|
'menu.logout': '退出登录',
|
|
28
28
|
|
|
29
|
+
// Subscription quota + upgrade (sidebar account menu / HIT cards).
|
|
30
|
+
'subscription.title': '订阅',
|
|
31
|
+
'subscription.dialogTitle': '华秋 EDA 订阅',
|
|
32
|
+
'subscription.gen': '系统设计(GEN)',
|
|
33
|
+
'subscription.erc': '电路检查(ERC)',
|
|
34
|
+
'subscription.package': '套餐:{name}',
|
|
35
|
+
'subscription.quota': '可用次数:{count}',
|
|
36
|
+
'subscription.quotaCount': '{used} / {total}次',
|
|
37
|
+
'subscription.noQuota': '无可用次数',
|
|
38
|
+
'subscription.unsubscribed': '未订阅',
|
|
39
|
+
'subscription.subscribe': '订阅 / 升级',
|
|
40
|
+
'subscription.subscribing': '正在打开订阅…',
|
|
41
|
+
'subscription.desc': '当前账号没有可用的 GEN 生成次数,请订阅或升级套餐后重试。',
|
|
42
|
+
'subscription.hostUnavailable': '无法打开订阅页面(宿主未提供对话框服务)。',
|
|
43
|
+
// Avatar popup (redesigned sidebar account menu).
|
|
44
|
+
'subscription.quotaHeader': '套餐与使用额度',
|
|
45
|
+
'subscription.upgrade': '订阅 / 升级套餐',
|
|
46
|
+
'subscription.online': '在线',
|
|
47
|
+
|
|
29
48
|
'card.title': '华秋 EDA AI(eda.cn)登录',
|
|
30
49
|
'card.desc': '工具「{tool}」需要登录华秋 EDA AI 账号才能继续。请在下方的登录框完成登录(或点击左侧「华秋EDA AI登录」按钮);登录完成后,回复助手「已登录,请重试」,助手会自动重新调用该工具。',
|
|
31
50
|
'card.loggedIn': '✓ 已登录{nickname} —— 现在可以回复助手「已登录,请重试」,助手会重新调用工具。',
|
|
@@ -52,6 +71,24 @@ const en: Record<AuthCopyKey, string> = {
|
|
|
52
71
|
'menu.profile': 'Go to profile',
|
|
53
72
|
'menu.logout': 'Log out',
|
|
54
73
|
|
|
74
|
+
'subscription.title': 'Subscription',
|
|
75
|
+
'subscription.dialogTitle': 'Huaqiu EDA subscription',
|
|
76
|
+
'subscription.gen': 'System design (GEN)',
|
|
77
|
+
'subscription.erc': 'Circuit check (ERC)',
|
|
78
|
+
'subscription.package': 'Plan: {name}',
|
|
79
|
+
'subscription.quota': 'Available: {count}',
|
|
80
|
+
'subscription.quotaCount': '{used} / {total}',
|
|
81
|
+
'subscription.noQuota': 'No quota left',
|
|
82
|
+
'subscription.unsubscribed': 'Not subscribed',
|
|
83
|
+
'subscription.subscribe': 'Subscribe / Upgrade',
|
|
84
|
+
'subscription.subscribing': 'Opening subscription…',
|
|
85
|
+
'subscription.desc': 'This account has no GEN generation quota left. Subscribe or upgrade to continue.',
|
|
86
|
+
'subscription.hostUnavailable': 'Cannot open the subscription page (the host provides no dialog service).',
|
|
87
|
+
// Avatar popup (redesigned sidebar account menu).
|
|
88
|
+
'subscription.quotaHeader': 'Plan & usage quota',
|
|
89
|
+
'subscription.upgrade': 'Subscribe / Upgrade plan',
|
|
90
|
+
'subscription.online': 'Online',
|
|
91
|
+
|
|
55
92
|
'card.title': 'Huaqiu EDA AI (eda.cn) login',
|
|
56
93
|
// The reply phrase used to be hardcoded to the Chinese "已登录,请重试" even
|
|
57
94
|
// in these English strings, telling an English-speaking user to type Chinese.
|
package/src/client/index.tsx
CHANGED
|
@@ -73,6 +73,21 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
73
73
|
transport: createWebServerAuthTransport(),
|
|
74
74
|
windowLike: window,
|
|
75
75
|
documentLike: document,
|
|
76
|
+
// Host mode subscription dialog: the EDA host owns the UI. Open its
|
|
77
|
+
// native window through the hqEdge service's DialogService.OpenUrl
|
|
78
|
+
// bridge (fire-and-forget); absent hqEdge → no-op, the caller's
|
|
79
|
+
// subscribe button stays usable and reports the limitation.
|
|
80
|
+
openHostDialog: async (url, options) => {
|
|
81
|
+
const hqEdge = ctx.get?.<{
|
|
82
|
+
openUrl?(url: string, opts?: Record<string, unknown>): Promise<unknown>
|
|
83
|
+
} | undefined>('hqEdge')
|
|
84
|
+
if (!hqEdge || typeof hqEdge.openUrl !== 'function') return
|
|
85
|
+
await hqEdge.openUrl(url, {
|
|
86
|
+
...(options.title ? { title: options.title } : {}),
|
|
87
|
+
...(options.size ? { size: options.size } : {}),
|
|
88
|
+
...(options.key ? { key: options.key } : {}),
|
|
89
|
+
})
|
|
90
|
+
},
|
|
76
91
|
})
|
|
77
92
|
|
|
78
93
|
// The sidebar needs the host identity (targetHost) to decide whether the
|
package/src/client/lib.ts
CHANGED
|
@@ -24,6 +24,117 @@ export const AUTH_ORIGIN = 'https://auth.eda.cn'
|
|
|
24
24
|
/**「Go to profile」destination: the eda.cn account page. */
|
|
25
25
|
export const PROFILE_URL = 'https://www.eda.cn/account/profile'
|
|
26
26
|
|
|
27
|
+
// ── Subscription (quota + upgrade dialog) ────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Subscription landing page — the same page `hq-eda-ai` serves at
|
|
31
|
+
* `gen.eda.cn/subscription` (token taken from the URL query; see its
|
|
32
|
+
* `UserInfoContext`). The preferred window size for BOTH surfaces (KiCad
|
|
33
|
+
* native dialog via DialogService.OpenUrl and the standalone iframe dialog)
|
|
34
|
+
* is 1400×740.
|
|
35
|
+
*/
|
|
36
|
+
export const SUBSCRIPTION_ORIGIN = 'https://gen.eda.cn'
|
|
37
|
+
export const SUBSCRIPTION_PATH = '/subscription'
|
|
38
|
+
export const SUBSCRIPTION_PREFERRED_SIZE = { width: 1400, height: 740 } as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* eda.cn subscription product codes.
|
|
42
|
+
* - GEN (system/schematic design): `auto-design` — the SAME product code
|
|
43
|
+
* `hq-eda-ai` uses (`apps/web/src/lib/config/product-code.ts`), i.e. the
|
|
44
|
+
* subscription sold on `gen.eda.cn/subscription`.
|
|
45
|
+
* - ERC: `erc` — the code `hq-edge`'s own quota proxy
|
|
46
|
+
* (`apps/server/src/routes/project.ts`) queries with.
|
|
47
|
+
*/
|
|
48
|
+
export const GEN_PRODUCT_CODE = 'auto-design'
|
|
49
|
+
export const ERC_PRODUCT_CODE = 'erc'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the subscription landing URL. Mirrors `buildProfileUrl`: the token is
|
|
53
|
+
* always attached — `gen.eda.cn/subscription` consumes it to establish the
|
|
54
|
+
* session and hides it itself. `encodeURIComponent` is required (tokens are
|
|
55
|
+
* base64-ish and may contain `+`, `/` or `=`).
|
|
56
|
+
*/
|
|
57
|
+
export function buildSubscriptionUrl(options: { token: string }): string {
|
|
58
|
+
return `${SUBSCRIPTION_ORIGIN}${SUBSCRIPTION_PATH}?token=${encodeURIComponent(options.token)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Normalized active-subscription quota view (whatever the backend sends). */
|
|
62
|
+
export interface SubscriptionQuota {
|
|
63
|
+
hasSubscription: boolean
|
|
64
|
+
packageName: string
|
|
65
|
+
currentQuota: number
|
|
66
|
+
totalQuota: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SUB_API_PREFIX = '/sub-api'
|
|
70
|
+
const SUB_API_SUCCESS_CODES = [200, 200000]
|
|
71
|
+
|
|
72
|
+
function unwrapSubApiPayload<T>(payload: unknown): T | null {
|
|
73
|
+
if (payload && typeof payload === 'object' && 'code' in payload) {
|
|
74
|
+
const p = payload as { code: number; result?: T; data?: T }
|
|
75
|
+
if (!SUB_API_SUCCESS_CODES.includes(p.code)) return null
|
|
76
|
+
if (p.result !== undefined) return p.result as T
|
|
77
|
+
if (p.data !== undefined) return p.data as T
|
|
78
|
+
return undefined as T
|
|
79
|
+
}
|
|
80
|
+
return payload as T
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function quotaOf(subscription: Record<string, unknown> | null | undefined): SubscriptionQuota {
|
|
84
|
+
if (!subscription || (typeof subscription === 'object' && Object.keys(subscription).length === 0)) {
|
|
85
|
+
return { hasSubscription: false, packageName: '', currentQuota: 0, totalQuota: 0 }
|
|
86
|
+
}
|
|
87
|
+
const currentQuota = Number(subscription.currentQuota) || 0
|
|
88
|
+
return {
|
|
89
|
+
hasSubscription: currentQuota > 0,
|
|
90
|
+
packageName: typeof subscription.packageName === 'string' ? subscription.packageName : '',
|
|
91
|
+
currentQuota,
|
|
92
|
+
totalQuota: Number(subscription.totalQuota) || 0,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetch the active subscription quota DIRECTLY from eda.cn
|
|
98
|
+
* (`GET {baseUrl}/sub-api/subscriptions/active?userId&productCode` with
|
|
99
|
+
* `x-token` + `X-User-Id` headers) — the same API `hq-eda-ai`'s
|
|
100
|
+
* `getActiveSubscription` and `hq-edge`'s ERC quota proxy call, so displaying
|
|
101
|
+
* the available quota never depends on an hq-edge hop.
|
|
102
|
+
*
|
|
103
|
+
* Returns null when the fetch/unwrap fails (callers decide fail-open vs
|
|
104
|
+
* fail-closed); a well-formed response always yields a `SubscriptionQuota`
|
|
105
|
+
* (possibly with `hasSubscription: false`).
|
|
106
|
+
*/
|
|
107
|
+
export async function fetchSubscriptionQuota(options: {
|
|
108
|
+
userId: string | number
|
|
109
|
+
token: string
|
|
110
|
+
productCode: string
|
|
111
|
+
baseUrl?: string
|
|
112
|
+
fetchImpl?: typeof fetch
|
|
113
|
+
}): Promise<SubscriptionQuota | null> {
|
|
114
|
+
const { userId, token, productCode, baseUrl = 'https://www.eda.cn' } = options
|
|
115
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
116
|
+
const url = `${baseUrl}${SUB_API_PREFIX}/subscriptions/active?${new URLSearchParams({
|
|
117
|
+
userId: String(userId),
|
|
118
|
+
productCode,
|
|
119
|
+
}).toString()}`
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetchImpl(url, {
|
|
122
|
+
method: 'GET',
|
|
123
|
+
headers: {
|
|
124
|
+
Accept: 'application/json, text/plain, */*',
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
'x-token': token,
|
|
127
|
+
'X-User-Id': String(userId),
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
if (!res || !res.ok) return null
|
|
131
|
+
const json = (await res.json()) as unknown
|
|
132
|
+
return quotaOf(unwrapSubApiPayload<Record<string, unknown>>(json))
|
|
133
|
+
} catch {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
27
138
|
/**
|
|
28
139
|
* Build the「Go to profile」URL: the eda.cn account page WITH the access token
|
|
29
140
|
* in the query, mirroring `hq-eda-ai`'s `UserMenu`
|