Koee DocsKoee 文档

WebView Kit Development

A WebView Kit is a web app you host that opens inside Koee. People find it by its @handle, add it to Chats, and open it in the app's Kit container: an isolated WebView with a native title bar, a small JavaScript bridge to selected app capabilities, and its own website data for every account.

You create and manage WebView Kits yourself in the Developer Console. A new Kit is a private draft that only you and the admins you add can open. It becomes available to other people after it passes review.

For Dart modules compiled into the app, see Native Kit Development.

Status

Creating and managing WebView Kits requires developer access granted by an administrator and acceptance of the current Kit Developer Agreement. Developer access does not make a Kit official and does not express a user’s consent.

PlatformSupport
iOSiOS 17 or later
AndroidAndroid System WebView 120 or later, with multi-profile support
macOSmacOS 14 or later
Koee WebNot yet supported

On an older system or WebView the app explains what needs updating; there is no fallback mode.

The identity integration described below is implemented for the first native identity release and requires the matching server configuration and compatible app. It is not a statement that every deployed product/app version has enabled it. Check app.getInfo.capabilities; an older host cannot grant identity silently.

The bridge is at version 1. Methods and events are only ever added, never changed; detect what is available with KitBridge.version and the capabilities returned by app.getInfo.

How it works

  • Your Kit has a fixed entry URL and a list of allowed domains. The container loads the entry URL and keeps the main frame on HTTPS pages of the allowed domains.
  • Pages of allowed domains get window.KitBridge in the main frame. Frames inside the page (iframes) never get the bridge.
  • The Kit ID identifies your Kit everywhere: in the catalog, in network requests, in the User-Agent and in debugging tools. It is not secret.
  • Each Kit has an owner and up to 10 admins. Changes to what people see, or to what the Kit may do, go live only after review. Making things stricter (private, closing a capability, maintenance) needs no review and is saved at once; an open Kit picks it up the next time it is opened or returns to the foreground.

Official identity and first use

Official identity is assigned by the platform only to its own services. A Kit’s badge does not come from its developer account, a linked Bot or a Plaza promotion. Official Kits skip the third-party confirmation; website and device permissions still apply.

Before opening a third-party website for the first time, the app shows a native panel with its published name, handle, introduction, provider and policy links. The website is loaded only after the person selects Agree and open and the server accepts the displayed snapshot. Closing or declining leaves the added Kit in Chats. Opening a policy link is not consent.

The native panel accurately lists the scopes your Kit currently requests:

  • Without identity capabilities it confirms use of the third-party website and does not grant account access. Existing use-only consent is never upgraded silently into identity permission.
  • identity.profile requests profile: a Kit-specific account identifier, display name, avatar and effective app UI language. It enables display profile and signed launch tokens for backend authentication.
  • Optional identity.handle requests handle separately. A public handle can link the person to their public account and across Kits; request it only when needed. It requires identity.profile.

The platform never gives your website the app's session token, raw account ID, email, phone number, contacts or chat content. Opening the website also makes ordinary network requests, which the provider may process under its privacy policy.

Adding profile or handle requires new explicit consent; reducing enabled scopes immediately removes the corresponding fields from subsequent identity requests. Official Kits follow current platform policy instead of recording a fictitious user approval; their identity capabilities and access are still checked.

Consent belongs to the account across devices. People revoke it in the Kit’s About menu or Settings → Authorized applications (a separate Kit group). Removing a Kit revokes consent too; adding it again requires confirmation. Revocation clears local website data before reuse, but cannot delete data the provider already holds or guarantee sign-out from its server.

Provider, policy declaration, policy links and allowed-domain changes require new consent. Declare a new policy_version whenever policy content changes, even if its URL stays the same, and submit the draft for review before publishing. The platform does not automatically detect changes on your policy website. Name, icon and introduction changes do not invalidate all existing consents, but a panel already being submitted must be refreshed if its displayed snapshot changed.

Unpublished team previews are labelled Development preview and use separate consent records. Preview consent never authorizes the published version. Older clients without the required consent/identity protocol receive an update-required response; this does not prevent your public website being visited in a normal browser.

Create a Kit

In the Developer Console, open Kit and choose Create Kit:

FieldRules
NameUp to 20 characters. You can change it later (with review).
handle4–32 lowercase letters, digits or underscores. Shared with users, groups, channels and Bots; permanent once taken.
Kit ID5–100 characters, 1–5 dot-separated segments. Each segment starts with a lowercase letter and uses only a–z and 0–9, up to 30 characters. Permanent.
AgreementAccept the current Kit Developer Agreement.

About the Kit ID:

  • It works like an app's bundle ID or package name. A reverse domain such as com.example.shop is recommended but not required, and it does not have to be a domain you own. It is not checked against your web domains.
  • No segment can be koee or another reserved platform name; the first segment cannot be test, example or localhost; and a few whole IDs (such as admin or agreement) are reserved.
  • Kit IDs and handles are never released, even after a Kit is retired.

The new Kit is private, has no capabilities and has debugging off. The Overview page lists what is still required before you can submit it.

Kit settings

Settings marked Needs review are saved to the Kit's draft; they reach users only after the draft passes review. Settings marked Takes effect now need no review and are saved at once. A Kit that is already open picks them up the next time it is opened or returns to the foreground; a page that stays open in the foreground keeps its current settings until then. Identity reads additionally recheck current server authority on every request, including disabled capabilities and revocation.

PageContentsApplies
Store listingName, short description (up to 80 characters), category, iconNeeds review
Provider & policiesProvider name (up to 40 characters), contact (email or HTTPS link, up to 200 characters), privacy policy (HTTPS, required), policy version declaration (required, up to 100 characters), terms of use (HTTPS, optional); links up to 2048 charactersNeeds review
Web settingsEntry URL, allowed domains, title bar, orientation, loading background, pull to refreshNeeds review
CapabilitiesRequest capabilities; close or suspend approved onesRequests need review; closing takes effect now
Access & releaseRequest public access; make private; who can open a private Kit; maintenance; WebView debuggingPublic needs review; the rest takes effect now
TeamThe owner adds and removes adminsTakes effect now
ReviewSubmit, withdraw, history—

Two people editing at once cannot overwrite each other: a save based on an older version is refused, and the console asks you to reload.

Web settings

Entry URL and allowed domains

  • Allowed domains: up to 10 exact HTTPS origins (each up to 300 characters), such as https://app.example.com, without paths, wildcards or IP addresses. Koee's own service hosts cannot be used.
  • Entry URL: an HTTPS URL on one of the allowed domains, up to 2048 characters. It is loaded every time the Kit opens.
  • Changing either one is a draft change and needs review.
  • Use HTTPS everywhere. There is no HTTP, no local file access and no mixed content. For local development, expose your machine through an HTTPS tunnel (for example Cloudflare Tunnel or ngrok) and use that as a development Kit's domain.
  • Links and redirects between pages of allowed domains stay in the Kit.
  • Other HTTPS links, target="_blank" and window.open go through the app's external-link handling (a safety prompt or the system browser). Other schemes are blocked.
  • On Android, a form POST that leaves the allowed domains stops the Kit and shows a "blocked" page; the form data has already been sent by then. Keep forms on your own domains.
  • The system back button or gesture goes back in the page history first, then closes the Kit. The title bar has no back button of its own: it has close (which leaves the Kit at once) and the More menu, so provide in-page navigation where your app needs it.
  • alert, confirm and prompt are shown as app dialogs labelled with your Kit.
  • Camera, microphone and location permission requests from the page are refused. Use bridge capabilities instead (for example scan.qrCode).
  • Offline, DNS failures and HTTP 5xx responses show an app error page with a retry. Certificate errors show an error page without a retry button, and the certificate check cannot be bypassed (refreshing from the More menu checks it again).

Display

SettingValues
Title barApp title bar (native, with close and the More menu), or Immersive (your page fills the screen; floating buttons stay available)
TitleThe page's <title>, or always the Kit name. ui.setTitle changes it at runtime.
OrientationPortrait, landscape, or follow the device (iOS and Android)
Loading backgroundOptional #RRGGBB colors for light and dark mode, shown while the page loads
Pull to refreshOn or off (iOS and Android)

In immersive mode, lay out around safeArea from app.getInfo.

The More menu always offers refresh, copy link, report and about (name, provider and privacy policy).

JavaScript bridge

Getting the bridge

The container injects window.KitBridge into the main frame of allowed-domain pages when the document starts. When a page may load before the bridge is ready, wait for the kitbridgeready event:

function kitBridge() {
  if (window.KitBridge) return Promise.resolve(window.KitBridge);
  return new Promise((resolve) =>
    window.addEventListener('kitbridgeready', () => resolve(window.KitBridge), { once: true }));
}

const bridge = await kitBridge();
const info = await bridge.call('app.getInfo');

Outside the app (a normal browser), window.KitBridge never appears; build your page so it still works, or show a message.

interface KitBridge {
  readonly version: 1;
  call<T>(method: string, params?: object): Promise<T>;
  on(event: string, handler: (data: unknown) => void): () => void; // returns an unsubscribe function
}

A failed call rejects with { code, message }. Each method can be called at most 10 times per second.

A call message longer than 65,536 characters (UTF-16 code units, after JSON encoding) is dropped without an answer, so its promise never settles. Keep parameters small, and add your own timeout where a call might be large.

Methods

MethodParametersResultCapability
app.getInfo—{ product, platform, bridgeVersion, appBuild, kitId, locale, theme, safeArea, capabilities }—
ui.setTitle{ title }, up to 40 characters—— (ignored when the title is fixed to the Kit name)
ui.close———
ui.openExternal{ url }, HTTPS, up to 2048 characters, no user name or password——
share.system{ text?, url? }, at least one; url is HTTPS on an allowed domain, up to 2048 characters, no user name or password{ completed }share.system
scan.qrCode—{ text }: the scanned text with leading and trailing whitespace removed (never empty)scan.qr
user.getProfile—{ sub, name, avatar, locale, handle }identity.profile; handle additionally requires identity.handle and consent
auth.getLaunchToken{ nonce?: string }, 1–128 characters when present{ token, expiresAt }identity.profile
  • platform is ios, android or macos; theme is the system's light or dark mode; locale is the app's language when the page asks (changing the language inside the app does not send locale.changed yet); safeArea is { top, right, bottom, left } in logical pixels (all 0 with the app title bar).
  • capabilities lists the capabilities this Kit may use right now, in this app version.

Events

EventDataWhen
app.pause—The app goes to the background
app.resume—The app is back in the foreground and the Kit is still available
theme.changed{ theme }The system switches between light and dark mode
locale.changed{ locale }The app UI language changes
auth.changed{ reason: "revoked" or "account_switched" }The host invalidates the current identity; clear local state and end your own session

When the app returns to the foreground, the host covers the page and blocks interaction and bridge calls while it checks current access, use consent, capabilities and debugging. An unchanged authorization keeps the existing page and receives app.resume; a changed boundary restarts it, clearing old website data before reuse. If consent is needed, native confirmation appears first. Identity replies from an old document, account or consent boundary are discarded. A refused or failed check removes the page. This is host authorization checking, not a network-freeze guarantee for an already consented website's background requests. Before the first consent, the website is never constructed or loaded.

Error codes

CodeMeaning
unsupported_methodThe method does not exist in this app version
capability_deniedThe Kit has not been approved for this capability, or it is suspended
invalid_paramsParameters are missing or invalid
user_cancelledThe person cancelled (for example closed the scanner)
rate_limitedToo many calls
unavailableThe app could not complete the call right now
consent_requiredCurrent consent is absent or changed; return to native confirmation
identity_unavailableThe product identity service is not configured or cannot safely issue identity

Capabilities

CapabilityEnablesStatus
share.systemshare.systemAvailable
scan.qrscan.qrCodeAvailable
identity.profileDisplay profile and signed launch tokenNative identity release; requires approval and user consent
identity.handleOptional public handle in profile and signed claimsNative identity release; additionally requires identity.profile
bot.chatOpening the Kit's companion BotComing soon
chat.shareSharing to chatsComing soon

Request capabilities on the Capabilities page; for the published Kit they are granted when the review is approved. Before the first approval, the owner and admins can already test the available capabilities the draft requests. Ask only for what your Kit uses.

You can close or suspend an approved capability at any time without review. It is saved at once and reaches open Kits when they are next opened or return to the foreground; it also removes the request from your draft, so turning it back on needs a new review. For profile/token reads the server enforces closure or suspension immediately on the next request, without waiting for foreground resume.

Icon

  • Upload a PNG, exactly 512 × 512 pixels, up to 1 MB. The console can crop and export another image to the right size in your browser.
  • Do not round the corners: the app applies its own mask. An opaque background is recommended.
  • Without an uploaded icon, the Kit uses a default icon: one of a few symbols on a background color you choose. The default icon is also shown whenever the uploaded one cannot be displayed.
  • Icons you stop using are kept for 90 days for the review history. Each Kit can keep up to 50 MB of icons; delete old ones in the icon history to free space.

Website data and sign-in

  • Cookies, localStorage, IndexedDB and caches are kept separately for every account and Kit. Another account on the same device, another Kit and the app's other web views never see your Kit's data.
  • Each account has its own storage, so one account's site login never carries over to another account. Switching accounts shows the other account's own data (signed in only if that account signed in before), and switching back finds the first account's data as it was.
  • Signing out of the app or removing the Kit starts deleting that account's data for the Kit. Deletion is best effort (it is retried if the storage is in use) and does not sign the person out on your server; end server sessions yourself where that matters.
  • A draft and published Kit have the same Kit ID, but preview and published consent are separate boundaries. Transitioning between them clears website data before reuse; preview consent never grants published access. Use a separate development Kit for independent account IDs, keys and backend environments.
  • Use the signed launch-token flow below to sign people into your own backend. Display profile alone cannot authenticate a backend request.

Identity: display and backend sign-in

Display profile

interface KitProfile {
  sub: string;       // stable pseudonymous ID for this product and Kit
  name: string;
  avatar: string;    // host-supplied image data URL, including a default avatar
  locale: string;    // effective app UI language, never a timezone
  handle: string | null;
}
interface LaunchToken {
  token: string;
  expiresAt: number; // epoch milliseconds, exactly JWT exp * 1000
}
const info = await bridge.call('app.getInfo');
if (info.capabilities.includes('identity.profile')) {
  const profile = await bridge.call('user.getProfile');
  document.querySelector('#name').textContent = profile.name;
  document.querySelector('#avatar').src = profile.avatar;
}

Every profile call requests current authority from the server. It has no stale profile fallback. handle is null unless enabled and authorized, and may still be null if the person has no handle. Avatar bytes come from the person's own host-authorized avatar or a generated host default, not a public media URL or an app session token. Allow data: in your image Content Security Policy. Do not assume a specific image encoding; consume the data URL as an image.

sub is stable for this product and Kit, derived by a keyed one-way HMAC. It is not the product's internal account ID and differs between Kits and products. Deleted accounts that later register again receive a new identity. A static page can display profile without a backend. A backend must never trust a profile object posted by a browser; anyone can forge it.

Authenticated launch token

For backend authentication, issue a fresh unpredictable nonce bound to a secure pre-session cookie, request a launch token through the bridge, and exchange it with your own backend. Do not put tokens or nonces in URLs, logs, analytics or persistent browser storage. The app's session credentials never enter this flow.

// Your endpoints must enforce HTTPS, same-origin/CSRF checks, rate limits,
// no-store responses and a fresh secure pre-session cookie. See example README.
const { nonce } = await fetch('/session/kit/nonce', {
  method: 'POST', credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
  body: '{}',
}).then((response) => {
  if (!response.ok) throw new Error('Cannot start sign-in');
  return response.json();
});
const { token } = await bridge.call('auth.getLaunchToken', { nonce });
const response = await fetch('/session/kit/exchange', {
  method: 'POST', credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
  body: JSON.stringify({ token }),
});
if (!response.ok) throw new Error('Sign-in failed');

The host supplies the main-frame origin observed by the native WebView. The page cannot override it. Every token request rechecks current account, Kit access, publication/preview channel, capabilities, consent versions and revocation. The server signs only after revalidating authority; the host suppresses stale delivery. Tokens are limited to 30 per rolling minute and 500 per rolling 24 hours per user and Kit, in addition to bridge limits. Do not request one per animation frame or cache it as a long-lived session.

The signed header is { alg: "ES256", typ: "kit-launch+jwt", kid: "…" }.

ClaimContract
issExactly https://ims.koee.app for this product
audExactly your Kit ID (a single string)
subThe Kit-specific identifier; use (iss, sub) as your account key
iat, expInteger JWT seconds; exp = iat + 300
jtiUnique token ID
originNative-observed HTTPS origin, checked against current Kit configuration
nonceThe nonce supplied to the bridge, if present
scopeprofile or profile handle
name, localeCurrent display name and app UI language
handlePresent only with handle scope; nullable when the person has none

Tokens deliberately exclude avatar bytes. Use user.getProfile for display. The bridge expiry is milliseconds: expiresAt = exp * 1000. JWT timestamps are seconds; do not mix these units or infer timezones from locale.

Public keys and backend verification

The public endpoint is https://ims.koee.app/kit-keys/<kit_id>/jwks.json. It needs no login or App build/platform headers, is exempt from client minimum-version checks, permits CORS and returns Cache-Control: public, max-age=300. Keys are unique per Kit. Only the platform controls signing keys; developers receive public keys/JWKS information, never a private key or shared signing secret.

Your backend must verify all of the following before creating its own session:

  1. ES256 signature, exact typ of kit-launch+jwt, and a known kid selected only from your configured Kit's JWKS. Never follow JWT jku, x5u, embedded keys, or a client-supplied issuer/JWKS URL. Cache at most 300 seconds; an unknown kid may refresh once, then fails closed. Network/key errors must reject.
  2. Exact product iss, exact Kit aud and an origin in your own configured HTTPS origin set. Do not accept another Kit's or another product's token.
  3. Integer iat and exp, positive lifetime no greater than 300 seconds, and strict expiry: nowMs >= exp * 1000 rejects with no leeway. Only iat may be at most 60 seconds ahead of your clock. Do not use a library-wide 60-second clock tolerance; that also extends expiry. Recheck time after async key fetch.
  4. A matching, fresh nonce bound to this pre-session, consumed atomically once. Alternatively build an atomic shared replay store for (iss, aud, jti) until at least expiry, together with secure request binding. Merely comparing a nonce, storing it per process, or doing separate read/delete is not enough.
  5. Only after all checks succeed, create a fresh your-own-backend session keyed by (iss, sub). Name, handle and avatar are display attributes, not authentication keys. Enforce your own business permissions separately.

Download the complete runnable examples, dependency manifests and offline tests:

Both examples use a durable SQLite atomic nonce store shared by backend processes. For multiple hosts/serverless instances use one shared transactional store; local per-instance databases are not sufficient. The README describes the required pre-session cookie, CSRF and HTTP integration. They are backend modules and tests, not a ready-made login server.

Revocation and session lifetime

Removing/revoking a Kit, losing access, account deletion or disabling identity stops new identity reads for that authorization. Unpublishing ends published user access; authorized managers may separately consent to an unpublished development preview with isolated grants. Existing signed tokens may still verify offline for their remaining lifetime (at most 300 seconds). Platform revocation cannot erase data already held by your backend or automatically end your sessions. Ask for a fresh launch token before sensitive operations and enforce your own session lifetime/logout policy.

On auth.changed, clear the displayed profile and your local identity state and call your own logout route. The host also closes or resets the affected document and website data. Treat the event as best effort: your backend cannot depend on a browser receiving it as a revocation guarantee.

bridge.on('auth.changed', () => {
  document.querySelector('#name').textContent = '';
  document.querySelector('#avatar').removeAttribute('src');
  // End the site's own session through its normal CSRF-protected logout flow.
  void logoutOwnSession();
});

Normal platform key rotation publishes the previous key for 24 hours alongside the new one. Revoked keys disappear from fresh JWKS immediately, but an existing key cache may still accept them for up to five minutes. On a revocation notice, refresh all backend key caches immediately. Neither rotation nor the overlap extends a token's expiry. Kit identity is distinct from the external Account Sign-in protocol.

Test and debug

  • Before the first approval, only the Kit's owner and admins can open it: search for its @handle in the app and open it. This uses the draft's settings, including the available capabilities it requests. People you add under Access & release can open it only after it is published.
  • A draft missing its entry URL or allowed domains shows "This Kit hasn't finished setup yet".
  • WebView debugging (on Access & release) needs no review and applies only to the owner and admins; it is always off for everyone else. An open Kit picks up a change when it is next opened or returns to the foreground. With it on, connect Safari Web Inspector (iOS, macOS) or chrome://inspect (Android). On Android, WebView debugging applies to the whole app process on your device while the Kit is on screen, so other web content in the app can be inspected too.
  • With debugging on, the More menu has Developer tools: Kit ID and container version, entry URL, enabled capabilities, reload, reset website data, and the last 100 bridge calls (method, result code and duration; no parameters or results).
  • The User-Agent ends with KoeeKit/1 (<Kit ID>). Use it for statistics or troubleshooting only; never for security decisions.
  • Recommended setup: a development Kit (for example @shop_dev) that stays private, points at your test domain and has debugging on, and a separate production Kit (@shop). Handles are permanent, so pick the names deliberately.

Submit for review

  1. Complete every item on the Overview checklist: name, description, provider name, contact, privacy policy, entry URL and allowed domains.
  2. On Review, check the list of changes against the live version, then choose Submit for review.
  3. While it is in review you can keep editing; later edits are not part of that submission. Submitting again replaces the version in review, and you can withdraw it at any time.
  4. Approval publishes the submitted version. A rejection comes with a reason, shown on the Overview page.

Review checks the settings you submit: addresses, capabilities and listing. You remain responsible for your web content after approval; reported problems can lead to maintenance, suspension of capabilities or retirement.

If the published content changes while a submission waits (you make the Kit private, close or suspend a capability, or the operators edit it), the submission expires and you need to submit again. Maintenance, debugging, team and access-list changes do not affect it. If the operators disable or retire the Kit, the submission is withdrawn.

Visibility and discovery

  • Private (the default): only the users, groups and channels on your access list can open the Kit; for a group or channel that means its current members. Add them by handle: users must be people (not Bots), and you must be an owner or admin of a group or channel you add. A Kit can list up to 1000 users and 50 groups and channels.
  • Public: anyone can find the Kit by its full @handle. Request it on Access & release; it applies after review. Making a Kit private again needs no review: people outside the access list can no longer open it, and a Kit they already have open closes when it next returns to the foreground.
  • Approval does not put a Kit in Plaza. Plaza is curated by the operators, and only public, published Kits are eligible.
  • Maintenance stops the Kit from opening for a while; people who added it see "under maintenance". The operators can also disable or retire a Kit. Retirement is permanent.

Limits

ActionLimit
Active Kits you own10
Kits you can ever create30
Kits in review at the same time5
Creating Kits3 per 24 hours
Submitting and withdrawing10 each per 24 hours
Checking a Kit ID or handle30 per minute
Icon uploads50 per 24 hours, 2 at a time, 50 MB stored per Kit
Admins per Kit10

Admins and icon storage are counted per Kit; the other limits per account. The number of creations, submissions, withdrawals, ID and handle checks, and icon uploads also has a per-network-address limit of three times the account limit, so a team sharing one connection shares that allowance; the two uploads at a time are per account only. The 24-hour limits are rolling windows, not calendar days; checking Kit IDs and handles share one allowance; and attempts that fail can still count.

Security and privacy checklist

  • Put no secrets in your pages or JavaScript: anyone can inspect them. Validate everything on your server.
  • user.getProfile is unsigned display data and cannot authenticate a backend request. Use auth.getLaunchToken and verify its signature, claims and single-use nonce on your server before creating a session.
  • Keep the allowed domains under your control. Removing a domain is a draft change that needs review, so if you lose control of one, put the Kit into maintenance right away, contact support at support@koee.app, and submit the change.
  • Keep your privacy policy link working and accurate, and collect only what your Kit needs.
  • Do not try to escape the Kit: no navigation tricks, hidden frames that impersonate the app, or requests for permissions the container refuses.

Coming soon

  • A companion Bot (bot.chat) and sharing to chats (chat.share).
  • Support in Koee Web.
  • A browser shim of KitBridge for developing without the app.

WebView Kit 开发

WebView Kit 是由你托管、在 Koee 内打开的网页应用。用户通过 @handle 找到它,添加到聊天列表,并在 App 的 Kit 容器中打开。容器为隔离的 WebView,提供原生标题栏、连接特定 App 能力的小型 JavaScript 桥接,以及按账号隔离的网站数据。

你可在开发者控制台自行创建和管理 WebView Kit。新 Kit 是私有草稿,仅你和添加的管理员可打开;通过审核后才能供其他人使用。

编译进 App 的 Dart 模块请见原生 Kit 开发。

状态

创建和管理 WebView Kit 需要管理员授予开发者权限,并接受当前 Kit 开发者协议。开发者权限不使 Kit 成为官方 Kit,也不代表用户同意。

平台支持要求
iOSiOS 17 或更高
AndroidAndroid System WebView 120 或更高,支持多用户配置
macOSmacOS 14 或更高
Koee Web尚不支持

系统或 WebView 过旧时,App 会说明需要更新的内容,不提供回退模式。

下文身份接入已为首个原生身份版本实现,要求匹配的服务端配置与兼容 App;不代表所有已部署产品或 App 版本均已启用。请检查 app.getInfo.capabilities,旧宿主不能静默授予身份权限。

桥接版本为 1。方法和事件只新增、不改变;通过 KitBridge.version 和 app.getInfo 返回的 capabilities 检测可用能力。

工作原理

  • Kit 有固定入口 URL和允许域名列表。容器加载入口 URL,并将主框架限制在允许域名的 HTTPS 页面内。
  • 允许域名页面的主框架获得 window.KitBridge,页面内 iframe 永不获得桥接。
  • Kit ID 在目录、网络请求、User-Agent 和调试工具中标识 Kit,不是秘密。
  • 每个 Kit 有一个所有者和最多 10 个管理员。用户可见内容或能力变化需审核后上线;收紧限制(私有、关闭能力、维护)无需审核,立即保存。已打开 Kit 在下次打开或恢复前台时获取这些修改。

官方身份与首次使用

平台仅向自己的服务授予官方身份。Kit 标记不来自开发者账号、关联 Bot 或广场推荐。官方 Kit 跳过第三方确认,但网站和设备权限仍适用。

首次打开第三方网站前,App 显示原生面板,包含已发布名称、handle、介绍、提供者及政策链接。仅当用户选择同意并打开且服务端接受所显示快照后才加载网站。关闭或拒绝会保留聊天列表中的 Kit。打开政策链接不等于同意。

原生面板准确列出 Kit 当前申请的权限范围:

  • 不含身份能力时,仅确认使用第三方网站,不授予账号访问。已有仅使用同意不会静默升级为身份授权。
  • identity.profile 申请资料:Kit 专属账号标识、显示名、头像和有效 App 界面语言,用于资料展示及后端认证的签名启动 token。
  • 可选 identity.handle 单独申请 handle。公开 handle 可关联公开账号及跨 Kit 身份,仅在需要时申请;它依赖 identity.profile。

平台不会向网站提供 App 会话 token、原始账号 ID、邮箱、手机号、联系人或聊天内容。打开网站也会产生普通网络请求,提供者可按其隐私政策处理。

增加 profile 或 handle 需新的明确同意;减少已启用范围会立即从后续身份请求移除相应字段。官方 Kit 按当前平台策略执行,不记录虚构用户同意;其身份能力和访问仍须校验。

同意记录属于账号并跨设备生效。用户可在 Kit 的关于菜单,或设置 → 已授权应用中的独立 Kit 分组撤销。移除 Kit 也会撤销同意,重新添加需再次确认。撤销在复用前清除本地网站数据,但不能删除提供者已持有的数据,也不保证其服务端退出。

提供者、政策声明、政策链接和允许域名变化需重新同意。政策内容变更时,即使 URL 不变,也须声明新 policy_version,提交草稿审核后发布。平台不会自动检测政策网站变更。名称、图标和介绍变化不使全部已有同意失效;但正在提交的确认面板若显示快照已变,应刷新后再提交。

未发布的团队预览标为开发预览,使用独立同意记录。预览同意永不授权已发布版本。缺少所需同意/身份协议的旧客户端收到需要更新的响应;这不妨碍用户在普通浏览器访问公开网站。

创建 Kit

在开发者控制台打开 Kit,选择创建 Kit:

字段规则
名称最多 20 字符;后续可修改,需审核。
handle4–32 个小写字母、数字或下划线;与用户、群组、频道和 Bot 共享命名空间,占用后永久保留。
Kit ID5–100 字符,1–5 个点分隔段。各段以小写字母开头,仅用 a–z 和 0–9,每段最多 30 字符,永久保留。
协议接受当前 Kit 开发者协议。

关于 Kit ID:

  • 类似 App 的 Bundle ID 或包名。推荐但不强制使用 com.example.shop 这类反向域名;不要求拥有对应域名,也不会与网页域名核对。
  • 各段不能是 koee 或其他平台保留名;首段不能是 test、example 或 localhost;另有少量完整 ID(如 admin、agreement)被保留。
  • Kit ID 和 handle 永不释放,即使 Kit 永久下架。

新 Kit 默认为私有、无能力、关闭调试。概览页列出提交前仍需完成的项目。

Kit 设置

标为需审核的设置保存在草稿中,审核通过后才对用户生效。标为立即生效的设置无需审核,直接保存。已打开 Kit 在下次打开或恢复前台时获取修改;一直停留前台的页面在此之前保持当前设置。身份读取还会在每次请求重新检查当前服务端授权,包括能力停用与撤销。

页面内容生效方式
展示资料名称、简短描述(最多 80 字符)、分类、图标需审核
提供者与政策提供者名称(最多 40 字符)、联系信息(邮箱或 HTTPS 链接,最多 200 字符)、隐私政策(HTTPS,必填)、政策版本声明(必填,最多 100 字符)、使用条款(HTTPS,可选);链接最多 2048 字符需审核
网页设置入口 URL、允许域名、标题栏、方向、加载背景、下拉刷新需审核
能力申请能力,关闭或暂停已批准能力申请需审核;关闭立即生效
访问与发布申请公开、设为私有、私有访问人员、维护、WebView 调试公开需审核,其余立即生效
团队所有者添加和移除管理员立即生效
审核提交、撤回、历史—

多人同时编辑不会互相覆盖:基于旧版本的保存会被拒绝,控制台要求重新加载。

网页设置

入口 URL 与允许域名

  • 允许域名:最多 10 个精确 HTTPS Origin(每个最多 300 字符),如 https://app.example.com,不含路径、通配符或 IP。不得使用 Koee 自有服务主机。
  • 入口 URL:属于允许域名的 HTTPS URL,最多 2048 字符,每次打开 Kit 时加载。
  • 修改任一项均保存为草稿,需审核。
  • 全部使用 HTTPS,不允许 HTTP、本地文件或混合内容。本地开发可通过 HTTPS tunnel(如 Cloudflare Tunnel 或 ngrok)暴露开发机,并作为开发 Kit 域名。

Kit 内导航

  • 允许域名之间的页面链接与跳转保留在 Kit 内。
  • 其他 HTTPS 链接、target="_blank" 和 window.open 交给 App 外部链接处理(安全提示或系统浏览器);其他 scheme 被阻止。
  • Android 上离开允许域名的表单 POST 会停止 Kit 并显示阻止页,但此时表单数据已发送。表单应留在自己的域名。
  • 系统返回按钮或手势先返回网页历史,再关闭 Kit。标题栏没有独立返回按钮,仅有立即离开 Kit 的关闭和更多菜单;网页需要时应自行提供页面内导航。
  • alert、confirm 和 prompt 以带 Kit 标识的 App 对话框显示。
  • 页面请求相机、麦克风和定位权限会被拒绝,应使用桥接能力(如 scan.qrCode)。
  • 离线、DNS 失败和 HTTP 5xx 显示可重试的 App 错误页;证书错误无重试按钮,不可绕过校验(更多菜单刷新会再次检查)。

显示

设置可选值
标题栏App 标题栏(原生,含关闭与更多菜单)或沉浸式(网页全屏,保留浮动按钮)
标题使用页面 <title> 或始终显示 Kit 名称;ui.setTitle 可在运行时修改。
方向竖屏、横屏或跟随设备(iOS、Android)
加载背景可选的浅色/深色 #RRGGBB,页面加载时显示
下拉刷新开启或关闭(iOS、Android)

沉浸模式按 app.getInfo 的 safeArea 布局。

更多菜单始终提供刷新、复制链接、举报及关于(名称、提供者和隐私政策)。

JavaScript 桥接

获取桥接

容器在文档开始时向允许域名页面的主框架注入 window.KitBridge。页面可能先于桥接就绪时,等待 kitbridgeready 事件:

function kitBridge() {
  if (window.KitBridge) return Promise.resolve(window.KitBridge);
  return new Promise((resolve) =>
    window.addEventListener('kitbridgeready', () => resolve(window.KitBridge), { once: true }));
}

const bridge = await kitBridge();
const info = await bridge.call('app.getInfo');

普通浏览器等 App 外环境不会出现 window.KitBridge;网页应仍可运行,或显示说明。

interface KitBridge {
  readonly version: 1;
  call<T>(method: string, params?: object): Promise<T>;
  on(event: string, handler: (data: unknown) => void): () => void; // returns an unsubscribe function
}

失败调用以 { code, message } 拒绝。每个方法每秒最多调用 10 次。

调用消息 JSON 编码后超过 65,536 个字符(UTF-16 code unit)时会被丢弃,不返回结果,promise 永不结束。参数应保持小型;可能较大的调用自行设置超时。

方法

方法参数结果能力
app.getInfo—{ product, platform, bridgeVersion, appBuild, kitId, locale, theme, safeArea, capabilities }—
ui.setTitle{ title },最多 40 字符——(标题固定为 Kit 名称时忽略)
ui.close———
ui.openExternal{ url },HTTPS,最多 2048 字符,不含用户名或密码——
share.system{ text?, url? },至少一项;url 属于允许域名的 HTTPS,最多 2048 字符,无用户名或密码{ completed }share.system
scan.qrCode—{ text }:移除首尾空白后的扫码文字,永不为空scan.qr
user.getProfile—{ sub, name, avatar, locale, handle }identity.profile;handle 另需 identity.handle 和同意
auth.getLaunchToken{ nonce?: string },提供时为 1–128 字符{ token, expiresAt }identity.profile
  • platform 为 ios、android 或 macos;theme 为系统 light/dark;locale 为调用时 App 语言(在 App 内切换语言目前还不发送 locale.changed);safeArea 为逻辑像素 { top, right, bottom, left },使用 App 标题栏时均为 0。
  • capabilities 列出此 App 版本中 Kit 当前可用能力。

事件

事件数据触发时机
app.pause—App 进入后台
app.resume—App 恢复前台且 Kit 仍可用
theme.changed{ theme }系统切换浅色/深色模式
locale.changed{ locale }App 界面语言变更
auth.changed{ reason: "revoked" or "account_switched" }宿主使当前身份失效;清除本地状态并结束本站会话

App 恢复前台时,宿主遮盖网页,阻止交互及桥接调用,同时检查当前访问、使用同意、能力和调试配置。授权未变化时保留页面并发送 app.resume;边界变化时先清除旧网站数据,再重启页面。需同意时先展示原生确认。旧文档、账号或同意边界的身份响应被丢弃;检查拒绝或失败会移除页面。这是宿主授权检查,不保证冻结已获同意网站的后台网络请求。首次同意前,网站不会被构建或加载。

错误码

代码含义
unsupported_method当前 App 版本不存在该方法
capability_deniedKit 未获批该能力或能力已暂停
invalid_params参数缺失或无效
user_cancelled用户取消,例如关闭扫描器
rate_limited调用过多
unavailableApp 当前无法完成调用
consent_required当前同意缺失或已变化,返回原生确认
identity_unavailable产品身份服务未配置,或无法安全签发身份

能力

能力提供功能状态
share.systemshare.system可用
scan.qrscan.qrCode可用
identity.profile展示资料与签名启动 token原生身份版本,需批准和用户同意
identity.handle资料与签名 claims 中可选公开 handle原生身份版本,另需 identity.profile
bot.chat打开 Kit 配套 Bot计划中
chat.share分享到聊天计划中

在能力页申请;已发布 Kit 的能力在审核通过后授予。首次批准前,所有者和管理员可测试草稿申请的可用能力。仅申请 Kit 实际使用的能力。

已批准能力可随时关闭或暂停,无需审核,立即保存;已打开 Kit 下次打开或恢复前台获取变更。该能力也从草稿移除,再开启需新审核。资料/token 读取在下次请求立即执行服务端关闭或暂停规则,不等待恢复前台。

图标

  • 上传恰好 512 × 512 像素、最大 1 MB 的 PNG。控制台可在浏览器裁剪并导出其他图片至正确尺寸。
  • 不自行圆角,App 会应用自己的遮罩,建议不透明背景。
  • 未上传图标时使用默认图标(从少量符号和自选背景色中组合);上传图标无法显示时也使用默认图标。
  • 停用图标在审核历史中保留 90 天。每个 Kit 最多存储 50 MB 图标,可在历史中删除旧图标释放空间。

网站数据与登录

  • Cookie、localStorage、IndexedDB 和缓存按账号与 Kit分别保存。同设备的另一账号、其他 Kit 和 App 其他 WebView 均无法访问。
  • 每个账号独立存储,网站登录不会跨账号继承。切换账号显示其自己的数据(仅此前登录过才有登录状态);切回来保留原账号数据。
  • 退出 App 或移除 Kit 会开始删除该账号的 Kit 数据。删除尽力完成,存储占用时重试;不会退出你的服务端,必要时自行结束服务端会话。
  • 草稿和已发布 Kit 共用 Kit ID,但预览和发布同意为独立边界。转换时先清除网站数据再复用,预览同意不授予已发布权限。独立账号 ID、密钥和后端环境应使用单独开发 Kit。
  • 通过下文签名启动 token 登录自己的后端,展示资料本身不能认证后端请求。

身份:资料展示与后端登录

展示资料

interface KitProfile {
  sub: string;       // stable pseudonymous ID for this product and Kit
  name: string;
  avatar: string;    // host-supplied image data URL, including a default avatar
  locale: string;    // effective app UI language, never a timezone
  handle: string | null;
}
interface LaunchToken {
  token: string;
  expiresAt: number; // epoch milliseconds, exactly JWT exp * 1000
}
const info = await bridge.call('app.getInfo');
if (info.capabilities.includes('identity.profile')) {
  const profile = await bridge.call('user.getProfile');
  document.querySelector('#name').textContent = profile.name;
  document.querySelector('#avatar').src = profile.avatar;
}

每次资料调用都从服务端获取当前授权,不回退到过期资料。handle 未启用或未授权时为 null,用户没有 handle 时也可为空。头像来自用户自身经宿主授权的头像或宿主生成的默认头像,不是公开媒体 URL,也不是 App 会话 token。图片 Content Security Policy 应允许 data:;不要假定特定图片编码,将 data URL 作为图片使用。

sub 通过带密钥的单向 HMAC 派生,在该产品和 Kit 内稳定。它不是产品内部账号 ID,不同 Kit/产品之间不同。账号删除后重新注册会获得新身份。静态页面无需后端即可展示资料;后端绝不能信任浏览器提交的资料对象,任何人都可伪造。

已认证启动 token

后端认证时,先签发新的不可预测 nonce,绑定安全的预会话 Cookie,通过桥接请求启动 token,再与自己的后端兑换。token 和 nonce 不进入 URL、日志、分析或持久浏览器存储。App 会话凭据不参与该流程。

// Your endpoints must enforce HTTPS, same-origin/CSRF checks, rate limits,
// no-store responses and a fresh secure pre-session cookie. See example README.
const { nonce } = await fetch('/session/kit/nonce', {
  method: 'POST', credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
  body: '{}',
}).then((response) => {
  if (!response.ok) throw new Error('Cannot start sign-in');
  return response.json();
});
const { token } = await bridge.call('auth.getLaunchToken', { nonce });
const response = await fetch('/session/kit/exchange', {
  method: 'POST', credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
  body: JSON.stringify({ token }),
});
if (!response.ok) throw new Error('Sign-in failed');

宿主提供原生 WebView 观察到的主框架 Origin,网页不能覆盖。每次 token 请求重新检查账号、Kit 访问、发布/预览渠道、能力、同意版本和撤销。服务端复核授权后才签名,宿主抑制迟到的旧结果。除桥接限制外,每用户每 Kit 最多滚动一分钟 30 次、滚动 24 小时 500 次。不要每动画帧请求,也不将它缓存为长期会话。

签名头为 { alg: "ES256", typ: "kit-launch+jwt", kid: "…" }。

Claim约定
iss精确等于该产品的 https://ims.koee.app
aud精确等于你的 Kit ID,单一字符串
subKit 专属标识,以 (iss, sub) 作为账号键
iat, expJWT 整数秒,exp = iat + 300
jti唯一 token ID
origin原生观察的 HTTPS Origin,按当前 Kit 配置校验
nonce提供时为传入桥接的 nonce
scopeprofile 或 profile handle
name, locale当前显示名和 App 界面语言
handle仅在 handle scope 下出现;用户无 handle 时可为空

token 刻意不包含头像字节,展示用 user.getProfile。桥接到期时间为毫秒:expiresAt = exp * 1000;JWT 时间为秒。不得混用单位,也不从 locale 推断时区。

公钥与后端验证

公开接口为 https://ims.koee.app/kit-keys/<kit_id>/jwks.json。无需登录或 App build/platform 请求头,豁免客户端最低版本检查,允许 CORS,并返回 Cache-Control: public, max-age=300。密钥按 Kit 独立,仅平台控制签名密钥;开发者获取公钥/JWKS 信息,不获取私钥或共享签名秘密。

后端创建本站会话前必须验证全部条件:

  1. ES256 签名、精确 typ 为 kit-launch+jwt,且 kid 仅来自预配置 Kit 的 JWKS。不得跟随 JWT jku、x5u、嵌入密钥或客户端提交的 issuer/JWKS URL。缓存最多 300 秒;未知 kid 最多刷新一次,仍未知则拒绝。网络或密钥错误须拒绝。
  2. 精确产品 iss、Kit aud,且 origin 在本站配置的 HTTPS Origin 集合内。不得接受其他 Kit 或产品 token。
  3. iat 与 exp 为整数,有效时长大于零且最多 300 秒,严格过期:nowMs >= exp * 1000 即拒绝,无宽限。仅 iat 可比本机时钟最多超前 60 秒。不要设置库级 60 秒 clock tolerance,这也会延长过期;异步获取密钥后重新检查时间。
  4. nonce 匹配且新鲜,绑定此预会话,并以原子方式消费一次。也可为 (iss, aud, jti) 构建共享原子重放存储,至少保留至到期,并安全绑定请求。仅比较 nonce、按进程存储或分开读取/删除均不足。
  5. 全部通过后才创建自己的后端新会话,以 (iss, sub) 为键。名称、handle 和头像是展示属性,不是认证键;单独执行本站业务权限。

下载完整可运行示例、依赖清单与离线测试:

两个示例均使用后端进程共享的持久 SQLite 原子 nonce 存储。多主机/serverless 实例需使用同一共享事务存储,各实例本地数据库不足。README 说明预会话 Cookie、CSRF 和 HTTP 接入要求。这些是后端模块与测试,不是完整登录服务器。

撤销与会话期限

移除/撤销 Kit、失去访问、账号删除或停用身份能力,会停止该授权的新身份读取。取消发布会结束已发布用户的访问;获准管理者可另行同意未发布开发预览,使用隔离授权。已有签名 token 仍可能在剩余期限内离线验证(最多 300 秒)。平台撤销无法删除后端已持有数据或自动结束本站会话。敏感操作前申请新启动 token,并执行自己的会话期限/退出策略。

收到 auth.changed 时清除展示资料和本地身份,并调用本站退出路由。宿主也会关闭或重置受影响文档及网站数据。此事件为尽力通知,后端不能依赖浏览器收到它来保证撤销。

bridge.on('auth.changed', () => {
  document.querySelector('#name').textContent = '';
  document.querySelector('#avatar').removeAttribute('src');
  // End the site's own session through its normal CSRF-protected logout flow.
  void logoutOwnSession();
});

正常密钥轮换将旧公钥与新公钥同时发布 24 小时。撤销密钥立即从新的 JWKS 移除,但已有缓存仍可能最多五分钟接受它。收到撤销通知时立即刷新所有后端密钥缓存。轮换及重叠期都不延长 token 期限。Kit 身份与外部账号登录协议独立。

测试与调试

  • 首次批准前,仅所有者和管理员可在 App 搜索 @handle 打开,使用草稿设置和所申请的可用能力。访问与发布中添加的用户仅在发布后可打开。
  • 草稿缺少入口 URL 或允许域名时显示“此 Kit 尚未完成设置”。
  • WebView 调试位于访问与发布,无需审核,仅对所有者和管理员生效,其他人始终关闭。下次打开或恢复前台时获取变更。启用后可连接 Safari Web Inspector(iOS、macOS)或 chrome://inspect(Android)。Android 上调试在 Kit 显示时作用于整个 App 进程,因此也能检查 App 内其他网页内容。
  • 调试启用时,更多菜单提供开发者工具:Kit ID、容器版本、入口 URL、已启用能力、重新加载、重置网站数据及最近 100 次桥接调用(方法、结果码和耗时,不含参数或结果)。
  • User-Agent 末尾为 KoeeKit/1 (<Kit ID>),仅用于统计或排障,不作安全判断。
  • 推荐使用私有、连接测试域名、开启调试的开发 Kit(如 @shop_dev),及独立生产 Kit(@shop)。handle 永久保留,请慎重命名。

提交审核

  1. 完成概览清单:名称、描述、提供者名称、联系信息、隐私政策、入口 URL 与允许域名。
  2. 在审核页核对与线上版本的变化,选择提交审核。
  3. 审核期间可继续编辑,后续编辑不属于该次提交;再次提交替换待审版本,可随时撤回。
  4. 批准后发布已提交版本;拒绝原因显示在概览页。

审核检查提交的地址、能力和展示配置。获批后你仍对网页内容负责;问题举报可导致维护、暂停能力或永久下架。

等待审核时,已发布内容若变化(设为私有、关闭/暂停能力、运营人员编辑),提交会过期,需要重提。维护、调试、团队和访问列表变更不影响提交。运营人员停用或永久下架 Kit 时,提交被撤回。

可见性与发现

  • 私有(默认):仅访问列表中的用户、群组和频道可打开;群组/频道按当前成员计算。通过 handle 添加,用户必须为真人而非 Bot;添加群组/频道要求你是其所有者或管理员。最多列出 1000 用户和合计 50 个群组/频道。
  • 公开:任何人可通过完整 @handle 找到 Kit,在访问与发布申请并经审核生效。重新设为私有无需审核;列表外用户不能打开,已打开页面在下次恢复前台关闭。
  • 审核批准不自动进入广场。广场由运营策划,仅公开且已发布 Kit 有资格展示。
  • 维护暂时阻止打开,已添加用户看到维护提示。运营人员也可停用或永久下架;永久下架不可撤销。

限制

操作上限
拥有的活跃 Kit10
累计可创建 Kit30
同时待审 Kit5
创建 Kit每 24 小时 3 次
提交与撤回每 24 小时各 10 次
检查 Kit ID 或 handle每分钟 30 次
上传图标每 24 小时 50 次,同时 2 次,每 Kit 存储 50 MB
每 Kit 管理员10

管理员与图标存储按 Kit 计算,其余按账号。创建、提交、撤回、ID/handle 检查及图标上传另有网络地址上限,为账号上限的三倍,共用连接的团队共享该额度;同时两个上传仅按账号计算。24 小时为滚动窗口而非日历日,ID 和 handle 检查共用额度,失败尝试也可能计数。

安全与隐私检查清单

  • 网页和 JavaScript 不放秘密,任何人都可检查;服务端验证所有输入。
  • user.getProfile 是未签名的展示数据,不能认证后端请求。创建会话前,用 auth.getLaunchToken 并在服务端验证签名、claims 和一次性 nonce。
  • 保持允许域名受控。移除域名属于需审核的草稿变更;若失去控制,应立即设为维护,联系 support@koee.app 并提交变更。
  • 隐私政策链接保持有效且准确,仅收集 Kit 所需数据。
  • 不尝试逃离 Kit:不用导航技巧、冒充 App 的隐藏框架或请求容器拒绝的权限。

后续计划

  • 配套 Bot(bot.chat)与分享到聊天(chat.share)。
  • 支持 Koee Web。
  • 无需 App 即可开发的浏览器 KitBridge shim。