ADD - expand phone application platform

Add the Companies app, resilient call handling, custom app registry, vendor compatibility, storage policies, frontend bridge, tests, documentation, config, locale, and SQL migrations.
This commit is contained in:
Leon.Schmidt
2026-08-11 00:22:39 +02:00
parent 72e5f91e02
commit 42ac955052
77 changed files with 17630 additions and 333 deletions
+451
View File
@@ -0,0 +1,451 @@
local APP_ID_MAX_LENGTH = 64
local DESCRIPTION_MAX_LENGTH = 320
local DEVELOPER_MAX_LENGTH = 96
local NAME_MAX_LENGTH = 64
local PATH_MAX_LENGTH = 512
local VERSION_MAX_LENGTH = 32
local ALLOWED_CATEGORIES = {
games = true,
productivity = true,
shopping = true,
social = true,
utilities = true,
}
local ALLOWED_ORIENTATIONS = {
any = true,
landscape = true,
portrait = true,
}
local ALLOWED_PERMISSIONS = {
["app.close"] = true,
["app.open"] = true,
["camera.capture"] = true,
["contacts.pick"] = true,
["device.storage"] = true,
["locale.read"] = true,
["location.read"] = true,
["media.pick"] = true,
["notifications"] = true,
["notifications.critical"] = true,
["nui.fetch"] = true,
["theme.read"] = true,
}
local RESERVED_APP_IDS = {
["app-store"] = true,
banking = true,
billing = true,
calculator = true,
calendar = true,
camera = true,
citymarkt = true,
clock = true,
companies = true,
crewlink = true,
darkchat = true,
feather = true,
flare = true,
fliptok = true,
garage = true,
house = true,
["local-pages"] = true,
mail = true,
map = true,
memory = true,
messages = true,
minesweeper = true,
music = true,
["neon-drop"] = true,
notes = true,
["number-merge"] = true,
phone = true,
photos = true,
picstagram = true,
radio = true,
settings = true,
["sky-flappy"] = true,
skyride = true,
snake = true,
["tower-stack"] = true,
weather = true,
}
local bundled_manifests = {}
local bundled_manifests_by_id = {}
SkyPhoneApps = SkyPhoneApps or {}
SkyPhoneApps.ProtocolVersion = 1
local function trim(value)
return value:match("^%s*(.-)%s*$")
end
local function validate_app_id(app_id)
if type(app_id) ~= "string" then
return false, "invalid_app_id"
end
if #app_id == 0 or #app_id > APP_ID_MAX_LENGTH then
return false, "invalid_app_id"
end
if not app_id:match("^[a-z0-9][a-z0-9._-]+$") then
return false, "invalid_app_id"
end
return true
end
local function validate_text(value, error_code, maximum_length, required)
if value == nil and not required then
return nil
end
if type(value) ~= "string" then
return nil, error_code
end
local normalized = trim(value)
if (required and #normalized == 0) or #normalized > maximum_length then
return nil, error_code
end
return normalized
end
local function validate_localized_text(value, error_code, maximum_length, required)
if type(value) == "string" then
return validate_text(value, error_code, maximum_length, required)
end
if type(value) ~= "table" then
return nil, error_code
end
local normalized = {}
local count = 0
for locale, text in pairs(value) do
if type(locale) ~= "string" or not locale:match("^[a-zA-Z][a-zA-Z0-9_-]*$") then
return nil, error_code
end
local normalized_text = validate_text(text, error_code, maximum_length, true)
if not normalized_text then
return nil, error_code
end
count = count + 1
if count > 16 then
return nil, error_code
end
normalized[locale:lower():gsub("_", "-")] = normalized_text
end
if count == 0 then
return nil, error_code
end
return normalized
end
local function validate_local_path(path, required)
if path == nil and not required then
return nil
end
if type(path) ~= "string" or #path == 0 or #path > PATH_MAX_LENGTH then
return nil, "invalid_local_path"
end
if path:sub(1, 1) == "/" or path:find("\\", 1, true) or path:find(":", 1, true) then
return nil, "invalid_local_path"
end
if not path:match("^web/[%w%._%-%/]+$") then
return nil, "invalid_local_path"
end
for segment in path:gmatch("[^/]+") do
if segment == "." or segment == ".." then
return nil, "invalid_local_path"
end
end
return path
end
local function validate_string_array(value, error_code, maximum_items, validator)
if value == nil then
return {}
end
if type(value) ~= "table" then
return nil, error_code
end
local normalized = {}
local count = 0
for key in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
return nil, error_code
end
count = count + 1
end
if count ~= #value or count > maximum_items then
return nil, error_code
end
for index = 1, count do
local item = value[index]
if type(item) ~= "string" then
return nil, error_code
end
local normalized_item, validation_error = validator(item)
if not normalized_item then
return nil, validation_error or error_code
end
normalized[index] = normalized_item
end
return normalized
end
local function validate_permissions(value)
local permissions, error_code = validate_string_array(
value,
"invalid_permissions",
32,
function(permission)
if not ALLOWED_PERMISSIONS[permission] then
return nil, "unknown_permission"
end
return permission
end
)
if not permissions then
return nil, error_code
end
local seen = {}
for index = 1, #permissions do
local permission = permissions[index]
if seen[permission] then
return nil, "duplicate_permission"
end
seen[permission] = true
end
return permissions
end
local function validate_grid_order(value)
if value == nil then
return nil
end
if type(value) ~= "number" or value ~= math.floor(value) or value < 0 or value > 9999 then
return nil, "invalid_grid_order"
end
return value
end
local function validate_icon_background(value)
if value == nil then
return nil
end
if type(value) ~= "string" or #value == 0 or #value > 64 then
return nil, "invalid_icon_background"
end
if value:find("[\r\n;{}]") then
return nil, "invalid_icon_background"
end
return value
end
local function normalize_manifest(folder, definition)
if type(folder) ~= "string" or type(definition) ~= "table" then
return nil, "invalid_manifest"
end
if definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then
return nil, "unsupported_schema_version"
end
local app_id = definition.id
local valid_id, id_error = validate_app_id(app_id)
if not valid_id then
return nil, id_error
end
if RESERVED_APP_IDS[app_id] then
return nil, "reserved_app_id"
end
if folder ~= app_id then
return nil, "folder_id_mismatch"
end
if definition.defaultInstalled ~= nil and type(definition.defaultInstalled) ~= "boolean" then
return nil, "invalid_default_installed"
end
if definition.removable ~= nil and type(definition.removable) ~= "boolean" then
return nil, "invalid_removable"
end
local version, version_error = validate_text(
definition.version or "1.0.0",
"invalid_version",
VERSION_MAX_LENGTH,
true
)
if not version then
return nil, version_error
end
if not version:match("^[%w%._+-]+$") then
return nil, "invalid_version"
end
local name, name_error = validate_localized_text(
definition.name,
"invalid_name",
NAME_MAX_LENGTH,
true
)
if not name then
return nil, name_error
end
local description, description_error = validate_localized_text(
definition.description,
"invalid_description",
DESCRIPTION_MAX_LENGTH,
true
)
if not description then
return nil, description_error
end
local developer, developer_error = validate_text(
definition.developer,
"invalid_developer",
DEVELOPER_MAX_LENGTH,
false
)
if definition.developer ~= nil and not developer then
return nil, developer_error
end
local category = definition.category or "utilities"
if not ALLOWED_CATEGORIES[category] then
return nil, "invalid_category"
end
local entry, entry_error = validate_local_path(definition.entry, true)
if not entry then
return nil, entry_error
end
local icon, icon_error = validate_local_path(definition.icon, false)
if definition.icon ~= nil and not icon then
return nil, icon_error
end
local screenshots, screenshots_error = validate_string_array(
definition.screenshots,
"invalid_screenshots",
8,
function(path)
return validate_local_path(path, true)
end
)
if not screenshots then
return nil, screenshots_error
end
local permissions, permissions_error = validate_permissions(definition.permissions)
if not permissions then
return nil, permissions_error
end
local orientation = definition.orientation or "portrait"
if not ALLOWED_ORIENTATIONS[orientation] then
return nil, "invalid_orientation"
end
local grid_order, grid_order_error = validate_grid_order(definition.gridOrder)
if definition.gridOrder ~= nil and not grid_order then
return nil, grid_order_error
end
local icon_background, background_error = validate_icon_background(definition.iconBackground)
if definition.iconBackground ~= nil and not icon_background then
return nil, background_error
end
local bridge_mode = definition.bridgeMode or "sky"
if bridge_mode ~= "sky" and bridge_mode ~= "legacy" then
return nil, "invalid_bridge_mode"
end
return {
bridgeMode = bridge_mode,
category = category,
defaultInstalled = definition.defaultInstalled == true,
description = description,
developer = developer,
entry = entry,
folder = folder,
gridOrder = grid_order,
icon = icon,
iconBackground = icon_background,
id = app_id,
name = name,
orientation = orientation,
permissions = permissions,
removable = definition.removable ~= false,
schemaVersion = SkyPhoneApps.ProtocolVersion,
screenshots = screenshots,
version = version,
}
end
---@param folder string App folder relative to custom_apps.
---@param definition table Declarative bundled app manifest.
function SkyPhoneApps.RegisterManifest(folder, definition)
local manifest, error_code = normalize_manifest(folder, definition)
if not manifest then
error(("[sky_phone] Invalid custom app manifest '%s': %s"):format(tostring(folder), error_code), 2)
end
if bundled_manifests_by_id[manifest.id] then
error(("[sky_phone] Duplicate bundled custom app id '%s'."):format(manifest.id), 2)
end
bundled_manifests[#bundled_manifests + 1] = manifest
bundled_manifests_by_id[manifest.id] = manifest
end
function SkyPhoneApps.GetBundledManifests()
return bundled_manifests
end
function SkyPhoneApps.GetBundledManifest(app_id)
return bundled_manifests_by_id[app_id]
end
SkyPhoneApps.AllowedCategories = ALLOWED_CATEGORIES
SkyPhoneApps.AllowedOrientations = ALLOWED_ORIENTATIONS
SkyPhoneApps.AllowedPermissions = ALLOWED_PERMISSIONS
SkyPhoneApps.ReservedAppIds = RESERVED_APP_IDS
SkyPhoneApps.ValidateAppId = validate_app_id
SkyPhoneApps.ValidateLocalizedText = validate_localized_text
SkyPhoneApps.ValidatePermissions = validate_permissions