@authup/core-http-kit

中文文档
1.0.0-beta.73发布于 2026/11/02Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 4000

Typed HTTP client for the Authup API, with a hook that obtains and refreshes tokens.

原始使用文档
文档目录14 节
  1. Why @authup/core-http-kit?
  2. Installation
  3. Usage
  4. Create a client
  5. Entity APIs
  6. Obtain a token
  7. Keep a token fresh
  8. Device authorization
  9. Check permissions
  10. Testing
  11. Options
  12. API
  13. Notes
  14. License

Authup logo

@authup/core-http-kit

Typed HTTP client for the Authup API, with a hook that obtains and refreshes tokens.
You stop hand-writing fetch calls, query strings and token renewal against an Authup server.

npm version CI License: Apache-2.0

Installation · Usage · API · Documentation


Why @authup/core-http-kit?

Authup's management API and its OAuth2 endpoints are plain HTTP, so any client works. What you would otherwise write yourself is the part around the request: the URL per entity, the rapiq query string, the { data, meta } response shape, the token grant, and the retry when the token expired.

ConcernWithout itWith it
EndpointsA URL and a response type per callclient.user.getMany(), client.role.create(), ... one sub-API per entity, typed with the domain types from @authup/core-kit
QueriesEncoding filter, sort, fields, include and page by handPass filters, sorts, fields, relations, pagination; it is serialized for you
TokensCalling /token, storing the token, setting the headerclient.token.createWithPassword() and siblings, plus ClientAuthenticationHook
ExpiryCatching a 401, renewing, replayingThe hook renews on a 401 or a token error and replays the request once
TestsMocking fetchcreateFakeClient() from @authup/core-http-kit/testing answers by route

Installation

npm install @authup/core-http-kit

ESM only. Node.js >=26.0.0, or any browser with fetch. There are no peer dependencies.

Usage

Create a client

import { Client } from '@authup/core-http-kit';

const client = new Client({ baseURL: 'https://auth.example.com' });

const { data: users, meta } = await client.user.getMany({
    filters: { realmId: '<realm id>' },
    sorts: { name: 'ASC' },
    pagination: { limit: 20 },
});
// meta.total, meta.schema

const { data: user } = await client.user.getOne(users[0].id);
  • baseURL has no default: Client sends relative URLs unless you set it. API_URL_DEFAULT (http://localhost:3001) is exported for callers that want a fallback.
  • A collection read answers { data: T[], meta: { total, limit?, offset?, schema? } }. A record read and every write answer { data: T, meta }.
  • The query takes filters, sorts, fields, relations and pagination, or an already assembled rapiq IQuery (for example one built with and() / or() from @rapiq/core). The server drops whatever the entity's schema does not allow; meta.schema lists what it does.

Entity APIs

Every entity has a sub-API on the client. Most carry the same verbs:

VerbRequest
getMany(query?)GET /<collection>
getOne(id, query?)GET /<collection>/:id
create(data)POST /<collection>
update(id, data)POST /<collection>/:id
createOrUpdate(idOrName, data)PUT /<collection>/:idOrName (realm, client, role, scope, user, permission, identity provider)
delete(id)DELETE /<collection>/:id
getStats(query?)GET /<collection>/@stats, grouped counts (realm, client, scope, identity provider, key, trust anchor, user, path, role, policy, permission, session, event)
getSchema()GET /<collection>/@schema, the queryable vocabulary (every entity API except userAuthenticator)

Not every entity offers every verb: events are read-only, sessions and session tokens are read and delete only (plus deleteMany), junctions such as userRole or clientScope carry no update. A few carry extras, for example client.client.rotateSecret(id, payload?), client.session.deleteMany(query?) and client.realm.getOne(id), whose meta.endpoints holds the realm's issuer, openidConfiguration and jwks URLs.

To pick a sub-API by entity type string, use pickEntityAPI. Every verb is optional on the result, so guard per method:

import { EntityType } from '@authup/core-kit';
import { pickEntityAPI } from '@authup/core-http-kit';

const api = pickEntityAPI(client, EntityType.ROLE);
if (api && api.getMany) {
    const { data: roles } = await api.getMany({ pagination: { limit: 10 } });
}

Obtain a token

client.token wraps the /token endpoint:

const grant = await client.token.createWithPassword({
    username: 'admin',
    password: 'start123',
    realm_name: 'master',
});

client.setAuthorizationHeader({ type: 'Bearer', token: grant.access_token });
MethodGrant
createWithPassword({ username, password, otp?, realm_id?, realm_name? })password
createWithClientCredentials({ client_id, client_secret })client_credentials
createWithRefreshToken({ refresh_token })refresh_token
createWithAuthorizationCode({ code, redirect_uri, code_verifier? })authorization_code
createWithDeviceCode({ device_code, client_id? })urn:ietf:params:oauth:grant-type:device_code
introspect({ token })POST /token/introspect
revoke({ token })POST /token/revoke

A user holding a second factor adds otp (a TOTP or recovery code) to the password grant.

Keep a token fresh

ClientAuthenticationHook attaches to one or more clients and gets a token when the server asks for one:

import { Client, ClientAuthenticationHook, createClientTokenCreator } from '@authup/core-http-kit';

const client = new Client({ baseURL: 'https://auth.example.com' });

const hook = new ClientAuthenticationHook({
    tokenCreator: createClientTokenCreator(
        { id: '<client id>', secret: '<client secret>' },
        { client: { baseURL: 'https://auth.example.com' } },
    ),
});
hook.attach(client);
  • The hook does not fetch a token up front. The first request goes out without one; a 401 (or a token error code in the body) calls tokenCreator, sets Authorization: Bearer on every attached client and replays the request once.
  • Concurrent failures share one tokenCreator call.
  • With timer (on by default) it renews again 60 seconds before expires_in runs out.
  • A key error (JWK_* code) unsets the header and rejects without renewing; a failed renewal unsets it as well.
  • createUserTokenCreator({ name, password, realmId?, realmName? }, { client }) is the password-grant counterpart. You can pass any () => Promise<OAuth2TokenGrantResponse> instead.
  • The hook emits refreshFinished, refreshFailed, headerSet and headerRemoved (ClientAuthenticationHookEventName).

Device authorization

A device without a browser asks for a code, shows it, and polls:

import { isClientError } from '@authup/core-http-kit';

const device = await client.deviceAuthorization.create({ client_id: '<client id>', scope: 'openid' });
console.log(`Open ${device.verification_uri_complete}`);

async function poll(interval: number) {
    await new Promise((resolve) => { setTimeout(resolve, interval * 1000); });
    try {
        return await client.token.createWithDeviceCode({
            device_code: device.device_code,
            client_id: '<client id>',
        });
    } catch (e) {
        const error = isClientError(e) ? e.response?.data?.error : undefined;
        if (error === 'authorization_pending') return poll(interval);
        if (error === 'slow_down') return poll(interval + 5);
        throw e;
    }
}

const grant = await poll(device.interval);

client.deviceAuthorization.lookup / approve / deny({ user_code }) are the calls of the page that approves the code; they ride the client's own bearer.

Check permissions

client.authorization.check() asks the server which permissions the caller holds, and in which realms:

const permissions = await client.authorization.check({ names: ['user_read', 'user_update'] });
const canUpdate = permissions.some((permission) => permission.name === 'user_update');
  • Omit names to check every permission. realms and clients pick which definitions are evaluated by the realm and client that own them; both default to [null] (definitions owned by no realm and no client). Pass own (your realm, or the client your token was issued to), ownOrNull (plus unowned), granted (plus the owner of every definition you hold a grant of) or a list of ids (null for unowned).
  • targetRealms picks the resource realms each definition is checked against. It defaults to ownOrNull (the caller's realm plus global rows); pass own or a list of realm ids (null for global).
  • Each entry carries name, the owning realmId / clientId (omitted when null) and the targetRealms it holds in. Only definitions the caller holds in at least one requested target realm appear, so absent means denied.
  • checkWithMaxAge() also returns maxAge, the seconds until a date or time policy could change the answer.
  • client.authorization.get() reads the identity-free permission catalog instead. It needs one of permission_read, permission_update or permission_delete.

Testing

import { createFakeClient } from '@authup/core-http-kit/testing';

const fake = createFakeClient({
    handlers: {
        'GET /users/:id': (req) => ({ data: { id: req.params.id, name: 'alice' }, meta: {} }),
    },
});

const { data } = await fake.user.getOne('1');
// fake.requests lists every request: method, url, body, params, headers
  • Handler keys are '<METHOD> <path>' with :param segments, or '*' as a catch-all.
  • An unmatched request answers fallback, by default { data: [], meta: { total: 0 } }. baseURL defaults to http://fake.test.
  • A urlencoded body (token requests) is recorded as a plain object.

Options

ClientAuthenticationHookOptions:

OptionTypeDefaultDescription
tokenCreator() => Promise<OAuth2TokenGrantResponse>requiredCalled when a token is needed
timerbooleantrueRenew 60 seconds before the token expires
baseURLstringAccepted for compatibility; the hook does not read it

Client takes hapic's client options (baseURL, headers, ...).

API

ExportKindPurpose
Client, IClientclass, interfaceThe HTTP client and its contract, with one sub-API per entity and workflow
ClientAuthenticationHookclassObtains, renews and applies tokens for attached clients
ClientAuthenticationHookEventNameenumEvents the hook emits
createClientTokenCreator, createUserTokenCreatorfunctionToken creators for the client-credentials and password grants
TokenCreatortype() => Promise<OAuth2TokenGrantResponse>
pickEntityAPIfunctionResolve a sub-API by EntityType string
EntityAPIDispatch, ClientEntityAPIKeytypeThe per-verb optional surface pickEntityAPI returns, and the keys it accepts
EntityRecordResponse, EntityCollectionResponsetypeThe { data, meta } response shapes
EntityStatsQuery, EntityStatsResponse, buildStatsURLtype, functionThe @stats read
EntityQueryInput, buildQueryStringtype, functionThe query input of every read and its URL serialization
UserAPI, RoleAPI, OAuth2TokenAPI, AuthorizationAPI, ...classThe sub-APIs, each with an I*API interface and *CreatePayload / *UpdatePayload types
AuthorizeInfo, ClientSummary, RealmSummary, StatusResponsetypeWorkflow response shapes
isClientError, getClientErrorCodefunctionDetect a hapic ClientError and read its code
setHeader, unsetHeaderfunctionHeader helpers re-exported from hapic
CookieNameenumThe cookie names the Authup web kit stores tokens under
API_URL_DEFAULTconsthttp://localhost:3001

@authup/core-http-kit/testing exports FakeClient, createFakeClient, matchRoute and the Fake* types.

Notes

  • Token calls drop the client's own Authorization header. client.token.* and client.deviceAuthorization.create remove the header the client carries, so client credentials in the body are never mixed with a bearer. Pass { authorizationHeaderInherit: true } (or { authorizationHeader }) as the second argument to send one, for example to introspect with your own credential: client.token.introspect({ token }, { authorizationHeaderInherit: true }).
  • Error messages come from the server. On a failed request the client replaces the error's message with the body's message when it has one, so the URL is not in it any more.
  • A refresh is retried once per request. If the replayed request fails again, its error reaches you.

License

Made with 💚

Published under the Apache 2.0 License.

版本与发布记录

@authup/core-http-kit 共发布 59 个版本,首次发布于 2024/05/08;过去 12 个月发布了 44 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.0.0-beta.73latest2026/11/025 天前
1.0.0-beta.722026/10/2711 天前
1.0.0-beta.712026/10/2018 天前
1.0.0-beta.702026/10/1919 天前
1.0.0-beta.692026/10/1424 天前
1.0.0-beta.682026/09/251 个月前

关于 @authup/core-http-kit 的常见问题

@authup/core-http-kit 是什么?

Typed HTTP client for the Authup API, with a hook that obtains and refreshes tokens.

如何安装 @authup/core-http-kit?

在项目目录运行 npm install @authup/core-http-kit,或使用 pnpm add / yarn add / bun add。

@authup/core-http-kit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@authup/core-http-kit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@authup/core-http-kit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@authup/core-http-kit 可以商用吗?

@authup/core-http-kit 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@authup/core-http-kit 的下载量有多少?

根据 npm 官方下载统计,@authup/core-http-kit 在 2026/10/07 – 2026/11/05 这 30 天内被下载 4,000 次,日均约 133 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@authup/core-http-kit 还在维护吗?

最新版本 1.0.0-beta.73 发布于 2026/11/02(5 天前);至今共发布 59 个版本,其中过去 12 个月发布了 44 个;首次发布于 2024/05/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。