diff --git a/APP_SPECS.md b/APP_SPECS.md new file mode 100644 index 0000000..4f8692d --- /dev/null +++ b/APP_SPECS.md @@ -0,0 +1,667 @@ +# 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/)