@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
package/dist/types.js
CHANGED
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsDH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,CAAe,EACU,EAAE,CAC1B,CAAwB,CAAC,iBAAiB,KAAK,IAAI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexys/user-management-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pure TypeScript client for the Nexys user-management API (auth, tenants, users, roles, refresh tokens, profile). No React, no DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
+
"bun": "./src/index.ts",
|
|
12
13
|
"import": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
16
21
|
"scripts": {
|
|
17
22
|
"build": "tsc -p tsconfig.build.json",
|
|
18
23
|
"test": "bun test",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createClient, type Endpoint } from "./endpoint.js";
|
|
4
|
+
import {
|
|
5
|
+
adminListUsers,
|
|
6
|
+
adminRevokeUserTokens,
|
|
7
|
+
me,
|
|
8
|
+
signIn,
|
|
9
|
+
signOut,
|
|
10
|
+
} from "./endpoints/index.js";
|
|
11
|
+
import { parseApiError, type ApiError } from "./result.js";
|
|
12
|
+
import { isTwoFactorChallenge } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const jsonResponse = (body: unknown, status = 200): Response =>
|
|
15
|
+
new Response(JSON.stringify(body), {
|
|
16
|
+
status,
|
|
17
|
+
headers: { "content-type": "application/json" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const makeClient = (
|
|
21
|
+
responder: (url: string, init: RequestInit) => Response | Promise<Response>,
|
|
22
|
+
opts: { onUnauthorized?: () => void; headers?: Record<string, string> } = {}
|
|
23
|
+
) => {
|
|
24
|
+
const fetchSpy = mock((input: RequestInfo | URL, init?: RequestInit) =>
|
|
25
|
+
Promise.resolve(responder(input.toString(), init ?? {}))
|
|
26
|
+
);
|
|
27
|
+
const client = createClient({
|
|
28
|
+
baseUrl: "https://auth.example.com/",
|
|
29
|
+
fetch: fetchSpy as unknown as typeof fetch,
|
|
30
|
+
...opts,
|
|
31
|
+
});
|
|
32
|
+
return { client, fetchSpy };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("createClient + call", () => {
|
|
36
|
+
test("strips a trailing slash, posts JSON, includes credentials", async () => {
|
|
37
|
+
const { client, fetchSpy } = makeClient((url, init) => {
|
|
38
|
+
expect(url).toBe("https://auth.example.com/api/auth/sign-in/email");
|
|
39
|
+
expect(init.method).toBe("POST");
|
|
40
|
+
expect(init.credentials).toBe("include");
|
|
41
|
+
const headers = init.headers as Record<string, string>;
|
|
42
|
+
expect(headers["content-type"]).toBe("application/json");
|
|
43
|
+
expect(headers["accept"]).toBe("application/json");
|
|
44
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
45
|
+
email: "a@b.co",
|
|
46
|
+
password: "hunter2hunter2",
|
|
47
|
+
});
|
|
48
|
+
return jsonResponse({ user: { id: "u1", email: "a@b.co" } });
|
|
49
|
+
});
|
|
50
|
+
const res = await client.call(signIn, { email: "a@b.co", password: "hunter2hunter2" });
|
|
51
|
+
expect(res.ok).toBe(true);
|
|
52
|
+
if (res.ok && !isTwoFactorChallenge(res.data)) {
|
|
53
|
+
expect(res.data.user.id).toBe("u1");
|
|
54
|
+
}
|
|
55
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("returns ok=false with the parsed http error on a 4xx JSON body", async () => {
|
|
59
|
+
const { client } = makeClient(() =>
|
|
60
|
+
jsonResponse(
|
|
61
|
+
{ code: "INVALID_EMAIL_OR_PASSWORD", message: "Invalid email or password." },
|
|
62
|
+
401
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
const res = await client.call(signIn, { email: "x@y.z", password: "whatever" });
|
|
66
|
+
expect(res.ok).toBe(false);
|
|
67
|
+
if (!res.ok && res.error.kind === "http") {
|
|
68
|
+
expect(res.error.status).toBe(401);
|
|
69
|
+
expect(res.error.code).toBe("INVALID_EMAIL_OR_PASSWORD");
|
|
70
|
+
expect(res.error.message).toBe("Invalid email or password.");
|
|
71
|
+
} else {
|
|
72
|
+
throw new Error("expected http error");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns ok=false with { kind: 'network' } when fetch throws", async () => {
|
|
77
|
+
const { client } = makeClient(() => {
|
|
78
|
+
throw new Error("dns failure");
|
|
79
|
+
});
|
|
80
|
+
const res = await client.call(me);
|
|
81
|
+
expect(res.ok).toBe(false);
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
expect(res.error.kind).toBe("network");
|
|
84
|
+
if (res.error.kind === "network") {
|
|
85
|
+
expect(res.error.message).toBe("dns failure");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("fires onUnauthorized once when the server returns 401", async () => {
|
|
91
|
+
const onUnauthorized = mock(() => {});
|
|
92
|
+
const { client } = makeClient(() => jsonResponse({ code: "unauth" }, 401), {
|
|
93
|
+
onUnauthorized,
|
|
94
|
+
});
|
|
95
|
+
await client.call(me);
|
|
96
|
+
expect(onUnauthorized).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("serialises query params and drops undefined", async () => {
|
|
100
|
+
const { client } = makeClient((url) => {
|
|
101
|
+
expect(url).toBe(
|
|
102
|
+
"https://auth.example.com/api/admin/users?status=active&limit=25"
|
|
103
|
+
);
|
|
104
|
+
return jsonResponse({ total: 0, rows: [] });
|
|
105
|
+
});
|
|
106
|
+
await client.call(adminListUsers, { status: "active", limit: 25, offset: undefined });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("encodes path parameters", async () => {
|
|
110
|
+
const { client } = makeClient((url) => {
|
|
111
|
+
expect(url).toBe(
|
|
112
|
+
"https://auth.example.com/api/admin/users/u%2F1/revoke-tokens"
|
|
113
|
+
);
|
|
114
|
+
return jsonResponse({ ok: true, sessionsRevoked: 0, refreshTokensRevoked: 0 });
|
|
115
|
+
});
|
|
116
|
+
await client.call(adminRevokeUserTokens, { userId: "u/1" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("merges configured extra headers into every request", async () => {
|
|
120
|
+
const { client, fetchSpy } = makeClient(() => jsonResponse({ success: true }), {
|
|
121
|
+
headers: { "x-tenant": "acme" },
|
|
122
|
+
});
|
|
123
|
+
await client.call(signOut);
|
|
124
|
+
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
|
|
125
|
+
const headers = init.headers as Record<string, string>;
|
|
126
|
+
expect(headers["x-tenant"]).toBe("acme");
|
|
127
|
+
expect(headers["accept"]).toBe("application/json");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("handles 204 No Content by feeding null into parseOutput", async () => {
|
|
131
|
+
type In = { id: string };
|
|
132
|
+
const noContentEndpoint: Endpoint<In, { ok: true }, ApiError> = {
|
|
133
|
+
method: "DELETE",
|
|
134
|
+
path: ({ id }) => `/api/items/${id}`,
|
|
135
|
+
parseOutput: (data) => {
|
|
136
|
+
expect(data).toBeNull();
|
|
137
|
+
return { ok: true };
|
|
138
|
+
},
|
|
139
|
+
parseError: parseApiError,
|
|
140
|
+
};
|
|
141
|
+
const { client } = makeClient(() => new Response(null, { status: 204 }));
|
|
142
|
+
const res = await client.call(noContentEndpoint, { id: "a" });
|
|
143
|
+
expect(res.ok).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("omits the input argument for endpoints whose Input is void", async () => {
|
|
147
|
+
const { client } = makeClient(() =>
|
|
148
|
+
jsonResponse({
|
|
149
|
+
user: { id: "u1", email: "a@b.co", name: "A", role: "user" },
|
|
150
|
+
session: { activeOrganizationId: null, tenantRoles: [] },
|
|
151
|
+
authSource: "cookie",
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
// No 2nd arg — typechecks because me: Endpoint<void, …>
|
|
155
|
+
const res = await client.call(me);
|
|
156
|
+
expect(res.ok).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
package/src/endpoint.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { NetworkError, Result } from "./result.js";
|
|
2
|
+
|
|
3
|
+
export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An Endpoint is plain data — no execution, no transport. It says
|
|
7
|
+
* "this is how the URL is shaped, how to encode the body, and how to
|
|
8
|
+
* parse the response (ok or error)". Run it with `call(endpoint, input)`.
|
|
9
|
+
*/
|
|
10
|
+
export interface Endpoint<Input, Output, Err> {
|
|
11
|
+
method: Method;
|
|
12
|
+
path: (input: Input) => string;
|
|
13
|
+
/** Returns the JSON body. Omit for GET / DELETE. */
|
|
14
|
+
body?: (input: Input) => unknown;
|
|
15
|
+
/** Returns query params; undefined / null entries are dropped. */
|
|
16
|
+
query?: (input: Input) => Record<string, string | number | boolean | undefined | null>;
|
|
17
|
+
parseOutput: (data: unknown) => Output;
|
|
18
|
+
parseError: (status: number, data: unknown) => Err;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ClientConfig {
|
|
22
|
+
/** API base URL — e.g. https://auth.example.com (no trailing slash). */
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
/** Override the global fetch (SSR / tests). */
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
/** Headers merged into every request. */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
/** Fired whenever the server returns 401 — typically triggers a re-login flow. */
|
|
29
|
+
onUnauthorized?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pull I, O, E out of a concrete endpoint so call() can be typed
|
|
34
|
+
* without callers spelling the parameters.
|
|
35
|
+
*/
|
|
36
|
+
export type EndpointInput<E> = E extends Endpoint<infer I, unknown, unknown> ? I : never;
|
|
37
|
+
export type EndpointOutput<E> = E extends Endpoint<unknown, infer O, unknown> ? O : never;
|
|
38
|
+
export type EndpointError<E> = E extends Endpoint<unknown, unknown, infer Err> ? Err : never;
|
|
39
|
+
|
|
40
|
+
/** "If Input is void, omit the input arg" trick. */
|
|
41
|
+
type CallArgs<I> = [I] extends [void] ? [] : [input: I];
|
|
42
|
+
|
|
43
|
+
export interface UmClient {
|
|
44
|
+
/** Execute an endpoint. Never throws — returns Result. */
|
|
45
|
+
call<I, O, E>(
|
|
46
|
+
endpoint: Endpoint<I, O, E>,
|
|
47
|
+
...args: CallArgs<I>
|
|
48
|
+
): Promise<Result<O, E | NetworkError>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stripTrailing = (b: string): string => b.replace(/\/+$/, "");
|
|
52
|
+
|
|
53
|
+
const encodeQuery = (
|
|
54
|
+
q: Record<string, string | number | boolean | undefined | null> | undefined
|
|
55
|
+
): string => {
|
|
56
|
+
if (!q) return "";
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (const [k, v] of Object.entries(q)) {
|
|
59
|
+
if (v === undefined || v === null || v === "") continue;
|
|
60
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
61
|
+
}
|
|
62
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a client. The returned object exposes a single `call` method;
|
|
67
|
+
* everything else is endpoint data.
|
|
68
|
+
*
|
|
69
|
+
* const um = createClient({ baseUrl: "https://auth.example.com" });
|
|
70
|
+
* const res = await um.call(signIn, { email, password });
|
|
71
|
+
* if (res.ok) { … } else { … }
|
|
72
|
+
*/
|
|
73
|
+
export const createClient = (config: ClientConfig): UmClient => {
|
|
74
|
+
const baseUrl = stripTrailing(config.baseUrl);
|
|
75
|
+
const fetcher = config.fetch ?? (globalThis.fetch?.bind(globalThis) as typeof fetch);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
call: async <I, O, E>(
|
|
79
|
+
endpoint: Endpoint<I, O, E>,
|
|
80
|
+
...args: CallArgs<I>
|
|
81
|
+
): Promise<Result<O, E | NetworkError>> => {
|
|
82
|
+
const input = (args[0] as I) ?? (undefined as I);
|
|
83
|
+
const url = `${baseUrl}${endpoint.path(input)}${encodeQuery(endpoint.query?.(input))}`;
|
|
84
|
+
const init: RequestInit = {
|
|
85
|
+
method: endpoint.method,
|
|
86
|
+
credentials: "include",
|
|
87
|
+
headers: {
|
|
88
|
+
accept: "application/json",
|
|
89
|
+
...(endpoint.body ? { "content-type": "application/json" } : {}),
|
|
90
|
+
...(config.headers ?? {}),
|
|
91
|
+
},
|
|
92
|
+
...(endpoint.body ? { body: JSON.stringify(endpoint.body(input)) } : {}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let res: Response;
|
|
96
|
+
try {
|
|
97
|
+
res = await fetcher(url, init);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: {
|
|
102
|
+
kind: "network",
|
|
103
|
+
message: e instanceof Error ? e.message : String(e),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 204 No Content — no body to parse. Endpoints that legitimately
|
|
109
|
+
// return 204 should set parseOutput to accept null.
|
|
110
|
+
const data: unknown =
|
|
111
|
+
res.status === 204
|
|
112
|
+
? null
|
|
113
|
+
: await res.json().catch(() => null);
|
|
114
|
+
|
|
115
|
+
if (res.status === 401) config.onUnauthorized?.();
|
|
116
|
+
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
return { ok: false, error: endpoint.parseError(res.status, data) };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, data: endpoint.parseOutput(data) };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
|
|
5
|
+
export interface AuditLogRow {
|
|
6
|
+
id: string;
|
|
7
|
+
action: string;
|
|
8
|
+
actorId: string | null;
|
|
9
|
+
actorEmail: string | null;
|
|
10
|
+
actorType: string | null;
|
|
11
|
+
resourceType: string | null;
|
|
12
|
+
resourceId: string | null;
|
|
13
|
+
organizationId: string | null;
|
|
14
|
+
ipAddress: string | null;
|
|
15
|
+
userAgent: string | null;
|
|
16
|
+
metadata: unknown;
|
|
17
|
+
success: boolean;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuditLogListResponse {
|
|
22
|
+
total: number;
|
|
23
|
+
rows: AuditLogRow[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Query filters supported by `/api/admin/audit-logs`.
|
|
28
|
+
* Mirrors `apps/server/crates/um-server/src/routes/admin_audit.rs` — keep
|
|
29
|
+
* field names + semantics aligned with the server's `ListQuery`. Note:
|
|
30
|
+
* `actionPrefix` is a prefix match; there's no exact-action filter.
|
|
31
|
+
*/
|
|
32
|
+
export interface AdminListAuditLogsInput {
|
|
33
|
+
actorId?: string;
|
|
34
|
+
actionPrefix?: string;
|
|
35
|
+
organizationId?: string;
|
|
36
|
+
since?: string;
|
|
37
|
+
until?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
offset?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const adminListAuditLogs: Endpoint<
|
|
43
|
+
AdminListAuditLogsInput,
|
|
44
|
+
AuditLogListResponse,
|
|
45
|
+
ApiError
|
|
46
|
+
> = {
|
|
47
|
+
method: "GET",
|
|
48
|
+
path: () => "/api/admin/audit-logs",
|
|
49
|
+
query: (input) => ({
|
|
50
|
+
actorId: input.actorId,
|
|
51
|
+
actionPrefix: input.actionPrefix,
|
|
52
|
+
organizationId: input.organizationId,
|
|
53
|
+
since: input.since,
|
|
54
|
+
until: input.until,
|
|
55
|
+
limit: input.limit,
|
|
56
|
+
offset: input.offset,
|
|
57
|
+
}),
|
|
58
|
+
parseOutput: (data) => data as AuditLogListResponse,
|
|
59
|
+
parseError: parseApiError,
|
|
60
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
|
|
5
|
+
export interface AdminKeyRow {
|
|
6
|
+
kid: string;
|
|
7
|
+
alg: string;
|
|
8
|
+
status: "active" | "retired" | "revoked" | (string & {});
|
|
9
|
+
createdAt: string;
|
|
10
|
+
retiredAt: string | null;
|
|
11
|
+
revokedAt: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const adminListKeys: Endpoint<void, { keys: AdminKeyRow[] }, ApiError> = {
|
|
15
|
+
method: "GET",
|
|
16
|
+
path: () => "/api/admin/keys",
|
|
17
|
+
parseOutput: (data) => data as { keys: AdminKeyRow[] },
|
|
18
|
+
parseError: parseApiError,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const adminRotateKey: Endpoint<
|
|
22
|
+
void,
|
|
23
|
+
{ kid: string; createdAt: string },
|
|
24
|
+
ApiError
|
|
25
|
+
> = {
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: () => "/api/admin/keys/rotate",
|
|
28
|
+
parseOutput: (data) => data as { kid: string; createdAt: string },
|
|
29
|
+
parseError: parseApiError,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const adminRevokeKey: Endpoint<{ kid: string }, { ok: true }, ApiError> = {
|
|
33
|
+
method: "POST",
|
|
34
|
+
path: ({ kid }) => `/api/admin/keys/${encodeURIComponent(kid)}/revoke`,
|
|
35
|
+
parseOutput: () => ({ ok: true }),
|
|
36
|
+
parseError: parseApiError,
|
|
37
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
import type {
|
|
5
|
+
AdminUserListResponse,
|
|
6
|
+
AdminUserRow,
|
|
7
|
+
AuthMethodsResponse,
|
|
8
|
+
UserMembership,
|
|
9
|
+
UserRole,
|
|
10
|
+
UserStatus,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
|
|
13
|
+
export interface AdminListUsersInput {
|
|
14
|
+
status?: UserStatus;
|
|
15
|
+
includeDeleted?: boolean;
|
|
16
|
+
limit?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const adminListUsers: Endpoint<
|
|
21
|
+
AdminListUsersInput,
|
|
22
|
+
AdminUserListResponse,
|
|
23
|
+
ApiError
|
|
24
|
+
> = {
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: () => "/api/admin/users",
|
|
27
|
+
query: (input) => ({
|
|
28
|
+
status: input.status,
|
|
29
|
+
includeDeleted: input.includeDeleted ? "true" : undefined,
|
|
30
|
+
limit: input.limit,
|
|
31
|
+
offset: input.offset,
|
|
32
|
+
}),
|
|
33
|
+
parseOutput: (data) => data as AdminUserListResponse,
|
|
34
|
+
parseError: parseApiError,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Single user with linked auth providers — powers the detail page. */
|
|
38
|
+
export const adminGetUser: Endpoint<{ userId: string }, AdminUserRow, ApiError> = {
|
|
39
|
+
method: "GET",
|
|
40
|
+
path: ({ userId }) => `/api/admin/users/${encodeURIComponent(userId)}`,
|
|
41
|
+
parseOutput: (data) => data as AdminUserRow,
|
|
42
|
+
parseError: parseApiError,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Auth-method inventory: accounts, passkeys, 2FA flag. */
|
|
46
|
+
export const adminAuthMethods: Endpoint<
|
|
47
|
+
{ userId: string },
|
|
48
|
+
AuthMethodsResponse,
|
|
49
|
+
ApiError
|
|
50
|
+
> = {
|
|
51
|
+
method: "GET",
|
|
52
|
+
path: ({ userId }) =>
|
|
53
|
+
`/api/admin/users/${encodeURIComponent(userId)}/auth-methods`,
|
|
54
|
+
parseOutput: (data) => data as AuthMethodsResponse,
|
|
55
|
+
parseError: parseApiError,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add a sign-in method as `{authType, value}`. authType "credential"
|
|
60
|
+
* sets a password (value = the password); a social authType (google,
|
|
61
|
+
* microsoft, …) pre-links the provider account (value = provider-side
|
|
62
|
+
* account id or email) so the user's first social sign-in lands here.
|
|
63
|
+
*/
|
|
64
|
+
export const adminAddAccount: Endpoint<
|
|
65
|
+
{ userId: string; authType: string; value: string },
|
|
66
|
+
{ id: string; providerId: string },
|
|
67
|
+
ApiError
|
|
68
|
+
> = {
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: ({ userId }) => `/api/admin/users/${encodeURIComponent(userId)}/accounts`,
|
|
71
|
+
body: ({ authType, value }) => ({ authType, value }),
|
|
72
|
+
parseOutput: (data) => data as { id: string; providerId: string },
|
|
73
|
+
parseError: parseApiError,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Unlink an account row (social link or password credential). 409 if it's the last sign-in method. */
|
|
77
|
+
export const adminUnlinkAccount: Endpoint<
|
|
78
|
+
{ userId: string; accountId: string },
|
|
79
|
+
{ ok: true },
|
|
80
|
+
ApiError
|
|
81
|
+
> = {
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
path: ({ userId, accountId }) =>
|
|
84
|
+
`/api/admin/users/${encodeURIComponent(userId)}/accounts/${encodeURIComponent(accountId)}`,
|
|
85
|
+
parseOutput: () => ({ ok: true }),
|
|
86
|
+
parseError: parseApiError,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Remove a passkey. 409 if it's the last sign-in method. */
|
|
90
|
+
export const adminDeletePasskey: Endpoint<
|
|
91
|
+
{ userId: string; passkeyId: string },
|
|
92
|
+
{ ok: true },
|
|
93
|
+
ApiError
|
|
94
|
+
> = {
|
|
95
|
+
method: "DELETE",
|
|
96
|
+
path: ({ userId, passkeyId }) =>
|
|
97
|
+
`/api/admin/users/${encodeURIComponent(userId)}/passkeys/${encodeURIComponent(passkeyId)}`,
|
|
98
|
+
parseOutput: () => ({ ok: true }),
|
|
99
|
+
parseError: parseApiError,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Admin kill-switch for a user's TOTP (lost authenticator). */
|
|
103
|
+
export const adminDisableTwoFactor: Endpoint<
|
|
104
|
+
{ userId: string },
|
|
105
|
+
{ ok: true },
|
|
106
|
+
ApiError
|
|
107
|
+
> = {
|
|
108
|
+
method: "POST",
|
|
109
|
+
path: ({ userId }) =>
|
|
110
|
+
`/api/admin/users/${encodeURIComponent(userId)}/two-factor/disable`,
|
|
111
|
+
parseOutput: () => ({ ok: true }),
|
|
112
|
+
parseError: parseApiError,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Tenants the user belongs to — powers the "manage tenants" panel. */
|
|
116
|
+
export const adminListUserTenants: Endpoint<
|
|
117
|
+
{ userId: string },
|
|
118
|
+
{ memberships: UserMembership[] },
|
|
119
|
+
ApiError
|
|
120
|
+
> = {
|
|
121
|
+
method: "GET",
|
|
122
|
+
path: ({ userId }) => `/api/admin/users/${encodeURIComponent(userId)}/tenants`,
|
|
123
|
+
parseOutput: (data) => data as { memberships: UserMembership[] },
|
|
124
|
+
parseError: parseApiError,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export interface SetUserStatusResponse {
|
|
128
|
+
ok: true;
|
|
129
|
+
status: UserStatus;
|
|
130
|
+
sessionsRevoked: number;
|
|
131
|
+
refreshTokensRevoked: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const adminSetUserStatus: Endpoint<
|
|
135
|
+
{ userId: string; status: UserStatus },
|
|
136
|
+
SetUserStatusResponse,
|
|
137
|
+
ApiError
|
|
138
|
+
> = {
|
|
139
|
+
method: "POST",
|
|
140
|
+
path: ({ userId }) => `/api/admin/users/${encodeURIComponent(userId)}/status`,
|
|
141
|
+
body: ({ status }) => ({ status }),
|
|
142
|
+
parseOutput: (data) => data as SetUserStatusResponse,
|
|
143
|
+
parseError: parseApiError,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Role change rides the Better Auth admin-plugin route — the server
|
|
148
|
+
* guards demoting the last active superadmin (409).
|
|
149
|
+
*/
|
|
150
|
+
export const adminSetUserRole: Endpoint<
|
|
151
|
+
{ userId: string; role: UserRole },
|
|
152
|
+
{ ok: true },
|
|
153
|
+
ApiError
|
|
154
|
+
> = {
|
|
155
|
+
method: "POST",
|
|
156
|
+
path: () => "/api/auth/admin/set-role",
|
|
157
|
+
body: (input) => input,
|
|
158
|
+
parseOutput: () => ({ ok: true }),
|
|
159
|
+
parseError: parseApiError,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const adminRevokeUserTokens: Endpoint<
|
|
163
|
+
{ userId: string },
|
|
164
|
+
{ ok: true; sessionsRevoked: number; refreshTokensRevoked: number },
|
|
165
|
+
ApiError
|
|
166
|
+
> = {
|
|
167
|
+
method: "POST",
|
|
168
|
+
path: ({ userId }) => `/api/admin/users/${encodeURIComponent(userId)}/revoke-tokens`,
|
|
169
|
+
parseOutput: (data) =>
|
|
170
|
+
data as { ok: true; sessionsRevoked: number; refreshTokensRevoked: number },
|
|
171
|
+
parseError: parseApiError,
|
|
172
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint.js";
|
|
2
|
+
import type { ApiError } from "../result.js";
|
|
3
|
+
import { parseApiError } from "../result.js";
|
|
4
|
+
import type { ApiTokenCreated, ApiTokenRow } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Personal API tokens ("CLI" credentials). They authenticate as
|
|
8
|
+
* `Authorization: Bearer um_…` or `x-api-key`. Creating/revoking
|
|
9
|
+
* requires a session credential — a token cannot mint more tokens.
|
|
10
|
+
* Revoked tokens keep their row (status flips); the server's retention
|
|
11
|
+
* policy deletes dead rows after TOKEN_RETENTION_DAYS.
|
|
12
|
+
*/
|
|
13
|
+
export const createApiToken: Endpoint<
|
|
14
|
+
{ name: string; expiresInDays?: number },
|
|
15
|
+
ApiTokenCreated,
|
|
16
|
+
ApiError
|
|
17
|
+
> = {
|
|
18
|
+
method: "POST",
|
|
19
|
+
path: () => "/api/me/tokens",
|
|
20
|
+
body: (input) => input,
|
|
21
|
+
parseOutput: (data) => data as ApiTokenCreated,
|
|
22
|
+
parseError: parseApiError,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const listApiTokens: Endpoint<void, { tokens: ApiTokenRow[] }, ApiError> = {
|
|
26
|
+
method: "GET",
|
|
27
|
+
path: () => "/api/me/tokens",
|
|
28
|
+
parseOutput: (data) => data as { tokens: ApiTokenRow[] },
|
|
29
|
+
parseError: parseApiError,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const revokeApiToken: Endpoint<{ id: string }, { ok: true }, ApiError> = {
|
|
33
|
+
method: "DELETE",
|
|
34
|
+
path: (input) => `/api/me/tokens/${input.id}`,
|
|
35
|
+
parseOutput: () => ({ ok: true }),
|
|
36
|
+
parseError: parseApiError,
|
|
37
|
+
};
|