@mica/sdk (SDK contract v1)
    Preparing search index...

    Interface AppManifest

    interface AppManifest {
        author?: string;
        badgeStore?: Readable<number>;
        bundleUrl?: string;
        color: string;
        core: boolean;
        defaultProps?: Record<string, unknown>;
        description?: string;
        devices?: readonly ("phone" | "tablet")[];
        icon: string | Component<any, {}, string> | Snippet<[]> | null;
        id: string;
        installedAt?: string;
        isRemote?: boolean;
        keybinds?: AppKeybindInput[];
        name: string;
        networkHosts?: readonly string[];
        permissions?: (
            | "messages"
            | "admin"
            | "location"
            | "camera"
            | "account"
            | "bank"
            | "call"
            | "contacts"
            | "control-center"
            | "highscores"
            | "jobs"
            | "mail"
            | "marketplace"
            | "media"
            | "notifications"
            | "reports"
            | "social"
            | "storage"
            | "app-events"
            | "app-registry"
            | "app-registry-write"
            | "clock"
            | "clock-write"
            | "devtools"
            | "display"
            | "display-write"
            | "keybinds"
            | "keybinds-write"
            | "lock-screen"
            | "lock-screen-write"
            | "music"
            | "navigation"
            | "notification-settings"
            | "notification-settings-write"
            | "system-hardware"
            | "system-hardware-write"
            | "theme"
            | "theme-write"
            | "wallpaper"
            | "wallpaper-write"
        )[];
        preload?: () => void | Promise<unknown>;
        requires?: ("jobs" | "money")[];
        requiresAdmin?: boolean;
        requiresNetwork?: boolean;
        sdkContract?: string;
        services?: readonly string[];
        tile: AppTile;
        updatedAt?: string;
        version?: string;
        widget?: {
            load?: () => Promise<{ default: Component<{ size: "2x1" | "2x2" }> }>;
            sizes: readonly ("2x1" | "2x2")[];
        };
    }
    Index
    author?: string

    App author or developer team

    badgeStore?: Readable<number>

    Reactive unread count for the launcher badge — unreadMailCount and friends.

    bundleUrl?: string

    Remote bundle URL if dynamically loaded

    color: string

    tile flattened to the class string every consumer interpolates. Derived — do not author it.

    Still here because ten-odd call sites read it (AppIcon, Launcher, Dock, AppDrawer, FolderPopup, DragGhost, ToastHost, the Store and Settings listings) and because an add-on bundle published before tile existed still sets it. defineApp derives whichever of the two was not given, so both are always present and always agree; writing both is refused rather than merged.

    core: boolean

    Does this app ship with the phone, and is it therefore not uninstallable?

    Required, and deliberately not inferred. It was once isSystem, defaulted from author — a display string — so an app whose author was anything other than 'Community' silently became unremovable, and naming yourself 'micaOS' was enough to do it. Two things followed from deriving a protection boundary instead of stating it: the derivation was circular (isSystem read author, then author read isSystem), and a second, subtly different copy of it grew in the Store — so the registry and the uninstall button could disagree about the same app, and the button threw.

    A remote app is never core. defineApp forces false when isRemote is set and throws on an explicit core: true beside it: a downloaded bundle asking for protection is broken or hostile. That closes a footgun rather than preventing an attack — an add-on runs in the shell's own JS context regardless (§7).

    defaultProps?: Record<string, unknown>

    Default props passed when launching app component

    description?: string

    Brief description of app functionality

    devices?: readonly ("phone" | "tablet")[]

    Which devices this app appears on (MICA-260). Absent means ['phone'].

    A visibility contract exactly as requires is: an app whose list lacks the device on screen has no icon on it, in the launcher, the dock, the drawer, a folder or search, and openApp refuses it. Nothing about the server changes.

    Listing 'tablet' says the app is usable at 1280x800, not merely that it loads: the shell renders tablet.svelte beside index.svelte when the app ships one and index.svelte stretched to the frame when it does not. useDisplay().device says which frame the app is in, for the app that wants one root and two layouts.

    defineApp supplies no default, for the reason requires has none: absent is the only thing a bundle published before this field existed can say, and it has to keep meaning what it always did — a phone app.

    icon: string | Component<any, {}, string> | Snippet<[]> | null

    A Svelte component, a snippet, or an image URL. Null renders no glyph, which is what a remote app that shipped without one gets.

    id: string

    Unique id — contacts, crypto_tracker. lower_snake_case.

    The stable one. It is a directory name, the mica:<id>: storage namespace, the <app> segment of every net event, a keybind claim and the ?app= deep link. Renaming it is a data migration, not a rename, which is exactly why it is not derived from name: a display string should be free to change without orphaning stored data or altering event names that server code handles.

    installedAt?: string

    ISO date string when app was installed

    isRemote?: boolean

    Flag indicating whether app was dynamically loaded from a remote bundle

    keybinds?: AppKeybindInput[]

    Default hotkeys this app wants, shown grouped under it in Settings > Shortcuts. Only apps for which this is set contribute app-scoped keybinds; most apps omit it.

    name: string

    Display name on the home screen. Cosmetic — the launcher label, the Store listing and the error-boundary message, nothing that is keyed on.

    Optional on input. defineApp title-cases the id when it is omitted, so crypto_tracker becomes "Crypto Tracker" and all twelve apps in this repo needed only the id. Give it explicitly when the display name is not simply the id — "GPS", "My Bank".

    networkHosts?: readonly string[]

    MICA-24: the exact origins a core: false add-on's sandboxed frame may fetch().

    The frame has no Content-Security-Policy otherwise, so an add-on's own JS — not just the code an author wrote, but anything a supply-chain compromise slipped into its bundle — could silently call out to any host on the internet. srcdoc.ts turns this list into the frame's connect-src; an empty or absent list means connect-src 'none', blocking outbound fetch() entirely.

    Declaring this without also declaring requiresNetwork: true is refused by defineApp — asking for real network egress while saying the app does not need network at all is a contradiction, not an oversight to let through quietly. The reverse is fine and common: an app can need requiresNetwork (in-game cell signal, for its ordinary server-proxied calls) without ever calling fetch() itself, and should simply leave this empty rather than declare a host it does not use.

    Full origins only (https://api.example.com, no path) — defineApp rejects anything else, since a bare hostname or a wildcard is not a valid CSP source and srcdoc.ts does no correction of its own.

    permissions?: (
        | "messages"
        | "admin"
        | "location"
        | "camera"
        | "account"
        | "bank"
        | "call"
        | "contacts"
        | "control-center"
        | "highscores"
        | "jobs"
        | "mail"
        | "marketplace"
        | "media"
        | "notifications"
        | "reports"
        | "social"
        | "storage"
        | "app-events"
        | "app-registry"
        | "app-registry-write"
        | "clock"
        | "clock-write"
        | "devtools"
        | "display"
        | "display-write"
        | "keybinds"
        | "keybinds-write"
        | "lock-screen"
        | "lock-screen-write"
        | "music"
        | "navigation"
        | "notification-settings"
        | "notification-settings-write"
        | "system-hardware"
        | "system-hardware-write"
        | "theme"
        | "theme-write"
        | "wallpaper"
        | "wallpaper-write"
    )[]

    What this app reaches for, as shown to a player in the Store.

    A disclosure, not only a sandbox. A core: true app runs in the shell's own JS context, so nothing stops it importing a hook it did not declare; a core: false add-on runs in a sandboxed iframe and has the shell re-check every call. Either way §2.9 is the real boundary underneath: the server gates privileged actions and does not take a NUI request as proof of intent.

    What is enforced is that the disclosure is true. sdk/permissions.test.ts reads each app's SDK imports and fails the build if it reaches for contacts, photos, the camera, notifications or local storage without saying so. Declaring more than the scan finds is allowed — network and location have no hook to infer them from.

    preload?: () => void | Promise<unknown>

    Load this app's data while the phone opens, before the launcher draws.

    Declared by the app rather than listed by the shell. bootstrap.ts used to name each store by hand, and nothing connected that list to the apps it was loading for — so an app that shipped a badgeStore and was forgotten there showed a stale badge until somebody opened it, which is precisely when a badge no longer matters.

    Only for what has to be right before first paint. Everything else belongs in onAppForeground, which reloads per visit; this runs once per phone-open.

    requires?: ("jobs" | "money")[]

    Server capabilities this app cannot function without. Absent means it needs none.

    A visibility contract, not a security boundary — the same caveat requiresAdmin carries, and worth restating rather than inferring. All this does is keep the icon away from a phone where the app could not work; the server still refuses every action it would have refused anyway (§2.9), and an app hidden here is not a disabled server endpoint. A modified client can still emit whatever it likes.

    Not permissions, which is a disclosure of what the app reaches for, shown to a player before they install it — that is a statement about the app, this is a statement about what the deployment can do. Not requiresNetwork either: that blocks the app at runtime, behind a "Not Network" screen, because cell signal comes back. A capability the server does not have is not a transient condition, so the honest outcome is absence rather than an app that is present and always errors.

    Absent is the only thing a bundle published before this field existed can say, and it means exactly the right thing: this app needs nothing of the server beyond what every app needs. defineApp supplies no default, so nothing already installed changes behaviour and no consumer has to distinguish undefined from [].

    requiresAdmin?: boolean

    Hide the app entirely unless the player holds an admin ace.

    A visibility hint, not a permission. The server gates every privileged action it exposes; this only stops the icon appearing for everyone else.

    requiresNetwork?: boolean

    Does this app need the NUI bridge to work at all?

    Used to be inferred from permissions.includes('network') — but network disclosed nothing (every app talks to its own service, so marking it on everyone would mean marking it on no one) and conflated two different questions: what a player should be told an app reaches for, and whether the phone should block the app while signal is out. This is the second question, stated on its own rather than smuggled through the permission list. Shell.svelte's "Not Network" gate reads this, not permissions.

    sdkContract?: string

    MICA-196: the SDK_CONTRACT_VERSION this bundle was compiled against.

    sdk/version.ts published that number for an add-on to read and act on, and said plainly what it did not do: "nothing consults it at install or boot time. There is no runtime compatibility gate anywhere in the phone (MICA-173), and adding one is new manifest surface that needs deciding on its own." This is that surface, decided.

    Without it, an add-on built against a future contract installs cleanly and fails wherever it first touches something that moved — a missing export, a renamed prop — as an ErrorBoundary crash a player reads as "this add-on is broken". The version was knowable at the bundle's build; nothing was carrying it across.

    Checked for equality, not order. SDK_CONTRACT_VERSION is typed string and deliberately carries no ordering — its own doc explains why a literal type there would turn a comparison into a type error — so "newer" and "older" are not questions this can ask. What it can say is whether the phone is running the contract the bundle was built for, and a bundle built for a different one is refused at install and again at hydrate.

    Absent means "did not say", and is never refused. Every add-on published before this field existed omits it, and a bundle built by a third-party bundler that never heard of __MICA_VERSION__ omits it too. Declaring it is how an add-on asks to be refused early and legibly rather than late and confusingly.

    services?: readonly string[]

    MICA-196: the server services this app owns, named rather than inferred.

    useService(id) is the generic door to a server service and the id is the caller's to choose, so something has to decide whose service an add-on may name. Until this field that decision was a string prefix — id === appId || id.startsWith(appId + '_') — over a flat namespace that already holds blabber_dms and phone_call_log. Two apps therefore reach the same service whenever one's id is the other's prefix: Blabber reaches blabber_dms by the prefix rule, and an add-on that simply takes the id blabber_dms reaches it by owning it. Neither is a bug in either app; the rule cannot tell them apart, because it derives ownership from spelling instead of reading it.

    So an app states what it owns, and the registry refuses an install whose claim collides with an installed app's — that check is what makes the declaration worth anything, and it is why this is not merely documentation.

    Every entry must be the app's own id or sit under <id>_. That is the same namespace the prefix rule enforced, kept deliberately: this field exists to make a claim checkable, not to let an app claim more than it could before. A declaration outside it is refused here, at definition time, where the author can see it.

    Absent is not "owns nothing". Every add-on published before this field existed says nothing, and for those the old prefix rule still answers — see IframeHostServer's serviceAllowed. Refusing them, or reading absence as an empty list, would break every one of them, which is exactly the failure @mica/sdk is built to avoid. Declaring it is how an app opts into the stricter, checked answer.

    tile: AppTile

    The launcher tile: what it is painted, and what the glyph on it inherits.

    The authored field (MICA-91). It replaces a free-form color string that held both roles at once, positionally and unchecked — which is what let a tile and its glyph disagree, and what MICA-88 had to go and reconcile by hand across sixteen apps.

    updatedAt?: string

    ISO date string when app was last updated

    version?: string

    Semantic version string (e.g. "1.0.0")

    widget?: {
        load?: () => Promise<{ default: Component<{ size: "2x1" | "2x2" }> }>;
        sizes: readonly ("2x1" | "2x2")[];
    }

    MICA-245: what this app offers the home screen besides its icon. Absent means nothing.

    sizes is the whole declaration for a core: false add-on: its bundle hands a widget root to bootAddOn beside the app root, and the shell boots it in a sandboxed frame of its own under exactly the permissions, grant and facet table the app runs with. load is for a core: true app only — the widget component, lazily, as the registry loads the app's own entry — and defineApp refuses it on an add-on, whose code the shell never imports. sizes is non-empty, repeats nothing, and names only '2x1' and '2x2'.