@robosystems/core 0.7.2 → 0.8.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.
@@ -1,4 +1,4 @@
1
- import type { APIKey, AuthProviders, 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>;
@@ -63,6 +75,58 @@ export declare class RoboSystemsAuthClient {
63
75
  * the caller renders the default password posture.
64
76
  */
65
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. The settings lane must carry a fresh re-auth proof —
91
+ * `password`, or a reauth-ceremony `assertion` when adding a passkey
92
+ * beside an existing one; a session alone is refused by the backend. The
93
+ * forced-enrollment lane passes only the `mfaToken` from an
94
+ * `mfa_enrollment_required` login, which is its own freshness proof.
95
+ */
96
+ getPasskeyRegistrationOptions(proof?: {
97
+ mfaToken?: string;
98
+ password?: string;
99
+ assertion?: Record<string, unknown>;
100
+ }): Promise<Record<string, unknown>>;
101
+ /**
102
+ * Finish enrollment. First passkey returns recovery codes (once); in the
103
+ * forced-enrollment lane the result carries the completed login and the
104
+ * session token is stored.
105
+ */
106
+ completePasskeyEnrollment(credential: Record<string, unknown>, options?: {
107
+ name?: string;
108
+ mfaToken?: string;
109
+ }): Promise<PasskeyEnrollmentResult>;
110
+ /** Enrolled passkeys for the settings surface. */
111
+ listPasskeys(): Promise<Record<string, unknown>[]>;
112
+ /** Fresh-assertion options for destructive lifecycle actions. */
113
+ getPasskeyReauthOptions(): Promise<Record<string, unknown>>;
114
+ /** Remove a passkey; exactly one re-auth proof must be supplied. */
115
+ deletePasskey(passkeyId: string, proof: {
116
+ password?: string;
117
+ assertion?: Record<string, unknown>;
118
+ }): Promise<void>;
119
+ /** MFA posture for the settings surface; null on any failure. */
120
+ getMfaStatus(): Promise<{
121
+ passkeyCount: number;
122
+ recoveryCodesRemaining: number;
123
+ enforcementApplies: boolean;
124
+ } | null>;
125
+ /** Replace the recovery-code set; codes are shown exactly once. */
126
+ regenerateRecoveryCodes(proof: {
127
+ password?: string;
128
+ assertion?: Record<string, unknown>;
129
+ }): Promise<string[]>;
66
130
  clearAuthCache(): void;
67
131
  /**
68
132
  * Send password reset email
@@ -1,5 +1,5 @@
1
1
  var _a;
2
- import { checkPasswordStrength, client, completeSsoAuth, createUserApiKey, forgotPassword, generateSsoToken, getAuthProviders, 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) {
@@ -511,6 +530,136 @@ export class RoboSystemsAuthClient {
511
530
  return null;
512
531
  }
513
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. The settings lane must carry a fresh re-auth proof —
574
+ * `password`, or a reauth-ceremony `assertion` when adding a passkey
575
+ * beside an existing one; a session alone is refused by the backend. The
576
+ * forced-enrollment lane passes only the `mfaToken` from an
577
+ * `mfa_enrollment_required` login, which is its own freshness proof.
578
+ */
579
+ async getPasskeyRegistrationOptions(proof) {
580
+ const response = await getPasskeyRegistrationOptions({
581
+ client: this.client,
582
+ // The proof fields land in client 1.11's generated request model;
583
+ // until the regen they ride as extra JSON properties, hence the cast.
584
+ body: {
585
+ mfa_token: proof === null || proof === void 0 ? void 0 : proof.mfaToken,
586
+ password: proof === null || proof === void 0 ? void 0 : proof.password,
587
+ assertion: proof === null || proof === void 0 ? void 0 : proof.assertion,
588
+ },
589
+ });
590
+ return response.data.options;
591
+ }
592
+ /**
593
+ * Finish enrollment. First passkey returns recovery codes (once); in the
594
+ * forced-enrollment lane the result carries the completed login and the
595
+ * session token is stored.
596
+ */
597
+ async completePasskeyEnrollment(credential, options) {
598
+ var _a;
599
+ const response = await verifyPasskeyRegistration({
600
+ client: this.client,
601
+ body: {
602
+ credential,
603
+ name: options === null || options === void 0 ? void 0 : options.name,
604
+ mfa_token: options === null || options === void 0 ? void 0 : options.mfaToken,
605
+ },
606
+ });
607
+ const data = response.data;
608
+ let auth;
609
+ if (data.auth) {
610
+ auth = await this.finalizeAuthResponse(data.auth);
611
+ }
612
+ return {
613
+ passkey: data.passkey,
614
+ recoveryCodes: (_a = data.recovery_codes) !== null && _a !== void 0 ? _a : undefined,
615
+ auth,
616
+ };
617
+ }
618
+ /** Enrolled passkeys for the settings surface. */
619
+ async listPasskeys() {
620
+ var _a, _b;
621
+ const response = await listUserPasskeys({ client: this.client });
622
+ return ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.passkeys) !== null && _b !== void 0 ? _b : []);
623
+ }
624
+ /** Fresh-assertion options for destructive lifecycle actions. */
625
+ async getPasskeyReauthOptions() {
626
+ const response = await getPasskeyReauthOptions({ client: this.client });
627
+ return response.data.options;
628
+ }
629
+ /** Remove a passkey; exactly one re-auth proof must be supplied. */
630
+ async deletePasskey(passkeyId, proof) {
631
+ await deleteUserPasskey({
632
+ client: this.client,
633
+ path: { passkey_id: passkeyId },
634
+ body: { password: proof.password, assertion: proof.assertion },
635
+ });
636
+ }
637
+ /** MFA posture for the settings surface; null on any failure. */
638
+ async getMfaStatus() {
639
+ try {
640
+ const response = await getMfaStatus({ client: this.client });
641
+ const data = response.data;
642
+ if (!data || typeof data.passkey_count !== 'number') {
643
+ return null;
644
+ }
645
+ return {
646
+ passkeyCount: data.passkey_count,
647
+ recoveryCodesRemaining: data.recovery_codes_remaining,
648
+ enforcementApplies: data.enforcement_applies,
649
+ };
650
+ }
651
+ catch (_a) {
652
+ return null;
653
+ }
654
+ }
655
+ /** Replace the recovery-code set; codes are shown exactly once. */
656
+ async regenerateRecoveryCodes(proof) {
657
+ const response = await regenerateMfaRecoveryCodes({
658
+ client: this.client,
659
+ body: { password: proof.password, assertion: proof.assertion },
660
+ });
661
+ return response.data.codes;
662
+ }
514
663
  // Clear request deduplication cache (useful after login/logout)
515
664
  clearAuthCache() {
516
665
  this.lastAuthCheck = null;
@@ -6,14 +6,28 @@ 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
+ }
17
31
  /**
18
32
  * Deployment auth posture from `GET /v1/auth/providers`.
19
33
  *
@@ -136,6 +150,8 @@ export interface UseUserHook {
136
150
  export interface SDKAuthResponse {
137
151
  user: AuthUser;
138
152
  message?: string;
153
+ status?: AuthFlowStatus;
154
+ mfa_token?: string | null;
139
155
  token?: string;
140
156
  expires_in?: number | null;
141
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, AuthProviders, 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.2",
3
+ "version": "0.8.1",
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.9 <2",
30
+ "@robosystems/client": ">=1.10 <2",
31
31
  "flowbite-react": "^0.12.5",
32
32
  "next": ">=15 <17",
33
33
  "react": ">=18 <20",