local policies_by_id = {} local policy_ids_by_adapter = {} local policy_ids_by_owner = {} local current_resource = GetCurrentResourceName() local function validate_owner_resource(owner_resource, allow_stopping) if type(owner_resource) ~= "string" or #owner_resource == 0 or #owner_resource > 64 or not owner_resource:match("^[%w][%w._-]*$") then return false, "invalid_owner_resource" end local state = GetResourceState(owner_resource) if state == "started" or state == "starting" or (allow_stopping and state == "stopping") then return true end return false, "owner_resource_not_running" end local function add_owner_index(index, resource_name, app_id) local app_ids = index[resource_name] if not app_ids then app_ids = {} index[resource_name] = app_ids end app_ids[app_id] = true end local function remove_owner_index(index, resource_name, app_id) local app_ids = index[resource_name] if not app_ids then return end app_ids[app_id] = nil if not next(app_ids) then index[resource_name] = nil end end local function build_permission_set(permissions) local permission_set = {} for index = 1, #permissions do permission_set[permissions[index]] = true end return permission_set end local function register_policy(policy) local app_id = policy.id local existing = policies_by_id[app_id] if existing then print(( "[sky_phone] Rejected duplicate custom app policy '%s' from '%s'; it is already owned by '%s'." ):format(app_id, policy.ownerResource, existing.ownerResource)) return false, "duplicate_app_id" end policies_by_id[app_id] = policy add_owner_index(policy_ids_by_owner, policy.ownerResource, app_id) if policy.adapterResource then add_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id) end return true end local function remove_policy(app_id) local policy = policies_by_id[app_id] if not policy then return false, "app_not_found" end policies_by_id[app_id] = nil remove_owner_index(policy_ids_by_owner, policy.ownerResource, app_id) if policy.adapterResource then remove_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id) end return true end local function get_direct_owner() local owner_resource = GetInvokingResource() if not owner_resource then return nil, "missing_invoking_resource" end local valid_owner, owner_error = validate_owner_resource(owner_resource, false) if not valid_owner then return nil, owner_error end return owner_resource end local function get_trusted_adapter() local adapter_resource = GetInvokingResource() if not adapter_resource or not Config.CustomApps.TrustedAdapters[adapter_resource] then return nil, "untrusted_adapter" end local valid_adapter, adapter_error = validate_owner_resource(adapter_resource, false) if not valid_adapter then return nil, adapter_error end return adapter_resource end local function normalize_policy(owner_resource, adapter_resource, definition) if type(definition) ~= "table" then return nil, "invalid_definition" end if definition.schemaVersion ~= nil and definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then return nil, "unsupported_schema_version" end local valid_id, id_error = SkyPhoneApps.ValidateAppId(definition.id) if not valid_id then return nil, id_error end if SkyPhoneApps.ReservedAppIds[definition.id] then return nil, "reserved_app_id" end local permissions, permissions_error = SkyPhoneApps.ValidatePermissions(definition.permissions) if not permissions then return nil, permissions_error end return { adapterResource = adapter_resource, bundled = false, id = definition.id, ownerResource = owner_resource, permissions = build_permission_set(permissions), } end local function add_custom_app_policy(definition) if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then return false, "external_apps_disabled" end local owner_resource, owner_error = get_direct_owner() if not owner_resource then return false, owner_error end local policy, policy_error = normalize_policy(owner_resource, nil, definition) if not policy then return false, policy_error end return register_policy(policy) end local function add_custom_app_policy_from_adapter(owner_resource, definition) if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then return false, "external_apps_disabled" end local adapter_resource, adapter_error = get_trusted_adapter() if not adapter_resource then return false, adapter_error end local valid_owner, owner_error = validate_owner_resource(owner_resource, false) if not valid_owner then return false, owner_error end local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition) if not policy then return false, policy_error end return register_policy(policy) end local function verify_owned_policy(owner_resource, app_id, adapter_resource) local valid_id, id_error = SkyPhoneApps.ValidateAppId(app_id) if not valid_id then return nil, id_error end local policy = policies_by_id[app_id] if not policy then return nil, "app_not_found" end if policy.ownerResource ~= owner_resource then return nil, "app_owner_mismatch" end if adapter_resource and policy.adapterResource ~= adapter_resource then return nil, "app_adapter_mismatch" end return policy end local function replace_policy(policy, adapter_resource) local existing, policy_error = verify_owned_policy( policy.ownerResource, policy.id, adapter_resource ) if not existing then return false, policy_error end if existing.bundled then return false, "bundled_app" end policies_by_id[policy.id] = policy return true end local function update_custom_app_policy(definition) if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then return false, "external_apps_disabled" end local owner_resource, owner_error = get_direct_owner() if not owner_resource then return false, owner_error end local policy, policy_error = normalize_policy(owner_resource, nil, definition) if not policy then return false, policy_error end return replace_policy(policy, nil) end local function update_custom_app_policy_from_adapter(owner_resource, definition) if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then return false, "external_apps_disabled" end local adapter_resource, adapter_error = get_trusted_adapter() if not adapter_resource then return false, adapter_error end local valid_owner, owner_error = validate_owner_resource(owner_resource, false) if not valid_owner then return false, owner_error end local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition) if not policy then return false, policy_error end return replace_policy(policy, adapter_resource) end local function remove_custom_app_policy(app_id) local owner_resource, owner_error = get_direct_owner() if not owner_resource then return false, owner_error end local policy, policy_error = verify_owned_policy(owner_resource, app_id) if not policy then return false, policy_error end if policy.bundled then return false, "bundled_app" end return remove_policy(app_id) end local function remove_custom_app_policy_from_adapter(owner_resource, app_id) local adapter_resource, adapter_error = get_trusted_adapter() if not adapter_resource then return false, adapter_error end local policy, policy_error = verify_owned_policy(owner_resource, app_id, adapter_resource) if not policy then return false, policy_error end return remove_policy(app_id) end local function has_custom_app_permission(app_id, permission) local valid_id = SkyPhoneApps.ValidateAppId(app_id) if not valid_id or type(permission) ~= "string" or not SkyPhoneApps.AllowedPermissions[permission] then return false end local policy = policies_by_id[app_id] return policy ~= nil and policy.permissions[permission] == true end local function get_custom_app_policy(app_id) local valid_id = SkyPhoneApps.ValidateAppId(app_id) if not valid_id then return nil end local policy = policies_by_id[app_id] if not policy then return nil end local permissions = {} for permission in pairs(policy.permissions) do permissions[#permissions + 1] = permission end table.sort(permissions) return { bundled = policy.bundled, id = policy.id, ownerResource = policy.ownerResource, permissions = permissions, } end local function get_custom_app_capabilities() return { abiVersion = 1, enabled = Config.CustomApps.Enabled, policyRegistration = true, policyUpdate = true, protocolVersion = SkyPhoneApps.ProtocolVersion, } end SkyPhoneApps.GetPolicy = get_custom_app_policy SkyPhoneApps.HasPermission = has_custom_app_permission if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then local bundled = SkyPhoneApps.GetBundledManifests() for index = 1, #bundled do local manifest = bundled[index] local registered, register_error = register_policy({ adapterResource = nil, bundled = true, id = manifest.id, ownerResource = current_resource, permissions = build_permission_set(manifest.permissions), }) if not registered then error(("[sky_phone] Could not register bundled custom app policy '%s': %s"):format( manifest.id, register_error )) end end end exports("AddCustomAppPolicy", add_custom_app_policy) exports("AddCustomAppPolicyFromAdapter", add_custom_app_policy_from_adapter) exports("GetCustomAppCapabilities", get_custom_app_capabilities) exports("GetCustomAppPolicy", get_custom_app_policy) exports("HasCustomAppPermission", has_custom_app_permission) exports("RemoveCustomAppPolicy", remove_custom_app_policy) exports("RemoveCustomAppPolicyFromAdapter", remove_custom_app_policy_from_adapter) exports("UpdateCustomAppPolicy", update_custom_app_policy) exports("UpdateCustomAppPolicyFromAdapter", update_custom_app_policy_from_adapter) AddEventHandler("onResourceStop", function(resource_name) if resource_name == current_resource then return end local app_ids = {} local owner_policies = policy_ids_by_owner[resource_name] if owner_policies then for app_id in pairs(owner_policies) do app_ids[app_id] = true end end local adapter_policies = policy_ids_by_adapter[resource_name] if adapter_policies then for app_id in pairs(adapter_policies) do app_ids[app_id] = true end end for app_id in pairs(app_ids) do remove_policy(app_id) end end)