@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 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
- 无 UI、framework-agnostic 的认证域名校验、统一登录地址、账号密码登录、refresh、可信上下文消费与 logout 工具。它不包含 React、Router、页面、表单、样式或后端授权实现。
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
- appCode: 'business-web',
11
- allowedCallbackOrigins: ['https://business.example.com'],
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
- const authorizeUrl = auth.buildAuthorizeUrl({
15
- callbackUrl: 'https://business.example.com/auth/callback',
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
- `getContext()` 需要后端返回可信 `principal_kind`、`selected_tenant`、`default_target` 和 `app_access`。当前服务端响应缺失这些字段时,SDK 返回 `AUTH_CONTEXT_MISSING` 或 `APP_ACCESS_DENIED`,不会从 roles 或 memberships 猜测。
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
- `getIdentity()` 是旧 `/api/v1/auth/me` 的兼容读取入口,只用于 IAM 内部页面展示 user、tenant、membership、roles 和 permissions;它不能替代可信 `AuthContext`,也不能放行外部业务 callback。
175
+ ## 公开类型与结果
22
176
 
23
- 应用应通过一个 `TokenStore` adapter 将 SDK 接到自己的 session store。SDK 负责 login/refresh HTTP 与 token rotation,应用业务 client 只负责 Bearer 注入和一次重试。
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
- `createAuthKit` 和 `createRefreshCoordinator` 保留为旧版本兼容入口,新业务请使用 `create`。
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' | 'CALLBACK_ORIGIN_DENIED' | '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';
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
- callbackUrl: string;
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: AuthorizeOptions) => AuthKitResult<string>;
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
- /** 清理回调 URL 中不应保留在 history 或 referrer 的认证参数。 */
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: (requestedTenantId?: string) => AuthKitResult<{
148
+ buildAuthorizeUrl: (_requestedTenantId?: string) => AuthKitResult<{
133
149
  url: string;
134
150
  }>;
135
151
  decideDestination: (context: AuthContext) => AuthKitResult<{
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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
- /** 清理回调 URL 中不应保留在 history 或 referrer 的认证参数。 */
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 callbackUrl = parseHttpUrl(options.callbackUrl, config.runtime === 'development' || config.runtime === 'test');
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
- const allowedOrigins = config.allowedCallbackOrigins ?? [];
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 body = await response.json();
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 body = await response.json();
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 getContext() {
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
- const response = await fetchImpl(`${authDomain}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${current.accessToken}` } });
230
- if (!response.ok)
231
- return response.status === 401 ? fail('TOKEN_EXPIRED') : responseError(response.status);
232
- const context = normalizeContext(await response.json());
233
- if (!context)
234
- return fail('AUTH_CONTEXT_MISSING');
235
- if (!context.appAccess?.allowed)
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 current = tokenStore.get();
245
- if (!current?.accessToken)
246
- return fail('LOGIN_REQUIRED');
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 response = await fetchImpl(`${authDomain}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${current.accessToken}` } });
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 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');
410
+ return fail('LOGOUT_FAILED', response.status >= 408);
411
+ return ok(undefined);
253
412
  }
254
413
  catch {
255
- return fail('AUTH_PROTOCOL_UNAVAILABLE', true);
414
+ return fail('LOGOUT_FAILED', true);
415
+ }
416
+ finally {
417
+ tokenStore.clear();
256
418
  }
257
419
  }
258
- async function logout() {
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(requestedTenantId) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msdongcha/auth-kit",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Framework-agnostic authentication context, redirect, callback and session coordination utilities.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",