@msdongcha/auth-kit 0.0.1 → 0.0.2
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 +137 -10
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,152 @@
|
|
|
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)` 返回的客户端
|
|
13
43
|
|
|
14
|
-
|
|
15
|
-
|
|
44
|
+
### `validate()`
|
|
45
|
+
|
|
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)
|
|
17
77
|
```
|
|
18
78
|
|
|
19
|
-
`
|
|
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
|
+
### `logout()`
|
|
109
|
+
|
|
110
|
+
只清空 `TokenStore`,不发送网络请求。
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await auth.logout()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 其他公开工具
|
|
117
|
+
|
|
118
|
+
### `sanitizeCallbackUrl(url)`
|
|
119
|
+
|
|
120
|
+
返回已删除认证参数的路径、查询和 hash,适合回调页在写入 TokenStore 后清理浏览器地址。
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
window.history.replaceState(null, '', sanitizeCallbackUrl(window.location.href))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `resolveTenantDecision(input)`
|
|
127
|
+
|
|
128
|
+
优先消费 `AuthContext` 中受信任的 selected tenant。只有显式设置 `allowLegacyDefault: true` 时,才兼容一个激活的 legacy `isDefault` membership。
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const decision = resolveTenantDecision({ context: authContext })
|
|
132
|
+
if (decision.ok) useTenant(decision.value.selectedTenant.id)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `createAuthKit(config)`
|
|
136
|
+
|
|
137
|
+
旧版兼容入口。它返回 `validateConfiguration()`、`buildAuthorizeUrl()`、`decideDestination(context)` 和 `applySession(result, adapter?)`。新业务使用 `create()`;此入口仅保留给仍依赖旧配置和返回形状的调用方。
|
|
138
|
+
|
|
139
|
+
### `createRefreshCoordinator()`
|
|
140
|
+
|
|
141
|
+
创建旧版 refresh single-flight 协调器。`refresh(task)` 会让同一实例中并发的布尔刷新任务共用一个 Promise。
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const coordinator = createRefreshCoordinator()
|
|
145
|
+
await coordinator.refresh(() => refreshBusinessSession())
|
|
146
|
+
```
|
|
20
147
|
|
|
21
|
-
|
|
148
|
+
## 公开类型与结果
|
|
22
149
|
|
|
23
|
-
|
|
150
|
+
`AuthKitConfig`、`AuthClient`、`TokenStore`、`TokenSet`、`AuthIdentity`、`AuthContext`、`AuthKitResult`、`LoginResult`、`AuthorizeOptions`、`TenantDecision` 和相关 legacy 类型均从包根导出。由 `create()` 返回的客户端方法返回 `{ ok: true, value }` 或 `{ ok: false, error, retryable? }`;调用方应先判断 `ok` 再读取 `value`。
|
|
24
151
|
|
|
25
|
-
`
|
|
152
|
+
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' | '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;
|
|
@@ -90,21 +92,26 @@ export interface AuthIdentity {
|
|
|
90
92
|
attrs?: Record<string, unknown>;
|
|
91
93
|
}
|
|
92
94
|
export interface AuthorizeOptions {
|
|
93
|
-
|
|
95
|
+
returnTo?: string;
|
|
96
|
+
app?: string;
|
|
97
|
+
/** @deprecated 请改用 returnTo。 */
|
|
98
|
+
callbackUrl?: string;
|
|
99
|
+
/** @deprecated 请改用 app。 */
|
|
94
100
|
appCode?: string;
|
|
101
|
+
/** @deprecated 请改用 app。 */
|
|
95
102
|
clientId?: string;
|
|
96
103
|
state?: string;
|
|
97
104
|
}
|
|
98
105
|
export interface AuthClient {
|
|
99
106
|
validate: () => AuthKitResult<void>;
|
|
100
|
-
buildAuthorizeUrl: (options
|
|
107
|
+
buildAuthorizeUrl: (options?: AuthorizeOptions) => AuthKitResult<string>;
|
|
101
108
|
login: (username: string, password: string) => Promise<AuthKitResult<LoginResult>>;
|
|
102
109
|
refreshToken: () => Promise<AuthKitResult<LoginResult>>;
|
|
103
110
|
getIdentity: () => Promise<AuthKitResult<AuthIdentity>>;
|
|
104
111
|
getContext: () => Promise<AuthKitResult<AuthContext>>;
|
|
105
112
|
logout: () => Promise<AuthKitResult<void>>;
|
|
106
113
|
}
|
|
107
|
-
/**
|
|
114
|
+
/** 清理不应保留在 history 或 referrer 的认证参数。 */
|
|
108
115
|
export declare function sanitizeCallbackUrl(value: string): string;
|
|
109
116
|
export interface LegacyMembershipCandidate {
|
|
110
117
|
membershipId: string;
|
|
@@ -129,7 +136,7 @@ export declare function createAuthKit(config: LegacyAuthKitConfig): {
|
|
|
129
136
|
validateConfiguration: () => AuthKitResult<{
|
|
130
137
|
loginEntryUrl: string;
|
|
131
138
|
}>;
|
|
132
|
-
buildAuthorizeUrl: (
|
|
139
|
+
buildAuthorizeUrl: (_requestedTenantId?: string) => AuthKitResult<{
|
|
133
140
|
url: string;
|
|
134
141
|
}>;
|
|
135
142
|
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
|
}
|
|
@@ -143,20 +174,20 @@ export function create(config) {
|
|
|
143
174
|
return fail('AUTH_DOMAIN_INVALID');
|
|
144
175
|
return ok(undefined);
|
|
145
176
|
}
|
|
146
|
-
function buildAuthorizeUrl(options) {
|
|
177
|
+
function buildAuthorizeUrl(options = {}) {
|
|
147
178
|
const valid = validate();
|
|
148
179
|
if (!valid.ok)
|
|
149
180
|
return valid;
|
|
150
|
-
const
|
|
181
|
+
const url = new URL('/auth/login', `${authDomain}/`);
|
|
182
|
+
const app = options.app ?? config.app ?? options.appCode ?? options.clientId ?? config.appCode ?? config.clientId ?? '';
|
|
183
|
+
const configuredReturnTo = options.returnTo ?? options.callbackUrl;
|
|
184
|
+
const returnTo = configuredReturnTo ?? currentBrowserUrl();
|
|
185
|
+
if (returnTo === null)
|
|
186
|
+
return fail('RETURN_TO_UNAVAILABLE');
|
|
187
|
+
const callbackUrl = parseHttpUrl(returnTo, config.runtime === 'development' || config.runtime === 'test');
|
|
151
188
|
if (!callbackUrl)
|
|
152
189
|
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());
|
|
190
|
+
url.searchParams.set('app', app);
|
|
160
191
|
url.searchParams.set('return_to', callbackUrl.toString());
|
|
161
192
|
if (options.state)
|
|
162
193
|
url.searchParams.set('state', options.state);
|
|
@@ -176,7 +207,10 @@ export function create(config) {
|
|
|
176
207
|
});
|
|
177
208
|
if (!response.ok)
|
|
178
209
|
return responseError(response.status);
|
|
179
|
-
const
|
|
210
|
+
const responseBody = unwrapResponse(await response.json());
|
|
211
|
+
if (!responseBody.ok)
|
|
212
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
213
|
+
const body = responseBody.value;
|
|
180
214
|
const tokens = toTokenSet(body);
|
|
181
215
|
if (!tokens)
|
|
182
216
|
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
@@ -202,7 +236,12 @@ export function create(config) {
|
|
|
202
236
|
tokenStore.clear();
|
|
203
237
|
return response.status === 401 ? fail('TOKEN_EXPIRED') : responseError(response.status);
|
|
204
238
|
}
|
|
205
|
-
const
|
|
239
|
+
const responseBody = unwrapResponse(await response.json());
|
|
240
|
+
if (!responseBody.ok) {
|
|
241
|
+
tokenStore.clear();
|
|
242
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
243
|
+
}
|
|
244
|
+
const body = responseBody.value;
|
|
206
245
|
const tokens = toTokenSet(body);
|
|
207
246
|
if (!tokens) {
|
|
208
247
|
tokenStore.clear();
|
|
@@ -221,7 +260,7 @@ export function create(config) {
|
|
|
221
260
|
refreshPending = runRefresh().finally(() => { refreshPending = null; });
|
|
222
261
|
return refreshPending;
|
|
223
262
|
}
|
|
224
|
-
async function
|
|
263
|
+
async function getRemoteIdentity() {
|
|
225
264
|
const current = tokenStore.get();
|
|
226
265
|
if (!current?.accessToken)
|
|
227
266
|
return fail('LOGIN_REQUIRED');
|
|
@@ -229,31 +268,32 @@ export function create(config) {
|
|
|
229
268
|
const response = await fetchImpl(`${authDomain}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${current.accessToken}` } });
|
|
230
269
|
if (!response.ok)
|
|
231
270
|
return response.status === 401 ? fail('TOKEN_EXPIRED') : responseError(response.status);
|
|
232
|
-
const
|
|
233
|
-
if (!
|
|
234
|
-
return fail('
|
|
235
|
-
|
|
236
|
-
return fail('APP_ACCESS_DENIED');
|
|
237
|
-
return ok(context);
|
|
271
|
+
const responseBody = unwrapResponse(await response.json());
|
|
272
|
+
if (!responseBody.ok)
|
|
273
|
+
return fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
274
|
+
return ok(responseBody.value);
|
|
238
275
|
}
|
|
239
276
|
catch {
|
|
240
277
|
return fail('AUTH_PROTOCOL_UNAVAILABLE', true);
|
|
241
278
|
}
|
|
242
279
|
}
|
|
280
|
+
async function getContext() {
|
|
281
|
+
const remoteIdentity = await getRemoteIdentity();
|
|
282
|
+
if (!remoteIdentity.ok)
|
|
283
|
+
return remoteIdentity;
|
|
284
|
+
const context = normalizeContext(remoteIdentity.value);
|
|
285
|
+
if (!context)
|
|
286
|
+
return fail('AUTH_CONTEXT_MISSING');
|
|
287
|
+
if (!context.appAccess?.allowed)
|
|
288
|
+
return fail('APP_ACCESS_DENIED');
|
|
289
|
+
return ok(context);
|
|
290
|
+
}
|
|
243
291
|
async function getIdentity() {
|
|
244
|
-
const
|
|
245
|
-
if (!
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!response.ok)
|
|
250
|
-
return response.status === 401 ? fail('TOKEN_EXPIRED') : responseError(response.status);
|
|
251
|
-
const identity = normalizeIdentity(await response.json());
|
|
252
|
-
return identity ? ok(identity) : fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return fail('AUTH_PROTOCOL_UNAVAILABLE', true);
|
|
256
|
-
}
|
|
292
|
+
const remoteIdentity = await getRemoteIdentity();
|
|
293
|
+
if (!remoteIdentity.ok)
|
|
294
|
+
return remoteIdentity;
|
|
295
|
+
const identity = normalizeIdentity(remoteIdentity.value);
|
|
296
|
+
return identity ? ok(identity) : fail('AUTH_PROTOCOL_UNAVAILABLE');
|
|
257
297
|
}
|
|
258
298
|
async function logout() {
|
|
259
299
|
tokenStore.clear();
|
|
@@ -276,18 +316,15 @@ export function createAuthKit(config) {
|
|
|
276
316
|
return fail('LOGIN_ENTRY_ORIGIN_DENIED');
|
|
277
317
|
return ok({ loginEntryUrl: entryUrl.toString() });
|
|
278
318
|
}
|
|
279
|
-
function buildAuthorizeUrl(
|
|
319
|
+
function buildAuthorizeUrl(_requestedTenantId) {
|
|
280
320
|
const valid = validateConfiguration();
|
|
281
321
|
if (!valid.ok)
|
|
282
322
|
return valid;
|
|
283
323
|
if (config.isLoginHost)
|
|
284
324
|
return fail('AUTH_ENTRY_CALLBACK_LOOP');
|
|
285
325
|
const url = new URL(valid.value.loginEntryUrl);
|
|
286
|
-
url.searchParams.set('client_id', config.clientId);
|
|
287
326
|
url.searchParams.set('app', config.appCode);
|
|
288
327
|
url.searchParams.set('return_to', callbackUrl.toString());
|
|
289
|
-
if (requestedTenantId)
|
|
290
|
-
url.searchParams.set('requested_tenant_id', requestedTenantId);
|
|
291
328
|
return ok({ url: url.toString() });
|
|
292
329
|
}
|
|
293
330
|
function decideDestination(context) {
|
package/package.json
CHANGED