15 KiB
15 KiB
Mosis App Specifications
This document defines the complete specification for Mosis apps, including package format, manifest schema, permissions, and available APIs.
Package Format
Mosis apps are distributed as .mosis files, which are signed ZIP archives.
File Structure
com.developer.appname-1.0.0.mosis
├── manifest.json # App metadata (required)
├── META-INF/
│ ├── MANIFEST.MF # SHA-256 hashes of all files
│ ├── CERT.PEM # Developer's public key
│ └── CERT.SIG # Ed25519 signature of MANIFEST.MF
├── assets/
│ ├── main.rml # Entry point (required)
│ ├── screens/ # Additional RML screens
│ ├── styles/ # RCSS stylesheets
│ └── scripts/ # Lua scripts
├── icons/
│ ├── icon-32.png # 32x32 icon
│ ├── icon-64.png # 64x64 icon
│ └── icon-128.png # 128x128 icon
└── locales/ # Optional i18n files
├── en.json
└── es.json
Size Limits
| Limit | Value |
|---|---|
| Max package size | 50 MB |
| Max individual file | 10 MB |
| Max files count | 1000 |
| Max path length | 256 chars |
| Max manifest size | 64 KB |
Allowed File Types
| Category | Extensions |
|---|---|
| UI | .rml |
| Styles | .rcss |
| Scripts | .lua |
| Images | .png, .jpg, .jpeg, .tga, .webp |
| Fonts | .ttf, .otf |
| Data | .json |
| Audio | .ogg, .wav, .mp3 |
Forbidden: .exe, .dll, .so, .dylib, .sh, .bat, .ps1, .py, .js, nested archives
Manifest Schema
Required Fields
{
"$schema": "https://mosis.dev/schemas/manifest-v1.json",
"id": "com.developer.appname",
"name": "My App",
"version": "1.0.0",
"version_code": 1,
"entry": "assets/main.rml",
"min_mosis_version": "1.0.0"
}
Full Schema
{
"$schema": "https://mosis.dev/schemas/manifest-v1.json",
"id": "com.developer.appname",
"name": "My App",
"version": "1.0.0",
"version_code": 1,
"description": "A short description of the app",
"entry": "assets/main.rml",
"author": {
"name": "Developer Name",
"email": "dev@example.com",
"url": "https://developer.com"
},
"permissions": [
"storage",
"network.internet",
"camera"
],
"icons": {
"32": "icons/icon-32.png",
"64": "icons/icon-64.png",
"128": "icons/icon-128.png"
},
"min_mosis_version": "1.0.0",
"target_mosis_version": "1.2.0",
"category": "utilities",
"tags": ["productivity", "tools"],
"orientation": "portrait",
"background_color": "#FFFFFF",
"locales": ["en", "es", "fr"],
"default_locale": "en",
"network": {
"allowed_domains": [
"api.example.com",
"*.cdn.example.com"
],
"allow_http": false,
"max_connections": 10
}
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique package ID (reverse domain: ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$) |
name |
string | Yes | Display name (max 30 chars) |
version |
string | Yes | Semantic version (X.Y.Z) |
version_code |
integer | Yes | Incremental build number (must increase with each release) |
entry |
string | Yes | Path to entry RML file |
min_mosis_version |
string | Yes | Minimum Mosis version required |
description |
string | No | Short description (max 80 chars) |
author |
object | No | Author information |
permissions |
array | No | Required permissions |
icons |
object | No | Icon paths by size |
category |
string | No | App store category |
tags |
array | No | Searchable tags |
orientation |
string | No | portrait, landscape, any |
background_color |
string | No | Hex color for loading screen |
locales |
array | No | Supported locale codes |
Signing
Algorithm
- Key type: Ed25519
- Signature size: 64 bytes
- Hash: SHA-256 for file manifests
MANIFEST.MF Format
Manifest-Version: 1.0
Created-By: mosis-cli 1.0.0
Name: manifest.json
SHA-256-Digest: base64encodedHash==
Name: assets/main.rml
SHA-256-Digest: base64encodedHash==
Name: assets/scripts/main.lua
SHA-256-Digest: base64encodedHash==
Signing Flow
- Generate
MANIFEST.MFwith SHA-256 hash of each file - Sign
MANIFEST.MFwith developer's Ed25519 private key - Store signature in
META-INF/CERT.SIG - Include developer's public key in
META-INF/CERT.PEM
Verification Flow
- Extract
META-INF/MANIFEST.MF - Verify signature using
CERT.PEM - Verify
CERT.PEMis registered with developer account - Verify each file hash matches
MANIFEST.MF
Permissions
Apps must declare required permissions in the manifest. Some permissions are auto-granted, others require user approval.
Permission Categories
| Permission | Description | Risk Level |
|---|---|---|
storage |
App-private file access | Normal (auto-granted) |
network.internet |
HTTP requests | Dangerous |
network.websocket |
WebSocket connections | Dangerous |
camera |
Camera access | Dangerous |
microphone |
Audio recording | Dangerous |
location.coarse |
Approximate location | Dangerous |
location.fine |
Precise GPS location | Dangerous |
contacts.read |
Read contacts | Dangerous |
contacts.write |
Modify contacts | Dangerous |
bluetooth |
Bluetooth access | Dangerous |
sensors.body |
Body sensors (heart rate) | Dangerous |
clipboard.read |
Read clipboard | Dangerous |
clipboard.write |
Write to clipboard | Dangerous |
system.notifications |
Show notifications | Normal |
Requesting Permissions
-- Request a dangerous permission (shows system dialog)
local granted = permissions.request("camera")
if granted then
-- Permission granted, can now use camera API
camera.start()
else
-- Permission denied, show fallback UI
showPermissionDeniedMessage()
end
User Gesture Requirements
Dangerous hardware permissions (camera, microphone, location) require a recent user gesture (within 5 seconds) before the permission request is shown.
Runtime Limits
Apps run in a sandboxed Lua environment with resource limits.
Memory Limits
| Resource | Limit |
|---|---|
| Total memory | 16 MB |
| Max string size | 1 MB |
| Max table entries | 100,000 |
CPU Limits
| Context | Instruction Limit | Approx Time |
|---|---|---|
| Event handler | 1,000,000 | ~10ms |
| Init/load | 10,000,000 | ~100ms |
| Timer callback | 500,000 | ~5ms |
Storage Limits
| Resource | Limit |
|---|---|
| App storage | 100 MB |
| Cache storage | 50 MB |
| Single file | 50 MB |
| Files per directory | 10,000 |
| SQLite database | 50 MB |
Lua APIs
Safe Built-in Globals
-- Retained from standard Lua
print -- Redirected to system logger
type, tonumber, tostring
pairs, ipairs, next
pcall, xpcall
assert, error
select, unpack
string.* -- Except string.dump
table.*
math.*
utf8.*
Removed Globals (Security)
-- These do NOT exist in the sandbox
dofile, loadfile, load, loadstring
rawget, rawset, rawequal, rawlen
getmetatable, setmetatable
collectgarbage
os, io, debug, package, require
ffi, jit, newproxy
Storage API
-- App-private storage (auto-granted)
storage.write("data/config.json", json.encode(config))
local data = storage.read("data/config.json")
local exists = storage.exists("data/config.json")
storage.delete("data/config.json")
storage.mkdir("data/subfolder")
local files = storage.list("data/")
-- Cache storage (can be cleared by system)
storage.write("cache/temp.dat", data)
-- Temp storage (cleared on app stop)
storage.write("temp/session.dat", data)
Database API (SQLite)
-- Open database (creates if not exists)
local db = database.open("mydata.db")
-- Execute SQL
db:exec([[
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)
]])
-- Prepared statements with parameters
local stmt = db:prepare("INSERT INTO users (name, email) VALUES (?, ?)")
stmt:bind("John Doe", "john@example.com")
stmt:exec()
-- Query with results
local rows = db:query("SELECT * FROM users WHERE name LIKE ?", "%John%")
for _, row in ipairs(rows) do
print(row.id, row.name, row.email)
end
-- Close when done
db:close()
Network API
HTTP Requests
-- Requires "network.internet" permission
local response = network.request({
url = "https://api.example.com/data",
method = "POST", -- GET, POST, PUT, DELETE, PATCH
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. token
},
body = json.encode({key = "value"}),
timeout = 30000 -- milliseconds
})
-- Response structure
if response.error then
print("Error:", response.error)
else
print("Status:", response.status) -- 200, 404, etc.
print("Body:", response.body) -- Response body string
-- response.headers: table of headers
end
WebSocket
-- Requires "network.websocket" permission
local ws = network.websocket("wss://api.example.com/ws")
ws:on("open", function()
ws:send(json.encode({type = "hello"}))
end)
ws:on("message", function(data)
local msg = json.decode(data)
print("Received:", msg.type)
end)
ws:on("close", function(code, reason)
print("Closed:", code, reason)
end)
ws:on("error", function(err)
print("Error:", err)
end)
-- Send data
ws:send("Hello, server!")
-- Close connection
ws:close()
Camera API
-- Requires "camera" permission + user gesture
local granted = permissions.request("camera")
if not granted then return end
-- Start camera preview
camera.start({
facing = "back", -- "back" or "front"
resolution = "720p", -- "480p", "720p", "1080p"
target = "preview" -- Element ID to render preview
})
-- Capture photo
camera.capture(function(image)
-- image.data: base64 encoded JPEG
-- image.width, image.height: dimensions
storage.write("photos/capture.jpg", base64.decode(image.data))
end)
-- Stop camera
camera.stop()
Microphone API
-- Requires "microphone" permission + user gesture
local granted = permissions.request("microphone")
if not granted then return end
-- Start recording
microphone.start({
format = "wav", -- "wav" or "ogg"
sample_rate = 44100,
channels = 1
})
-- Stop and get recording
microphone.stop(function(audio)
-- audio.data: base64 encoded audio
-- audio.duration: length in seconds
storage.write("recordings/voice.wav", base64.decode(audio.data))
end)
Location API
-- Requires "location.fine" or "location.coarse" permission
local granted = permissions.request("location.fine")
if not granted then return end
-- Get current location
location.get(function(pos)
if pos.error then
print("Error:", pos.error)
else
print("Lat:", pos.latitude)
print("Lon:", pos.longitude)
print("Accuracy:", pos.accuracy) -- meters
end
end)
-- Watch location changes
local watch_id = location.watch(function(pos)
print("Location updated:", pos.latitude, pos.longitude)
end, {
interval = 5000, -- minimum time between updates (ms)
distance = 10 -- minimum distance change (meters)
})
-- Stop watching
location.unwatch(watch_id)
Sensors API
-- Motion sensors (no permission required)
sensors.accelerometer.start(function(data)
print("Accel:", data.x, data.y, data.z)
end, { interval = 100 }) -- ms
sensors.gyroscope.start(function(data)
print("Gyro:", data.x, data.y, data.z)
end)
sensors.magnetometer.start(function(data)
print("Mag:", data.x, data.y, data.z)
end)
-- Stop sensor
sensors.accelerometer.stop()
-- Body sensors (requires "sensors.body" permission)
local granted = permissions.request("sensors.body")
if granted then
sensors.heartrate.start(function(data)
print("Heart rate:", data.bpm)
end)
end
Timer API
-- One-shot timer
local timer_id = timer.after(1000, function()
print("1 second elapsed")
end)
-- Repeating timer
local interval_id = timer.every(500, function()
print("Every 500ms")
end)
-- Cancel timers
timer.cancel(timer_id)
timer.cancel(interval_id)
JSON API
-- Encode Lua table to JSON string
local json_str = json.encode({
name = "John",
age = 30,
active = true,
tags = {"lua", "mosis"}
})
-- Decode JSON string to Lua table
local data = json.decode(json_str)
print(data.name) -- "John"
Crypto API
-- Hashing
local hash = crypto.sha256("hello world")
local hash = crypto.sha512("hello world")
local hash = crypto.md5("hello world") -- Not for security
-- HMAC
local mac = crypto.hmac("sha256", "secret key", "message")
-- Random bytes
local random = crypto.random(32) -- 32 random bytes (base64)
-- UUID generation
local uuid = crypto.uuid() -- "550e8400-e29b-41d4-a716-446655440000"
Navigation
-- Navigate to another screen
navigateTo("screens/settings.rml")
-- Go back
goBack()
-- Go to home screen
goHome()
-- Get current screen
local screen = getCurrentScreen()
App Categories
Valid categories for the category manifest field:
| Category | Description |
|---|---|
utilities |
Tools and utilities |
productivity |
Work and productivity |
communication |
Messaging and social |
entertainment |
Games and media |
lifestyle |
Health, fitness, lifestyle |
finance |
Banking and finance |
education |
Learning and reference |
news |
News and magazines |
travel |
Travel and navigation |
shopping |
Shopping and commerce |
Error Handling
-- Safe API calls with pcall
local ok, result = pcall(function()
return network.request({url = "https://api.example.com"})
end)
if not ok then
print("Error:", result)
end
-- API errors return error field
local response = network.request({url = "https://invalid"})
if response.error then
print("Network error:", response.error)
end
Best Practices
Do
- Request only the permissions you need
- Handle permission denials gracefully
- Cache network responses when appropriate
- Clean up timers and resources when done
- Use try/catch patterns for error handling
- Store sensitive data encrypted
Don't
- Request permissions before they're needed
- Store sensitive data in plain text
- Make unnecessary network requests
- Hold camera/microphone open unnecessarily
- Create infinite loops or excessive recursion
- Assume network is always available