From 596bfecd6f8caa7c067a206362ec047cad4d6cd2 Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 11 Jul 2025 08:41:27 +0530 Subject: [PATCH 1/4] feat(javascript): implement universal logging utility with customizable configuration and log levels --- docs/developer/LOGGER.md | 150 ++++++ packages/javascript/src/index.ts | 15 + .../src/utils/__tests__/logger.test.ts | 153 +++++++ packages/javascript/src/utils/logger.ts | 426 ++++++++++++++++++ 4 files changed, 744 insertions(+) create mode 100644 docs/developer/LOGGER.md create mode 100644 packages/javascript/src/utils/__tests__/logger.test.ts create mode 100644 packages/javascript/src/utils/logger.ts diff --git a/docs/developer/LOGGER.md b/docs/developer/LOGGER.md new file mode 100644 index 000000000..10db86f24 --- /dev/null +++ b/docs/developer/LOGGER.md @@ -0,0 +1,150 @@ +# Logger Utility + +A universal logging utility that works seamlessly in both browser and Node.js environments with beautiful formatting, colors, and flexible configuration. + +## Features + +- 🌐 **Universal**: Works in both browser and Node.js environments +- 🎨 **Beautiful Output**: Colored terminal output (Node.js) and styled console output (browser) +- � **Package-Aware**: Support for package-specific and component-specific loggers +- �📊 **Log Levels**: DEBUG, INFO, WARN, ERROR, SILENT +- 🏷️ **Prefixes**: Customizable prefixes with support for nested component loggers +- ⏰ **Timestamps**: Optional timestamp inclusion +- 🔧 **Configurable**: Flexible configuration options +- 🎯 **Type Safe**: Full TypeScript support + +## Basic Usage + +```typescript +import logger, { LogLevel } from '@asgardeo/javascript'; + +// Basic logging with package-specific prefix +logger.info('Application started'); +// Output: 🛡️ Asgardeo - @asgardeo/javascript [INFO] Application started + +logger.warn('This is a warning'); +logger.error('Something went wrong'); +logger.debug('Debug information'); // Only shown if level is DEBUG + +// Configure log level +logger.setLevel(LogLevel.DEBUG); +logger.debug('Now debug messages will show'); + +// Set to production level +logger.setLevel(LogLevel.WARN); // Only WARN and ERROR will show +``` + +## Named Function Exports + +```typescript +import { info, warn, error, debug } from '@asgardeo/javascript'; + +info('Quick info message'); +warn('Quick warning'); +error('Quick error'); +debug('Quick debug'); +``` + +## Package-Specific Loggers + +```typescript +import { createPackageLogger, createPackageComponentLogger } from '@asgardeo/javascript'; + +// Create logger for specific package +const nextjsLogger = createPackageLogger('@asgardeo/nextjs'); +nextjsLogger.info('Next.js package message'); +// Output: 🛡️ Asgardeo - @asgardeo/nextjs [INFO] Next.js package message + +const reactLogger = createPackageLogger('@asgardeo/react'); +reactLogger.error('React package error'); +// Output: 🛡️ Asgardeo - @asgardeo/react [ERROR] React package error + +// Create logger for package + component +const nextAuthLogger = createPackageComponentLogger('@asgardeo/nextjs', 'Authentication'); +nextAuthLogger.info('User signed in'); +// Output: 🛡️ Asgardeo - @asgardeo/nextjs - Authentication [INFO] User signed in +``` + +## Custom Logger Configuration + +```typescript +import { createLogger, LogLevel } from '@asgardeo/javascript'; + +const customLogger = createLogger({ + level: LogLevel.DEBUG, + prefix: '🛡️ Asgardeo - MyCustomApp', + timestamps: true, + showLevel: true, +}); + +customLogger.info('Custom configured message'); +// Output: [2025-01-10T10:30:45.123Z] 🛡️ Asgardeo - MyCustomApp [INFO] Custom configured message +``` + +## Component-Specific Loggers + +```typescript +import { createComponentLogger } from '@asgardeo/javascript'; + +// Create loggers for different components (uses default package prefix) +const authLogger = createComponentLogger('Authentication'); +const apiLogger = createComponentLogger('API'); +const uiLogger = createComponentLogger('UI'); + +authLogger.info('User signed in successfully'); +// Output: 🛡️ Asgardeo - @asgardeo/javascript - Authentication [INFO] User signed in successfully + +apiLogger.error('API request failed'); +// Output: 🛡️ Asgardeo - @asgardeo/javascript - API [ERROR] API request failed + +uiLogger.debug('Rendering component'); +// Output: 🛡️ Asgardeo - @asgardeo/javascript - UI [DEBUG] Rendering component +``` + +## Real-World Package Examples + +### Next.js Package Usage + +```typescript +import { createPackageComponentLogger } from '@asgardeo/javascript'; + +const authLogger = createPackageComponentLogger('@asgardeo/nextjs', 'Authentication'); +const sessionLogger = createPackageComponentLogger('@asgardeo/nextjs', 'SessionManager'); + +export class NextAuthService { + async signIn(credentials: Credentials): Promise { + authLogger.info('Starting Next.js sign-in process', { username: credentials.username }); + + try { + sessionLogger.debug('Creating session cookie'); + const user = await this.performSignIn(credentials); + authLogger.info('Next.js sign-in successful', { userId: user.id }); + return user; + } catch (error) { + authLogger.error('Next.js sign-in failed', { error: error.message, username: credentials.username }); + throw error; + } + } +} +``` + +### Multi-Package Application + +```typescript +// In @asgardeo/nextjs package +import { createPackageComponentLogger } from '@asgardeo/javascript'; +const nextLogger = createPackageComponentLogger('@asgardeo/nextjs', 'Provider'); + +// In @asgardeo/react package +import { createPackageComponentLogger } from '@asgardeo/javascript'; +const reactLogger = createPackageComponentLogger('@asgardeo/react', 'Hook'); + +// In @asgardeo/node package +import { createPackageComponentLogger } from '@asgardeo/javascript'; +const nodeLogger = createPackageComponentLogger('@asgardeo/node', 'Client'); + +// Each will have distinct, identifiable output: +// 🛡️ Asgardeo - @asgardeo/nextjs - Provider [INFO] ... +// 🛡️ Asgardeo - @asgardeo/react - Hook [INFO] ... +// 🛡️ Asgardeo - @asgardeo/node - Client [INFO] ... +``` diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 09bd1fb5b..c01d490f2 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -132,4 +132,19 @@ export {default as processOpenIDScopes} from './utils/processOpenIDScopes'; export {default as withVendorCSSClassPrefix} from './utils/withVendorCSSClassPrefix'; export {default as transformBrandingPreferenceToTheme} from './utils/transformBrandingPreferenceToTheme'; +export { + default as logger, + createLogger, + createComponentLogger, + createPackageLogger, + createPackageComponentLogger, + LogLevel, + configure as configureLogger, + debug, + info, + warn, + error, +} from './utils/logger'; +export type {LoggerConfig} from './utils/logger'; + export {default as StorageManager} from './StorageManager'; diff --git a/packages/javascript/src/utils/__tests__/logger.test.ts b/packages/javascript/src/utils/__tests__/logger.test.ts new file mode 100644 index 000000000..0f558d519 --- /dev/null +++ b/packages/javascript/src/utils/__tests__/logger.test.ts @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import logger, {createLogger, createComponentLogger, LogLevel} from '../logger'; + +describe('Logger', () => { + beforeEach(() => { + // Reset console methods before each test + jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'info').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(console, 'debug').mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('Basic logging', () => { + it('should log info messages', () => { + logger.info('Test info message'); + expect(console.info).toHaveBeenCalled(); + }); + + it('should log warning messages', () => { + logger.warn('Test warning message'); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should log error messages', () => { + logger.error('Test error message'); + expect(console.error).toHaveBeenCalled(); + }); + + it('should log debug messages when level is DEBUG', () => { + logger.setLevel(LogLevel.DEBUG); + logger.debug('Test debug message'); + expect(console.debug).toHaveBeenCalled(); + }); + + it('should not log debug messages when level is INFO', () => { + logger.setLevel(LogLevel.INFO); + logger.debug('Test debug message'); + expect(console.debug).not.toHaveBeenCalled(); + }); + }); + + describe('Log levels', () => { + it('should respect log level filtering', () => { + logger.setLevel(LogLevel.WARN); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warning message'); + logger.error('Error message'); + + expect(console.debug).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); + + it('should silence all logs when level is SILENT', () => { + logger.setLevel(LogLevel.SILENT); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warning message'); + logger.error('Error message'); + + expect(console.debug).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); + }); + }); + + describe('Custom loggers', () => { + it('should create logger with custom configuration', () => { + const customLogger = createLogger({ + level: LogLevel.DEBUG, + prefix: 'Custom', + timestamps: false, + showLevel: false, + }); + + expect(customLogger.getLevel()).toBe(LogLevel.DEBUG); + expect(customLogger.getConfig().prefix).toBe('Custom'); + expect(customLogger.getConfig().timestamps).toBe(false); + expect(customLogger.getConfig().showLevel).toBe(false); + }); + + it('should create component logger with nested prefix', () => { + const componentLogger = createComponentLogger('Authentication'); + + componentLogger.info('Test message'); + + expect(console.info).toHaveBeenCalled(); + // The exact format depends on environment detection + }); + + it('should create child logger', () => { + const parentLogger = createLogger({prefix: 'Parent'}); + const childLogger = parentLogger.child('Child'); + + expect(childLogger.getConfig().prefix).toBe('Parent - Child'); + }); + }); + + describe('Configuration', () => { + it('should update configuration', () => { + const testLogger = createLogger({level: LogLevel.INFO}); + + testLogger.configure({ + level: LogLevel.DEBUG, + prefix: 'Updated', + }); + + expect(testLogger.getLevel()).toBe(LogLevel.DEBUG); + expect(testLogger.getConfig().prefix).toBe('Updated'); + }); + }); + + describe('Custom formatter', () => { + it('should use custom formatter when provided', () => { + const mockFormatter = jest.fn(); + const customLogger = createLogger({ + formatter: mockFormatter, + }); + + customLogger.info('Test message', {data: 'test'}); + + expect(mockFormatter).toHaveBeenCalledWith(LogLevel.INFO, 'Test message', {data: 'test'}); + expect(console.info).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/javascript/src/utils/logger.ts b/packages/javascript/src/utils/logger.ts new file mode 100644 index 000000000..e77393d12 --- /dev/null +++ b/packages/javascript/src/utils/logger.ts @@ -0,0 +1,426 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Log levels enum + */ +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + SILENT = 4, +} + +/** + * Logger configuration interface + */ +export interface LoggerConfig { + /** Minimum log level to output */ + level: LogLevel; + /** Custom prefix for all log messages */ + prefix?: string; + /** Whether to include timestamps */ + timestamps?: boolean; + /** Whether to include log level in output */ + showLevel?: boolean; + /** Custom log formatter function */ + formatter?: (level: LogLevel, message: string, ...args: any[]) => void; +} + +const PREFIX: string = '🛡️ Asgardeo'; + +/** + * Default logger configuration + */ +const DEFAULT_CONFIG: LoggerConfig = { + level: LogLevel.INFO, + prefix: `${PREFIX}`, + timestamps: true, + showLevel: true, +}; + +/** + * Environment detection utilities + */ +const isBrowser = (): boolean => { + /* @ts-ignore */ + return typeof window !== 'undefined' && typeof window.document !== 'undefined'; +}; + +const isNode = (): boolean => { + /* @ts-ignore */ + return typeof process !== 'undefined' && process.versions && process.versions.node; +}; + +/** + * Color codes for terminal output (Node.js) + */ +const COLORS = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +/** + * Browser console styling + */ +const BROWSER_STYLES = { + debug: 'color: #6b7280; font-weight: normal;', + info: 'color: #2563eb; font-weight: bold;', + warn: 'color: #d97706; font-weight: bold;', + error: 'color: #dc2626; font-weight: bold;', + prefix: 'color: #7c3aed; font-weight: bold;', + timestamp: 'color: #6b7280; font-size: 0.9em;', +}; + +/** + * Universal logger class that works in both browser and Node.js environments + */ +class Logger { + private config: LoggerConfig; + + constructor(config: Partial = {}) { + this.config = {...DEFAULT_CONFIG, ...config}; + } + + /** + * Update logger configuration + */ + configure(config: Partial): void { + this.config = {...this.config, ...config}; + } + + /** + * Get current configuration + */ + getConfig(): LoggerConfig { + return {...this.config}; + } + + /** + * Check if a log level should be output + */ + private shouldLog(level: LogLevel): boolean { + return level >= this.config.level; + } + + /** + * Get timestamp string + */ + private getTimestamp(): string { + return new Date().toISOString(); + } + + /** + * Get log level string + */ + private getLevelString(level: LogLevel): string { + switch (level) { + case LogLevel.DEBUG: + return 'DEBUG'; + case LogLevel.INFO: + return 'INFO'; + case LogLevel.WARN: + return 'WARN'; + case LogLevel.ERROR: + return 'ERROR'; + default: + return 'UNKNOWN'; + } + } + + /** + * Format message for Node.js terminal + */ + private formatForNode(level: LogLevel, message: string): string { + const parts: string[] = []; + + // Add timestamp + if (this.config.timestamps) { + parts.push(`${COLORS.gray}[${this.getTimestamp()}]${COLORS.reset}`); + } + + // Add prefix + if (this.config.prefix) { + parts.push(`${COLORS.magenta}${this.config.prefix}${COLORS.reset}`); + } + + // Add log level + if (this.config.showLevel) { + const levelStr = this.getLevelString(level); + let coloredLevel: string; + + switch (level) { + case LogLevel.DEBUG: + coloredLevel = `${COLORS.gray}[${levelStr}]${COLORS.reset}`; + break; + case LogLevel.INFO: + coloredLevel = `${COLORS.blue}[${levelStr}]${COLORS.reset}`; + break; + case LogLevel.WARN: + coloredLevel = `${COLORS.yellow}[${levelStr}]${COLORS.reset}`; + break; + case LogLevel.ERROR: + coloredLevel = `${COLORS.red}[${levelStr}]${COLORS.reset}`; + break; + default: + coloredLevel = `[${levelStr}]`; + } + parts.push(coloredLevel); + } + + // Add message + parts.push(message); + + return parts.join(' '); + } + + /** + * Log message using appropriate method + */ + private logMessage(level: LogLevel, message: string, ...args: any[]): void { + if (!this.shouldLog(level)) { + return; + } + + // Use custom formatter if provided + if (this.config.formatter) { + this.config.formatter(level, message, ...args); + return; + } + + if (isBrowser()) { + this.logToBrowser(level, message, ...args); + } else if (isNode()) { + this.logToNode(level, message, ...args); + } else { + // Fallback for other environments + console.log(message, ...args); + } + } + + /** + * Log to browser console with styling + */ + private logToBrowser(level: LogLevel, message: string, ...args: any[]): void { + const parts: string[] = []; + const styles: string[] = []; + + // Add timestamp + if (this.config.timestamps) { + parts.push(`%c[${this.getTimestamp()}]`); + styles.push(BROWSER_STYLES.timestamp); + } + + // Add prefix + if (this.config.prefix) { + parts.push(`%c${this.config.prefix}`); + styles.push(BROWSER_STYLES.prefix); + } + + // Add log level and message + if (this.config.showLevel) { + const levelStr = this.getLevelString(level); + parts.push(`%c[${levelStr}]`); + + switch (level) { + case LogLevel.DEBUG: + styles.push(BROWSER_STYLES.debug); + break; + case LogLevel.INFO: + styles.push(BROWSER_STYLES.info); + break; + case LogLevel.WARN: + styles.push(BROWSER_STYLES.warn); + break; + case LogLevel.ERROR: + styles.push(BROWSER_STYLES.error); + break; + default: + styles.push(''); + } + } + + parts.push(`%c${message}`); + styles.push('color: inherit; font-weight: normal;'); + + const formattedMessage = parts.join(' '); + + // Use appropriate console method + switch (level) { + case LogLevel.DEBUG: + console.debug(formattedMessage, ...styles, ...args); + break; + case LogLevel.INFO: + console.info(formattedMessage, ...styles, ...args); + break; + case LogLevel.WARN: + console.warn(formattedMessage, ...styles, ...args); + break; + case LogLevel.ERROR: + console.error(formattedMessage, ...styles, ...args); + break; + default: + console.log(formattedMessage, ...styles, ...args); + } + } + + /** + * Log to Node.js console + */ + private logToNode(level: LogLevel, message: string, ...args: any[]): void { + const formattedMessage = this.formatForNode(level, message); + + // Use appropriate console method + switch (level) { + case LogLevel.DEBUG: + console.debug(formattedMessage, ...args); + break; + case LogLevel.INFO: + console.info(formattedMessage, ...args); + break; + case LogLevel.WARN: + console.warn(formattedMessage, ...args); + break; + case LogLevel.ERROR: + console.error(formattedMessage, ...args); + break; + default: + console.log(formattedMessage, ...args); + } + } + + /** + * Log debug message + */ + debug(message: string, ...args: any[]): void { + this.logMessage(LogLevel.DEBUG, message, ...args); + } + + /** + * Log info message + */ + info(message: string, ...args: any[]): void { + this.logMessage(LogLevel.INFO, message, ...args); + } + + /** + * Log warning message + */ + warn(message: string, ...args: any[]): void { + this.logMessage(LogLevel.WARN, message, ...args); + } + + /** + * Log error message + */ + error(message: string, ...args: any[]): void { + this.logMessage(LogLevel.ERROR, message, ...args); + } + + /** + * Create a child logger with additional prefix + */ + child(prefix: string): Logger { + const childPrefix = this.config.prefix ? `${this.config.prefix} - ${prefix}` : prefix; + return new Logger({ + ...this.config, + prefix: childPrefix, + }); + } + + /** + * Set log level + */ + setLevel(level: LogLevel): void { + this.config.level = level; + } + + /** + * Get current log level + */ + getLevel(): LogLevel { + return this.config.level; + } +} + +/** + * Default logger instance + */ +const logger = new Logger(); + +/** + * Create a new logger instance with custom configuration + */ +export const createLogger = (config?: Partial): Logger => { + return new Logger(config); +}; + +/** + * Default export - global logger instance + */ +export default logger; + +/** + * Named exports for convenience + */ +export const debug = (message: string, ...args: any[]) => logger.debug(message, ...args); +export const info = (message: string, ...args: any[]) => logger.info(message, ...args); +export const warn = (message: string, ...args: any[]) => logger.warn(message, ...args); +export const error = (message: string, ...args: any[]) => logger.error(message, ...args); + +/** + * Configure the default logger + */ +export const configure = (config: Partial) => logger.configure(config); + +/** + * Create component-specific loggers + */ +export const createComponentLogger = (component: string) => { + return logger.child(component); +}; + +/** + * Create package-specific logger + */ +export const createPackageLogger = (packageName: string) => { + return createLogger({ + prefix: `${PREFIX} - ${packageName}`, + level: LogLevel.INFO, + timestamps: true, + showLevel: true, + }); +}; + +/** + * Create package component logger (package + component) + */ +export const createPackageComponentLogger = (packageName: string, component: string) => { + const packageLogger = createPackageLogger(packageName); + return packageLogger.child(component); +}; From b35b78aedf17ce683ebbb3cd88553142c345004f Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 11 Jul 2025 11:29:47 +0530 Subject: [PATCH 2/4] fix(nextjs): fix session issues --- packages/nextjs/package.json | 3 +- packages/nextjs/src/AsgardeoNextClient.ts | 13 +- packages/nextjs/src/index.ts | 7 + .../src/middleware/asgardeoMiddleware.ts | 45 ++- .../nextjs/src/server/AsgardeoProvider.tsx | 48 ++- .../src/server/actions/getMyOrganizations.ts | 42 +- .../nextjs/src/server/actions/getSessionId.ts | 21 +- .../src/server/actions/getSessionPayload.ts | 46 +++ .../actions/handleOAuthCallbackAction.ts | 63 ++- .../nextjs/src/server/actions/isSignedIn.ts | 47 ++- .../nextjs/src/server/actions/signInAction.ts | 97 +++-- .../src/server/actions/signOutAction.ts | 27 +- packages/nextjs/src/utils/SessionManager.ts | 200 ++++++++++ packages/nextjs/src/utils/sessionUtils.ts | 94 ++++- pnpm-lock.yaml | 368 ++---------------- samples/teamspace-nextjs/package.json | 2 +- 16 files changed, 702 insertions(+), 421 deletions(-) create mode 100644 packages/nextjs/src/server/actions/getSessionPayload.ts create mode 100644 packages/nextjs/src/utils/SessionManager.ts diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 0aeb45634..b199495ed 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -46,6 +46,7 @@ "@asgardeo/node": "workspace:^", "@asgardeo/react": "workspace:^", "@types/react": "^19.1.4", + "jose": "^5.10.0", "tslib": "^2.8.1" }, "devDependencies": { @@ -57,8 +58,8 @@ "eslint": "8.57.0", "next": "^15.3.2", "prettier": "^2.6.2", - "rimraf": "^6.0.1", "react": "^19.1.0", + "rimraf": "^6.0.1", "typescript": "~5.7.2", "vitest": "^3.1.3" }, diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 5d083e601..0e2224b82 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -378,7 +378,18 @@ class AsgardeoNextClient exte } getAccessToken(sessionId?: string): Promise { - return this.asgardeo.getAccessToken(sessionId as string); + if (!sessionId) { + return Promise.reject(new Error('Session ID is required to get access token')); + } + + return this.asgardeo.getAccessToken(sessionId as string).then( + token => { + return token; + }, + error => { + throw error; + }, + ); } /** diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index d2f2bb635..4c0f9bec8 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -24,8 +24,15 @@ export * from './client/contexts/Asgardeo/useAsgardeo'; export {default as isSignedIn} from './server/actions/isSignedIn'; +export {default as getSessionId} from './server/actions/getSessionId'; + +export {default as getSessionPayload} from './server/actions/getSessionPayload'; + export {default as handleOAuthCallback} from './server/actions/handleOAuthCallbackAction'; +export {default as SessionManager} from './utils/SessionManager'; +export * from './utils/SessionManager'; + export {default as CreateOrganization} from './client/components/presentation/CreateOrganization/CreateOrganization'; export {CreateOrganizationProps} from './client/components/presentation/CreateOrganization/CreateOrganization'; diff --git a/packages/nextjs/src/middleware/asgardeoMiddleware.ts b/packages/nextjs/src/middleware/asgardeoMiddleware.ts index fdcc72b39..b20955d62 100644 --- a/packages/nextjs/src/middleware/asgardeoMiddleware.ts +++ b/packages/nextjs/src/middleware/asgardeoMiddleware.ts @@ -19,6 +19,12 @@ import {NextRequest, NextResponse} from 'next/server'; import {CookieConfig} from '@asgardeo/node'; import {AsgardeoNextConfig} from '../models/config'; +import SessionManager, {SessionTokenPayload} from '../utils/SessionManager'; +import { + hasValidSession as hasValidJWTSession, + getSessionFromRequest, + getSessionIdFromRequest, +} from '../utils/sessionUtils'; export type AsgardeoMiddlewareOptions = Partial; @@ -37,6 +43,8 @@ export type AsgardeoMiddlewareContext = { isSignedIn: () => boolean; /** Get the session ID from the current request */ getSessionId: () => string | undefined; + /** Get the session payload from JWT session if available */ + getSession: () => Promise; }; type AsgardeoMiddlewareHandler = ( @@ -45,25 +53,43 @@ type AsgardeoMiddlewareHandler = ( ) => Promise | NextResponse | void; /** - * Checks if a request has a valid session ID in cookies. + * Legacy function: Checks if a request has a valid session ID in cookies. * This is a lightweight check that can be used in middleware. * + * @deprecated Use hasValidJWTSession for JWT-based sessions * @param request - The Next.js request object * @returns True if a session ID exists, false otherwise */ -const hasValidSession = (request: NextRequest): boolean => { +const hasValidSessionLegacy = (request: NextRequest): boolean => { const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; return Boolean(sessionId && sessionId.trim().length > 0); }; +/** + * Enhanced session validation that checks both JWT and legacy sessions + * + * @param request - The Next.js request object + * @returns True if a valid session exists, false otherwise + */ +const hasValidSession = async (request: NextRequest): Promise => { + try { + // Try JWT session first + return await hasValidJWTSession(request); + } catch { + // Fall back to legacy session check + return hasValidSessionLegacy(request); + } +}; + /** * Gets the session ID from the request cookies. + * Supports both JWT and legacy session formats. * * @param request - The Next.js request object * @returns The session ID if it exists, undefined otherwise */ -const getSessionIdFromRequest = (request: NextRequest): string | undefined => { - return request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; +const getSessionIdFromRequestMiddleware = async (request: NextRequest): Promise => { + return await getSessionIdFromRequest(request); }; /** @@ -129,8 +155,8 @@ const asgardeoMiddleware = ( return async (request: NextRequest): Promise => { const resolvedOptions = typeof options === 'function' ? options(request) : options || {}; - const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; - const isAuthenticated = hasValidSession(request); + const sessionId = await getSessionIdFromRequestMiddleware(request); + const isAuthenticated = await hasValidSession(request); const asgardeo: AsgardeoMiddlewareContext = { protectRoute: async (options?: {redirect?: string}): Promise => { @@ -166,6 +192,13 @@ const asgardeoMiddleware = ( }, isSignedIn: () => isAuthenticated, getSessionId: () => sessionId, + getSession: async () => { + try { + return await getSessionFromRequest(request); + } catch { + return undefined; + } + }, }; if (handler) { diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 409e31557..9d5235ec0 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -27,6 +27,7 @@ import getBrandingPreference from './actions/getBrandingPreference'; import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction'; import getMyOrganizations from './actions/getMyOrganizations'; import getSessionId from './actions/getSessionId'; +import getSessionPayload from './actions/getSessionPayload'; import getUserAction from './actions/getUserAction'; import getUserProfileAction from './actions/getUserProfileAction'; import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction'; @@ -88,8 +89,10 @@ const AsgardeoServerProvider: FC> return <>; } - const sessionId: string = (await getSessionId()) as string; - const _isSignedIn: boolean = await isSignedIn(sessionId); + // Try to get session information from JWT first, then fall back to legacy + const sessionPayload = await getSessionPayload(); + const sessionId: string = sessionPayload?.sessionId || (await getSessionId()) || ''; + const _isSignedIn: boolean = sessionPayload ? true : await isSignedIn(sessionId); let user: User = {}; let userProfile: UserProfile = { @@ -106,24 +109,43 @@ const AsgardeoServerProvider: FC> let brandingPreference: BrandingPreference | null = null; if (_isSignedIn) { - // Check if there's a `user_org` claim in the ID token to determine if this is an organization login - const idToken = await asgardeoClient.getDecodedIdToken(sessionId); let updatedBaseUrl = config?.baseUrl; - if (idToken?.['user_org']) { - // Treat this login as an organization login and modify the base URL + if (sessionPayload?.organizationId) { updatedBaseUrl = `${config?.baseUrl}/o`; config = {...config, baseUrl: updatedBaseUrl}; + } else if (sessionId) { + try { + const idToken = await asgardeoClient.getDecodedIdToken(sessionId); + if (idToken?.['user_org']) { + updatedBaseUrl = `${config?.baseUrl}/o`; + config = {...config, baseUrl: updatedBaseUrl}; + } + } catch { + // Continue without organization info + } } - const userResponse = await getUserAction(sessionId); - const userProfileResponse = await getUserProfileAction(sessionId); - const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId); - myOrganizations = await getMyOrganizations({}, sessionId); + try { + const userResponse = await getUserAction(sessionId); + const userProfileResponse = await getUserProfileAction(sessionId); + const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId); + + if (sessionId) { + myOrganizations = await getMyOrganizations({}, sessionId); + } else { + console.warn('[AsgardeoServerProvider] No session ID available, skipping organization fetch'); + } - user = userResponse.data?.user || {}; - userProfile = userProfileResponse.data?.userProfile; - currentOrganization = currentOrganizationResponse?.data?.organization as Organization; + user = userResponse.data?.user || {}; + userProfile = userProfileResponse.data?.userProfile; + currentOrganization = currentOrganizationResponse?.data?.organization as Organization; + } catch (error) { + user = {}; + userProfile = {schemas: [], profile: {}, flattenedProfile: {}}; + currentOrganization = {id: '', name: '', orgHandle: ''}; + myOrganizations = []; + } } // Fetch branding preference if branding is enabled in config diff --git a/packages/nextjs/src/server/actions/getMyOrganizations.ts b/packages/nextjs/src/server/actions/getMyOrganizations.ts index d63aa7115..f7d9b123c 100644 --- a/packages/nextjs/src/server/actions/getMyOrganizations.ts +++ b/packages/nextjs/src/server/actions/getMyOrganizations.ts @@ -27,7 +27,47 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; const getMyOrganizations = async (options?: any, sessionId?: string | undefined): Promise => { try { const client = AsgardeoNextClient.getInstance(); - return await client.getMyOrganizations(options, sessionId); + + // Get session ID if not provided + let resolvedSessionId = sessionId; + if (!resolvedSessionId) { + // Import getSessionId locally to avoid circular dependencies + const {default: getSessionId} = await import('./getSessionId'); + resolvedSessionId = await getSessionId(); + } + + if (!resolvedSessionId) { + throw new AsgardeoAPIError( + 'No session ID available for fetching organizations', + 'getMyOrganizations-SessionError-001', + 'nextjs', + 401, + ); + } + + // Check if user is signed in by trying to get access token + try { + const accessToken = await client.getAccessToken(resolvedSessionId); + + if (!accessToken) { + throw new AsgardeoAPIError( + 'No access token available - user is not signed in', + 'getMyOrganizations-NoAccessToken-001', + 'nextjs', + 401, + ); + } + } catch (error) { + console.error('[getMyOrganizations] Failed to get access token:', error); + throw new AsgardeoAPIError( + 'User is not signed in - access token retrieval failed', + 'getMyOrganizations-NotSignedIn-001', + 'nextjs', + 401, + ); + } + + return await client.getMyOrganizations(options, resolvedSessionId); } catch (error) { throw new AsgardeoAPIError( `Failed to get the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, diff --git a/packages/nextjs/src/server/actions/getSessionId.ts b/packages/nextjs/src/server/actions/getSessionId.ts index 3d21dc49f..d1f7da563 100644 --- a/packages/nextjs/src/server/actions/getSessionId.ts +++ b/packages/nextjs/src/server/actions/getSessionId.ts @@ -21,11 +21,30 @@ import {CookieConfig} from '@asgardeo/node'; import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; import {cookies} from 'next/headers'; +import SessionManager from '../../utils/SessionManager'; +/** + * Get the session ID from cookies. + * Tries JWT session first, then falls back to legacy session ID. + * + * @returns The session ID if it exists, undefined otherwise + */ const getSessionId = async (): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - return cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + const sessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + + if (sessionToken) { + try { + const sessionPayload = await SessionManager.verifySessionToken(sessionToken); + + return sessionPayload.sessionId; + } catch (error) { + return undefined; + } + } + + return undefined; }; export default getSessionId; diff --git a/packages/nextjs/src/server/actions/getSessionPayload.ts b/packages/nextjs/src/server/actions/getSessionPayload.ts new file mode 100644 index 000000000..108475bc6 --- /dev/null +++ b/packages/nextjs/src/server/actions/getSessionPayload.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use server'; + +import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; +import {cookies} from 'next/headers'; +import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; + +/** + * Get the session payload from JWT session cookie. + * This includes user ID, session ID, scopes, and organization ID. + * + * @returns The session payload if valid JWT session exists, undefined otherwise + */ +const getSessionPayload = async (): Promise => { + const cookieStore: ReadonlyRequestCookies = await cookies(); + + const sessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + if (!sessionToken) { + return undefined; + } + + try { + return await SessionManager.verifySessionToken(sessionToken); + } catch { + return undefined; + } +}; + +export default getSessionPayload; diff --git a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts index 10cea44f1..d85982ef7 100644 --- a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts +++ b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts @@ -18,9 +18,9 @@ 'use server'; -import { cookies } from 'next/headers'; -import { CookieConfig } from '@asgardeo/node'; +import {cookies} from 'next/headers'; import AsgardeoNextClient from '../../AsgardeoNextClient'; +import SessionManager from '../../utils/SessionManager'; /** * Server action to handle OAuth callback with authorization code. @@ -35,7 +35,7 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; const handleOAuthCallbackAction = async ( code: string, state: string, - sessionState?: string + sessionState?: string, ): Promise<{ success: boolean; error?: string; @@ -45,56 +45,87 @@ const handleOAuthCallbackAction = async ( if (!code || !state) { return { success: false, - error: 'Missing required OAuth parameters: code and state are required' + error: 'Missing required OAuth parameters: code and state are required', }; } - // Get the Asgardeo client instance const asgardeoClient = AsgardeoNextClient.getInstance(); if (!asgardeoClient.isInitialized) { return { success: false, - error: 'Asgardeo client is not initialized' + error: 'Asgardeo client is not initialized', }; } - // Get the session ID from cookies const cookieStore = await cookies(); - const sessionId = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + let sessionId: string | undefined; + + const tempSessionToken = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + + if (tempSessionToken) { + try { + const tempSession = await SessionManager.verifyTempSession(tempSessionToken); + sessionId = tempSession.sessionId; + } catch { + // TODO: Invalid temp session, throw error. + } + } if (!sessionId) { return { success: false, - error: 'No session found. Please start the authentication flow again.' + error: 'No session found. Please start the authentication flow again.', }; } // Exchange the authorization code for tokens - await asgardeoClient.signIn( + const signInResult = await asgardeoClient.signIn( { code, session_state: sessionState, state, } as any, {}, - sessionId + sessionId, ); - // Get the after sign-in URL from configuration + if (signInResult) { + try { + const idToken = await asgardeoClient.getDecodedIdToken(sessionId); + const userIdFromToken = idToken.sub || signInResult['sub'] || sessionId; + const scopes = idToken['scope'] ? idToken['scope'].split(' ') : []; + const organizationId = idToken['user_org'] || idToken['organization_id']; + + const sessionToken = await SessionManager.createSessionToken( + userIdFromToken, + sessionId, + scopes, + organizationId, + ); + + cookieStore.set(SessionManager.getSessionCookieName(), sessionToken, SessionManager.getSessionCookieOptions()); + + cookieStore.delete(SessionManager.getTempSessionCookieName()); + } catch (error) { + console.warn( + '[handleOAuthCallbackAction] Failed to create JWT session, continuing with legacy session:', + error, + ); + } + } + const config = await asgardeoClient.getConfiguration(); const afterSignInUrl = config.afterSignInUrl || '/'; return { success: true, - redirectUrl: afterSignInUrl + redirectUrl: afterSignInUrl, }; } catch (error) { - console.error('[handleOAuthCallbackAction] OAuth callback error:', error); - return { success: false, - error: error instanceof Error ? error.message : 'Authentication failed' + error: error instanceof Error ? error.message : 'Authentication failed', }; } }; diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index 746b6784b..adcd70114 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -20,12 +20,51 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; import getSessionId from './getSessionId'; +import getSessionPayload from './getSessionPayload'; -const isSignedIn = async (sessionId: string): Promise => { - const client = AsgardeoNextClient.getInstance(); - const accessToken: string | undefined = await client.getAccessToken(sessionId); +/** + * Check if the user is currently signed in. + * First tries JWT session validation, then falls back to legacy session check. + * + * @param sessionId - Optional session ID to check (if not provided, gets from cookies) + * @returns True if user is signed in, false otherwise + */ +const isSignedIn = async (sessionId?: string): Promise => { + try { + const sessionPayload = await getSessionPayload(); + + if (sessionPayload) { + const resolvedSessionId = sessionPayload.sessionId; + + if (resolvedSessionId) { + const client = AsgardeoNextClient.getInstance(); + try { + const accessToken = await client.getAccessToken(resolvedSessionId); + return !!accessToken; + } catch (error) { + return false; + } + } + } + + const resolvedSessionId = sessionId || (await getSessionId()); + + if (!resolvedSessionId) { + return false; + } + + const client = AsgardeoNextClient.getInstance(); + + try { + const accessToken = await client.getAccessToken(resolvedSessionId); - return !!accessToken; + return !!accessToken; + } catch (error) { + return false; + } + } catch { + return false; + } }; export default isSignedIn; diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index 4edc3da11..17a61dfa5 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -28,6 +28,7 @@ import { EmbeddedSignInFlowInitiateResponse, } from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; +import SessionManager from '../../utils/SessionManager'; /** * Server action for signing in a user. @@ -54,11 +55,46 @@ const signInAction = async ( const client = AsgardeoNextClient.getInstance(); const cookieStore = await cookies(); - let userId: string | undefined = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + let sessionId: string | undefined; + let userId: string | undefined; - if (!userId) { - userId = generateSessionId(); - cookieStore.set(CookieConfig.SESSION_COOKIE_NAME, userId, { + const existingSessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + + if (existingSessionToken) { + try { + const sessionPayload = await SessionManager.verifySessionToken(existingSessionToken); + sessionId = sessionPayload.sessionId; + userId = sessionPayload.sub; + } catch { + // Invalid session token, will create new temp session + } + } + + if (!sessionId) { + const tempSessionToken = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + + if (tempSessionToken) { + try { + const tempSession = await SessionManager.verifyTempSession(tempSessionToken); + sessionId = tempSession.sessionId; + } catch { + // Invalid temp session, will create new one + } + } + } + + if (!sessionId) { + sessionId = generateSessionId(); + + const tempSessionToken = await SessionManager.createTempSession(sessionId); + + cookieStore.set( + SessionManager.getTempSessionCookieName(), + tempSessionToken, + SessionManager.getTempSessionCookieOptions(), + ); + + cookieStore.set(CookieConfig.SESSION_COOKIE_NAME, sessionId, { httpOnly: CookieConfig.DEFAULT_HTTP_ONLY, maxAge: CookieConfig.DEFAULT_MAX_AGE, sameSite: CookieConfig.DEFAULT_SAME_SITE, @@ -67,33 +103,48 @@ const signInAction = async ( } // If no payload provided, redirect to sign-in URL for redirect-based sign-in. - // If there's a payload, handle the embedded sign-in flow. if (!payload) { - const defaultSignInUrl = await client.getAuthorizeRequestUrl({}, userId); - + const defaultSignInUrl = await client.getAuthorizeRequestUrl({}, sessionId); return {success: true, data: {signInUrl: String(defaultSignInUrl)}}; - } else { - const response: any = await client.signIn(payload, request!, userId); - - if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { - // Complete the sign-in process - await client.signIn( - { - code: response?.authData?.code, - session_state: response?.authData?.session_state, - state: response?.authData?.state, - } as any, - {}, - userId, + } + + // Handle embedded sign-in flow + const response: any = await client.signIn(payload, request!, sessionId); + + if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { + const signInResult = await client.signIn( + { + code: response?.authData?.code, + session_state: response?.authData?.session_state, + state: response?.authData?.state, + } as any, + {}, + sessionId, + ); + + if (signInResult) { + const idToken = await client.getDecodedIdToken(sessionId); + const userIdFromToken = idToken['sub'] || signInResult['sub'] || sessionId; + const scopes = idToken['scope'] ? idToken['scope'].split(' ') : []; + const organizationId = idToken['user_org'] || idToken['organization_id']; + + const sessionToken = await SessionManager.createSessionToken( + userIdFromToken, + sessionId, + scopes, + organizationId, ); - const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + cookieStore.set(SessionManager.getSessionCookieName(), sessionToken, SessionManager.getSessionCookieOptions()); - return {success: true, data: {afterSignInUrl: String(afterSignInUrl)}}; + cookieStore.delete(SessionManager.getTempSessionCookieName()); } - return {success: true, data: response as EmbeddedSignInFlowInitiateResponse}; + const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + return {success: true, data: {afterSignInUrl: String(afterSignInUrl)}}; } + + return {success: true, data: response as EmbeddedSignInFlowInitiateResponse}; } catch (error) { console.error('[signInAction] Error during sign-in:', error); return {success: false, error: String(error)}; diff --git a/packages/nextjs/src/server/actions/signOutAction.ts b/packages/nextjs/src/server/actions/signOutAction.ts index d49a20f13..1d319a4c5 100644 --- a/packages/nextjs/src/server/actions/signOutAction.ts +++ b/packages/nextjs/src/server/actions/signOutAction.ts @@ -18,16 +18,35 @@ 'use server'; -import {NextRequest, NextResponse} from 'next/server'; +import {cookies} from 'next/headers'; import AsgardeoNextClient from '../../AsgardeoNextClient'; -import deleteSessionId from './deleteSessionId'; +import SessionManager from '../../utils/SessionManager'; +import getSessionId from './getSessionId'; +/** + * Server action for signing out a user. + * Clears both JWT and legacy session cookies. + * + * @returns Promise that resolves with success status and optional after sign-out URL + */ const signOutAction = async (): Promise<{success: boolean; data?: {afterSignOutUrl?: string}; error?: unknown}> => { try { const client = AsgardeoNextClient.getInstance(); - const afterSignOutUrl: string = await client.signOut(); + const sessionId = await getSessionId(); + + let afterSignOutUrl: string = '/'; + + if (sessionId) { + afterSignOutUrl = await client.signOut({}, sessionId); + } + + const cookieStore = await cookies(); + + cookieStore.delete(SessionManager.getSessionCookieName()); + + cookieStore.delete(SessionManager.getTempSessionCookieName()); - await deleteSessionId(); + await import('./deleteSessionId').then(module => module.default()); return {success: true, data: {afterSignOutUrl}}; } catch (error) { diff --git a/packages/nextjs/src/utils/SessionManager.ts b/packages/nextjs/src/utils/SessionManager.ts new file mode 100644 index 000000000..d9909ff2b --- /dev/null +++ b/packages/nextjs/src/utils/SessionManager.ts @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {SignJWT, jwtVerify, JWTPayload} from 'jose'; +import {AsgardeoRuntimeError} from '@asgardeo/node'; + +/** + * Session token payload interface + */ +export interface SessionTokenPayload extends JWTPayload { + /** User ID */ + sub: string; + /** Session ID */ + sessionId: string; + /** OAuth scopes */ + scopes: string[]; + /** Organization ID if applicable */ + organizationId?: string; + /** Issued at timestamp */ + iat: number; + /** Expiration timestamp */ + exp: number; +} + +/** + * Session management utility class for JWT-based session cookies + */ +class SessionManager { + private static readonly SESSION_COOKIE_NAME = 'asgardeo_session'; + private static readonly TEMP_SESSION_COOKIE_NAME = 'asgardeo_temp_session'; + private static readonly DEFAULT_EXPIRY_SECONDS = 3600; // 1 hour + + /** + * Get the signing secret from environment variable + * Throws error in production if not set + */ + private static getSecret(): Uint8Array { + const secret = process.env['ASGARDEO_SECRET']; + + if (!secret) { + if (process.env['NODE_ENV'] === 'production') { + throw new AsgardeoRuntimeError( + 'ASGARDEO_SECRET environment variable is required in production', + 'session-secret-required', + 'nextjs', + 'Set the ASGARDEO_SECRET environment variable with a secure random string', + ); + } + // Use a default secret for development (not secure) + console.warn('⚠️ Using default secret for development. Set ASGARDEO_SECRET for production!'); + return new TextEncoder().encode('development-secret-not-for-production'); + } + + return new TextEncoder().encode(secret); + } + + /** + * Create a temporary session cookie for login initiation + */ + static async createTempSession(sessionId: string): Promise { + const secret = this.getSecret(); + + const jwt = await new SignJWT({ + sessionId, + type: 'temp', + }) + .setProtectedHeader({alg: 'HS256'}) + .setIssuedAt() + .setExpirationTime('15m') // Temporary sessions expire in 15 minutes + .sign(secret); + + return jwt; + } + + /** + * Create a session cookie with user information + */ + static async createSessionToken( + userId: string, + sessionId: string, + scopes: string[], + organizationId?: string, + expirySeconds: number = this.DEFAULT_EXPIRY_SECONDS, + ): Promise { + const secret = this.getSecret(); + + const jwt = await new SignJWT({ + sessionId, + scopes, + organizationId, + type: 'session', + } as Omit) + .setProtectedHeader({alg: 'HS256'}) + .setSubject(userId) + .setIssuedAt() + .setExpirationTime(Date.now() / 1000 + expirySeconds) + .sign(secret); + + return jwt; + } + + /** + * Verify and decode a session token + */ + static async verifySessionToken(token: string): Promise { + try { + const secret = this.getSecret(); + const {payload} = await jwtVerify(token, secret); + + return payload as SessionTokenPayload; + } catch (error) { + throw new AsgardeoRuntimeError( + `Invalid session token: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'invalid-session-token', + 'nextjs', + 'Session token verification failed', + ); + } + } + + /** + * Verify and decode a temporary session token + */ + static async verifyTempSession(token: string): Promise<{sessionId: string}> { + try { + const secret = this.getSecret(); + const {payload} = await jwtVerify(token, secret); + + if (payload['type'] !== 'temp') { + throw new Error('Invalid token type'); + } + + return {sessionId: payload['sessionId'] as string}; + } catch (error) { + throw new AsgardeoRuntimeError( + `Invalid temporary session token: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'invalid-temp-session-token', + 'nextjs', + 'Temporary session token verification failed', + ); + } + } + + /** + * Get session cookie options + */ + static getSessionCookieOptions() { + return { + httpOnly: true, + secure: process.env['NODE_ENV'] === 'production', + sameSite: 'lax' as const, + path: '/', + maxAge: this.DEFAULT_EXPIRY_SECONDS, + }; + } + + /** + * Get temporary session cookie options + */ + static getTempSessionCookieOptions() { + return { + httpOnly: true, + secure: process.env['NODE_ENV'] === 'production', + sameSite: 'lax' as const, + path: '/', + maxAge: 15 * 60, // 15 minutes + }; + } + + /** + * Get session cookie name + */ + static getSessionCookieName(): string { + return this.SESSION_COOKIE_NAME; + } + + /** + * Get temporary session cookie name + */ + static getTempSessionCookieName(): string { + return this.TEMP_SESSION_COOKIE_NAME; + } +} + +export default SessionManager; diff --git a/packages/nextjs/src/utils/sessionUtils.ts b/packages/nextjs/src/utils/sessionUtils.ts index aff97f681..f61813091 100644 --- a/packages/nextjs/src/utils/sessionUtils.ts +++ b/packages/nextjs/src/utils/sessionUtils.ts @@ -17,26 +17,102 @@ */ import {NextRequest} from 'next/server'; +import SessionManager, {SessionTokenPayload} from './SessionManager'; import {CookieConfig} from '@asgardeo/node'; /** - * Checks if a request has a valid session ID in cookies. - * This is a lightweight check that can be used in middleware. + * Checks if a request has a valid session cookie (JWT). + * This verifies the JWT signature and expiration. * * @param request - The Next.js request object - * @returns True if a session ID exists, false otherwise + * @returns True if a valid session exists, false otherwise */ -export const hasValidSession = (request: NextRequest): boolean => { - const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; - return Boolean(sessionId && sessionId.trim().length > 0); +export const hasValidSession = async (request: NextRequest): Promise => { + try { + const sessionToken = request.cookies.get(SessionManager.getSessionCookieName())?.value; + if (!sessionToken) { + return false; + } + + await SessionManager.verifySessionToken(sessionToken); + return true; + } catch { + return false; + } }; /** - * Gets the session ID from the request cookies. + * Gets the session payload from the request cookies. + * This includes user ID, session ID, and scopes. + * + * @param request - The Next.js request object + * @returns The session payload if valid, undefined otherwise + */ +export const getSessionFromRequest = async (request: NextRequest): Promise => { + try { + const sessionToken = request.cookies.get(SessionManager.getSessionCookieName())?.value; + if (!sessionToken) { + return undefined; + } + + return await SessionManager.verifySessionToken(sessionToken); + } catch { + return undefined; + } +}; + +/** + * Gets the session ID from the request cookies (legacy support). + * First tries to get from JWT session, then falls back to legacy session ID cookie. * * @param request - The Next.js request object * @returns The session ID if it exists, undefined otherwise */ -export const getSessionIdFromRequest = (request: NextRequest): string | undefined => { - return request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; +export const getSessionIdFromRequest = async (request: NextRequest): Promise => { + try { + // Try JWT session first + const sessionPayload = await getSessionFromRequest(request); + if (sessionPayload) { + return sessionPayload.sessionId; + } + + // Fall back to legacy session ID cookie for backward compatibility + return request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + } catch { + // Fall back to legacy session ID cookie + return request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + } +}; + +/** + * Gets the temporary session ID from request cookies. + * + * @param request - The Next.js request object + * @returns The temporary session ID if valid, undefined otherwise + */ +export const getTempSessionFromRequest = async (request: NextRequest): Promise => { + try { + const tempToken = request.cookies.get(SessionManager.getTempSessionCookieName())?.value; + if (!tempToken) { + return undefined; + } + + const tempSession = await SessionManager.verifyTempSession(tempToken); + return tempSession.sessionId; + } catch { + return undefined; + } +}; + +/** + * Legacy function for backward compatibility. + * Checks if a request has a valid session ID in cookies. + * + * @deprecated Use hasValidSession instead for JWT-based sessions + * @param request - The Next.js request object + * @returns True if a session ID exists, false otherwise + */ +export const hasValidSessionLegacy = (request: NextRequest): boolean => { + const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + return Boolean(sessionId && sessionId.trim().length > 0); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e87d82d9..051ebeae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,6 +218,9 @@ importers: '@types/react': specifier: ^19.1.4 version: 19.1.5 + jose: + specifier: ^5.10.0 + version: 5.10.0 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -242,7 +245,7 @@ importers: version: 8.57.0 next: specifier: ^15.3.2 - version: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.0) + version: 15.3.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.0) prettier: specifier: ^2.6.2 version: 2.8.8 @@ -545,46 +548,6 @@ importers: specifier: ^2.2.2 version: 2.2.10(typescript@5.1.6) - samples/asgardeo-react: - dependencies: - '@asgardeo/react': - specifier: workspace:^ - version: link:../../packages/react - react: - specifier: ^19.1.0 - version: 19.1.0 - react-dom: - specifier: ^19.1.0 - version: 19.1.0(react@19.1.0) - devDependencies: - '@eslint/js': - specifier: ^9.29.0 - version: 9.30.1 - '@types/react': - specifier: ^19.1.8 - version: 19.1.8 - '@types/react-dom': - specifier: ^19.1.6 - version: 19.1.6(@types/react@19.1.8) - '@vitejs/plugin-react': - specifier: ^4.5.2 - version: 4.6.0(vite@7.0.2(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0)) - eslint: - specifier: ^9.29.0 - version: 9.30.1(jiti@2.4.2) - eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.30.1(jiti@2.4.2)) - eslint-plugin-react-refresh: - specifier: ^0.4.20 - version: 0.4.20(eslint@9.30.1(jiti@2.4.2)) - globals: - specifier: ^16.2.0 - version: 16.3.0 - vite: - specifier: ^7.0.0 - version: 7.0.2(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0) - samples/teamspace-react: dependencies: '@asgardeo/react': @@ -772,10 +735,6 @@ packages: resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} - engines: {node: '>=6.9.0'} - '@babel/eslint-parser@7.27.1': resolution: {integrity: sha512-q8rjOuadH0V6Zo4XLMkJ3RMQ9MSBqwaDByyYB0izsYdaIWGNLmEblbCOf1vyFHICcg16CD7Fsi51vcQnYxmt6Q==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} @@ -787,18 +746,10 @@ packages: resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': - resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -809,12 +760,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} @@ -835,20 +780,11 @@ packages: resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.27.2': resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -873,18 +809,10 @@ packages: resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.0': - resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} - engines: {node: '>=6.9.0'} - '@babel/types@7.27.1': resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.0': - resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} - engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1335,18 +1263,10 @@ packages: resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.3.0': - resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1367,10 +1287,6 @@ packages: resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.30.1': - resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1881,9 +1797,6 @@ packages: '@types/react': optional: true - '@rolldown/pluginutils@1.0.0-beta.19': - resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} - '@rolldown/pluginutils@1.0.0-beta.9': resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} @@ -2264,20 +2177,12 @@ packages: peerDependencies: '@types/react': ^19.0.0 - '@types/react-dom@19.1.6': - resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} - peerDependencies: - '@types/react': ^19.0.0 - '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} '@types/react@19.1.5': resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} - '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2486,12 +2391,6 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitejs/plugin-react@4.6.0': - resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3809,16 +3708,6 @@ packages: jiti: optional: true - eslint@9.30.1: - resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - espree@10.3.0: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4143,10 +4032,6 @@ packages: resolution: {integrity: sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==} engines: {node: '>=18'} - globals@16.3.0: - resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} - engines: {node: '>=18'} - globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -6948,26 +6833,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.28.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 - convert-source-map: 2.0.0 - debug: 4.4.1 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/eslint-parser@7.27.1(@babel/core@7.27.1)(eslint@8.57.0)': dependencies: '@babel/core': 7.27.1 @@ -6984,14 +6849,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/generator@7.28.0': - dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.27.2 @@ -7000,8 +6857,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.27.1 @@ -7018,15 +6873,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-plugin-utils@7.27.1': {} '@babel/helper-string-parser@7.27.1': {} @@ -7040,39 +6886,20 @@ snapshots: '@babel/template': 7.27.2 '@babel/types': 7.27.1 - '@babel/helpers@7.27.6': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.0 - '@babel/parser@7.27.2': dependencies: '@babel/types': 7.27.1 - '@babel/parser@7.28.0': - dependencies: - '@babel/types': 7.28.0 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.27.1': {} '@babel/template@7.27.2': @@ -7093,28 +6920,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.0': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/types': 7.28.0 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - '@babel/types@7.27.1': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.28.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@bcoe/v8-coverage@1.0.2': {} '@changesets/apply-release-plan@7.0.12': @@ -7508,11 +7318,6 @@ snapshots: eslint: 9.28.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': - dependencies: - eslint: 9.30.1(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.20.0': @@ -7523,18 +7328,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-array@0.21.0': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - '@eslint/config-helpers@0.2.2': {} - '@eslint/config-helpers@0.3.0': {} - '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 @@ -7571,8 +7366,6 @@ snapshots: '@eslint/js@9.28.0': {} - '@eslint/js@9.30.1': {} - '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.3.1': @@ -7738,6 +7531,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29 + optional: true '@jridgewell/gen-mapping@0.3.8': dependencies: @@ -7751,8 +7545,8 @@ snapshots: '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 optional: true '@jridgewell/sourcemap-codec@1.5.0': {} @@ -7766,6 +7560,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + optional: true '@jspm/core@2.1.0': {} @@ -7977,8 +7772,6 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 - '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rolldown/pluginutils@1.0.0-beta.9': {} '@rollup/plugin-commonjs@25.0.8(rollup@4.40.2)': @@ -8321,10 +8114,6 @@ snapshots: dependencies: '@types/react': 19.1.5 - '@types/react-dom@19.1.6(@types/react@19.1.8)': - dependencies: - '@types/react': 19.1.8 - '@types/react@18.3.23': dependencies: '@types/prop-types': 15.7.15 @@ -8335,10 +8124,6 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/react@19.1.8': - dependencies: - csstype: 3.1.3 - '@types/resolve@1.20.2': {} '@types/semver@7.7.0': {} @@ -8715,18 +8500,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.6.0(vite@7.0.2(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0))': - dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) - '@rolldown/pluginutils': 1.0.0-beta.19 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 7.0.2(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-vue@5.2.4(vite@5.4.19(@types/node@24.0.3)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2))(vue@3.5.14(typescript@5.8.3))': dependencies: vite: 5.4.19(@types/node@24.0.3)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2) @@ -9001,14 +8774,14 @@ snapshots: '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6) '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.1.6) eslint: 8.57.0 - eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.5(eslint@8.57.0))(eslint@8.57.0) - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0) - eslint-config-airbnb-typescript: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6))(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.5(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-typescript: 17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0) eslint-config-prettier: 8.10.0(eslint@8.57.0) eslint-plugin-eslint-plugin: 5.5.1(eslint@8.57.0) eslint-plugin-header: 3.1.1(eslint@8.57.0) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.1.6) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) eslint-plugin-node: 11.1.0(eslint@8.57.0) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8) @@ -10124,15 +9897,6 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0): - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.57.0 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0) - object.assign: 4.1.7 - object.entries: 1.1.9 - semver: 6.3.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: confusing-browser-globals: 1.0.11 @@ -10142,13 +9906,13 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6))(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3) '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.1.6) eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0) eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3))(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: @@ -10158,17 +9922,6 @@ snapshots: eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0) - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.5(eslint@8.57.0))(eslint@8.57.0): - dependencies: - eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) - eslint-plugin-react: 7.37.5(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) - object.assign: 4.1.7 - object.entries: 1.1.9 - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.0))(eslint-plugin-react@7.37.5(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 @@ -10286,12 +10039,12 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.1.6): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.1.6) eslint: 8.57.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0)(typescript@5.7.3) transitivePeerDependencies: - supports-color - typescript @@ -10360,18 +10113,10 @@ snapshots: dependencies: eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react-hooks@5.2.0(eslint@9.30.1(jiti@2.4.2)): - dependencies: - eslint: 9.30.1(jiti@2.4.2) - eslint-plugin-react-refresh@0.4.20(eslint@9.28.0(jiti@2.4.2)): dependencies: eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react-refresh@0.4.20(eslint@9.30.1(jiti@2.4.2)): - dependencies: - eslint: 9.30.1(jiti@2.4.2) - eslint-plugin-react@7.37.5(eslint@8.57.0): dependencies: array-includes: 3.1.8 @@ -10572,48 +10317,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.30.1(jiti@2.4.2): - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.0 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.30.1 - '@eslint/plugin-kit': 0.3.1 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.7 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.1 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.4.2 - transitivePeerDependencies: - - supports-color - espree@10.3.0: dependencies: acorn: 8.14.1 @@ -10980,8 +10683,6 @@ snapshots: globals@16.1.0: {} - globals@16.3.0: {} - globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -11778,7 +11479,7 @@ snapshots: negotiator@1.0.0: {} - next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.0): + next@15.3.2(@babel/core@7.27.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.0): dependencies: '@next/env': 15.3.2 '@swc/counter': 0.1.3 @@ -11788,7 +11489,7 @@ snapshots: postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.27.1)(react@19.1.0) optionalDependencies: '@next/swc-darwin-arm64': 15.3.2 '@next/swc-darwin-x64': 15.3.2 @@ -13037,10 +12738,12 @@ snapshots: style-search@0.1.0: {} - styled-jsx@5.1.6(react@19.1.0): + styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0): dependencies: client-only: 0.0.1 react: 19.1.0 + optionalDependencies: + '@babel/core': 7.27.1 stylehacks@5.1.1(postcss@8.5.3): dependencies: @@ -13202,7 +12905,7 @@ snapshots: terser@5.39.2: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -13577,23 +13280,6 @@ snapshots: terser: 5.39.2 yaml: 2.8.0 - vite@7.0.2(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0): - dependencies: - esbuild: 0.25.4 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 24.0.3 - fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.30.1 - sass: 1.89.0 - terser: 5.39.2 - yaml: 2.8.0 - vitepress@1.6.3(@algolia/client-search@5.25.0)(@types/node@24.0.3)(@types/react@18.3.23)(axios@1.9.0)(fuse.js@7.1.0)(jwt-decode@4.0.0)(lightningcss@1.30.1)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.0)(search-insights@2.17.3)(terser@5.39.2)(typescript@5.8.3): dependencies: '@docsearch/css': 3.8.2 @@ -13742,9 +13428,9 @@ snapshots: dependencies: debug: 4.4.1 eslint: 8.57.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 lodash: 4.17.21 semver: 7.7.2 diff --git a/samples/teamspace-nextjs/package.json b/samples/teamspace-nextjs/package.json index ab8eabe34..a77f45a4d 100644 --- a/samples/teamspace-nextjs/package.json +++ b/samples/teamspace-nextjs/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "scripts": { "build": "next build", - "dev": "next dev", + "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev", "lint": "next lint", "start": "next start" }, From df89e209d3047ab214b6ee6e25c2498b7f304aca Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 11 Jul 2025 12:34:56 +0530 Subject: [PATCH 3/4] chore: add example .env.local file for Asgardeo configuration --- samples/teamspace-nextjs/.env.local.example | 21 +++++++++++++++++++++ samples/teamspace-nextjs/.gitignore | 4 +++- samples/teamspace-react/.env.local.example | 4 ++-- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 samples/teamspace-nextjs/.env.local.example diff --git a/samples/teamspace-nextjs/.env.local.example b/samples/teamspace-nextjs/.env.local.example new file mode 100644 index 000000000..bae3c339a --- /dev/null +++ b/samples/teamspace-nextjs/.env.local.example @@ -0,0 +1,21 @@ +# The base URL for your Asgardeo organization (replace ) +NEXT_PUBLIC_ASGARDEO_BASE_URL='https://api.asgardeo.io/t/' + +# The OAuth client ID for your application (get this from Asgardeo console) +NEXT_PUBLIC_ASGARDEO_CLIENT_ID='' + +# The OAuth client secret for your application (keep this secret, never commit to public repos) +ASGARDEO_CLIENT_SECRET='' + +# Secret used for signing JWT session cookies (change this in production!) +ASGARDEO_SECRET='' + +# Optional: URL to redirect to after successful sign-in +# NEXT_PUBLIC_ASGARDEO_AFTER_SIGN_IN_URL='http://localhost:3000/dashboard' + +# Optional: Custom sign-in and sign-up routes +# NEXT_PUBLIC_ASGARDEO_SIGN_IN_URL='/signin' +# NEXT_PUBLIC_ASGARDEO_SIGN_UP_URL='/signup' + +# Optional: Scopes to request during authentication +# NEXT_PUBLIC_ASGARDEO_SCOPES='internal_organization_create internal_organization_view internal_organization_update internal_organization_delete' diff --git a/samples/teamspace-nextjs/.gitignore b/samples/teamspace-nextjs/.gitignore index f650315f3..e5fcc51ee 100644 --- a/samples/teamspace-nextjs/.gitignore +++ b/samples/teamspace-nextjs/.gitignore @@ -18,10 +18,12 @@ yarn-error.log* # env files .env* +# allow .env.local.example to be committed with sample values. +!.env.local.example # vercel .vercel # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts diff --git a/samples/teamspace-react/.env.local.example b/samples/teamspace-react/.env.local.example index 020179b5e..40bb043f5 100644 --- a/samples/teamspace-react/.env.local.example +++ b/samples/teamspace-react/.env.local.example @@ -1,5 +1,5 @@ # This file is an example of the .env.local file. # DO NOT modify the sample values. -VITE_ASGARDEO_BASE_URL='https://api.asgardeo.io/t/' -VITE_ASGARDEO_CLIENT_ID='' +VITE_ASGARDEO_BASE_URL='https://api.asgardeo.io/t/' +VITE_ASGARDEO_CLIENT_ID='' From ee3b7c669dc4a3c401569ed2b1861256d8df90da Mon Sep 17 00:00:00 2001 From: Brion Date: Fri, 11 Jul 2025 12:38:03 +0530 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20add=20changeset=20=F0=9F=A6=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/late-animals-rush.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/late-animals-rush.md diff --git a/.changeset/late-animals-rush.md b/.changeset/late-animals-rush.md new file mode 100644 index 000000000..28d9e8036 --- /dev/null +++ b/.changeset/late-animals-rush.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/nextjs': patch +--- + +Move to a session cookie to give the ability to do route level scope based authorization.