@easecation/iam-client
v1.5.9
Published
IAM V2 client library for EaseCation services
Maintainers
Readme
@easecation/iam-client
IAM V2 client library for EaseCation services. Provides an API wrapper and an Express middleware for validating RS256 access tokens against IAM.
Install
yarn add @easecation/iam-clientQuick Start
import { IamClient } from '@easecation/iam-client';
const client = new IamClient({
iamBaseUrl: 'http://localhost:8401',
clientId: '10007',
clientSecret: 'YOUR_CLIENT_SECRET',
});
// Exchange OAuth code for access + refresh tokens
const tokens = await client.exchangeOAuthCode(code, redirectUri);
// Verify access token and fetch permissions
const verify = await client.verifyToken(tokens.access_token);
// Fetch profile + user_data (batch)
const bootstrap = await client.getUserBootstrap(tokens.access_token, [
'group',
'ECID',
'serverAdmin',
'serverAdminDue',
]);
// HR helper
const hrBootstrap = await client.getHrBootstrapData(tokens.access_token);
// Open V2 Data API with a pre-provisioned API key only.
// This does not require clientId/clientSecret or app_session.
const apiKeyOnlyClient = new IamClient({
iamBaseUrl: 'http://localhost:8401',
apiKey: 'ec_xxx',
});
const feishuUsers = await apiKeyOnlyClient.requestOpenV2Data('/feishu/batch', {
body: { uids: [123] },
});
// Or override the API key for one request.
const permissionUsers = await client.requestOpenV2Data('/permission/user', {
apiKey: 'ec_xxx',
body: { permission_code: '5#sen.admin' },
});
// Parameterized reverse lookup for a registered data type owned by this app.
const channelUsers = await client.searchUserData({
data_code: 'channel_ids',
filter: { path: '$', operator: 'array_contains', value: '频道B' },
});
// Resolve a tenant-scoped Feishu identity to open_id and IAM uid.
const identities = await client.resolveFeishuIdentities({
identities: [{ user_id: 'tenant_user_id', organization: '宁波易什' }],
});
// Read Feishu avatar URLs by IAM uid. IAM owns the Feishu credentials and
// transparently refreshes its persistent cache.
const avatars = await client.getFeishuAvatars({ uids: [123, 456] });
const preferredAvatar = avatars.data.data?.avatars[0]?.avatar?.avatar_240;
// Legacy V1 (callback verify)
const valid = await client.verifyIamCallbackToken(iamToken);
// Run downstream calls with a cached app API key.
// If the callback throws a 401-style error, IAM refreshes the key and retries once.
const player = await client.withAppApiKey(async (apiKey) => {
const response = await fetch('https://api.example.test/players/info?displayName=Steve', {
headers: { 'X-API-Key': apiKey },
});
if (response.status === 401) throw { status: 401 };
if (!response.ok) throw new Error(`Downstream failed: ${response.status}`);
return response.json();
});
// Use requestWithAppApiKey when the current business flow depends on the response.
const requiredResponse = await client.requestWithAppApiKey({
url: 'https://msg-api.example.test/sendmsg',
method: 'POST',
data: { message: 'important result' },
});
// Use dispatchWithAppApiKey when the response is optional. It uses the same
// API key injection and one-time 401 refresh, but does not block this flow.
const dispatched = client.dispatchWithAppApiKey(
{
url: 'https://msg-api.example.test/sendmsg',
method: 'POST',
data: { message: 'optional result' },
},
{
onResult: result => {
if (result.status === 'fulfilled') {
console.log(result.response.status);
}
},
},
);
// Observation is optional and process-local. A callback registered after the
// response arrives runs immediately.
client.getAppApiKeyDispatch(dispatched.requestId)?.onResult(result => {
console.log(result.status);
});
const optionalResult = dispatched.getResult();
// Business delegation: app A calls app B as an authorized user without seeing
// the user's own API key. IAM returns a short-lived user API key for app B.
// Requirements:
// 1. User OAuth-authorized app A with scope "<targetAppId>.permission".
// 2. App B granted app A scope "iam.business.delegate" in App Grants.
const me = await client.requestWithDelegatedUserApiKey({
apiKey: 'ec_app_key_for_app_a', // optional; omit to use config.apiKey/getAppApiKey()
userId: 123,
targetApplicationId: 5,
url: 'https://ecapi.example.test/users/me',
});Required vs Optional Results
Choose between the two app API key request methods based on whether the response is required by the current business flow, not based on request duration:
requestWithAppApiKey()returns a rejecting promise for normal request/response control flow. Its default timeout is the client'stimeoutMs.dispatchWithAppApiKey()returns immediately with anAppApiKeyDispatch. Its default Axios timeout is0, and failures are captured as a rejected dispatch result instead of becoming unhandled promise rejections.
Both methods share the same implementation for API key resolution,
X-Api-Key injection, successful GET caching, and one-time automatic key
refresh after a downstream 401.
A dispatch exposes getResult(), waitForResult(), and onResult(). Settled
results are retained only in the current process, for 5 minutes and up to 100
results by default. Configure these limits with
appApiKeyDispatchResultRetentionMs and
appApiKeyDispatchMaxRetainedResults. FC instance recycling discards all
dispatch references and results.
Express Middleware
import express from 'express';
import { IamClient, createIamAuthMiddleware } from '@easecation/iam-client';
const client = new IamClient({
iamBaseUrl: 'http://localhost:8401',
clientId: '10007',
clientSecret: 'YOUR_CLIENT_SECRET',
});
const iamAuth = createIamAuthMiddleware(client);
const router = express.Router();
router.get('/profile', iamAuth(), (req, res) => {
const iamReq = req as any;
res.json({ uid: iamReq.iamUid, permissions: iamReq.iamPermissions });
});
router.post('/admin', iamAuth(['admin.write']), handler);API Highlights
getAppSessionToken()getAppApiKey(options?)withAppApiKey(callback, options?)requestWithAppApiKey(options)dispatchWithAppApiKey(options, { onResult? })getAppApiKeyDispatch(requestId)getDelegatedUserApiKey(options),getDelegatedUserApiKeyData(options)requestWithDelegatedUserApiKey({ apiKey?, userId, targetApplicationId, url, ... })requestOpenV2Data(path, { apiKey?, body?, params?, method? })searchUserData(request, { apiKey? })resolveFeishuIdentities(request, { apiKey? })getFeishuAvatars(request, { apiKey?, cache? })exchangeOAuthCode(code, redirectUri)verifyToken(accessToken)refreshToken(refreshToken)revokeToken(refreshToken)getUserProfile(accessToken)getUserPermissions(userId, targetApplicationId?)getJwks()
Notes
- App session authentication uses the
X-App-Session-Tokenheader. - Verify results are cached locally with a TTL that never exceeds the access token expiration.
- Concurrent cache misses for the same API key or Open V2 Data request share one IAM call.
- Concurrent app API key refreshes share one creation request; failed requests are never retained.
cache: falsecontinues to bypass both response caching and concurrent-request sharing.
