Permissions System
The Qirvo Plugin SDK uses a comprehensive permissions system to ensure security and user privacy. This guide covers all available permissions, how to request them, and best practices for secure plugin development.
Table of Contents
- Permission Types
- Requesting Permissions
- Runtime Permission Checks
- Permission Scopes
- User Consent
- Security Best Practices
Permission Types
Core Permissions
Storage Permissions
{
"permissions": [
"storage-read", // Read from plugin storage
"storage-write" // Write to plugin storage
]
}
Usage Example:
export default class StoragePlugin extends BasePlugin {
async onEnable(context: PluginRuntimeContext): Promise<void> {
// Check storage permissions
if (this.hasPermission(context, 'storage-read')) {
const data = await context.storage.get('user-data');
this.log('info', 'Loaded user data:', data);
}
if (this.hasPermission(context, 'storage-write')) {
await context.storage.set('last-access', new Date().toISOString());
}
}
private hasPermission(context: PluginRuntimeContext, permission: string): boolean {
return context.plugin.permissions.some(p =>
p.type === permission && p.granted
);
}
}
Network Permissions
{
"permissions": [
"network-access" // Make HTTP requests to external APIs
]
}
Usage Example:
export default class NetworkPlugin extends BasePlugin {
async fetchExternalData(): Promise<any> {
if (!this.hasPermission(this.context, 'network-access')) {
throw new Error('Network access permission not granted');
}
try {
const response = await this.context.api.http.get('https://api.example.com/data');
return await response.json();
} catch (error) {
this.log('error', 'Network request failed:', error);
throw error;
}
}
}
Notification Permissions
{
"permissions": [
"notifications" // Show desktop notifications
]
}
Usage Example:
export default class NotificationPlugin extends BasePlugin {
async showNotification(title: string, message: string): Promise<void> {
if (!this.hasPermission(this.context, 'notifications')) {
this.log('warn', 'Notification permission not granted');
return;
}
await this.context.api.notifications.show({
title,
message,
type: 'info',
duration: 5000
});
}
}
System Access Permissions
File System Access
{
"permissions": [
"filesystem-access" // Access local file system (with user consent)
]
}
Usage Example:
export default class FileSystemPlugin extends BasePlugin {
async readUserFile(filePath: string): Promise<string> {
if (!this.hasPermission(this.context, 'filesystem-access')) {
throw new Error('File system access permission not granted');
}
// File system operations require additional user consent
const consent = await this.requestUserConsent(
'File Access',
`This plugin wants to read: ${filePath}`,
['Allow', 'Deny']
);
if (consent !== 'Allow') {
throw new Error('User denied file access');
}
// Implement secure file reading
return await this.secureFileRead(filePath);
}
}
Clipboard Access
{
"permissions": [
"clipboard-read", // Read from clipboard
"clipboard-write" // Write to clipboard
]
}
Usage Example:
export default class ClipboardPlugin extends BasePlugin {
async copyToClipboard(text: string): Promise<void> {
if (!this.hasPermission(this.context, 'clipboard-write')) {
throw new Error('Clipboard write permission not granted');
}
await navigator.clipboard.writeText(text);
await this.notify('Copied', 'Text copied to clipboard', 'success');
}
async pasteFromClipboard(): Promise<string> {
if (!this.hasPermission(this.context, 'clipboard-read')) {
throw new Error('Clipboard read permission not granted');
}
return await navigator.clipboard.readText();
}
}
Device Access Permissions
Location Access
{
"permissions": [
"geolocation" // Access user's location
]
}
Usage Example:
export default class LocationPlugin extends BasePlugin {
async getCurrentLocation(): Promise<{ lat: number; lng: number }> {
if (!this.hasPermission(this.context, 'geolocation')) {
throw new Error('Geolocation permission not granted');
}
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
lat: position.coords.latitude,
lng: position.coords.longitude
});
},
(error) => {
this.log('error', 'Geolocation error:', error);
reject(error);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000 // 5 minutes
}
);
});
}
}
Media Access
{
"permissions": [
"camera", // Access camera
"microphone" // Access microphone
]
}
Usage Example:
export default class MediaPlugin extends BasePlugin {
async startVideoRecording(): Promise<MediaStream> {
const hasCamera = this.hasPermission(this.context, 'camera');
const hasMicrophone = this.hasPermission(this.context, 'microphone');
if (!hasCamera && !hasMicrophone) {
throw new Error('No media permissions granted');
}
const constraints: MediaStreamConstraints = {
video: hasCamera,
audio: hasMicrophone
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.log('info', 'Media stream started');
return stream;
} catch (error) {
this.log('error', 'Media access failed:', error);
throw error;
}
}
}
Data Access Permissions
Calendar Access
{
"permissions": [
"calendar" // Access user's calendar data
]
}
Usage Example:
export default class CalendarPlugin extends BasePlugin {
async getTodaysEvents(): Promise<CalendarEvent[]> {
if (!this.hasPermission(this.context, 'calendar')) {
throw new Error('Calendar permission not granted');
}
if (!this.context.api.qirvo?.calendar) {
throw new Error('Calendar API not available');
}
const today = new Date();
return await this.context.api.qirvo.calendar.getEventsForDate(today);
}
async createEvent(eventData: CreateEventData): Promise<CalendarEvent> {
if (!this.hasPermission(this.context, 'calendar')) {
throw new Error('Calendar permission not granted');
}
return await this.context.api.qirvo.calendar.createEvent(eventData);
}
}
Contacts Access
{
"permissions": [
"contacts" // Access user's contacts
]
}
Usage Example:
export default class ContactsPlugin extends BasePlugin {
async searchContacts(query: string): Promise<Contact[]> {
if (!this.hasPermission(this.context, 'contacts')) {
throw new Error('Contacts permission not granted');
}
// Implement secure contact search
return await this.secureContactSearch(query);
}
private async secureContactSearch(query: string): Promise<Contact[]> {
// Sanitize query to prevent injection
const sanitizedQuery = query.replace(/[^\w\s]/gi, '');
// Implement contact search logic
// This would typically interface with the system's contact API
return [];
}
}
Requesting Permissions
Manifest Declaration
All permissions must be declared in your plugin's manifest.json:
{
"manifest_version": 1,
"name": "My Plugin",
"permissions": [
"storage-read",
"storage-write",
"network-access",
"notifications",
"geolocation"
],
"permission_descriptions": {
"storage-read": "Read plugin settings and cached data",
"storage-write": "Save plugin settings and cache data",
"network-access": "Fetch data from external weather APIs",
"notifications": "Show weather alerts and updates",
"geolocation": "Get your location for local weather forecasts"
}
}
Permission Descriptions
Provide clear, user-friendly descriptions for each permission:
{
"permission_descriptions": {
"network-access": "Connect to external APIs to fetch real-time data",
"filesystem-access": "Read and write files for data import/export",
"camera": "Take photos for document scanning",
"microphone": "Record voice notes and transcribe audio",
"geolocation": "Provide location-based recommendations",
"calendar": "Create events and check availability",
"contacts": "Suggest contacts for sharing and collaboration"
}
}
Runtime Permission Requests
For sensitive permissions, request additional consent at runtime:
export default class SensitivePlugin extends BasePlugin {
async requestSensitiveOperation(): Promise<void> {
// Check if permission is granted
if (!this.hasPermission(this.context, 'filesystem-access')) {
throw new Error('File system permission not granted');
}
// Request additional user consent for specific operation
const consent = await this.requestUserConsent(
'File Access Required',
'This plugin needs to access your Documents folder to import data. Your files will not be uploaded or shared.',
['Allow Once', 'Allow Always', 'Deny'],
{
icon: 'folder',
details: [
'Files will be processed locally',
'No data will be sent to external servers',
'You can revoke this permission anytime'
]
}
);
switch (consent) {
case 'Allow Once':
await this.performFileOperation(false);
break;
case 'Allow Always':
await this.setStorage('file_access_granted', true);
await this.performFileOperation(true);
break;
case 'Deny':
throw new Error('User denied file access');
}
}
private async requestUserConsent(
title: string,
message: string,
options: string[],
details?: {
icon?: string;
details?: string[];
}
): Promise<string> {
// This would be implemented by the Qirvo platform
return new Promise((resolve) => {
// Show consent dialog
const dialog = {
title,
message,
options,
...details
};
// Platform-specific consent UI
// Returns user's choice
resolve('Allow Once');
});
}
}
Runtime Permission Checks
Permission Validation Helper
class PermissionValidator {
constructor(private context: PluginRuntimeContext) {}
hasPermission(permission: string): boolean {
return this.context.plugin.permissions.some(p =>
p.type === permission && p.granted
);
}
requirePermission(permission: string): void {
if (!this.hasPermission(permission)) {
throw new Error(`Permission '${permission}' is required but not granted`);
}
}
requirePermissions(permissions: string[]): void {
const missing = permissions.filter(p => !this.hasPermission(p));
if (missing.length > 0) {
throw new Error(`Missing permissions: ${missing.join(', ')}`);
}
}
getGrantedPermissions(): string[] {
return this.context.plugin.permissions
.filter(p => p.granted)
.map(p => p.type);
}
getDeniedPermissions(): string[] {
return this.context.plugin.permissions
.filter(p => !p.granted)
.map(p => p.type);
}
}
// Usage in plugin
export default class SecurePlugin extends BasePlugin {
private permissions: PermissionValidator;
async onEnable(context: PluginRuntimeContext): Promise<void> {
this.permissions = new PermissionValidator(context);
// Check required permissions
try {
this.permissions.requirePermissions(['storage-read', 'storage-write']);
await this.initializeStorage();
} catch (error) {
this.log('error', 'Required permissions not granted:', error);
await this.showPermissionError();
return;
}
// Optional permissions
if (this.permissions.hasPermission('network-access')) {
await this.enableNetworkFeatures();
}
if (this.permissions.hasPermission('notifications')) {
await this.enableNotifications();
}
}
}
Graceful Degradation
export default class AdaptivePlugin extends BasePlugin {
async onEnable(context: PluginRuntimeContext): Promise<void> {
const permissions = new PermissionValidator(context);
// Core functionality (always available)
await this.initializeCore();
// Network features (optional)
if (permissions.hasPermission('network-access')) {
await this.enableOnlineFeatures();
this.log('info', 'Online features enabled');
} else {
await this.enableOfflineMode();
this.log('info', 'Running in offline mode');
}
// Notification features (optional)
if (permissions.hasPermission('notifications')) {
await this.enableNotifications();
} else {
await this.enableInAppAlerts();
}
// Location features (optional)
if (permissions.hasPermission('geolocation')) {
await this.enableLocationFeatures();
} else {
await this.enableManualLocationEntry();
}
}
private async enableOfflineMode(): Promise<void> {
// Implement offline functionality
await this.setStorage('offline_mode', true);
await this.loadCachedData();
}
private async enableInAppAlerts(): Promise<void> {
// Use in-app notifications instead of system notifications
this.useInAppNotifications = true;
}
private async enableManualLocationEntry(): Promise<void> {
// Provide manual location input instead of GPS
this.showLocationInput = true;
}
}
Permission Scopes
Scoped Permissions
Some permissions can be scoped to specific resources:
{
"permissions": [
{
"type": "network-access",
"scope": ["api.weather.com", "api.openweathermap.org"]
},
{
"type": "filesystem-access",
"scope": ["~/Documents", "~/Downloads"]
},
{
"type": "calendar",
"scope": ["read-only"]
}
]
}
Time-Limited Permissions
{
"permissions": [
{
"type": "camera",
"duration": "session" // Only for current session
},
{
"type": "geolocation",
"duration": 3600 // 1 hour in seconds
}
]
}
Conditional Permissions
export default class ConditionalPlugin extends BasePlugin {
async requestLocationBasedFeature(): Promise<void> {
// Only request location permission when actually needed
if (!this.hasPermission(this.context, 'geolocation')) {
const consent = await this.requestPermissionConsent(
'geolocation',
'Location access is needed to provide weather updates for your area'
);
if (!consent) {
// Provide alternative functionality
await this.showManualLocationEntry();
return;
}
}
await this.enableLocationFeatures();
}
}
User Consent
Consent Dialog Best Practices
interface ConsentOptions {
title: string;
message: string;
purpose: string;
dataUsage: string[];
retention: string;
sharing: string;
alternatives?: string;
}
export default class ConsentAwarePlugin extends BasePlugin {
async requestDataAccess(type: string): Promise<boolean> {
const consentOptions: ConsentOptions = {
title: 'Data Access Request',
message: `${this.context.plugin.name} would like to access your ${type}`,
purpose: 'To provide personalized recommendations and sync your data',
dataUsage: [
'Data is processed locally on your device',
'No personal information is sent to external servers',
'Data is used only for plugin functionality'
],
retention: 'Data is stored until you uninstall the plugin',
sharing: 'Your data is never shared with third parties',
alternatives: 'You can use manual input instead of automatic data access'
};
return await this.showConsentDialog(consentOptions);
}
private async showConsentDialog(options: ConsentOptions): Promise<boolean> {
// Platform-specific consent dialog
return new Promise((resolve) => {
// Implementation would be provided by Qirvo platform
resolve(true);
});
}
}
Consent Tracking
export default class ConsentTrackingPlugin extends BasePlugin {
async trackConsent(permission: string, granted: boolean): Promise<void> {
const consentRecord = {
permission,
granted,
timestamp: new Date().toISOString(),
version: this.context.plugin.version,
userAgent: navigator.userAgent
};
await this.setStorage(`consent_${permission}`, consentRecord);
// Log consent for audit purposes
this.log('info', 'Consent recorded:', consentRecord);
}
async getConsentHistory(): Promise<any[]> {
const keys = await this.context.storage.keys();
const consentKeys = keys.filter(key => key.startsWith('consent_'));
const history = [];
for (const key of consentKeys) {
const record = await this.getStorage(key);
if (record) {
history.push(record);
}
}
return history.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
}
Security Best Practices
Principle of Least Privilege
{
"permissions": [
// ❌ Don't request unnecessary permissions
// "filesystem-access",
// "camera",
// "microphone",
// ✅ Only request what you actually need
"storage-read",
"storage-write",
"network-access",
"notifications"
]
}
Input Validation
export default class SecurePlugin extends BasePlugin {
async processUserInput(input: string): Promise<void> {
// Validate and sanitize input
const sanitized = this.sanitizeInput(input);
if (!this.isValidInput(sanitized)) {
throw new Error('Invalid input provided');
}
await this.processValidInput(sanitized);
}
private sanitizeInput(input: string): string {
return input
.trim()
.replace(/[<>]/g, '') // Remove potential HTML
.replace(/['"]/g, '') // Remove quotes
.substring(0, 1000); // Limit length
}
private isValidInput(input: string): boolean {
// Implement validation logic
return input.length > 0 && input.length <= 1000;
}
}
Secure Data Handling
export default class DataSecurePlugin extends BasePlugin {
async storeSecureData(key: string, data: any): Promise<void> {
// Encrypt sensitive data before storage
const encrypted = await this.encryptData(data);
await this.setStorage(key, encrypted);
}
async retrieveSecureData(key: string): Promise<any> {
const encrypted = await this.getStorage(key);
if (!encrypted) return null;
return await this.decryptData(encrypted);
}
private async encryptData(data: any): Promise<string> {
// Implement encryption (this would use a proper crypto library)
const jsonString = JSON.stringify(data);
return btoa(jsonString); // Basic encoding (use proper encryption in production)
}
private async decryptData(encrypted: string): Promise<any> {
// Implement decryption
const jsonString = atob(encrypted);
return JSON.parse(jsonString);
}
}
Permission Monitoring
export default class MonitoredPlugin extends BasePlugin {
async onEnable(context: PluginRuntimeContext): Promise<void> {
// Monitor permission usage
this.setupPermissionMonitoring();
// Regular permission audit
setInterval(() => {
this.auditPermissions();
}, 24 * 60 * 60 * 1000); // Daily
}
private setupPermissionMonitoring(): void {
// Override permission-requiring methods to log usage
const originalHttpGet = this.context.api.http.get;
this.context.api.http.get = async (url: string, options?: any) => {
this.logPermissionUsage('network-access', { url });
return await originalHttpGet.call(this.context.api.http, url, options);
};
}
private logPermissionUsage(permission: string, details: any): void {
const usage = {
permission,
timestamp: new Date().toISOString(),
details
};
this.log('debug', 'Permission used:', usage);
}
private async auditPermissions(): Promise<void> {
const grantedPermissions = this.context.plugin.permissions
.filter(p => p.granted)
.map(p => p.type);
this.log('info', 'Permission audit:', {
granted: grantedPermissions,
plugin: this.context.plugin.name,
version: this.context.plugin.version
});
}
}
This comprehensive permissions system ensures that Qirvo plugins operate securely while providing users with full control over their data and privacy.
Next: External Services for third-party API integration documentation.