From 6a8be4d6791a2aff761f379739b63c7d2a71a344 Mon Sep 17 00:00:00 2001 From: DerEchteAlec Date: Mon, 17 Aug 2026 05:16:10 +0200 Subject: [PATCH] FIX - harden server startup diagnostics --- frontend/src/serverStartup.contract.test.ts | 35 +++++++++++++ sky_phone/fxmanifest.lua | 4 +- sky_phone/source/server/fliptok.lua | 3 +- sky_phone/source/server/media.lua | 3 +- sky_phone/source/server/media_import.lua | 23 ++++++--- sky_phone/source/server/phone.lua | 56 ++++++++++++++++++--- sky_phone/source/server/picstagram.lua | 3 +- 7 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 frontend/src/serverStartup.contract.test.ts diff --git a/frontend/src/serverStartup.contract.test.ts b/frontend/src/serverStartup.contract.test.ts new file mode 100644 index 0000000..925f58c --- /dev/null +++ b/frontend/src/serverStartup.contract.test.ts @@ -0,0 +1,35 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const manifest = readFileSync( + new URL('../../sky_phone/fxmanifest.lua', import.meta.url), + 'utf8', +) +const phoneServer = readFileSync( + new URL('../../sky_phone/source/server/phone.lua', import.meta.url), + 'utf8', +) + +describe('server startup contracts', () => { + it('registers the development-open callback before the database migration runs', () => { + expect(manifest.indexOf("'source/server/phone.lua'")).toBeLessThan( + manifest.indexOf("'source/server/db_migrate.lua'"), + ) + expect( + phoneServer.indexOf( + 'Bridge.Callbacks.Register("sky_phone:device:development-open"', + ), + ).toBeLessThan( + phoneServer.indexOf('Bridge.Database.AfterMigration("sky_phone"'), + ) + }) + + it('queues early development opens until the resource has fully started', () => { + expect(phoneServer).toContain( + 'AddEventHandler("onServerResourceStart", function(resource_name)', + ) + expect(phoneServer).toContain('pending_development_opens[source] = true') + expect(phoneServer).toContain('development_open_handler = open_phone') + }) +}) diff --git a/sky_phone/fxmanifest.lua b/sky_phone/fxmanifest.lua index 5df68e1..f58cd71 100644 --- a/sky_phone/fxmanifest.lua +++ b/sky_phone/fxmanifest.lua @@ -65,12 +65,12 @@ server_scripts { 'source/bridge/server/voice.lua', 'source/server/custom_apps.lua', 'source/server/custom_app_compat.lua', - 'source/server/db_migrate.lua', - 'source/server/lb_phone_migration.lua', 'source/server/media_metadata.lua', 'source/server/companies.lua', 'source/server/sim.lua', 'source/server/phone.lua', + 'source/server/db_migrate.lua', + 'source/server/lb_phone_migration.lua', 'source/server/custom_app_storage.lua', 'source/server/payphones.lua', 'source/server/calls.lua', diff --git a/sky_phone/source/server/fliptok.lua b/sky_phone/source/server/fliptok.lua index b3ad2f1..ed809f4 100644 --- a/sky_phone/source/server/fliptok.lua +++ b/sky_phone/source/server/fliptok.lua @@ -19,7 +19,8 @@ local password_pepper = tostring(Config.Server.FlipTokPasswordPepper or "") if password_pepper == "" then Bridge.Debug( "warn", - "[sky_phone] Config.Server.FlipTokPasswordPepper is empty; configure it in config/config.lua before production use." + "[sky_phone] Config.Server.FlipTokPasswordPepper is empty. FlipTok passwords still work, but their hashes lack the required server-side secret. Set a stable random value in config/config.lua before production; changing it later invalidates existing FlipTok passwords.", + { always = true } ) end diff --git a/sky_phone/source/server/media.lua b/sky_phone/source/server/media.lua index 8d6a1a5..cec2e17 100644 --- a/sky_phone/source/server/media.lua +++ b/sky_phone/source/server/media.lua @@ -975,7 +975,8 @@ end) if not api_configured() then Bridge.Debug( "warn", - "[sky_phone] Camera uploads and FiveManage imports are disabled until Config.Media.FiveManage.ApiKey is set in config/media.lua." + "[sky_phone] FiveManage media integration is disabled because Config.Media.FiveManage.ApiKey is empty in config/media.lua. Camera photo and video uploads, Voice Memo uploads, remote Gallery deletion, and FiveManage imports are unavailable. Add a FiveManage V3 token with Media access and restart sky_phone.", + { always = true } ) end end) diff --git a/sky_phone/source/server/media_import.lua b/sky_phone/source/server/media_import.lua index 22ef403..ec52e79 100644 --- a/sky_phone/source/server/media_import.lua +++ b/sky_phone/source/server/media_import.lua @@ -251,12 +251,23 @@ local function build_registry() end websites[website.Id] = website else - Bridge.Debug( - "warn", - "[sky_phone] Media import website at index %s was disabled: %s.", - tostring(index), - tostring(website_error) - ) + local source_name = type(definition) == "table" and definition.Id or nil + if website_error == "missing_api_key" then + Bridge.Debug( + "warn", + "[sky_phone] Media import source '%s' at index %s is disabled because Config.Media.FiveManage.ApiKey is empty in server-only config/media.lua. Add a FiveManage V3 token with Media access and restart sky_phone.", + tostring(source_name or "unknown"), + tostring(index) + ) + else + Bridge.Debug( + "warn", + "[sky_phone] Media import source '%s' at index %s is disabled because its configuration is invalid: %s.", + tostring(source_name or "unknown"), + tostring(index), + tostring(website_error) + ) + end end end end diff --git a/sky_phone/source/server/phone.lua b/sky_phone/source/server/phone.lua index fdb1ccd..5a25bea 100644 --- a/sky_phone/source/server/phone.lua +++ b/sky_phone/source/server/phone.lua @@ -1,3 +1,47 @@ +local development_open_handler +local pending_development_opens = {} +local server_started = false + +Bridge.Callbacks.Register("sky_phone:device:development-open", function(source) + if not Config.Phone.DevelopmentCommand then + return { success = false, error = "disabled" } + end + if not server_started or not development_open_handler then + pending_development_opens[source] = true + return { success = true, data = { queued = true } } + end + return { success = development_open_handler(source, nil) } +end) + +AddEventHandler("playerDropped", function() + pending_development_opens[source] = nil +end) + +AddEventHandler("onServerResourceStart", function(resource_name) + if resource_name ~= GetCurrentResourceName() then + return + end + + server_started = true + if not development_open_handler then + Bridge.Debug("error", "[sky_phone] Server initialization did not register the development phone handler.") + return + end + + for player_source in pairs(pending_development_opens) do + pending_development_opens[player_source] = nil + local success, opened = pcall(development_open_handler, player_source, nil) + if not success then + Bridge.Debug( + "error", + "[sky_phone] Queued development phone open failed for source %s: %s", + tostring(player_source), + tostring(opened) + ) + end + end +end) + Bridge.Database.AfterMigration("sky_phone", function() Bridge.Debug("debug", "[sky_phone] Server initialization started after database migration.", { always = true }) @@ -14,7 +58,8 @@ local passcode_pepper = tostring(Config.Server.PasscodePepper or "") if passcode_pepper == "" then Bridge.Debug( "warn", - "[sky_phone] Config.Server.PasscodePepper is empty; configure it in config/config.lua before production use." + "[sky_phone] Config.Server.PasscodePepper is empty. Device passcodes still work, but their hashes lack the required server-side secret. Set a stable random value in config/config.lua before production; changing it later invalidates existing device passcodes.", + { always = true } ) end local allowed_device_namespaces = { @@ -965,6 +1010,8 @@ local function open_phone(source, used_item) return true end +development_open_handler = open_phone + function SkyPhone.OpenDeviceForCall(source, imei) local matches = find_device_slots(source, imei) if not matches[1] then @@ -1123,13 +1170,6 @@ Bridge.Callbacks.Register("sky_phone:security:disable-passcode", function(source return { success = true, data = { security = security_status(session.imei) } } end) -Bridge.Callbacks.Register("sky_phone:device:development-open", function(source) - if not Config.Phone.DevelopmentCommand then - return { success = false, error = "disabled" } - end - return { success = open_phone(source, nil) } -end) - Bridge.Callbacks.Register("sky_phone:device:save", function(source, data) if not SkyPhone.AllowOperation(source, "device_save", 120, 60) then return { success = false, error = "rate_limited" } diff --git a/sky_phone/source/server/picstagram.lua b/sky_phone/source/server/picstagram.lua index e659fdb..301b62f 100644 --- a/sky_phone/source/server/picstagram.lua +++ b/sky_phone/source/server/picstagram.lua @@ -5,7 +5,8 @@ local password_pepper = tostring(Config.Server.PicstagramPasswordPepper or "") if password_pepper == "" then Bridge.Debug( "warn", - "[sky_phone] Config.Server.PicstagramPasswordPepper is empty; configure it in config/config.lua before production use." + "[sky_phone] Config.Server.PicstagramPasswordPepper is empty. Picstagram passwords still work, but their hashes lack the required server-side secret. Set a stable random value in config/config.lua before production; changing it later invalidates existing Picstagram passwords.", + { always = true } ) end