@nexys/user-management-client 0.1.0 → 0.2.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 +19 -0
- package/dist/endpoints/admin-users.d.ts +53 -23
- package/dist/endpoints/admin-users.d.ts.map +1 -1
- package/dist/endpoints/admin-users.js +56 -23
- package/dist/endpoints/admin-users.js.map +1 -1
- package/dist/endpoints/api-tokens.d.ts +23 -0
- package/dist/endpoints/api-tokens.d.ts.map +1 -0
- package/dist/endpoints/api-tokens.js +28 -0
- package/dist/endpoints/api-tokens.js.map +1 -0
- package/dist/endpoints/auth.d.ts +50 -7
- package/dist/endpoints/auth.d.ts.map +1 -1
- package/dist/endpoints/auth.js +33 -4
- package/dist/endpoints/auth.js.map +1 -1
- package/dist/endpoints/index.d.ts +4 -0
- package/dist/endpoints/index.d.ts.map +1 -1
- package/dist/endpoints/index.js +4 -0
- package/dist/endpoints/index.js.map +1 -1
- package/dist/endpoints/me.d.ts +14 -1
- package/dist/endpoints/me.d.ts.map +1 -1
- package/dist/endpoints/me.js +11 -0
- package/dist/endpoints/me.js.map +1 -1
- package/dist/endpoints/passkeys.d.ts +39 -0
- package/dist/endpoints/passkeys.d.ts.map +1 -0
- package/dist/endpoints/passkeys.js +53 -0
- package/dist/endpoints/passkeys.js.map +1 -0
- package/dist/endpoints/projects.d.ts +69 -0
- package/dist/endpoints/projects.d.ts.map +1 -0
- package/dist/endpoints/projects.js +73 -0
- package/dist/endpoints/projects.js.map +1 -0
- package/dist/endpoints/tenants.d.ts +72 -103
- package/dist/endpoints/tenants.d.ts.map +1 -1
- package/dist/endpoints/tenants.js +64 -82
- package/dist/endpoints/tenants.js.map +1 -1
- package/dist/endpoints/two-factor.d.ts +50 -0
- package/dist/endpoints/two-factor.d.ts.map +1 -0
- package/dist/endpoints/two-factor.js +64 -0
- package/dist/endpoints/two-factor.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +128 -44
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -2
- package/src/endpoint.test.ts +158 -0
- package/src/endpoint.ts +123 -0
- package/src/endpoints/admin-audit.ts +60 -0
- package/src/endpoints/admin-keys.ts +37 -0
- package/src/endpoints/admin-users.ts +172 -0
- package/src/endpoints/api-tokens.ts +37 -0
- package/src/endpoints/auth.ts +203 -0
- package/src/endpoints/health.ts +50 -0
- package/src/endpoints/index.ts +12 -0
- package/src/endpoints/me.ts +79 -0
- package/src/endpoints/passkeys.ts +77 -0
- package/src/endpoints/projects.ts +117 -0
- package/src/endpoints/refresh-tokens.ts +25 -0
- package/src/endpoints/tenants.ts +313 -0
- package/src/endpoints/two-factor.ts +84 -0
- package/src/index.ts +26 -0
- package/src/result.ts +39 -0
- package/src/types.ts +285 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
import type {
|
|
5
|
+
FullOrganization,
|
|
6
|
+
Invitation,
|
|
7
|
+
Member,
|
|
8
|
+
MyTenantPermissions,
|
|
9
|
+
RoleRow,
|
|
10
|
+
TenantRole,
|
|
11
|
+
TenantRow,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
|
|
14
|
+
export const adminListTenants: Endpoint<void, { tenants: TenantRow[] }, ApiError> = {
|
|
15
|
+
method: "GET",
|
|
16
|
+
path: () => "/api/admin/tenants",
|
|
17
|
+
parseOutput: (data) => data as { tenants: TenantRow[] },
|
|
18
|
+
parseError: parseApiError,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const createTenant: Endpoint<
|
|
22
|
+
{
|
|
23
|
+
name: string;
|
|
24
|
+
slug?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Attach the tenant to a project, gated by the project's settings
|
|
27
|
+
* (`allowTenantCreation`, `tenancyMode`).
|
|
28
|
+
*/
|
|
29
|
+
projectId?: string;
|
|
30
|
+
},
|
|
31
|
+
TenantRow,
|
|
32
|
+
ApiError
|
|
33
|
+
> = {
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: () => "/api/auth/organization/create",
|
|
36
|
+
body: (input) => input,
|
|
37
|
+
parseOutput: (data) => data as TenantRow,
|
|
38
|
+
parseError: parseApiError,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Update tenant name/slug/logo and the per-tenant `metadata` settings
|
|
43
|
+
* blob. Omitted fields keep their current value; `metadata: null`
|
|
44
|
+
* clears the blob. Requires org owner/admin (or a superadmin).
|
|
45
|
+
*/
|
|
46
|
+
export const updateTenant: Endpoint<
|
|
47
|
+
{
|
|
48
|
+
organizationId: string;
|
|
49
|
+
data: {
|
|
50
|
+
name?: string;
|
|
51
|
+
slug?: string;
|
|
52
|
+
logo?: string | null;
|
|
53
|
+
metadata?: Record<string, unknown> | null;
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
slug: string;
|
|
60
|
+
logo: string | null;
|
|
61
|
+
metadata: Record<string, unknown> | null;
|
|
62
|
+
},
|
|
63
|
+
ApiError
|
|
64
|
+
> = {
|
|
65
|
+
method: "POST",
|
|
66
|
+
path: () => "/api/auth/organization/update",
|
|
67
|
+
body: (input) => input,
|
|
68
|
+
parseOutput: (data) =>
|
|
69
|
+
data as {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
slug: string;
|
|
73
|
+
logo: string | null;
|
|
74
|
+
metadata: Record<string, unknown> | null;
|
|
75
|
+
},
|
|
76
|
+
parseError: parseApiError,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Owner self-service tenant delete. Cascades members, invitations, and
|
|
81
|
+
* roles. Requires the caller to be the org owner (or a superadmin). For
|
|
82
|
+
* the superadmin system-wide path see {@link adminDeleteTenant}.
|
|
83
|
+
*/
|
|
84
|
+
export const deleteTenant: Endpoint<
|
|
85
|
+
{ organizationId: string },
|
|
86
|
+
{ ok: true; id: string },
|
|
87
|
+
ApiError
|
|
88
|
+
> = {
|
|
89
|
+
method: "POST",
|
|
90
|
+
path: () => "/api/auth/organization/delete",
|
|
91
|
+
body: (input) => input,
|
|
92
|
+
parseOutput: (data) => data as { ok: true; id: string },
|
|
93
|
+
parseError: parseApiError,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const setActiveTenant: Endpoint<
|
|
97
|
+
{ organizationId: string | null },
|
|
98
|
+
{ session: { activeOrganizationId: string | null } },
|
|
99
|
+
ApiError
|
|
100
|
+
> = {
|
|
101
|
+
method: "POST",
|
|
102
|
+
path: () => "/api/auth/organization/set-active",
|
|
103
|
+
body: (input) => input,
|
|
104
|
+
parseOutput: (data) =>
|
|
105
|
+
data as { session: { activeOrganizationId: string | null } },
|
|
106
|
+
parseError: parseApiError,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const getFullTenant: Endpoint<
|
|
110
|
+
{ organizationId?: string },
|
|
111
|
+
FullOrganization,
|
|
112
|
+
ApiError
|
|
113
|
+
> = {
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: () => "/api/auth/organization/get-full-organization",
|
|
116
|
+
query: (input) => ({ organizationId: input.organizationId }),
|
|
117
|
+
parseOutput: (data) => data as FullOrganization,
|
|
118
|
+
parseError: parseApiError,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const inviteMember: Endpoint<
|
|
122
|
+
{ email: string; role: TenantRole; organizationId: string },
|
|
123
|
+
Invitation,
|
|
124
|
+
ApiError
|
|
125
|
+
> = {
|
|
126
|
+
method: "POST",
|
|
127
|
+
path: () => "/api/auth/organization/invite-member",
|
|
128
|
+
body: (input) => input,
|
|
129
|
+
parseOutput: (data) => data as Invitation,
|
|
130
|
+
parseError: parseApiError,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const acceptInvitation: Endpoint<
|
|
134
|
+
{ invitationId: string },
|
|
135
|
+
{ memberId: string; organizationId: string; role: TenantRole },
|
|
136
|
+
ApiError
|
|
137
|
+
> = {
|
|
138
|
+
method: "POST",
|
|
139
|
+
path: () => "/api/auth/organization/accept-invitation",
|
|
140
|
+
body: (input) => input,
|
|
141
|
+
parseOutput: (data) =>
|
|
142
|
+
data as { memberId: string; organizationId: string; role: TenantRole },
|
|
143
|
+
parseError: parseApiError,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const updateMemberRole: Endpoint<
|
|
147
|
+
{ memberId: string; role: TenantRole; organizationId: string },
|
|
148
|
+
{ ok: true },
|
|
149
|
+
ApiError
|
|
150
|
+
> = {
|
|
151
|
+
method: "POST",
|
|
152
|
+
path: () => "/api/auth/organization/update-member-role",
|
|
153
|
+
body: (input) => input,
|
|
154
|
+
parseOutput: () => ({ ok: true }),
|
|
155
|
+
parseError: parseApiError,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Direct member assignment — no invitation round-trip. The target
|
|
160
|
+
* user must already exist (lookup by userId or email); requires
|
|
161
|
+
* tenant owner/admin or a superadmin.
|
|
162
|
+
*/
|
|
163
|
+
export const addMember: Endpoint<
|
|
164
|
+
{
|
|
165
|
+
organizationId: string;
|
|
166
|
+
role: TenantRole;
|
|
167
|
+
userId?: string;
|
|
168
|
+
email?: string;
|
|
169
|
+
},
|
|
170
|
+
Member,
|
|
171
|
+
ApiError
|
|
172
|
+
> = {
|
|
173
|
+
method: "POST",
|
|
174
|
+
path: () => "/api/auth/organization/add-member",
|
|
175
|
+
body: (input) => input,
|
|
176
|
+
parseOutput: (data) => data as Member,
|
|
177
|
+
parseError: parseApiError,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const removeMember: Endpoint<
|
|
181
|
+
{ memberId: string; organizationId: string },
|
|
182
|
+
{ ok: true },
|
|
183
|
+
ApiError
|
|
184
|
+
> = {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: () => "/api/auth/organization/remove-member",
|
|
187
|
+
body: (input) => input,
|
|
188
|
+
parseOutput: () => ({ ok: true }),
|
|
189
|
+
parseError: parseApiError,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const adminDeleteTenant: Endpoint<
|
|
193
|
+
{ id: string },
|
|
194
|
+
{ ok: true },
|
|
195
|
+
ApiError
|
|
196
|
+
> = {
|
|
197
|
+
method: "DELETE",
|
|
198
|
+
path: (input) => `/api/admin/tenants/${input.id}`,
|
|
199
|
+
parseOutput: () => ({ ok: true }),
|
|
200
|
+
parseError: parseApiError,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const listTenantRoles: Endpoint<
|
|
204
|
+
{ orgId: string },
|
|
205
|
+
{ roles: RoleRow[] },
|
|
206
|
+
ApiError
|
|
207
|
+
> = {
|
|
208
|
+
method: "GET",
|
|
209
|
+
path: (input) => `/api/tenants/${input.orgId}/roles`,
|
|
210
|
+
parseOutput: (data) => data as { roles: RoleRow[] },
|
|
211
|
+
parseError: parseApiError,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const createTenantRole: Endpoint<
|
|
215
|
+
{ orgId: string; name: string; description?: string; permissions?: string[] },
|
|
216
|
+
RoleRow,
|
|
217
|
+
ApiError
|
|
218
|
+
> = {
|
|
219
|
+
method: "POST",
|
|
220
|
+
path: (input) => `/api/tenants/${input.orgId}/roles`,
|
|
221
|
+
body: (input) => ({
|
|
222
|
+
name: input.name,
|
|
223
|
+
description: input.description,
|
|
224
|
+
permissions: input.permissions,
|
|
225
|
+
}),
|
|
226
|
+
parseOutput: (data) => data as RoleRow,
|
|
227
|
+
parseError: parseApiError,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Update a role's name/description and, when `permissions` is present,
|
|
232
|
+
* replace its permission set wholesale.
|
|
233
|
+
*/
|
|
234
|
+
export const updateTenantRole: Endpoint<
|
|
235
|
+
{
|
|
236
|
+
orgId: string;
|
|
237
|
+
roleId: string;
|
|
238
|
+
name?: string;
|
|
239
|
+
description?: string;
|
|
240
|
+
permissions?: string[];
|
|
241
|
+
},
|
|
242
|
+
RoleRow,
|
|
243
|
+
ApiError
|
|
244
|
+
> = {
|
|
245
|
+
method: "PATCH",
|
|
246
|
+
path: (input) => `/api/tenants/${input.orgId}/roles/${input.roleId}`,
|
|
247
|
+
body: (input) => ({
|
|
248
|
+
name: input.name,
|
|
249
|
+
description: input.description,
|
|
250
|
+
permissions: input.permissions,
|
|
251
|
+
}),
|
|
252
|
+
parseOutput: (data) => data as RoleRow,
|
|
253
|
+
parseError: parseApiError,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const deleteTenantRole: Endpoint<
|
|
257
|
+
{ orgId: string; roleId: string },
|
|
258
|
+
{ ok: true },
|
|
259
|
+
ApiError
|
|
260
|
+
> = {
|
|
261
|
+
method: "DELETE",
|
|
262
|
+
path: (input) => `/api/tenants/${input.orgId}/roles/${input.roleId}`,
|
|
263
|
+
parseOutput: () => ({ ok: true }),
|
|
264
|
+
parseError: parseApiError,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/** Custom roles assigned to one member (with their permissions). */
|
|
268
|
+
export const listMemberRoles: Endpoint<
|
|
269
|
+
{ orgId: string; memberId: string },
|
|
270
|
+
{ roles: RoleRow[] },
|
|
271
|
+
ApiError
|
|
272
|
+
> = {
|
|
273
|
+
method: "GET",
|
|
274
|
+
path: (input) => `/api/tenants/${input.orgId}/members/${input.memberId}/roles`,
|
|
275
|
+
parseOutput: (data) => data as { roles: RoleRow[] },
|
|
276
|
+
parseError: parseApiError,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export const assignMemberRole: Endpoint<
|
|
280
|
+
{ orgId: string; memberId: string; roleId: string },
|
|
281
|
+
{ ok: true; roleId: string; memberId: string },
|
|
282
|
+
ApiError
|
|
283
|
+
> = {
|
|
284
|
+
method: "POST",
|
|
285
|
+
path: (input) => `/api/tenants/${input.orgId}/members/${input.memberId}/roles`,
|
|
286
|
+
body: (input) => ({ roleId: input.roleId }),
|
|
287
|
+
parseOutput: (data) => data as { ok: true; roleId: string; memberId: string },
|
|
288
|
+
parseError: parseApiError,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const removeMemberRole: Endpoint<
|
|
292
|
+
{ orgId: string; memberId: string; roleId: string },
|
|
293
|
+
{ ok: true },
|
|
294
|
+
ApiError
|
|
295
|
+
> = {
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
path: (input) =>
|
|
298
|
+
`/api/tenants/${input.orgId}/members/${input.memberId}/roles/${input.roleId}`,
|
|
299
|
+
parseOutput: () => ({ ok: true }),
|
|
300
|
+
parseError: parseApiError,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** The caller's own effective role/permissions inside a tenant. */
|
|
304
|
+
export const myTenantPermissions: Endpoint<
|
|
305
|
+
{ orgId: string },
|
|
306
|
+
MyTenantPermissions,
|
|
307
|
+
ApiError
|
|
308
|
+
> = {
|
|
309
|
+
method: "GET",
|
|
310
|
+
path: (input) => `/api/tenants/${input.orgId}/permissions/me`,
|
|
311
|
+
parseOutput: (data) => data as MyTenantPermissions,
|
|
312
|
+
parseError: parseApiError,
|
|
313
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
import type { AuthSuccess, TwoFactorEnableResponse } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start TOTP enrolment (cookie-auth). Returns the secret, the
|
|
8
|
+
* `otpauth://` URI to render as a QR, and 10 single-use backup codes
|
|
9
|
+
* that are shown exactly once. Enrolment isn't active until
|
|
10
|
+
* {@link twoFactorConfirm} verifies the first code.
|
|
11
|
+
*/
|
|
12
|
+
export const twoFactorEnable: Endpoint<void, TwoFactorEnableResponse, ApiError> = {
|
|
13
|
+
method: "POST",
|
|
14
|
+
path: () => "/api/auth/two-factor/enable",
|
|
15
|
+
parseOutput: (data) => data as TwoFactorEnableResponse,
|
|
16
|
+
parseError: parseApiError,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const twoFactorConfirm: Endpoint<{ code: string }, { ok: true }, ApiError> = {
|
|
20
|
+
method: "POST",
|
|
21
|
+
path: () => "/api/auth/two-factor/confirm",
|
|
22
|
+
body: (input) => input,
|
|
23
|
+
parseOutput: () => ({ ok: true }),
|
|
24
|
+
parseError: parseApiError,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Requires a current TOTP code; wipes the secret + backup codes. */
|
|
28
|
+
export const twoFactorDisable: Endpoint<{ code: string }, { ok: true }, ApiError> = {
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: () => "/api/auth/two-factor/disable",
|
|
31
|
+
body: (input) => input,
|
|
32
|
+
parseOutput: () => ({ ok: true }),
|
|
33
|
+
parseError: parseApiError,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Complete a password sign-in that returned a
|
|
38
|
+
* {@link TwoFactorChallenge}: `token` is the challenge token, `code`
|
|
39
|
+
* the current TOTP code. Sets the session cookie on success.
|
|
40
|
+
*/
|
|
41
|
+
export const verifyTotp: Endpoint<
|
|
42
|
+
{ token: string; code: string },
|
|
43
|
+
AuthSuccess,
|
|
44
|
+
ApiError
|
|
45
|
+
> = {
|
|
46
|
+
method: "POST",
|
|
47
|
+
path: () => "/api/auth/two-factor/verify-totp",
|
|
48
|
+
body: (input) => input,
|
|
49
|
+
parseOutput: (data) => data as AuthSuccess,
|
|
50
|
+
parseError: parseApiError,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Step-up re-verification for an already authenticated caller (cookie
|
|
55
|
+
* or bearer). Answers whether `code` is a current TOTP code for the
|
|
56
|
+
* caller without touching sessions or tokens — resource servers use it
|
|
57
|
+
* to gate sensitive actions (payments, key exports) on a fresh second
|
|
58
|
+
* factor. `enabled: false` means the caller has no verified TOTP
|
|
59
|
+
* enrolment (and `valid` is then always false).
|
|
60
|
+
*/
|
|
61
|
+
export const twoFactorStepUp: Endpoint<
|
|
62
|
+
{ code: string },
|
|
63
|
+
{ enabled: boolean; valid: boolean },
|
|
64
|
+
ApiError
|
|
65
|
+
> = {
|
|
66
|
+
method: "POST",
|
|
67
|
+
path: () => "/api/me/two-factor/step-up",
|
|
68
|
+
body: (input) => input,
|
|
69
|
+
parseOutput: (data) => data as { enabled: boolean; valid: boolean },
|
|
70
|
+
parseError: parseApiError,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Like {@link verifyTotp} but consumes a single-use backup code. */
|
|
74
|
+
export const verifyBackupCode: Endpoint<
|
|
75
|
+
{ token: string; code: string },
|
|
76
|
+
AuthSuccess,
|
|
77
|
+
ApiError
|
|
78
|
+
> = {
|
|
79
|
+
method: "POST",
|
|
80
|
+
path: () => "/api/auth/two-factor/verify-backup-code",
|
|
81
|
+
body: (input) => input,
|
|
82
|
+
parseOutput: (data) => data as AuthSuccess,
|
|
83
|
+
parseError: parseApiError,
|
|
84
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Core types & runtime
|
|
2
|
+
export {
|
|
3
|
+
createClient,
|
|
4
|
+
type ClientConfig,
|
|
5
|
+
type Endpoint,
|
|
6
|
+
type EndpointError,
|
|
7
|
+
type EndpointInput,
|
|
8
|
+
type EndpointOutput,
|
|
9
|
+
type Method,
|
|
10
|
+
type UmClient,
|
|
11
|
+
} from "./endpoint.js";
|
|
12
|
+
export {
|
|
13
|
+
err,
|
|
14
|
+
ok,
|
|
15
|
+
parseApiError,
|
|
16
|
+
type ApiError,
|
|
17
|
+
type NetworkError,
|
|
18
|
+
type Result,
|
|
19
|
+
} from "./result.js";
|
|
20
|
+
|
|
21
|
+
// Endpoints — all named exports, grouped by domain
|
|
22
|
+
export * from "./endpoints/index.js";
|
|
23
|
+
|
|
24
|
+
// Wire types
|
|
25
|
+
export type * from "./types.js";
|
|
26
|
+
export { isTwoFactorChallenge } from "./types.js";
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result + error types. Endpoints never throw — `call` collapses
|
|
3
|
+
* thrown exceptions into `{ kind: "network" }` and HTTP errors into
|
|
4
|
+
* whatever shape `endpoint.parseError` produces.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type Result<T, E> = { ok: true; data: T } | { ok: false; error: E };
|
|
8
|
+
|
|
9
|
+
export type NetworkError = { kind: "network"; message: string };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default HTTP error envelope mirroring the rust server's
|
|
13
|
+
* `{ code, message }` shape. Individual endpoints can narrow this
|
|
14
|
+
* with a discriminated union when they want callers to switch on
|
|
15
|
+
* specific codes (e.g. sign-in's INVALID_EMAIL_OR_PASSWORD).
|
|
16
|
+
*/
|
|
17
|
+
export type ApiError = {
|
|
18
|
+
kind: "http";
|
|
19
|
+
status: number;
|
|
20
|
+
code: string | undefined;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lift a JSON error body into `ApiError`. Tolerates non-JSON
|
|
26
|
+
* (data === null) and missing fields.
|
|
27
|
+
*/
|
|
28
|
+
export const parseApiError = (status: number, data: unknown): ApiError => {
|
|
29
|
+
const body = (data ?? {}) as { code?: string; error?: string; message?: string };
|
|
30
|
+
return {
|
|
31
|
+
kind: "http",
|
|
32
|
+
status,
|
|
33
|
+
code: body.code ?? body.error,
|
|
34
|
+
message: body.message ?? body.error ?? `Request failed (${status})`,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const ok = <T>(data: T): Result<T, never> => ({ ok: true, data });
|
|
39
|
+
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
|