Merge pull request #36 from ciphera-net/staging
Performance insights, Goals & Events, 2FA improvements, auth fixes
This commit is contained in:
33
CHANGELOG.md
33
CHANGELOG.md
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Performance insights.** Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard.
|
||||||
|
- **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events.
|
||||||
|
- **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired.
|
||||||
|
- **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days.
|
||||||
|
- **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA.
|
||||||
|
|
||||||
|
## [0.11.1-alpha] - 2026-02-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Safer sign-in from the Ciphera hub.** When you open Pulse from the Ciphera Apps page, your credentials are no longer visible in the browser address bar. Sign-in now uses a secure one-time code that expires in seconds, so your session stays private even if someone sees your screen or browser history.
|
||||||
|
|
||||||
## [0.11.0-alpha] - 2026-02-22
|
## [0.11.0-alpha] - 2026-02-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -18,10 +36,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
- **Better form experience.** Forms now auto-focus the first field when they open, text inputs enforce character limits with a visible counter when you're close, and the settings page warns you before navigating away with unsaved changes.
|
- **Better form experience.** Forms now auto-focus the first field when they open, text inputs enforce character limits with a visible counter when you're close, and the settings page warns you before navigating away with unsaved changes.
|
||||||
- **Accessibility improvements.** The notification bell, workspace switcher, and all dashboard tabs are now fully keyboard-navigable. Screen readers announce unread counts, active organizations, and tab changes correctly. Decorative icons are hidden from assistive technology.
|
- **Accessibility improvements.** The notification bell, workspace switcher, and all dashboard tabs are now fully keyboard-navigable. Screen readers announce unread counts, active organizations, and tab changes correctly. Decorative icons are hidden from assistive technology.
|
||||||
- **Smooth organization switching.** Switching between organizations now shows a branded loading screen instead of a blank flash while the page reloads.
|
- **Smooth organization switching.** Switching between organizations now shows a branded loading screen instead of a blank flash while the page reloads.
|
||||||
- **Graceful server shutdown.** Deployments no longer kill in-flight requests or interrupt background tasks. The server finishes ongoing work before shutting down.
|
- **Graceful server shutdown.** Deployments no longer kill in-flight requests or interrupt background tasks. The server finishes ongoing work before shutting down, so your active sessions aren't cut off mid-action.
|
||||||
- **Database connection pooling.** The backend now limits and recycles database connections, preventing exhaustion under load and reducing query latency.
|
- **Database connection pooling.** The backend now limits and recycles database connections, preventing exhaustion under load and keeping queries fast even with many concurrent users.
|
||||||
- **Date range validation.** Analytics, funnel, and uptime queries now reject invalid date ranges (end before start, or spans longer than a year) instead of silently returning empty or oversized results.
|
- **Date range validation.** Analytics, funnel, and uptime queries now reject invalid date ranges (end before start, or spans longer than a year) and show a clear error instead of empty or confusing results.
|
||||||
- **Excluded paths limit.** Sites can now have up to 50 excluded paths. Previously there was no cap, which could slow down event processing.
|
- **Excluded paths limit.** Sites can now have up to 50 excluded paths. Previously there was no cap, which could slow down event processing; the limit keeps things fast while still giving you flexibility.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -29,8 +47,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
- **Clearer error messages.** When something goes wrong, the error message now tells you what failed (e.g. "Failed to load uptime monitors") instead of a generic "Failed to load data".
|
- **Clearer error messages.** When something goes wrong, the error message now tells you what failed (e.g. "Failed to load uptime monitors") instead of a generic "Failed to load data".
|
||||||
- **Faster favicon loading.** Site icons in the dashboard, referrers, and campaigns now use Next.js image optimization for better caching and lazy loading.
|
- **Faster favicon loading.** Site icons in the dashboard, referrers, and campaigns now use Next.js image optimization for better caching and lazy loading.
|
||||||
- **Tighter name limits.** Site, funnel, and monitor names are now capped at 100 characters instead of 255 — long enough for any real name, short enough to not break the UI.
|
- **Tighter name limits.** Site, funnel, and monitor names are now capped at 100 characters instead of 255 — long enough for any real name, short enough to not break the UI.
|
||||||
- **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time.
|
- **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time and fewer edge cases slip through.
|
||||||
- **Smaller page downloads.** Icon imports are now tree-shaken so only the icons actually used are included in the bundle.
|
- **Smaller page downloads.** Icon imports are now tree-shaken so only the icons actually used are included in the bundle, reducing download size and speeding up page loads.
|
||||||
- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. Error logs are now also suppressed in production and only appear during development.
|
- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. Error logs are now also suppressed in production and only appear during development.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -168,7 +186,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...HEAD
|
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.1-alpha...HEAD
|
||||||
|
[0.11.1-alpha]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...v0.11.1-alpha
|
||||||
[0.11.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.10.0-alpha...v0.11.0-alpha
|
[0.11.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.10.0-alpha...v0.11.0-alpha
|
||||||
[0.10.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...v0.10.0-alpha
|
[0.10.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...v0.10.0-alpha
|
||||||
[0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha
|
[0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface UserPayload {
|
|||||||
/** Error type returned to client for mapping to user-facing copy (no sensitive details). */
|
/** Error type returned to client for mapping to user-facing copy (no sensitive details). */
|
||||||
export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server'
|
export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server'
|
||||||
|
|
||||||
export async function exchangeAuthCode(code: string, codeVerifier: string, redirectUri: string) {
|
export async function exchangeAuthCode(code: string, codeVerifier: string | null, redirectUri: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${AUTH_API_URL}/oauth/token`, {
|
const res = await fetch(`${AUTH_API_URL}/oauth/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -45,7 +45,7 @@ export async function exchangeAuthCode(code: string, codeVerifier: string, redir
|
|||||||
code,
|
code,
|
||||||
client_id: 'pulse-app',
|
client_id: 'pulse-app',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
code_verifier: codeVerifier,
|
code_verifier: codeVerifier || '',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { logger } from '@/lib/utils/logger'
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { useAuth } from '@/lib/auth/context'
|
import { useAuth } from '@/lib/auth/context'
|
||||||
import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
|
import { AUTH_URL, default as apiRequest } from '@/lib/api/client'
|
||||||
import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth'
|
import { exchangeAuthCode } from '@/app/actions/auth'
|
||||||
import { authMessageFromErrorType, type AuthErrorType } from '@ciphera-net/ui'
|
import { authMessageFromErrorType, type AuthErrorType } from '@ciphera-net/ui'
|
||||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ function AuthCallbackContent() {
|
|||||||
const code = searchParams.get('code')
|
const code = searchParams.get('code')
|
||||||
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
||||||
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : ''
|
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : ''
|
||||||
if (!code || !codeVerifier) return
|
if (!code) return
|
||||||
const result = await exchangeAuthCode(code, codeVerifier, redirectUri)
|
const result = await exchangeAuthCode(code, codeVerifier, redirectUri)
|
||||||
if (result.success && result.user) {
|
if (result.success && result.user) {
|
||||||
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
||||||
@@ -47,59 +47,25 @@ function AuthCallbackContent() {
|
|||||||
}, [searchParams, login, router])
|
}, [searchParams, login, router])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// * Prevent double execution (React Strict Mode or fast re-renders)
|
|
||||||
if (processedRef.current && !isRetrying) return
|
if (processedRef.current && !isRetrying) return
|
||||||
|
|
||||||
// * Check for direct token passing (from auth-frontend direct login)
|
|
||||||
// * This flow exposes tokens in URL, kept for legacy support.
|
|
||||||
// * Recommended: Use Authorization Code flow (below)
|
|
||||||
const token = searchParams.get('token')
|
|
||||||
const refreshToken = searchParams.get('refresh_token')
|
|
||||||
|
|
||||||
if (token && refreshToken) {
|
|
||||||
processedRef.current = true
|
|
||||||
const handleDirectTokens = async () => {
|
|
||||||
const result = await setSessionAction(token, refreshToken)
|
|
||||||
if (result.success && result.user) {
|
|
||||||
// * Fetch full profile (including display_name) before navigating so header shows correct name on first paint
|
|
||||||
try {
|
|
||||||
const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me')
|
|
||||||
const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role }
|
|
||||||
login(merged)
|
|
||||||
} catch {
|
|
||||||
login(result.user)
|
|
||||||
}
|
|
||||||
if (typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')) {
|
|
||||||
router.push('/welcome')
|
|
||||||
} else {
|
|
||||||
const raw = searchParams.get('returnTo') || '/'
|
|
||||||
const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
|
|
||||||
router.push(safe)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setError(authMessageFromErrorType('invalid'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleDirectTokens()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = searchParams.get('code')
|
const code = searchParams.get('code')
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
const state = searchParams.get('state')
|
const state = searchParams.get('state')
|
||||||
|
|
||||||
if (!code || !state) return
|
|
||||||
|
|
||||||
const storedState = localStorage.getItem('oauth_state')
|
const storedState = localStorage.getItem('oauth_state')
|
||||||
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
const codeVerifier = localStorage.getItem('oauth_code_verifier')
|
||||||
|
|
||||||
if (!codeVerifier) {
|
// * Full OAuth flow (app-initiated): validate state + use PKCE
|
||||||
setError('Missing code verifier')
|
// * Session-authorized flow (from auth hub): no stored state or verifier
|
||||||
return
|
const isFullOAuth = !!storedState && !!codeVerifier
|
||||||
}
|
|
||||||
if (state !== storedState) {
|
if (isFullOAuth) {
|
||||||
logger.error('State mismatch', { received: state, stored: storedState })
|
if (state !== storedState) {
|
||||||
setError('Invalid state')
|
logger.error('State mismatch', { received: state, stored: storedState })
|
||||||
return
|
setError('Invalid state')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processedRef.current = true
|
processedRef.current = true
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import api from '@/lib/api/client'
|
|||||||
import { deriveAuthKey } from '@/lib/crypto/password'
|
import { deriveAuthKey } from '@/lib/crypto/password'
|
||||||
import { deleteAccount, getUserSessions, revokeSession, updateUserPreferences, updateDisplayName } from '@/lib/api/user'
|
import { deleteAccount, getUserSessions, revokeSession, updateUserPreferences, updateDisplayName } from '@/lib/api/user'
|
||||||
import { setup2FA, verify2FA, disable2FA, regenerateRecoveryCodes } from '@/lib/api/2fa'
|
import { setup2FA, verify2FA, disable2FA, regenerateRecoveryCodes } from '@/lib/api/2fa'
|
||||||
|
import { registerPasskey, listPasskeys, deletePasskey } from '@/lib/api/webauthn'
|
||||||
|
|
||||||
export default function ProfileSettings() {
|
export default function ProfileSettings() {
|
||||||
const { user, refresh, logout } = useAuth()
|
const { user, refresh, logout } = useAuth()
|
||||||
@@ -46,6 +47,9 @@ export default function ProfileSettings() {
|
|||||||
onRegenerateRecoveryCodes={regenerateRecoveryCodes}
|
onRegenerateRecoveryCodes={regenerateRecoveryCodes}
|
||||||
onGetSessions={getUserSessions}
|
onGetSessions={getUserSessions}
|
||||||
onRevokeSession={revokeSession}
|
onRevokeSession={revokeSession}
|
||||||
|
onRegisterPasskey={registerPasskey}
|
||||||
|
onListPasskeys={listPasskeys}
|
||||||
|
onDeletePasskey={deletePasskey}
|
||||||
onUpdatePreferences={updateUserPreferences}
|
onUpdatePreferences={updateUserPreferences}
|
||||||
deriveAuthKey={deriveAuthKey}
|
deriveAuthKey={deriveAuthKey}
|
||||||
refreshUser={refresh}
|
refreshUser={refresh}
|
||||||
|
|||||||
@@ -27,14 +27,16 @@ export async function verify2FA(code: string): Promise<Verify2FAResponse> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disable2FA(): Promise<void> {
|
export async function disable2FA(passwordDerived: string): Promise<void> {
|
||||||
return apiRequest<void>('/auth/2fa/disable', {
|
return apiRequest<void>('/auth/2fa/disable', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ password: passwordDerived }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function regenerateRecoveryCodes(): Promise<RegenerateCodesResponse> {
|
export async function regenerateRecoveryCodes(passwordDerived: string): Promise<RegenerateCodesResponse> {
|
||||||
return apiRequest<RegenerateCodesResponse>('/auth/2fa/recovery', {
|
return apiRequest<RegenerateCodesResponse>('/auth/2fa/recovery', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ password: passwordDerived }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
54
lib/api/webauthn.ts
Normal file
54
lib/api/webauthn.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* WebAuthn / Passkey API client for settings (list, register, delete).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { startRegistration, type PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/browser'
|
||||||
|
import apiRequest from './client'
|
||||||
|
|
||||||
|
export interface BeginRegistrationResponse {
|
||||||
|
sessionId: string
|
||||||
|
creationOptions: {
|
||||||
|
publicKey: Record<string, unknown>
|
||||||
|
mediation?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasskeyCredential {
|
||||||
|
id: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListPasskeysResponse {
|
||||||
|
credentials: PasskeyCredential[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerPasskey(): Promise<void> {
|
||||||
|
const { sessionId, creationOptions } = await apiRequest<BeginRegistrationResponse>(
|
||||||
|
'/auth/webauthn/register/begin',
|
||||||
|
{ method: 'POST' }
|
||||||
|
)
|
||||||
|
const optionsJSON = creationOptions?.publicKey
|
||||||
|
if (!optionsJSON) {
|
||||||
|
throw new Error('Invalid registration options')
|
||||||
|
}
|
||||||
|
const response = await startRegistration({
|
||||||
|
optionsJSON: optionsJSON as unknown as PublicKeyCredentialCreationOptionsJSON,
|
||||||
|
})
|
||||||
|
await apiRequest<{ message: string }>('/auth/webauthn/register/finish', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ sessionId, response }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPasskeys(): Promise<ListPasskeysResponse> {
|
||||||
|
return apiRequest<ListPasskeysResponse>('/auth/webauthn/credentials', {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePasskey(credentialId: string): Promise<void> {
|
||||||
|
return apiRequest<void>(
|
||||||
|
`/auth/webauthn/credentials/${encodeURIComponent(credentialId)}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -110,7 +110,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
// * 1. Check server-side session (cookies)
|
// * 1. Check server-side session (cookies)
|
||||||
const session = await getSessionAction()
|
let session = await getSessionAction()
|
||||||
|
|
||||||
|
// * 2. If no access_token but refresh_token may exist, try refresh (fixes 15-min inactivity logout)
|
||||||
|
if (!session && typeof window !== 'undefined') {
|
||||||
|
const refreshRes = await fetch('/api/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
if (refreshRes.ok) {
|
||||||
|
session = await getSessionAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
setUser(session)
|
setUser(session)
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ export function middleware(request: NextRequest) {
|
|||||||
const hasRefresh = request.cookies.has('refresh_token')
|
const hasRefresh = request.cookies.has('refresh_token')
|
||||||
const hasSession = hasAccess || hasRefresh
|
const hasSession = hasAccess || hasRefresh
|
||||||
|
|
||||||
// * Authenticated user hitting /login or /signup → send them home
|
// * Authenticated user (with access token) hitting /login or /signup → send them home.
|
||||||
if (hasSession && AUTH_ONLY_ROUTES.has(pathname)) {
|
// * Only check access_token; stale refresh_token alone must not block login (fixes post-inactivity sign-in).
|
||||||
|
if (hasAccess && AUTH_ONLY_ROUTES.has(pathname)) {
|
||||||
return NextResponse.redirect(new URL('/', request.url))
|
return NextResponse.redirect(new URL('/', request.url))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.9.0-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.9.0-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.58",
|
"@ciphera-net/ui": "^0.0.64",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@stripe/react-stripe-js": "^5.6.0",
|
"@stripe/react-stripe-js": "^5.6.0",
|
||||||
"@stripe/stripe-js": "^8.7.0",
|
"@stripe/stripe-js": "^8.7.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
@@ -1541,9 +1542,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.0.58",
|
"version": "0.0.64",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.58/ac48a989da2db79880ce2fa7f89b63a62e2b68c9",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.64/1630605518a705ba9e74f003b9c66646bcc699ac",
|
||||||
"integrity": "sha512-cvptYjs+E72EQvM5YGx5pp4SOiyJ7t5qv5NSRfoFxtcTCwR4sKUN4SoZUA+HV3tLlq4qXXHAB98E7qgbBRIn+Q==",
|
"integrity": "sha512-xY+yALuCqWtsH78t6xmy2JQnQ8WFSlihElHnetFr6GQp9mOTCA5rlQq+a8hyg4xW7uXtIbKmPBxVnH5TlH9lBQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@@ -2717,6 +2718,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@simplewebauthn/browser": {
|
||||||
|
"version": "13.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
|
||||||
|
"integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@stripe/react-stripe-js": {
|
"node_modules/@stripe/react-stripe-js": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.11.0-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -10,9 +10,10 @@
|
|||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.58",
|
"@ciphera-net/ui": "^0.0.64",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@stripe/react-stripe-js": "^5.6.0",
|
"@stripe/react-stripe-js": "^5.6.0",
|
||||||
"@stripe/stripe-js": "^8.7.0",
|
"@stripe/stripe-js": "^8.7.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user