@slyxup/core 0.2.4 → 1.0.1

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 CHANGED
@@ -3,9 +3,11 @@ export interface SlyxupUser {
3
3
  projectId: string | null;
4
4
  email: string;
5
5
  emailVerified: boolean;
6
+ username: string | null;
6
7
  firstName: string | null;
7
8
  lastName: string | null;
8
9
  avatarUrl: string | null;
10
+ twoFactorEnabled: boolean;
9
11
  preferences: Record<string, unknown> | null;
10
12
  createdAt: string;
11
13
  updatedAt: string;
@@ -16,18 +18,21 @@ export interface SlyxupSession {
16
18
  expiresAt: string;
17
19
  }
18
20
  export interface SignInInput {
19
- email: string;
21
+ email?: string;
22
+ username?: string;
20
23
  password: string;
21
24
  projectId?: string;
22
25
  }
23
- export interface SignUpInput extends SignInInput {
26
+ export interface SignUpInput extends Omit<SignInInput, 'username'> {
24
27
  firstName?: string;
25
28
  lastName?: string;
29
+ username?: string;
26
30
  }
27
31
  export interface UpdateUserInput {
28
32
  firstName?: string;
29
33
  lastName?: string;
30
34
  avatarUrl?: string;
35
+ username?: string | null;
31
36
  preferences?: Record<string, unknown>;
32
37
  }
33
38
  export interface SessionResponse {
@@ -51,6 +56,9 @@ export interface SlyxupSessionInfo {
51
56
  export interface SessionsResponse {
52
57
  ok: true;
53
58
  sessions: SlyxupSessionInfo[];
59
+ total: number;
60
+ limit: number;
61
+ offset: number;
54
62
  }
55
63
  export interface RevokeSessionsResponse {
56
64
  ok: true;
@@ -66,6 +74,44 @@ export interface AuthResponse {
66
74
  sessionToken?: string;
67
75
  expiresAt?: string;
68
76
  }
77
+ /** Returned by signIn when the account has 2FA enabled. */
78
+ export interface TwoFactorRequiredResponse {
79
+ ok: false;
80
+ code: '2FA_REQUIRED';
81
+ challengeToken: string;
82
+ }
83
+ /** Union returned by client.auth.signIn. */
84
+ export type SignInResponse = AuthResponse | TwoFactorRequiredResponse;
85
+ export interface TOTPSetupResponse {
86
+ ok: true;
87
+ secret: string;
88
+ provisioningUri: string;
89
+ accountName: string;
90
+ }
91
+ export interface EnableTOTPResponse {
92
+ ok: true;
93
+ recoveryCodes: string[];
94
+ }
95
+ export interface TwoFactorStatusResponse {
96
+ ok: boolean;
97
+ enabled: boolean;
98
+ }
99
+ export interface ConnectedAccount {
100
+ id: string;
101
+ provider: 'google' | 'github';
102
+ providerAccountId: string;
103
+ createdAt: string;
104
+ }
105
+ export interface ConnectedAccountsResponse {
106
+ ok: true;
107
+ accounts: ConnectedAccount[];
108
+ }
109
+ /** Input for completing a 2FA sign-in challenge. */
110
+ export interface CompleteSignInInput {
111
+ challengeToken: string;
112
+ code?: string;
113
+ recoveryCode?: string;
114
+ }
69
115
  export interface ErrorResponse {
70
116
  ok: false;
71
117
  error: string;
@@ -74,6 +120,8 @@ export type Result<T> = T | ErrorResponse;
74
120
  export interface SlyxupClientOptions {
75
121
  /** Publishable key — pk_test_xxx / pk_live_xxx */
76
122
  publishableKey?: string;
123
+ /** Secret key — sk_test_xxx / sk_live_xxx (server-side admin API) */
124
+ secretKey?: string;
77
125
  /** API base URL, default https://auth.slyxup.online */
78
126
  apiUrl?: string;
79
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;IACvC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,WAAW,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,15 +1,10 @@
1
1
  {
2
2
  "name": "@slyxup/core",
3
- "version": "0.2.4",
3
+ "version": "1.0.1",
4
4
  "description": "@slyxup/core — SlyxUp SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
- "scripts": {
9
- "build": "tsc -p tsconfig.json",
10
- "typecheck": "tsc --noEmit",
11
- "test": "vitest run --passWithNoTests"
12
- },
13
8
  "repository": {
14
9
  "type": "git",
15
10
  "url": "git+https://github.com/slyxup/stack.git",
@@ -26,5 +21,10 @@
26
21
  },
27
22
  "devDependencies": {
28
23
  "vitest": "^4.1.11"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run --passWithNoTests"
29
29
  }
30
- }
30
+ }
package/src/client.ts CHANGED
@@ -7,14 +7,21 @@ import {
7
7
  import type {
8
8
  AuthResponse,
9
9
  ChangePasswordInput,
10
+ CompleteSignInInput,
11
+ ConnectedAccountsResponse,
12
+ EnableTOTPResponse,
10
13
  ErrorResponse,
11
14
  Result,
12
15
  RevokeSessionsResponse,
13
16
  SessionResponse,
14
17
  SessionsResponse,
15
18
  SignInInput,
19
+ SignInResponse,
16
20
  SignUpInput,
17
21
  SlyxupClientOptions,
22
+ TOTPSetupResponse,
23
+ TwoFactorRequiredResponse,
24
+ TwoFactorStatusResponse,
18
25
  UpdateUserInput,
19
26
  UserResponse,
20
27
  } from './types.js';
@@ -49,20 +56,32 @@ function createCookieJar() {
49
56
  */
50
57
  export class SlyxupClient {
51
58
  readonly publishableKey?: string;
59
+ readonly secretKey?: string;
52
60
  readonly apiUrl: string;
53
61
  private _getToken?: () => string | undefined;
54
62
  private _request?: <T>(path: string, init?: RequestInit) => Promise<T>;
55
63
 
56
64
  readonly auth: {
57
65
  signUp: (input: SignUpInput) => Promise<AuthResponse>;
58
- signIn: (input: SignInInput) => Promise<AuthResponse>;
66
+ signIn: (input: SignInInput) => Promise<SignInResponse>;
59
67
  signOut: () => Promise<{ ok: true }>;
60
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>;
61
77
  };
62
78
 
63
79
  readonly sessions: {
64
80
  get: () => Promise<SessionResponse>;
65
- list: () => Promise<SessionsResponse>;
81
+ list: (opts?: {
82
+ limit?: number;
83
+ offset?: number;
84
+ }) => Promise<SessionsResponse>;
66
85
  revoke: (sessionId: string) => Promise<{ ok: true }>;
67
86
  revokeOthers: () => Promise<RevokeSessionsResponse>;
68
87
  };
@@ -71,12 +90,100 @@ export class SlyxupClient {
71
90
  change: (input: ChangePasswordInput) => Promise<{ ok: true }>;
72
91
  };
73
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
+
74
109
  readonly users: {
75
110
  me: () => Promise<UserResponse>;
76
111
  update: (input: UpdateUserInput) => Promise<UserResponse>;
77
112
  delete: () => Promise<{ ok: true }>;
78
113
  };
79
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
+ /** List users in the project */
122
+ listUsers: (opts?: { limit?: number; offset?: number }) => Promise<{
123
+ ok: true;
124
+ users: Array<{
125
+ id: string;
126
+ email: string;
127
+ firstName: string | null;
128
+ lastName: string | null;
129
+ emailVerified: boolean;
130
+ blocked: boolean;
131
+ createdAt: string;
132
+ }>;
133
+ total: number;
134
+ }>;
135
+ /** Get a single user by ID */
136
+ getUser: (
137
+ userId: string
138
+ ) => Promise<{
139
+ ok: true;
140
+ user: {
141
+ id: string;
142
+ email: string;
143
+ firstName: string | null;
144
+ lastName: string | null;
145
+ emailVerified: boolean;
146
+ blocked: boolean;
147
+ createdAt: string;
148
+ updatedAt: string;
149
+ };
150
+ }>;
151
+ /** List active sessions */
152
+ listSessions: () => Promise<{
153
+ ok: true;
154
+ sessions: Array<{
155
+ id: string;
156
+ userId: string;
157
+ ipAddress: string | null;
158
+ userAgent: string | null;
159
+ expiresAt: string;
160
+ isExpired: boolean;
161
+ createdAt: string;
162
+ }>;
163
+ }>;
164
+ /** List API keys */
165
+ listKeys: () => Promise<{
166
+ ok: true;
167
+ keys: Array<{
168
+ id: string;
169
+ name: string;
170
+ prefix: string;
171
+ environment: string;
172
+ type: string;
173
+ lastUsedAt: string | null;
174
+ createdAt: string;
175
+ }>;
176
+ }>;
177
+ /** Create an API key (full key returned only once) */
178
+ createKey: (input: {
179
+ name: string;
180
+ type: 'publishable' | 'secret';
181
+ environment: 'test' | 'live';
182
+ }) => Promise<{ ok: true; id: string; key: string; prefix: string }>;
183
+ /** Revoke an API key */
184
+ revokeKey: (keyId: string) => Promise<{ ok: true }>;
185
+ };
186
+
80
187
  /** Get current session token (for custom project APIs that need Bearer) */
81
188
  getToken(): string | undefined {
82
189
  return this._getToken?.();
@@ -89,15 +196,33 @@ export class SlyxupClient {
89
196
 
90
197
  constructor(options: SlyxupClientOptions = {}) {
91
198
  const jar = createCookieJar();
199
+ // Persist token in localStorage so refresh keeps session for cross-origin (auth -> billing) and for dashboard project APIs
200
+ const STORAGE_KEY = 'slyxup_session_token';
92
201
  let storedToken: string | undefined;
202
+ try {
203
+ if (typeof window !== 'undefined' && window.localStorage) {
204
+ storedToken = window.localStorage.getItem(STORAGE_KEY) ?? undefined;
205
+ }
206
+ } catch {}
207
+ const persistToken = (t: string | undefined) => {
208
+ storedToken = t;
209
+ try {
210
+ if (typeof window !== 'undefined' && window.localStorage) {
211
+ if (t) window.localStorage.setItem(STORAGE_KEY, t);
212
+ else window.localStorage.removeItem(STORAGE_KEY);
213
+ }
214
+ } catch {}
215
+ };
93
216
  this.publishableKey = options.publishableKey;
217
+ this.secretKey = options.secretKey;
94
218
  this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
95
219
  this._getToken = () => storedToken;
96
220
  // _request will be assigned after `request` is defined below
97
221
 
98
222
  const requestInner = async <T>(
99
223
  path: string,
100
- init: RequestInit & { body?: string } = {}
224
+ init: RequestInit & { body?: string } = {},
225
+ opts?: { captureError?: boolean }
101
226
  ): Promise<Result<T>> => {
102
227
  let res: Response;
103
228
  try {
@@ -134,6 +259,7 @@ export class SlyxupClient {
134
259
  .catch(() => ({ ok: false, error: 'Invalid response' }))) as object;
135
260
 
136
261
  if (!res.ok) {
262
+ if (opts?.captureError) return data as Result<T>;
137
263
  if (res.status === 401)
138
264
  throw new UnauthorizedError(
139
265
  'error' in data ? String(data.error) : undefined
@@ -151,30 +277,53 @@ export class SlyxupClient {
151
277
  return data as Result<T>;
152
278
  };
153
279
 
154
- const post = <T>(path: string, body?: unknown) =>
155
- requestInner<T>(path, {
156
- method: 'POST',
157
- body: body === undefined ? undefined : JSON.stringify(body),
158
- });
280
+ const post = <T>(
281
+ path: string,
282
+ body?: unknown,
283
+ opts?: { captureError?: boolean }
284
+ ) =>
285
+ requestInner<T>(
286
+ path,
287
+ {
288
+ method: 'POST',
289
+ body: body === undefined ? undefined : JSON.stringify(body),
290
+ },
291
+ opts
292
+ );
159
293
 
160
294
  this.auth = {
161
295
  signUp: async (input) => {
162
296
  const res = await post<AuthResponse>('/v1/auth/sign-up', input);
163
297
  if (!('user' in res))
164
298
  throw new SlyxupError(res.error, 400, 'api_error');
165
- if (res.sessionToken) storedToken = res.sessionToken;
299
+ if (res.sessionToken) persistToken(res.sessionToken);
166
300
  return res;
167
301
  },
168
302
  signIn: async (input) => {
169
- const res = await post<AuthResponse>('/v1/auth/sign-in', input);
303
+ // The server may answer 403 with code 2FA_REQUIRED — we must surface
304
+ // the challenge back to the caller instead of throwing a generic error.
305
+ const res = await post<SignInResponse>('/v1/auth/sign-in', input, {
306
+ captureError: true,
307
+ });
308
+ if ('challengeToken' in res) {
309
+ return res as TwoFactorRequiredResponse;
310
+ }
170
311
  if (!('user' in res))
171
- throw new SlyxupError(res.error, 401, 'api_error');
172
- if (res.sessionToken) storedToken = res.sessionToken;
312
+ throw new UnauthorizedError(
313
+ (res as { error?: string }).error ?? 'Sign in failed'
314
+ );
315
+ if (res.sessionToken) persistToken(res.sessionToken);
316
+ return res;
317
+ },
318
+ completeSignIn: async (input) => {
319
+ const res = await post<AuthResponse>('/v1/auth/sign-in/2fa', input);
320
+ if (!('user' in res)) throw new UnauthorizedError(res.error);
321
+ if (res.sessionToken) persistToken(res.sessionToken);
173
322
  return res;
174
323
  },
175
324
  signOut: async () => {
176
325
  const res = await post<{ ok: true }>('/v1/auth/sign-out');
177
- storedToken = undefined;
326
+ persistToken(undefined);
178
327
  jar.clear();
179
328
  return res as { ok: true };
180
329
  },
@@ -185,6 +334,35 @@ export class SlyxupClient {
185
334
  if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
186
335
  return res as { ok: true };
187
336
  },
337
+ forgotPassword: async (email: string, projectId?: string) => {
338
+ const res = await post<{ ok: true }>(
339
+ '/v1/verification/password/forgot',
340
+ {
341
+ email,
342
+ projectId,
343
+ }
344
+ );
345
+ if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
346
+ return res as { ok: true };
347
+ },
348
+ resetPassword: async (token: string, password: string) => {
349
+ const res = await post<{ ok: true }>(
350
+ '/v1/verification/password/reset',
351
+ {
352
+ token,
353
+ password,
354
+ }
355
+ );
356
+ if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
357
+ return res as { ok: true };
358
+ },
359
+ verifyEmail: async (token: string) => {
360
+ const res = await post<{ ok: true }>('/v1/verification/verify', {
361
+ token,
362
+ });
363
+ if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
364
+ return res as { ok: true };
365
+ },
188
366
  };
189
367
 
190
368
  this.sessions = {
@@ -193,16 +371,26 @@ export class SlyxupClient {
193
371
  if (!('session' in res)) throw new UnauthorizedError(res.error);
194
372
  return res;
195
373
  },
196
- list: async () => {
197
- const res = await requestInner<SessionsResponse>('/v1/sessions');
374
+ list: async (opts?: { limit?: number; offset?: number }) => {
375
+ const params = new URLSearchParams();
376
+ if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
377
+ if (opts?.offset !== undefined)
378
+ params.set('offset', String(opts.offset));
379
+ const qs = params.toString();
380
+ const res = await requestInner<SessionsResponse>(
381
+ `/v1/sessions${qs ? `?${qs}` : ''}`
382
+ );
198
383
  if (!('sessions' in res))
199
384
  throw new SlyxupError(res.error, 400, 'api_error');
200
385
  return res;
201
386
  },
202
387
  revoke: async (sessionId: string) => {
203
- const res = await requestInner<{ ok: true }>(`/v1/sessions/${sessionId}`, {
204
- method: 'DELETE',
205
- });
388
+ const res = await requestInner<{ ok: true }>(
389
+ `/v1/sessions/${sessionId}`,
390
+ {
391
+ method: 'DELETE',
392
+ }
393
+ );
206
394
  return res as { ok: true };
207
395
  },
208
396
  revokeOthers: async () => {
@@ -223,6 +411,65 @@ export class SlyxupClient {
223
411
  },
224
412
  };
225
413
 
414
+ this.twoFactor = {
415
+ setup: async () => {
416
+ const res = await requestInner<TOTPSetupResponse>('/v1/user/2fa/setup');
417
+ if (!('secret' in res))
418
+ throw new SlyxupError(res.error, 400, 'api_error');
419
+ return res;
420
+ },
421
+ status: async () => {
422
+ const res = await requestInner<TwoFactorStatusResponse>(
423
+ '/v1/user/2fa/status'
424
+ );
425
+ return res as TwoFactorStatusResponse;
426
+ },
427
+ enable: async (secret, code) => {
428
+ const res = await post<EnableTOTPResponse>(
429
+ '/v1/user/2fa/enable',
430
+ { secret, code },
431
+ { captureError: true }
432
+ );
433
+ if (!('recoveryCodes' in res))
434
+ throw new SlyxupError(res.error, 400, 'api_error');
435
+ return res;
436
+ },
437
+ verify: async (code) => {
438
+ const res = await post<{ ok: boolean; valid: boolean }>(
439
+ '/v1/user/2fa/verify',
440
+ { code },
441
+ { captureError: true }
442
+ );
443
+ if (!('valid' in res))
444
+ throw new SlyxupError(res.error, 400, 'api_error');
445
+ return res as { ok: boolean; valid: boolean };
446
+ },
447
+ disable: async (code) => {
448
+ const res = await post<{ ok: true }>('/v1/user/2fa/disable', { code });
449
+ if (!res.ok) throw new SlyxupError(res.error, 400, 'api_error');
450
+ return res as { ok: true };
451
+ },
452
+ };
453
+
454
+ this.accounts = {
455
+ list: async () => {
456
+ const res =
457
+ await requestInner<ConnectedAccountsResponse>('/v1/user/accounts');
458
+ if (!('accounts' in res))
459
+ throw new SlyxupError(res.error, 400, 'api_error');
460
+ return res;
461
+ },
462
+ unlink: async (accountId, provider) => {
463
+ const res = await requestInner<{ ok: true }>(
464
+ `/v1/user/accounts/${accountId}?provider=${encodeURIComponent(provider)}`,
465
+ {
466
+ method: 'DELETE',
467
+ }
468
+ );
469
+ return res as { ok: true };
470
+ },
471
+ };
472
+
226
473
  this.users = {
227
474
  me: async () => {
228
475
  const res = await requestInner<UserResponse>('/v1/user');
@@ -246,7 +493,138 @@ export class SlyxupClient {
246
493
  },
247
494
  };
248
495
 
249
- this._request = requestInner as unknown as <T>(path: string, init?: RequestInit) => Promise<T>;
496
+ // ── Admin API (requires secretKey) ──
497
+ const sk = options.secretKey;
498
+ const adminRequest = async <T>(
499
+ path: string,
500
+ init: RequestInit = {}
501
+ ): Promise<T> => {
502
+ if (!sk)
503
+ throw new SlyxupError(
504
+ 'Secret key required for admin API',
505
+ 400,
506
+ 'api_error'
507
+ );
508
+ const res = await fetch(`${this.apiUrl}${path}`, {
509
+ ...init,
510
+ headers: {
511
+ 'Content-Type': 'application/json',
512
+ Authorization: `Bearer ${sk}`,
513
+ ...init.headers,
514
+ },
515
+ });
516
+ const data = (await res
517
+ .json()
518
+ .catch(() => ({ ok: false, error: 'Invalid response' }))) as object;
519
+ if (!res.ok) {
520
+ throw new SlyxupError(
521
+ 'error' in data
522
+ ? String((data as { error: unknown }).error)
523
+ : `Request failed (${res.status})`,
524
+ res.status,
525
+ 'api_error'
526
+ );
527
+ }
528
+ return data as T;
529
+ };
530
+
531
+ this.admin = {
532
+ getProject: async () => {
533
+ const res = await adminRequest<{
534
+ ok: true;
535
+ project: { id: string; name: string; createdAt: string };
536
+ }>('/v1/admin/project');
537
+ return res;
538
+ },
539
+ listUsers: async (opts) => {
540
+ const params = new URLSearchParams();
541
+ if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
542
+ if (opts?.offset !== undefined)
543
+ params.set('offset', String(opts.offset));
544
+ const qs = params.toString();
545
+ const res = await adminRequest<{
546
+ ok: true;
547
+ users: Array<{
548
+ id: string;
549
+ email: string;
550
+ firstName: string | null;
551
+ lastName: string | null;
552
+ emailVerified: boolean;
553
+ blocked: boolean;
554
+ createdAt: string;
555
+ }>;
556
+ total: number;
557
+ }>(`/v1/admin/users${qs ? `?${qs}` : ''}`);
558
+ return res;
559
+ },
560
+ getUser: async (userId) => {
561
+ const res = await adminRequest<{
562
+ ok: true;
563
+ user: {
564
+ id: string;
565
+ email: string;
566
+ firstName: string | null;
567
+ lastName: string | null;
568
+ emailVerified: boolean;
569
+ blocked: boolean;
570
+ createdAt: string;
571
+ updatedAt: string;
572
+ };
573
+ }>(`/v1/admin/users/${encodeURIComponent(userId)}`);
574
+ return res;
575
+ },
576
+ listSessions: async () => {
577
+ const res = await adminRequest<{
578
+ ok: true;
579
+ sessions: Array<{
580
+ id: string;
581
+ userId: string;
582
+ ipAddress: string | null;
583
+ userAgent: string | null;
584
+ expiresAt: string;
585
+ isExpired: boolean;
586
+ createdAt: string;
587
+ }>;
588
+ }>('/v1/admin/sessions');
589
+ return res;
590
+ },
591
+ listKeys: async () => {
592
+ const res = await adminRequest<{
593
+ ok: true;
594
+ keys: Array<{
595
+ id: string;
596
+ name: string;
597
+ prefix: string;
598
+ environment: string;
599
+ type: string;
600
+ lastUsedAt: string | null;
601
+ createdAt: string;
602
+ }>;
603
+ }>('/v1/admin/keys');
604
+ return res;
605
+ },
606
+ createKey: async (input) => {
607
+ const res = await adminRequest<{
608
+ ok: true;
609
+ id: string;
610
+ key: string;
611
+ prefix: string;
612
+ }>('/v1/admin/keys', { method: 'POST', body: JSON.stringify(input) });
613
+ return res;
614
+ },
615
+ revokeKey: async (keyId) => {
616
+ const res = await adminRequest<{ ok: true }>(
617
+ `/v1/admin/keys/${encodeURIComponent(keyId)}`,
618
+ { method: 'DELETE' }
619
+ );
620
+ return res as { ok: true };
621
+ },
622
+ };
623
+
624
+ this._request = requestInner as unknown as <T>(
625
+ path: string,
626
+ init?: RequestInit
627
+ ) => Promise<T>;
250
628
  }
251
629
  }
252
630