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.
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 withscript-injection. It owns lifecycle retries, frame traversal, trusted page input, failure isolation, diagnostics, and teardown.browser: scoped browser identity control, available only withnetwork-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>.titleshortcut.<action-id>.titleand.descriptionsettings.<category-id>.titleand.descriptionsettings.<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.
Navigation and popups
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.