Home
GuideAPP Design · Plugins

Provider Development Guide

A Provider loads one remote service or one app-owned internal view. It owns the service URL, navigation rules, login workarounds, request hooks, DOM injection, PiP eligibility, Menu metadata, default shortcut, and Session defaults.

Plugins/Provider

Minimal Provider

import { AbstractProvider, provider } from '@kawaikara/site-api';

@provider()
export default class ExampleProvider extends AbstractProvider {
  async load(): Promise<void> {
    await this.context.viewer.loadURL('https://example.com/');
  }
}

Place it in its own directory:

Providers/ExampleVideo/
├── manifest.json
├── locale.json
├── Provider.ts
└── Provider.js

Identity, permissions, and static contributions live in manifest.json. A Provider manifest uses contributes for menu placement, shortcuts, addresses, locale, isolation, and feature switches. The decorator owns Provider settings and site-specific DOM policy as well as executable behavior. Keep translated Provider copy in locale.json and point to it with contributes.locale.resource. The Provider must be listed by the containing Bundle manifest.

Context capabilities

SiteContext exposes bounded application services:

  • viewer: load a remote URL or an application-owned internal view. Each call enforces its corresponding manifest permission.
  • page: the application-owned injection and page-data pipeline, available only with script-injection. It owns lifecycle retries, frame traversal, trusted page input, failure isolation, diagnostics, and teardown.
  • browser: scoped browser identity control, available only with network-interception. Static identities should use manifest metadata.
  • externalBrowser: complete login in a temporary external browser and import cookies into the current Session.
  • actions: create opaque action URLs for injected controls.
  • locale: resolved app, Bundle, and Provider locale values.
  • logger: Provider-scoped diagnostics.

Remote pages never receive the application IPC bridge.

Remote integrations should extend AbstractUrlProvider. It registers the standard document phases before navigation, tolerates only Electron’s expected ERR_ABORTED SPA handoff, and provides beforeLoad(), afterLoad(), and requirePage() without relying on a built-in-only helper.

Lifecycle

Create all event subscriptions before the initial loadURL() call. Add cleanup handles to this.subscriptions; the default unload() implementation disposes them. Treat a Provider instance as single-use.

Kawaikara activates matching Plugins before calling Provider.load(). When the user changes Provider, Plugins deactivate in reverse order and then the Provider unloads.

onSettingsChanged(settings) runs once before load() and again after the user saves Provider settings. Values are stored under providerSettings[providerId], so two Providers may use the same local key without sharing state. Missing keys must always resolve to Provider defaults.

Providers declare their own Preferences UI through @provider({ settings }). Keep translated copy in a locale.json next to Provider.ts and declare that file as contributes.locale.resource in the Provider manifest. The app renders each category inside Preferences > Bundles > the containing Bundle, without adding Provider-specific React code. Supported controls currently are boolean and item-list; list values contain stable { id, label } pairs and are intended for data such as blocked publishers.

{
  "contributes": {
    "locale": {
      "resource": "locale.json",
      "supportedLocales": ["en-US", "ko-KR", "ja-JP"],
      "defaultLocale": "inherit"
    }
  }
}
@provider({
  settings: {
    categories: [{
      id: 'playback',
      settings: [{
        type: 'boolean',
        key: 'short-form-video.auto-advance',
        defaultValue: true,
      }],
    }],
  },
})
export default class ExampleProvider extends AbstractProvider {}

When presentation text is omitted, Kawaikara derives its locale key from the stable contribution id. A directly supplied value such as title: "Playback" always wins and is displayed literally. The inferred keys are:

  • menu.panel.<panel-id>.title
  • shortcut.<action-id>.title and .description
  • settings.<category-id>.title and .description
  • settings.<setting-key>.title, .description, and .emptyText

Required titles without either an explicit value or a matching resource entry make the Bundle fail validation. Optional descriptions and empty text are used only when their keys exist. The locale resource is keyed by locale and then by stable message id:

{
  "en-US": {
    "settings.playback.title": "Playback",
    "settings.short-form-video.auto-advance.title": "Play the next video automatically"
  },
  "ko-KR": {
    "settings.playback.title": "재생",
    "settings.short-form-video.auto-advance.title": "다음 영상 자동 재생"
  }
}

Import the same file through defineProviderLocale() only when executable Provider code also needs a translated runtime string, such as an injected page announcement.

Short-form video capabilities

Use manifest.contributes.shortFormVideo for vertical feeds instead of implementing app keys in each Provider. The standard interface supports previous, next, auto-advance, and publisher quick-ban capabilities. Kawaikara binds the shared Shortcut preferences in normal mode and registers them globally only while its unified PiP is active. The Provider handles navigation through onAction() and resolves the current publisher through getShortFormVideoPublisher().

Provider-only actions that do not fit this interface can be declared under manifest.contributes.shortcut.actions. They appear automatically on the shared Shortcut page and are dispatched to onAction() only while that Provider is active.

Override allowNavigation(url) to reject unexpected main-frame destinations. Override onNewWindow(url) to choose viewer, popup, external, or deny. Use an isolated popup for OAuth flows that require window.opener, postMessage, or response_mode=web_message. The base implementation allows HTTP(S) windows as sandboxed popups because an OAuth flow may begin on the site’s own callback URL. Override it with a narrow policy when the Provider can classify its popup URLs reliably.

Use externalBrowser.login() only for services that reject embedded Electron login. Keep completion URL patterns narrow and never log cookies or callback tokens.

When a remote login button must be redirected to that browser, use createExternalLoginFlow(). It installs the standard idempotent button bridge, owns pending state and cleanup, refreshes after cookie import, and exposes one handleAction() method for the Provider.

Injection

Register repeatable features with context.page.register() before navigation. The default phases are dom-ready and did-finish-load; use frames: 'all' and include frame-ready only when a player lives in a child frame. source may be a function, so current settings and action URLs can be serialized on every pass. Kawaikara isolates failures between registrations and removes every hook when the Provider is unloaded.

this.subscriptions.add(this.context.page!.register({
  id: 'example.player-controls',
  source: () => createPlayerInjection(this.currentSettings),
  phases: ['dom-ready', 'did-finish-load', 'frame-ready'],
  frames: 'all',
}));

Use page.execute(id, source) for an explicit command or typed page-data query, and page.executeInAllFrames() only when its result genuinely spans frames. These calls still pass through the same permission, logging, and frame boundary; Providers must not reach Electron webContents directly. Injection remains idempotent because streaming SPAs can navigate without replacing the document.

Declare script-injection in the Provider manifest and grant it in the top-level Bundle manifest. The Bundle list is the installation-consent boundary; the Provider list determines which capabilities that Provider receives.

Keep injected entry points in a Provider-local Inject/ directory, split by feature rather than accumulating one Provider-sized string. Write the page-world function as TypeScript, keep it self-contained, and serialize it at the final viewer boundary. Imports and module variables are not available after a function is converted to page code.

Request hooks

onBeforeRequest() can cancel or redirect requests. onBeforeSendHeaders() can return replacement headers. The runtime applies the resolved Provider locale to Accept-Language after the Provider header hook.

Declare network-interception when using either hook.

For a static UA workaround, declare contributes.browserIdentity instead of rewriting headers in Provider code. userAgent: "chromium" strips Electron and application product tokens; clientHints: "auto" derives a matching Chrome major version. requestHosts bounds header changes to those hosts and their subdomains. Kawaikara applies the identity to the viewer, popups, and initial requests and restores it automatically.

PiP eligibility

Declare "pictureInPicture": { "enabled": true } under the Provider manifest’s contributes, then keep route-dependent behavior in code:

@provider({
  pictureInPicture: {
    contentOverlaySelectors: ['.service-caption-container'],
  },
})
export default class ExampleProvider extends AbstractProvider {
  allowPictureInPicture(url: string): boolean {
    return new URL(url).pathname.startsWith('/watch/');
  }
}

The manifest contribution controls whether the app exposes the unified PiP action. From that point Kawaikara automatically owns video discovery, frame and SPA reinjection, Return and Play/Pause controls, input routing, cleanup, and a generic DOM-caption selector set. Providers do not inject the PiP overlay.

The route guard prevents home-page previews and unrelated <video> elements from being selected. Add only service-specific caption containers through the decorator’s pictureInPicture.contentOverlaySelectors; common Video.js, Shaka, JW Player, THEOplayer, AMP, and semantic subtitle/caption selectors are supplied by the app. Page control selectors and request policy are code-owned for the same reason: they depend on the site’s live DOM and player implementation.

Internal views

viewer.loadInternalView('video') opens the app-owned Video renderer. Internal views are not arbitrary Bundle Renderer code. Declare internal-view and omit navigation when a Provider does not load remote content.

PluginView panels

Use menu.panels for Provider-owned panels. Plugins can contribute the same shape through @plugin({ panels }). Declare plugin-view at the Provider and Bundle permission boundary. One panel occupies the whole PluginView surface; multiple panels automatically receive a browser-style selector.

{
  "permissions": ["navigation", "plugin-view"],
  "contributes": {
    "menu": {
      "category": "Examples",
      "panels": [
        {
          "id": "help",
          "title": { "en-US": "Help", "ko-KR": "도움말" },
          "content": { "kind": "html", "html": "<main>...</main>" }
        }
      ]
    }
  }
}

Panel ids must be unique only inside the contributing Provider or Plugin. Titles are display copy and may be duplicated. HTML runs in a sandboxed frame without application IPC; internal ids select only Renderer views shipped by Kawaikara.

Browser profiles

A Provider is isolated in its own persistent Session by default. It may declare isolation.defaultBrowserProfile to use a browser profile defined by its Bundle. Users can override the assignment in Preferences. Mark DRM Providers with isolation.drm so shared-state choices can be warned about.

Checklist

  • Provider source is TypeScript and its manifest points to one compiled JavaScript entry.
  • Identity and descriptive text exist only in the Provider manifest.
  • Provider manifest permissions declare runtime capabilities and remain a subset of the Bundle permission grant.
  • Event hooks are registered before initial navigation.
  • Injection is idempotent and cleanup is complete.
  • Login, URL, popup, and PiP guards are narrow.
  • Credentials, cookies, and tokens never enter logs.