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.
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
- Extract into a random staging directory.
- Reject traversal paths, symbolic links, oversized archives, excessive entry counts, and files outside the staging root.
- Validate the Bundle manifest and every declared Provider/Plugin manifest.
- Verify IDs, SemVer, API versions, relative directories, entry files, permissions, and duplicate contributions.
- Show a trusted-code warning and require explicit user confirmation.
- Atomically move the directory to
KawaiData/Bundles/<bundle-id>. - 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
.kawaifile 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
apiVersionmust 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
providerscontains 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
providerIdsaccurately 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.