Skip to main content

Capabilities: Storage, Todos, Network, and Host UI

This page gives an overview of the host capabilities a component can call and their boundaries. For full signatures, see WidgetApi methods; all limits are also listed in the Security Model.

Instance Storage (storage)

Each component instance has its own private key-value store, scoped by the MessagePort session — a component can never read or write across instances or components, and it cannot use localStorage directly (sandbox isolation makes host storage unreachable).

await api.storage.set('lastSurface', 'widget')
const { found, value } = await api.storage.get('lastSurface')
const { keys } = await api.storage.keys()
const { removed } = await api.storage.remove('lastSurface')
const unsubscribe = api.storage.onChanged((event) => {
// event: { key: string, operation: 'set' | 'remove' },刻意不携带 value
})

Constraints:

  • Values must be plain JSON (WidgetJsonValue: null | boolean | number | string | array | plain object); functions, circular references, and host objects are rejected.
  • Per-instance quota is 1 MiB, metered by the host transaction before commit; exceeding it returns QUOTA_EXCEEDED.
  • Keys are limited to 256 characters.
  • storage.get uses { found, value? } to distinguish "key does not exist" from "value is JSON null".

Instance Channel (channel)

Multiple surfaces of the same instance (e.g. a tile and a modal) can send messages to each other:

await api.channel.publish({ type: 'refresh' })
const unsubscribe = await api.channel.subscribe((event) => {
// event.message 是发送方消息的结构化克隆副本
})
await unsubscribe() // 同时移除监听并退订

Messages are limited to the same instanceId, up to 64 KiB each, plain JSON only. Communication between different instances or different components is not possible.

Todos (todos)

Accessing the user's todo data requires permissions declared in the Manifest, and each permission is independent:

MethodRequired permission
todos.list / todos.onChangedtodos.read
todos.createtodos.create
todos.updatetodos.update
todos.removetodos.delete
if (context.permissions.granted.includes('todos.read')) {
const { items, nextCursor } = await api.todos.list({ limit: 50 })
}
  • Todo text is limited to 500 characters; list supports cursor pagination (cursor + limit, max 100 per page).
  • For update, passing null for dueDate explicitly clears it; dates must be real YYYY-MM-DD dates.
  • After a successful write, the host broadcasts a todoChanged event to all live surfaces.
  • The host re-checks the grant on every call; after revocation, the next call immediately returns PERMISSION_DENIED.

Controlled Network (network.fetch)

network.fetch is the component's only network egress (the surface CSP's connect-src 'none' forbids components from making network requests directly):

const response = await api.network.fetch({
url: 'https://api.example.com/data',
method: 'GET',
headers: { 'Accept': 'application/json' },
})
// response: { status: number, headers: Record<string, string>, body: string }

Constraints (validated by the SDK and enforced by the host Broker):

  • The URL must be an HTTPS origin already declared in the Manifest network section and consented to by the user; localhost, bare IPs, URLs with credentials, and ports deemed dangerous by the Fetch spec are forbidden. Unauthorized origins return ORIGIN_NOT_ALLOWED.
  • Method whitelist: GET | POST | PUT | PATCH | DELETE.
  • No cookies (credentials: 'omit'), no referrer, and all redirects are rejected.
  • Request body ≤ 1 MiB; URL ≤ 2048 characters; ≤ 32 request headers (name ≤ 128 characters, value ≤ 8 KiB).
  • Forbidden request headers: cookie, host, origin, referer, plus any proxy- or sec- prefix.
  • Response body ≤ 2 MiB (RESPONSE_TOO_LARGE when exceeded); only whitelisted response headers are passed through: cache-control, content-language, content-length, content-type, etag, last-modified.
  • 10-second timeout; at most 4 concurrent network requests per instance (CONCURRENCY_LIMIT when exceeded).
  • An Authorization header may be passed explicitly, but it is never logged.

On the Web platform, requests are subject to the target site's CORS policy (PaTab does not provide a proxy bypass); on the browser-extension platform, this is implemented via an optional host permission that the user can revoke at any time.

Host UI (ui)

All UI is rendered by the host and carries a third-party origin badge; components cannot impersonate system UI:

await api.ui.toast('已保存')
const { confirmed } = await api.ui.confirm({
title: '删除数据?',
message: '该操作不可撤销。',
confirmLabel: '删除',
cancelLabel: '取消',
})
const { opened } = await api.ui.openExternal({ url: 'https://example.com' })
  • confirm: cancelling, pressing ESC, or closing via the overlay all return { confirmed: false }. Title ≤ 120 characters, body ≤ 1000, button labels ≤ 40.
  • openExternal: HTTPS URLs only (≤ 2048 characters); requires a valid user gesture, and the host opens a new tab with noopener,noreferrer only after confirming the target origin; when blocked, it returns { opened: false } instead of throwing.
  • toast: text ≤ 500 characters; at most 3 calls within 10 seconds.
  • Calling alert/confirm/window.open inside a component is forbidden (the sandbox does not provide them); always use the SDK.

Rate and Size Limits Quick Reference

Enforced by the host Broker on every call:

LimitValue
Single RPC request size256 KiB
Single channel message64 KiB
Request rate100 calls per 10-second window
Concurrent requests16 (of which 4 network)
toast3 per 10 seconds
Repeated violations3 consecutive violations trigger flood protection and destroy the iframe

Error codes for exceeding limits: RATE_LIMITED, CONCURRENCY_LIMIT.