add comprehensive app specifications document
This commit is contained in:
667
APP_SPECS.md
Normal file
667
APP_SPECS.md
Normal file
@@ -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/)
|
||||
Reference in New Issue
Block a user