@robosystems/core 0.6.3 → 0.7.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.
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useRouter } from 'next/navigation';
4
4
  import { useEffect } from 'react';
5
+ import { isSafeRelativePath } from '../auth-core/login-home';
5
6
  import { BrandSpinner } from '../ui-components';
6
7
  import { useAuth } from './AuthProvider';
7
8
  export function AuthGuard({ children, redirectTo = '/login', loadingComponent, }) {
@@ -9,15 +10,28 @@ export function AuthGuard({ children, redirectTo = '/login', loadingComponent, }
9
10
  const router = useRouter();
10
11
  useEffect(() => {
11
12
  if (!isLoading && !isAuthenticated) {
13
+ // Carry the intended destination so the login flow can land the user
14
+ // back where they were headed instead of the default landing page.
15
+ // A bare "/" carries no information — the default landing already
16
+ // covers it — so it is not forwarded.
17
+ let target = redirectTo;
18
+ if (typeof window !== 'undefined') {
19
+ const destination = window.location.pathname + window.location.search;
20
+ if (destination !== '/' &&
21
+ destination !== redirectTo &&
22
+ isSafeRelativePath(destination)) {
23
+ target = `${redirectTo}?return_to=${encodeURIComponent(destination)}`;
24
+ }
25
+ }
12
26
  try {
13
- router.push(redirectTo);
27
+ router.push(target);
14
28
  }
15
29
  catch (error) {
16
30
  if (process.env.NODE_ENV === 'development') {
17
31
  // Fallback to window.location if router.push fails
18
32
  console.error('Router push failed, using window.location fallback:', error);
19
33
  }
20
- window.location.href = redirectTo;
34
+ window.location.href = target;
21
35
  }
22
36
  }
23
37
  }, [isAuthenticated, isLoading, router, redirectTo]);
@@ -5,7 +5,9 @@ import { clearEntitySelection } from '../actions/entity-actions';
5
5
  import { clearGraphSelection } from '../actions/graph-actions';
6
6
  import { performLogoutCleanup } from '../auth-core/cleanup';
7
7
  import { RoboSystemsAuthClient } from '../auth-core/client';
8
+ import { CURRENT_APP, isLoginHome } from '../auth-core/config';
8
9
  import { useTokenExpiryHandler } from '../auth-core/hooks';
10
+ import { buildLoginHomeUrl, buildReturnTo } from '../auth-core/login-home';
9
11
  import { getTimeUntilExpiry, getTokenStatus } from '../auth-core/token-storage';
10
12
  // Configuration constants
11
13
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
@@ -108,7 +110,7 @@ export function AuthProvider({ children, apiUrl = process.env.NEXT_PUBLIC_ROBOSY
108
110
  setIsLoading(false);
109
111
  }
110
112
  }, [authClient]);
111
- const logout = useCallback(async (reason) => {
113
+ const logout = useCallback(async (reason, options) => {
112
114
  try {
113
115
  await authClient.logout();
114
116
  }
@@ -129,23 +131,46 @@ export function AuthProvider({ children, apiUrl = process.env.NEXT_PUBLIC_ROBOSY
129
131
  Promise.allSettled([clearGraphSelection(), clearEntitySelection()]),
130
132
  new Promise((resolve) => setTimeout(resolve, 2000)),
131
133
  ]);
132
- // Clear auth client cache and local user/session state
134
+ // Clear auth client cache and local storage state. Deliberately NO
135
+ // setUser(null) here: the hard navigation below tears the tree down
136
+ // anyway, and nulling the user first wakes AuthGuard, whose
137
+ // client-side redirect (→ /login → LoginRedirector → login home)
138
+ // can outrace the queued /logout navigation — the anchor session
139
+ // then survives and silently signs the user back in, undoing the
140
+ // logout (observed E2E under centralized login).
133
141
  authClient.clearAuthCache();
134
- setUser(null);
135
142
  setSessionWarning({ show: false, timeLeft: 0 });
136
143
  // Perform comprehensive cleanup of all user-specific data
137
144
  performLogoutCleanup();
138
145
  debugLog('Logout cleanup completed');
139
146
  // Always hard-redirect out of the authenticated area. A full-page
140
147
  // navigation (not a client-side router.push) guarantees the
141
- // authenticated tree tears down instead of AuthGuard flashing a blank
142
- // screen once user state clears. A forced logout (reason set, e.g.
148
+ // authenticated tree tears down without AuthGuard racing it (see
149
+ // above). A forced logout (reason set, e.g.
143
150
  // session_expired) goes to the login page so it can explain why the
144
- // session ended and let the user sign back in; a manual logout goes to
145
- // the public homepage, since the user likely isn't trying to sign in
146
- // again right away.
151
+ // session ended and let the user sign back in. A manual logout on a
152
+ // product app chains through the login home's /logout so the anchor
153
+ // session dies too — otherwise the next visit silently re-bridges and
154
+ // "log out" appears not to work. On the login home itself (and in
155
+ // single-app deployments, where the app is its own login home) manual
156
+ // logout goes to the public homepage. `options.redirectTo` overrides
157
+ // the destination — used by the login home's /logout route to land the
158
+ // user back on the app they came from.
147
159
  if (typeof window !== 'undefined') {
148
- window.location.href = reason ? `/login?reason=${reason}` : '/';
160
+ if (options === null || options === void 0 ? void 0 : options.redirectTo) {
161
+ window.location.href = options.redirectTo;
162
+ }
163
+ else if (reason) {
164
+ window.location.href = `/login?reason=${reason}`;
165
+ }
166
+ else if (!isLoginHome()) {
167
+ window.location.href = buildLoginHomeUrl('logout', {
168
+ returnTo: buildReturnTo(CURRENT_APP),
169
+ });
170
+ }
171
+ else {
172
+ window.location.href = '/';
173
+ }
149
174
  }
150
175
  }
151
176
  }, [authClient]);
@@ -494,8 +519,8 @@ export function AuthProvider({ children, apiUrl = process.env.NEXT_PUBLIC_ROBOSY
494
519
  throw new Error(response.message || 'Registration failed');
495
520
  }
496
521
  };
497
- const forgotPassword = async (email) => {
498
- return authClient.forgotPassword(email);
522
+ const forgotPassword = async (email, options) => {
523
+ return authClient.forgotPassword(email, options);
499
524
  };
500
525
  const resetPassword = async (token, newPassword) => {
501
526
  const result = await authClient.resetPassword(token, newPassword);
@@ -534,8 +559,8 @@ export function AuthProvider({ children, apiUrl = process.env.NEXT_PUBLIC_ROBOSY
534
559
  }
535
560
  return result;
536
561
  };
537
- const resendVerificationEmail = async (email) => {
538
- return authClient.resendVerificationEmail(email);
562
+ const resendVerificationEmail = async (email, options) => {
563
+ return authClient.resendVerificationEmail(email, options);
539
564
  };
540
565
  const value = {
541
566
  user,
@@ -0,0 +1,20 @@
1
+ export interface LoginRedirectorProps {
2
+ /** Which auth surface this page stands in for. */
3
+ mode?: 'login' | 'register';
4
+ apiUrl: string;
5
+ /** Where a completed bridge without a returnUrl lands. */
6
+ homePath?: string;
7
+ }
8
+ /**
9
+ * Product-app stand-in for the login/register pages under centralized login.
10
+ *
11
+ * Two jobs:
12
+ * - `?session_id=` present: consume the SSO bridge handoff exactly as the
13
+ * full login page did. On failure it renders an error with a manual link
14
+ * to the login home — never an automatic redirect, which with a live
15
+ * anchor session would loop (login home re-bridges → fails → back again).
16
+ * - otherwise: redirect to the login home, upgrading a local `?return_to=`
17
+ * path to app-qualified form and forwarding foreign query params
18
+ * (e.g. `invite`) so mailed links keep working.
19
+ */
20
+ export declare function LoginRedirector({ mode, apiUrl, homePath, }: LoginRedirectorProps): import("react").JSX.Element;
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { CURRENT_APP, isLoginHome } from '../auth-core/config';
5
+ import { buildLoginHomeUrl, buildReturnTo, isSafeRelativePath, } from '../auth-core/login-home';
6
+ import { useSSO } from '../auth-core/sso';
7
+ import { BrandSpinner } from '../ui-components';
8
+ /**
9
+ * Product-app stand-in for the login/register pages under centralized login.
10
+ *
11
+ * Two jobs:
12
+ * - `?session_id=` present: consume the SSO bridge handoff exactly as the
13
+ * full login page did. On failure it renders an error with a manual link
14
+ * to the login home — never an automatic redirect, which with a live
15
+ * anchor session would loop (login home re-bridges → fails → back again).
16
+ * - otherwise: redirect to the login home, upgrading a local `?return_to=`
17
+ * path to app-qualified form and forwarding foreign query params
18
+ * (e.g. `invite`) so mailed links keep working.
19
+ */
20
+ export function LoginRedirector({ mode = 'login', apiUrl, homePath = '/home', }) {
21
+ const { handleSSOLogin } = useSSO(apiUrl);
22
+ const [bridgeFailed, setBridgeFailed] = useState(false);
23
+ const [misconfigured, setMisconfigured] = useState(false);
24
+ const startedRef = useRef(false);
25
+ useEffect(() => {
26
+ var _a;
27
+ // Guard against StrictMode double-mount: the first run consumes
28
+ // session_id and strips it from the URL, so a second run would
29
+ // misread the state as a failed bridge.
30
+ if (startedRef.current) {
31
+ return;
32
+ }
33
+ startedRef.current = true;
34
+ const params = new URLSearchParams(window.location.search);
35
+ const sessionId = params.get('session_id');
36
+ if (sessionId) {
37
+ const returnUrl = params.get('returnUrl');
38
+ void handleSSOLogin().then((user) => {
39
+ if (user) {
40
+ // With a returnUrl, handleSSOLogin navigates there itself.
41
+ if (!returnUrl) {
42
+ window.location.href = homePath;
43
+ }
44
+ }
45
+ else {
46
+ setBridgeFailed(true);
47
+ }
48
+ });
49
+ return;
50
+ }
51
+ // Never self-redirect: mounted on the app that IS the login home
52
+ // (misconfiguration — the login home renders real auth forms, not this
53
+ // redirector), buildLoginHomeUrl() resolves to this same app and
54
+ // window.location.replace() would loop. Fail safe with a static state,
55
+ // mirroring AuthProvider.logout's isLoginHome() guard.
56
+ if (isLoginHome()) {
57
+ console.error('[LoginRedirector] rendered on the login home app — refusing to self-redirect. Check NEXT_PUBLIC_LOGIN_HOME_APP / the centralized-login flag.');
58
+ setMisconfigured(true);
59
+ return;
60
+ }
61
+ const localReturnTo = params.get('return_to');
62
+ const path = localReturnTo && isSafeRelativePath(localReturnTo)
63
+ ? localReturnTo
64
+ : undefined;
65
+ const reason = (_a = params.get('reason')) !== null && _a !== void 0 ? _a : undefined;
66
+ // replace() keeps the redirector page out of history so Back returns
67
+ // to the page the user actually came from.
68
+ window.location.replace(buildLoginHomeUrl(mode, {
69
+ returnTo: buildReturnTo(CURRENT_APP, path),
70
+ reason,
71
+ forwardParams: params,
72
+ }));
73
+ }, [handleSSOLogin, mode, homePath]);
74
+ if (misconfigured) {
75
+ return (_jsxs("div", { className: "flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center", children: [_jsx("p", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Sign-in is temporarily unavailable" }), _jsx("p", { className: "max-w-md text-sm text-gray-600 dark:text-gray-400", children: "This page is misconfigured. Please contact support if the problem persists." })] }));
76
+ }
77
+ if (bridgeFailed) {
78
+ const retryUrl = buildLoginHomeUrl(mode, {
79
+ returnTo: buildReturnTo(CURRENT_APP),
80
+ reason: 'bridge_failed',
81
+ });
82
+ return (_jsxs("div", { className: "flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center", children: [_jsx("p", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Sign-in could not be completed" }), _jsx("p", { className: "max-w-md text-sm text-gray-600 dark:text-gray-400", children: "The sign-in handoff expired or was invalid. This can happen when a sign-in link is reused or takes too long to load." }), _jsx("a", { href: retryUrl, className: "rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: "Continue to sign in" })] }));
83
+ }
84
+ return _jsx(BrandSpinner, { size: "xl", fullScreen: true });
85
+ }
@@ -1,6 +1,7 @@
1
1
  export { AppSwitcher } from './AppSwitcher';
2
2
  export { AuthGuard } from './AuthGuard';
3
3
  export { AuthProvider, useAuth, useOptionalAuth } from './AuthProvider';
4
+ export { LoginRedirector } from './LoginRedirector';
4
5
  export { SignInForm } from './SignInForm';
5
6
  export { SignUpForm } from './SignUpForm';
6
7
  export { TurnstileWidget, useTurnstile } from './TurnstileWidget';
@@ -1,6 +1,7 @@
1
1
  export { AppSwitcher } from './AppSwitcher';
2
2
  export { AuthGuard } from './AuthGuard';
3
3
  export { AuthProvider, useAuth, useOptionalAuth } from './AuthProvider';
4
+ export { LoginRedirector } from './LoginRedirector';
4
5
  export { SignInForm } from './SignInForm';
5
6
  export { SignUpForm } from './SignUpForm';
6
7
  export { TurnstileWidget, useTurnstile } from './TurnstileWidget';
@@ -35,7 +35,9 @@ export declare class RoboSystemsAuthClient {
35
35
  */
36
36
  private syncClientConfigToken;
37
37
  login(email: string, password: string): Promise<AuthResponse>;
38
- register(email: string, password: string, name?: string, captchaToken?: string, inviteToken?: string): Promise<AuthResponse>;
38
+ register(email: string, password: string, name?: string, captchaToken?: string, inviteToken?: string, options?: {
39
+ appSource?: string;
40
+ }): Promise<AuthResponse>;
39
41
  logout(): Promise<void>;
40
42
  getCurrentUser(): Promise<AuthUser>;
41
43
  private performAuthCheck;
@@ -57,7 +59,9 @@ export declare class RoboSystemsAuthClient {
57
59
  /**
58
60
  * Send password reset email
59
61
  */
60
- forgotPassword(email: string): Promise<{
62
+ forgotPassword(email: string, options?: {
63
+ appSource?: string;
64
+ }): Promise<{
61
65
  success: boolean;
62
66
  message?: string;
63
67
  }>;
@@ -87,7 +91,9 @@ export declare class RoboSystemsAuthClient {
87
91
  /**
88
92
  * Resend verification email
89
93
  */
90
- resendVerificationEmail(email: string): Promise<{
94
+ resendVerificationEmail(email: string, options?: {
95
+ appSource?: string;
96
+ }): Promise<{
91
97
  success: boolean;
92
98
  message?: string;
93
99
  }>;
@@ -18,6 +18,15 @@ const setSDKClientConfig = (_a = sdkClientsModule.setSDKClientConfig) !== null &
18
18
  // Configuration constants
19
19
  const CACHE_TTL_MS = 30 * 1000; // 30 seconds - optimized for performance
20
20
  const ERROR_CACHE_TTL_MS = 5 * 1000; // 5 seconds - allow reasonable retry delay
21
+ /**
22
+ * Identifies which product app an auth request originates from, for email
23
+ * branding. Under centralized login every interactive auth request comes
24
+ * from the login home, so Referer-based detection on the backend collapses
25
+ * to one app — the originating app (from `return_to`) rides this header
26
+ * instead.
27
+ */
28
+ const APP_SOURCE_HEADER = 'X-App-Source';
29
+ const appSourceHeaders = (appSource) => appSource ? { [APP_SOURCE_HEADER]: appSource } : undefined;
21
30
  // Custom error class for token expiry
22
31
  export class TokenExpiredError extends Error {
23
32
  constructor(message = 'Token expired') {
@@ -200,7 +209,7 @@ export class RoboSystemsAuthClient {
200
209
  refresh_threshold: sdkResponse.refresh_threshold,
201
210
  };
202
211
  }
203
- async register(email, password, name, captchaToken, inviteToken) {
212
+ async register(email, password, name, captchaToken, inviteToken, options) {
204
213
  const response = await registerUser({
205
214
  client: this.client,
206
215
  body: {
@@ -210,6 +219,7 @@ export class RoboSystemsAuthClient {
210
219
  captcha_token: captchaToken || undefined,
211
220
  invite_token: inviteToken || undefined,
212
221
  },
222
+ headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
213
223
  });
214
224
  // Check for error responses (4xx/5xx)
215
225
  if (response.error) {
@@ -490,11 +500,12 @@ export class RoboSystemsAuthClient {
490
500
  /**
491
501
  * Send password reset email
492
502
  */
493
- async forgotPassword(email) {
503
+ async forgotPassword(email, options) {
494
504
  try {
495
505
  await forgotPassword({
496
506
  client: this.client,
497
507
  body: { email },
508
+ headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
498
509
  });
499
510
  return {
500
511
  success: true,
@@ -616,11 +627,12 @@ export class RoboSystemsAuthClient {
616
627
  /**
617
628
  * Resend verification email
618
629
  */
619
- async resendVerificationEmail(email) {
630
+ async resendVerificationEmail(email, options) {
620
631
  try {
621
632
  await resendVerificationEmail({
622
633
  client: this.client,
623
634
  body: { email },
635
+ headers: appSourceHeaders(options === null || options === void 0 ? void 0 : options.appSource),
624
636
  });
625
637
  return {
626
638
  success: true,
@@ -17,3 +17,11 @@ export declare const BRAND_COLORS: Record<AppName, string>;
17
17
  export declare const BRAND_GRADIENTS: Record<AppName, string>;
18
18
  export declare const APP_CONFIGS: Record<string, AppConfig>;
19
19
  export declare const getAppConfig: (appName: string) => AppConfig;
20
+ /**
21
+ * The app hosting the interactive auth surface ("login home"). Product apps
22
+ * redirect login/register there; single-app deployments designate their own
23
+ * app key so the architecture degrades to local auth by config.
24
+ */
25
+ export declare const LOGIN_HOME_APP: AppName;
26
+ export declare const getLoginHomeUrl: () => string;
27
+ export declare const isLoginHome: () => boolean;
@@ -54,3 +54,11 @@ export const APP_CONFIGS = {
54
54
  export const getAppConfig = (appName) => {
55
55
  return APP_CONFIGS[appName] || APP_CONFIGS.robosystems;
56
56
  };
57
+ /**
58
+ * The app hosting the interactive auth surface ("login home"). Product apps
59
+ * redirect login/register there; single-app deployments designate their own
60
+ * app key so the architecture degrades to local auth by config.
61
+ */
62
+ export const LOGIN_HOME_APP = process.env.NEXT_PUBLIC_LOGIN_HOME_APP || 'robosystems';
63
+ export const getLoginHomeUrl = () => getAppConfig(LOGIN_HOME_APP).url;
64
+ export const isLoginHome = () => CURRENT_APP === LOGIN_HOME_APP;
@@ -2,5 +2,6 @@ export * from './cleanup';
2
2
  export * from './client';
3
3
  export * from './config';
4
4
  export * from './hooks';
5
+ export * from './login-home';
5
6
  export * from './sso';
6
7
  export * from './types';
@@ -2,5 +2,6 @@ export * from './cleanup';
2
2
  export * from './client';
3
3
  export * from './config';
4
4
  export * from './hooks';
5
+ export * from './login-home';
5
6
  export * from './sso';
6
7
  export * from './types';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Helpers for the centralized login home.
3
+ *
4
+ * Interactive auth (login/register/verify/reset) renders on one designated
5
+ * app — the "login home" — and product apps redirect to it carrying a
6
+ * `return_to` value so the user is bridged back after authenticating.
7
+ *
8
+ * `return_to` grammar: `app` or `app:/path`. The app segment is validated
9
+ * against APP_CONFIGS; the path must be a same-app relative path. Never a
10
+ * free-form URL — an open redirect on a login surface is a phishing
11
+ * amplifier.
12
+ */
13
+ export interface ParsedReturnTo {
14
+ app: string;
15
+ /** Relative path on the target app, or null for its default landing. */
16
+ path: string | null;
17
+ }
18
+ /**
19
+ * True when `path` is a same-app relative path. Rejects scheme-relative
20
+ * URLs ("//host"), absolute URLs, backslashes (browsers normalize
21
+ * "/\host" to "//host"), and ASCII tab/newline characters (browsers
22
+ * strip those before URL parsing, so "/<TAB>/host" would normalize
23
+ * to "//host").
24
+ */
25
+ export declare function isSafeRelativePath(path: string): boolean;
26
+ /** Build a `return_to` value. An unsafe path degrades to app-only. */
27
+ export declare function buildReturnTo(app: string, path?: string | null): string;
28
+ /**
29
+ * Parse a raw `return_to` query value (already URL-decoded by
30
+ * URLSearchParams — do not decode again). Unknown apps return null;
31
+ * an unsafe path degrades to the app's default landing.
32
+ */
33
+ export declare function parseReturnTo(raw: string | null | undefined): ParsedReturnTo | null;
34
+ export interface LoginHomeUrlOptions {
35
+ /** `return_to` value (build with buildReturnTo). */
36
+ returnTo?: string;
37
+ /** Sign-in notice key (e.g. session_expired) rendered by the login home. */
38
+ reason?: string;
39
+ /**
40
+ * Query params to carry through to the login home (e.g. `invite`).
41
+ * Params owned by the redirect/bridge flow are dropped.
42
+ */
43
+ forwardParams?: URLSearchParams;
44
+ }
45
+ /** Build an absolute URL on the login home for the given auth surface. */
46
+ export declare function buildLoginHomeUrl(mode: 'login' | 'register' | 'logout', options?: LoginHomeUrlOptions): string;
@@ -0,0 +1,61 @@
1
+ import { APP_CONFIGS, getLoginHomeUrl } from './config';
2
+ /**
3
+ * True when `path` is a same-app relative path. Rejects scheme-relative
4
+ * URLs ("//host"), absolute URLs, backslashes (browsers normalize
5
+ * "/\host" to "//host"), and ASCII tab/newline characters (browsers
6
+ * strip those before URL parsing, so "/<TAB>/host" would normalize
7
+ * to "//host").
8
+ */
9
+ export function isSafeRelativePath(path) {
10
+ if (!path.startsWith('/') || path.startsWith('//')) {
11
+ return false;
12
+ }
13
+ return !/[\\\t\n\r]/.test(path);
14
+ }
15
+ /** Build a `return_to` value. An unsafe path degrades to app-only. */
16
+ export function buildReturnTo(app, path) {
17
+ if (path && isSafeRelativePath(path)) {
18
+ return `${app}:${path}`;
19
+ }
20
+ return app;
21
+ }
22
+ /**
23
+ * Parse a raw `return_to` query value (already URL-decoded by
24
+ * URLSearchParams — do not decode again). Unknown apps return null;
25
+ * an unsafe path degrades to the app's default landing.
26
+ */
27
+ export function parseReturnTo(raw) {
28
+ if (!raw) {
29
+ return null;
30
+ }
31
+ const separator = raw.indexOf(':');
32
+ const app = separator === -1 ? raw : raw.slice(0, separator);
33
+ const path = separator === -1 ? null : raw.slice(separator + 1);
34
+ if (!APP_CONFIGS[app]) {
35
+ return null;
36
+ }
37
+ if (path !== null && !isSafeRelativePath(path)) {
38
+ return { app, path: null };
39
+ }
40
+ return { app, path };
41
+ }
42
+ /** Params the redirect/bridge flow owns; never forwarded verbatim. */
43
+ const OWNED_PARAMS = new Set(['session_id', 'returnUrl', 'return_to', 'reason']);
44
+ /** Build an absolute URL on the login home for the given auth surface. */
45
+ export function buildLoginHomeUrl(mode, options = {}) {
46
+ const url = new URL(`/${mode}`, getLoginHomeUrl());
47
+ if (options.forwardParams) {
48
+ options.forwardParams.forEach((value, key) => {
49
+ if (!OWNED_PARAMS.has(key)) {
50
+ url.searchParams.append(key, value);
51
+ }
52
+ });
53
+ }
54
+ if (options.returnTo) {
55
+ url.searchParams.set('return_to', options.returnTo);
56
+ }
57
+ if (options.reason) {
58
+ url.searchParams.set('reason', options.reason);
59
+ }
60
+ return url.toString();
61
+ }
@@ -71,10 +71,14 @@ export interface AuthContextType {
71
71
  };
72
72
  login: (email: string, password: string) => Promise<AuthUser>;
73
73
  register: (email: string, password: string, name?: string) => Promise<AuthUser>;
74
- logout: (reason?: string) => Promise<void>;
74
+ logout: (reason?: string, options?: {
75
+ redirectTo?: string;
76
+ }) => Promise<void>;
75
77
  refreshUser: () => Promise<AuthUser | null>;
76
78
  refreshSession: (force?: boolean) => Promise<void>;
77
- forgotPassword: (email: string) => Promise<{
79
+ forgotPassword: (email: string, options?: {
80
+ appSource?: string;
81
+ }) => Promise<{
78
82
  success: boolean;
79
83
  message?: string;
80
84
  }>;
@@ -92,7 +96,9 @@ export interface AuthContextType {
92
96
  message?: string;
93
97
  user?: AuthUser;
94
98
  }>;
95
- resendVerificationEmail: (email: string) => Promise<{
99
+ resendVerificationEmail: (email: string, options?: {
100
+ appSource?: string;
101
+ }) => Promise<{
96
102
  success: boolean;
97
103
  message?: string;
98
104
  }>;
package/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * as AuthCore from './auth-core';
2
2
  export { clearToken, getAuthHeader, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
3
3
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
4
- export { AppSwitcher, AuthGuard, AuthProvider, SignInForm, SignUpForm, useAuth, } from './auth-components';
4
+ export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
5
5
  export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
6
6
  export { createGraphContext, createGraphProvider, createUseGraphContext, EntityProvider, GraphContext, GraphProvider, OrgProvider, ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, type EntityContextValue, type EntityProviderProps, type GraphContextValue, type GraphProviderProps, type GraphState, type OrgContextState, type OrgContextValue, type OrgProviderProps, type ServiceOfferings, } from './contexts';
7
7
  export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, type EntityCookie, type GraphCookie, type McpConnectorUrl, type SidebarCookie, } from './lib';
@@ -28,3 +28,6 @@ import { client } from '@robosystems/client';
28
28
  export { client };
29
29
  export type { APIKey, AppName, AuthContextType, AuthUser } from './auth-core';
30
30
  export { CURRENT_APP } from './auth-core/config';
31
+ export { getLoginHomeUrl, isLoginHome, LOGIN_HOME_APP, } from './auth-core/config';
32
+ export { buildLoginHomeUrl, buildReturnTo, isSafeRelativePath, parseReturnTo, } from './auth-core/login-home';
33
+ export type { LoginHomeUrlOptions, ParsedReturnTo, } from './auth-core/login-home';
package/index.js CHANGED
@@ -6,7 +6,7 @@ export { clearToken, getAuthHeader, getToken, getValidToken, handleAuthResponse,
6
6
  // Export auth cleanup utilities
7
7
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
8
8
  // Export auth-components
9
- export { AppSwitcher, AuthGuard, AuthProvider, SignInForm, SignUpForm, useAuth, } from './auth-components';
9
+ export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
10
10
  // Export hooks
11
11
  export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, } from './hooks';
12
12
  // Export contexts
@@ -75,3 +75,6 @@ if (process.env.NODE_ENV !== 'test') {
75
75
  export { client };
76
76
  // App identity
77
77
  export { CURRENT_APP } from './auth-core/config';
78
+ // Centralized login home
79
+ export { getLoginHomeUrl, isLoginHome, LOGIN_HOME_APP, } from './auth-core/config';
80
+ 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.6.3",
3
+ "version": "0.7.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,