Permissions and permissionsChange
An app gets exactly the permissions it declares in pithyx.json and an administrator confirms. The administrator can revoke each one later and grant it again; the window then gets permissionsChange with the complete new list. Using an API grants nothing: a call without the permission fails with app.permission.denied. Build the interface from app.context.permissions and keep it current with the event.
The permissions
Section titled “The permissions”| Permission | Lets the app |
|---|---|
files.read |
read files in shared folders (with files.shares) and files the user hands to it |
files.write |
write those files |
files.shares |
list the shared folders the user can use and browse them with the user’s rights |
notifications |
show notifications with app.notify |
directory.read |
list all users and groups with app.directory.users() and groups() |
network.internet |
open connections to the internet from the window (connect-src https:) and from the backend |
network.lan |
open connections from the backend to other devices in the local network; the window and the box itself stay out of reach |
Everything else needs no permission: the context, window calls, app.storage, app.session, logs, the backend and storage requests the administrator mapped. Which users may open the app at all is a separate grant per user and group, not a permission of the app.
Asking for directory.read makes the install dialog suggest the app for administrators only, as root containers and special rights do; ask for it only when the app really needs everybody’s names.
Example
Section titled “Example”The manifest declares "permissions": ["notifications", "directory.read"]. The board works without either and offers more with them:
// A team board that offers what its permissions allow and follows when an administrator changes them.// pithyx.json declares: "permissions": ["notifications", "directory.read"]import { PithyxError, type DirectoryUser, type PithyxApp } from '@pithyx/sdk';
interface Features { notify: boolean; pickPeople: boolean;}
/** What the app may do now; context.permissions is always the current, complete list. */export function features(app: PithyxApp): Features { const has = (permission: string) => app.context.permissions.includes(permission); return { notify: has('notifications'), pickPeople: has('directory.read') };}
/** Everyone to assign a card to, or only the signed-in user without `directory.read`. */export async function people( app: PithyxApp,): Promise<Pick<DirectoryUser, 'id' | 'name' | 'displayName'>[]> { const { user } = app.context; if (!features(app).pickPeople) { return [{ id: user.id, name: user.name, displayName: user.displayName }]; } try { return await app.directory.users(); } catch (error) { // Revoked between the check and the call: the SDK or the daemon refuses with the missing permission. if (error instanceof PithyxError && error.code === 'app.permission.denied') { return [{ id: user.id, name: user.name, displayName: user.displayName }]; } throw error; }}
export async function remind(app: PithyxApp, title: string): Promise<boolean> { if (!features(app).notify) { return false; } try { await app.notify({ title, level: 'info', tag: 'board-reminder' }); return true; } catch (error) { if (error instanceof PithyxError && error.code === 'app.permission.denied') { return false; } throw error; }}
/** Renders once and again after every change; returns the function that stops following. */export function follow(app: PithyxApp, render: (features: Features) => void): () => void { render(features(app)); return app.on('permissionsChange', () => { // The SDK has replaced context.permissions and dropped its backend token before handlers run. render(features(app)); });}How changes arrive
Section titled “How changes arrive”- At install the dialog lists every declared permission and the administrator confirms them. An update that declares a new permission waits until an administrator confirms it; a revoked permission stays revoked across updates.
- When an administrator grants or revokes one, every open window of the app gets
permissionsChangewith the full list. Before your handlers run, the SDK replacescontext.permissionsand drops its backend token, so the next backend call gets a fresh one. - A call that needs a revoked permission fails with
app.permission.denied;params.permissionnames it. The SDK refusesdirectory.*at once, the daemon checks every call again. - When the user loses access to the app itself (the grant is revoked, or the app is disabled or removed), its windows close and the backend’s sockets of that user are closed.
Edge cases
Section titled “Edge cases”- Race. A permission can go between your check and your call. Handle
app.permission.deniedon every call that needs one, as the example does. network.internetis part of the window’s Content-Security-Policy, which the browser fixes when the page loads. A change applies to the window after a reload. A backend gets or loses its internet network with a restart.network.lanworks the same way for the backend.- Admins have every app, but an app still holds only the permissions confirmed for it.
- Shared folders need
files.sharesfor the list andfiles.readorfiles.writefor content; the user’s own access to each shared folder applies on top. See Files and app storage. - Mock desktop. The panel of
pithyx devlists the manifest’s permissions as switches; each switch sendspermissionsChange. The mock refusesnotifications,directory.*and its file calls without the permission, like the box.
Error codes
Section titled “Error codes”app.permission.denied, app.token.invalid, app.app.not_found. An administrator who grants a permission the manifest does not declare gets app.permission.not_requested; the app never sees it.