Skip to content

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-*

三种认证形态

身份分两层:clientTokenclient 级长期固定身份,由 client 承载;代授权(代表谁)是 per-call 的,由调用层 actAsToken / actAsSource 表达,各返回一个只暴露 api()ScopedView

形态调用注入的请求头
APP_ONLYclient.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 双参签名保证 sourcesourceId 成对。约束前移到编译期,不再依赖运行期校验。

上游凭证换取 User Token

把 atkonbase 当身份中枢时,终端用户经上游(钉钉 / OIDC / OAuth2)登录后,由业务后端换取代表该用户的 User Token,再走强通道调 V1。完整端到端流程(含浏览器侧取凭证、安全约定)见上游身份接入与 User Token 桥接

方法作用
client.exchangeUserToken(source, credential)上游凭证换 User Token,返回 IdentityTokenResultDTOaccessToken 即 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),返回 AuthorizeUrlauthorizeUrl / 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 主动 invalidate

createClientCredentialsTokenProviderclientId + clientSecretPOST /v1/auth/token,返回带缓存 + 提前续期 + 并发去重RefreshableTokenProviderwithClientCredentials 工厂内部即用它装配;需要调参时单独构造:

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(默认)且 clientTokenRefreshableTokenProvider 时,收到 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!okAtkonbaseApiError(payload, httpStatus)
204 / Content-Length: 0ok 时返回 undefined;否则抛错
非 JSONok 时返回字符串;否则抛错
网络 / 超时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 拒绝——此时链接本身仍然有效,自建取件界面据此字段决定是否呈现下载入口。随 SDK 2.3.0 提供。

⚠️ ticket 是 IP-bound:ticket 与首次验密码请求的公网 IP 绑定。verifyPassword 调用方与最终下载方必须是同一公网 IP,否则下载被拒。服务端代理场景下不要把 ticket 透传给前端浏览器——服务端拿到的 ticket 绑的是服务端出口 IP;正确做法是集成方服务端自己用 ticket 走 buildDownloadUrl + fetch 流式转发。前端直连无此问题。

错误处理与 v1 一致:getMeta / verifyPassword 失败抛 AtkonbaseApiError,网络 / 超时抛 AtkonbaseNetworkErrorbuildDownloadUrl 仅拼字符串,不抛业务错。

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 的差异

主题TypeScriptJava
主入口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.dataapi() 内化解包:非 2xx 与 code != 0AtkonbaseApiException,成功读 dto.getData()
错误码常量errorCodes 覆盖 V1 子集ErrorCodes 覆盖 V1 子集

Java 用法见 Java SDK