@sentroy-co/client-sdk 2.13.8 → 2.14.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.
Files changed (59) hide show
  1. package/README.md +24 -5
  2. package/dist/auth/admin/index.d.ts +111 -10
  3. package/dist/auth/admin/index.d.ts.map +1 -1
  4. package/dist/auth/admin/index.js +125 -20
  5. package/dist/auth/admin/index.js.map +1 -1
  6. package/dist/auth/client.d.ts +127 -1
  7. package/dist/auth/client.d.ts.map +1 -1
  8. package/dist/auth/client.js +361 -3
  9. package/dist/auth/client.js.map +1 -1
  10. package/dist/auth/index.d.ts +1 -1
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js.map +1 -1
  13. package/dist/auth/react/index.d.ts +63 -4
  14. package/dist/auth/react/index.d.ts.map +1 -1
  15. package/dist/auth/react/index.js +180 -1
  16. package/dist/auth/react/index.js.map +1 -1
  17. package/dist/auth/types.d.ts +55 -0
  18. package/dist/auth/types.d.ts.map +1 -1
  19. package/dist/cli/ai.d.ts +35 -0
  20. package/dist/cli/ai.d.ts.map +1 -0
  21. package/dist/cli/ai.js +399 -0
  22. package/dist/cli/ai.js.map +1 -0
  23. package/dist/cli/args.d.ts +62 -0
  24. package/dist/cli/args.d.ts.map +1 -0
  25. package/dist/cli/args.js +199 -0
  26. package/dist/cli/args.js.map +1 -0
  27. package/dist/cli/env.d.ts.map +1 -1
  28. package/dist/cli/env.js +8 -2
  29. package/dist/cli/env.js.map +1 -1
  30. package/dist/cli/format.d.ts +37 -0
  31. package/dist/cli/format.d.ts.map +1 -0
  32. package/dist/cli/format.js +129 -0
  33. package/dist/cli/format.js.map +1 -0
  34. package/dist/cli/index.d.ts +8 -2
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +128 -25
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/cli/mail.d.ts +25 -0
  39. package/dist/cli/mail.d.ts.map +1 -0
  40. package/dist/cli/mail.js +253 -0
  41. package/dist/cli/mail.js.map +1 -0
  42. package/dist/cli/storage.d.ts +28 -0
  43. package/dist/cli/storage.d.ts.map +1 -0
  44. package/dist/cli/storage.js +189 -0
  45. package/dist/cli/storage.js.map +1 -0
  46. package/package.json +8 -2
  47. package/skill/SKILL.md +542 -0
  48. package/src/auth/admin/index.ts +227 -31
  49. package/src/auth/client.ts +438 -4
  50. package/src/auth/index.ts +9 -0
  51. package/src/auth/react/index.tsx +255 -4
  52. package/src/auth/types.ts +66 -0
  53. package/src/cli/ai.ts +440 -0
  54. package/src/cli/args.ts +225 -0
  55. package/src/cli/env.ts +10 -2
  56. package/src/cli/format.ts +147 -0
  57. package/src/cli/index.ts +147 -25
  58. package/src/cli/mail.ts +363 -0
  59. package/src/cli/storage.ts +307 -0
@@ -3,6 +3,14 @@ import {
3
3
  type SignupResponse,
4
4
  type LoginResponse,
5
5
  type AuthTokensResponse,
6
+ type LoginOutcome,
7
+ type SessionSummary,
8
+ type ActivityEntry,
9
+ type MfaStatus,
10
+ type MfaEnrollResponse,
11
+ type MfaVerifyEnrollmentResponse,
12
+ type PasskeySummary,
13
+ type SocialProvider,
6
14
  } from "./types"
7
15
  import { AuthHttp, type AuthHttpOptions } from "./http"
8
16
 
@@ -61,6 +69,37 @@ function decodeBase64Url(s: string): string {
61
69
  throw new Error("No base64 decoder available")
62
70
  }
63
71
 
72
+ /**
73
+ * Optional `@simplewebauthn/browser` import — RP webauthn flow kullanmak
74
+ * isterse kendi devDependencies'ine ekler; lazy import ile bundle'a
75
+ * sızmaz.
76
+ */
77
+ async function loadSimpleWebAuthnBrowser(): Promise<{
78
+ startRegistration: (opts: {
79
+ optionsJSON: Record<string, unknown>
80
+ }) => Promise<unknown>
81
+ startAuthentication: (opts: {
82
+ optionsJSON: Record<string, unknown>
83
+ }) => Promise<unknown>
84
+ }> {
85
+ try {
86
+ // String-concat hides the specifier from TS resolver — `@simplewebauthn/browser`
87
+ // is an optional peer; SDK ships without it bundled. RP'nin npm install'unda
88
+ // varsa runtime'da çözülür, yoksa catch'e düşüp kullanıcıya net hata verir.
89
+ const specifier = "@simplewebauthn/" + "browser"
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ const mod: any = await import(/* @vite-ignore */ specifier)
92
+ return {
93
+ startRegistration: mod.startRegistration,
94
+ startAuthentication: mod.startAuthentication,
95
+ }
96
+ } catch {
97
+ throw new Error(
98
+ "Passkey support requires `@simplewebauthn/browser` — install it as a peer dependency.",
99
+ )
100
+ }
101
+ }
102
+
64
103
  function localStorageAdapter(projectSlug: string): AuthStorageAdapter {
65
104
  if (typeof window === "undefined" || !window.localStorage) {
66
105
  return memoryStorageAdapter()
@@ -186,14 +225,45 @@ export class SentroyAuth {
186
225
  return res
187
226
  }
188
227
 
228
+ /**
229
+ * Sign in with email/password. MFA enrolled user'lar için response
230
+ * discriminated union: `kind: "mfa"` → caller `verifyMfa()` çağırır.
231
+ * `kind: "tokens"` → session kuruldu.
232
+ */
189
233
  async signIn(input: {
190
234
  email: string
191
235
  password: string
192
- }): Promise<LoginResponse> {
193
- const res = await this.http.request<LoginResponse>("/login", {
194
- method: "POST",
195
- json: input,
236
+ rememberMe?: boolean
237
+ }): Promise<LoginOutcome> {
238
+ const res = await this.http.request<
239
+ LoginResponse | { mfaRequired: true; mfaToken: string; factorType: "totp" }
240
+ >("/login", { method: "POST", json: input })
241
+ if ("mfaRequired" in res && res.mfaRequired) {
242
+ return { kind: "mfa", data: res }
243
+ }
244
+ const tokens = res as LoginResponse
245
+ this.persist({
246
+ accessToken: tokens.accessToken,
247
+ refreshToken: tokens.refreshToken,
248
+ user: tokens.user,
196
249
  })
250
+ return { kind: "tokens", data: tokens }
251
+ }
252
+
253
+ /**
254
+ * MFA verify — `signIn` ile `kind: "mfa"` döndüyse, kullanıcıdan code
255
+ * (veya recovery code) alıp bu method'u çağır. Başarılıysa session
256
+ * kurulur ve login tamamlanır.
257
+ */
258
+ async verifyMfa(input: {
259
+ mfaToken: string
260
+ code?: string
261
+ recoveryCode?: string
262
+ }): Promise<LoginResponse> {
263
+ const res = await this.http.request<LoginResponse>(
264
+ "/login/mfa/verify",
265
+ { method: "POST", json: input },
266
+ )
197
267
  this.persist({
198
268
  accessToken: res.accessToken,
199
269
  refreshToken: res.refreshToken,
@@ -224,6 +294,21 @@ export class SentroyAuth {
224
294
  })
225
295
  }
226
296
 
297
+ /**
298
+ * Reset password using token from email. `newPassword` policy +
299
+ * HaveIBeenPwned breach check yapılır.
300
+ */
301
+ async confirmPasswordReset(input: {
302
+ token: string
303
+ newPassword: string
304
+ }): Promise<SentroyAuthUser> {
305
+ const res = await this.http.request<{ user: SentroyAuthUser }>(
306
+ "/password-reset/confirm",
307
+ { method: "POST", json: input },
308
+ )
309
+ return res.user
310
+ }
311
+
227
312
  async verifyEmail(token: string): Promise<SentroyAuthUser> {
228
313
  const res = await this.http.request<{ user: SentroyAuthUser }>(
229
314
  "/verify-email",
@@ -241,6 +326,355 @@ export class SentroyAuth {
241
326
  return res.user
242
327
  }
243
328
 
329
+ // ─── Magic link ──────────────────────────────────────────────────────────
330
+
331
+ /**
332
+ * Email magic-link request. Project'in `magicLinkEnabled` true
333
+ * olması gerekir. Uniform 200 response — email yoksa da hata vermez.
334
+ */
335
+ async sendMagicLink(input: { email: string; redirectUri?: string }): Promise<void> {
336
+ await this.http.request("/magic-link/request", {
337
+ method: "POST",
338
+ json: input,
339
+ })
340
+ }
341
+
342
+ /**
343
+ * Magic link mail'inden gelen token ile login. Session kurulur.
344
+ */
345
+ async consumeMagicLink(token: string): Promise<LoginResponse> {
346
+ const res = await this.http.request<LoginResponse & { redirectUri?: string | null }>(
347
+ "/magic-link/consume",
348
+ { method: "POST", json: { token } },
349
+ )
350
+ this.persist({
351
+ accessToken: res.accessToken,
352
+ refreshToken: res.refreshToken,
353
+ user: res.user,
354
+ })
355
+ return res
356
+ }
357
+
358
+ // ─── Invitation ──────────────────────────────────────────────────────────
359
+
360
+ /**
361
+ * Accept admin invitation. Token mail'den gelir, kullanıcı password
362
+ * + optional displayName girer; hesap create + session kurulur.
363
+ */
364
+ async acceptInvitation(input: {
365
+ token: string
366
+ password: string
367
+ displayName?: string
368
+ }): Promise<LoginResponse> {
369
+ const res = await this.http.request<LoginResponse>(
370
+ "/invitation/accept",
371
+ { method: "POST", json: input },
372
+ )
373
+ this.persist({
374
+ accessToken: res.accessToken,
375
+ refreshToken: res.refreshToken,
376
+ user: res.user,
377
+ })
378
+ return res
379
+ }
380
+
381
+ // ─── Social federation ───────────────────────────────────────────────────
382
+
383
+ /**
384
+ * Provider authorize URL üret. `window.location.assign(url)` ile
385
+ * RP'nin sayfasından redirect — callback'te Sentroy session kurulur,
386
+ * redirectUri fragment'ında token'lar döner.
387
+ */
388
+ socialAuthorizeUrl(
389
+ provider: SocialProvider,
390
+ opts: { redirectUri?: string; rememberMe?: boolean } = {},
391
+ ): string {
392
+ const params = new URLSearchParams()
393
+ if (opts.redirectUri) params.set("redirectUri", opts.redirectUri)
394
+ if (opts.rememberMe) params.set("rememberMe", "1")
395
+ const qs = params.toString()
396
+ return `${this.http.baseUrl}/api/v1/auth/${this.http.projectSlug}/social/${provider}/authorize${qs ? `?${qs}` : ""}`
397
+ }
398
+
399
+ /**
400
+ * `window.location.hash`tan social login redirect sonrası gelen
401
+ * `#access_token=...&refresh_token=...&token_type=Bearer` parse +
402
+ * session kur. RP sayfasına redirectUri varsayılan akış kullanıldıysa
403
+ * çağırın. Başarılıysa user döner, fail'da null.
404
+ */
405
+ async consumeRedirectFragment(): Promise<SentroyAuthUser | null> {
406
+ if (typeof window === "undefined") return null
407
+ const hash = window.location.hash.replace(/^#/, "")
408
+ if (!hash) return null
409
+ const params = new URLSearchParams(hash)
410
+ const accessToken = params.get("access_token")
411
+ const refreshToken = params.get("refresh_token")
412
+ if (!accessToken || !refreshToken) return null
413
+ // Fragment'ı URL'den temizle (history clean)
414
+ window.history.replaceState(
415
+ null,
416
+ "",
417
+ window.location.pathname + window.location.search,
418
+ )
419
+ const user = await this.fetchMe(accessToken)
420
+ if (!user) return null
421
+ this.persist({ accessToken, refreshToken, user })
422
+ return user
423
+ }
424
+
425
+ // ─── /me (current user info) ─────────────────────────────────────────────
426
+
427
+ async getCurrentUser(): Promise<SentroyAuthUser | null> {
428
+ const restored = this.storage.read()
429
+ if (!restored) return null
430
+ const user = await this.fetchMe(restored.accessToken)
431
+ if (user) {
432
+ this.persist({ ...restored, user })
433
+ }
434
+ return user
435
+ }
436
+
437
+ private async fetchMe(accessToken: string): Promise<SentroyAuthUser | null> {
438
+ try {
439
+ return await this.http.request<SentroyAuthUser>("/me", {
440
+ method: "GET",
441
+ bearer: accessToken,
442
+ })
443
+ } catch {
444
+ return null
445
+ }
446
+ }
447
+
448
+ // ─── /me/sessions ────────────────────────────────────────────────────────
449
+
450
+ async listSessions(): Promise<SessionSummary[]> {
451
+ return this.http.request<SessionSummary[]>("/me/sessions", {
452
+ method: "GET",
453
+ bearer: this.requireToken(),
454
+ })
455
+ }
456
+
457
+ async revokeSession(id: string): Promise<void> {
458
+ await this.http.request(`/me/sessions/${encodeURIComponent(id)}`, {
459
+ method: "DELETE",
460
+ bearer: this.requireToken(),
461
+ })
462
+ }
463
+
464
+ // ─── /me/password ────────────────────────────────────────────────────────
465
+
466
+ /**
467
+ * Change password. Backend tüm session'ları revoke eder; SDK local
468
+ * session'ı temizler — caller `signIn` ile tekrar oturum açar.
469
+ */
470
+ async changePassword(input: {
471
+ currentPassword: string
472
+ newPassword: string
473
+ }): Promise<void> {
474
+ await this.http.request("/me/password", {
475
+ method: "POST",
476
+ json: input,
477
+ bearer: this.requireToken(),
478
+ })
479
+ this.clearSession()
480
+ }
481
+
482
+ // ─── /me/email/change ────────────────────────────────────────────────────
483
+
484
+ /**
485
+ * Request email change — confirmation mail yeni adrese gönderilir.
486
+ * Kullanıcı `confirmEmailChange(token)` ile finalize eder.
487
+ */
488
+ async requestEmailChange(input: {
489
+ newEmail: string
490
+ currentPassword: string
491
+ }): Promise<void> {
492
+ await this.http.request("/me/email/change-request", {
493
+ method: "POST",
494
+ json: input,
495
+ bearer: this.requireToken(),
496
+ })
497
+ }
498
+
499
+ /** Token-based confirm (mail link'inden gelir). */
500
+ async confirmEmailChange(token: string): Promise<SentroyAuthUser> {
501
+ const user = await this.http.request<SentroyAuthUser>(
502
+ "/me/email/change-confirm",
503
+ { method: "POST", json: { token } },
504
+ )
505
+ // Email changed — tüm sessions revoke edildi, local clear
506
+ this.clearSession()
507
+ return user
508
+ }
509
+
510
+ // ─── /me/account (delete) ────────────────────────────────────────────────
511
+
512
+ async requestAccountDeletion(currentPassword: string): Promise<void> {
513
+ await this.http.request("/me/account/delete-request", {
514
+ method: "POST",
515
+ json: { currentPassword },
516
+ bearer: this.requireToken(),
517
+ })
518
+ }
519
+
520
+ async confirmAccountDeletion(token: string): Promise<void> {
521
+ await this.http.request("/me/account/delete-confirm", {
522
+ method: "POST",
523
+ json: { token },
524
+ })
525
+ this.clearSession()
526
+ }
527
+
528
+ // ─── /me/activity ────────────────────────────────────────────────────────
529
+
530
+ async getActivity(): Promise<ActivityEntry[]> {
531
+ return this.http.request<ActivityEntry[]>("/me/activity", {
532
+ method: "GET",
533
+ bearer: this.requireToken(),
534
+ })
535
+ }
536
+
537
+ // ─── /me/mfa ─────────────────────────────────────────────────────────────
538
+
539
+ readonly mfa = {
540
+ getStatus: async (): Promise<MfaStatus> =>
541
+ this.http.request<MfaStatus>("/me/mfa", {
542
+ method: "GET",
543
+ bearer: this.requireToken(),
544
+ }),
545
+ enrollTotp: async (): Promise<MfaEnrollResponse> =>
546
+ this.http.request<MfaEnrollResponse>("/me/mfa/totp/enroll", {
547
+ method: "POST",
548
+ bearer: this.requireToken(),
549
+ }),
550
+ verifyTotpEnrollment: async (
551
+ code: string,
552
+ ): Promise<MfaVerifyEnrollmentResponse> =>
553
+ this.http.request<MfaVerifyEnrollmentResponse>(
554
+ "/me/mfa/totp/verify-enrollment",
555
+ { method: "POST", json: { code }, bearer: this.requireToken() },
556
+ ),
557
+ disableTotp: async (currentPassword: string): Promise<void> => {
558
+ await this.http.request("/me/mfa/totp/disable", {
559
+ method: "POST",
560
+ json: { currentPassword },
561
+ bearer: this.requireToken(),
562
+ })
563
+ },
564
+ }
565
+
566
+ // ─── /me/passkey + /passkey/auth ─────────────────────────────────────────
567
+
568
+ readonly passkey = {
569
+ list: async (): Promise<PasskeySummary[]> =>
570
+ this.http.request<PasskeySummary[]>("/me/passkey", {
571
+ method: "GET",
572
+ bearer: this.requireToken(),
573
+ }),
574
+ delete: async (id: string): Promise<void> => {
575
+ await this.http.request(`/me/passkey/${encodeURIComponent(id)}`, {
576
+ method: "DELETE",
577
+ bearer: this.requireToken(),
578
+ })
579
+ },
580
+ /**
581
+ * Register a new passkey on this device.
582
+ *
583
+ * Browser-only: dynamically imports `@simplewebauthn/browser`. RP
584
+ * SDK kullanıyor ama webauthn/browser bağımlılığı yoksa caller
585
+ * `peerDependencies` aracılığıyla manuel ekler.
586
+ */
587
+ register: async (deviceName?: string): Promise<void> => {
588
+ const begin = await this.http.request<{
589
+ options: unknown
590
+ challengeToken: string
591
+ }>("/me/passkey/register/begin", {
592
+ method: "POST",
593
+ bearer: this.requireToken(),
594
+ })
595
+ const sw = await loadSimpleWebAuthnBrowser()
596
+ const attestation = await sw.startRegistration({
597
+ optionsJSON: begin.options as Parameters<typeof sw.startRegistration>[0]["optionsJSON"],
598
+ })
599
+ await this.http.request("/me/passkey/register/complete", {
600
+ method: "POST",
601
+ json: {
602
+ challengeToken: begin.challengeToken,
603
+ response: attestation,
604
+ deviceName:
605
+ deviceName ??
606
+ (typeof navigator !== "undefined"
607
+ ? navigator.userAgent.slice(0, 80)
608
+ : null),
609
+ },
610
+ bearer: this.requireToken(),
611
+ })
612
+ },
613
+ /**
614
+ * Sign in with passkey. Email opsiyonel — verilirse server o
615
+ * user'ın passkey'lerini allowList yapar, yoksa "usernameless".
616
+ * Session kurulur.
617
+ */
618
+ authenticate: async (
619
+ opts: { email?: string; rememberMe?: boolean } = {},
620
+ ): Promise<LoginResponse> => {
621
+ const begin = await this.http.request<{
622
+ options: unknown
623
+ challengeToken: string
624
+ }>("/passkey/authenticate/begin", {
625
+ method: "POST",
626
+ json: { email: opts.email },
627
+ })
628
+ const sw = await loadSimpleWebAuthnBrowser()
629
+ const assertion = await sw.startAuthentication({
630
+ optionsJSON: begin.options as Parameters<typeof sw.startAuthentication>[0]["optionsJSON"],
631
+ })
632
+ const res = await this.http.request<LoginResponse>(
633
+ "/passkey/authenticate/complete",
634
+ {
635
+ method: "POST",
636
+ json: {
637
+ challengeToken: begin.challengeToken,
638
+ response: assertion,
639
+ rememberMe: opts.rememberMe,
640
+ },
641
+ },
642
+ )
643
+ this.persist({
644
+ accessToken: res.accessToken,
645
+ refreshToken: res.refreshToken,
646
+ user: res.user,
647
+ })
648
+ return res
649
+ },
650
+ }
651
+
652
+ /**
653
+ * Force refresh now — caller'ın sürdüğü access token süresi dolmuş
654
+ * olabilir; bu method yeni token'ları persist eder.
655
+ */
656
+ async refreshNow(): Promise<void> {
657
+ await this.refresh()
658
+ }
659
+
660
+ /** Manual session injection — fragment / cookie redirect dışında tokens
661
+ * başka bir kanaldan elde edildiyse (örn. RP custom auth callback). */
662
+ setSession(input: {
663
+ accessToken: string
664
+ refreshToken: string
665
+ user: SentroyAuthUser
666
+ }): void {
667
+ this.persist(input)
668
+ }
669
+
670
+ private requireToken(): string {
671
+ const restored = this.storage.read()
672
+ if (!restored?.accessToken) {
673
+ throw new Error("Not signed in — accessToken missing.")
674
+ }
675
+ return restored.accessToken
676
+ }
677
+
244
678
  /**
245
679
  * Subscription pattern — Firebase Auth uyumlu. Caller'ın hemen mevcut
246
680
  * state'i alabilmesi için constructor'da restore edilen user
package/src/auth/index.ts CHANGED
@@ -23,4 +23,13 @@ export {
23
23
  type SignupResponse,
24
24
  type LoginResponse,
25
25
  type AuthTokensResponse,
26
+ type LoginOutcome,
27
+ type MfaChallengeResponse,
28
+ type SessionSummary,
29
+ type ActivityEntry,
30
+ type MfaStatus,
31
+ type MfaEnrollResponse,
32
+ type MfaVerifyEnrollmentResponse,
33
+ type PasskeySummary,
34
+ type SocialProvider,
26
35
  } from "./types"