@bentoforge/umami-iam 0.1.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 ADDED
@@ -0,0 +1,60 @@
1
+ # @bentoforge/umami-iam
2
+
3
+ Typed TypeScript client SDK for the [umami](../../README.md) micro-IAM service. It holds the access
4
+ token **in memory only** and silently refreshes it via the `HttpOnly` cookie on a 401 — the refresh
5
+ token's value is never touched by JS.
6
+
7
+ ```bash
8
+ npm install @bentoforge/umami-iam
9
+ ```
10
+
11
+ ```ts
12
+ import { UmamiClient } from "@bentoforge/umami-iam";
13
+
14
+ const umami = new UmamiClient({
15
+ baseUrl: "https://umami.example.com",
16
+ onTokenChange: (token) => {
17
+ /* update app state */
18
+ },
19
+ });
20
+
21
+ // Password login (handles the MFA challenge shape)
22
+ const res = await umami.login("owner@acme.test", "secret");
23
+ if (res.mfaRequired) {
24
+ await umami.login("owner@acme.test", "secret", "123456"); // with the TOTP code
25
+ }
26
+
27
+ const me = await umami.getMe();
28
+ umami.hasPermission("write:members"); // decodes the token's claims
29
+
30
+ // Passwordless passkey login (browser)
31
+ await umami.loginWithPasskey("owner@acme.test");
32
+
33
+ // Silent refresh on page reload
34
+ await umami.refresh();
35
+ ```
36
+
37
+ ## What it covers
38
+
39
+ - **Auth**: `login`, `refresh`, `logout`, `logoutAll`, `getMe`, `getClaims`, `hasPermission`
40
+ - **MFA**: `totpSetup` / `totpVerify` / `totpDisable`; `registerPasskey` / `loginWithPasskey`
41
+ (wrapping `navigator.credentials`)
42
+ - **API keys**: `exchangeApiKey` (M2M/BFF), plus `createApiKey` / `listApiKeys` / `deleteApiKey`
43
+ - **Tenants**: signup, get/patch, status/license, packages, features, entitlements, usage
44
+ - **Users**: `createUser` / `listUsers` / `patchUser`
45
+ - **Config**: `getConfig` / `putConfig`
46
+
47
+ All request/response types mirror the Rust server contract (see [`src/types.ts`](src/types.ts));
48
+ errors throw `UmamiError` with the HTTP `status` and parsed `body`.
49
+
50
+ ## Build
51
+
52
+ ```bash
53
+ npm install
54
+ npm run build # tsc → dist/
55
+ npm run typecheck
56
+ ```
57
+
58
+ > API keys are **server-side credentials** — never ship an `umk_…` key in browser JS. In a browser
59
+ > use `login`/`loginWithPasskey` (user auth); use `exchangeApiKey` only from a backend/BFF. See
60
+ > [docs/API-KEYS.md](../../docs/API-KEYS.md).
@@ -0,0 +1,180 @@
1
+ import type { AccessClaims, ApiErrorBody, ApiKeyView, AuditEntry, Config, CreateApiKeyRequest, CreateApiKeyResponse, CreatePatRequest, CreateTenantRequest, CreateTenantResponse, CreateUserRequest, CustomFieldsSchema, EntitlementsResponse, ExchangeResponse, FeatureToggle, LoginResponse, MeResponse, MessagingCodeResponse, MessagingLink, MetricUsage, MfaStatus, PatchUserRequest, ResetPasswordResponse, ResolvedMessagingUser, Tenant, TenantStatus, TotpSetup, UsageResponse, UserView } from "./types.js";
2
+ /** An error carrying the server's HTTP status and (parsed) body. */
3
+ export declare class UmamiError extends Error {
4
+ readonly status: number;
5
+ readonly body?: ApiErrorBody;
6
+ constructor(status: number, message: string, body?: ApiErrorBody);
7
+ }
8
+ export interface UmamiClientOptions {
9
+ /** Base URL of the umami service, e.g. `https://umami.example.com`. */
10
+ baseUrl: string;
11
+ /** Called whenever the in-memory access token changes (login/refresh/logout). */
12
+ onTokenChange?: (token: string | null) => void;
13
+ }
14
+ /**
15
+ * Typed client for the umami API. Holds the access token **in memory only** and silently refreshes
16
+ * it via the `HttpOnly` cookie on a 401. Never touches the refresh cookie's value.
17
+ */
18
+ export declare class UmamiClient {
19
+ private readonly baseUrl;
20
+ private readonly onTokenChange?;
21
+ private accessToken;
22
+ constructor(options: UmamiClientOptions);
23
+ /** The current in-memory access token, if any. */
24
+ getAccessToken(): string | null;
25
+ /** Decodes the current access token's claims (no signature verification). */
26
+ getClaims(): AccessClaims | null;
27
+ /** Whether the current token grants a permission. */
28
+ hasPermission(permission: string): boolean;
29
+ private setToken;
30
+ private doFetch;
31
+ /** Performs a request, refreshing once on a 401 for authenticated calls. */
32
+ private request;
33
+ private handle;
34
+ /** Password login by username. On success the access token is stored; if MFA is enabled and no
35
+ * `totpCode` is given, the response has `mfaRequired: true` and no token. Pass `api` to mint the
36
+ * token for a product API directly (default: the umami admin API); the session keeps that
37
+ * audience across refreshes. */
38
+ login(username: string, password: string, totpCode?: string, api?: string): Promise<LoginResponse>;
39
+ /** Silent refresh via the cookie. Returns whether a fresh token was obtained. */
40
+ refresh(): Promise<boolean>;
41
+ /** Logs out this device and clears the in-memory token. */
42
+ logout(): Promise<void>;
43
+ /** Revokes all of the user's sessions (bumps `tokenVersion`). */
44
+ logoutAll(): Promise<void>;
45
+ /** Current profile (user + tenant). */
46
+ getMe(): Promise<MeResponse>;
47
+ /** Update the caller's own profile (name/locale). Blocked for `self:readonly` users. */
48
+ patchMe(body: {
49
+ name?: string;
50
+ locale?: string;
51
+ }): Promise<MeResponse>;
52
+ /** Re-scope the access token to another tenant (requires `admin:system`). Access-token only —
53
+ * a later silent refresh returns to the home tenant. Returns the active tenant id. */
54
+ switchTenant(tenantId: string): Promise<string>;
55
+ totpSetup(): Promise<TotpSetup>;
56
+ totpVerify(code: string): Promise<MfaStatus>;
57
+ totpDisable(code: string): Promise<MfaStatus>;
58
+ /** Enrols a passkey for the authenticated user via `navigator.credentials.create`. */
59
+ registerPasskey(): Promise<{
60
+ credentialId: string;
61
+ }>;
62
+ /** Passwordless login with a passkey via `navigator.credentials.get`; stores the token. Pass
63
+ * `api` to mint the token for a product API directly (default: umami); the session keeps that
64
+ * audience across refreshes. */
65
+ loginWithPasskey(username: string, api?: string): Promise<void>;
66
+ /** Exchanges an `umk_…` API key for a short-lived token (stores it). Server-side/BFF use.
67
+ * `api` selects the target API when the key allows more than one (see `docs/AUDIENCES.md`). */
68
+ exchangeApiKey(apiKey: string, api?: string): Promise<ExchangeResponse>;
69
+ /** Downstream token exchange: mints a token for a product API (`api` from the config catalog)
70
+ * for the currently-logged-in user, WITHOUT replacing the stored umami token. Returns the
71
+ * downstream token for the caller to use against that API. */
72
+ exchange(api: string): Promise<ExchangeResponse>;
73
+ /** List every tenant (system-admin only; sorted newest-updated first, capped at 250). `q` is an
74
+ * optional case-insensitive search: whitespace-separated terms must all match (over name / slug /
75
+ * custom fields). `truncated` is true when more than 250 matched. */
76
+ listTenants(q?: string): Promise<{
77
+ tenants: Tenant[];
78
+ truncated: boolean;
79
+ }>;
80
+ /** Create a tenant and its first owner (system-admin only). */
81
+ createTenant(request: CreateTenantRequest): Promise<CreateTenantResponse>;
82
+ /** Delete a tenant — only succeeds when it has no users (system-admin only). */
83
+ deleteTenant(tenantId: string): Promise<{
84
+ status: string;
85
+ }>;
86
+ getTenant(tenantId: string): Promise<Tenant>;
87
+ patchTenant(tenantId: string, body: Partial<Pick<Tenant, "name" | "plan" | "customFields">>): Promise<Tenant>;
88
+ patchStatus(tenantId: string, status: TenantStatus): Promise<Tenant>;
89
+ patchLicense(tenantId: string, body: {
90
+ plan?: string;
91
+ billedUntil?: string;
92
+ seatsLimit?: number;
93
+ }): Promise<Tenant>;
94
+ getEntitlements(tenantId: string): Promise<EntitlementsResponse>;
95
+ assignPackage(tenantId: string, request: {
96
+ code: string;
97
+ monthlyPrice?: string;
98
+ }): Promise<Tenant>;
99
+ removePackage(tenantId: string, assignmentId: string): Promise<Tenant>;
100
+ setFeature(tenantId: string, code: string, value: FeatureToggle): Promise<Tenant>;
101
+ /** Role codes assignable to a user given their tenant's features (feeds the UI role picker). */
102
+ assignableRoles(userId: string): Promise<{
103
+ codes: string[];
104
+ }>;
105
+ /** Scope codes assignable to a service key in the given tenant. */
106
+ assignableScopes(tenantId: string): Promise<{
107
+ codes: string[];
108
+ }>;
109
+ /** Authorization features grantable to the given tenant right now (system admin). */
110
+ assignableFeatures(tenantId: string): Promise<{
111
+ codes: string[];
112
+ }>;
113
+ /** Grant an authorization feature to a tenant (system admin). */
114
+ grantFeature(tenantId: string, code: string): Promise<{
115
+ status: string;
116
+ }>;
117
+ /** Revoke an authorization feature from a tenant (system admin). */
118
+ revokeFeature(tenantId: string, code: string): Promise<{
119
+ status: string;
120
+ }>;
121
+ getUsage(tenantId: string): Promise<UsageResponse>;
122
+ incrementUsage(tenantId: string, metric: string, amount?: number): Promise<MetricUsage>;
123
+ createUser(request: CreateUserRequest): Promise<UserView>;
124
+ /** List the caller's tenant's users (sorted by recent activity, capped at 250). `q` is an
125
+ * optional case-insensitive search over username / email / name / custom fields. */
126
+ listUsers(q?: string): Promise<{
127
+ users: UserView[];
128
+ truncated: boolean;
129
+ }>;
130
+ patchUser(userId: string, body: PatchUserRequest): Promise<UserView>;
131
+ /** Hard-delete a user in the caller's tenant (cannot delete your own account). */
132
+ deleteUser(userId: string): Promise<{
133
+ status: string;
134
+ }>;
135
+ /** Admin reset of a user's password. Omit `newPassword` to have a temporary one generated and
136
+ * returned once. Invalidates the user's existing sessions/tokens. */
137
+ resetPassword(userId: string, newPassword?: string): Promise<ResetPasswordResponse>;
138
+ /** Change the current user's own password (verifies the current one; logs out other sessions). */
139
+ changePassword(currentPassword: string, newPassword: string): Promise<void>;
140
+ /** The current user's own audit trail (newest first). */
141
+ myAudit(limit?: number): Promise<AuditEntry[]>;
142
+ /** A tenant's audit trail (requires `admin:tenant`; own tenant). */
143
+ tenantAudit(tenantId: string, limit?: number): Promise<AuditEntry[]>;
144
+ /** The caller's current link code (rotated if expired), with deep links when configured. */
145
+ getMessagingCode(): Promise<MessagingCodeResponse>;
146
+ /** Replace the caller's link code (invalidates the old). */
147
+ regenerateMessagingCode(): Promise<MessagingCodeResponse>;
148
+ /** The caller's linked external identities. */
149
+ listMessagingLinks(): Promise<{
150
+ links: MessagingLink[];
151
+ }>;
152
+ /** Remove one of the caller's linked identities. */
153
+ deleteMessagingLink(platform: string, externalId: string): Promise<{
154
+ status: string;
155
+ }>;
156
+ /** Machine (`messaging:link`): claim a `(platform, externalId)` mapping from a link code. */
157
+ createMessagingLink(code: string, platform: string, externalId: string): Promise<{
158
+ userId: string;
159
+ tenantId: string;
160
+ }>;
161
+ /** Machine (`messaging:resolve`): resolve an identity to user info. */
162
+ resolveMessaging(platform: string, externalId: string): Promise<ResolvedMessagingUser>;
163
+ /** Machine (`messaging:resolve`): resolve an identity to a minted token for `api`. */
164
+ resolveMessagingToken(platform: string, externalId: string, api: string): Promise<{
165
+ accessToken: string;
166
+ expiresIn: number;
167
+ }>;
168
+ getConfig(): Promise<Config>;
169
+ putConfig(config: Config): Promise<Config>;
170
+ /** The user + tenant custom-field schemas (any authenticated admin; no `manage:config` needed). */
171
+ getCustomFields(): Promise<CustomFieldsSchema>;
172
+ createApiKey(tenantId: string, request: CreateApiKeyRequest): Promise<CreateApiKeyResponse>;
173
+ listApiKeys(tenantId: string): Promise<ApiKeyView[]>;
174
+ deleteApiKey(tenantId: string, keyId: string): Promise<void>;
175
+ /** Create a personal access token that acts as the current user (optionally down-scoped).
176
+ * The `apiKey` secret in the response is shown only once. */
177
+ createMyPat(request: CreatePatRequest): Promise<CreateApiKeyResponse>;
178
+ listMyPats(): Promise<ApiKeyView[]>;
179
+ deleteMyPat(keyId: string): Promise<void>;
180
+ }
package/dist/client.js ADDED
@@ -0,0 +1,429 @@
1
+ import { assertionToJSON, b64urlToBuffer, registrationToJSON, toCreationOptions, toRequestOptions, } from "./webauthn.js";
2
+ /** An error carrying the server's HTTP status and (parsed) body. */
3
+ export class UmamiError extends Error {
4
+ constructor(status, message, body) {
5
+ super(message);
6
+ this.name = "UmamiError";
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ /**
12
+ * Typed client for the umami API. Holds the access token **in memory only** and silently refreshes
13
+ * it via the `HttpOnly` cookie on a 401. Never touches the refresh cookie's value.
14
+ */
15
+ export class UmamiClient {
16
+ constructor(options) {
17
+ this.accessToken = null;
18
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
19
+ this.onTokenChange = options.onTokenChange;
20
+ }
21
+ /** The current in-memory access token, if any. */
22
+ getAccessToken() {
23
+ return this.accessToken;
24
+ }
25
+ /** Decodes the current access token's claims (no signature verification). */
26
+ getClaims() {
27
+ if (!this.accessToken)
28
+ return null;
29
+ const parts = this.accessToken.split(".");
30
+ if (parts.length < 2)
31
+ return null;
32
+ try {
33
+ const json = new TextDecoder().decode(b64urlToBuffer(parts[1]));
34
+ return JSON.parse(json);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /** Whether the current token grants a permission. */
41
+ hasPermission(permission) {
42
+ return this.getClaims()?.permissions?.includes(permission) ?? false;
43
+ }
44
+ setToken(token) {
45
+ this.accessToken = token;
46
+ this.onTokenChange?.(token);
47
+ }
48
+ // ── transport ───────────────────────────────────────────────────────────────
49
+ doFetch(path, init, useAuth) {
50
+ const headers = new Headers(init.headers);
51
+ if (init.body != null && !headers.has("content-type")) {
52
+ headers.set("content-type", "application/json");
53
+ }
54
+ if (useAuth && this.accessToken) {
55
+ headers.set("authorization", `Bearer ${this.accessToken}`);
56
+ }
57
+ return fetch(`${this.baseUrl}${path}`, { ...init, headers, credentials: "include" });
58
+ }
59
+ /** Performs a request, refreshing once on a 401 for authenticated calls. */
60
+ async request(path, init = {}, useAuth = true) {
61
+ let response = await this.doFetch(path, init, useAuth);
62
+ if (response.status === 401 && useAuth) {
63
+ const refreshed = await this.refresh().catch(() => false);
64
+ if (refreshed)
65
+ response = await this.doFetch(path, init, useAuth);
66
+ }
67
+ return this.handle(response);
68
+ }
69
+ async handle(response) {
70
+ if (!response.ok) {
71
+ let body;
72
+ try {
73
+ body = (await response.json());
74
+ }
75
+ catch {
76
+ // non-JSON error body
77
+ }
78
+ throw new UmamiError(response.status, body?.message ?? response.statusText, body);
79
+ }
80
+ if (response.status === 204)
81
+ return undefined;
82
+ const text = await response.text();
83
+ return (text ? JSON.parse(text) : undefined);
84
+ }
85
+ // ── auth ────────────────────────────────────────────────────────────────────
86
+ /** Password login by username. On success the access token is stored; if MFA is enabled and no
87
+ * `totpCode` is given, the response has `mfaRequired: true` and no token. Pass `api` to mint the
88
+ * token for a product API directly (default: the umami admin API); the session keeps that
89
+ * audience across refreshes. */
90
+ async login(username, password, totpCode, api) {
91
+ const data = await this.request("/auth/login", { method: "POST", body: JSON.stringify({ username, password, totpCode, api }) }, false);
92
+ if (data.accessToken)
93
+ this.setToken(data.accessToken);
94
+ return data;
95
+ }
96
+ /** Silent refresh via the cookie. Returns whether a fresh token was obtained. */
97
+ async refresh() {
98
+ const response = await this.doFetch("/auth/refresh", { method: "POST" }, false);
99
+ if (!response.ok) {
100
+ this.setToken(null);
101
+ return false;
102
+ }
103
+ const data = (await response.json());
104
+ this.setToken(data.accessToken);
105
+ return true;
106
+ }
107
+ /** Logs out this device and clears the in-memory token. */
108
+ async logout() {
109
+ await this.request("/auth/logout", { method: "POST" }, false).catch(() => undefined);
110
+ this.setToken(null);
111
+ }
112
+ /** Revokes all of the user's sessions (bumps `tokenVersion`). */
113
+ async logoutAll() {
114
+ await this.request("/auth/logout-all", { method: "POST" }, true);
115
+ }
116
+ /** Current profile (user + tenant). */
117
+ getMe() {
118
+ return this.request("/auth/me");
119
+ }
120
+ /** Update the caller's own profile (name/locale). Blocked for `self:readonly` users. */
121
+ patchMe(body) {
122
+ return this.request("/auth/me", { method: "PATCH", body: JSON.stringify(body) });
123
+ }
124
+ /** Re-scope the access token to another tenant (requires `admin:system`). Access-token only —
125
+ * a later silent refresh returns to the home tenant. Returns the active tenant id. */
126
+ async switchTenant(tenantId) {
127
+ const data = await this.request("/auth/switch-tenant", {
128
+ method: "POST",
129
+ body: JSON.stringify({ tenantId }),
130
+ });
131
+ this.setToken(data.accessToken);
132
+ return this.getClaims()?.tenant ?? tenantId;
133
+ }
134
+ // ── MFA: TOTP ─────────────────────────────────────────────────────────────────
135
+ totpSetup() {
136
+ return this.request("/auth/mfa/totp/setup", { method: "POST" });
137
+ }
138
+ totpVerify(code) {
139
+ return this.request("/auth/mfa/totp/verify", {
140
+ method: "POST",
141
+ body: JSON.stringify({ code }),
142
+ });
143
+ }
144
+ totpDisable(code) {
145
+ return this.request("/auth/mfa/totp/disable", {
146
+ method: "POST",
147
+ body: JSON.stringify({ code }),
148
+ });
149
+ }
150
+ // ── MFA: WebAuthn (passkeys) ──────────────────────────────────────────────────
151
+ /** Enrols a passkey for the authenticated user via `navigator.credentials.create`. */
152
+ async registerPasskey() {
153
+ const start = await this.request("/auth/webauthn/register/start", { method: "POST" });
154
+ const publicKey = toCreationOptions(start.options.publicKey);
155
+ const credential = (await navigator.credentials.create({
156
+ publicKey,
157
+ }));
158
+ if (!credential)
159
+ throw new Error("Passkey registration was cancelled");
160
+ return this.request("/auth/webauthn/register/finish", {
161
+ method: "POST",
162
+ body: JSON.stringify({
163
+ ceremonyId: start.ceremonyId,
164
+ credential: registrationToJSON(credential),
165
+ }),
166
+ });
167
+ }
168
+ /** Passwordless login with a passkey via `navigator.credentials.get`; stores the token. Pass
169
+ * `api` to mint the token for a product API directly (default: umami); the session keeps that
170
+ * audience across refreshes. */
171
+ async loginWithPasskey(username, api) {
172
+ const start = await this.request("/auth/webauthn/login/start", { method: "POST", body: JSON.stringify({ username }) }, false);
173
+ const publicKey = toRequestOptions(start.options.publicKey);
174
+ const credential = (await navigator.credentials.get({
175
+ publicKey,
176
+ }));
177
+ if (!credential)
178
+ throw new Error("Passkey login was cancelled");
179
+ const data = await this.request("/auth/webauthn/login/finish", {
180
+ method: "POST",
181
+ body: JSON.stringify({
182
+ ceremonyId: start.ceremonyId,
183
+ credential: assertionToJSON(credential),
184
+ api,
185
+ }),
186
+ }, false);
187
+ this.setToken(data.accessToken);
188
+ }
189
+ // ── API-key exchange (M2M / BFF) ──────────────────────────────────────────────
190
+ /** Exchanges an `umk_…` API key for a short-lived token (stores it). Server-side/BFF use.
191
+ * `api` selects the target API when the key allows more than one (see `docs/AUDIENCES.md`). */
192
+ async exchangeApiKey(apiKey, api) {
193
+ const data = await this.request("/auth/token", { method: "POST", body: JSON.stringify(api ? { apiKey, api } : { apiKey }) }, false);
194
+ this.setToken(data.accessToken);
195
+ return data;
196
+ }
197
+ /** Downstream token exchange: mints a token for a product API (`api` from the config catalog)
198
+ * for the currently-logged-in user, WITHOUT replacing the stored umami token. Returns the
199
+ * downstream token for the caller to use against that API. */
200
+ exchange(api) {
201
+ return this.request("/auth/exchange", {
202
+ method: "POST",
203
+ body: JSON.stringify({ api }),
204
+ });
205
+ }
206
+ // ── tenants ────────────────────────────────────────────────────────────────────
207
+ /** List every tenant (system-admin only; sorted newest-updated first, capped at 250). `q` is an
208
+ * optional case-insensitive search: whitespace-separated terms must all match (over name / slug /
209
+ * custom fields). `truncated` is true when more than 250 matched. */
210
+ listTenants(q) {
211
+ const qs = q ? `?q=${encodeURIComponent(q)}` : "";
212
+ return this.request(`/tenants${qs}`);
213
+ }
214
+ /** Create a tenant and its first owner (system-admin only). */
215
+ createTenant(request) {
216
+ return this.request("/tenants", {
217
+ method: "POST",
218
+ body: JSON.stringify(request),
219
+ });
220
+ }
221
+ /** Delete a tenant — only succeeds when it has no users (system-admin only). */
222
+ deleteTenant(tenantId) {
223
+ return this.request(`/tenants/${enc(tenantId)}`, { method: "DELETE" });
224
+ }
225
+ getTenant(tenantId) {
226
+ return this.request(`/tenants/${enc(tenantId)}`);
227
+ }
228
+ patchTenant(tenantId, body) {
229
+ return this.request(`/tenants/${enc(tenantId)}`, {
230
+ method: "PATCH",
231
+ body: JSON.stringify(body),
232
+ });
233
+ }
234
+ patchStatus(tenantId, status) {
235
+ return this.request(`/tenants/${enc(tenantId)}/status`, {
236
+ method: "PATCH",
237
+ body: JSON.stringify({ status }),
238
+ });
239
+ }
240
+ patchLicense(tenantId, body) {
241
+ return this.request(`/tenants/${enc(tenantId)}/license`, {
242
+ method: "PATCH",
243
+ body: JSON.stringify(body),
244
+ });
245
+ }
246
+ getEntitlements(tenantId) {
247
+ return this.request(`/tenants/${enc(tenantId)}/entitlements`);
248
+ }
249
+ assignPackage(tenantId, request) {
250
+ return this.request(`/tenants/${enc(tenantId)}/packages`, {
251
+ method: "POST",
252
+ body: JSON.stringify(request),
253
+ });
254
+ }
255
+ removePackage(tenantId, assignmentId) {
256
+ return this.request(`/tenants/${enc(tenantId)}/packages/${enc(assignmentId)}`, {
257
+ method: "DELETE",
258
+ });
259
+ }
260
+ setFeature(tenantId, code, value) {
261
+ return this.request(`/tenants/${enc(tenantId)}/features/${enc(code)}`, {
262
+ method: "PUT",
263
+ body: JSON.stringify({ value }),
264
+ });
265
+ }
266
+ // ── authorization: assignable roles/scopes/features + feature grant/revoke ─────
267
+ /** Role codes assignable to a user given their tenant's features (feeds the UI role picker). */
268
+ assignableRoles(userId) {
269
+ return this.request(`/users/${enc(userId)}/assignable-roles`);
270
+ }
271
+ /** Scope codes assignable to a service key in the given tenant. */
272
+ assignableScopes(tenantId) {
273
+ return this.request(`/tenants/${enc(tenantId)}/assignable-scopes`);
274
+ }
275
+ /** Authorization features grantable to the given tenant right now (system admin). */
276
+ assignableFeatures(tenantId) {
277
+ return this.request(`/tenants/${enc(tenantId)}/assignable-features`);
278
+ }
279
+ /** Grant an authorization feature to a tenant (system admin). */
280
+ grantFeature(tenantId, code) {
281
+ return this.request(`/tenants/${enc(tenantId)}/features/${enc(code)}`, {
282
+ method: "POST",
283
+ });
284
+ }
285
+ /** Revoke an authorization feature from a tenant (system admin). */
286
+ revokeFeature(tenantId, code) {
287
+ return this.request(`/tenants/${enc(tenantId)}/features/${enc(code)}`, {
288
+ method: "DELETE",
289
+ });
290
+ }
291
+ getUsage(tenantId) {
292
+ return this.request(`/tenants/${enc(tenantId)}/usage`);
293
+ }
294
+ incrementUsage(tenantId, metric, amount = 1) {
295
+ return this.request(`/tenants/${enc(tenantId)}/usage/${enc(metric)}`, {
296
+ method: "POST",
297
+ body: JSON.stringify({ amount }),
298
+ });
299
+ }
300
+ // ── users ────────────────────────────────────────────────────────────────────
301
+ createUser(request) {
302
+ return this.request("/users", { method: "POST", body: JSON.stringify(request) });
303
+ }
304
+ /** List the caller's tenant's users (sorted by recent activity, capped at 250). `q` is an
305
+ * optional case-insensitive search over username / email / name / custom fields. */
306
+ listUsers(q) {
307
+ const qs = q ? `?q=${encodeURIComponent(q)}` : "";
308
+ return this.request(`/users${qs}`);
309
+ }
310
+ patchUser(userId, body) {
311
+ return this.request(`/users/${enc(userId)}`, {
312
+ method: "PATCH",
313
+ body: JSON.stringify(body),
314
+ });
315
+ }
316
+ /** Hard-delete a user in the caller's tenant (cannot delete your own account). */
317
+ deleteUser(userId) {
318
+ return this.request(`/users/${enc(userId)}`, { method: "DELETE" });
319
+ }
320
+ /** Admin reset of a user's password. Omit `newPassword` to have a temporary one generated and
321
+ * returned once. Invalidates the user's existing sessions/tokens. */
322
+ resetPassword(userId, newPassword) {
323
+ return this.request(`/users/${enc(userId)}/password`, {
324
+ method: "POST",
325
+ body: JSON.stringify(newPassword ? { newPassword } : {}),
326
+ });
327
+ }
328
+ // ── Self-service password + audit ──────────────────────────────────────────────
329
+ /** Change the current user's own password (verifies the current one; logs out other sessions). */
330
+ async changePassword(currentPassword, newPassword) {
331
+ await this.request("/auth/me/password", {
332
+ method: "POST",
333
+ body: JSON.stringify({ currentPassword, newPassword }),
334
+ });
335
+ }
336
+ /** The current user's own audit trail (newest first). */
337
+ async myAudit(limit) {
338
+ const qs = limit ? `?limit=${limit}` : "";
339
+ const data = await this.request(`/auth/me/audit${qs}`);
340
+ return data.entries;
341
+ }
342
+ /** A tenant's audit trail (requires `admin:tenant`; own tenant). */
343
+ async tenantAudit(tenantId, limit) {
344
+ const qs = limit ? `?limit=${limit}` : "";
345
+ const data = await this.request(`/tenants/${enc(tenantId)}/audit${qs}`);
346
+ return data.entries;
347
+ }
348
+ // ── messaging links ─────────────────────────────────────────────────────────
349
+ /** The caller's current link code (rotated if expired), with deep links when configured. */
350
+ getMessagingCode() {
351
+ return this.request("/auth/me/messaging-code");
352
+ }
353
+ /** Replace the caller's link code (invalidates the old). */
354
+ regenerateMessagingCode() {
355
+ return this.request("/auth/me/messaging-code/regenerate", {
356
+ method: "POST",
357
+ });
358
+ }
359
+ /** The caller's linked external identities. */
360
+ listMessagingLinks() {
361
+ return this.request("/auth/me/messaging-links");
362
+ }
363
+ /** Remove one of the caller's linked identities. */
364
+ deleteMessagingLink(platform, externalId) {
365
+ return this.request(`/auth/me/messaging-links/${enc(platform)}/${enc(externalId)}`, { method: "DELETE" });
366
+ }
367
+ /** Machine (`messaging:link`): claim a `(platform, externalId)` mapping from a link code. */
368
+ createMessagingLink(code, platform, externalId) {
369
+ return this.request("/messaging/links", {
370
+ method: "POST",
371
+ body: JSON.stringify({ code, platform, externalId }),
372
+ });
373
+ }
374
+ /** Machine (`messaging:resolve`): resolve an identity to user info. */
375
+ resolveMessaging(platform, externalId) {
376
+ const qs = `?platform=${encodeURIComponent(platform)}&externalId=${encodeURIComponent(externalId)}`;
377
+ return this.request(`/messaging/resolve${qs}`);
378
+ }
379
+ /** Machine (`messaging:resolve`): resolve an identity to a minted token for `api`. */
380
+ resolveMessagingToken(platform, externalId, api) {
381
+ const qs = `?platform=${encodeURIComponent(platform)}&externalId=${encodeURIComponent(externalId)}` +
382
+ `&format=jwt&api=${encodeURIComponent(api)}`;
383
+ return this.request(`/messaging/resolve${qs}`);
384
+ }
385
+ // ── config ────────────────────────────────────────────────────────────────────
386
+ getConfig() {
387
+ return this.request("/config");
388
+ }
389
+ putConfig(config) {
390
+ return this.request("/config", { method: "PUT", body: JSON.stringify(config) });
391
+ }
392
+ /** The user + tenant custom-field schemas (any authenticated admin; no `manage:config` needed). */
393
+ getCustomFields() {
394
+ return this.request("/config/custom-fields");
395
+ }
396
+ // ── API keys: tenant service keys (write:members) ──────────────────────────────
397
+ createApiKey(tenantId, request) {
398
+ return this.request(`/tenants/${enc(tenantId)}/api-keys`, {
399
+ method: "POST",
400
+ body: JSON.stringify(request),
401
+ });
402
+ }
403
+ async listApiKeys(tenantId) {
404
+ const data = await this.request(`/tenants/${enc(tenantId)}/api-keys`);
405
+ return data.keys;
406
+ }
407
+ async deleteApiKey(tenantId, keyId) {
408
+ await this.request(`/tenants/${enc(tenantId)}/api-keys/${enc(keyId)}`, { method: "DELETE" });
409
+ }
410
+ // ── Personal access tokens: your own (self-service) ────────────────────────────
411
+ /** Create a personal access token that acts as the current user (optionally down-scoped).
412
+ * The `apiKey` secret in the response is shown only once. */
413
+ createMyPat(request) {
414
+ return this.request("/auth/me/api-keys", {
415
+ method: "POST",
416
+ body: JSON.stringify(request),
417
+ });
418
+ }
419
+ async listMyPats() {
420
+ const data = await this.request("/auth/me/api-keys");
421
+ return data.keys;
422
+ }
423
+ async deleteMyPat(keyId) {
424
+ await this.request(`/auth/me/api-keys/${enc(keyId)}`, { method: "DELETE" });
425
+ }
426
+ }
427
+ function enc(value) {
428
+ return encodeURIComponent(value);
429
+ }
@@ -0,0 +1,4 @@
1
+ export type { UmamiClientOptions } from "./client.js";
2
+ export { UmamiClient, UmamiError } from "./client.js";
3
+ export * from "./types.js";
4
+ export { assertionToJSON, b64urlToBuffer, bufferToB64url, registrationToJSON, toCreationOptions, toRequestOptions, } from "./webauthn.js";