@hyper.authio/admin 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -2
- package/dist/index.cjs +687 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40020 -0
- package/dist/index.d.ts +40020 -0
- package/dist/index.js +668 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -4
package/README.md
CHANGED
|
@@ -1,3 +1,172 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hyper.authio/admin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Authio **Admin API** client for Node.js 20.19+ and edge runtimes. Every operation under `/api/v1/admin/` (359 of
|
|
4
|
+
them) is generated from the server's own OpenAPI document, typed end to end, and grouped by area; authentication with
|
|
5
|
+
an account credential, the envelopes, the errors, paging and `Retry-After` are handled for you.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hyper.authio/admin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It depends on [`@hyper.authio/client`](../client/README.md) (account credentials, envelopes, errors) and
|
|
12
|
+
[`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) (the typed fetch by path).
|
|
13
|
+
|
|
14
|
+
## Configure
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createAuthioAdminClient } from "@hyper.authio/admin";
|
|
18
|
+
|
|
19
|
+
const admin = createAuthioAdminClient({
|
|
20
|
+
authority: "https://auth.example.com",
|
|
21
|
+
credential: process.env.AUTHIO_ADMIN_CREDENTIAL, // ak_…, pat_… or cs_… (with credentialId)
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Option | Default | |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `authority` | | Required. Authio's base URL; the Admin API is under `{authority}/api/v1/admin`. HTTPS, except on loopback or with `requireHttps: false`. |
|
|
28
|
+
| `credential` | | An account credential. `ak_…` (API key) and `pat_…` (personal access token) are sent directly; `cs_…` (client secret) is exchanged first. |
|
|
29
|
+
| `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}`. |
|
|
30
|
+
| `presentAs` | `bearer` | How `ak_` and `pat_` are sent: `Authorization: Bearer` or `x-api-key`. |
|
|
31
|
+
| `exchangePersonalAccessToken` | `false` | Exchanges a `pat_` with the `personal_access_token` grant instead of sending it. |
|
|
32
|
+
| `scopes` | | Narrows an exchanged token to `admin:{permission}:{method}` scopes. |
|
|
33
|
+
| `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`. |
|
|
34
|
+
| `accessToken` | | Instead of `credential`: an access token, or a function returning one, called for every request. |
|
|
35
|
+
| `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. |
|
|
36
|
+
| `httpTimeout` | 30 s | Bounds every call, combined with the call's `signal` (streams excepted). |
|
|
37
|
+
| `fetch`, `clock` | global `fetch`, system clock | Replace them in tests. |
|
|
38
|
+
|
|
39
|
+
Option names follow the shared `conformance/options.md`; there are no aliases.
|
|
40
|
+
|
|
41
|
+
## Call it
|
|
42
|
+
|
|
43
|
+
Operations are grouped by the controller their `operationId` names: `Realm_GetAllPaginated` is
|
|
44
|
+
`admin.realm.getAllPaginated`, `Client_Get` is `admin.client.get`. Path and query parameters are passed by name in one
|
|
45
|
+
object, and the request body as `body`; the second argument takes `signal`, `requestId` (sent as `X-Request-Id`) and
|
|
46
|
+
`headers`. Each method answers what the envelope carries:
|
|
47
|
+
|
|
48
|
+
- the object (`data`) for most operations;
|
|
49
|
+
- the page `{ items, totalCount, pageNumber, pageSize }` the envelope's `data` holds for a listing;
|
|
50
|
+
- nothing for `204`;
|
|
51
|
+
- the raw `Response` for a stream (`admin.accessLog.stream()`, Server-Sent Events).
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const realm = await admin.realm.getByRealmId({ realm: "acme" }); // AuthioAdminSchema<"RealmResponse">
|
|
55
|
+
const page = await admin.realmClient.getClientsPaginated({ realm: "acme", pageSize: 50, searchTerm: "web" });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Every refusal throws `AuthioAdminError`, the same class `@hyper.authio/client` exports:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { AuthioAdminError } from "@hyper.authio/admin";
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await admin.realm.create({ body: { name: "Acme", realmId: "acme" } });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof AuthioAdminError) {
|
|
67
|
+
error.status; // the HTTP status (never the envelope's statusCode)
|
|
68
|
+
error.errorCode; // "ValidationFailed", "RealmNotFound", …: the top-level errorCode
|
|
69
|
+
error.notifications; // [{ message, field?, code? }]
|
|
70
|
+
error.requestId; // from the envelope, else X-Request-Id
|
|
71
|
+
error.retryAfterSeconds; // from Retry-After, on 429 and 503
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Every page
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
for await (const client of admin.paginate(admin.realmClient.getClientsPaginated, { realm: "acme", pageSize: 100 })) {
|
|
80
|
+
console.log(client.clientId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for await (const page of admin.pages(admin.realm.getAllPaginated)) console.log(page.totalCount);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`paginate` starts at page 1 and stops at `totalCount`, or at an empty page.
|
|
87
|
+
|
|
88
|
+
### The typed fetch by path
|
|
89
|
+
|
|
90
|
+
`admin.api` is an [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) client over the same document, authenticated
|
|
91
|
+
and retried the same way. It answers `{ data, error, response }` with the raw envelopes (`ApiResult…` on success,
|
|
92
|
+
`ErrorResult` on failure):
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const { data, error } = await admin.api.GET("/api/v1/admin/realms/{realm}", { params: { path: { realm: "acme" } } });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`admin.fetch` is a plain `fetch` with the same authentication and retries, for anything else.
|
|
99
|
+
|
|
100
|
+
## Examples
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const realm = "acme";
|
|
104
|
+
|
|
105
|
+
// Realms
|
|
106
|
+
await admin.realm.create({ body: { name: "Acme", realmId: realm } });
|
|
107
|
+
await admin.realm.update({ realm, body: { name: "Acme Corp", description: "Production tenants" } });
|
|
108
|
+
const realms = await admin.realm.getAllPaginated({ pageSize: 20 });
|
|
109
|
+
|
|
110
|
+
// Clients
|
|
111
|
+
const created = await admin.realmClient.create({ realm, body: { name: "Storefront", pkce: true, redirectUri: "https://shop.example.com/callback" } });
|
|
112
|
+
const client = await admin.client.get({ realm, client: "storefront" });
|
|
113
|
+
await admin.client.updateStatus({ realm, client: "storefront", body: { status: "Inactive" } });
|
|
114
|
+
const rotated = await admin.clientSecret.rotate({ realm, client: "storefront", body: { previousValueValidForMinutes: 60 } });
|
|
115
|
+
|
|
116
|
+
// Roles: a claim is a permission, its type the resource and its value the method
|
|
117
|
+
await admin.realmRole.create({ realm, body: { name: "auditor", claims: [{ claimType: "orders", claimValue: "read" }] } });
|
|
118
|
+
await admin.clientRole.create({ realm, client: "storefront", body: { name: "support", claims: [{ claimType: "tickets", claimValue: "write" }] } });
|
|
119
|
+
await admin.realmUser.addRoles({ realm, userId: "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c", body: { roles: ["auditor"] } });
|
|
120
|
+
|
|
121
|
+
// Groups (per client)
|
|
122
|
+
const group = await admin.group.create({ realm, client: "storefront", body: { name: "Finance", isActive: true } });
|
|
123
|
+
await admin.group.addMember({ realm, client: "storefront", groupId: group.id!, body: { userId: "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c" } });
|
|
124
|
+
|
|
125
|
+
// Scopes
|
|
126
|
+
await admin.realmScope.create({ realm, body: { name: "orders:read", protocol: "openid-connect", assignedType: "Optional" } });
|
|
127
|
+
await admin.clientScope.create({ realm, client: "storefront", body: { name: "orders:write", protocol: "openid-connect" } });
|
|
128
|
+
|
|
129
|
+
// Resource servers (APIs)
|
|
130
|
+
await admin.resourceServer.create({
|
|
131
|
+
realm,
|
|
132
|
+
body: { identifier: "https://orders.example.com", name: "Orders API", scopes: [{ name: "orders:read" }], clientIds: [created.id!] },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Consumers (machine callers of a client)
|
|
136
|
+
await admin.clientConsumer.create({ realm, client: "storefront", body: { name: "billing-worker", type: "ApiKey", value: "a-long-random-value" } });
|
|
137
|
+
|
|
138
|
+
// Account credentials (ak_, cs_, pat_): the answer is the only one that carries the value
|
|
139
|
+
const credential = await admin.secret.create({ body: { name: "ci", type: "PersonalAccessToken", allowedScopeIds: [] } });
|
|
140
|
+
await admin.secret.rotate({ id: credential.id!, body: { previousValueValidForMinutes: 15 } });
|
|
141
|
+
|
|
142
|
+
// Webhooks, for a client or for the whole realm
|
|
143
|
+
const hook = await admin.webhook.create({
|
|
144
|
+
realm,
|
|
145
|
+
client: "storefront",
|
|
146
|
+
body: { name: "Audit", url: "https://hooks.example.com/authio", subscribedEvents: ["user_created", "login_failed"] },
|
|
147
|
+
});
|
|
148
|
+
await admin.webhook.sendTest({ realm, client: "storefront", webhookId: hook.id! });
|
|
149
|
+
await admin.realmWebhook.create({ realm, body: { name: "SIEM", url: "https://siem.example.com/authio", subscribedEvents: ["attack_detected"] } });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The full list is in the `AuthioAdminAreas` type (each method's summary, method and path are in its doc comment) and
|
|
153
|
+
in `authioAdminOperations`. The request and answer types are exported as `AuthioAdminSchema<"Name">`,
|
|
154
|
+
`AuthioAdminParams<"OperationId">`, `AuthioAdminPaths`, `AuthioAdminComponents` and `AuthioAdminOperations`.
|
|
155
|
+
|
|
156
|
+
## Generated from the server
|
|
157
|
+
|
|
158
|
+
`openapi/v1.json` is a copy of authio.api's `openapi/v1.json`, which the server regenerates and checks on every build;
|
|
159
|
+
`openapi/SOURCE` names the commit it was copied from (also exported as `authioAdminOpenApiCommit`). From it,
|
|
160
|
+
`scripts/generate.mjs` keeps the paths under `/api/v1/admin/` and the schemas they reach, writes the types with
|
|
161
|
+
[openapi-typescript](https://openapi-ts.dev/) (`src/generated/schema.ts`) and the grouped operations
|
|
162
|
+
(`src/generated/operations.ts`). The generated code is committed, and CI fails when running the generator would
|
|
163
|
+
change it.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run sync-openapi -w @hyper.authio/admin -- ../authio.api origin/master # copy, record the commit, regenerate
|
|
167
|
+
npm run generate -w @hyper.authio/admin # regenerate from the copy
|
|
168
|
+
npm run generate -w @hyper.authio/admin -- --check # what CI runs
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Area and method names come from the `operationId`s (`{Controller}_{Action}`), which the server keeps stable; a renamed
|
|
172
|
+
controller or action is a breaking change of this package.
|