# 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/)
