@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.
- package/auth-core/client.d.ts +65 -1
- package/auth-core/client.js +154 -5
- package/auth-core/types.d.ts +16 -0
- package/index.d.ts +1 -1
- package/package.json +2 -2
package/auth-core/client.d.ts
CHANGED
|
@@ -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
|
package/auth-core/client.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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;
|
package/auth-core/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
30
|
+
"@robosystems/client": ">=1.10 <2",
|
|
31
31
|
"flowbite-react": "^0.12.5",
|
|
32
32
|
"next": ">=15 <17",
|
|
33
33
|
"react": ">=18 <20",
|