npm package: @mywallpaper/addon-sdk
Description: SDK for building MyWallpaper addons - TypeScript types, manifest validation, and utilities
Author: MyWallpaper Team
Latest version: 2.17.1

# @mywallpaper/addon-sdk

Official SDK for building MyWallpaper addons.

## Documentation

Full documentation for building MyWallpaper addons is available in the [docs/](docs/) directory.

To run the documentation site locally:

```bash
cd docs
npm install
npm run dev
```

The documentation includes:
- [Getting Started Guide](docs/docs/guide/getting-started.md)
- [API Reference](docs/docs/api/overview.md)
- [Manifest Specification](docs/docs/guide/manifest.md)
- [Examples](docs/docs/examples/basic.md)

## Installation

```bash
npm install @mywallpaper/addon-sdk
# or
yarn add @mywallpaper/addon-sdk
# or
pnpm add @mywallpaper/addon-sdk
```

## Quick Start

### 1. Create your addon files

```
my-addon/
├── manifest.json    # Addon metadata and settings
├── index.html       # Main HTML file
├── styles.css       # (optional) Styles
└── script.js        # (optional) JavaScript
```

### 2. Define your manifest.json

```json
{
  "name": "My Awesome Addon",
  "version": "1.0.0",
  "sdkVersion": "2.13.0",
  "description": "A cool widget for your desktop",
  "author": "Your Name",
  "capabilities": {
    "hotReload": true,
    "systemEvents": ["viewport:resize", "theme:change"]
  },
  "settings": {
    "backgroundColor": {
      "type": "color",
      "label": "Background Color",
      "default": "#1a1a2e"
    },
    "animationSpeed": {
      "type": "range",
      "label": "Animation Speed",
      "min": 0.1,
      "max": 5,
      "step": 0.1,
      "default": 1
    }
  }
}
```

### 3. Use the SDK in your addon

```html
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background: var(--bg-color, #1a1a2e);
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script>
    // The MyWallpaper API is automatically injected
    const api = window.MyWallpaper

    // Get initial config
    const { backgroundColor, animationSpeed } = api.config

    // Apply initial config
    document.body.style.setProperty('--bg-color', backgroundColor)

    // React to settings changes (hot-reload)
    api.onSettingsChange((settings, changedKeys) => {
      if (changedKeys.includes('backgroundColor')) {
        document.body.style.setProperty('--bg-color', settings.backgroundColor)
      }
      if (changedKeys.includes('animationSpeed')) {
        updateAnimationSpeed(settings.animationSpeed)
      }
    })

    // Handle viewport resize
    api.onEvent('viewport:resize', ({ width, height }) => {
      const canvas = document.getElementById('canvas')
      canvas.width = width
      canvas.height = height
      redraw()
    })

    // Signal addon is ready
    api.ready({
      capabilities: ['hot-reload'],
      subscribedEvents: ['viewport:resize']
    })

    // After visual setup is complete
    initCanvas()
    api.renderComplete()
  </script>
</body>
</html>
```

## TypeScript Support

This package includes full TypeScript definitions. Use them for type-safe development:

```typescript
import type {
  MyWallpaperAPI,
  SystemEventType,
  AddonManifest
} from '@mywallpaper/addon-sdk'

// Type-safe event handling
const api = window.MyWallpaper

api.onEvent('viewport:resize', ({ width, height }) => {
  // TypeScript knows width and height are numbers
  console.log(`${width}x${height}`)
})

api.onEvent('theme:change', ({ theme }) => {
  // TypeScript knows theme is 'light' | 'dark' | 'system'
  document.body.className = theme
})
```

### Manifest Validation

```typescript
import { validateManifest, type AddonManifest } from '@mywallpaper/addon-sdk/manifest'

const manifest: AddonManifest = {
  name: 'My Addon',
  version: '1.0.0',
  settings: {
    primaryColor: { type: 'color', default: '#ff0000' }
  }
}

const result = validateManifest(manifest)
if (!result.success) {
  console.error('Invalid manifest:', result.errors)
}
```

## API Reference

### `window.MyWallpaper`

The main API object, automatically injected into your addon.

#### Properties

| Property | Type | Description |
|----------|------|-------------|
| `config` | `Record<string, unknown>` | Current settings values |
| `layerId` | `string` | Unique layer instance ID |
| `version` | `'2.0'` | SDK version |
| `storage` | `StorageAPI` | Persistent storage API |
| `network` | `NetworkAPI` | Secure network requests API |

#### Methods

##### `ready(options?)`

Signal that your addon is initialized and ready.

```typescript
api.ready({
  capabilities: ['hot-reload', 'storage'],
  subscribedEvents: ['viewport:resize', 'theme:change']
})
```

##### `renderComplete()`

Signal that visual rendering is complete (for screenshot service).

```typescript
await loadAllImages()
drawCanvas()
api.renderComplete()
```

##### `onSettingsChange(callback)`

React to settings changes (hot-reload).

```typescript
api.onSettingsChange((settings, changedKeys) => {
  changedKeys.forEach(key => {
    console.log(`${key} changed to ${settings[key]}`)
  })
})
```

##### `onButtonClick(settingKey, callback)` / `offButtonClick(settingKey)`

Handle button setting clicks.

```typescript
// Register a handler for a button setting
api.onButtonClick('resetButton', () => {
  resetToDefaults()
  console.log('Reset clicked!')
})

// Remove the handler when no longer needed
api.offButtonClick('resetButton')
```

With manifest:

```json
{
  "settings": {
    "resetButton": {
      "type": "button",
      "buttonLabel": "Reset to Defaults"
    }
  }
}
```

##### `onEvent(event, callback)` / `offEvent(event, callback)`

Subscribe/unsubscribe to system events.

```typescript
const handler = ({ width, height }) => resize(width, height)

api.onEvent('viewport:resize', handler)
// Later...
api.offEvent('viewport:resize', handler)
```

##### `requestPermission(permission, reason)`

Request a permission from the user.

```typescript
const granted = await api.requestPermission(
  'storage',
  'Save your preferences for next time'
)

if (granted) {
  await api.storage.set('initialized', true)
}
```

#### Lifecycle Methods

```typescript
api.onMount(() => console.log('Addon mounted'))
api.onUnmount(() => cleanup())
api.onPause(() => pauseAnimations())
api.onResume(() => resumeAnimations())
```

### Storage API

Persistent key-value storage that syncs across devices.

```typescript
const { storage } = window.MyWallpaper

// Store data
await storage.set('preferences', { theme: 'dark', fontSize: 14 })

// Retrieve data (with TypeScript generics)
const prefs = await storage.get<{ theme: string; fontSize: number }>('preferences')

// List all keys
const keys = await storage.keys()

// Check storage usage
const bytesUsed = await storage.size()
console.log(`Using ${bytesUsed} of 1MB quota`)

// Delete a key
await storage.delete('oldData')

// Clear all storage
await storage.clear()
```

### OAuth API

Access OAuth provider APIs (GitHub, Google, Discord, Spotify, Twitch) securely without ever seeing the user's tokens.

**How it works:**
1. User connects their OAuth provider in MyWallpaper settings
2. Your addon requests permission (e.g., `oauth:github`)
3. API calls are proxied through the host - your addon never sees the token
4. The host handles token refresh automatically

**Step 1:** Declare required OAuth permission in `manifest.json`:

```json
{
  "permissions": {
    "required": ["oauth:github"],
    "optional": ["oauth:spotify"]
  }
}
```

**Step 2:** Request permission and make API calls:

```typescript
const api = window.MyWallpaper

// Request GitHub permission
const granted = await api.requestPermission(
  'oauth:github',
  'Display your GitHub profile information'
)

if (!granted) {
  console.log('GitHub access denied')
  return
}

// Make API calls through the proxy
const response = await api.oauth.request('github', '/user')

if (response.ok) {
  console.log('GitHub profile:', response.data)
  console.log('Username:', response.data.login)
  console.log('Followers:', response.data.followers)
}

// POST request example
const result = await api.oauth.request('github', '/user/repos', {
  method: 'POST',
  body: { name: 'new-repo', private: true }
})

// Check if user has connected a provider
const isConnected = await api.oauth.isConnected('github')
```

#### Supported OAuth Providers

| Provider | Permission | API Base URL |
|----------|------------|--------------|
| GitHub | `oauth:github` | `https://api.github.com` |
| Google | `oauth:google` | `https://www.googleapis.com` |
| Discord | `oauth:discord` | `https://discord.com/api/v10` |
| Spotify | `oauth:spotify` | `https://api.spotify.com/v1` |
| Twitch | `oauth:twitch` | `https://api.twitch.tv/helix` |

#### OAuthResponse

| Property | Type | Description |
|----------|------|-------------|
| `ok` | `boolean` | `true` if status is 200-299 |
| `status` | `number` | HTTP status code |
| `headers` | `Record<string, string>` | Response headers |
| `data` | `unknown` | Parsed JSON response |

**Security:** Your addon NEVER sees OAuth tokens. All requests are proxied through the host, which adds the `Authorization` header server-side. Tokens are encrypted at rest with AES-256-GCM.

---

### Network API

**SECURITY:** Direct `fetch()` and `XMLHttpRequest` are blocked for security.
All network requests MUST go through `MyWallpaper.network.fetch()`.

**Step 1:** Declare allowed domains in your `manifest.json`:

```json
{
  "permissions": {
    "network": {
      "domains": ["api.weather.com", "api.example.com"]
    }
  }
}
```

**Step 2:** Request permission and make requests:

```typescript
const { network } = window.MyWallpaper

// Request network permission first
const granted = await MyWallpaper.requestPermission(
  'network',
  'Fetch weather data to display current conditions'
)

if (!granted) {
  console.log('Network permission denied')
  return
}

// GET request
const response = await network.fetch('https://api.weather.com/current?city=Paris')
if (response.ok) {
  const weather = response.data
  console.log(`Temperature: ${weather.temp}°C`)
}

// POST request with JSON
const result = await network.fetch('https://api.example.com/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' })
})

// Handle errors
try {
  const data = await network.fetch('https://blocked-domain.com/api')
} catch (error) {
  // Error: Domain not allowed. Allowed domains: api.weather.com, api.example.com
}
```

#### NetworkResponse

| Property | Type | Description |
|----------|------|-------------|
| `ok` | `boolean` | `true` if status is 200-299 |
| `status` | `number` | HTTP status code |
| `statusText` | `string` | HTTP status text |
| `headers` | `Record<string, string>` | Response headers |
| `data` | `unknown` | Auto-parsed response (JSON, text, or base64) |

## System Events

| Event | Data | Description |
|-------|------|-------------|
| `viewport:resize` | `{ width, height, aspectRatio }` | Viewport dimensions changed |
| `theme:change` | `{ theme: 'light' \| 'dark' \| 'system' }` | User changed theme |
| `visibility:change` | `{ visible: boolean }` | Page visibility changed |
| `layer:focus` | `{ layerId: string }` | Layer gained focus |
| `layer:blur` | `{ layerId: string }` | Layer lost focus |
| `app:idle` | `{ idleSeconds: number }` | User is idle |
| `app:active` | `{}` | User became active |

## Setting Types

| Type | UI Control | Value Type |
|------|------------|------------|
| `string` | Text input | `string` |
| `number` | Number input | `number` |
| `boolean` | Toggle | `boolean` |
| `color` | Color picker | `string` (hex) |
| `select` | Dropdown | `string` |
| `range` | Slider | `number` |
| `image` | Image upload | `string` (URL) |
| `textarea` | Multi-line text | `string` |
| `radio` | Radio buttons | `string` |
| `multi-select` | Checkboxes | `string[]` |
| `gradient` | Gradient editor | `{ stops: [...] }` |
| `vector2` | X/Y inputs | `{ x, y }` |
| `section` | Visual divider | (no value) |
| `button` | Action button | (no value) |

### Setting Definition Example

```json
{
  "settings": {
    "header": {
      "type": "section",
      "label": "Appearance"
    },
    "primaryColor": {
      "type": "color",
      "label": "Primary Color",
      "default": "#3b82f6",
      "description": "Main accent color"
    },
    "opacity": {
      "type": "range",
      "label": "Opacity",
      "min": 0,
      "max": 100,
      "step": 1,
      "default": 100
    },
    "style": {
      "type": "select",
      "label": "Style",
      "default": "modern",
      "options": [
        { "value": "modern", "label": "Modern" },
        { "value": "classic", "label": "Classic" },
        { "value": "minimal", "label": "Minimal" }
      ]
    }
  }
}
```

## Permissions

| Permission | Description | Default Quota |
|------------|-------------|---------------|
| `storage` | Persistent storage | 1MB |
| `network` | External requests | Domain whitelist |
| `audio` | Audio playback | - |
| `cpu-high` | Higher CPU quota | - |
| `notifications` | Desktop notifications | - |
| `oauth:github` | GitHub API access | - |
| `oauth:google` | Google API access | - |
| `oauth:discord` | Discord API access | - |
| `oauth:spotify` | Spotify API access | - |
| `oauth:twitch` | Twitch API access | - |

Declare permissions in manifest for better UX:

```json
{
  "permissions": {
    "storage": { "quota": 512 },
    "network": { "domains": ["api.example.com"] }
  }
}
```

## Utilities

The SDK includes helpful utilities:

```typescript
import {
  createDefaultConfig,
  mergeConfigs,
  compareVersions,
  formatBytes,
  debounce,
  throttle
} from '@mywallpaper/addon-sdk'

// Create defaults from manifest
const defaults = createDefaultConfig(manifest)

// Merge with user config
const config = mergeConfigs(defaults, userConfig)

// Version comparison
if (compareVersions('2.0.0', '1.9.9') > 0) {
  console.log('v2 is newer')
}

// Format storage size
console.log(formatBytes(1048576)) // "1 MB"

// Debounce expensive operations
const saveSettings = debounce((settings) => {
  localStorage.setItem('settings', JSON.stringify(settings))
}, 1000)

// Throttle frequent events
const updatePosition = throttle((x, y) => {
  element.style.transform = `translate(${x}px, ${y}px)`
}, 16) // ~60fps
```

## Local Development

Test your addon locally with the real MyWallpaper runtime using **Dev Mode**.

### 1. Start a local server

```bash
cd my-addon

# Option A: Using npx (no install needed)
npx serve .

# Option B: Using Python
python -m http.server 5173

# Option C: Using VS Code Live Server extension
# Right-click index.html → "Open with Live Server"
```

Your addon is now available at `http://localhost:5173` (or another port).

### 2. Enable Dev Mode in MyWallpaper

1. Go to **Settings > Developer**
2. Enter your localhost URL: `http://localhost:5173`
3. Click **Test** to verify the connection
4. Toggle **Enable Developer Mode** ON

Your addon now renders with the **real runtime** - same security sandbox, same APIs, same behavior as production!

### 3. Iterate

- Edit your code
- Refresh the page (or the addon will hot-reload if you support it)
- See changes instantly

> **Note:** Dev Mode only allows `localhost`, `127.0.0.1`, and `[::1]` URLs for security.

## Examples

Check out the `/examples` directory for complete addon examples:

- `basic/` - Minimal addon structure with settings
- `weather-widget/` - Weather widget with network API
- `github-profile/` - GitHub profile using OAuth API proxy

## Best Practices

1. **Always call `ready()`** - The host waits for this signal
2. **Call `renderComplete()`** - Helps screenshot service capture your addon correctly
3. **Declare capabilities** - Helps the host optimize resource allocation
4. **Use hot-reload** - Better UX when users change settings
5. **Request permissions early** - Ask for storage permission on first interaction
6. **Handle lifecycle** - Pause animations when hidden/paused to save resources
7. **Validate your manifest** - Use `validateManifest()` during development

## Migration from v1.0

v2.0 removes backward compatibility with v1.0 addons. Key changes:

- `apiVersion` field removed (always v2.0)
- Trust system types removed (unused)
- `convertToLegacyFormat()` removed
- `isLegacyAddonMessage()` removed
- Permissions now use 30s timeout (was 100ms bug)
- Storage now properly waits for response

## License

MIT