# 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 ```json { "$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 ```json { "$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 1. Generate `MANIFEST.MF` with SHA-256 hash of each file 2. Sign `MANIFEST.MF` with developer's Ed25519 private key 3. Store signature in `META-INF/CERT.SIG` 4. Include developer's public key in `META-INF/CERT.PEM` ### Verification Flow 1. Extract `META-INF/MANIFEST.MF` 2. Verify signature using `CERT.PEM` 3. Verify `CERT.PEM` is registered with developer account 4. 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 ```lua -- 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 ```lua -- 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) ```lua -- 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 ```lua -- 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) ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 ```lua -- 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 --- ## References - [RmlUi Documentation](https://mikke89.github.io/RmlUiDoc/) - [Lua 5.4 Reference Manual](https://www.lua.org/manual/5.4/) - [Ed25519 Specification](https://ed25519.cr.yp.to/)