@msdongcha/auth-kit 0.0.1 → 0.0.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/CHANGELOG.md +8 -0
- package/README.md +164 -10
- package/dist/index.d.ts +22 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -43
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.0.3 - 2026-10-23
|
|
6
|
+
|
|
7
|
+
- `logout()` 在清理本地 TokenStore 前调用 IAM `/api/v1/auth/logout` 撤销 refresh token,并返回明确的 `LOGOUT_FAILED` 错误。
|
|
8
|
+
- 新增 `getMemberships()`,以统一类型返回当前用户的租户成员列表。
|
|
9
|
+
- 新增 `switchTenant()`,在服务端返回完整 token 后原子切换会话,并保护并发切换。
|
|
10
|
+
|
|
3
11
|
## 0.0.1
|
|
4
12
|
|
|
5
13
|
- 初始发布:无 UI Auth-kit 核心、配置校验、跳转决策、显式 SessionAdapter 与 refresh single-flight。
|
package/README.md
CHANGED
|
@@ -1,25 +1,179 @@
|
|
|
1
1
|
# @msdongcha/auth-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 初始化
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @msdongcha/auth-kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`TokenStore` 由业务应用实现。SDK 只通过它读取、轮换和清理 token;不会读取 React、Router、页面状态或浏览器存储。
|
|
4
10
|
|
|
5
11
|
```ts
|
|
6
|
-
import { create } from '@msdongcha/auth-kit'
|
|
12
|
+
import { create, type TokenStore } from '@msdongcha/auth-kit'
|
|
13
|
+
|
|
14
|
+
const tokenStore: TokenStore = {
|
|
15
|
+
get: () => {
|
|
16
|
+
const accessToken = sessionStorage.getItem('access_token')
|
|
17
|
+
const refreshToken = sessionStorage.getItem('refresh_token')
|
|
18
|
+
return accessToken ? { accessToken, refreshToken: refreshToken ?? undefined } : null
|
|
19
|
+
},
|
|
20
|
+
set: ({ accessToken, refreshToken }) => {
|
|
21
|
+
sessionStorage.setItem('access_token', accessToken)
|
|
22
|
+
if (refreshToken) sessionStorage.setItem('refresh_token', refreshToken)
|
|
23
|
+
else sessionStorage.removeItem('refresh_token')
|
|
24
|
+
},
|
|
25
|
+
clear: () => {
|
|
26
|
+
sessionStorage.removeItem('access_token')
|
|
27
|
+
sessionStorage.removeItem('refresh_token')
|
|
28
|
+
},
|
|
29
|
+
}
|
|
7
30
|
|
|
8
31
|
const auth = create({
|
|
9
32
|
authDomain: 'https://iam.example.com',
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
app: 'business-web',
|
|
34
|
+
tokenStore,
|
|
12
35
|
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`authDomain` 必须是 HTTPS;仅当 `runtime` 为 `development` 或 `test` 时允许 localhost HTTP。`app` 是认证入口使用的应用标识。`fetchImpl` 用于注入 fetch 实现或测试替身。
|
|
39
|
+
|
|
40
|
+
不要配置 `allowedCallbackOrigins`。回跳地址的白名单只由 IAM 服务端配置和校验,SDK 不在客户端复制这项授权决策。
|
|
41
|
+
|
|
42
|
+
## `create(config)` 返回的客户端
|
|
43
|
+
|
|
44
|
+
### `validate()`
|
|
13
45
|
|
|
14
|
-
|
|
15
|
-
|
|
46
|
+
校验认证域名及当前 runtime 是否允许该 URL。
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const result = auth.validate()
|
|
50
|
+
if (!result.ok) console.error(result.error)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `buildAuthorizeUrl(options?)`
|
|
54
|
+
|
|
55
|
+
创建 IAM 登录地址,只输出 `app`、`return_to` 和可选的 `state` 参数。
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const result = auth.buildAuthorizeUrl({
|
|
59
|
+
returnTo: 'https://business.example.com/orders',
|
|
60
|
+
state: 'csrf-state',
|
|
16
61
|
})
|
|
62
|
+
|
|
63
|
+
if (result.ok) window.location.assign(result.value)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
显式传入 `returnTo` 时优先使用。浏览器中省略它时,SDK 使用当前页面的绝对 URL,并删除查询或 hash 中的 `access_token`、`refresh_token`、`code`、`id_token` 和 `token` 参数。非浏览器环境必须显式传入有效的 `returnTo`,否则返回 `RETURN_TO_UNAVAILABLE`。SDK 只校验 URL 格式与 runtime;最终回跳是否允许由 IAM 服务端决定。
|
|
67
|
+
|
|
68
|
+
`appCode`、`clientId` 和 `callbackUrl` 仍是迁移期的兼容别名,分别归一化为 `app` 和 `returnTo`;SDK 不会输出 `client_id` 或 `callback_url`。
|
|
69
|
+
|
|
70
|
+
### `login(username, password)`
|
|
71
|
+
|
|
72
|
+
向 `POST /api/v1/auth/token` 发送 `{ username, password }`,将返回的 token 写入 `TokenStore`,并返回 token、可选的可信上下文和身份信息。
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const result = await auth.login('alice', 'secret')
|
|
76
|
+
if (!result.ok) throw new Error(result.error)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `refreshToken()`
|
|
80
|
+
|
|
81
|
+
使用 `TokenStore` 中的 refresh token 调用 `POST /api/v1/auth/refresh`。同一客户端实例中的并发刷新会合并为一次请求;成功后轮换 token,失败时清空 token。
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const result = await auth.refreshToken()
|
|
85
|
+
if (!result.ok) window.location.assign('/login')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `getIdentity()`
|
|
89
|
+
|
|
90
|
+
从 `TokenStore` 读取 access token,以 Bearer token 调用 `GET /api/v1/auth/me`,返回 IAM 远程响应中的 user、roles 和 permissions。SDK 不使用 JWKS,也不在本地解析 token。
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const result = await auth.getIdentity()
|
|
94
|
+
if (result.ok && result.value.permissions.includes('orders:read')) {
|
|
95
|
+
// 渲染订单入口
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `getContext()`
|
|
100
|
+
|
|
101
|
+
同样通过 `GET /api/v1/auth/me` 获取远程数据,但只在响应包含可信的 `principal_kind`、`selected_tenant`、`default_target` 和 `app_access` 时返回 `AuthContext`。缺少这些字段时返回 `AUTH_CONTEXT_MISSING`;`app_access.allowed` 为 false 时返回 `APP_ACCESS_DENIED`。不会从 roles 或 memberships 推断上下文。
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const result = await auth.getContext()
|
|
105
|
+
if (result.ok) console.log(result.value.selectedTenant)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `getMemberships()`
|
|
109
|
+
|
|
110
|
+
使用 `TokenStore` 中的 access token 调用 `GET /api/v1/auth/memberships`,返回 camelCase 的租户成员列表。SDK 会保留 `membershipStatus`;列表中的成员关系不代表服务端一定允许切换。
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const result = await auth.getMemberships()
|
|
114
|
+
if (result.ok) {
|
|
115
|
+
for (const membership of result.value) {
|
|
116
|
+
console.log(membership.tenantId, membership.isDefault)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `switchTenant(membershipId)`
|
|
122
|
+
|
|
123
|
+
使用当前 access token 调用 `POST /api/v1/auth/switch-tenant`。服务端返回完整的新 access token 和 refresh token 后,SDK 才会原子替换 `TokenStore`;请求失败或响应不完整时保留原会话。
|
|
124
|
+
|
|
125
|
+
同一客户端实例中,对相同 membership 的并发切换会合并为一次请求;切换未完成时请求其他 membership 会返回 `TENANT_SWITCH_IN_PROGRESS`。
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const result = await auth.switchTenant('membership-id')
|
|
129
|
+
if (result.ok) {
|
|
130
|
+
console.log(result.value.identity?.roles)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `logout()`
|
|
135
|
+
|
|
136
|
+
如果 `TokenStore` 中存在 refresh token,则调用 `POST /api/v1/auth/logout` 撤销它,请求体为 `{ refresh_token }`。无论服务端请求成功或失败,SDK 都会清空本地 `TokenStore`;请求失败时返回 `LOGOUT_FAILED`。没有 refresh token 时只执行本地清理。
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const result = await auth.logout()
|
|
140
|
+
if (!result.ok) console.error(result.error)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 其他公开工具
|
|
144
|
+
|
|
145
|
+
### `sanitizeCallbackUrl(url)`
|
|
146
|
+
|
|
147
|
+
返回已删除认证参数的路径、查询和 hash,适合回调页在写入 TokenStore 后清理浏览器地址。
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
window.history.replaceState(null, '', sanitizeCallbackUrl(window.location.href))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `resolveTenantDecision(input)`
|
|
154
|
+
|
|
155
|
+
优先消费 `AuthContext` 中受信任的 selected tenant。只有显式设置 `allowLegacyDefault: true` 时,才兼容一个激活的 legacy `isDefault` membership。
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const decision = resolveTenantDecision({ context: authContext })
|
|
159
|
+
if (decision.ok) useTenant(decision.value.selectedTenant.id)
|
|
17
160
|
```
|
|
18
161
|
|
|
19
|
-
`
|
|
162
|
+
### `createAuthKit(config)`
|
|
163
|
+
|
|
164
|
+
旧版兼容入口。它返回 `validateConfiguration()`、`buildAuthorizeUrl()`、`decideDestination(context)` 和 `applySession(result, adapter?)`。新业务使用 `create()`;此入口仅保留给仍依赖旧配置和返回形状的调用方。
|
|
165
|
+
|
|
166
|
+
### `createRefreshCoordinator()`
|
|
167
|
+
|
|
168
|
+
创建旧版 refresh single-flight 协调器。`refresh(task)` 会让同一实例中并发的布尔刷新任务共用一个 Promise。
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const coordinator = createRefreshCoordinator()
|
|
172
|
+
await coordinator.refresh(() => refreshBusinessSession())
|
|
173
|
+
```
|
|
20
174
|
|
|
21
|
-
|
|
175
|
+
## 公开类型与结果
|
|
22
176
|
|
|
23
|
-
|
|
177
|
+
`AuthKitConfig`、`AuthClient`、`TokenStore`、`TokenSet`、`AuthIdentity`、`AuthContext`、`TenantMembership`、`AuthKitResult`、`LoginResult`、`AuthorizeOptions`、`TenantDecision` 和相关 legacy 类型均从包根导出。由 `create()` 返回的客户端方法返回 `{ ok: true, value }` 或 `{ ok: false, error, retryable? }`;调用方应先判断 `ok` 再读取 `value`。
|
|
24
178
|
|
|
25
|
-
`
|
|
179
|
+
SDK 可以解包 IAM 的 `{ success, message, data }` 响应信封,也兼容裸 JSON 响应。它不实现 IAM 后端授权、回跳白名单或 JWKS token 校验。
|
package/dist/index.d.ts
CHANGED
|
@@ -48,9 +48,11 @@ export interface SessionAdapter<T = void> {
|
|
|
48
48
|
}
|
|
49
49
|
export interface AuthKitConfig {
|
|
50
50
|
authDomain: string;
|
|
51
|
+
app?: string;
|
|
52
|
+
/** @deprecated 请改用 app。 */
|
|
51
53
|
appCode?: string;
|
|
54
|
+
/** @deprecated 请改用 app。 */
|
|
52
55
|
clientId?: string;
|
|
53
|
-
allowedCallbackOrigins?: readonly string[];
|
|
54
56
|
tokenStore?: TokenStore;
|
|
55
57
|
fetchImpl?: typeof fetch;
|
|
56
58
|
runtime?: 'development' | 'test' | 'staging' | 'production';
|
|
@@ -66,7 +68,7 @@ export interface LegacyAuthKitConfig {
|
|
|
66
68
|
allowedLoginOrigins?: readonly string[];
|
|
67
69
|
isLoginHost?: boolean;
|
|
68
70
|
}
|
|
69
|
-
export type AuthErrorCode = 'AUTH_DOMAIN_MISSING' | 'AUTH_DOMAIN_INVALID' | 'CALLBACK_URL_INVALID' | '
|
|
71
|
+
export type AuthErrorCode = 'AUTH_DOMAIN_MISSING' | 'AUTH_DOMAIN_INVALID' | 'CALLBACK_URL_INVALID' | 'RETURN_TO_UNAVAILABLE' | 'AUTH_ENTRY_CALLBACK_LOOP' | 'LOGIN_REQUIRED' | 'INVALID_CREDENTIALS' | 'AUTH_CONTEXT_MISSING' | 'PRINCIPAL_CONTEXT_INVALID' | 'TENANT_CONTEXT_INVALID' | 'TENANT_ACCESS_DENIED' | 'TENANT_SWITCH_IN_PROGRESS' | 'APP_ACCESS_DENIED' | 'CALLBACK_REJECTED' | 'TOKEN_EXPIRED' | 'REFRESH_FAILED' | 'LOGOUT_FAILED' | 'AUTH_PROTOCOL_UNAVAILABLE';
|
|
70
72
|
export type AuthKitError = AuthErrorCode | 'LOGIN_ENTRY_URL_MISSING' | 'INVALID_LOGIN_ENTRY_URL' | 'LOGIN_ENTRY_ORIGIN_DENIED' | 'INVALID_CALLBACK' | 'CROSS_APP_CONTEXT' | 'IDENTITY_CONTEXT_MISSING';
|
|
71
73
|
export type AuthKitResult<T> = {
|
|
72
74
|
ok: true;
|
|
@@ -89,22 +91,36 @@ export interface AuthIdentity {
|
|
|
89
91
|
permissions: string[];
|
|
90
92
|
attrs?: Record<string, unknown>;
|
|
91
93
|
}
|
|
94
|
+
export interface TenantMembership {
|
|
95
|
+
membershipId: string;
|
|
96
|
+
tenantId: string;
|
|
97
|
+
tenantName?: string;
|
|
98
|
+
membershipStatus: string;
|
|
99
|
+
isDefault: boolean;
|
|
100
|
+
}
|
|
92
101
|
export interface AuthorizeOptions {
|
|
93
|
-
|
|
102
|
+
returnTo?: string;
|
|
103
|
+
app?: string;
|
|
104
|
+
/** @deprecated 请改用 returnTo。 */
|
|
105
|
+
callbackUrl?: string;
|
|
106
|
+
/** @deprecated 请改用 app。 */
|
|
94
107
|
appCode?: string;
|
|
108
|
+
/** @deprecated 请改用 app。 */
|
|
95
109
|
clientId?: string;
|
|
96
110
|
state?: string;
|
|
97
111
|
}
|
|
98
112
|
export interface AuthClient {
|
|
99
113
|
validate: () => AuthKitResult<void>;
|
|
100
|
-
buildAuthorizeUrl: (options
|
|
114
|
+
buildAuthorizeUrl: (options?: AuthorizeOptions) => AuthKitResult<string>;
|
|
101
115
|
login: (username: string, password: string) => Promise<AuthKitResult<LoginResult>>;
|
|
102
116
|
refreshToken: () => Promise<AuthKitResult<LoginResult>>;
|
|
103
117
|
getIdentity: () => Promise<AuthKitResult<AuthIdentity>>;
|
|
104
118
|
getContext: () => Promise<AuthKitResult<AuthContext>>;
|
|
119
|
+
getMemberships: () => Promise<AuthKitResult<TenantMembership[]>>;
|
|
120
|
+
switchTenant: (membershipId: string) => Promise<AuthKitResult<LoginResult>>;
|
|
105
121
|
logout: () => Promise<AuthKitResult<void>>;
|
|
106
122
|
}
|
|
107
|
-
/**
|
|
123
|
+
/** 清理不应保留在 history 或 referrer 的认证参数。 */
|
|
108
124
|
export declare function sanitizeCallbackUrl(value: string): string;
|
|
109
125
|
export interface LegacyMembershipCandidate {
|
|
110
126
|
membershipId: string;
|
|
@@ -129,7 +145,7 @@ export declare function createAuthKit(config: LegacyAuthKitConfig): {
|
|
|
129
145
|
validateConfiguration: () => AuthKitResult<{
|
|
130
146
|
loginEntryUrl: string;
|
|
131
147
|
}>;
|
|
132
|
-
buildAuthorizeUrl: (
|
|
148
|
+
buildAuthorizeUrl: (_requestedTenantId?: string) => AuthKitResult<{
|
|
133
149
|
url: string;
|
|
134
150
|
}>;
|
|
135
151
|
decideDestination: (context: AuthContext) => AuthKitResult<{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,wBAAwB,GAAG,eAAe,GAAG,oBAAoB,CAAA;AAEhH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,GAAG,UAAU,CAAA;IAC7B,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,CAAC,CAAA;CACV;AAED,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,QAAQ,GAAG,IAAI,CAAA;IAC1B,GAAG,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAA;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,IAAI;IACtC,GAAG,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACpD,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
const SENSITIVE_CALLBACK_PARAMS = ['access_token', 'refresh_token', 'code', 'id_token', 'token'];
|
|
2
|
+
function removeSensitiveCallbackParams(url) {
|
|
3
|
+
for (const param of SENSITIVE_CALLBACK_PARAMS)
|
|
4
|
+
url.searchParams.delete(param);
|
|
5
|
+
const hash = url.hash.slice(1);
|
|
6
|
+
const queryIndex = hash.indexOf('?');
|
|
7
|
+
const hashQuery = queryIndex >= 0 ? hash.slice(queryIndex + 1) : hash.includes('=') ? hash : null;
|
|
8
|
+
if (!hashQuery)
|
|
9
|
+
return url;
|
|
10
|
+
const hashParams = new URLSearchParams(hashQuery);
|
|
11
|
+
for (const param of SENSITIVE_CALLBACK_PARAMS)
|
|
12
|
+
hashParams.delete(param);
|
|
13
|
+
const sanitizedHashQuery = hashParams.toString();
|
|
14
|
+
url.hash = queryIndex >= 0
|
|
15
|
+
? sanitizedHashQuery ? `${hash.slice(0, queryIndex)}?${sanitizedHashQuery}` : hash.slice(0, queryIndex)
|
|
16
|
+
: sanitizedHashQuery;
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
/** 清理不应保留在 history 或 referrer 的认证参数。 */
|
|
2
20
|
export function sanitizeCallbackUrl(value) {
|
|
3
|
-
const url = new URL(value);
|
|
4
|
-
url.searchParams.delete('access_token');
|
|
5
|
-
url.searchParams.delete('refresh_token');
|
|
6
|
-
url.searchParams.delete('code');
|
|
21
|
+
const url = removeSensitiveCallbackParams(new URL(value));
|
|
7
22
|
return `${url.pathname}${url.search}${url.hash}`;
|
|
8
23
|
}
|
|
9
24
|
function parseHttpUrl(value, allowLocalhost = true) {
|
|
@@ -19,6 +34,16 @@ function parseHttpUrl(value, allowLocalhost = true) {
|
|
|
19
34
|
return null;
|
|
20
35
|
}
|
|
21
36
|
}
|
|
37
|
+
function currentBrowserUrl() {
|
|
38
|
+
if (typeof window === 'undefined')
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
return removeSensitiveCallbackParams(new URL(window.location.href)).toString();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
function ok(value) {
|
|
23
48
|
return { ok: true, value };
|
|
24
49
|
}
|
|
@@ -28,6 +53,12 @@ function fail(error, retryable = false) {
|
|
|
28
53
|
function isRecord(value) {
|
|
29
54
|
return Boolean(value && typeof value === 'object');
|
|
30
55
|
}
|
|
56
|
+
/** 认证服务与业务接口统一使用响应信封,SDK 对外仍返回既有领域对象。 */
|
|
57
|
+
function unwrapResponse(value) {
|
|
58
|
+
if (!isRecord(value) || !('success' in value))
|
|
59
|
+
return { ok: true, value };
|
|
60
|
+
return value.success === true ? { ok: true, value: value.data } : { ok: false };
|
|
61
|
+
}
|
|
31
62
|
function stringValue(value) {
|
|
32
63
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
33
64
|
}
|
|
@@ -90,6 +121,37 @@ function normalizeIdentity(value) {
|
|
|
90
121
|
...(attrs ? { attrs } : {}),
|
|
91
122
|
};
|
|
92
123
|
}
|
|
124
|
+
function normalizeBoolean(value) {
|
|
125
|
+
if (value === true || value === 1 || value === '1' || value === 'true')
|
|
126
|
+
return true;
|
|
127
|
+
if (value === false || value === 0 || value === '0' || value === 'false')
|
|
128
|
+
return false;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function normalizeMemberships(value) {
|
|
132
|
+
if (!isRecord(value) || !Array.isArray(value.memberships))
|
|
133
|
+
return null;
|
|
134
|
+
const memberships = [];
|
|
135
|
+
for (const item of value.memberships) {
|
|
136
|
+
if (!isRecord(item))
|
|
137
|
+
return null;
|
|
138
|
+
const membershipId = stringValue(item.membership_id)?.trim();
|
|
139
|
+
const tenantId = stringValue(item.tenant_id)?.trim();
|
|
140
|
+
const membershipStatus = stringValue(item.membership_status)?.trim();
|
|
141
|
+
const tenantName = stringValue(item.tenant_name)?.trim();
|
|
142
|
+
const isDefault = item.is_default === undefined ? false : normalizeBoolean(item.is_default);
|
|
143
|
+
if (!membershipId || !tenantId || !membershipStatus || isDefault === undefined)
|
|
144
|
+
return null;
|
|
145
|
+
memberships.push({
|
|
146
|
+
membershipId,
|
|
147
|
+
tenantId,
|
|
148
|
+
...(tenantName ? { tenantName } : {}),
|
|
149
|
+
membershipStatus,
|
|
150
|
+
isDefault,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return memberships;
|
|
154
|
+
}
|
|
93
155
|
/** 只消费后端 selected tenant;旧接口仅允许 is_default 兼容,不猜 joined_at 或身份。 */
|
|
94
156
|
export function resolveTenantDecision(input) {
|
|
95
157
|
const context = input.context;
|
|
@@ -136,6 +198,8 @@ export function create(config) {
|
|
|
136
198
|
const tokenStore = config.tokenStore ?? createMemoryStore();
|
|
137
199
|
const authDomain = config.authDomain.replace(/\/$/, '');
|
|
138
200
|
let refreshPending = null;
|
|
201
|
+
let membershipsPending = null;
|
|
202
|
+
let switchPending = null;
|
|
139
203
|
function validate() {
|
|
140
204
|
if (!config.authDomain)
|
|
141
205
|
return fail('AUTH_DOMAIN_MISSING');
|
|
@@ -143,20 +207,20 @@ export function create(config) {
|
|
|
143
207
|
return fail('AUTH_DOMAIN_INVALID');
|
|
144
208
|
return ok(undefined);
|
|
145
209
|
}
|
|
146
|
-
function buildAuthorizeUrl(options) {
|
|
210
|
+
function buildAuthorizeUrl(options = {}) {
|
|
147
211
|
const valid = validate();
|
|
148
212
|
if (!valid.ok)
|
|
149
213
|
return valid;
|
|
150
|
-
const
|
|
214
|
+
const url = new URL('/auth/login', `${authDomain}/`);
|
|
215
|
+
const app = options.app ?? config.app ?? options.appCode ?? options.clientId ?? config.appCode ?? config.clientId ?? '';
|
|
216
|
+
const configuredReturnTo = options.returnTo ?? options.callbackUrl;
|
|
217
|
+
const returnTo = configuredReturnTo ?? currentBrowserUrl();
|
|
218
|
+
if (returnTo === null)
|
|
219
|
+
return fail('RETURN_TO_UNAVAILABLE');
|
|
220
|
+
const callbackUrl = parseHttpUrl(returnTo, config.runtime === 'development' || config.runtime === 'test');
|
|
151
221
|
if (!callbackUrl)
|
|
152
222
|
return fail('CALLBACK_URL_INVALID');
|
|
153
|
-
|
|
154
|
-
if (allowedOrigins.length > 0 && !allowedOrigins.includes(callbackUrl.origin))
|
|
155
|
-
return fail('CALLBACK_ORIGIN_DENIED');
|
|
156
|
-
const url = new URL('/auth/login', `${authDomain}/`);
|
|
157
|
-
url.searchParams.set('app', options.appCode ?? config.appCode ?? '');
|
|
158
|
-
url.searchParams.set('client_id', options.clientId ?? config.clientId ?? '');
|
|
159
|
-
url.searchParams.set('callback_url', callbackUrl.toString());
|
|
223
|
+
url.searchParams.set('app', app);
|
|
160
224
|
url.searchParams.set('return_to', callbackUrl.toString());
|
|
161
225
|
if (options.state)
|
|
162
226
|
url.searchParams.set('state', options.state);
|
|
@@ -176,7 +240,10 @@ export function create(config) {
|
|
|
176
240
|
});
|
|
177
241
|
if (!response.ok)
|
|
178
242
|
return responseError(response.status);
|
|
179
|
-
const
|
|
243
|
+
const responseBody = unwrapResponse(await response.json());
|
|
244
|
+
if (!responseBody.ok)
|
|
245
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
246
|
+
const body = responseBody.value;
|
|
180
247
|
const tokens = toTokenSet(body);
|
|
181
248
|
if (!tokens)
|
|
182
249
|
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
@@ -202,7 +269,12 @@ export function create(config) {
|
|
|
202
269
|
tokenStore.clear();
|
|
203
270
|
return response.status === 401 ? fail('TOKEN_EXPIRED') : responseError(response.status);
|
|
204
271
|
}
|
|
205
|
-
const
|
|
272
|
+
const responseBody = unwrapResponse(await response.json());
|
|
273
|
+
if (!responseBody.ok) {
|
|
274
|
+
tokenStore.clear();
|
|
275
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
276
|
+
}
|
|
277
|
+
const body = responseBody.value;
|
|
206
278
|
const tokens = toTokenSet(body);
|
|
207
279
|
if (!tokens) {
|
|
208
280
|
tokenStore.clear();
|
|
@@ -221,45 +293,131 @@ export function create(config) {
|
|
|
221
293
|
refreshPending = runRefresh().finally(() => { refreshPending = null; });
|
|
222
294
|
return refreshPending;
|
|
223
295
|
}
|
|
224
|
-
async function
|
|
296
|
+
async function getAuthorizedJson(path, init = {}, forbiddenError = 'APP_ACCESS_DENIED', preserveLegacyErrors = false) {
|
|
225
297
|
const current = tokenStore.get();
|
|
226
298
|
if (!current?.accessToken)
|
|
227
299
|
return fail('LOGIN_REQUIRED');
|
|
300
|
+
let response;
|
|
228
301
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return fail('APP_ACCESS_DENIED');
|
|
237
|
-
return ok(context);
|
|
302
|
+
response = await fetchImpl(`${authDomain}${path}`, {
|
|
303
|
+
...init,
|
|
304
|
+
headers: {
|
|
305
|
+
Authorization: `Bearer ${current.accessToken}`,
|
|
306
|
+
...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
307
|
+
},
|
|
308
|
+
});
|
|
238
309
|
}
|
|
239
310
|
catch {
|
|
240
311
|
return fail('AUTH_PROTOCOL_UNAVAILABLE', true);
|
|
241
312
|
}
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
if (response.status === 401)
|
|
315
|
+
return fail('TOKEN_EXPIRED');
|
|
316
|
+
if (response.status === 403)
|
|
317
|
+
return fail(forbiddenError);
|
|
318
|
+
if (preserveLegacyErrors)
|
|
319
|
+
return responseError(response.status);
|
|
320
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE', response.status === 429 || response.status >= 500);
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const responseBody = unwrapResponse(await response.json());
|
|
324
|
+
if (!responseBody.ok)
|
|
325
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
326
|
+
return ok(responseBody.value);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE', preserveLegacyErrors);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function getRemoteIdentity() {
|
|
333
|
+
return getAuthorizedJson('/api/v1/auth/me', {}, 'APP_ACCESS_DENIED', true);
|
|
334
|
+
}
|
|
335
|
+
async function getContext() {
|
|
336
|
+
const remoteIdentity = await getRemoteIdentity();
|
|
337
|
+
if (!remoteIdentity.ok)
|
|
338
|
+
return remoteIdentity;
|
|
339
|
+
const context = normalizeContext(remoteIdentity.value);
|
|
340
|
+
if (!context)
|
|
341
|
+
return fail('AUTH_CONTEXT_MISSING');
|
|
342
|
+
if (!context.appAccess?.allowed)
|
|
343
|
+
return fail('APP_ACCESS_DENIED');
|
|
344
|
+
return ok(context);
|
|
242
345
|
}
|
|
243
346
|
async function getIdentity() {
|
|
244
|
-
const
|
|
245
|
-
if (!
|
|
246
|
-
return
|
|
347
|
+
const remoteIdentity = await getRemoteIdentity();
|
|
348
|
+
if (!remoteIdentity.ok)
|
|
349
|
+
return remoteIdentity;
|
|
350
|
+
const identity = normalizeIdentity(remoteIdentity.value);
|
|
351
|
+
return identity ? ok(identity) : fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
352
|
+
}
|
|
353
|
+
async function runGetMemberships() {
|
|
354
|
+
const result = await getAuthorizedJson('/api/v1/auth/memberships', {}, 'TENANT_ACCESS_DENIED');
|
|
355
|
+
if (!result.ok)
|
|
356
|
+
return result;
|
|
357
|
+
const memberships = normalizeMemberships(result.value);
|
|
358
|
+
return memberships ? ok(memberships) : fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
359
|
+
}
|
|
360
|
+
function getMemberships() {
|
|
361
|
+
if (!membershipsPending)
|
|
362
|
+
membershipsPending = runGetMemberships().finally(() => { membershipsPending = null; });
|
|
363
|
+
return membershipsPending;
|
|
364
|
+
}
|
|
365
|
+
async function runSwitchTenant(membershipId) {
|
|
366
|
+
const result = await getAuthorizedJson('/api/v1/auth/switch-tenant', {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
body: JSON.stringify({ membership_id: membershipId }),
|
|
369
|
+
}, 'TENANT_ACCESS_DENIED');
|
|
370
|
+
if (!result.ok)
|
|
371
|
+
return result;
|
|
372
|
+
const tokens = toTokenSet(result.value);
|
|
373
|
+
if (!tokens?.refreshToken)
|
|
374
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
375
|
+
const context = isRecord(result.value) ? normalizeContext(result.value.context ?? result.value.auth_context) ?? undefined : undefined;
|
|
376
|
+
const identity = normalizeIdentity(result.value) ?? undefined;
|
|
377
|
+
tokenStore.set(tokens);
|
|
378
|
+
return ok({ ...tokens, context, identity });
|
|
379
|
+
}
|
|
380
|
+
function switchTenant(rawMembershipId) {
|
|
381
|
+
const membershipId = rawMembershipId.trim();
|
|
382
|
+
if (!membershipId)
|
|
383
|
+
return Promise.resolve(fail('TENANT_CONTEXT_INVALID'));
|
|
384
|
+
if (switchPending) {
|
|
385
|
+
return switchPending.membershipId === membershipId
|
|
386
|
+
? switchPending.promise
|
|
387
|
+
: Promise.resolve(fail('TENANT_SWITCH_IN_PROGRESS'));
|
|
388
|
+
}
|
|
389
|
+
const promise = runSwitchTenant(membershipId).finally(() => {
|
|
390
|
+
if (switchPending?.promise === promise)
|
|
391
|
+
switchPending = null;
|
|
392
|
+
});
|
|
393
|
+
switchPending = { membershipId, promise };
|
|
394
|
+
return promise;
|
|
395
|
+
}
|
|
396
|
+
async function logout() {
|
|
247
397
|
try {
|
|
248
|
-
const
|
|
398
|
+
const current = tokenStore.get();
|
|
399
|
+
if (!current?.refreshToken)
|
|
400
|
+
return ok(undefined);
|
|
401
|
+
const valid = validate();
|
|
402
|
+
if (!valid.ok)
|
|
403
|
+
return valid;
|
|
404
|
+
const response = await fetchImpl(`${authDomain}/api/v1/auth/logout`, {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
407
|
+
body: JSON.stringify({ refresh_token: current.refreshToken }),
|
|
408
|
+
});
|
|
249
409
|
if (!response.ok)
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
return identity ? ok(identity) : fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
410
|
+
return fail('LOGOUT_FAILED', response.status >= 408);
|
|
411
|
+
return ok(undefined);
|
|
253
412
|
}
|
|
254
413
|
catch {
|
|
255
|
-
return fail('
|
|
414
|
+
return fail('LOGOUT_FAILED', true);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
tokenStore.clear();
|
|
256
418
|
}
|
|
257
419
|
}
|
|
258
|
-
|
|
259
|
-
tokenStore.clear();
|
|
260
|
-
return ok(undefined);
|
|
261
|
-
}
|
|
262
|
-
return { validate, buildAuthorizeUrl, login, refreshToken, getIdentity, getContext, logout };
|
|
420
|
+
return { validate, buildAuthorizeUrl, login, refreshToken, getIdentity, getContext, getMemberships, switchTenant, logout };
|
|
263
421
|
}
|
|
264
422
|
/** 创建旧版兼容实例;新业务接入请使用 create。 */
|
|
265
423
|
export function createAuthKit(config) {
|
|
@@ -276,18 +434,15 @@ export function createAuthKit(config) {
|
|
|
276
434
|
return fail('LOGIN_ENTRY_ORIGIN_DENIED');
|
|
277
435
|
return ok({ loginEntryUrl: entryUrl.toString() });
|
|
278
436
|
}
|
|
279
|
-
function buildAuthorizeUrl(
|
|
437
|
+
function buildAuthorizeUrl(_requestedTenantId) {
|
|
280
438
|
const valid = validateConfiguration();
|
|
281
439
|
if (!valid.ok)
|
|
282
440
|
return valid;
|
|
283
441
|
if (config.isLoginHost)
|
|
284
442
|
return fail('AUTH_ENTRY_CALLBACK_LOOP');
|
|
285
443
|
const url = new URL(valid.value.loginEntryUrl);
|
|
286
|
-
url.searchParams.set('client_id', config.clientId);
|
|
287
444
|
url.searchParams.set('app', config.appCode);
|
|
288
445
|
url.searchParams.set('return_to', callbackUrl.toString());
|
|
289
|
-
if (requestedTenantId)
|
|
290
|
-
url.searchParams.set('requested_tenant_id', requestedTenantId);
|
|
291
446
|
return ok({ url: url.toString() });
|
|
292
447
|
}
|
|
293
448
|
function decideDestination(context) {
|
package/package.json
CHANGED