const cors = require('cors') const express = require('express') const app = express() const port = Number(process.argv[2]) || 3001 app.use(cors()) app.use(express.json()) function calendarTime(dayOffset, hour, minute = 0) { const value = new Date() value.setDate(value.getDate() + dayOffset) value.setHours(hour, minute, 0, 0) return value.getTime() } function isoTime(offsetMilliseconds) { return new Date(Date.now() + offsetMilliseconds).toISOString() } let authenticated = true let draft = null let mockBankBalance = 24787 let mockCashBalance = 2350 let nextBankTransactionId = 7 const mockBankTransactions = [ { id: 1, kind: 'transfer_in', amount: 3200, label: 'Sofia Turner', reference: 'mock-1', createdAt: Date.now() - 45 * 60 * 1000 }, { id: 2, kind: 'transfer_out', amount: 680, label: 'Vincent Cole', reference: 'mock-2', createdAt: Date.now() - 5 * 60 * 60 * 1000 }, { id: 3, kind: 'deposit', amount: 1250, label: '', reference: 'mock-3', createdAt: Date.now() - 25 * 60 * 60 * 1000 }, { id: 4, kind: 'transfer_out', amount: 420, label: 'Maya Brooks', reference: 'mock-4', createdAt: Date.now() - 50 * 60 * 60 * 1000 }, { id: 5, kind: 'withdrawal', amount: 300, label: '', reference: 'mock-5', createdAt: Date.now() - 76 * 60 * 60 * 1000 }, { id: 6, kind: 'transfer_in', amount: 950, label: 'Noah Bennett', reference: 'mock-6', createdAt: Date.now() - 120 * 60 * 60 * 1000 }, ] let contactSequence = 2 const contacts = [ { created_at: '2026-08-04 12:00:00', id: 'contact-1', name: 'Jenica Chong', phone_number: '5558675309', updated_at: '2026-08-04 12:00:00', }, ] const attachmentAssets = { gif: new Set(['celebrate', 'hearts', 'party', 'thumbs_up', 'wow']), image: new Set([ 'camera-1', 'camera-2', 'camera-3', 'city-lights', 'desert-road', 'ocean-air', 'sunset-drive', ]), video: new Set(['city-loop', 'ocean-loop', 'sunset-loop']), } const gifMocks = [ ['ICOgUNjpvO0PC', 'Cat reaction'], ['MDJ9IbxxvDUQM', 'Happy dog'], ['l0HlPystfePnAI3G8', 'Celebrate'], ['26ufdipQqU2lhNA4g', 'Wow'], ['3o7abKhOpu0NwenH3O', 'Perfect'], ['xT0xeJpnrWC4XWblEk', 'Party'], ['111ebonMs90YLu', 'Thumbs up'], ['5GoVLqeAOo6PK', 'Excited'], ['TdfyKrN7HGTIY', 'Happy dance'], ['14udF3WUwwGMaA', 'Surprised'], ['3o6Zt6ML6BklcajjsA', 'Applause'], ['13CoXDiaCcCoyk', 'Let us go'], ['R6gvnAxj2ISzJdbA63', 'Yes'], ['xUPGcEliCc7bETyfO8', 'Laughing'], ['26BRuo6sLetdllPAQ', 'Dancing'], ['l46CyJmS9KUbokzsI', 'Amazing'], ['g9582DNuQppxC', 'Celebration'], ['artj92V8o75VPL7AeQ', 'High five'], ] const accountDevices = [ { created_at: '2026-08-04 12:00:00', current: true, device_name: 'iFruit Phone', imei: '356938035643809', updated_at: '2026-08-04 12:00:00', }, ] const messages = [ { body: '## Welcome to iFruit Mail\n\nYour shared mailbox is **ready to use**.\n\n- Send formatted messages\n- Keep drafts on every linked device\n- Reply without losing the conversation\n\n> Tip: use the small formatting bar while composing.', created_at: '2026-08-04 11:30:00', folder: 'inbox', id: 1, is_read: false, message_id: 'b73f3872-54cc-4d74-a058-6caa24f0dfff', preview: 'Your shared mailbox is **ready to use**. Send formatted messages and keep drafts on every linked device.', recipients: ['demo@ifruit.com'], sender: 'support@ifruit.com', subject: 'Welcome to iFruit Mail', trashed_at: null, }, { body: 'The meet is behind the casino at nine. Bring the blue car.', created_at: '2026-08-04 10:15:00', folder: 'inbox', id: 2, is_read: true, message_id: '79042d13-f86d-4418-a490-8b558539c20b', preview: 'The meet is behind the casino at nine. Bring the blue car.', recipients: ['demo@ifruit.com', 'jamie@ifruit.com'], sender: 'morgan@ifruit.com', subject: 'Tonight', trashed_at: null, }, { body: 'I will be there.', created_at: '2026-08-03 18:05:00', folder: 'sent', id: 3, is_read: true, message_id: 'c4414c1e-5fe3-46ea-bc0b-b5b9785c91bd', preview: 'I will be there.', recipients: ['morgan@ifruit.com'], sender: 'demo@ifruit.com', subject: 'Re: Tonight', trashed_at: null, }, { body: 'This is an old message.', created_at: '2026-08-02 09:20:00', folder: 'inbox', id: 4, is_read: true, message_id: 'd99669ab-c1ac-42d4-8ed4-3c4fe7fa37fb', preview: 'This is an old message.', recipients: ['demo@ifruit.com'], sender: 'news@ifruit.com', subject: 'Old mail', trashed_at: '2026-08-04 08:00:00', }, ] const smsMessages = [ { body: 'Bin gleich am Würfelpark. Kommst du auch?', created_at: '2026-08-06 13:04:00', direction: 'received', id: 'sms-1', media_duration_ms: null, media_mime: null, media_payload: null, media_waveform: null, message_type: 'text', media_asset_id: null, read_at: null, recipient_number: '5551234567', sender_number: '5558675309', }, { body: 'Ja, gib mir fünf Minuten.', created_at: '2026-08-06 13:05:00', direction: 'sent', id: 'sms-2', media_duration_ms: null, media_mime: null, media_payload: null, media_waveform: null, message_type: 'text', media_asset_id: null, read_at: null, recipient_number: '5558675309', sender_number: '5551234567', }, ] const darkChatProfile = { id: 1, darkId: 'dark:7X4K-P92D', inviteCode: 'DC-7X4K-NOVA', alias: 'Nightshade', avatarSeed: 267, notificationMode: 'private', activityVisible: false, createdAt: '2026-08-01 21:20:00', } const darkChatPeers = [ { id: 2, darkId: 'dark:N0VA-41KQ', alias: 'Nova', originalAlias: 'Nova', avatarSeed: 142, activityVisible: true, isContact: true, blocked: false }, { id: 3, darkId: 'dark:ECH0-77LM', alias: 'Echo', originalAlias: 'Echo', avatarSeed: 311, activityVisible: false, isContact: true, blocked: false }, ] const darkChatConversations = [ { id: 'dc-conversation-nova-0000-000000000001', peer: darkChatPeers[0], disappearingSeconds: 3600, notificationsEnabled: true, readReceipts: true, blockedByPeer: false, createdAt: '2026-08-03 22:12:00', }, ] const darkChatMessages = [ { id: 'dc-message-00000000-0000-000000000001', conversationId: darkChatConversations[0].id, direction: 'received', senderProfileId: 2, messageType: 'text', body: 'The east gate is clear. Are you close?', reactions: {}, createdAt: '2026-08-06 22:42:00', readAt: '2026-08-06 22:43:00', }, { id: 'dc-message-00000000-0000-000000000002', conversationId: darkChatConversations[0].id, direction: 'sent', senderProfileId: 1, messageType: 'text', body: 'Two minutes. Keep this channel quiet. 🟣', reactions: { 2: '👍' }, createdAt: '2026-08-06 22:43:00', readAt: '2026-08-06 22:43:30', }, { id: 'dc-message-00000000-0000-000000000003', conversationId: darkChatConversations[0].id, direction: 'received', senderProfileId: 2, messageType: 'gif', body: '', mediaPayload: 'https://media.giphy.com/media/ICOgUNjpvO0PC/giphy.gif', reactions: {}, createdAt: '2026-08-06 22:44:00', readAt: null, }, ] function darkChatBootstrap() { return { profile: darkChatProfile, contacts: darkChatPeers.filter((peer) => peer.isContact).map((peer) => ({ ...peer, createdAt: '2026-08-03 22:12:00' })), conversations: darkChatConversations.map((conversation) => { const thread = darkChatMessages.filter((message) => message.conversationId === conversation.id) const last = thread.at(-1) return { id: conversation.id, peer: conversation.peer, disappearingSeconds: conversation.disappearingSeconds, blocked: conversation.peer.blocked, lastMessage: last?.body ?? '', lastMessageType: last?.messageType ?? 'system', lastMessageAt: last?.createdAt ?? conversation.createdAt, unread: thread.filter((message) => message.direction === 'received' && !message.readAt).length, } }), } } const marketplaceListings = [ { id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01', seller_account_id: 2, seller_name: 'morgan', seller_since: '2026-07-02 10:00:00', seller_active: 2, title: 'Sultan RS in excellent condition', description: 'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.', category: 'vehicles', item_condition: 'very_good', price_type: 'negotiable', price: 185000, district: 'vinewood', status: 'active', revision: 1, show_phone: 0, phone_number: null, created_at: '2026-08-06 09:20:00', updated_at: '2026-08-06 09:20:00', expires_at: '2026-08-13 09:20:00', image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', is_favorite: false, images: [ { media_id: 'capture-car', gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', sort_order: 1, }, { media_id: 'city-lights', gradient: 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)', sort_order: 2, }, { media_id: 'ocean-air', gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', sort_order: 3, }, ], }, { id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf02', seller_account_id: 3, seller_name: 'jamie', seller_since: '2026-06-14 10:00:00', seller_active: 1, title: 'Modern apartment near Vespucci', description: 'Bright apartment with a city view, underground parking and modern furniture. Available immediately after viewing.', category: 'property', item_condition: 'very_good', price_type: 'fixed', price: 420000, district: 'vespucci', status: 'active', revision: 1, show_phone: 0, phone_number: null, created_at: '2026-08-05 17:10:00', updated_at: '2026-08-05 17:10:00', expires_at: '2026-08-12 17:10:00', image: null, is_favorite: true, images: [], }, { id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf03', seller_account_id: 4, seller_name: 'citytech', seller_since: '2026-05-22 10:00:00', seller_active: 3, title: 'Nearly new gaming laptop', description: 'Fast gaming laptop including charger and carrying bag. Runs quietly and can be tested before purchase.', category: 'electronics', item_condition: 'very_good', price_type: 'fixed', price: 3500, district: 'los_santos', status: 'reserved', revision: 2, show_phone: 0, phone_number: null, created_at: '2026-08-05 12:30:00', updated_at: '2026-08-06 08:00:00', expires_at: '2026-08-12 12:30:00', image: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', is_favorite: false, images: [ { media_id: 'ocean-air', gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', sort_order: 1, }, ], }, { id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04', seller_account_id: 1, seller_name: 'demo', seller_since: '2026-08-04 12:00:00', seller_active: 1, title: 'Complete mechanic tool set', description: 'Complete mechanic tool set with trolley, sockets and diagnostic equipment. Everything is clean and ready for work.', category: 'tools', item_condition: 'very_good', price_type: 'fixed', price: 7800, district: 'south_los_santos', status: 'active', revision: 1, show_phone: 1, phone_number: '5551234567', created_at: '2026-08-06 10:45:00', updated_at: '2026-08-06 10:45:00', expires_at: '2026-08-13 10:45:00', image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)', is_favorite: false, images: [ { media_id: 'capture-tools', gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)', sort_order: 1, }, ], }, ] let linkedAccount = { devices: accountDevices, email: 'demo@ifruit.com', id: 1, } let mockNotes = [ { body: 'Meet Morgan in Vinewood and inspect the Sultan RS.', createdAt: Date.now() - 3_600_000, id: 'demo-note-marketplace', pinned: true, revision: 1, title: 'CityMarkt viewing', updatedAt: Date.now() - 3_600_000, }, { body: 'Repair kit\nRadio\nFlashlight\nTwo bottles of water', createdAt: Date.now() - 7_200_000, id: 'demo-note-packing-list', pinned: true, revision: 2, title: 'Patrol equipment', updatedAt: Date.now() - 1_800_000, }, { body: 'Call Jamie after the garage inspection and confirm the final price.', createdAt: Date.now() - 86_400_000, id: 'demo-note-call-jamie', pinned: false, revision: 1, title: 'Call Jamie', updatedAt: Date.now() - 86_400_000, }, { body: 'Observatory, Vespucci Pier, Paleto lookout, Sandy airfield.', createdAt: Date.now() - 172_800_000, id: 'demo-note-photo-spots', pinned: false, revision: 1, title: 'Photo locations', updatedAt: Date.now() - 172_800_000, }, ] let calendarEvents = [ { endsAt: calendarTime(0, 10), id: 'calendar-team-briefing', note: 'Meet in the Mission Row briefing room. Bring the updated patrol plan.', remindedAt: null, reminderMinutes: 30, revision: 1, startsAt: calendarTime(0, 9), title: 'Team briefing', }, { endsAt: calendarTime(0, 21, 30), id: 'calendar-citymarkt-viewing', note: 'Inspect the blue Sultan RS at the Vinewood garage and take a short test drive.', remindedAt: null, reminderMinutes: 60, revision: 2, startsAt: calendarTime(0, 20), title: 'CityMarkt vehicle viewing', }, { endsAt: calendarTime(1, 15, 30), id: 'calendar-garage-service', note: 'Routine service, wheel alignment and diagnostics.', remindedAt: null, reminderMinutes: 10, revision: 1, startsAt: calendarTime(1, 14), title: 'Garage appointment', }, { endsAt: calendarTime(3, 22), id: 'calendar-birthday-dinner', note: 'Table is booked on the terrace. Pick up the cake first.', remindedAt: null, reminderMinutes: 1440, revision: 1, startsAt: calendarTime(3, 19, 30), title: "Jamie's birthday dinner", }, { endsAt: calendarTime(-2, 18), id: 'calendar-past-beach-cruise', note: 'Vespucci Pier to the Paleto lookout.', remindedAt: calendarTime(-2, 16), reminderMinutes: 60, revision: 1, startsAt: calendarTime(-2, 17), title: 'Beach cruise', }, ] const deviceData = { alarms: { payload: [ { enabled: true, id: 'demo-weekday-alarm', lastTriggeredMinute: null, note: 'Morning patrol', sound: 'radar', time: '07:15', weekdays: [1, 2, 3, 4, 5], }, { enabled: true, id: 'demo-garage-alarm', lastTriggeredMinute: null, note: 'Garage appointment', sound: 'chimes', time: '13:30', weekdays: [], }, { enabled: false, id: 'demo-weekend-alarm', lastTriggeredMinute: null, note: 'Weekend drive', sound: 'apex', time: '09:30', weekdays: [0, 6], }, ], revision: 3, }, apps: { payload: { claimedApps: [], }, revision: 2, }, games: { payload: { memory: { best: { small: { moves: 12, timeMs: 38_400 }, medium: { moves: 29, timeMs: 92_100 }, }, soundEnabled: true, }, minesweeper: { best: { quick: { timeMs: 44_200 }, classic: { timeMs: 128_700 }, }, elapsedMs: 0, game: null, soundEnabled: true, }, 'neon-drop': { bestLines: 18, bestScore: 12_450, soundEnabled: true }, 'number-merge': { bestScore: 8_920, game: null, highestTile: 1024, soundEnabled: true, }, 'sky-flappy': { design: 'neon', highScore: 23, soundEnabled: true }, snake: { highScore: 17, speed: 'normal' }, 'tower-stack': { highHeight: 21, highScore: 4_850, soundEnabled: true }, }, revision: 4, }, media: { payload: { captures: [ { capturedAt: Date.now() - 1_200_000, gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', id: 'capture-car', titleKey: 'Apps.photos.samples.capture', }, { capturedAt: Date.now() - 5_400_000, gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)', id: 'capture-tools', titleKey: 'Apps.photos.samples.capture', }, ], claimedApps: [], }, revision: 2, }, settings: { payload: { settings: { airplaneMode: false, appearanceMode: 'automatic', frame: 'black', notificationDurationSeconds: 10, notificationSound: 'chime', notificationVolume: 70, phoneScale: 100, ringtone: 'skyline', ringtoneVolume: 80, streamerMode: false, wallpaper: 'midnight', }, version: 1, }, revision: 1, }, } let mockContacts = [ { created_at: isoTime(-14 * 86_400_000), id: 'contact-morgan', name: 'Morgan Reed', phone_number: '5550192847', updated_at: isoTime(-2 * 86_400_000), }, { created_at: isoTime(-30 * 86_400_000), id: 'contact-jamie', name: 'Jamie Chen', phone_number: '5559876543', updated_at: isoTime(-30 * 86_400_000), }, { created_at: isoTime(-5 * 86_400_000), id: 'contact-mechanic', name: 'Downtown Customs', phone_number: '5550100101', updated_at: isoTime(-5 * 86_400_000), }, { created_at: isoTime(-3 * 86_400_000), id: 'contact-taxi', name: 'Los Santos Taxi', phone_number: '5552222222', updated_at: isoTime(-3 * 86_400_000), }, ] let recentCalls = [ { call_id: 'call-morgan-incoming', created_at: isoTime(-18 * 60_000), direction: 'incoming', duration_seconds: 246, id: 1, other_number: '5550192847', status: 'completed', }, { call_id: 'call-jamie-missed', created_at: isoTime(-95 * 60_000), direction: 'incoming', duration_seconds: 0, id: 2, other_number: '5559876543', status: 'missed', }, { call_id: 'call-customs-outgoing', created_at: isoTime(-25 * 60 * 60_000), direction: 'outgoing', duration_seconds: 83, id: 3, other_number: '5550100101', status: 'completed', }, { call_id: 'call-unknown-declined', created_at: isoTime(-2 * 86_400_000), direction: 'incoming', duration_seconds: 0, id: 4, other_number: '5554040404', status: 'declined', }, ] let mockMedia = [ { createdAt: Date.now() - 60_000, id: 1, mediaType: 'photo', url: 'https://picsum.photos/seed/sky-phone-1/600/800', }, { createdAt: Date.now() - 120_000, id: 2, mediaType: 'video', url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', }, ] const marketplaceInquiries = [ { id: '4903b923-409a-437e-971f-b7a2b10e9e31', listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01', seller_account_id: 2, buyer_account_id: 1, seller_name: 'morgan', buyer_name: 'demo', title: 'Sultan RS in excellent condition', price: 185000, price_type: 'negotiable', status: 'active', image: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', other_name: 'morgan', last_message: 'Sure, come by the Vinewood garage around 8 PM.', offer_id: 2, offer_amount: 178000, offer_proposer_account_id: 2, offer_status: 'pending', offer_revision: 2, unread: 2, updated_at: '2026-08-06 11:42:00', }, { id: '4903b923-409a-437e-971f-b7a2b10e9e32', listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04', seller_account_id: 1, buyer_account_id: 3, seller_name: 'demo', buyer_name: 'jamie', title: 'Complete mechanic tool set', price: 7800, price_type: 'fixed', status: 'active', image: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)', other_name: 'jamie', last_message: 'I can collect it today and pay the full price.', offer_id: 3, offer_amount: 7000, offer_proposer_account_id: 3, offer_status: 'pending', offer_revision: 1, unread: 3, updated_at: '2026-08-06 11:55:00', }, ] const marketplaceMessages = [ { id: 1, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31', sender_account_id: 1, body: 'Hi, is the Sultan still available?', created_at: '2026-08-06 11:35:00', read_at: '2026-08-06 11:36:00', }, { id: 2, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31', sender_account_id: 2, body: 'Yes, it is. You can also take it for a short test drive.', created_at: '2026-08-06 11:38:00', read_at: '2026-08-06 11:39:00', }, { id: 3, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31', sender_account_id: 2, body: 'Sure, come by the Vinewood garage around 8 PM.', created_at: '2026-08-06 11:42:00', read_at: null, }, { id: 4, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32', sender_account_id: 3, body: 'Hello, does the diagnostic equipment work with every vehicle?', created_at: '2026-08-06 11:51:00', read_at: null, }, { id: 5, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32', sender_account_id: 3, body: 'I can collect it today and pay the full price.', created_at: '2026-08-06 11:55:00', read_at: null, }, ] const marketplaceOffers = [ { id: 1, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31', proposer_account_id: 1, amount: 170000, status: 'countered', read_at: '2026-08-06 11:43:00', response_read_at: null, created_at: '2026-08-06 11:40:00', updated_at: '2026-08-06 11:44:00', }, { id: 2, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e31', proposer_account_id: 2, amount: 178000, status: 'pending', read_at: null, response_read_at: null, created_at: '2026-08-06 11:44:00', updated_at: '2026-08-06 11:44:00', }, { id: 3, inquiry_id: '4903b923-409a-437e-971f-b7a2b10e9e32', proposer_account_id: 3, amount: 7000, status: 'pending', read_at: null, response_read_at: null, created_at: '2026-08-06 11:56:00', updated_at: '2026-08-06 11:56:00', }, ] const pagesPosts = [ { id: 'pages-citymarkt-1', account_id: 2, author_name: 'morgan', source_type: 'citymarkt', citymarkt_listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf01', title: 'Sultan RS in excellent condition', body: 'Clean Sultan RS with fresh service, tidy interior and no known damage. Viewing and test drive available in Vinewood.', category: 'citymarkt', district: 'vinewood', created_at: Date.parse('2026-08-06T12:50:00Z'), like_count: 24, images: [ { media_id: 'capture-car', gradient: 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', sort_order: 1, }, ], }, { id: 'pages-1', account_id: 2, author_name: 'morgan', source_type: 'personal', citymarkt_listing_id: null, title: 'Best sunset view above Vinewood', body: 'Take the small trail behind the observatory shortly before sunset. The view across Los Santos is incredible and there is enough space to park two cars.', category: 'recommendation', district: 'vinewood', created_at: Date.parse('2026-08-06T12:20:00Z'), like_count: 18, images: [ { media_id: 'sunset-drive', gradient: 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)', sort_order: 1, }, ], }, { id: 'pages-2', account_id: 1, author_name: 'demo', source_type: 'personal', citymarkt_listing_id: null, title: 'Looking for people for a beach cruise', body: 'Meeting at Vespucci Pier tonight. Bring a clean car and a good playlist. Everyone is welcome.', category: 'community', district: 'vespucci', created_at: Date.parse('2026-08-06T11:10:00Z'), like_count: 7, images: [ { media_id: 'ocean-air', gradient: 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', sort_order: 1, }, ], }, { id: 'pages-3', account_id: 3, author_name: 'jamie', source_type: 'personal', citymarkt_listing_id: null, title: 'Mobile repair help around Sandy Shores', body: 'I can help with small repairs and jump starts around Sandy Shores this afternoon. Send me a message when you see me nearby.', category: 'service', district: 'sandy_shores', created_at: Date.parse('2026-08-05T19:40:00Z'), like_count: 12, images: [], }, ] const pagesReactions = [ { post_id: 'pages-1', account_id: 1, kind: 'like' }, { post_id: 'pages-3', account_id: 1, kind: 'save' }, ] function pageView(post) { const listing = marketplaceListings.find( (item) => item.id === post.citymarkt_listing_id, ) return { ...post, citymarkt_price: listing?.price ?? null, image: post.images[0]?.gradient ?? null, is_liked: pagesReactions.some( (item) => item.post_id === post.id && item.account_id === 1 && item.kind === 'like', ), is_owner: authenticated && post.account_id === 1, is_saved: pagesReactions.some( (item) => item.post_id === post.id && item.account_id === 1 && item.kind === 'save', ), } } function counts() { return { drafts: draft ? 1 : 0, inbox: messages.filter( (item) => item.folder === 'inbox' && !item.trashed_at, ).length, sent: messages.filter((item) => item.folder === 'sent' && !item.trashed_at) .length, trash: messages.filter((item) => item.trashed_at).length, unread: messages.filter( (item) => item.folder === 'inbox' && !item.trashed_at && !item.is_read, ).length, } } app.post('/api/:endpoint', (request, response) => { console.log(`[NUI] ${request.params.endpoint}`, request.body) const endpoint = request.params.endpoint const bankingOverview = () => ({ bank: mockBankBalance, cash: mockCashBalance, currency: '$', playerId: 42, playerName: 'Alex Morgan', transactions: mockBankTransactions, }) if (endpoint === 'banking:overview') { response.json({ success: true, data: bankingOverview() }) return } if (endpoint === 'banking:transfer') { const amount = Number(request.body.amount) if (!Number.isSafeInteger(amount) || amount <= 0) { response.json({ success: false, error: 'invalid_request' }) return } if (mockBankBalance < amount) { response.json({ success: false, error: 'insufficient_funds' }) return } const kind = 'transfer_out' mockBankBalance -= amount mockBankTransactions.unshift({ amount, createdAt: Date.now(), id: nextBankTransactionId++, kind, label: kind === 'transfer_out' ? `Player #${request.body.target}` : '', reference: `mock-${Date.now()}`, }) response.json({ success: true, data: bankingOverview() }) return } if (endpoint === 'weather:get') { response.json({ success: true, data: { clock: { year: 2026, month: 8, day: 5, hour: 17, minute: 20 }, condition: 'partly_cloudy', rainLevel: 0.08, region: 'los_santos', windSpeed: 3.2, }, }) return } if (endpoint === 'map:getPlayerCoords') { response.json({ success: true, data: { coords: { x: -75.2, y: -818.9, z: 326.2 } }, }) return } if (endpoint === 'darkchat:bootstrap') { response.json({ success: true, data: darkChatBootstrap() }) return } if (endpoint === 'darkchat:update-profile') { darkChatProfile.alias = String(request.body.alias ?? darkChatProfile.alias).trim() darkChatProfile.notificationMode = request.body.notificationMode ?? 'private' darkChatProfile.activityVisible = Boolean(request.body.activityVisible) response.json({ success: true, data: darkChatProfile }) return } if (endpoint === 'darkchat:start') { const identifier = String(request.body.identifier ?? '').toUpperCase() const peer = darkChatPeers.find((item) => item.darkId.toUpperCase() === identifier) ?? (identifier === 'DC-ECH0-77LM' ? darkChatPeers[1] : null) if (!peer) { response.json({ success: false, error: 'profile_not_found' }) return } let conversation = darkChatConversations.find((item) => item.peer.id === peer.id) if (!conversation) { conversation = { id: `dc-conversation-${Date.now()}`, peer, disappearingSeconds: 0, notificationsEnabled: true, readReceipts: true, blockedByPeer: false, createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '), } darkChatConversations.push(conversation) } response.json({ success: true, data: { conversationId: conversation.id } }) return } if (endpoint === 'darkchat:thread') { const conversation = darkChatConversations.find((item) => item.id === request.body.conversationId) if (!conversation) { response.json({ success: false, error: 'conversation_not_found' }) return } const thread = darkChatMessages.filter((message) => message.conversationId === conversation.id) for (const message of thread) { if (message.direction === 'received') message.readAt = message.readAt ?? new Date().toISOString() } response.json({ success: true, data: { conversation, messages: thread.map(({ mediaSecret, ...message }) => message) } }) return } if (endpoint === 'darkchat:send') { const conversation = darkChatConversations.find((item) => item.id === request.body.conversationId) if (!conversation || conversation.peer.blocked) { response.json({ success: false, error: conversation ? 'blocked' : 'conversation_not_found' }) return } const messageType = request.body.messageType ?? 'text' const body = String(request.body.body ?? '') if ((messageType === 'text' || messageType === 'emoji') && !body.trim()) { response.json({ success: false, error: 'invalid_message' }) return } const reply = darkChatMessages.find((message) => message.id === request.body.replyToId) const message = { id: `dc-message-${Date.now()}`, conversationId: conversation.id, direction: 'sent', senderProfileId: darkChatProfile.id, messageType, body, mediaPayload: messageType === 'gif' ? request.body.mediaPayload : undefined, mediaSecret: messageType === 'voice' ? request.body.mediaPayload : undefined, mediaMime: request.body.mediaMime, mediaDurationMs: request.body.mediaDurationMs, mediaWaveform: request.body.mediaWaveform, replyToId: request.body.replyToId, replyBody: reply?.body, reactions: {}, createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '), readAt: null, } darkChatMessages.push(message) const { mediaSecret, ...publicMessage } = message response.json({ success: true, data: publicMessage }) return } if (endpoint === 'darkchat:media') { const message = darkChatMessages.find((item) => item.id === request.body.messageId && item.messageType === 'voice') response.json(message?.mediaSecret ? { success: true, data: { mime: message.mediaMime, payload: message.mediaSecret } } : { success: false, error: 'message_not_found' }) return } if (endpoint === 'darkchat:react') { const message = darkChatMessages.find((item) => item.id === request.body.messageId) if (message) { const current = message.reactions[String(darkChatProfile.id)] if (current === request.body.reaction) delete message.reactions[String(darkChatProfile.id)] else message.reactions[String(darkChatProfile.id)] = request.body.reaction } response.json({ success: Boolean(message), error: message ? undefined : 'message_not_found' }) return } if (endpoint === 'darkchat:message-action') { const index = darkChatMessages.findIndex((item) => item.id === request.body.messageId) if (index >= 0 && request.body.action === 'delete_me') darkChatMessages.splice(index, 1) else if (index >= 0 && request.body.action === 'delete_all') { darkChatMessages[index].messageType = 'system' darkChatMessages[index].body = 'message_deleted' darkChatMessages[index].mediaPayload = undefined } response.json({ success: index >= 0 }) return } if (endpoint === 'darkchat:update-conversation') { const conversation = darkChatConversations.find((item) => item.id === request.body.conversationId) if (conversation) { conversation.disappearingSeconds = Number(request.body.disappearingSeconds) conversation.notificationsEnabled = Boolean(request.body.notificationsEnabled) conversation.readReceipts = Boolean(request.body.readReceipts) darkChatMessages.push({ id: `dc-system-${Date.now()}`, conversationId: conversation.id, direction: 'received', messageType: 'system', body: `timer_changed:${conversation.disappearingSeconds}`, reactions: {}, createdAt: new Date().toISOString().slice(0, 19).replace('T', ' '), }) } response.json({ success: Boolean(conversation) }) return } if (endpoint === 'darkchat:add-contact' || endpoint === 'darkchat:remove-contact') { const conversation = darkChatConversations.find((item) => item.id === request.body.conversationId) if (conversation) { conversation.peer.isContact = endpoint === 'darkchat:add-contact' if (request.body.alias) conversation.peer.alias = String(request.body.alias) } response.json({ success: Boolean(conversation) }) return } if (endpoint === 'darkchat:block') { const conversation = darkChatConversations.find((item) => item.id === request.body.conversationId) if (conversation) conversation.peer.blocked = Boolean(request.body.blocked) response.json({ success: Boolean(conversation) }) return } if (endpoint === 'darkchat:clear') { for (let index = darkChatMessages.length - 1; index >= 0; index -= 1) { if (darkChatMessages[index].conversationId === request.body.conversationId) darkChatMessages.splice(index, 1) } response.json({ success: true }) return } if (endpoint === 'darkchat:report') { response.json({ success: true }) return } if (endpoint === 'messages:conversations') { const grouped = new Map() for (const message of [...smsMessages].reverse()) { const phoneNumber = message.direction === 'sent' ? message.recipient_number : message.sender_number const conversation = grouped.get(phoneNumber) if (conversation) { if (message.direction === 'received' && !message.read_at) conversation.unread += 1 continue } grouped.set(phoneNumber, { lastMessage: message.body, lastMessageAt: message.created_at, lastMessageType: message.message_type, phoneNumber, unread: message.direction === 'received' && !message.read_at ? 1 : 0, }) } response.json({ success: true, data: [...grouped.values()] }) return } if (endpoint === 'messages:gifs') { const offset = Math.max(0, Number(request.body.offset ?? 0)) const pageSize = 6 const results = gifMocks.slice(offset, offset + pageSize).map(([id, title]) => ({ height: 200, id, previewUrl: `https://media.giphy.com/media/${id}/200w.gif`, title, url: `https://media.giphy.com/media/${id}/giphy.gif`, width: 200, })) response.json({ success: true, data: { hasMore: offset + results.length < gifMocks.length, nextOffset: offset + results.length, results, }, }) return } if (endpoint === 'development:bootstrap') { response.json({ success: true, data: { account: linkedAccount, device: { data: deviceData, imei: '356938035643809', name: 'Personal iFruit Phone', sim: { id: 'development-sim', number: '5551234567', registered: true, type: 'registered', }, }, notes: mockNotes, token: 'development', }, }) return } if (endpoint === 'messages:thread') { const number = String(request.body.phoneNumber) const thread = smsMessages.filter( (message) => message.sender_number === number || message.recipient_number === number, ) for (const message of thread) { if (message.direction === 'received') message.read_at = message.read_at ?? '2026-08-06 13:06:00' } response.json({ success: true, data: thread.map(({ media_payload, ...message }) => ({ ...message, media_asset_id: ['image', 'gif', 'video'].includes(message.message_type) ? media_payload : null, })), }) return } if (endpoint === 'messages:media') { const message = smsMessages.find( (item) => item.id === request.body.id && item.message_type === 'voice', ) response.json( message ? { success: true, data: { mime: message.media_mime, payload: message.media_payload, }, } : { success: false, error: 'message_not_found' }, ) return } if (endpoint === 'messages:send') { const body = String(request.body.body ?? '').trim() const phoneNumber = String(request.body.phoneNumber ?? '') const messageType = request.body.messageType ?? 'text' const isAttachment = ['image', 'gif', 'video'].includes(messageType) const requestedAttachmentId = String(request.body.mediaAssetId ?? '') const selectedMedia = /^\d+$/.test(requestedAttachmentId) ? mockMedia.find((item) => String(item.id) === requestedAttachmentId) : null const attachmentId = selectedMedia?.url ?? requestedAttachmentId if ( !phoneNumber || (messageType === 'text' && !body) || (messageType === 'voice' && !request.body.mediaPayload) || (isAttachment && !attachmentAssets[messageType].has(attachmentId) && !attachmentId.startsWith('https://') && !attachmentId.startsWith('data:image/')) ) { response.json({ success: false, error: 'invalid_message' }) return } const message = { body, created_at: new Date().toISOString().slice(0, 19).replace('T', ' '), direction: 'sent', id: `sms-${Date.now()}`, media_duration_ms: ['voice', 'video'].includes(messageType) ? request.body.mediaDurationMs ?? null : null, media_mime: messageType === 'voice' ? request.body.mediaMime : messageType === 'image' ? 'image/jpeg' : messageType === 'gif' ? 'image/gif' : messageType === 'video' ? 'video/mp4' : null, media_payload: messageType === 'voice' ? request.body.mediaPayload : isAttachment ? attachmentId : null, media_waveform: messageType === 'voice' ? request.body.mediaWaveform : null, message_type: messageType, media_asset_id: isAttachment ? attachmentId : null, read_at: null, recipient_number: phoneNumber, sender_number: '5551234567', } smsMessages.push(message) const { media_payload, ...publicMessage } = message response.json({ success: true, data: publicMessage }) return } if (endpoint === 'messages:delete') { const phoneNumbers = new Set( Array.isArray(request.body.phoneNumbers) ? request.body.phoneNumbers.map(String) : [], ) for (let index = smsMessages.length - 1; index >= 0; index -= 1) { const message = smsMessages[index] const otherNumber = message.direction === 'sent' ? message.recipient_number : message.sender_number if (phoneNumbers.has(otherNumber)) smsMessages.splice(index, 1) } response.json({ success: true }) return } if (endpoint === 'contacts:list') { response.json({ success: true, data: contacts }) return } if (endpoint === 'contacts:save') { const name = String(request.body.name ?? '').trim() const phoneNumber = String(request.body.phoneNumber ?? '').trim() if (!name || !phoneNumber) { response.json({ success: false, error: 'invalid_contact' }) return } let contact = contacts.find((item) => item.id === request.body.id) if (contact) { contact.name = name contact.phone_number = phoneNumber contact.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ') } else { const now = new Date().toISOString().slice(0, 19).replace('T', ' ') contact = { created_at: now, id: `contact-${contactSequence++}`, name, phone_number: phoneNumber, updated_at: now, } contacts.push(contact) } response.json({ success: true, data: contact }) return } if (endpoint === 'contacts:delete') { const index = contacts.findIndex((item) => item.id === request.body.id) if (index >= 0) contacts.splice(index, 1) response.json({ success: true }) return } if (endpoint === 'media:config') { response.json({ success: true, data: { videoBitrateKbps: 1500 } }) return } if (endpoint === 'gallery:list') { if (request.body.mockState === 'error') { response.json({ success: false, error: 'service_unavailable' }) return } if (request.body.mockState === 'empty') { response.json({ success: true, data: [] }) return } const filtered = request.body.mediaType ? mockMedia.filter((item) => item.mediaType === request.body.mediaType) : mockMedia const offset = Number(request.body.offset) || 0 const limit = Number(request.body.limit) || 30 response.json({ success: true, data: filtered.slice(offset, offset + limit), }) return } if ( endpoint === 'media:requestUpload' || endpoint === 'media:completeUpload' || endpoint === 'media:failUpload' || endpoint === 'media:cancelUpload' ) { response.json({ success: true }) return } if (endpoint === 'gallery:delete') { mockMedia = mockMedia.filter((item) => item.id !== Number(request.body.id)) response.json({ success: true }) return } if (endpoint === 'account:login' || endpoint === 'account:register') { authenticated = true linkedAccount = { devices: accountDevices, email: request.body.email.includes('@') ? request.body.email : `${request.body.email}@ifruit.com`, id: 1, } response.json({ success: true, data: linkedAccount }) return } if (endpoint === 'account:logout') { authenticated = false linkedAccount = null response.json({ success: true }) return } if (endpoint === 'account:devices') { response.json({ success: true, data: accountDevices }) return } if (endpoint === 'account:remove-device') { response.json({ success: true, data: accountDevices }) return } if (endpoint === 'device:save') { const current = deviceData[request.body.namespace] const revision = (current?.revision ?? 0) + 1 deviceData[request.body.namespace] = { payload: request.body.payload, revision, } response.json({ success: true, data: { revision } }) return } if (endpoint === 'device:factory-reset') { authenticated = false linkedAccount = null mockNotes = [] mockMedia = [] calendarEvents = [] for (const key of Object.keys(deviceData)) delete deviceData[key] response.json({ success: true }) return } if (endpoint === 'contacts:list') { response.json({ success: true, data: mockContacts }) return } if (endpoint === 'contacts:save') { const now = new Date().toISOString() const existing = mockContacts.find( (contact) => contact.id === request.body.id, ) if (existing) { Object.assign(existing, { name: request.body.name, phone_number: request.body.phoneNumber, updated_at: now, }) response.json({ success: true, data: existing }) return } const contact = { created_at: now, id: `contact-${Date.now()}`, name: request.body.name, phone_number: request.body.phoneNumber, updated_at: now, } mockContacts.push(contact) response.json({ success: true, data: contact }) return } if (endpoint === 'contacts:delete') { mockContacts = mockContacts.filter( (contact) => contact.id !== request.body.id, ) response.json({ success: true }) return } if (endpoint === 'calls:recents') { response.json({ success: true, data: recentCalls }) return } if (endpoint === 'calls:dial') { const id = `call-${Date.now()}` recentCalls.unshift({ call_id: id, created_at: Date.now(), direction: 'outgoing', duration_seconds: 0, id: recentCalls.length + 1, other_number: request.body.phoneNumber, status: 'completed', }) response.json({ success: true, data: { direction: 'outgoing', id, otherNumber: request.body.phoneNumber, startedAt: Date.now(), state: 'ringing', }, }) return } if ( endpoint === 'calls:answer' || endpoint === 'calls:decline' || endpoint === 'calls:hangup' ) { response.json({ success: true }) return } if (endpoint.startsWith('calendar:') && !authenticated) { response.json({ success: false, error: 'not_authenticated' }) return } if (endpoint === 'calendar:list') { const startsAt = Number(request.body.startsAt) * 1000 const endsAt = Number(request.body.endsAt) * 1000 response.json({ success: true, data: calendarEvents.filter( (event) => event.endsAt >= startsAt && event.startsAt < endsAt, ), }) return } if (endpoint === 'calendar:create') { const id = `calendar-${Date.now()}` calendarEvents.push({ ...request.body, endsAt: Number(request.body.endsAt) * 1000, id, remindedAt: null, revision: 1, startsAt: Number(request.body.startsAt) * 1000, }) response.json({ success: true, data: { id } }) return } if (endpoint === 'calendar:update') { const index = calendarEvents.findIndex( (event) => event.id === request.body.id, ) if (index < 0 || calendarEvents[index].revision !== request.body.revision) { response.json({ success: false, error: 'conflict' }) return } calendarEvents[index] = { ...calendarEvents[index], ...request.body, endsAt: Number(request.body.endsAt) * 1000, remindedAt: null, revision: calendarEvents[index].revision + 1, startsAt: Number(request.body.startsAt) * 1000, } response.json({ success: true }) return } if (endpoint === 'calendar:delete') { calendarEvents = calendarEvents.filter( (event) => event.id !== request.body.id, ) response.json({ success: true }) return } if (endpoint === 'pages:list') { const query = String(request.body.search ?? '').toLowerCase() let items = pagesPosts if (request.body.category && request.body.category !== 'all') items = items.filter((item) => item.category === request.body.category) if (query) items = items.filter((item) => `${item.title} ${item.body}`.toLowerCase().includes(query), ) if (request.body.saved) items = items.filter((post) => pagesReactions.some( (reaction) => reaction.post_id === post.id && reaction.account_id === 1 && reaction.kind === 'save', ), ) response.json({ success: true, data: { hasMore: false, items: items.map(pageView), offset: 0 }, }) return } if (endpoint === 'pages:get') { const post = pagesPosts.find((item) => item.id === request.body.id) response.json( post ? { success: true, data: pageView(post) } : { success: false, error: 'post_not_found' }, ) return } if (endpoint.startsWith('pages:') && !authenticated) { response.json({ success: false, error: 'not_authenticated' }) return } if (endpoint === 'pages:list-own') { response.json({ success: true, data: { hasMore: false, items: pagesPosts.filter((item) => item.account_id === 1).map(pageView), offset: 0, }, }) return } if (endpoint === 'pages:create') { const gradients = { 'sunset-drive': 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)', 'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', 'city-lights': 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)', 'desert-road': 'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)', } const id = `pages-${Date.now()}` const images = request.body.images.map((image, index) => ({ media_id: image.id, gradient: gradients[image.id] ?? 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)', sort_order: index + 1, })) pagesPosts.unshift({ ...request.body, id, account_id: 1, author_name: 'demo', source_type: 'personal', citymarkt_listing_id: null, created_at: new Date().toISOString(), like_count: 0, images, }) response.json({ success: true, data: { id } }) return } if (endpoint === 'pages:share-citymarkt') { const listing = marketplaceListings.find( (item) => item.id === request.body.listingId && item.seller_account_id === 1 && ['active', 'reserved'].includes(item.status), ) if (!listing) { response.json({ success: false, error: 'citymarkt_not_found' }) return } if (pagesPosts.some((item) => item.citymarkt_listing_id === listing.id)) { response.json({ success: false, error: 'citymarkt_already_shared' }) return } if ( pagesPosts.some( (item) => item.account_id === 1 && item.source_type === 'citymarkt' && item.created_at.slice(0, 10) === '2026-08-06', ) ) { response.json({ success: false, error: 'citymarkt_daily_limit' }) return } const id = `pages-${Date.now()}` pagesPosts.unshift({ id, account_id: 1, author_name: 'demo', source_type: 'citymarkt', citymarkt_listing_id: listing.id, title: listing.title, body: listing.description, category: 'citymarkt', district: listing.district, created_at: '2026-08-06 13:30:00', like_count: 0, images: listing.images, }) response.json({ success: true, data: { id } }) return } if (endpoint === 'pages:react') { const index = pagesReactions.findIndex( (item) => item.post_id === request.body.id && item.account_id === 1 && item.kind === request.body.kind, ) if (request.body.active && index < 0) pagesReactions.push({ post_id: request.body.id, account_id: 1, kind: request.body.kind, }) if (!request.body.active && index >= 0) pagesReactions.splice(index, 1) response.json({ success: true }) return } if (endpoint === 'pages:delete') { const index = pagesPosts.findIndex( (item) => item.id === request.body.id && item.account_id === 1, ) if (index < 0) { response.json({ success: false, error: 'post_not_found' }) return } pagesPosts.splice(index, 1) response.json({ success: true }) return } if (endpoint === 'marketplace:list') { const query = String(request.body.search ?? '').toLowerCase() let items = marketplaceListings.filter((item) => ['active', 'reserved'].includes(item.status), ) if (request.body.category && request.body.category !== 'all') items = items.filter((item) => item.category === request.body.category) if (request.body.district && request.body.district !== 'all') items = items.filter((item) => item.district === request.body.district) if (query) items = items.filter((item) => `${item.title} ${item.description}`.toLowerCase().includes(query), ) if (request.body.favorites) items = items.filter((item) => item.is_favorite) response.json({ success: true, data: { hasMore: false, items, offset: 0 } }) return } if (endpoint === 'marketplace:get') { const item = marketplaceListings.find( (listing) => listing.id === request.body.id, ) response.json( item ? { success: true, data: { ...item, is_owner: authenticated && item.seller_account_id === 1, }, } : { success: false, error: 'listing_not_found' }, ) return } if (endpoint.startsWith('marketplace:') && !authenticated) { response.json({ success: false, error: 'not_authenticated' }) return } if (endpoint === 'marketplace:counts') { response.json({ success: true, data: { active: marketplaceListings.filter( (item) => item.seller_account_id === 1 && ['active', 'reserved'].includes(item.status), ).length, unread: marketplaceInquiries.reduce( (total, item) => total + item.unread, 0, ), }, }) return } if (endpoint === 'marketplace:list-own') { response.json({ success: true, data: { hasMore: false, items: marketplaceListings.filter( (item) => item.seller_account_id === 1, ), offset: 0, }, }) return } if (endpoint === 'marketplace:create') { const id = `81bc9d37-20e1-4d8a-82f8-${String(Date.now()).slice(-12)}` const selected = request.body.images.map((image) => { const photo = { 'sunset-drive': 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)', 'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', 'city-lights': 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)', 'desert-road': 'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)', }[image.id] ?? 'linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)' return { media_id: image.id, gradient: photo, sort_order: 1 } }) marketplaceListings.unshift({ ...request.body, id, seller_account_id: 1, seller_name: 'demo', seller_since: '2026-08-04 12:00:00', seller_active: 1, item_condition: request.body.condition, price_type: request.body.priceType, show_phone: request.body.showPhone ? 1 : 0, phone_number: null, status: 'active', revision: 1, created_at: '2026-08-06 11:00:00', updated_at: '2026-08-06 11:00:00', expires_at: '2026-08-13 11:00:00', images: selected, image: selected[0]?.gradient ?? null, is_favorite: false, }) response.json({ success: true, data: { id } }) return } if (endpoint === 'marketplace:update') { const item = marketplaceListings.find( (listing) => listing.id === request.body.id, ) if (!item) { response.json({ success: false, error: 'listing_not_found' }) return } const selected = request.body.images.map((image, index) => ({ media_id: image.id, gradient: { 'sunset-drive': 'linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)', 'ocean-air': 'linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)', 'city-lights': 'linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)', 'desert-road': 'linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)', }[image.id] ?? item.image, sort_order: index + 1, })) Object.assign(item, request.body, { image: selected[0]?.gradient ?? null, images: selected, item_condition: request.body.condition, price_type: request.body.priceType, revision: item.revision + 1, }) response.json({ success: true, data: { revision: item.revision } }) return } if (endpoint === 'marketplace:favorite') { const item = marketplaceListings.find( (listing) => listing.id === request.body.id, ) if (item) item.is_favorite = request.body.favorite response.json({ success: true }) return } if (endpoint === 'marketplace:set-status') { const item = marketplaceListings.find( (listing) => listing.id === request.body.id, ) if (item) item.status = request.body.status response.json({ success: true }) return } if (endpoint === 'marketplace:list-inquiries') { response.json({ success: true, data: marketplaceInquiries }) return } if (endpoint === 'marketplace:send-message') { let inquiry = marketplaceInquiries.find( (item) => item.id === request.body.inquiryId || item.listing_id === request.body.listingId, ) if (!inquiry) { const listing = marketplaceListings.find( (item) => item.id === request.body.listingId, ) inquiry = { id: '4903b923-409a-437e-971f-b7a2b10e9e31', listing_id: listing.id, seller_account_id: listing.seller_account_id, buyer_account_id: 1, title: listing.title, price: listing.price, price_type: listing.price_type, status: listing.status, image: listing.image, other_name: listing.seller_name, last_message: request.body.body, offer_id: null, offer_amount: null, offer_proposer_account_id: null, offer_status: null, offer_revision: 0, unread: 0, updated_at: '2026-08-06 11:30:00', } marketplaceInquiries.push(inquiry) } marketplaceMessages.push({ id: marketplaceMessages.length + 1, inquiry_id: inquiry.id, sender_account_id: 1, body: request.body.body, created_at: '2026-08-06 12:00:00', read_at: null, }) inquiry.last_message = request.body.body inquiry.unread = 0 inquiry.updated_at = '2026-08-06 12:00:00' response.json({ success: true, data: { id: inquiry.id } }) return } if (endpoint === 'marketplace:get-inquiry') { const inquiry = marketplaceInquiries.find( (item) => item.id === request.body.id, ) if (inquiry) { inquiry.unread = 0 for (const offer of marketplaceOffers) { if (offer.inquiry_id === inquiry.id && offer.proposer_account_id !== 1) offer.read_at = '2026-08-06 12:01:00' if ( offer.inquiry_id === inquiry.id && offer.proposer_account_id === 1 && ['accepted', 'rejected'].includes(offer.status) ) offer.response_read_at = '2026-08-06 12:01:00' } } const listing = inquiry && marketplaceListings.find((item) => item.id === inquiry.listing_id) response.json( inquiry ? { success: true, data: { accountId: 1, inquiry: { ...inquiry, reserved_account_id: listing.reserved_account_id ?? null, status: listing.status, }, messages: marketplaceMessages.filter( (message) => message.inquiry_id === inquiry.id, ), offers: marketplaceOffers.filter( (offer) => offer.inquiry_id === inquiry.id, ), }, } : { success: false, error: 'inquiry_not_found' }, ) return } if (endpoint === 'marketplace:make-offer') { const inquiry = marketplaceInquiries.find( (item) => item.id === request.body.inquiryId, ) const amount = Number(request.body.amount) if (!inquiry || !Number.isInteger(amount) || amount < 1) { response.json({ success: false, error: 'invalid_offer' }) return } if (inquiry.offer_status === 'accepted') { response.json({ success: false, error: 'offer_closed' }) return } if ( inquiry.offer_status === 'pending' && inquiry.offer_proposer_account_id === 1 ) { response.json({ success: false, error: 'offer_waiting' }) return } const previous = marketplaceOffers.find( (offer) => offer.id === inquiry.offer_id, ) if (previous?.status === 'pending') previous.status = 'countered' const offer = { id: marketplaceOffers.length + 1, inquiry_id: inquiry.id, proposer_account_id: 1, amount, status: 'pending', read_at: null, response_read_at: null, created_at: '2026-08-06 12:02:00', updated_at: '2026-08-06 12:02:00', } marketplaceOffers.push(offer) inquiry.offer_id = offer.id inquiry.offer_amount = amount inquiry.offer_proposer_account_id = 1 inquiry.offer_status = 'pending' inquiry.offer_revision += 1 inquiry.updated_at = offer.updated_at response.json({ success: true, data: { id: offer.id } }) return } if (endpoint === 'marketplace:respond-offer') { const inquiry = marketplaceInquiries.find( (item) => item.id === request.body.inquiryId, ) const offer = inquiry && marketplaceOffers.find((item) => item.id === inquiry.offer_id) if ( !inquiry || !offer || offer.status !== 'pending' || offer.proposer_account_id === 1 ) { response.json({ success: false, error: 'offer_not_actionable' }) return } if (!['accepted', 'rejected'].includes(request.body.action)) { response.json({ success: false, error: 'invalid_offer_response' }) return } offer.status = request.body.action offer.updated_at = '2026-08-06 12:03:00' inquiry.offer_status = request.body.action inquiry.offer_revision += 1 inquiry.updated_at = offer.updated_at if (request.body.action === 'accepted') { const listing = marketplaceListings.find( (item) => item.id === inquiry.listing_id, ) listing.status = 'reserved' listing.reserved_account_id = inquiry.buyer_account_id inquiry.status = 'reserved' } response.json({ success: true }) return } if (endpoint === 'marketplace:report' || endpoint === 'marketplace:block') { response.json({ success: true }) return } if ( endpoint === 'sim:insert' && request.body.imei === '356938035643810' && !request.body.confirmed ) { response.json({ success: false, error: 'confirmation_required' }) return } if (endpoint === 'notes:list') { response.json({ success: true, data: mockNotes }) return } if (endpoint === 'notes:create') { mockNotes.unshift({ ...request.body, revision: 1 }) response.json({ success: true, data: mockNotes }) return } if (endpoint === 'notes:update') { const index = mockNotes.findIndex((note) => note.id === request.body.id) if (index >= 0) { mockNotes[index] = { ...request.body, revision: mockNotes[index].revision + 1, updatedAt: Date.now(), } } response.json({ success: true, data: mockNotes }) return } if (endpoint === 'notes:delete') { mockNotes = mockNotes.filter((note) => note.id !== request.body.id) response.json({ success: true, data: mockNotes }) return } if (endpoint === 'mail:login' || endpoint === 'mail:register') { authenticated = true linkedAccount = { devices: accountDevices, email: 'demo@ifruit.com', id: 1, } response.json({ success: true, data: linkedAccount }) return } if (endpoint === 'mail:logout') { authenticated = false response.json({ success: true }) return } if (endpoint.startsWith('mail:') && !authenticated) { response.json({ success: false, error: 'not_authenticated' }) return } if (endpoint === 'mail:counts') { response.json({ success: true, data: counts() }) return } if (endpoint === 'mail:list') { const { folder, search = '' } = request.body let items = folder === 'drafts' ? draft ? [{ ...draft, created_at: draft.updated_at, preview: draft.body }] : [] : messages.filter((item) => folder === 'trash' ? item.trashed_at : item.folder === folder && !item.trashed_at, ) const query = String(search).toLowerCase() if (query) { items = items.filter((item) => `${item.sender ?? ''} ${item.subject} ${item.preview}` .toLowerCase() .includes(query), ) } response.json({ success: true, data: { hasMore: false, items, offset: 0 } }) return } if (endpoint === 'mail:get') { const message = messages.find((item) => item.id === Number(request.body.id)) if (message) message.is_read = true response.json( message ? { success: true, data: message } : { success: false, error: 'message_not_found' }, ) return } if (endpoint === 'mail:get-draft') { response.json( draft ? { success: true, data: draft } : { success: false, error: 'draft_not_found' }, ) return } if (endpoint === 'mail:save-draft') { draft = { ...request.body, id: request.body.id ?? '748836d2-2308-4c21-a4d8-3af6df6dd7eb', created_at: '2026-08-04 11:40:00', updated_at: '2026-08-04 11:40:00', } response.json({ success: true, data: { id: draft.id } }) return } if (endpoint === 'mail:delete-draft') { draft = null response.json({ success: true }) return } if (endpoint === 'mail:send') { draft = null response.json({ success: true, data: { id: 'mock-sent-message' } }) return } if (endpoint === 'mail:trash' || endpoint === 'mail:restore') { const message = messages.find((item) => item.id === Number(request.body.id)) if (message) message.trashed_at = endpoint === 'mail:trash' ? '2026-08-04 12:00:00' : null response.json({ success: true }) return } if (endpoint === 'mail:delete-forever') { const index = messages.findIndex( (item) => item.id === Number(request.body.id), ) if (index >= 0) messages.splice(index, 1) response.json({ success: true }) return } if (endpoint === 'mail:empty-trash') { for (let index = messages.length - 1; index >= 0; index -= 1) { if (messages[index].trashed_at) messages.splice(index, 1) } response.json({ success: true }) return } if (endpoint === 'mail:set-read') { const message = messages.find((item) => item.id === Number(request.body.id)) if (message) message.is_read = request.body.read response.json({ success: true }) return } response.json({ success: true }) }) app.listen(port, () => { console.log(`Mock NUI server listening on http://localhost:${port}`) })