外观
TypeScript SDK
@atkonbase/sdk—— ATKONBASE 官方 TypeScript SDK。Node ≥ 18 / 浏览器 / Worker 通用,使用全局fetch,无运行时依赖。本页讲用法;端点契约见接口参考,认证形态的概念背景见集成拓扑。
安装
bash
pnpm add @atkonbase/sdk包必须用 pnpm 安装(preinstall 钩子强制)。
最小示例(APP_ONLY)
最常见的服务端到服务端调用——只用 Client 身份。withClientCredentials 一行构造,baseUrl 只传一次:
ts
import { AtkonbaseClient, V1DocumentApi } from "@atkonbase/sdk";
const client = AtkonbaseClient.withClientCredentials(
"https://atkonbase.example.com/api",
"${clientId}",
"${clientSecret}",
);
// typed API(路径 / 参数序列化 / 鉴权头 / 401 重试全自动)
const v1 = client.api(V1DocumentApi);
const dto = await v1.someEndpoint({ /* ... */ });
console.log(dto.data);高级构造
需要自定义 fetch / timeoutMs / defaultHeaders / 关闭 401 自动刷新时,改用全量构造(与 withClientCredentials 等价,只是把装配显式展开):
ts
import { AtkonbaseClient, createClientCredentialsTokenProvider } from "@atkonbase/sdk";
const clientToken = createClientCredentialsTokenProvider({
baseUrl: "https://atkonbase.example.com/api",
clientId: "${clientId}",
clientSecret: "${clientSecret}",
});
const client = new AtkonbaseClient({
baseUrl: "https://atkonbase.example.com/api",
auth: { clientToken }, // V1Auth:只承载 clientToken
timeoutMs: 60_000, // 默认 30s
autoRefreshOn401: true, // 默认开
});转义出口——spec 未覆盖或返回结构不规则时直接走原始路径:
ts
const data = await client.request<MyResponse>("/v1/some/resource", {
method: "GET",
query: { page: 1, size: 20 }, // 数组会展开为多值
});转义出口默认仅注入
clientToken(APP_ONLY)。需代授权时优先用actAsToken/actAsSource视图(见下);low-level 场景也可在headers里手动传X-Atk-User-Token/X-Atk-Act-As-*。
三种认证形态
身份分两层:clientToken 是 client 级长期固定身份,由 client 承载;代授权(代表谁)是 per-call 的,由调用层 actAsToken / actAsSource 表达,各返回一个只暴露 api() 的 ScopedView。
| 形态 | 调用 | 注入的请求头 |
|---|---|---|
| APP_ONLY | client.api(Xxx) | Authorization: Bearer <client> |
| 强通道(代表已登录用户) | client.actAsToken(userToken).api(Xxx) | 上 + X-Atk-User-Token: <user> |
| 弱通道(代表外部身份) | client.actAsSource(source, sourceId).api(Xxx) | 上 + X-Atk-Act-As-Source + X-Atk-Act-As-Source-Id |
ts
// APP_ONLY:纯 client 身份
const dto = await client.api(V1DocumentApi).someEndpoint({ /* ... */ });
// 强通道:代表一个已签发用户会话 token 的用户
const userDto = await client
.actAsToken(userSessionToken)
.api(V1DocumentApi)
.someEndpoint({ /* ... */ });
// 弱通道:Client 主张代表外部身份(需 server 侧开启 actAsAllowed)
const extDto = await client
.actAsSource("wechat", "openid-xxx")
.api(V1DocumentApi)
.someEndpoint({ /* ... */ });要点:
- 代授权是 per-call 的:
actAsToken/actAsSource每次按需调用产出ScopedView,同一个client可对不同终端用户安全并发复用——所有视图共享同一clientToken刷新单点与 401 自动刷新中间件。 - 通道互斥在编译期成立:
ScopedView只暴露api()/currentUser(),类型层面无法再叠加另一通道(强、弱不可同时);actAsSource双参签名保证source与sourceId成对。约束前移到编译期,不再依赖运行期校验。
上游凭证换取 User Token
把 atkonbase 当身份中枢时,终端用户经上游(钉钉 / OIDC / OAuth2)登录后,由业务后端换取代表该用户的 User Token,再走强通道调 V1。完整端到端流程(含浏览器侧取凭证、安全约定)见上游身份接入与 User Token 桥接。
| 方法 | 作用 |
|---|---|
client.exchangeUserToken(source, credential) | 上游凭证换 User Token,返回 IdentityTokenResultDTO(accessToken 即 User Token,附 refreshToken) |
client.refreshUserToken(refreshToken) | 用 refreshToken 续期,返回新的 IdentityTokenResultDTO |
client.logoutUserToken(userToken) | 按 token 值失效目标 User Token(不影响调用方持有的 client 登录态) |
client.actAsToken(userToken).currentUser() | 解析当前用户详情(profile / 主部门 / 所有部门 / 所有角色 / 权限码并集),基于 /v1/users/me/getDetail |
ts
import { AtkonbaseClient, V1DocumentApi } from "@atkonbase/sdk";
const client = AtkonbaseClient.withClientCredentials(baseUrl, clientId, clientSecret);
// 1. 上游凭证换 User Token(business backend;source 取 dingtalk / oidc / oauth2)
const token = await client.exchangeUserToken("oauth2", upstreamAuthorizationCode);
const userToken = token.accessToken!;
// 2. 解析当前用户 + 以该用户身份调 V1(ACL 按该用户解析)
const me = await client.actAsToken(userToken).currentUser();
const dto = await client.actAsToken(userToken).api(V1DocumentApi).someEndpoint({ /* ... */ });
// 3. 续期 / 登出
const renewed = await client.refreshUserToken(token.refreshToken!);
await client.logoutUserToken(userToken);⚠️ User Token 代表终端用户身份,仅存业务系统后端、不下发浏览器;前端只把一次性上游凭证交给后端换取。
currentUser()也可用于弱通道视图(actAsSource(...).currentUser()),解析外部身份映射出的内部用户。
Identity Broker 授权码流接入
把 ATKONBASE 当下游业务系统的登录中枢时,业务系统用标准 OAuth2 授权码流把终端用户重定向到 ATKONBASE 托管登录页完成认证,再用授权码兑换出代表该用户的 User Token——业务系统无需自建用户体系,对 ATKONBASE 背后对接的钉钉 / OIDC 等上游身份源完全无感。client.oauth(clientId, clientSecret) 产出一个 AtkonbaseOAuth,把「拼 authorize URL + 自管 PKCE + state CSRF 校验 + 调 token 端点兑换」收成两个原语(base URL 沿用 client 配置):
| 方法 | 作用 |
|---|---|
oauth.buildAuthorizeUrl(redirectUri, scope?) | 自动产出 state 与 PKCE(codeVerifier / code_challenge,S256),返回 AuthorizeUrl(authorizeUrl / state / codeVerifier) |
await oauth.exchangeCode(params) | 先校验回传 state 与期望一致(不一致即抛错、不发请求),再向授权码兑换端点换取,返回 IdentityTokenResultDTO |
接入只需两个路由:一个发起授权(把浏览器 302 到 authorizeUrl),一个接收回调(用授权码兑换 User Token)。
ts
import { AtkonbaseClient } from "@atkonbase/sdk";
const client = AtkonbaseClient.withClientCredentials(baseUrl, clientId, clientSecret);
const oauth = client.oauth(clientId, clientSecret);
// 路由 1:发起授权——产出授权 URL,把 state / codeVerifier 暂存到当前会话,再 302 用户浏览器
const authorize = oauth.buildAuthorizeUrl("https://your-app.example.com/callback");
req.session.oauthState = authorize.state;
req.session.oauthCodeVerifier = authorize.codeVerifier;
res.redirect(authorize.authorizeUrl);
// 路由 2:回调——用授权码兑换 User Token(内部自动校验 state)
const token = await oauth.exchangeCode({
code: callbackCode, // 回调 query 里的 code
redirectUri: "https://your-app.example.com/callback", // 须与发起时一致
codeVerifier: req.session.oauthCodeVerifier,
expectedState: req.session.oauthState,
returnedState: callbackState, // 回调 query 里的 state
});
const userToken = token.accessToken!; // 即 User Token,后续走强通道⚠️
clientSecret/codeVerifier/state/ User Token 均为后端机密,全程仅在业务系统后端使用、不下发浏览器。buildAuthorizeUrl产出的state/codeVerifier须由后端会话自行暂存、回调时取回——本模块不替业务方持久化。兑换出的 User Token 即现有 User Token,续期 / 登出复用上一节的refreshUserToken/logoutUserToken。
TokenProvider 与凭据刷新
token 字段接受三种形态:
ts
type TokenProvider =
| string // 固定 token
| (() => string | Promise<string>) // 动态 resolver(你自己控制缓存)
| RefreshableTokenProvider; // 可被 SDK 主动 invalidatecreateClientCredentialsTokenProvider 用 clientId + clientSecret 调 POST /v1/auth/token,返回带缓存 + 提前续期 + 并发去重的 RefreshableTokenProvider。withClientCredentials 工厂内部即用它装配;需要调参时单独构造:
ts
const provider = createClientCredentialsTokenProvider({
baseUrl: "https://atkonbase.example.com/api",
clientId: "${clientId}",
clientSecret: "${clientSecret}",
refreshSkewMs: 60_000, // 提前续期窗口,默认 60s
timeoutMs: 10_000, // token 端点超时,默认 10s
});resolve():缓存命中(未到 skew 窗口)直接返回;否则发刷新请求。- 并发去重:多个
resolve共享同一 in-flight 请求。 - 跨多个
AtkonbaseClient共享缓存时,复用同一个 provider 实例——不要重复调createClientCredentialsTokenProvider,每次都产出独立闭包状态。
401 自动刷新
autoRefreshOn401: true(默认)且 clientToken 是 RefreshableTokenProvider 时,收到 HTTP 401 会先 invalidate() 再重发一次(仅一次,避免循环)。固定字符串 token / 纯 resolver / 关闭开关时,退回「401 直接抛错」。
错误处理
typed API(client.api(...))的中间件已把 HTTP 非 2xx 与 code !== 0 都转成 AtkonbaseApiError,业务代码可直接读 dto.data,无需再判 code。解包规则:
| 情况 | 行为 |
|---|---|
JSON code === 0 | 返回 payload.data |
JSON code !== 0 或 !ok | 抛 AtkonbaseApiError(payload, httpStatus) |
204 / Content-Length: 0 | ok 时返回 undefined;否则抛错 |
| 非 JSON | ok 时返回字符串;否则抛错 |
| 网络 / 超时 | 抛 AtkonbaseNetworkError |
ts
import { AtkonbaseApiError, AtkonbaseNetworkError } from "@atkonbase/sdk";
try {
const dto = await client.api(V1DocumentApi).someEndpoint({ /* ... */ });
// 直接用 dto.data
} catch (e) {
if (e instanceof AtkonbaseApiError) {
// e.code(server 业务 code)/ e.httpStatus / e.message / e.data
} else if (e instanceof AtkonbaseNetworkError) {
// 网络 / 超时 / abort,原始异常在 e.cause
} else {
throw e;
}
}错误码常量 errorCodes
TypeScript SDK 提供错误码常量集合 errorCodes,与 Java SDK ErrorCodes 同名同值、同 V1 暴露子集。可直接引用具名常量判定 e.code,无需直接写死数字:
ts
import { AtkonbaseApiError, errorCodes } from "@atkonbase/sdk";
try {
const dto = await client.api(V1DocumentApi).someEndpoint({ /* ... */ });
} catch (e) {
if (e instanceof AtkonbaseApiError) {
if (e.code === errorCodes.USER_TOKEN_INVALID) {
// 用户 token 失效——引导重新登录 / 刷新
} else if (e.code === errorCodes.PERMISSION_DENIED) {
// 权限不足
}
// 请始终保留一个通用错误分支处理未显式判定的 code,不要假设错误码集合是封闭的(后续版本可能新增)
}
}完整含义与触发条件见错误码参考。
public 分享通路
AtkonbasePublicClient 覆盖 /public/s/{tenantCode}/{token}/** 匿名分享端点,无 auth,与 V1 client 类型层面互不替换:
ts
import { AtkonbasePublicClient } from "@atkonbase/sdk";
const publicClient = new AtkonbasePublicClient({ baseUrl });
// 1. 拉元信息(是否需要密码、是否可下载、文件大小、过期时间等)
const meta = await publicClient.getMeta(tenantCode, token);
// 2. 需要密码时,验证后拿一次性 ticket
let ticket: string | undefined;
if (meta.requiresPassword) {
const verify = await publicClient.verifyPassword(tenantCode, token, userInputPassword);
ticket = verify.ticket;
}
// 3. 该链接授了下载权才给出下载入口
if (meta.canDownload) {
// 拼下载 URL —— 推荐浏览器原生导航(绕开 CORS / 内存峰值)
const url = publicClient.buildDownloadUrl(tenantCode, token, { ticket });
window.location.href = url;
}
canDownload=false表示该链接只授了浏览权:元信息与文件夹目录浏览照常可用,仅/download会以174001 SHARE_LINK_UNAVAILABLE拒绝——此时链接本身仍然有效,自建取件界面据此字段决定是否呈现下载入口。随 SDK2.3.0提供。
⚠️ ticket 是 IP-bound:ticket 与首次验密码请求的公网 IP 绑定。
verifyPassword调用方与最终下载方必须是同一公网 IP,否则下载被拒。服务端代理场景下不要把 ticket 透传给前端浏览器——服务端拿到的 ticket 绑的是服务端出口 IP;正确做法是集成方服务端自己用 ticket 走buildDownloadUrl+fetch流式转发。前端直连无此问题。
错误处理与 v1 一致:getMeta / verifyPassword 失败抛 AtkonbaseApiError,网络 / 超时抛 AtkonbaseNetworkError;buildDownloadUrl 仅拼字符串,不抛业务错。
buildDownloadUrl第三参数的disposition?: "inline" | "attachment"参数当前保留但不生效:下载一律以attachment(触发下载)响应。如需浏览器内预览,请改用接口参考中的预览模式端点。
时间字段
V1 接口的时间字段(版本元信息、签名 URL 过期、分享授权过期时间等)在 SDK 中是 Date,wire 格式为不带时区偏移的本地时间 yyyy-MM-dd HH:mm:ss。提交带时间的写请求(如设置过期时间)时直接传 Date,SDK 会按本地时间分量序列化为该格式——不要自行调用 .toISOString()(会产出服务端收不下的 ISO-UTC):
ts
req.expireTime = new Date(Date.now() + 7 * 86_400_000); // SDK 自动序列化为 yyyy-MM-dd HH:mm:ss与 Java SDK 的差异
| 主题 | TypeScript | Java |
|---|---|---|
| 主入口 | client.api(V1DocumentApi) + client.request<T>(path) 转义出口 | client.api(V1DocumentApi.class) |
| 代授权 | client.actAsToken(token) / actAsSource(source, id) → ScopedView | 同左(形态镜像一致) |
TokenProvider 异步性 | 原生支持异步(() => Promise<string>) | Java 侧为同步签名 String resolve() |
| 错误统一 | 中间件把非 2xx 与 code !== 0 转成 AtkonbaseApiError,成功读 dto.data | api() 内化解包:非 2xx 与 code != 0 抛 AtkonbaseApiException,成功读 dto.getData() |
| 错误码常量 | errorCodes 覆盖 V1 子集 | ErrorCodes 覆盖 V1 子集 |
Java 用法见 Java SDK。