@authpi/admin
v1.3.0
Published
Official TypeScript Admin SDK for AuthPI
Readme
@authpi/admin
Official TypeScript Admin SDK for the AuthPI Core API.
Requirements: Node.js 18+, Bun, Deno, or Cloudflare Workers (server-side only — API keys are secrets)
Stability
@authpi/admin follows semantic versioning. Public exports, constructor options, generated request and response types, resource accessors, and documented method behavior are stable across 1.x; incompatible changes will ship in a new major version.
Installation
npm install @authpi/admin
# or
pnpm add @authpi/adminQuick Start
import { AuthPIAdmin } from "@authpi/admin";
const admin = new AuthPIAdmin({ apiKey: { id: "key_xxx", secret: "your_key_secret" }, accountId: "acc_xxx" });
// List issuers
const page = await admin.issuers.list({ limit: 10 });
console.log(page.data); // Issuer[]
// Scope into an issuer and manage users
const users = await admin.issuer("i_xxx").users.list();
for await (const user of admin.issuer("i_xxx").users.listAll()) {
console.log(user);
}
// Create a user
const user = await admin.issuer("i_xxx").users.create({
username_type: "email",
username: "[email protected]",
profile: { first_name: "Alice", last_name: "Smith" },
});
// Create a webhook with typed event subscriptions
const webhook = await admin.webhooks.create({
name: "Lifecycle events",
url: "https://example.com/webhooks/authpi",
auth: { type: "signature" },
events: ["organization.created", "user.created"],
});Request body types such as CreateWebhookInput are generated from the API schema. You can pass object literals directly to .create(...) and .update(...); TypeScript uses the method signature to validate required keys and enum values. If you build a payload before passing it, use satisfies:
import type { CreateWebhookInput } from "@authpi/admin";
const payload = {
name: "Lifecycle events",
url: "https://example.com/webhooks/authpi",
auth: { type: "signature" },
events: ["user.created"],
} satisfies CreateWebhookInput;
await admin.webhooks.create(payload);Authentication
API Key (default)
API keys are issued as an id + secret pair — both parts are shown once when you create the key in the dashboard. The SDK sends them as HTTP Basic credentials (key_id:key_secret):
const admin = new AuthPIAdmin({ apiKey: { id: "key_xxx", secret: "your_key_secret" }, accountId: "acc_xxx" });Bearer Token
For server-side applications authenticating on behalf of a user session:
const admin = new AuthPIAdmin({
accessToken: "tok_xxx",
accountId: "acc_xxx",
});With optional token refresh callback:
const admin = new AuthPIAdmin({
accessToken: "tok_xxx",
accountId: "acc_xxx",
onTokenExpired: async () => {
const newTokens = await myRefreshLogic();
return { accessToken: newTokens.accessToken };
},
});When onTokenExpired is provided, the SDK calls it on 401 responses and retries the request with the new token. Concurrent 401s are deduplicated — only one refresh runs at a time.
Account resolution
accountId is optional. When omitted, the SDK resolves it once via GET /v1/me on the first request and caches it — an API key always maps to exactly one account:
const admin = new AuthPIAdmin({ apiKey: { id: "key_xxx", secret: "your_key_secret" } });
const issuers = await admin.issuers.list(); // resolves the account transparentlyIf the credential can act on zero or multiple accounts (possible with user bearer tokens), the SDK throws a ConfigurationError naming the choices — pass accountId explicitly in that case.
You can also ask directly who the API considers you to be:
const me = await admin.whoami();
// { type: "api_key", key_id: "key_...", issuer_id: "i_...", accounts: [{ account_id, org_id, scopes }] }Retries
Read-only requests (GET, HEAD, OPTIONS) are automatically retried on 429, 502, 503, and 504 responses with exponential backoff. The Retry-After header is respected when present.
// Default: retries enabled (3 attempts, 1s base delay, exponential backoff)
const admin = new AuthPIAdmin({ apiKey: { id: "key_xxx", secret: "your_key_secret" }, accountId: "acc_xxx" });
// Disable retries
const admin = new AuthPIAdmin({ apiKey: { id: "key_xxx", secret: "your_key_secret" }, accountId: "acc_xxx", retries: false });
// Custom retry config
const admin = new AuthPIAdmin({
apiKey: { id: "key_xxx", secret: "your_key_secret" },
accountId: "acc_xxx",
retries: { limit: 5, delay: 500, backoff: "linear" },
});Mutations (POST, PATCH, DELETE) are never retried automatically. Use idempotency keys and handle retries explicitly for writes.
Scoped Client Pattern
The SDK mirrors the API's resource hierarchy. Navigate with chained accessors — singular for scoping into a specific entity, plural for collections:
// Account-level resources
admin.issuers.list()
admin.webhooks.create({ name: "Lifecycle events", url: "https://...", auth: { type: "signature" }, events: ["user.created"] })
admin.events.list({ limit: 50 })
// Issuer scope
const iss = admin.issuer("i_xxx");
iss.users.list()
iss.agents.create({ name: "bot" })
iss.clients.list()
iss.organizations.list()
// Organization scope (nested under issuer)
const org = admin.issuer("i_xxx").organization("org_xxx");
org.members.list()
org.invitations.list()
org.sso.addDomain({ domain: "acme.com" })
// User scope (nested under issuer)
const usr = admin.issuer("i_xxx").user("usr_xxx");
usr.get()
usr.sessions.list()
usr.tokens.list()
usr.trustedDevices.list()
usr.verifiers.list()
// Webhook scope
const wh = admin.webhook("wh_xxx");
wh.get()
wh.deliveries.list()All scope and resource accessors are lazily cached — repeated access returns the same instance.
Pagination
List endpoints return a Page<T> with cursor-based pagination:
// Manual pagination
const page = await admin.issuer("i_xxx").users.list({ limit: 25 });
console.log(page.data); // User[]
console.log(page.hasMore); // boolean
console.log(page.nextCursor); // string | undefined
// Fetch next page
if (page.hasMore) {
const next = await admin.issuer("i_xxx").users.list({
limit: 25,
cursor: page.nextCursor,
});
}
// Auto-pagination (yields individual items across all pages)
for await (const user of admin.issuer("i_xxx").users.listAll()) {
console.log(user);
}ETags & Optimistic Concurrency
GET responses include an _etag field. Pass it back on updates to prevent overwriting concurrent changes:
const user = await admin.issuer("i_xxx").users.get("usr_xxx");
console.log(user._etag); // "W/\"abc123\""
// Conditional update — fails with PreconditionFailedError if modified since
await admin.issuer("i_xxx").users.update("usr_xxx",
{ profile: { display_name: "Bob" } },
{ ifMatch: user._etag },
);The SDK automatically strips _etag (and all _-prefixed keys) from outbound request bodies, so passing a fetched object back as a body is safe.
Idempotency
Mutating operations accept an optional idempotency key to guarantee at-most-once execution:
await admin.issuer("i_xxx").users.create(
{ username_type: "email", username: "[email protected]" },
{ idempotencyKey: "req_abc123" },
);Custom Actions
Some resources expose additional action endpoints beyond CRUD:
// Block/unblock API keys (account-level)
await admin.apiKeys.block("key_xxx");
await admin.apiKeys.unblock("key_xxx");
// Rotate an API key
await admin.apiKeys.rotate("key_xxx");
// Rotate a client secret
await admin.issuer("i_xxx").clients.rotateSecret("c_xxx");
// Revoke all trusted devices for a user
await admin.issuer("i_xxx").user("usr_xxx").trustedDevices.revokeAll();Error Handling
The SDK maps HTTP status codes to specific error classes:
import {
ApiError,
NotFoundError,
ValidationError,
AuthenticationError,
RateLimitError,
PreconditionFailedError,
} from "@authpi/admin";
try {
await admin.issuer("i_xxx").users.get("usr_xxx");
} catch (err) {
if (err instanceof NotFoundError) {
console.log("User not found");
} else if (err instanceof ValidationError) {
console.log("Validation failed:", err.fields);
} else if (err instanceof RateLimitError) {
console.log(`Retry after ${err.retryAfter} seconds`);
} else if (err instanceof PreconditionFailedError) {
console.log(`Stale ETag, current: ${err.currentETag}`);
} else if (err instanceof AuthenticationError) {
console.log("Invalid API key");
}
}Error Hierarchy
| Error | Status | Extra Fields | Retryable |
|-------|--------|--------------|-----------|
| ApiError | — | error, errorDescription, statusCode, retryable, reference, rawBody | — |
| ValidationError | 400, 422 | fields | No |
| AuthenticationError | 401 | — | No |
| ForbiddenError | 403 | — | No |
| NotFoundError | 404 | — | No |
| ConflictError | 409 | — | No |
| PreconditionFailedError | 412 | currentETag | No |
| RateLimitError | 429 | retryAfter | Yes |
| InternalServerError | 500 | — | No |
| BadGatewayError | 502 | — | Yes |
| ServiceUnavailableError | 503 | — | Yes |
| GatewayTimeoutError | 504 | — | Yes |
| UnexpectedError | other | — | No |
Configuration
import { AuthPIAdmin } from "@authpi/admin";
const admin = new AuthPIAdmin({
// Authentication (apiKey or accessToken; accountId is always required)
apiKey: { id: "key_xxx", secret: "your_key_secret" }, // API key authentication
accessToken: "tok_xxx", // Bearer token authentication
accountId: "acc_xxx", // Required — the API has no "me" alias
// Token refresh (only with accessToken)
onTokenExpired: async () => { // Called on 401; concurrent calls are deduplicated
return { accessToken: "new_tok_xxx" };
},
// Connection
baseUrl: "https://api.authpi.com", // Default
fetch: customFetch, // Optional: custom fetch for alternative runtimes
// Retries (read-only requests only)
retries: true, // Default: enabled (3 attempts, 1s delay, exponential)
// retries: false, // Disable retries
// retries: { limit: 5, delay: 500, backoff: "linear" }, // Custom config
// Headers
defaultHeaders: { "X-Custom": "value" }, // Extra headers sent on every request
});Custom Fetch
Inject a custom fetch for environments that need it, or to add middleware (logging, retries):
import { AuthPIAdmin } from "@authpi/admin";
const admin = new AuthPIAdmin({
apiKey: { id: "key_xxx", secret: "your_key_secret" },
accountId: "acc_xxx",
fetch: async (url, init) => {
console.log(`${init?.method} ${url}`);
return globalThis.fetch(url, init);
},
});API Reference
AuthPIAdmin
| Property | Type | Description |
|----------|------|-------------|
| issuers | IssuersResource | Account-level issuers |
| webhooks | WebhooksResource | Account-level webhooks |
| events | EventsResource | Account-level events |
| notes | NotesResource | Account-level notes |
| accounts | AccountsResource | Account management |
| apiKeys | ApiKeysResource | Account-level API keys |
| domains | DomainsResource | Account-level domains |
| tokens | TokensResource | Account-level tokens |
| Method | Returns | Description |
|--------|---------|-------------|
| issuer(id) | IssuerScope | Scope into a specific issuer |
| webhook(id) | WebhookScope | Scope into a specific webhook |
Resource Methods
All resources expose a subset of these standard methods:
| Method | Description |
|--------|-------------|
| list(options?) | List resources (returns Page<T>) |
| listAll(options?) | Auto-paginating iterator (yields individual items) |
| get(id, options?) | Get a single resource |
| create(body, options?) | Create a resource |
| update(id, body, options?) | Update a resource |
| delete(id, options?) | Delete a resource |
Request Options
All methods accept an options object with:
| Option | Type | Description |
|--------|------|-------------|
| timeout | number | Per-request timeout in milliseconds |
| headers | Record<string, string> | Additional HTTP headers |
| ifMatch | string | ETag for conditional update/delete |
| idempotencyKey | string | Idempotency key for create/update |
License
MIT
