@slyxup/core 1.0.2 → 1.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/dist/types.d.ts +15 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -19
- package/src/client.ts +0 -766
- package/src/errors.ts +0 -41
- package/src/index.ts +0 -3
- package/src/types.ts +0 -151
- package/test/client.test.ts +0 -249
- package/test/errors.test.ts +0 -109
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -2
package/src/client.ts
DELETED
|
@@ -1,766 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
NetworkError,
|
|
3
|
-
RateLimitError,
|
|
4
|
-
SlyxupError,
|
|
5
|
-
UnauthorizedError,
|
|
6
|
-
} from './errors.js';
|
|
7
|
-
import type {
|
|
8
|
-
AuthResponse,
|
|
9
|
-
ChangePasswordInput,
|
|
10
|
-
CompleteSignInInput,
|
|
11
|
-
ConnectedAccountsResponse,
|
|
12
|
-
EnableTOTPResponse,
|
|
13
|
-
ErrorResponse,
|
|
14
|
-
Result,
|
|
15
|
-
RevokeSessionsResponse,
|
|
16
|
-
SessionResponse,
|
|
17
|
-
SessionsResponse,
|
|
18
|
-
SignInInput,
|
|
19
|
-
SignInResponse,
|
|
20
|
-
SignUpInput,
|
|
21
|
-
SlyxupClientOptions,
|
|
22
|
-
TOTPSetupResponse,
|
|
23
|
-
TwoFactorRequiredResponse,
|
|
24
|
-
TwoFactorStatusResponse,
|
|
25
|
-
UpdateUserInput,
|
|
26
|
-
UserResponse,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
|
|
29
|
-
const DEFAULT_API_URL = 'https://auth.slyxup.online';
|
|
30
|
-
|
|
31
|
-
/** Minimal cookie jar so the SDK works in Node/SSR (browsers manage cookies natively). */
|
|
32
|
-
function createCookieJar() {
|
|
33
|
-
let jar: string | undefined;
|
|
34
|
-
return {
|
|
35
|
-
capture(res: Response) {
|
|
36
|
-
const setCookie = res.headers.get('set-cookie');
|
|
37
|
-
if (setCookie) jar = setCookie.split(';')[0]?.trim();
|
|
38
|
-
},
|
|
39
|
-
header(): Record<string, string> {
|
|
40
|
-
return jar ? { Cookie: jar } : {};
|
|
41
|
-
},
|
|
42
|
-
clear() {
|
|
43
|
-
jar = undefined;
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* SlyxUp Core Client.
|
|
50
|
-
*
|
|
51
|
-
* ```ts
|
|
52
|
-
* const client = new SlyxupClient({ publishableKey: 'pk_test_xxx' });
|
|
53
|
-
* await client.auth.signIn({ email: 'a@b.com', password: '12345678' });
|
|
54
|
-
* const session = await client.sessions.get();
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export class SlyxupClient {
|
|
58
|
-
readonly publishableKey?: string;
|
|
59
|
-
readonly secretKey?: string;
|
|
60
|
-
readonly apiUrl: string;
|
|
61
|
-
private _getToken?: () => string | undefined;
|
|
62
|
-
private _request?: <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
63
|
-
|
|
64
|
-
readonly auth: {
|
|
65
|
-
signUp: (input: SignUpInput) => Promise<AuthResponse>;
|
|
66
|
-
signIn: (input: SignInInput) => Promise<SignInResponse>;
|
|
67
|
-
signOut: () => Promise<{ ok: true }>;
|
|
68
|
-
resendVerification: (email: string) => Promise<{ ok: true }>;
|
|
69
|
-
forgotPassword: (
|
|
70
|
-
email: string,
|
|
71
|
-
projectId?: string
|
|
72
|
-
) => Promise<{ ok: true }>;
|
|
73
|
-
resetPassword: (token: string, password: string) => Promise<{ ok: true }>;
|
|
74
|
-
verifyEmail: (token: string) => Promise<{ ok: true }>;
|
|
75
|
-
/** Complete a 2FA challenge returned by signIn. */
|
|
76
|
-
completeSignIn: (input: CompleteSignInInput) => Promise<AuthResponse>;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
readonly sessions: {
|
|
80
|
-
get: () => Promise<SessionResponse>;
|
|
81
|
-
list: (opts?: {
|
|
82
|
-
limit?: number;
|
|
83
|
-
offset?: number;
|
|
84
|
-
}) => Promise<SessionsResponse>;
|
|
85
|
-
revoke: (sessionId: string) => Promise<{ ok: true }>;
|
|
86
|
-
revokeOthers: () => Promise<RevokeSessionsResponse>;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
readonly password: {
|
|
90
|
-
change: (input: ChangePasswordInput) => Promise<{ ok: true }>;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
readonly twoFactor: {
|
|
94
|
-
setup: () => Promise<TOTPSetupResponse>;
|
|
95
|
-
status: () => Promise<TwoFactorStatusResponse>;
|
|
96
|
-
enable: (secret: string, code: string) => Promise<EnableTOTPResponse>;
|
|
97
|
-
verify: (code: string) => Promise<{ ok: boolean; valid: boolean }>;
|
|
98
|
-
disable: (code: string) => Promise<{ ok: true }>;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
readonly accounts: {
|
|
102
|
-
list: () => Promise<ConnectedAccountsResponse>;
|
|
103
|
-
unlink: (
|
|
104
|
-
accountId: string,
|
|
105
|
-
provider: 'google' | 'github'
|
|
106
|
-
) => Promise<{ ok: true }>;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
readonly users: {
|
|
110
|
-
me: () => Promise<UserResponse>;
|
|
111
|
-
update: (input: UpdateUserInput) => Promise<UserResponse>;
|
|
112
|
-
delete: () => Promise<{ ok: true }>;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
readonly admin: {
|
|
116
|
-
/** Get current project details */
|
|
117
|
-
getProject: () => Promise<{
|
|
118
|
-
ok: true;
|
|
119
|
-
project: { id: string; name: string; createdAt: string };
|
|
120
|
-
}>;
|
|
121
|
-
/** Get project statistics */
|
|
122
|
-
getStats: () => Promise<{
|
|
123
|
-
ok: true;
|
|
124
|
-
stats: {
|
|
125
|
-
totalUsers: number;
|
|
126
|
-
totalSessions: number;
|
|
127
|
-
blockedUsers: number;
|
|
128
|
-
verifiedUsers: number;
|
|
129
|
-
totalKeys: number;
|
|
130
|
-
};
|
|
131
|
-
}>;
|
|
132
|
-
/** List users in the project */
|
|
133
|
-
listUsers: (opts?: { limit?: number; offset?: number }) => Promise<{
|
|
134
|
-
ok: true;
|
|
135
|
-
users: Array<{
|
|
136
|
-
id: string;
|
|
137
|
-
email: string;
|
|
138
|
-
firstName: string | null;
|
|
139
|
-
lastName: string | null;
|
|
140
|
-
emailVerified: boolean;
|
|
141
|
-
blocked: boolean;
|
|
142
|
-
blockedReason: string | null;
|
|
143
|
-
role: string;
|
|
144
|
-
createdAt: string;
|
|
145
|
-
}>;
|
|
146
|
-
total: number;
|
|
147
|
-
}>;
|
|
148
|
-
/** Get a single user by ID */
|
|
149
|
-
getUser: (userId: string) => Promise<{
|
|
150
|
-
ok: true;
|
|
151
|
-
user: {
|
|
152
|
-
id: string;
|
|
153
|
-
email: string;
|
|
154
|
-
firstName: string | null;
|
|
155
|
-
lastName: string | null;
|
|
156
|
-
emailVerified: boolean;
|
|
157
|
-
blocked: boolean;
|
|
158
|
-
blockedReason: string | null;
|
|
159
|
-
role: string;
|
|
160
|
-
twoFactorEnabled: boolean;
|
|
161
|
-
createdAt: string;
|
|
162
|
-
updatedAt: string;
|
|
163
|
-
};
|
|
164
|
-
}>;
|
|
165
|
-
/** Block a user and revoke all their sessions */
|
|
166
|
-
blockUser: (userId: string, reason?: string) => Promise<{ ok: true }>;
|
|
167
|
-
/** Unblock a user */
|
|
168
|
-
unblockUser: (userId: string) => Promise<{ ok: true }>;
|
|
169
|
-
/** Delete a user and all their sessions */
|
|
170
|
-
deleteUser: (userId: string) => Promise<{ ok: true }>;
|
|
171
|
-
/** List sessions (paginated) */
|
|
172
|
-
listSessions: (opts?: {
|
|
173
|
-
limit?: number;
|
|
174
|
-
offset?: number;
|
|
175
|
-
}) => Promise<{
|
|
176
|
-
ok: true;
|
|
177
|
-
sessions: Array<{
|
|
178
|
-
id: string;
|
|
179
|
-
userId: string;
|
|
180
|
-
ipAddress: string | null;
|
|
181
|
-
userAgent: string | null;
|
|
182
|
-
expiresAt: string;
|
|
183
|
-
isExpired: boolean;
|
|
184
|
-
createdAt: string;
|
|
185
|
-
}>;
|
|
186
|
-
total: number;
|
|
187
|
-
}>;
|
|
188
|
-
/** Revoke a specific session */
|
|
189
|
-
revokeSession: (sessionId: string) => Promise<{ ok: true }>;
|
|
190
|
-
/** Revoke all sessions for a user */
|
|
191
|
-
revokeAllSessions: (userId: string) => Promise<{ ok: true }>;
|
|
192
|
-
/** List API keys */
|
|
193
|
-
listKeys: () => Promise<{
|
|
194
|
-
ok: true;
|
|
195
|
-
keys: Array<{
|
|
196
|
-
id: string;
|
|
197
|
-
name: string;
|
|
198
|
-
prefix: string;
|
|
199
|
-
environment: string;
|
|
200
|
-
type: string;
|
|
201
|
-
lastUsedAt: string | null;
|
|
202
|
-
createdAt: string;
|
|
203
|
-
}>;
|
|
204
|
-
}>;
|
|
205
|
-
/** Create an API key (full key returned only once) */
|
|
206
|
-
createKey: (input: {
|
|
207
|
-
name: string;
|
|
208
|
-
type: 'publishable' | 'secret';
|
|
209
|
-
environment: 'test' | 'live';
|
|
210
|
-
}) => Promise<{ ok: true; id: string; key: string; prefix: string }>;
|
|
211
|
-
/** Revoke an API key */
|
|
212
|
-
revokeKey: (keyId: string) => Promise<{ ok: true }>;
|
|
213
|
-
/** List audit logs */
|
|
214
|
-
listAuditLogs: (opts?: {
|
|
215
|
-
action?: string;
|
|
216
|
-
userId?: string;
|
|
217
|
-
limit?: number;
|
|
218
|
-
offset?: number;
|
|
219
|
-
}) => Promise<{
|
|
220
|
-
ok: true;
|
|
221
|
-
total: number;
|
|
222
|
-
logs: Array<{
|
|
223
|
-
id: string;
|
|
224
|
-
action: string;
|
|
225
|
-
userId: string | null;
|
|
226
|
-
metadata: Record<string, unknown> | null;
|
|
227
|
-
ipAddress: string | null;
|
|
228
|
-
userAgent: string | null;
|
|
229
|
-
createdAt: string;
|
|
230
|
-
}>;
|
|
231
|
-
}>;
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/** Get current session token (for custom project APIs that need Bearer) */
|
|
235
|
-
getToken(): string | undefined {
|
|
236
|
-
return this._getToken?.();
|
|
237
|
-
}
|
|
238
|
-
/** Raw request for custom endpoints (uses same auth headers + cookies as SDK) */
|
|
239
|
-
async request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
240
|
-
if (!this._request) throw new Error('Client not initialized');
|
|
241
|
-
return this._request<T>(path, init);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
constructor(options: SlyxupClientOptions = {}) {
|
|
245
|
-
const jar = createCookieJar();
|
|
246
|
-
// Persist token in localStorage so refresh keeps session for cross-origin (auth -> billing) and for dashboard project APIs
|
|
247
|
-
const STORAGE_KEY = 'slyxup_session_token';
|
|
248
|
-
let storedToken: string | undefined;
|
|
249
|
-
try {
|
|
250
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
251
|
-
storedToken = window.localStorage.getItem(STORAGE_KEY) ?? undefined;
|
|
252
|
-
}
|
|
253
|
-
} catch {}
|
|
254
|
-
const persistToken = (t: string | undefined) => {
|
|
255
|
-
storedToken = t;
|
|
256
|
-
try {
|
|
257
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
258
|
-
if (t) window.localStorage.setItem(STORAGE_KEY, t);
|
|
259
|
-
else window.localStorage.removeItem(STORAGE_KEY);
|
|
260
|
-
}
|
|
261
|
-
} catch {}
|
|
262
|
-
};
|
|
263
|
-
this.publishableKey = options.publishableKey;
|
|
264
|
-
this.secretKey = options.secretKey;
|
|
265
|
-
this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
|
|
266
|
-
this._getToken = () => storedToken;
|
|
267
|
-
// _request will be assigned after `request` is defined below
|
|
268
|
-
|
|
269
|
-
const requestInner = async <T>(
|
|
270
|
-
path: string,
|
|
271
|
-
init: RequestInit & { body?: string } = {},
|
|
272
|
-
opts?: { captureError?: boolean }
|
|
273
|
-
): Promise<Result<T>> => {
|
|
274
|
-
let res: Response;
|
|
275
|
-
try {
|
|
276
|
-
// Build headers: prefer cookie jar (SSR), fall back to stored Bearer token (browser cross-origin)
|
|
277
|
-
const authHeaders: Record<string, string> = {};
|
|
278
|
-
const jarHeader = jar.header();
|
|
279
|
-
if (Object.keys(jarHeader).length > 0) {
|
|
280
|
-
Object.assign(authHeaders, jarHeader);
|
|
281
|
-
} else if (storedToken) {
|
|
282
|
-
authHeaders.Authorization = `Bearer ${storedToken}`;
|
|
283
|
-
}
|
|
284
|
-
// Always send publishable key so server can scope auth to the correct project
|
|
285
|
-
// and reject requests with invalid / missing keys (fixes demo-with-wrong-pk bug).
|
|
286
|
-
if (this.publishableKey && this.publishableKey !== 'pk_test_missing') {
|
|
287
|
-
authHeaders['X-Publishable-Key'] = this.publishableKey;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
res = await fetch(`${this.apiUrl}${path}`, {
|
|
291
|
-
...init,
|
|
292
|
-
headers: {
|
|
293
|
-
'Content-Type': 'application/json',
|
|
294
|
-
...authHeaders,
|
|
295
|
-
...init.headers,
|
|
296
|
-
},
|
|
297
|
-
credentials: 'include',
|
|
298
|
-
});
|
|
299
|
-
} catch {
|
|
300
|
-
throw new NetworkError();
|
|
301
|
-
}
|
|
302
|
-
jar.capture(res);
|
|
303
|
-
|
|
304
|
-
const data = (await res
|
|
305
|
-
.json()
|
|
306
|
-
.catch(() => ({ ok: false, error: 'Invalid response' }))) as object;
|
|
307
|
-
|
|
308
|
-
if (!res.ok) {
|
|
309
|
-
if (opts?.captureError) return data as Result<T>;
|
|
310
|
-
if (res.status === 401)
|
|
311
|
-
throw new UnauthorizedError(
|
|
312
|
-
'error' in data ? String(data.error) : undefined
|
|
313
|
-
);
|
|
314
|
-
if (res.status === 429) throw new RateLimitError();
|
|
315
|
-
throw new SlyxupError(
|
|
316
|
-
'error' in data
|
|
317
|
-
? String(data.error)
|
|
318
|
-
: `Request failed (${res.status})`,
|
|
319
|
-
res.status,
|
|
320
|
-
'api_error'
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return data as Result<T>;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const post = <T>(
|
|
328
|
-
path: string,
|
|
329
|
-
body?: unknown,
|
|
330
|
-
opts?: { captureError?: boolean }
|
|
331
|
-
) =>
|
|
332
|
-
requestInner<T>(
|
|
333
|
-
path,
|
|
334
|
-
{
|
|
335
|
-
method: 'POST',
|
|
336
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
337
|
-
},
|
|
338
|
-
opts
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
this.auth = {
|
|
342
|
-
signUp: async (input) => {
|
|
343
|
-
const res = await post<AuthResponse>('/v1/auth/sign-up', input);
|
|
344
|
-
if (!('user' in res))
|
|
345
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
346
|
-
if (res.sessionToken) persistToken(res.sessionToken);
|
|
347
|
-
return res;
|
|
348
|
-
},
|
|
349
|
-
signIn: async (input) => {
|
|
350
|
-
// The server may answer 403 with code 2FA_REQUIRED — we must surface
|
|
351
|
-
// the challenge back to the caller instead of throwing a generic error.
|
|
352
|
-
const res = await post<SignInResponse>('/v1/auth/sign-in', input, {
|
|
353
|
-
captureError: true,
|
|
354
|
-
});
|
|
355
|
-
if ('challengeToken' in res) {
|
|
356
|
-
return res as TwoFactorRequiredResponse;
|
|
357
|
-
}
|
|
358
|
-
if (!('user' in res))
|
|
359
|
-
throw new UnauthorizedError(
|
|
360
|
-
(res as { error?: string }).error ?? 'Sign in failed'
|
|
361
|
-
);
|
|
362
|
-
if (res.sessionToken) persistToken(res.sessionToken);
|
|
363
|
-
return res;
|
|
364
|
-
},
|
|
365
|
-
completeSignIn: async (input) => {
|
|
366
|
-
const res = await post<AuthResponse>('/v1/auth/sign-in/2fa', input);
|
|
367
|
-
if (!('user' in res)) throw new UnauthorizedError(res.error);
|
|
368
|
-
if (res.sessionToken) persistToken(res.sessionToken);
|
|
369
|
-
return res;
|
|
370
|
-
},
|
|
371
|
-
signOut: async () => {
|
|
372
|
-
const res = await post<{ ok: true }>('/v1/auth/sign-out');
|
|
373
|
-
persistToken(undefined);
|
|
374
|
-
jar.clear();
|
|
375
|
-
return res as { ok: true };
|
|
376
|
-
},
|
|
377
|
-
resendVerification: async (email: string) => {
|
|
378
|
-
const res = await post<{ ok: true }>('/v1/verification/resend', {
|
|
379
|
-
email,
|
|
380
|
-
});
|
|
381
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
382
|
-
return res as { ok: true };
|
|
383
|
-
},
|
|
384
|
-
forgotPassword: async (email: string, projectId?: string) => {
|
|
385
|
-
const res = await post<{ ok: true }>(
|
|
386
|
-
'/v1/verification/password/forgot',
|
|
387
|
-
{
|
|
388
|
-
email,
|
|
389
|
-
projectId,
|
|
390
|
-
}
|
|
391
|
-
);
|
|
392
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
393
|
-
return res as { ok: true };
|
|
394
|
-
},
|
|
395
|
-
resetPassword: async (token: string, password: string) => {
|
|
396
|
-
const res = await post<{ ok: true }>(
|
|
397
|
-
'/v1/verification/password/reset',
|
|
398
|
-
{
|
|
399
|
-
token,
|
|
400
|
-
password,
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
404
|
-
return res as { ok: true };
|
|
405
|
-
},
|
|
406
|
-
verifyEmail: async (token: string) => {
|
|
407
|
-
const res = await post<{ ok: true }>('/v1/verification/verify', {
|
|
408
|
-
token,
|
|
409
|
-
});
|
|
410
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
411
|
-
return res as { ok: true };
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
this.sessions = {
|
|
416
|
-
get: async () => {
|
|
417
|
-
const res = await requestInner<SessionResponse>('/v1/session');
|
|
418
|
-
if (!('session' in res)) throw new UnauthorizedError(res.error);
|
|
419
|
-
return res;
|
|
420
|
-
},
|
|
421
|
-
list: async (opts?: { limit?: number; offset?: number }) => {
|
|
422
|
-
const params = new URLSearchParams();
|
|
423
|
-
if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
|
|
424
|
-
if (opts?.offset !== undefined)
|
|
425
|
-
params.set('offset', String(opts.offset));
|
|
426
|
-
const qs = params.toString();
|
|
427
|
-
const res = await requestInner<SessionsResponse>(
|
|
428
|
-
`/v1/sessions${qs ? `?${qs}` : ''}`
|
|
429
|
-
);
|
|
430
|
-
if (!('sessions' in res))
|
|
431
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
432
|
-
return res;
|
|
433
|
-
},
|
|
434
|
-
revoke: async (sessionId: string) => {
|
|
435
|
-
const res = await requestInner<{ ok: true }>(
|
|
436
|
-
`/v1/sessions/${sessionId}`,
|
|
437
|
-
{
|
|
438
|
-
method: 'DELETE',
|
|
439
|
-
}
|
|
440
|
-
);
|
|
441
|
-
return res as { ok: true };
|
|
442
|
-
},
|
|
443
|
-
revokeOthers: async () => {
|
|
444
|
-
const res = await requestInner<RevokeSessionsResponse>('/v1/sessions', {
|
|
445
|
-
method: 'DELETE',
|
|
446
|
-
});
|
|
447
|
-
if (!('revoked' in res))
|
|
448
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
449
|
-
return res;
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
this.password = {
|
|
454
|
-
change: async (input) => {
|
|
455
|
-
const res = await post<{ ok: true }>('/v1/user/password', input);
|
|
456
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
457
|
-
return res as { ok: true };
|
|
458
|
-
},
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
this.twoFactor = {
|
|
462
|
-
setup: async () => {
|
|
463
|
-
const res = await requestInner<TOTPSetupResponse>('/v1/user/2fa/setup');
|
|
464
|
-
if (!('secret' in res))
|
|
465
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
466
|
-
return res;
|
|
467
|
-
},
|
|
468
|
-
status: async () => {
|
|
469
|
-
const res = await requestInner<TwoFactorStatusResponse>(
|
|
470
|
-
'/v1/user/2fa/status'
|
|
471
|
-
);
|
|
472
|
-
return res as TwoFactorStatusResponse;
|
|
473
|
-
},
|
|
474
|
-
enable: async (secret, code) => {
|
|
475
|
-
const res = await post<EnableTOTPResponse>(
|
|
476
|
-
'/v1/user/2fa/enable',
|
|
477
|
-
{ secret, code },
|
|
478
|
-
{ captureError: true }
|
|
479
|
-
);
|
|
480
|
-
if (!('recoveryCodes' in res))
|
|
481
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
482
|
-
return res;
|
|
483
|
-
},
|
|
484
|
-
verify: async (code) => {
|
|
485
|
-
const res = await post<{ ok: boolean; valid: boolean }>(
|
|
486
|
-
'/v1/user/2fa/verify',
|
|
487
|
-
{ code },
|
|
488
|
-
{ captureError: true }
|
|
489
|
-
);
|
|
490
|
-
if (!('valid' in res))
|
|
491
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
492
|
-
return res as { ok: boolean; valid: boolean };
|
|
493
|
-
},
|
|
494
|
-
disable: async (code) => {
|
|
495
|
-
const res = await post<{ ok: true }>('/v1/user/2fa/disable', { code });
|
|
496
|
-
if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
|
|
497
|
-
return res as { ok: true };
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
this.accounts = {
|
|
502
|
-
list: async () => {
|
|
503
|
-
const res =
|
|
504
|
-
await requestInner<ConnectedAccountsResponse>('/v1/user/accounts');
|
|
505
|
-
if (!('accounts' in res))
|
|
506
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
507
|
-
return res;
|
|
508
|
-
},
|
|
509
|
-
unlink: async (accountId, provider) => {
|
|
510
|
-
const res = await requestInner<{ ok: true }>(
|
|
511
|
-
`/v1/user/accounts/${accountId}?provider=${encodeURIComponent(provider)}`,
|
|
512
|
-
{
|
|
513
|
-
method: 'DELETE',
|
|
514
|
-
}
|
|
515
|
-
);
|
|
516
|
-
return res as { ok: true };
|
|
517
|
-
},
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
this.users = {
|
|
521
|
-
me: async () => {
|
|
522
|
-
const res = await requestInner<UserResponse>('/v1/user');
|
|
523
|
-
if (!('user' in res)) throw new UnauthorizedError(res.error);
|
|
524
|
-
return res;
|
|
525
|
-
},
|
|
526
|
-
update: async (input) => {
|
|
527
|
-
const res = await requestInner<UserResponse>('/v1/user', {
|
|
528
|
-
method: 'PATCH',
|
|
529
|
-
body: JSON.stringify(input),
|
|
530
|
-
});
|
|
531
|
-
if (!('user' in res))
|
|
532
|
-
throw new SlyxupError(res.error, 400, 'api_error');
|
|
533
|
-
return res;
|
|
534
|
-
},
|
|
535
|
-
delete: async () => {
|
|
536
|
-
const res = await requestInner<{ ok: true }>('/v1/user', {
|
|
537
|
-
method: 'DELETE',
|
|
538
|
-
});
|
|
539
|
-
return res as { ok: true };
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
// ── Admin API (requires secretKey) ──
|
|
544
|
-
const sk = options.secretKey;
|
|
545
|
-
const adminRequest = async <T>(
|
|
546
|
-
path: string,
|
|
547
|
-
init: RequestInit = {}
|
|
548
|
-
): Promise<T> => {
|
|
549
|
-
if (!sk)
|
|
550
|
-
throw new SlyxupError(
|
|
551
|
-
'Secret key required for admin API',
|
|
552
|
-
400,
|
|
553
|
-
'api_error'
|
|
554
|
-
);
|
|
555
|
-
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
556
|
-
...init,
|
|
557
|
-
headers: {
|
|
558
|
-
'Content-Type': 'application/json',
|
|
559
|
-
Authorization: `Bearer ${sk}`,
|
|
560
|
-
...init.headers,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
const data = (await res
|
|
564
|
-
.json()
|
|
565
|
-
.catch(() => ({ ok: false, error: 'Invalid response' }))) as object;
|
|
566
|
-
if (!res.ok) {
|
|
567
|
-
throw new SlyxupError(
|
|
568
|
-
'error' in data
|
|
569
|
-
? String((data as { error: unknown }).error)
|
|
570
|
-
: `Request failed (${res.status})`,
|
|
571
|
-
res.status,
|
|
572
|
-
'api_error'
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
return data as T;
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
this.admin = {
|
|
579
|
-
getProject: async () => {
|
|
580
|
-
const res = await adminRequest<{
|
|
581
|
-
ok: true;
|
|
582
|
-
project: { id: string; name: string; createdAt: string };
|
|
583
|
-
}>('/v1/admin/project');
|
|
584
|
-
return res;
|
|
585
|
-
},
|
|
586
|
-
getStats: async () => {
|
|
587
|
-
const res = await adminRequest<{
|
|
588
|
-
ok: true;
|
|
589
|
-
stats: {
|
|
590
|
-
totalUsers: number;
|
|
591
|
-
totalSessions: number;
|
|
592
|
-
blockedUsers: number;
|
|
593
|
-
verifiedUsers: number;
|
|
594
|
-
totalKeys: number;
|
|
595
|
-
};
|
|
596
|
-
}>('/v1/admin/stats');
|
|
597
|
-
return res;
|
|
598
|
-
},
|
|
599
|
-
listUsers: async (opts) => {
|
|
600
|
-
const params = new URLSearchParams();
|
|
601
|
-
if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
|
|
602
|
-
if (opts?.offset !== undefined)
|
|
603
|
-
params.set('offset', String(opts.offset));
|
|
604
|
-
const qs = params.toString();
|
|
605
|
-
const res = await adminRequest<{
|
|
606
|
-
ok: true;
|
|
607
|
-
users: Array<{
|
|
608
|
-
id: string;
|
|
609
|
-
email: string;
|
|
610
|
-
firstName: string | null;
|
|
611
|
-
lastName: string | null;
|
|
612
|
-
emailVerified: boolean;
|
|
613
|
-
blocked: boolean;
|
|
614
|
-
blockedReason: string | null;
|
|
615
|
-
role: string;
|
|
616
|
-
createdAt: string;
|
|
617
|
-
}>;
|
|
618
|
-
total: number;
|
|
619
|
-
}>(`/v1/admin/users${qs ? `?${qs}` : ''}`);
|
|
620
|
-
return res;
|
|
621
|
-
},
|
|
622
|
-
getUser: async (userId) => {
|
|
623
|
-
const res = await adminRequest<{
|
|
624
|
-
ok: true;
|
|
625
|
-
user: {
|
|
626
|
-
id: string;
|
|
627
|
-
email: string;
|
|
628
|
-
firstName: string | null;
|
|
629
|
-
lastName: string | null;
|
|
630
|
-
emailVerified: boolean;
|
|
631
|
-
blocked: boolean;
|
|
632
|
-
blockedReason: string | null;
|
|
633
|
-
role: string;
|
|
634
|
-
twoFactorEnabled: boolean;
|
|
635
|
-
createdAt: string;
|
|
636
|
-
updatedAt: string;
|
|
637
|
-
};
|
|
638
|
-
}>(`/v1/admin/users/${encodeURIComponent(userId)}`);
|
|
639
|
-
return res;
|
|
640
|
-
},
|
|
641
|
-
blockUser: async (userId, reason) => {
|
|
642
|
-
const res = await adminRequest<{ ok: true }>(
|
|
643
|
-
`/v1/admin/users/${encodeURIComponent(userId)}/block`,
|
|
644
|
-
{
|
|
645
|
-
method: 'POST',
|
|
646
|
-
body: JSON.stringify({ reason }),
|
|
647
|
-
}
|
|
648
|
-
);
|
|
649
|
-
return res as { ok: true };
|
|
650
|
-
},
|
|
651
|
-
unblockUser: async (userId) => {
|
|
652
|
-
const res = await adminRequest<{ ok: true }>(
|
|
653
|
-
`/v1/admin/users/${encodeURIComponent(userId)}/unblock`,
|
|
654
|
-
{ method: 'POST' }
|
|
655
|
-
);
|
|
656
|
-
return res as { ok: true };
|
|
657
|
-
},
|
|
658
|
-
deleteUser: async (userId) => {
|
|
659
|
-
const res = await adminRequest<{ ok: true }>(
|
|
660
|
-
`/v1/admin/users/${encodeURIComponent(userId)}`,
|
|
661
|
-
{ method: 'DELETE' }
|
|
662
|
-
);
|
|
663
|
-
return res as { ok: true };
|
|
664
|
-
},
|
|
665
|
-
listSessions: async (opts) => {
|
|
666
|
-
const params = new URLSearchParams();
|
|
667
|
-
if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
|
|
668
|
-
if (opts?.offset !== undefined)
|
|
669
|
-
params.set('offset', String(opts.offset));
|
|
670
|
-
const qs = params.toString();
|
|
671
|
-
const res = await adminRequest<{
|
|
672
|
-
ok: true;
|
|
673
|
-
sessions: Array<{
|
|
674
|
-
id: string;
|
|
675
|
-
userId: string;
|
|
676
|
-
ipAddress: string | null;
|
|
677
|
-
userAgent: string | null;
|
|
678
|
-
expiresAt: string;
|
|
679
|
-
isExpired: boolean;
|
|
680
|
-
createdAt: string;
|
|
681
|
-
}>;
|
|
682
|
-
total: number;
|
|
683
|
-
}>(`/v1/admin/sessions${qs ? `?${qs}` : ''}`);
|
|
684
|
-
return res;
|
|
685
|
-
},
|
|
686
|
-
revokeSession: async (sessionId) => {
|
|
687
|
-
const res = await adminRequest<{ ok: true }>(
|
|
688
|
-
`/v1/admin/sessions/${encodeURIComponent(sessionId)}`,
|
|
689
|
-
{ method: 'DELETE' }
|
|
690
|
-
);
|
|
691
|
-
return res as { ok: true };
|
|
692
|
-
},
|
|
693
|
-
revokeAllSessions: async (userId) => {
|
|
694
|
-
const res = await adminRequest<{ ok: true }>(
|
|
695
|
-
`/v1/admin/sessions?userId=${encodeURIComponent(userId)}`,
|
|
696
|
-
{ method: 'DELETE' }
|
|
697
|
-
);
|
|
698
|
-
return res as { ok: true };
|
|
699
|
-
},
|
|
700
|
-
listKeys: async () => {
|
|
701
|
-
const res = await adminRequest<{
|
|
702
|
-
ok: true;
|
|
703
|
-
keys: Array<{
|
|
704
|
-
id: string;
|
|
705
|
-
name: string;
|
|
706
|
-
prefix: string;
|
|
707
|
-
environment: string;
|
|
708
|
-
type: string;
|
|
709
|
-
lastUsedAt: string | null;
|
|
710
|
-
createdAt: string;
|
|
711
|
-
}>;
|
|
712
|
-
}>('/v1/admin/keys');
|
|
713
|
-
return res;
|
|
714
|
-
},
|
|
715
|
-
createKey: async (input) => {
|
|
716
|
-
const res = await adminRequest<{
|
|
717
|
-
ok: true;
|
|
718
|
-
id: string;
|
|
719
|
-
key: string;
|
|
720
|
-
prefix: string;
|
|
721
|
-
}>('/v1/admin/keys', { method: 'POST', body: JSON.stringify(input) });
|
|
722
|
-
return res;
|
|
723
|
-
},
|
|
724
|
-
revokeKey: async (keyId) => {
|
|
725
|
-
const res = await adminRequest<{ ok: true }>(
|
|
726
|
-
`/v1/admin/keys/${encodeURIComponent(keyId)}`,
|
|
727
|
-
{ method: 'DELETE' }
|
|
728
|
-
);
|
|
729
|
-
return res as { ok: true };
|
|
730
|
-
},
|
|
731
|
-
listAuditLogs: async (opts) => {
|
|
732
|
-
const params = new URLSearchParams();
|
|
733
|
-
if (opts?.action) params.set('action', opts.action);
|
|
734
|
-
if (opts?.userId) params.set('userId', opts.userId);
|
|
735
|
-
if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
|
|
736
|
-
if (opts?.offset !== undefined)
|
|
737
|
-
params.set('offset', String(opts.offset));
|
|
738
|
-
const qs = params.toString();
|
|
739
|
-
const res = await adminRequest<{
|
|
740
|
-
ok: true;
|
|
741
|
-
total: number;
|
|
742
|
-
logs: Array<{
|
|
743
|
-
id: string;
|
|
744
|
-
action: string;
|
|
745
|
-
userId: string | null;
|
|
746
|
-
metadata: Record<string, unknown> | null;
|
|
747
|
-
ipAddress: string | null;
|
|
748
|
-
userAgent: string | null;
|
|
749
|
-
createdAt: string;
|
|
750
|
-
}>;
|
|
751
|
-
}>(`/v1/admin/audit${qs ? `?${qs}` : ''}`);
|
|
752
|
-
return res;
|
|
753
|
-
},
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
this._request = requestInner as unknown as <T>(
|
|
757
|
-
path: string,
|
|
758
|
-
init?: RequestInit
|
|
759
|
-
) => Promise<T>;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Re-exports so consumers can import from '@slyxup/core'
|
|
764
|
-
export * from './types.js';
|
|
765
|
-
export * from './errors.js';
|
|
766
|
-
// test publish after fix
|