@hyper.authio/admin
v0.10.0
Published
Authio Admin API client for Node.js and edge runtimes: every operation generated from the server's OpenAPI document, grouped by area and typed by path, with account credentials, envelopes, errors, paging and Retry-After.
Readme
@hyper.authio/admin
The Authio Admin API client for Node.js 20.19+ and edge runtimes. Every operation under /api/v1/admin/ (359 of
them) is generated from the server's own OpenAPI document, typed end to end, and grouped by area; authentication with
an account credential, the envelopes, the errors, paging and Retry-After are handled for you.
npm install @hyper.authio/adminIt depends on @hyper.authio/client (account credentials, envelopes, errors) and
openapi-fetch (the typed fetch by path).
Configure
import { createAuthioAdminClient } from "@hyper.authio/admin";
const admin = createAuthioAdminClient({
authority: "https://auth.example.com",
credential: process.env.AUTHIO_ADMIN_CREDENTIAL, // ak_…, pat_… or cs_… (with credentialId)
});| Option | Default | |
| --- | --- | --- |
| authority | | Required. Authio's base URL; the Admin API is under {authority}/api/v1/admin. HTTPS, except on loopback or with requireHttps: false. |
| credential | | An account credential. ak_… (API key) and pat_… (personal access token) are sent directly; cs_… (client secret) is exchanged first. |
| credentialId | | Required with a cs_ secret: it is exchanged at {authority}/realms/authio with client_credentials and the client_id cred_{id in format N}. |
| presentAs | bearer | How ak_ and pat_ are sent: Authorization: Bearer or x-api-key. |
| exchangePersonalAccessToken | false | Exchanges a pat_ with the personal_access_token grant instead of sending it. |
| scopes | | Narrows an exchanged token to admin:{permission}:{method} scopes. |
| tokenRefreshSkew | 60 s | How early an exchanged token is renewed (at most half its lifetime). Exchanged tokens are cached, and exchanged again once after a 401. |
| accessToken | | Instead of credential: an access token, or a function returning one, called for every request. |
| maxRetries | 1 | Times a GET, HEAD, PUT or DELETE answered 429 or 503 with Retry-After is sent again, each after waiting it. 0 turns it off. |
| httpTimeout | 30 s | Bounds every call, combined with the call's signal (streams excepted). |
| fetch, clock | global fetch, system clock | Replace them in tests. |
Option names follow the shared conformance/options.md; there are no aliases.
Call it
Operations are grouped by the controller their operationId names: Realm_GetAllPaginated is
admin.realm.getAllPaginated, Client_Get is admin.client.get. Path and query parameters are passed by name in one
object, and the request body as body; the second argument takes signal, requestId (sent as X-Request-Id) and
headers. Each method answers what the envelope carries:
- the object (
data) for most operations; - the page
{ items, totalCount, pageNumber, pageSize }the envelope'sdataholds for a listing; - nothing for
204; - the raw
Responsefor a stream (admin.accessLog.stream(), Server-Sent Events).
const realm = await admin.realm.getByRealmId({ realm: "acme" }); // AuthioAdminSchema<"RealmResponse">
const page = await admin.realmClient.getClientsPaginated({ realm: "acme", pageSize: 50, searchTerm: "web" });Every refusal throws AuthioAdminError, the same class @hyper.authio/client exports:
import { AuthioAdminError } from "@hyper.authio/admin";
try {
await admin.realm.create({ body: { name: "Acme", realmId: "acme" } });
} catch (error) {
if (error instanceof AuthioAdminError) {
error.status; // the HTTP status (never the envelope's statusCode)
error.errorCode; // "ValidationFailed", "RealmNotFound", …: the top-level errorCode
error.notifications; // [{ message, field?, code? }]
error.requestId; // from the envelope, else X-Request-Id
error.retryAfterSeconds; // from Retry-After, on 429 and 503
}
}Every page
for await (const client of admin.paginate(admin.realmClient.getClientsPaginated, { realm: "acme", pageSize: 100 })) {
console.log(client.clientId);
}
for await (const page of admin.pages(admin.realm.getAllPaginated)) console.log(page.totalCount);paginate starts at page 1 and stops at totalCount, or at an empty page.
The typed fetch by path
admin.api is an openapi-fetch client over the same document, authenticated
and retried the same way. It answers { data, error, response } with the raw envelopes (ApiResult… on success,
ErrorResult on failure):
const { data, error } = await admin.api.GET("/api/v1/admin/realms/{realm}", { params: { path: { realm: "acme" } } });admin.fetch is a plain fetch with the same authentication and retries, for anything else.
Examples
const realm = "acme";
// Realms
await admin.realm.create({ body: { name: "Acme", realmId: realm } });
await admin.realm.update({ realm, body: { name: "Acme Corp", description: "Production tenants" } });
const realms = await admin.realm.getAllPaginated({ pageSize: 20 });
// Clients
const created = await admin.realmClient.create({ realm, body: { name: "Storefront", pkce: true, redirectUri: "https://shop.example.com/callback" } });
const client = await admin.client.get({ realm, client: "storefront" });
await admin.client.updateStatus({ realm, client: "storefront", body: { status: "Inactive" } });
const rotated = await admin.clientSecret.rotate({ realm, client: "storefront", body: { previousValueValidForMinutes: 60 } });
// Roles: a claim is a permission, its type the resource and its value the method
await admin.realmRole.create({ realm, body: { name: "auditor", claims: [{ claimType: "orders", claimValue: "read" }] } });
await admin.clientRole.create({ realm, client: "storefront", body: { name: "support", claims: [{ claimType: "tickets", claimValue: "write" }] } });
await admin.realmUser.addRoles({ realm, userId: "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c", body: { roles: ["auditor"] } });
// Groups (per client)
const group = await admin.group.create({ realm, client: "storefront", body: { name: "Finance", isActive: true } });
await admin.group.addMember({ realm, client: "storefront", groupId: group.id!, body: { userId: "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c" } });
// Scopes
await admin.realmScope.create({ realm, body: { name: "orders:read", protocol: "openid-connect", assignedType: "Optional" } });
await admin.clientScope.create({ realm, client: "storefront", body: { name: "orders:write", protocol: "openid-connect" } });
// Resource servers (APIs)
await admin.resourceServer.create({
realm,
body: { identifier: "https://orders.example.com", name: "Orders API", scopes: [{ name: "orders:read" }], clientIds: [created.id!] },
});
// Consumers (machine callers of a client): Authio generates the credential, and consumer.value is the one place it is shown
const consumer = await admin.clientConsumer.create({ realm, client: "storefront", body: { name: "billing-worker", type: "ApiKey" } });
// Account credentials (ak_, cs_, pat_): the answer is the only one that carries the value
const credential = await admin.secret.create({ body: { name: "ci", type: "PersonalAccessToken", allowedScopeIds: [] } });
await admin.secret.rotate({ id: credential.id!, body: { previousValueValidForMinutes: 15 } });
// Webhooks, for a client or for the whole realm
const hook = await admin.webhook.create({
realm,
client: "storefront",
body: { name: "Audit", url: "https://hooks.example.com/authio", subscribedEvents: ["user_created", "login_failed"] },
});
await admin.webhook.sendTest({ realm, client: "storefront", webhookId: hook.id! });
await admin.realmWebhook.create({ realm, body: { name: "SIEM", url: "https://siem.example.com/authio", subscribedEvents: ["attack_detected"] } });The full list is in the AuthioAdminAreas type (each method's summary, method and path are in its doc comment) and
in authioAdminOperations. The request and answer types are exported as AuthioAdminSchema<"Name">,
AuthioAdminParams<"OperationId">, AuthioAdminPaths, AuthioAdminComponents and AuthioAdminOperations.
Generated from the server
openapi/v1.json is a copy of authio.api's openapi/v1.json, which the server regenerates and checks on every build;
openapi/SOURCE names the commit it was copied from (also exported as authioAdminOpenApiCommit). From it,
scripts/generate.mjs keeps the paths under /api/v1/admin/ and the schemas they reach, writes the types with
openapi-typescript (src/generated/schema.ts) and the grouped operations
(src/generated/operations.ts). The generated code is committed, and CI fails when running the generator would
change it.
npm run sync-openapi -w @hyper.authio/admin -- ../authio.api origin/master # copy, record the commit, regenerate
npm run generate -w @hyper.authio/admin # regenerate from the copy
npm run generate -w @hyper.authio/admin -- --check # what CI runsArea and method names come from the operationIds ({Controller}_{Action}), which the server keeps stable; a renamed
controller or action is a breaking change of this package.
