Home
GuideAPP Design · Plugins

Bundle Packaging and Distribution

Every installable archive is one Bundle. It must have a root manifest.json and at least one Provider directory. Every Provider and Plugin owns its own directory and manifest.

Plugins/Packaging

Required directory model

Every installable archive is one Bundle. It must have a root manifest.json and at least one Provider directory. Every Provider and Plugin owns its own directory and manifest.

example-bundle/
├── manifest.json
├── Update.js
├── Providers/
│   └── ExampleVideo/
│       ├── manifest.json
│       ├── Provider.js
│       ├── assets/
│       └── Plugins/
│           └── PlayerHelper/
│               ├── manifest.json
│               └── Plugin.js
└── Plugins/
    └── GlobalHelper/
        ├── manifest.json
        └── Plugin.js

The .kawai file is a ZIP container and may contain this layout at its root or inside exactly one enclosing directory.

Bundle manifest

{
  "schemaVersion": 1,
  "id": "example.streaming",
  "name": "Example Streaming",
  "description": "Example Providers for Kawaikara",
  "version": "1.0.0",
  "update": {
    "type": "archive",
    "url": "https://downloads.example.com/example.streaming.kawai"
  },
  "apiVersion": 1,
  "permissions": ["navigation", "script-injection"],
  "providers": ["Providers/ExampleVideo"],
  "plugins": ["Plugins/GlobalHelper"],
  "locale": {
    "supportedLocales": ["en-US", "ko-KR"],
    "defaultLocale": "en-US"
  },
  "browserProfiles": [
    {
      "id": "account",
      "name": "Example account",
      "persistent": true
    }
  ]
}

providers is required and cannot be empty. plugins is optional. All paths are relative directories beneath the Bundle root. update is optional. The archive form above must use a credential-free HTTPS URL that returns a ZIP-compatible .kawai archive with the same Bundle id.

When the newest archive depends on the release channel, platform, or architecture, use a resolver instead:

"update": {
  "type": "resolver",
  "main": "Update.js"
}

Update.js is a trusted CommonJS module at the Bundle root. It exports one function as default, resolveBundleUpdate, or resolveUpdate. The function receives { currentVersion, channel, platform, arch } and returns (or resolves to) a credential-free HTTPS archive URL. Resolver code is validated as part of the Bundle and executes in Main, so it is covered by the same trusted-code boundary as Provider and Plugin entries. The legacy top-level updateUrl string remains readable for existing Bundles, but new Bundles should use update.

Provider manifest

{
  "schemaVersion": 1,
  "id": "example.video",
  "name": "Example Video",
  "version": "1.0.0",
  "apiVersion": 1,
  "main": "Provider.js",
  "permissions": ["navigation"],
  "contributes": {
    "address": { "hosts": ["example.com"] },
    "shortcut": { "defaultKey": "Control+Alt+E" },
    "locale": {
      "supportedLocales": ["en-US", "ko-KR"],
      "defaultLocale": "inherit"
    },
    "isolation": { "drm": true },
    "pictureInPicture": { "enabled": true },
    "menu": {
      "category": "OTT",
      "order": 10,
      "icon": "https://example.com/favicon.ico"
    }
  },
  "plugins": ["Plugins/PlayerHelper"]
}

Write Provider source in TypeScript. main points to the compiled .js file and exports one Provider constructor as the module value, default, provider, or the only function export. The Provider manifest owns identity, permissions, and static contributions. contributes may contain address, menu, shortcut, short-form video, locale, isolation, browser identity, and the PiP enabled switch. The class uses @provider() as its runtime marker and owns settings, site behavior, PiP selectors, and page policy. Keep its translated setting and panel copy in a neighboring locale.json, referenced by contributes.locale.resource, rather than embedding locale maps in the manifest. Bundle-level permissions remain the installation consent boundary, while each Provider permission list must be a subset and determines its runtime API access. Both permissions and contributes.menu are required; use an empty permission array when a Provider needs no capability.

Provider-owned Plugin directories are relative to the Provider directory. Their runtime scope is always that Provider.

Plugin manifest

{
  "schemaVersion": 1,
  "id": "example.global-helper",
  "name": "Example Global Helper",
  "version": "1.0.0",
  "apiVersion": 1,
  "main": "Plugin.js",
  "providerIds": ["example.video"]
}

Plugin source is TypeScript and main points to its compiled JavaScript entry. For a Bundle-level Plugin, omit providerIds to target every Provider or list Provider IDs to restrict activation. A Provider-owned Plugin does not need providerIds; its containing Provider is the scope.

Building a Bundle

Provider and Plugin entries should bundle their runtime dependencies. The .kawai container installer does not run a package manager. Import the public Site API only for Kawaikara capabilities; do not import app Main modules or Electron internals. At activation, Kawaikara creates the reserved node_modules/@kawaikara/site-api bridge inside the installed Bundle so every entry receives the app’s exact Site API instance. Bundles must not use that reserved directory for their own files.

The official packages/builtin-sites package follows exactly this layout under src/Providers and src/Plugins. Its TypeScript composition root uses defineProvider, definePlugin, and defineBundle to represent the same filesystem ownership at build time.

Loader pipeline

  1. Extract into a random staging directory.
  2. Reject traversal paths, symbolic links, oversized archives, excessive entry counts, and files outside the staging root.
  3. Validate the Bundle manifest and every declared Provider/Plugin manifest.
  4. Verify IDs, SemVer, API versions, relative directories, entry files, permissions, and duplicate contributions.
  5. Show a trusted-code warning and require explicit user confirmation.
  6. Atomically move the directory to KawaiData/Bundles/<bundle-id>.
  7. Load compiled JavaScript entries and register the Bundle on the next app start.

One malformed installed Bundle is marked failed in Preferences without stopping other Bundles or application startup.

Bundles with update expose an Update action. Updates pass the same archive and manifest validation, replace the installed directory atomically, and take effect after restart. A built-in Bundle update is stored as a validated on-disk override and takes precedence over the embedded copy on the next start. Only user-installed Bundles expose Remove; removing one deletes its installed directory and already loaded code is unloaded on restart.

Limits and trust boundary

  • Maximum .kawai file size: 32 MB.
  • Maximum expanded size: 96 MB.
  • Maximum entries: 1,024.
  • Symbolic links and paths escaping their owner are rejected.
  • IDs use letters, numbers, ., _, and - and are limited to 128 characters.
  • Versions follow SemVer and apiVersion must match the current Site API.

The loader executes trusted compiled JavaScript in Main. Path and manifest validation is not a JavaScript sandbox. A Bundle can access Node.js during module initialization, so users must install only archives from trusted publishers.

Distribution checklist

  • Bundle providers contains at least one directory.
  • Every Provider and Plugin has TypeScript source, its own manifest.json, and a compiled JavaScript entry.
  • Provider identity and static configuration are declared in its manifest; @provider() only marks the constructor.
  • Each Provider manifest declares its capabilities, and the top-level Bundle manifest grants their union.
  • The top-level Bundle manifest owns its optional archive or resolver update source.
  • Provider-owned Plugins are stored below their owning Provider.
  • Bundle-level Plugin providerIds accurately expresses global or targeted scope.
  • No credentials, tokens, private URLs, or development artifacts are included.
  • All listeners, timers, browser state, and injections are cleaned up.