@awsmbiz/inbis-api-client 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,568 @@
1
+ import createClient from "openapi-fetch";
2
+ import { createAuthClient } from "better-auth/client";
3
+ import { adminClient } from "better-auth/client/plugins";
4
+ // src/api-error.ts
5
+ /**
6
+ * One error type for every failed call: app routes (the platform envelope
7
+ * `{ error: { code, message, requestId, details? } }`) and auth routes
8
+ * (Better Auth's `{ code, message, details? }`).
9
+ */
10
+ /** Codes the platform returns that callers commonly branch on. Any other server code passes through as is. */
11
+ const API_ERROR_CODES = {
12
+ /** No response: offline, DNS, CORS or the server is unreachable (`status` 0). */
13
+ NETWORK_ERROR: "NETWORK_ERROR",
14
+ /** Sign in again with one of `details.allowedMethods` (see {@link ApiError.requirement}). */
15
+ STEP_UP_REQUIRED: "STEP_UP_REQUIRED",
16
+ /** Complete two-factor authentication (or enrol an authenticator) to continue. */
17
+ TWO_FACTOR_REQUIRED: "TWO_FACTOR_REQUIRED",
18
+ /** The action needs a recent sign-in. */
19
+ SESSION_NOT_FRESH: "SESSION_NOT_FRESH",
20
+ /** The session has ended. */
21
+ SESSION_EXPIRED: "SESSION_EXPIRED",
22
+ /** A settings change would leave no administrator able to use the admin area. */
23
+ ADMIN_LOCKOUT: "ADMIN_LOCKOUT",
24
+ /** The runtime auth settings changed since they were loaded (a stale-version conflict). */
25
+ SETTINGS_VERSION_CONFLICT: "SETTINGS_VERSION_CONFLICT",
26
+ VALIDATION_FAILED: "VALIDATION_FAILED",
27
+ RATE_LIMITED: "RATE_LIMITED",
28
+ UNAUTHORIZED: "UNAUTHORIZED",
29
+ FORBIDDEN: "FORBIDDEN",
30
+ NOT_FOUND: "NOT_FOUND",
31
+ INTERNAL_ERROR: "INTERNAL_ERROR",
32
+ /** A failed response without a recognisable code. */
33
+ REQUEST_FAILED: "REQUEST_FAILED"
34
+ };
35
+ const HTTP_CODES = {
36
+ 400: "BAD_REQUEST",
37
+ 401: "UNAUTHORIZED",
38
+ 403: "FORBIDDEN",
39
+ 404: "NOT_FOUND",
40
+ 405: "METHOD_NOT_ALLOWED",
41
+ 409: "CONFLICT",
42
+ 413: "PAYLOAD_TOO_LARGE",
43
+ 415: "UNSUPPORTED_MEDIA_TYPE",
44
+ 422: "VALIDATION_FAILED",
45
+ 429: "RATE_LIMITED",
46
+ 500: "INTERNAL_ERROR"
47
+ };
48
+ const API_ERROR = Symbol.for("@awsmbiz/inbis-api-client/ApiError");
49
+ /** A failed call. `code` is stable and machine-readable; `message` is safe to show. */
50
+ var ApiError = class ApiError extends Error {
51
+ /** HTTP status; `0` when no response arrived (`NETWORK_ERROR`). */
52
+ status;
53
+ code;
54
+ /** The server's request ID (app routes), for support and logs. */
55
+ requestId;
56
+ /** Extra caller-safe information from the server, e.g. `{ allowedMethods }` or validation issues. */
57
+ details;
58
+ [API_ERROR] = true;
59
+ constructor(options) {
60
+ super(options.message, options.cause === void 0 ? void 0 : { cause: options.cause });
61
+ this.name = "ApiError";
62
+ this.status = options.status;
63
+ this.code = options.code;
64
+ this.requestId = options.requestId ?? null;
65
+ this.details = options.details;
66
+ }
67
+ /** The server could not be reached. */
68
+ get isNetworkError() {
69
+ return this.code === API_ERROR_CODES.NETWORK_ERROR;
70
+ }
71
+ /**
72
+ * A stale-version conflict: someone else changed the resource since it was
73
+ * loaded (`409` with a `…VERSION_CONFLICT` code, e.g. `SETTINGS_VERSION_CONFLICT`).
74
+ * Reload and let the user reapply their change.
75
+ */
76
+ get isVersionConflict() {
77
+ return this.status === 409 && this.code.endsWith("VERSION_CONFLICT");
78
+ }
79
+ /** What the session needs before retrying (`TWO_FACTOR_REQUIRED` / `STEP_UP_REQUIRED`), else `null`. */
80
+ get requirement() {
81
+ if (this.code === API_ERROR_CODES.TWO_FACTOR_REQUIRED) return { kind: "two-factor" };
82
+ if (this.code !== API_ERROR_CODES.STEP_UP_REQUIRED) return null;
83
+ const allowed = this.details?.allowedMethods;
84
+ return {
85
+ kind: "step-up",
86
+ allowedMethods: Array.isArray(allowed) ? allowed.filter((m) => typeof m === "string") : []
87
+ };
88
+ }
89
+ /** Reads a failed response's body (either error shape). Consumes the body. */
90
+ static async fromResponse(response) {
91
+ const body = await response.json().catch(() => null);
92
+ return ApiError.fromBody(response.status, body);
93
+ }
94
+ /** Builds the error from a status and a parsed body (either error shape, or anything else). */
95
+ static fromBody(status, body) {
96
+ const record = asRecord(body);
97
+ const fields = asRecord(record?.["error"]) ?? record;
98
+ const code = fields?.["code"];
99
+ const message = fields?.["message"];
100
+ const requestId = fields?.["requestId"];
101
+ return new ApiError({
102
+ status,
103
+ code: typeof code === "string" && code !== "" ? code : HTTP_CODES[status] ?? (status >= 500 ? "INTERNAL_ERROR" : "REQUEST_FAILED"),
104
+ message: typeof message === "string" && message !== "" ? message : "The request failed.",
105
+ requestId: typeof requestId === "string" ? requestId : null,
106
+ details: fields?.["details"]
107
+ });
108
+ }
109
+ /** No response arrived. */
110
+ static network(cause) {
111
+ return new ApiError({
112
+ status: 0,
113
+ code: API_ERROR_CODES.NETWORK_ERROR,
114
+ message: "Could not reach the server.",
115
+ cause
116
+ });
117
+ }
118
+ };
119
+ function asRecord(value) {
120
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
121
+ }
122
+ /** True for any {@link ApiError}, including instances from another copy of this package. */
123
+ function isApiError(value) {
124
+ return typeof value === "object" && value !== null && API_ERROR in value;
125
+ }
126
+ // src/api-client.ts
127
+ /** Creates the typed client for the app's routes (see the README for generating `Paths`). */
128
+ function createApiClient(options = {}) {
129
+ const client = createClient({
130
+ baseUrl: options.baseUrl ?? "",
131
+ credentials: "include",
132
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
133
+ ...options.headers === void 0 ? {} : { headers: options.headers }
134
+ });
135
+ client.use({
136
+ onResponse: async ({ response }) => {
137
+ if (!response.ok) throw await ApiError.fromResponse(response.clone());
138
+ },
139
+ onError: ({ error }) => isApiError(error) ? error : ApiError.network(error)
140
+ });
141
+ return client;
142
+ }
143
+ // src/role-admin.ts
144
+ /**
145
+ * `@awsmbiz/inbis-access-control`'s role admin API (`mountRoleAdmin`, under
146
+ * `/api/admin`): the permission catalogue, roles, and users' role
147
+ * assignments. Errors use the `@awsmbiz/inbis-api` envelope; every call rejects with
148
+ * an {@link ApiError}.
149
+ */
150
+ const reviveRole = (role) => ({
151
+ ...role,
152
+ createdAt: new Date(role.createdAt),
153
+ updatedAt: new Date(role.updatedAt)
154
+ });
155
+ const reviveUserRoles = (body) => ({ items: body.items.map((role) => ({
156
+ ...role,
157
+ assignedAt: new Date(role.assignedAt)
158
+ })) });
159
+ /** The role admin calls under `adminBase` (origin + admin path, e.g. `https://app.test/api/admin`). */
160
+ function createRoleAdminClient(adminBase, doFetch) {
161
+ const call = async (path, init = {}) => {
162
+ let response;
163
+ try {
164
+ response = await doFetch(`${adminBase}${path}`, {
165
+ method: init.method ?? "GET",
166
+ credentials: "include",
167
+ headers: init.body === void 0 ? { accept: "application/json" } : {
168
+ accept: "application/json",
169
+ "content-type": "application/json"
170
+ },
171
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
172
+ ...init.signal === void 0 ? {} : { signal: init.signal }
173
+ });
174
+ } catch (cause) {
175
+ throw ApiError.network(cause);
176
+ }
177
+ if (!response.ok) throw await ApiError.fromResponse(response);
178
+ return response;
179
+ };
180
+ const json = async (path, init) => await (await call(path, init)).json();
181
+ const role = (roleId) => `/roles/${encodeURIComponent(roleId)}`;
182
+ const userRole = (userId, roleId) => `/users/${encodeURIComponent(userId)}/roles/${encodeURIComponent(roleId)}`;
183
+ return {
184
+ permissions: { list: (init = {}) => json("/permissions", { signal: init.signal }) },
185
+ roles: {
186
+ list: async (init = {}) => ({ items: (await json("/roles", { signal: init.signal })).items.map(reviveRole) }),
187
+ get: async (roleId, init = {}) => reviveRole(await json(role(roleId), { signal: init.signal })),
188
+ create: async (input) => reviveRole(await json("/roles", {
189
+ method: "POST",
190
+ body: input
191
+ })),
192
+ update: async (roleId, changes) => reviveRole(await json(role(roleId), {
193
+ method: "PATCH",
194
+ body: changes
195
+ })),
196
+ delete: async (roleId) => {
197
+ await call(role(roleId), { method: "DELETE" });
198
+ }
199
+ },
200
+ userRoles: {
201
+ list: async (userId, init = {}) => reviveUserRoles(await json(`/users/${encodeURIComponent(userId)}/roles`, { signal: init.signal })),
202
+ assign: async (userId, roleId) => reviveUserRoles(await json(userRole(userId, roleId), { method: "PUT" })),
203
+ unassign: async (userId, roleId) => reviveUserRoles(await json(userRole(userId, roleId), { method: "DELETE" }))
204
+ }
205
+ };
206
+ }
207
+ // src/admin-routes.ts
208
+ /**
209
+ * The platform's admin app routes (Hono, under `/api/admin`): the dashboard
210
+ * stats (`mountAuthStats` from `@awsmbiz/inbis-auth/hono`), the audit timeline
211
+ * (`mountAuditTimeline` from `@awsmbiz/inbis-audit/hono`), and roles (`role-admin.ts`). Their errors use the
212
+ * `@awsmbiz/inbis-api` envelope; all reject with an {@link ApiError}.
213
+ */
214
+ const dateOrNull = (value) => value === null ? null : new Date(value);
215
+ const reviveSummary = (user) => ({
216
+ ...user,
217
+ banExpires: dateOrNull(user.banExpires),
218
+ createdAt: new Date(user.createdAt)
219
+ });
220
+ function searchOf(query) {
221
+ const params = new URLSearchParams();
222
+ for (const [key, value] of Object.entries(query)) {
223
+ if (value === void 0) continue;
224
+ params.set(key, value instanceof Date ? value.toISOString() : String(value));
225
+ }
226
+ const search = params.toString();
227
+ return search === "" ? "" : `?${search}`;
228
+ }
229
+ function createAdminRoutesClient(options) {
230
+ const request = async (method, path, signal) => {
231
+ let response;
232
+ try {
233
+ response = await options.fetch(`${options.baseURL}${options.adminPath}${path}`, {
234
+ method,
235
+ credentials: "include",
236
+ headers: method === "POST" ? {
237
+ accept: "application/json",
238
+ "content-type": "application/json"
239
+ } : { accept: "application/json" },
240
+ ...method === "POST" ? { body: "{}" } : {},
241
+ ...signal === void 0 ? {} : { signal }
242
+ });
243
+ } catch (cause) {
244
+ throw ApiError.network(cause);
245
+ }
246
+ if (!response.ok) throw await ApiError.fromResponse(response);
247
+ return await response.json();
248
+ };
249
+ const get = (path, signal) => request("GET", path, signal);
250
+ return {
251
+ ...createRoleAdminClient(`${options.baseURL}${options.adminPath}`, options.fetch),
252
+ stats: async (init = {}) => {
253
+ const body = await get("/auth-stats", init.signal);
254
+ return {
255
+ ...body,
256
+ generatedAt: new Date(body.generatedAt)
257
+ };
258
+ },
259
+ auditEvents: { list: async (query = {}, init = {}) => {
260
+ const body = await get(`/audit-events${searchOf(query)}`, init.signal);
261
+ return {
262
+ items: body.items.map((item) => ({
263
+ ...item,
264
+ occurredAt: new Date(item.occurredAt)
265
+ })),
266
+ nextCursor: body.nextCursor
267
+ };
268
+ } },
269
+ users: {
270
+ list: async (query = {}, init = {}) => {
271
+ const body = await get(`/users${searchOf(query)}`, init.signal);
272
+ return {
273
+ ...body,
274
+ users: body.users.map(reviveSummary)
275
+ };
276
+ },
277
+ get: async (userId, init = {}) => {
278
+ const { user } = await get(`/users/${encodeURIComponent(userId)}`, init.signal);
279
+ return {
280
+ ...reviveSummary(user),
281
+ signInMethods: user.signInMethods.map((method) => ({
282
+ ...method,
283
+ createdAt: dateOrNull(method.createdAt)
284
+ })),
285
+ updatedAt: new Date(user.updatedAt)
286
+ };
287
+ },
288
+ resetTwoFactor: (userId) => request("POST", `/users/${encodeURIComponent(userId)}/two-factor/reset`)
289
+ }
290
+ };
291
+ }
292
+ // src/auth-settings.ts
293
+ /**
294
+ * `@awsmbiz/inbis-auth`'s runtime settings routes (`mountAuthSettings`): the wire
295
+ * shapes and the calls. App routes, so errors use the platform envelope.
296
+ */
297
+ /** The settings and provider-status calls under `adminBase` (origin + admin path, e.g. `https://app.test/api/admin`). */
298
+ function authSettingsClients(adminBase, doFetch) {
299
+ const call = async (path, init = {}) => {
300
+ let response;
301
+ try {
302
+ response = await doFetch(`${adminBase}${path}`, {
303
+ ...init,
304
+ credentials: "include"
305
+ });
306
+ } catch (cause) {
307
+ throw ApiError.network(cause);
308
+ }
309
+ if (!response.ok) throw await ApiError.fromResponse(response);
310
+ return await response.json();
311
+ };
312
+ return {
313
+ settings: {
314
+ get: () => call("/auth-settings"),
315
+ update: (input) => call("/auth-settings", {
316
+ method: "PATCH",
317
+ headers: { "content-type": "application/json" },
318
+ body: JSON.stringify(input)
319
+ })
320
+ },
321
+ providers: { status: () => call("/auth-providers/status") }
322
+ };
323
+ }
324
+ // src/auth-admin-client.ts
325
+ const DEFAULT_BASE_PATH = "/api/auth";
326
+ function pageOrigin() {
327
+ return globalThis.location?.origin ?? "";
328
+ }
329
+ function betterAuthClient(options) {
330
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
331
+ return createAuthClient({
332
+ baseURL: `${options.baseURL ?? pageOrigin()}${options.basePath ?? DEFAULT_BASE_PATH}`,
333
+ plugins: [adminClient()],
334
+ fetchOptions: {
335
+ throw: true,
336
+ credentials: "include",
337
+ customFetchImpl: async (input, init) => {
338
+ try {
339
+ return await doFetch(input, init);
340
+ } catch (cause) {
341
+ throw ApiError.network(cause);
342
+ }
343
+ },
344
+ onError: ({ response, error }) => {
345
+ throw ApiError.fromBody(response.status, error);
346
+ }
347
+ }
348
+ });
349
+ }
350
+ /** Creates the client for auth admin endpoints. */
351
+ function createAuthAdminClient(options = {}) {
352
+ const client = betterAuthClient(options);
353
+ const call = client.$fetch;
354
+ return {
355
+ ...createAdminRoutesClient({
356
+ baseURL: options.baseURL ?? pageOrigin(),
357
+ adminPath: options.adminPath ?? "/api/admin",
358
+ fetch: options.fetch ?? ((input, init) => fetch(input, init))
359
+ }),
360
+ admin: client.admin,
361
+ userActions: {
362
+ create: (input) => call("/admin/create-user", {
363
+ method: "POST",
364
+ body: input
365
+ }),
366
+ revokeSession: (input) => call("/admin/revoke-user-session-by-id", {
367
+ method: "POST",
368
+ body: input
369
+ })
370
+ },
371
+ invitations: {
372
+ create: (input) => call("/admin/create-invitation", {
373
+ method: "POST",
374
+ body: input
375
+ }),
376
+ list: (input = {}) => call("/admin/list-invitations", {
377
+ method: "GET",
378
+ query: input
379
+ }),
380
+ resend: (input) => call("/admin/resend-invitation", {
381
+ method: "POST",
382
+ body: input
383
+ }),
384
+ revoke: (input) => call("/admin/revoke-invitation", {
385
+ method: "POST",
386
+ body: input
387
+ })
388
+ },
389
+ ...authSettingsClients(`${options.baseURL ?? pageOrigin()}${options.adminPath ?? "/api/admin"}`, options.fetch ?? ((input, init) => fetch(input, init)))
390
+ };
391
+ }
392
+ // src/query.ts
393
+ /** Invalidates every query whose key starts with one of `keys`. */
394
+ async function invalidateQueries(queryClient, ...keys) {
395
+ await Promise.all(keys.map((queryKey) => queryClient.invalidateQueries({ queryKey })));
396
+ }
397
+ function apiQueryKey(path, params) {
398
+ return params === void 0 ? ["api", path] : [
399
+ "api",
400
+ path,
401
+ params
402
+ ];
403
+ }
404
+ /** Invalidates every query of the given app-route paths (any params), or of all app routes when none is given. */
405
+ function invalidateApi(queryClient, ...paths) {
406
+ return invalidateQueries(queryClient, ...paths.length === 0 ? [["api"]] : paths.map((path) => apiQueryKey(path)));
407
+ }
408
+ /**
409
+ * Query options for a `GET` app route: keyed by path and `params` (see
410
+ * {@link apiQueryKey}), failing with an `ApiError`:
411
+ * `useQuery(apiQueryOptions(api, "/v1/notes/{id}", { params: { path: { id } } }))`.
412
+ */
413
+ function apiQueryOptions(client, path, ...[init]) {
414
+ const params = init?.params;
415
+ const get = client.GET;
416
+ return {
417
+ queryKey: params === void 0 ? apiQueryKey(path) : apiQueryKey(path, params),
418
+ queryFn: async ({ signal }) => (await get(path, {
419
+ ...init,
420
+ signal
421
+ })).data
422
+ };
423
+ }
424
+ /**
425
+ * Query keys for auth admin data. Invalidate `users()` after any change to a
426
+ * user (it covers lists, details and sessions), `invitations()` after any
427
+ * invitation change, `stats()` after anything the dashboard counts, or `all`
428
+ * for everything.
429
+ */
430
+ const authAdminKeys = {
431
+ all: ["auth-admin"],
432
+ users: () => ["auth-admin", "users"],
433
+ userList: (query) => [
434
+ "auth-admin",
435
+ "users",
436
+ "list",
437
+ query
438
+ ],
439
+ user: (userId) => [
440
+ "auth-admin",
441
+ "users",
442
+ "detail",
443
+ userId
444
+ ],
445
+ userSessions: (userId) => [
446
+ "auth-admin",
447
+ "users",
448
+ "detail",
449
+ userId,
450
+ "sessions"
451
+ ],
452
+ /** The admin directory (`users.list`: combined filters). */
453
+ userDirectory: (query) => [
454
+ "auth-admin",
455
+ "users",
456
+ "directory",
457
+ query
458
+ ],
459
+ /** One user's profile, sign-in methods and 2FA status (`users.get`). */
460
+ userProfile: (userId) => [
461
+ "auth-admin",
462
+ "users",
463
+ "detail",
464
+ userId,
465
+ "profile"
466
+ ],
467
+ invitations: () => ["auth-admin", "invitations"],
468
+ invitationList: (query) => [
469
+ "auth-admin",
470
+ "invitations",
471
+ "list",
472
+ query
473
+ ],
474
+ settings: () => ["auth-admin", "settings"],
475
+ providerStatus: () => [
476
+ "auth-admin",
477
+ "providers",
478
+ "status"
479
+ ],
480
+ stats: () => ["auth-admin", "stats"],
481
+ auditEvents: () => ["auth-admin", "audit-events"],
482
+ auditEventList: (query) => [
483
+ "auth-admin",
484
+ "audit-events",
485
+ "list",
486
+ query
487
+ ],
488
+ /** The declared permission catalogue (`permissions.list`). */
489
+ permissions: () => ["auth-admin", "permissions"],
490
+ /** Every role query: invalidate after a role change or an assignment (it changes `userCount`). */
491
+ roles: () => ["auth-admin", "roles"],
492
+ roleList: () => [
493
+ "auth-admin",
494
+ "roles",
495
+ "list"
496
+ ],
497
+ role: (roleId) => [
498
+ "auth-admin",
499
+ "roles",
500
+ "detail",
501
+ roleId
502
+ ],
503
+ /** A user's assigned roles (`userRoles.list`); covered by `users()`. */
504
+ userRoles: (userId) => [
505
+ "auth-admin",
506
+ "users",
507
+ "detail",
508
+ userId,
509
+ "roles"
510
+ ]
511
+ };
512
+ /** Query options for auth admin data, keyed by {@link authAdminKeys}. */
513
+ function authAdminQueries(auth) {
514
+ return {
515
+ userList: (query) => ({
516
+ queryKey: authAdminKeys.userList(query),
517
+ queryFn: ({ signal }) => auth.admin.listUsers({
518
+ query,
519
+ fetchOptions: { signal }
520
+ })
521
+ }),
522
+ user: (userId) => ({
523
+ queryKey: authAdminKeys.user(userId),
524
+ queryFn: ({ signal }) => auth.admin.getUser({
525
+ query: { id: userId },
526
+ fetchOptions: { signal }
527
+ })
528
+ }),
529
+ userSessions: (userId) => ({
530
+ queryKey: authAdminKeys.userSessions(userId),
531
+ queryFn: ({ signal }) => auth.admin.listUserSessions({
532
+ userId,
533
+ fetchOptions: { signal }
534
+ })
535
+ }),
536
+ userDirectory: (query) => ({
537
+ queryKey: authAdminKeys.userDirectory(query),
538
+ queryFn: ({ signal }) => auth.users.list(query, { signal })
539
+ }),
540
+ userProfile: (userId) => ({
541
+ queryKey: authAdminKeys.userProfile(userId),
542
+ queryFn: ({ signal }) => auth.users.get(userId, { signal })
543
+ }),
544
+ invitationList: (query) => ({
545
+ queryKey: authAdminKeys.invitationList(query),
546
+ queryFn: () => auth.invitations.list(query)
547
+ }),
548
+ settings: () => ({
549
+ queryKey: authAdminKeys.settings(),
550
+ queryFn: () => auth.settings.get()
551
+ }),
552
+ providerStatus: () => ({
553
+ queryKey: authAdminKeys.providerStatus(),
554
+ queryFn: () => auth.providers.status()
555
+ }),
556
+ stats: () => ({
557
+ queryKey: authAdminKeys.stats(),
558
+ queryFn: ({ signal }) => auth.stats({ signal })
559
+ }),
560
+ auditEventList: (query) => ({
561
+ queryKey: authAdminKeys.auditEventList(query),
562
+ queryFn: ({ signal }) => auth.auditEvents.list(query, { signal })
563
+ })
564
+ };
565
+ }
566
+ export { API_ERROR_CODES, ApiError, apiQueryKey, apiQueryOptions, authAdminKeys, authAdminQueries, createApiClient, createAuthAdminClient, invalidateApi, invalidateQueries, isApiError };
567
+
568
+ //# sourceMappingURL=index.js.map