@robosystems/core 0.7.1 → 0.8.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.
@@ -1,4 +1,4 @@
1
- import type { APIKey, AuthResponse, AuthUser, CreateAPIKeyRequest, SSOTokenResponse } from './types';
1
+ import type { APIKey, AuthProviders, AuthResponse, AuthUser, CreateAPIKeyRequest, PasskeyEnrollmentResult, SSOTokenResponse } from './types';
2
2
  export declare class TokenExpiredError extends Error {
3
3
  constructor(message?: string);
4
4
  }
@@ -35,6 +35,18 @@ export declare class RoboSystemsAuthClient {
35
35
  */
36
36
  private syncClientConfigToken;
37
37
  login(email: string, password: string): Promise<AuthResponse>;
38
+ /**
39
+ * Validate an auth-shaped SDK response, store the session token when one
40
+ * is present, and map to the public shape. Shared by every entrance that
41
+ * can mint a session: password login, MFA verify, passwordless login,
42
+ * and forced-enrollment completion.
43
+ *
44
+ * An MFA-step response (`status` of `mfa_required` /
45
+ * `mfa_enrollment_required`) carries no token — `success` is false and
46
+ * `mfaToken` authorizes the next step. Pre-MFA backends send no `status`
47
+ * at all, which reads as authenticated.
48
+ */
49
+ private finalizeAuthResponse;
38
50
  register(email: string, password: string, name?: string, captchaToken?: string, inviteToken?: string, options?: {
39
51
  appSource?: string;
40
52
  }): Promise<AuthResponse>;
@@ -55,6 +67,60 @@ export declare class RoboSystemsAuthClient {
55
67
  }>;
56
68
  ssoComplete(sessionId: string): Promise<AuthResponse>;
57
69
  checkAuthentication(): Promise<AuthUser | null>;
70
+ /**
71
+ * Fetch the deployment's auth posture (which sign-in methods to render).
72
+ *
73
+ * A rendering hint, not a security boundary — the backend enforces every
74
+ * flag regardless of what the page shows. Any failure returns null and
75
+ * the caller renders the default password posture.
76
+ */
77
+ getAuthProviders(): Promise<AuthProviders | null>;
78
+ /** Second-factor step: exchange an mfa_required token for assertion options. */
79
+ getMfaOptions(mfaToken: string): Promise<Record<string, unknown>>;
80
+ /** Second-factor step: complete with an assertion or a recovery code. */
81
+ verifyMfa(mfaToken: string, input: {
82
+ assertion?: Record<string, unknown>;
83
+ recoveryCode?: string;
84
+ }): Promise<AuthResponse>;
85
+ /** Passwordless login: usernameless assertion options. */
86
+ getPasskeyLoginOptions(): Promise<Record<string, unknown>>;
87
+ /** Passwordless login: assertion → session. */
88
+ completePasskeyLogin(assertion: Record<string, unknown>): Promise<AuthResponse>;
89
+ /**
90
+ * Begin enrollment. Authenticated settings flow omits `mfaToken`; the
91
+ * forced-enrollment lane passes the token from an
92
+ * `mfa_enrollment_required` login.
93
+ */
94
+ getPasskeyRegistrationOptions(mfaToken?: string): Promise<Record<string, unknown>>;
95
+ /**
96
+ * Finish enrollment. First passkey returns recovery codes (once); in the
97
+ * forced-enrollment lane the result carries the completed login and the
98
+ * session token is stored.
99
+ */
100
+ completePasskeyEnrollment(credential: Record<string, unknown>, options?: {
101
+ name?: string;
102
+ mfaToken?: string;
103
+ }): Promise<PasskeyEnrollmentResult>;
104
+ /** Enrolled passkeys for the settings surface. */
105
+ listPasskeys(): Promise<Record<string, unknown>[]>;
106
+ /** Fresh-assertion options for destructive lifecycle actions. */
107
+ getPasskeyReauthOptions(): Promise<Record<string, unknown>>;
108
+ /** Remove a passkey; exactly one re-auth proof must be supplied. */
109
+ deletePasskey(passkeyId: string, proof: {
110
+ password?: string;
111
+ assertion?: Record<string, unknown>;
112
+ }): Promise<void>;
113
+ /** MFA posture for the settings surface; null on any failure. */
114
+ getMfaStatus(): Promise<{
115
+ passkeyCount: number;
116
+ recoveryCodesRemaining: number;
117
+ enforcementApplies: boolean;
118
+ } | null>;
119
+ /** Replace the recovery-code set; codes are shown exactly once. */
120
+ regenerateRecoveryCodes(proof: {
121
+ password?: string;
122
+ assertion?: Record<string, unknown>;
123
+ }): Promise<string[]>;
58
124
  clearAuthCache(): void;
59
125
  /**
60
126
  * Send password reset email
@@ -1,5 +1,5 @@
1
1
  var _a;
2
- import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, forgotPassword, generateSsoToken, getCurrentAuthUser, getInvitationPreview, getPasswordPolicy, listUserApiKeys, loginUser, logoutUser, refreshAuthSession, registerUser, resendVerificationEmail, resetPassword, revokeUserApiKey, ssoTokenExchange, validateResetToken, verifyEmail, } from '@robosystems/client';
2
+ import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, deleteUserPasskey, forgotPassword, generateSsoToken, getAuthProviders, getCurrentAuthUser, getInvitationPreview, getMfaOptions, getMfaStatus, getPasskeyLoginOptions, getPasskeyReauthOptions, getPasskeyRegistrationOptions, getPasswordPolicy, listUserApiKeys, listUserPasskeys, loginUser, logoutUser, refreshAuthSession, regenerateMfaRecoveryCodes, registerUser, resendVerificationEmail, resetPassword, revokeUserApiKey, ssoTokenExchange, validateResetToken, verifyEmail, verifyMfa, verifyPasskeyLogin, verifyPasskeyRegistration, } from '@robosystems/client';
3
3
  import * as sdkClientsModule from '@robosystems/client/clients';
4
4
  import { getToken, getValidToken } from './token-storage';
5
5
  // Global config hook for the clients package's lazy default singleton
@@ -188,7 +188,24 @@ export class RoboSystemsAuthClient {
188
188
  client: this.client,
189
189
  body: { email, password },
190
190
  });
191
- const sdkResponse = this.validateSDKAuthResponse(response.data);
191
+ return this.finalizeAuthResponse(response.data);
192
+ }
193
+ /**
194
+ * Validate an auth-shaped SDK response, store the session token when one
195
+ * is present, and map to the public shape. Shared by every entrance that
196
+ * can mint a session: password login, MFA verify, passwordless login,
197
+ * and forced-enrollment completion.
198
+ *
199
+ * An MFA-step response (`status` of `mfa_required` /
200
+ * `mfa_enrollment_required`) carries no token — `success` is false and
201
+ * `mfaToken` authorizes the next step. Pre-MFA backends send no `status`
202
+ * at all, which reads as authenticated.
203
+ */
204
+ async finalizeAuthResponse(data) {
205
+ var _a, _b, _c;
206
+ const sdkResponse = this.validateSDKAuthResponse(data);
207
+ const status = sdkResponse.status;
208
+ const authenticated = !status || status === 'authenticated';
192
209
  // Store JWT token with expiry information if present in response
193
210
  if (sdkResponse.token) {
194
211
  const { storeToken } = await import('./token-storage');
@@ -202,11 +219,13 @@ export class RoboSystemsAuthClient {
202
219
  }
203
220
  return {
204
221
  user: sdkResponse.user,
205
- success: true,
222
+ success: authenticated,
223
+ status,
224
+ mfaToken: (_a = sdkResponse.mfa_token) !== null && _a !== void 0 ? _a : undefined,
206
225
  message: sdkResponse.message,
207
226
  token: sdkResponse.token,
208
- expires_in: sdkResponse.expires_in,
209
- refresh_threshold: sdkResponse.refresh_threshold,
227
+ expires_in: (_b = sdkResponse.expires_in) !== null && _b !== void 0 ? _b : undefined,
228
+ refresh_threshold: (_c = sdkResponse.refresh_threshold) !== null && _c !== void 0 ? _c : undefined,
210
229
  };
211
230
  }
212
231
  async register(email, password, name, captchaToken, inviteToken, options) {
@@ -491,6 +510,148 @@ export class RoboSystemsAuthClient {
491
510
  return null;
492
511
  }
493
512
  }
513
+ /**
514
+ * Fetch the deployment's auth posture (which sign-in methods to render).
515
+ *
516
+ * A rendering hint, not a security boundary — the backend enforces every
517
+ * flag regardless of what the page shows. Any failure returns null and
518
+ * the caller renders the default password posture.
519
+ */
520
+ async getAuthProviders() {
521
+ try {
522
+ const response = await getAuthProviders({ client: this.client });
523
+ const data = response.data;
524
+ if (!data || typeof data.password_auth !== 'boolean') {
525
+ return null;
526
+ }
527
+ return data;
528
+ }
529
+ catch (_a) {
530
+ return null;
531
+ }
532
+ }
533
+ // ---- Passkey MFA -------------------------------------------------------
534
+ // WebAuthn options and credential payloads are opaque JSON between the
535
+ // backend RP library and the browser's navigator.credentials — core
536
+ // carries them verbatim and never imports a WebAuthn library. Methods
537
+ // that can mint a session route through finalizeAuthResponse so token
538
+ // storage stays in one place.
539
+ /** Second-factor step: exchange an mfa_required token for assertion options. */
540
+ async getMfaOptions(mfaToken) {
541
+ const response = await getMfaOptions({
542
+ client: this.client,
543
+ body: { mfa_token: mfaToken },
544
+ });
545
+ return response.data.options;
546
+ }
547
+ /** Second-factor step: complete with an assertion or a recovery code. */
548
+ async verifyMfa(mfaToken, input) {
549
+ const response = await verifyMfa({
550
+ client: this.client,
551
+ body: {
552
+ mfa_token: mfaToken,
553
+ assertion: input.assertion,
554
+ recovery_code: input.recoveryCode,
555
+ },
556
+ });
557
+ return this.finalizeAuthResponse(response.data);
558
+ }
559
+ /** Passwordless login: usernameless assertion options. */
560
+ async getPasskeyLoginOptions() {
561
+ const response = await getPasskeyLoginOptions({ client: this.client });
562
+ return response.data.options;
563
+ }
564
+ /** Passwordless login: assertion → session. */
565
+ async completePasskeyLogin(assertion) {
566
+ const response = await verifyPasskeyLogin({
567
+ client: this.client,
568
+ body: { assertion },
569
+ });
570
+ return this.finalizeAuthResponse(response.data);
571
+ }
572
+ /**
573
+ * Begin enrollment. Authenticated settings flow omits `mfaToken`; the
574
+ * forced-enrollment lane passes the token from an
575
+ * `mfa_enrollment_required` login.
576
+ */
577
+ async getPasskeyRegistrationOptions(mfaToken) {
578
+ const response = await getPasskeyRegistrationOptions({
579
+ client: this.client,
580
+ body: { mfa_token: mfaToken },
581
+ });
582
+ return response.data.options;
583
+ }
584
+ /**
585
+ * Finish enrollment. First passkey returns recovery codes (once); in the
586
+ * forced-enrollment lane the result carries the completed login and the
587
+ * session token is stored.
588
+ */
589
+ async completePasskeyEnrollment(credential, options) {
590
+ var _a;
591
+ const response = await verifyPasskeyRegistration({
592
+ client: this.client,
593
+ body: {
594
+ credential,
595
+ name: options === null || options === void 0 ? void 0 : options.name,
596
+ mfa_token: options === null || options === void 0 ? void 0 : options.mfaToken,
597
+ },
598
+ });
599
+ const data = response.data;
600
+ let auth;
601
+ if (data.auth) {
602
+ auth = await this.finalizeAuthResponse(data.auth);
603
+ }
604
+ return {
605
+ passkey: data.passkey,
606
+ recoveryCodes: (_a = data.recovery_codes) !== null && _a !== void 0 ? _a : undefined,
607
+ auth,
608
+ };
609
+ }
610
+ /** Enrolled passkeys for the settings surface. */
611
+ async listPasskeys() {
612
+ var _a, _b;
613
+ const response = await listUserPasskeys({ client: this.client });
614
+ return ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.passkeys) !== null && _b !== void 0 ? _b : []);
615
+ }
616
+ /** Fresh-assertion options for destructive lifecycle actions. */
617
+ async getPasskeyReauthOptions() {
618
+ const response = await getPasskeyReauthOptions({ client: this.client });
619
+ return response.data.options;
620
+ }
621
+ /** Remove a passkey; exactly one re-auth proof must be supplied. */
622
+ async deletePasskey(passkeyId, proof) {
623
+ await deleteUserPasskey({
624
+ client: this.client,
625
+ path: { passkey_id: passkeyId },
626
+ body: { password: proof.password, assertion: proof.assertion },
627
+ });
628
+ }
629
+ /** MFA posture for the settings surface; null on any failure. */
630
+ async getMfaStatus() {
631
+ try {
632
+ const response = await getMfaStatus({ client: this.client });
633
+ const data = response.data;
634
+ if (!data || typeof data.passkey_count !== 'number') {
635
+ return null;
636
+ }
637
+ return {
638
+ passkeyCount: data.passkey_count,
639
+ recoveryCodesRemaining: data.recovery_codes_remaining,
640
+ enforcementApplies: data.enforcement_applies,
641
+ };
642
+ }
643
+ catch (_a) {
644
+ return null;
645
+ }
646
+ }
647
+ /** Replace the recovery-code set; codes are shown exactly once. */
648
+ async regenerateRecoveryCodes(proof) {
649
+ const response = await regenerateMfaRecoveryCodes({
650
+ client: this.client,
651
+ body: { password: proof.password, assertion: proof.assertion },
652
+ });
653
+ return response.data.codes;
654
+ }
494
655
  // Clear request deduplication cache (useful after login/logout)
495
656
  clearAuthCache() {
496
657
  this.lastAuthCheck = null;
@@ -6,14 +6,45 @@ export interface AuthUser {
6
6
  createdAt: string;
7
7
  updatedAt: string;
8
8
  }
9
+ /**
10
+ * Login flow state. `authenticated` is the terminal state (token present);
11
+ * the two MFA states mean no session exists yet and `mfaToken` authorizes
12
+ * the next step (the second-factor handshake or forced enrollment).
13
+ */
14
+ export type AuthFlowStatus = 'authenticated' | 'mfa_required' | 'mfa_enrollment_required';
9
15
  export interface AuthResponse {
10
16
  user: AuthUser;
11
17
  success: boolean;
12
18
  message?: string;
19
+ status?: AuthFlowStatus;
20
+ mfaToken?: string;
13
21
  token?: string;
14
22
  expires_in?: number;
15
23
  refresh_threshold?: number;
16
24
  }
25
+ /** Enrollment result from completing a passkey registration ceremony. */
26
+ export interface PasskeyEnrollmentResult {
27
+ passkey: Record<string, unknown>;
28
+ recoveryCodes?: string[];
29
+ auth?: AuthResponse;
30
+ }
31
+ /**
32
+ * Deployment auth posture from `GET /v1/auth/providers`.
33
+ *
34
+ * A rendering hint, not a security boundary — the backend enforces every
35
+ * flag regardless of what the login surface shows. One byte-identical
36
+ * frontend build renders password-primary (managed), OIDC-primary
37
+ * (tenant), or closed-registration postures from this payload.
38
+ */
39
+ export interface AuthProviders {
40
+ password_auth: boolean;
41
+ oidc: {
42
+ enabled: boolean;
43
+ provider_label?: string | null;
44
+ };
45
+ registration: boolean;
46
+ passkeys: boolean;
47
+ }
17
48
  export interface LoginRequest {
18
49
  email: string;
19
50
  password: string;
@@ -119,6 +150,8 @@ export interface UseUserHook {
119
150
  export interface SDKAuthResponse {
120
151
  user: AuthUser;
121
152
  message?: string;
153
+ status?: AuthFlowStatus;
154
+ mfa_token?: string | null;
122
155
  token?: string;
123
156
  expires_in?: number | null;
124
157
  refresh_threshold?: number | null;
package/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export type { TaskCreateResponse, TaskMonitorState, TaskPollingOptions, TaskStat
26
26
  export { useGraphCreation, useOperationMonitoring, useRepositorySubscription, type OperationMonitorState, type UseOperationMonitoringResult, } from './task-monitoring/operationHooks';
27
27
  import { client } from '@robosystems/client';
28
28
  export { client };
29
- export type { APIKey, AppName, AuthContextType, AuthUser } from './auth-core';
29
+ export type { APIKey, AppName, AuthContextType, AuthFlowStatus, AuthProviders, AuthUser, PasskeyEnrollmentResult, } from './auth-core';
30
30
  export { CURRENT_APP } from './auth-core/config';
31
31
  export { getLoginHomeUrl, isLoginHome, LOGIN_HOME_APP, } from './auth-core/config';
32
32
  export { buildLoginHomeUrl, buildReturnTo, isSafeRelativePath, parseReturnTo, } from './auth-core/login-home';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -27,7 +27,7 @@
27
27
  "tailwind-merge": "^3"
28
28
  },
29
29
  "peerDependencies": {
30
- "@robosystems/client": ">=1.2 <2",
30
+ "@robosystems/client": ">=1.10 <2",
31
31
  "flowbite-react": "^0.12.5",
32
32
  "next": ">=15 <17",
33
33
  "react": ">=18 <20",