Skip to content

Address security vulnerabilities and localization gaps in remote JSON import#24

Merged
carnach merged 2 commits into
developmentfrom
copilot/sub-pr-23
Feb 7, 2026
Merged

Address security vulnerabilities and localization gaps in remote JSON import#24
carnach merged 2 commits into
developmentfrom
copilot/sub-pr-23

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 7, 2026

Addresses code review feedback on PR #23 regarding SSRF vulnerabilities, missing translations, and error handling in the remote JSON import feature.

Security (SSRF Mitigation)

  • Protocol validation: Restrict remote URLs to http/https only
  • Memory exhaustion protection: Enforce hard byte limit while reading response stream, independent of content-length header
  • Redirect prevention: Set redirect: 'manual' to prevent redirect-based SSRF attacks
// Before: relied solely on content-length header
return response.json() as Promise<unknown>

// After: stream with hard limit enforcement
const reader = response.body?.getReader()
let totalBytes = 0
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  totalBytes += value.length
  if (totalBytes > MAX_RESPONSE_SIZE) {
    throw new Error('Response size exceeds maximum allowed limit.')
  }
  text += decoder.decode(value, { stream: true })
}

Localization

  • Added 14 translation keys for hardcoded UI strings (button labels, error messages, placeholders)
  • Changed placeholder URL from spliit.app to neutral example.com

Error Handling

  • syncAssociatedGroups: Wrapped in try/catch for best-effort execution—import succeeds even if group association sync fails
  • Passkey authentication: Differentiate error types (rate limiting, not found, user cancellation) instead of generic "not found" for all failures

Code Quality

  • Fixed inconsistent indentation (2-space vs 4-space) via prettier

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI mentioned this pull request Feb 7, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
my-spliit-instance Ready Ready Preview, Comment Feb 7, 2026 11:21am

…r handling

- Add protocol validation (http/https only) to validateRemoteUrl
- Enforce hard byte limit while reading response in fetchTrpcJson
- Prevent redirect-based SSRF by setting redirect: 'manual'
- Add all missing translation keys for hardcoded UI strings
- Change placeholder URL from spliit.app to neutral example.com
- Wrap syncAssociatedGroups in try/catch for best-effort handling
- Improve passkey error handling to differentiate error types (rate limit, not found, user cancel)
- Run prettier to fix inconsistent indentation

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Feb 7, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • checkpoint.prisma.io
    • Triggering command: /opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node /home/REDACTED/work/spliit/spliit/node_modules/prisma/build/child {&#34;product&#34;:&#34;prisma&#34;,&#34;version&#34;:&#34;6.18.0&#34;,&#34;cli_install_type&#34;:&#34;local&#34;,&#34;information&#34;:&#34;&#34;,&#34;local_timestamp&#34;:&#34;2026-02-07T11:16:17Z&#34;,&#34;project_hash&#34;:&#34;fe730ba0&#34;,&#34;cli_path&#34;:&#34;/home/REDACTED/work/spliit/spliit/node_modules/.bin/prisma&#34;,&#34;cli_path_hash&#34;:&#34;86e8da49&#34;,&#34;endpoin (dns block)
    • Triggering command: /opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node /home/REDACTED/work/spliit/spliit/node_modules/prisma/build/child {&#34;product&#34;:&#34;prisma&#34;,&#34;version&#34;:&#34;6.18.0&#34;,&#34;cli_install_type&#34;:&#34;local&#34;,&#34;information&#34;:&#34;&#34;,&#34;local_timestamp&#34;:&#34;2026-02-07T11:16:19Z&#34;,&#34;project_hash&#34;:&#34;fe730ba0&#34;,&#34;cli_path&#34;:&#34;/home/REDACTED/work/spliit/spliit/node_modules/.bin/prisma&#34;,&#34;cli_path_hash&#34;:&#34;86e8da49&#34;,&#34;endpoin (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Expand JSON import flow for group support Address security vulnerabilities and localization gaps in remote JSON import Feb 7, 2026
Copilot AI requested a review from carnach February 7, 2026 11:22
@carnach carnach marked this pull request as ready for review February 7, 2026 11:22
Copilot AI review requested due to automatic review settings February 7, 2026 11:22
@carnach carnach merged commit f330724 into development Feb 7, 2026
4 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves the remote JSON import flow by adding SSRF mitigations on the server route, localizing previously hardcoded UI strings in the import dialog, and refining error handling for anonymous/passkey authentication and group association syncing.

Changes:

  • Hardened remote JSON import endpoint (protocol restriction, redirect prevention, response-size hard limit while streaming).
  • Localized Import-from-URL/file UI strings and related error messages; updated example placeholder URL.
  • Made associated-group syncing best-effort and improved passkey error differentiation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/components/import-json-button.tsx Uses next-intl keys for import UI/errors and makes group association sync best-effort.
src/components/anonymous-auth-menu.tsx Refines passkey auth error handling and includes formatting/indent consistency updates.
src/app/groups/json/import/remote/route.ts Adds SSRF mitigations and enforces a hard response-size cap while reading remote responses.
messages/en-US.json Adds JSONImport.* translation keys used by the import UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 38 to 106
function isPrivateIP(hostname: string): boolean {
// Check for localhost
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
return true
// Check for localhost
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1'
) {
return true
}

// Check for private IPv4 ranges
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
const match = hostname.match(ipv4Regex)
if (match && match.length === 5) {
const a = Number(match[1])
const b = Number(match[2])
const c = Number(match[3])
const d = Number(match[4])

// Validate octets are in valid range
if (a > 255 || b > 255 || c > 255 || d > 255) {
return true // Invalid IP, treat as unsafe
}

// Check for private IPv4 ranges
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
const match = hostname.match(ipv4Regex)
if (match && match.length === 5) {
const a = Number(match[1])
const b = Number(match[2])
const c = Number(match[3])
const d = Number(match[4])

// Validate octets are in valid range
if (a > 255 || b > 255 || c > 255 || d > 255) {
return true // Invalid IP, treat as unsafe
}

// 127.0.0.0/8 - Loopback
if (a === 127) return true

// 10.0.0.0/8 - Private
if (a === 10) return true

// 172.16.0.0/12 - Private
if (a === 172 && b >= 16 && b <= 31) return true

// 192.168.0.0/16 - Private
if (a === 192 && b === 168) return true

// 169.254.0.0/16 - Link-local (includes cloud metadata endpoint)
if (a === 169 && b === 254) return true
}
// 127.0.0.0/8 - Loopback
if (a === 127) return true

return false
}
// 10.0.0.0/8 - Private
if (a === 10) return true

function validateRemoteUrl(url: URL, localOrigin: string): { valid: boolean; error?: string } {
// Check if URL is same origin
if (url.origin === localOrigin) {
return { valid: false, error: 'Remote URL must be different from this site.' }
}
// 172.16.0.0/12 - Private
if (a === 172 && b >= 16 && b <= 31) return true

// Check for private IPs and localhost
if (isPrivateIP(url.hostname)) {
return { valid: false, error: 'Cannot fetch from private or local addresses.' }
}
// 192.168.0.0/16 - Private
if (a === 192 && b === 168) return true

return { valid: true }
// 169.254.0.0/16 - Link-local (includes cloud metadata endpoint)
if (a === 169 && b === 254) return true
}

return false
}

function deserializeTrpcData(entry: TrpcBatchEntry) {
if (entry.error?.message) {
throw new Error(entry.error.message)
function validateRemoteUrl(
url: URL,
localOrigin: string,
): { valid: boolean; error?: string } {
// Only allow http/https protocols to reduce SSRF risk
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return {
valid: false,
error: 'Only http and https protocols are allowed.',
}
if (!entry.result?.data) {
throw new Error('Remote response is missing data.')
}

// Check if URL is same origin
if (url.origin === localOrigin) {
return {
valid: false,
error: 'Remote URL must be different from this site.',
}
const data = entry.result.data as { json?: unknown; meta?: unknown }
if (data && typeof data === 'object' && 'json' in data) {
try {
return superjson.deserialize(data as SuperJSONResult)
} catch (error) {
return (data as { json?: unknown }).json
}
}

// Check for private IPs and localhost
if (isPrivateIP(url.hostname)) {
return {
valid: false,
error: 'Cannot fetch from private or local addresses.',
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSRF mitigation is incomplete: isPrivateIP only checks localhost + a few private IPv4 ranges and does not cover private/link-local IPv6 ranges (e.g. fc00::/7, fe80::/10). Also, validateRemoteUrl only inspects url.hostname and does not resolve DNS, so hostnames that resolve to private IPs (e.g. 127.0.0.1.nip.io) would still be allowed. Consider resolving A/AAAA records and rejecting any private/link-local results, and expanding the IPv6 checks.

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +242
const decoder = new TextDecoder()
let text = ''
let totalBytes = 0

try {
const response = await fetch(url, {
method: 'GET',
headers: { accept: 'application/json' },
signal: controller.signal,
})
while (true) {
const { done, value } = await reader.read()
if (done) break

if (!response.ok) {
throw new Error(`Remote request failed with status ${response.status}.`)
totalBytes += value.length
if (totalBytes > MAX_RESPONSE_SIZE) {
throw new Error('Response size exceeds maximum allowed limit.')
}

// Check response size
const contentLength = response.headers.get('content-length')
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
throw new Error('Response size exceeds maximum allowed limit.')
}
text += decoder.decode(value, { stream: true })
}

return response.json() as Promise<unknown>
// Final decode to flush any remaining bytes
text += decoder.decode()

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchTrpcJson builds the response body via text += decoder.decode(...) inside a loop. For multi-megabyte payloads this can be quadratic due to repeated string reallocation and increase memory/CPU despite the 10MB cap. Consider collecting decoded chunks (or Uint8Array chunks) and joining once, or using an incremental buffer approach before JSON.parse.

Copilot uses AI. Check for mistakes.
Comment on lines 985 to 1003
} catch (error) {
console.error('Passkey authentication error:', error)

// Detect user cancellation (NotAllowedError)
if (error instanceof Error && error.name === 'NotAllowedError') {
toast({
title: 'Authentication cancelled',
description: 'Passkey authentication was cancelled.',
})
return
}

// For not-linked users without username, suggest adding username
if (!isLinked && !username.trim()) {
toast({
title: 'Passkey not found',
description: 'Try entering your username and retrying passkey login.',
})
} else {
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In handleAuthenticatePasskey, the catch block shows a "Passkey not found" toast whenever the user is not linked and no username is entered. That will also happen for unrelated failures after options retrieval (e.g. /auth-verify failing, network errors), which contradicts the earlier status-based handling and can mislead users. Consider only showing the "not found" guidance when the options request actually returned 404 (e.g. track the status / throw a typed error), and otherwise show the real error.

Copilot uses AI. Check for mistakes.
carnach added a commit that referenced this pull request Feb 7, 2026
* add anonymous account

* feat: implement database-backed sessions and improve group management

Add database-backed session storage to fix session expiration on server restart

- Create AnonymousSession table with token, userId, expiresAt, and challenge fields

- Replace in-memory Map with Prisma queries in SessionStore

- Sessions now persist across server restarts and deployments

Fix group recovery and sign-out behavior

- Fix naming conflict between setRecentGroups state setter and helper function

- Groups now properly save to localStorage when logging in

- Sign-out only clears localStorage, preserving groups in database for recovery

- Groups restore automatically when logging back in with same account

Improve UI for logged-in users

- Move Restore from backup and Import from JSON to account dropdown menu

- Hide restore/import options from groups page for logged-in users

- Better organization of account management features

Add server-side authorization to group deletion

- Validate user session before allowing group deletion

- Verify group ownership through anonymousUserGroup table

- Return appropriate error codes (401 UNAUTHORIZED, 403 FORBIDDEN)

- Prevents unauthorized deletion even if UI is bypassed

* update sigin UI

* feat: support multiple passkeys per account with names

Add ability to register multiple passkeys per anonymous account with custom names

- Create new Passkey table with name, credentialId, publicKey, counter, lastUsedAt fields

- Migrate from single passkey storage on AnonymousUser to separate Passkey table

- Add passkey name input dialog when registering new passkeys

- Display list of all registered passkeys with creation dates

- Add individual passkey deletion with confirmation dialog

- Update lastUsedAt timestamp when passkey is used for authentication

- Add /api/anonymous-users/passkey/list endpoint to fetch user's passkeys

- Maintain backward compatibility with deprecated passkey fields on AnonymousUser

UI improvements:

- Show passkey list with name and creation date in account dialog

- Add 'Add passkey' button that opens name input dialog

- Individual remove button for each passkey

- Better UX with 'New account' button highlighted when generating username

- Changed 'Recover account' to 'Existing account' for clarity

* fix: update auth-options to use new Passkey table

- Change auth-options endpoint to fetch credentials from Passkey table
- Support multiple passkeys by returning all credential IDs for allowCredentials
- Fixes 'No passkey registered' error when authenticating

* fix: ensure user exists before creating passkey auth session

- Add upsert to create AnonymousUser if not exists before session creation
- Prevents foreign key constraint violation in AnonymousSession table
- Handle both authenticated and discoverable credential flows

* feat: enforce passphrase security requirements and separate change/reset flows

- Always require current passphrase for normal passphrase change
- Add separate reset flow for passkey-authenticated users
- Reset with passkey only requires new passphrase (no current)
- Add hasPassphrase flag to groups API response
- Update UI to conditionally show options based on passphrase state
- Add passkeyResetMode state to differentiate between change and reset
- Server-side validation enforces security requirements

* Address code review feedback: authorization, error handling, and cleanup optimization (#20)

* Initial plan

* Address code review comments: fix security and reliability issues

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Refactor: extract session retrieval helper and add temp user cleanup

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Improve error messages and add error logging for debugging

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Polish error message for clarity

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* test passkey fix

* try no username first

* test IOS

* update ux

* update UI remove import button

* finalise permissions

* additional permissoins

* Add remote JSON import flow with auto-association and iOS icon fixes

* Address security vulnerabilities and translations in remote JSON import (#22)

* Initial plan

* Add security improvements to remote JSON import route

- Add rate limiting to prevent SSRF abuse
- Add timeout (30s) to fetch operations
- Add groupId validation to prevent injection/path traversal
- Add SSRF protection (block private IPs, localhost, metadata endpoints)
- Add response size limit (10MB) and max expenses limit (10k)
- Add error handling to normalizeDate function
- Improve error logging in import-json-button.tsx
- Extract ASSOCIATED_GROUPS_KEY constant to shared file

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Translate import success messages to all supported languages

- pt-BR: Portuguese (Brazil)
- ru-RU: Russian
- ko: Korean
- id: Indonesian
- fr-FR: French
- zh-TW: Chinese Traditional
- ro: Romanian
- ja-JP: Japanese
- eu: Basque
- zh-CN: Chinese Simplified
- nl-NL: Dutch
- he: Hebrew
- cs-CZ: Czech
- uk-UA: Ukrainian
- tr-TR: Turkish
- pt: Portuguese
- pl-PL: Polish
- es: Spanish
- de-DE: German
- ca: Catalan
- fi: Finnish
- it-IT: Italian

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Address code review feedback on security improvements

- Fix IP validation to properly handle octets and validate ranges
- Remove redundant cloud metadata endpoint check (covered by link-local)
- Fix normalizeDate to throw error instead of using epoch as default
- Filter out expenses with invalid dates instead of setting to epoch
- Sanitize logging to avoid exposing sensitive data

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix log injection vulnerability in normalizeDate

Use template literal instead of string concatenation to prevent
potential log injection when logging the type of invalid date values.

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix json

* empty username when accessing the login screen

* Address security vulnerabilities and localization gaps in remote JSON import (#24)

* Initial plan

* Address code review feedback: SSRF protection, translations, and error handling

- Add protocol validation (http/https only) to validateRemoteUrl
- Enforce hard byte limit while reading response in fetchTrpcJson
- Prevent redirect-based SSRF by setting redirect: 'manual'
- Add all missing translation keys for hardcoded UI strings
- Change placeholder URL from spliit.app to neutral example.com
- Wrap syncAssociatedGroups in try/catch for best-effort handling
- Improve passkey error handling to differentiate error types (rate limit, not found, user cancel)
- Run prettier to fix inconsistent indentation

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix duplicate imports causing build failure in import-json-button.tsx (#25)

* Initial plan

* Fix duplicate imports in import-json-button.tsx

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>
carnach added a commit that referenced this pull request Feb 7, 2026
* add anonymous account

* feat: implement database-backed sessions and improve group management

Add database-backed session storage to fix session expiration on server restart

- Create AnonymousSession table with token, userId, expiresAt, and challenge fields

- Replace in-memory Map with Prisma queries in SessionStore

- Sessions now persist across server restarts and deployments

Fix group recovery and sign-out behavior

- Fix naming conflict between setRecentGroups state setter and helper function

- Groups now properly save to localStorage when logging in

- Sign-out only clears localStorage, preserving groups in database for recovery

- Groups restore automatically when logging back in with same account

Improve UI for logged-in users

- Move Restore from backup and Import from JSON to account dropdown menu

- Hide restore/import options from groups page for logged-in users

- Better organization of account management features

Add server-side authorization to group deletion

- Validate user session before allowing group deletion

- Verify group ownership through anonymousUserGroup table

- Return appropriate error codes (401 UNAUTHORIZED, 403 FORBIDDEN)

- Prevents unauthorized deletion even if UI is bypassed

* update sigin UI

* feat: support multiple passkeys per account with names

Add ability to register multiple passkeys per anonymous account with custom names

- Create new Passkey table with name, credentialId, publicKey, counter, lastUsedAt fields

- Migrate from single passkey storage on AnonymousUser to separate Passkey table

- Add passkey name input dialog when registering new passkeys

- Display list of all registered passkeys with creation dates

- Add individual passkey deletion with confirmation dialog

- Update lastUsedAt timestamp when passkey is used for authentication

- Add /api/anonymous-users/passkey/list endpoint to fetch user's passkeys

- Maintain backward compatibility with deprecated passkey fields on AnonymousUser

UI improvements:

- Show passkey list with name and creation date in account dialog

- Add 'Add passkey' button that opens name input dialog

- Individual remove button for each passkey

- Better UX with 'New account' button highlighted when generating username

- Changed 'Recover account' to 'Existing account' for clarity

* fix: update auth-options to use new Passkey table

- Change auth-options endpoint to fetch credentials from Passkey table
- Support multiple passkeys by returning all credential IDs for allowCredentials
- Fixes 'No passkey registered' error when authenticating

* fix: ensure user exists before creating passkey auth session

- Add upsert to create AnonymousUser if not exists before session creation
- Prevents foreign key constraint violation in AnonymousSession table
- Handle both authenticated and discoverable credential flows

* feat: enforce passphrase security requirements and separate change/reset flows

- Always require current passphrase for normal passphrase change
- Add separate reset flow for passkey-authenticated users
- Reset with passkey only requires new passphrase (no current)
- Add hasPassphrase flag to groups API response
- Update UI to conditionally show options based on passphrase state
- Add passkeyResetMode state to differentiate between change and reset
- Server-side validation enforces security requirements

* Address code review feedback: authorization, error handling, and cleanup optimization (#20)

* Initial plan

* Address code review comments: fix security and reliability issues

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Refactor: extract session retrieval helper and add temp user cleanup

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Improve error messages and add error logging for debugging

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Polish error message for clarity

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* test passkey fix

* try no username first

* test IOS

* update ux

* update UI remove import button

* finalise permissions

* additional permissoins

* Add remote JSON import flow with auto-association and iOS icon fixes

* Address security vulnerabilities and translations in remote JSON import (#22)

* Initial plan

* Add security improvements to remote JSON import route

- Add rate limiting to prevent SSRF abuse
- Add timeout (30s) to fetch operations
- Add groupId validation to prevent injection/path traversal
- Add SSRF protection (block private IPs, localhost, metadata endpoints)
- Add response size limit (10MB) and max expenses limit (10k)
- Add error handling to normalizeDate function
- Improve error logging in import-json-button.tsx
- Extract ASSOCIATED_GROUPS_KEY constant to shared file

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Translate import success messages to all supported languages

- pt-BR: Portuguese (Brazil)
- ru-RU: Russian
- ko: Korean
- id: Indonesian
- fr-FR: French
- zh-TW: Chinese Traditional
- ro: Romanian
- ja-JP: Japanese
- eu: Basque
- zh-CN: Chinese Simplified
- nl-NL: Dutch
- he: Hebrew
- cs-CZ: Czech
- uk-UA: Ukrainian
- tr-TR: Turkish
- pt: Portuguese
- pl-PL: Polish
- es: Spanish
- de-DE: German
- ca: Catalan
- fi: Finnish
- it-IT: Italian

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Address code review feedback on security improvements

- Fix IP validation to properly handle octets and validate ranges
- Remove redundant cloud metadata endpoint check (covered by link-local)
- Fix normalizeDate to throw error instead of using epoch as default
- Filter out expenses with invalid dates instead of setting to epoch
- Sanitize logging to avoid exposing sensitive data

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix log injection vulnerability in normalizeDate

Use template literal instead of string concatenation to prevent
potential log injection when logging the type of invalid date values.

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix json

* empty username when accessing the login screen

* Address security vulnerabilities and localization gaps in remote JSON import (#24)

* Initial plan

* Address code review feedback: SSRF protection, translations, and error handling

- Add protocol validation (http/https only) to validateRemoteUrl
- Enforce hard byte limit while reading response in fetchTrpcJson
- Prevent redirect-based SSRF by setting redirect: 'manual'
- Add all missing translation keys for hardcoded UI strings
- Change placeholder URL from spliit.app to neutral example.com
- Wrap syncAssociatedGroups in try/catch for best-effort handling
- Improve passkey error handling to differentiate error types (rate limit, not found, user cancel)
- Run prettier to fix inconsistent indentation

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix duplicate imports causing build failure in import-json-button.tsx (#25)

* Initial plan

* Fix duplicate imports in import-json-button.tsx

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* add whats new

* add whats new

* [WIP] Add end-user documentation and UX improvements (#27)

* Initial plan

* Address PR review comments: fix unused imports, update links, improve security and error handling

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Improve icon choice and clarify HTML stripping behavior

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>
carnach added a commit that referenced this pull request Feb 9, 2026
* add anonymous account

* feat: implement database-backed sessions and improve group management

Add database-backed session storage to fix session expiration on server restart

- Create AnonymousSession table with token, userId, expiresAt, and challenge fields

- Replace in-memory Map with Prisma queries in SessionStore

- Sessions now persist across server restarts and deployments

Fix group recovery and sign-out behavior

- Fix naming conflict between setRecentGroups state setter and helper function

- Groups now properly save to localStorage when logging in

- Sign-out only clears localStorage, preserving groups in database for recovery

- Groups restore automatically when logging back in with same account

Improve UI for logged-in users

- Move Restore from backup and Import from JSON to account dropdown menu

- Hide restore/import options from groups page for logged-in users

- Better organization of account management features

Add server-side authorization to group deletion

- Validate user session before allowing group deletion

- Verify group ownership through anonymousUserGroup table

- Return appropriate error codes (401 UNAUTHORIZED, 403 FORBIDDEN)

- Prevents unauthorized deletion even if UI is bypassed

* update sigin UI

* feat: support multiple passkeys per account with names

Add ability to register multiple passkeys per anonymous account with custom names

- Create new Passkey table with name, credentialId, publicKey, counter, lastUsedAt fields

- Migrate from single passkey storage on AnonymousUser to separate Passkey table

- Add passkey name input dialog when registering new passkeys

- Display list of all registered passkeys with creation dates

- Add individual passkey deletion with confirmation dialog

- Update lastUsedAt timestamp when passkey is used for authentication

- Add /api/anonymous-users/passkey/list endpoint to fetch user's passkeys

- Maintain backward compatibility with deprecated passkey fields on AnonymousUser

UI improvements:

- Show passkey list with name and creation date in account dialog

- Add 'Add passkey' button that opens name input dialog

- Individual remove button for each passkey

- Better UX with 'New account' button highlighted when generating username

- Changed 'Recover account' to 'Existing account' for clarity

* fix: update auth-options to use new Passkey table

- Change auth-options endpoint to fetch credentials from Passkey table
- Support multiple passkeys by returning all credential IDs for allowCredentials
- Fixes 'No passkey registered' error when authenticating

* fix: ensure user exists before creating passkey auth session

- Add upsert to create AnonymousUser if not exists before session creation
- Prevents foreign key constraint violation in AnonymousSession table
- Handle both authenticated and discoverable credential flows

* feat: enforce passphrase security requirements and separate change/reset flows

- Always require current passphrase for normal passphrase change
- Add separate reset flow for passkey-authenticated users
- Reset with passkey only requires new passphrase (no current)
- Add hasPassphrase flag to groups API response
- Update UI to conditionally show options based on passphrase state
- Add passkeyResetMode state to differentiate between change and reset
- Server-side validation enforces security requirements

* Address code review feedback: authorization, error handling, and cleanup optimization (#20)

* Initial plan

* Address code review comments: fix security and reliability issues

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Refactor: extract session retrieval helper and add temp user cleanup

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Improve error messages and add error logging for debugging

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Polish error message for clarity

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* test passkey fix

* try no username first

* test IOS

* update ux

* update UI remove import button

* finalise permissions

* additional permissoins

* Add remote JSON import flow with auto-association and iOS icon fixes

* Address security vulnerabilities and translations in remote JSON import (#22)

* Initial plan

* Add security improvements to remote JSON import route

- Add rate limiting to prevent SSRF abuse
- Add timeout (30s) to fetch operations
- Add groupId validation to prevent injection/path traversal
- Add SSRF protection (block private IPs, localhost, metadata endpoints)
- Add response size limit (10MB) and max expenses limit (10k)
- Add error handling to normalizeDate function
- Improve error logging in import-json-button.tsx
- Extract ASSOCIATED_GROUPS_KEY constant to shared file

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Translate import success messages to all supported languages

- pt-BR: Portuguese (Brazil)
- ru-RU: Russian
- ko: Korean
- id: Indonesian
- fr-FR: French
- zh-TW: Chinese Traditional
- ro: Romanian
- ja-JP: Japanese
- eu: Basque
- zh-CN: Chinese Simplified
- nl-NL: Dutch
- he: Hebrew
- cs-CZ: Czech
- uk-UA: Ukrainian
- tr-TR: Turkish
- pt: Portuguese
- pl-PL: Polish
- es: Spanish
- de-DE: German
- ca: Catalan
- fi: Finnish
- it-IT: Italian

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Address code review feedback on security improvements

- Fix IP validation to properly handle octets and validate ranges
- Remove redundant cloud metadata endpoint check (covered by link-local)
- Fix normalizeDate to throw error instead of using epoch as default
- Filter out expenses with invalid dates instead of setting to epoch
- Sanitize logging to avoid exposing sensitive data

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix log injection vulnerability in normalizeDate

Use template literal instead of string concatenation to prevent
potential log injection when logging the type of invalid date values.

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* fix json

* empty username when accessing the login screen

* Address security vulnerabilities and localization gaps in remote JSON import (#24)

* Initial plan

* Address code review feedback: SSRF protection, translations, and error handling

- Add protocol validation (http/https only) to validateRemoteUrl
- Enforce hard byte limit while reading response in fetchTrpcJson
- Prevent redirect-based SSRF by setting redirect: 'manual'
- Add all missing translation keys for hardcoded UI strings
- Change placeholder URL from spliit.app to neutral example.com
- Wrap syncAssociatedGroups in try/catch for best-effort handling
- Improve passkey error handling to differentiate error types (rate limit, not found, user cancel)
- Run prettier to fix inconsistent indentation

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Fix duplicate imports causing build failure in import-json-button.tsx (#25)

* Initial plan

* Fix duplicate imports in import-json-button.tsx

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* add whats new

* add whats new

* [WIP] Add end-user documentation and UX improvements (#27)

* Initial plan

* Address PR review comments: fix unused imports, update links, improve security and error handling

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* Improve icon choice and clarify HTML stripping behavior

Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>

* change icon

* update update with links to forked instance

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: carnach <26198260+carnach@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants