# The AI Platform Miniapp SDK

`@theaiplatform/miniapp-sdk` is the TypeScript SDK for building, testing, and
shipping portable miniapps on The AI Platform.

```bash
pnpm add @theaiplatform/miniapp-sdk
```

The package provides the host-injected SDK API, isolated surface lifecycle
types, browser helpers, portable React UI components, manifest schema,
Rstest/Playwright integration, and the Rspack/Rslib integration used to produce
descriptor-backed Module Federation targets.

Start with the [Miniapp SDK documentation](https://docs.theaiplatform.app/miniapps/).

## Generation-2 source manifest

`SourceManifestV2` is the provider-neutral package input. It contains only a
display-only version label, optional local name/slug, exact artifacts and
target locks, permissions, and runtime effects. TAP package/release identity,
workspace or publisher coordinates, Zephyr coordinates, lifecycle state,
channels, tags, ranges, and dependency declarations are rejected.

Source adapters pass the received bytes unchanged. Parse and hash those exact
bytes first; derive the sorted closure separately:

```ts
import {
  extractSourceManifestClosureV2,
  readSourceManifestV2,
} from '@theaiplatform/miniapp-sdk/source-manifest';

const exact = await readSourceManifestV2(sourceBytes);
const closure = extractSourceManifestClosureV2(exact.parsedManifest);
```

`exact.rawBytes`, `exact.sha256`, and `exact.byteLength` are the descriptor
identity. Never replace them with serialized `parsedManifest` or `closure`
bytes. `manifest.tap.json` and npm's transport-only `package.json` are reserved
paths; neither can enter `artifacts`.

Desktop packages can feature-detect `sdk.navigation.openExternal`. Call it
synchronously from a trusted click with a canonical HTTPS URL without
credentials. The calling desktop UI surface must declare the on-demand
`navigation.open-external` action and list the exact origin in an
`external-navigation` effect. That surface must be workspace-scoped, and the
package's `compatibility.tapHost` range must exclude versions before `2.3.4`.
The effect does not grant SDK HTTP access. Successful calls open only the
operating-system browser; TAP-relative routes continue to use
`sdk.navigation.open`.

Miniapps can persist links to their own resources in Chat without constructing
or exposing a TAP router URL. Declare `chat.compose` on the calling surface,
then call `sdk.chat.stageDeepLink` synchronously from the user's click. The
package supplies only a label and bounded JSON target; the host derives and
binds the exact workspace, installation, package, release, digest, and surface
provenance.

```ts
button.addEventListener('click', () => {
  void sdk.chat.stageDeepLink?.({
    label: 'Open selected board',
    target: { kind: 'board.card.v1', cardId: selectedCardId },
  });
});

const unsubscribe = sdk.navigation.subscribeDeepLinks?.(({ target }) => {
  if (target.kind === 'board.card.v1' && typeof target.cardId === 'string') {
    openCard(target.cardId);
  }
});
```

Chat renders a host-owned button, never an anchor. On activation, the host
requires the same exact installed release and delivers the opaque target only
to that package surface; release or generation drift fails closed. Keep the
subscription for the surface lifetime and call `unsubscribe` when it unmounts.

## Authoring and package lifecycle

Define the project once in `tap-miniapp.config.ts`. This authoring config is
not the finalized host descriptor:

```ts
import packageJson from './package.json' with { type: 'json' };
import {
  defineTapMiniapp,
  packageContributionProvider,
} from '@theaiplatform/miniapp-sdk/authoring';
import { commandTargetBuilder } from '@theaiplatform/miniapp-sdk/lifecycle';

export default defineTapMiniapp({
  versionLabel: packageJson.version,
  presentation: {
    name: 'Example',
    slug: 'example',
    description: 'An example miniapp.',
    categories: ['other'],
  },
  compatibility: { tapHost: '>=2.5.5' },
  targets: {
    desktop: {
      remoteName: 'example_desktop',
      exposes: {
        './ui/desktop': { source: './src/surface.tsx', runtime: 'webview' },
      },
      builder: commandTargetBuilder({
        command: 'pnpm',
        args: ['run', 'build:target'],
      }),
    },
  },
  contributions: [packageContributionProvider()],
  runtimePolicy: { checkpoint: 'none', lifecycleExpose: null },
});
```

The project owns its display-only version label and target builder. The
executing SDK derives `compatibility.tapSdk`, versioned specialist artifacts,
target locks, integrity values, and the exact source closure. TAP mints package
and release identity only after import. Generated inputs stay under
`.tap-build`; the lifecycle replaces `dist` only after every target and
verification hook passes.

Use the same lifecycle from scripts or TypeScript:

```bash
pnpm exec tap-miniapp build
pnpm exec tap-miniapp check
pnpm exec tap-miniapp publish
pnpm exec tap-miniapp publish --from dist
pnpm exec tap-miniapp publish --watch
```

```ts
import {
  buildTapMiniapp,
  checkTapMiniapp,
  publishTapMiniapp,
} from '@theaiplatform/miniapp-sdk/lifecycle';
```

`check` runs the complete build and verification path without replacing
`dist`. `publish --from` re-verifies an assembled package before it invokes the
configured publisher. Only `publish` and `publish --watch` can contact a
publisher; `build` and `dev` stay local.

Two version values have separate owners:

| Value                  | Source                                  |
| ---------------------- | --------------------------------------- |
| `versionLabel`         | The project's display/version label     |
| `compatibility.tapSdk` | The exact executing SDK package version |

An SDK dependency bump changes SDK compatibility without renaming specialists.
A version-label bump rematerializes every version-scoped specialist and lock
without choosing TAP Registry identity.

Every package built with SDK 0.12 must set `compatibility.tapHost` to a range
that excludes host versions before `2.4.1`. SDK 0.13 packages must exclude host
versions before `2.5.5`, regardless of which SDK APIs the package uses.

### Contribution authoring CLI

The installed `tap-miniapp` binary adds logical, versionless package
contributions:

```bash
pnpm exec tap-miniapp create specialist \
  --id query-expert \
  --purpose "Analyze query results and explain the important details." \
  --non-interactive \
  --json

pnpm exec tap-miniapp create chat-block \
  --id query-report \
  --specialist query-expert \
  --primitive report \
  --non-interactive \
  --json
```

Chat blocks use the host-rendered `tap-primitives-v1` protocol. The supported
primitives are `table`, `report`, and `notice`. The compiler creates the locked
payload schema and teaches the selected specialist to emit the untrusted draft
form that The AI Platform validates and seals.

Packages that need app-specific presentation can instead pair a `chat.block`
with an exact same-package `ui.renderer` using the
`tap-federated-view-v1` protocol. Both contributions must declare the same
locked payload schema and target set. The renderer expose receives only the
immutable payload, a live theme value, and the host actions declared by its
manifest; it does not receive the miniapp SDK bridge, tools, events, storage,
navigation, frames, or network access. The locked shell cancels document
navigation before a request is sent; WebViews without that browser boundary
fail closed to the persisted fallback.

```ts
import type { TapFederatedChatRendererModule } from '@theaiplatform/miniapp-sdk/chat-renderer';

const renderer: TapFederatedChatRendererModule<MyPreview> = {
  mount(container, context) {
    // Validate context.payload with the package-owned locked schema first.
    const button = document.createElement('button');
    button.textContent = 'Open full app';
    button.onclick = () => context.actions['open-app']?.invoke();
    container.replaceChildren(button);
    return { unmount: () => container.replaceChildren() };
  },
};

export default renderer;
```

The host mounts that expose lazily in a bounded, capability-free iframe. A
missing release, mismatched provenance or schema digest, load failure, crash,
or invalid renderer message preserves the sealed chat block's static fallback.
Raw isolated HTML remains unsupported.

Run `pnpm exec tap-miniapp --help` for the full flag contract. Create commands never overwrite an existing contribution or file.

### Zephyr publication

Install the separately released first-party Zephyr publisher adapter and set
the authoring config's `publisher` field to `zephyrPublisher(...)`. The
adapter validates the project-owned Zephyr coordinates before authentication,
uploads the exact source-manifest closure, and returns the immutable deployment
root. Keep credentials and control-plane
profile selection in the project environment. Local, linked, and offline builds
remain non-importable; only a successful public Zephyr publication returns an
import URL.

## Supported entry points

- `@theaiplatform/miniapp-sdk`
- `@theaiplatform/miniapp-sdk/sdk`
- `@theaiplatform/miniapp-sdk/web`
- `@theaiplatform/miniapp-sdk/ui`
- `@theaiplatform/miniapp-sdk/ui/wasm`
- `@theaiplatform/miniapp-sdk/ui/styles.css`
- `@theaiplatform/miniapp-sdk/ui/tailwind.css`
- `@theaiplatform/miniapp-sdk/ui-components.json`
- `@theaiplatform/miniapp-sdk/react`
- `@theaiplatform/miniapp-sdk/surface`
- `@theaiplatform/miniapp-sdk/inline-renderer`
- `@theaiplatform/miniapp-sdk/link-unfurl`
- `@theaiplatform/miniapp-sdk/chat-renderer`
- `@theaiplatform/miniapp-sdk/vscode-webview`
- `@theaiplatform/miniapp-sdk/config`
- `@theaiplatform/miniapp-sdk/authoring`
- `@theaiplatform/miniapp-sdk/lifecycle`
- `@theaiplatform/miniapp-sdk/source-manifest`
- `@theaiplatform/miniapp-sdk/source-manifest.schema.json`
- `@theaiplatform/miniapp-sdk/rspack`
- `@theaiplatform/miniapp-sdk/testing/rstest`
- `@theaiplatform/miniapp-sdk/testing/rstest-config`
- `@theaiplatform/miniapp-sdk/testing/tap.test.schema.json`
- `@theaiplatform/miniapp-sdk/config-schema.json`

## In-app E2E testing

Pin the SDK to an exact version. The authoring compiler writes that executing
version to `manifest.compatibility.tapSdk`. Test Lab rejects ranges, tags,
duplicate dependency declarations, and output built by a different SDK:

```json
{
  "devDependencies": {
    "@theaiplatform/miniapp-sdk": "0.16.0"
  }
}
```

Generate or audit the test project with the installed package binary:

```bash
pnpm exec tap-miniapp-test list
pnpm exec tap-miniapp-test scaffold
pnpm exec tap-miniapp-test scaffold --check
pnpm exec tap-miniapp-test matrix
pnpm exec tap-miniapp-test doctor
```

All commands accept `--root`, `--manifest`, `--descriptor`, and `--json`.
`scaffold` creates only missing files and preserves user-owned files.
Its smoke test asserts the exact surface-target cell, profile, matrix entry,
deterministic seed, SDK/host/runner versions, and all host-attested digests
before resetting Surface fixture state (or remounting a non-Surface row).
After that lifecycle boundary it also
requires the generated `#tap-root` to be visible and non-empty and the
generated lifecycle error region to remain hidden, so a blank or failed mount
cannot pass as a loaded surface. Add one app-specific loaded-state assertion
to the generated smoke test: structural checks cannot recognize an app-owned
error shell. New positive profiles capture screenshots with `always`; denied
and error profiles retain `failure-only` by default.
`migrate --dry-run` previews the deterministic schema-v1 projection;
`migrate` preserves the exact old document as `tap.test.v1.json` before an
atomic replacement. Run `doctor` in CI so missing files, unsafe paths, stale
versions, empty test selections, capability gaps, and surface applicability
branches fail before Test Lab starts a browser.

The generated `rstest.tap.config.ts` uses the public deterministic helper:

```ts
import { defineTapRstestConfig } from '@theaiplatform/miniapp-sdk/testing/rstest-config';

export default defineTapRstestConfig({
  include: ['tests/e2e/**/*.tap.ts'],
});
```

It fixes Rstest to one worker, serial execution, no retries, no isolation, a
Node test environment, the process timezone pinned to UTC, and an error for
empty suites. Safe Rstest options remain composable; incompatible overrides
fail at config load rather than silently making a run nondeterministic. New
scaffolds pass the descriptor's exact
effective `testMatch` as `include` and use the `.tap.ts` suffix so host-driven
tests do not leak into ordinary unit-test discovery.

`tap.test.json` schema v2 describes profiles separately from matrix entries.
Every manifest-declared surface and target needs exactly one positive matrix
entry. A positive entry names the capabilities it verifies:

- `action:<permission-action-id>`
- `effect:<effect-kind>:<resource>`
- `effect:<effect-kind>:*` when the manifest effect has no resources

When a cell has multiple allowed rows—for example, one loaded journey plus
scripted HTTP or transport-error recovery rows—suffix exactly one matrix entry
ID with `-positive`. Doctor uses that explicit marker as the cell's capability
authority while leaving the other allowed rows available as error evidence.
Cells with only one allowed row remain backward compatible without the suffix.

`permissionScenario` accepts only `default`, `all-denied`, `read-only`,
`http-denied`, or `deny:<action-id>`. Its `deniedActions` must exactly mirror
that choice (`[]`, `["*"]`, `["do:*"]`, `["network.request"]`, or the one
named action). Host-internal `synthetic:*` scenarios are runtime diagnostics,
not portable descriptor inputs; `doctor` rejects them before Test Lab.

Write denied/error matrix entries for every high-risk action and host effect.
Use `network.request` only for network-effect denial. A supported exact
`host-api` effect can use its canonical fixed permission action only when both
the action ID and catalog resource match the host mapping; a resource-matching
alias does not qualify. Fixture storage and presence effects use the host policy actions
`storage.read` or `storage.write` and `presence.write`, respectively. A
credential effect is a high-risk secret-use coverage requirement and therefore
uses only an exact declared action whose catalog resource is
`tap.credentials:use`; a `credentials.read` denial can cover metadata listing
and `effect:host-api:tap.credentials:read`, but it does not prove secret-use
denial. Use the all-denied `*` scenario only when no exact action expresses the
denial. A waiver may temporarily replace a denied/error case only when it
explains the gap and has a future `expiresAt`; it never replaces positive
capability attribution. Put surface applicability in each matrix entry's
`testMatch`, rather than returning or branching on `tap.surfaceId` inside a
test shared by multiple surfaces. These capability names are preflight
declarations and coverage requirements; only an executed Test Lab result is
behavioral evidence.

The canonical descriptor schema is published at
`@theaiplatform/miniapp-sdk/testing/tap.test.schema.json`. Profile environment
fields fix viewport, locale, IANA timezone, theme, reduced motion, and the
unsigned 32-bit fixture seed. Set optional `fixedNow` to a canonical UTC RFC
3339 instant such as `2026-01-01T00:00:00Z` when the app derives routes,
queries, labels, or fixture data from the wall clock. The adapter installs that
clock before mounting the surface, so tests never need to scrape a
machine-dependent date and remount. Federated surfaces that create app-owned
identifiers should use `context.entropy.randomUUID()`: ordinary mounts retain
cryptographically strong UUIDs, while Test Lab derives a frame-scoped,
resettable stream from this profile seed. This keeps retained remounts distinct
and the same test sequence reproducible without test-only globals or query
parameters. Artifact policy independently controls
trace and screenshot capture with `off`, `failure-only`, or `always`, plus an
optional total byte limit.

Surface profiles may pair an existing `credentialSlots` entry with an explicit
`httpCredentialFixtures` entry containing only `slot`, a supported
`credentialType`, `displayName`, and bounded non-secret `metadataFields`. Test
Lab binds that declaration to the run-selected alias before exposing it through
`sdk.credentials.listHttp`; an alias without an explicit fixture remains
invisible. Metadata must exactly match the credential type's public vault
shape: bearer `{}`, Basic `{ username }`, header auth `{ header_name }`, or API
key `{ placement, parameter_name }`; secret and extra keys are rejected. The
fixture contract never accepts credential material, does not apply to Live TAP,
and does not bypass the independent `credentials.read`, `credentials.use`,
credentials-effect, exact-alias, or explicit-fixture checks.

The `/testing/rstest` entry point extends Rstest's Playwright fixtures with a
host-provided miniapp surface and exact TAP session provenance. Tests can assert
both the rendered UI and the package/surface identity selected by the Test Lab.
The package ID is host-provided Registry identity, not stable authoring input:

```ts
import { expect, test } from '@theaiplatform/miniapp-sdk/testing/rstest';

test('mounts the miniapp surface', async ({ surface, tap }) => {
  await expect(surface.locator('body')).toBeVisible();
  expect(tap.packageId.length).toBeGreaterThan(0);
  expect(tap.surfaceId).toBe('example-surface');
  expect(tap.environment.seed).toBe(tap.seed);
  expect(tap.environment.fixedNow).toBe('2026-01-01T00:00:00Z');
  expect(tap.artifacts.trace).toBe('failure-only');
});
```

Fixture-backed tests can seed the exact channel access returned by
`sdk.channels.getAccess`. Omit `access` for the default participating
`read`/`write` channel, or declare the bounded capabilities needed by the
scenario:

```ts
await tap.fixture.seed({
  channels: [
    {
      roomId: tap.channelId,
      title: 'Fixture channel',
      access: {
        isParticipant: true,
        capabilities: ['read', 'write', 'manage'],
      },
    },
  ],
});
```

Fixture files and `tap.fixture.seed()` or `tap.fixture.http.script()` payloads
can use `tap-fixture-workspace-v1` and `tap-fixture-channel-v1` as reserved
scope references in string values. Before applying the fixture operation, the
Test Lab host binds every occurrence—including embedded URLs, headers, body
text, storage values, presence state, and VFS paths—to the run's actual
workspace or channel. It validates the bound payload again, and reset restores
the same bound base state. Do not use either token as ordinary fixture content
or as a JSON object key.

Channel access is part of the runtime fixture snapshot and digest, so reset
restores it with the rest of the deterministic state. The session-level
`tap.fixtureDigest` attests the immutable descriptor fixture inputs selected
for the run. By contrast, `tap.fixture.snapshot()`, fixture mutation receipts,
and `tap.control.reset()` report the current runtime realm digest and
generation. Compare runtime receipts with runtime snapshots; do not compare
them directly with the descriptor-input digest.

Script deterministic failures before the miniapp makes an `sdk.http` request.
The helper validates the same bounded `code` and `message` contract exposed by
`MiniAppHostActionError`, and each exact script is consumed once:

```ts
import { createTapMiniappTestTransportError } from '@theaiplatform/miniapp-sdk/testing/rstest';

await tap.fixture.http.script({
  request: {
    method: 'GET',
    url: 'https://api.example.test/projects',
  },
  transportError: createTapMiniappTestTransportError({
    code: 'connection_failed',
    message: 'The fixture origin is unavailable.',
  }),
});
```

Use `response` for a completed HTTP exchange or `transportError` for a failure
that occurs before any response exists. Supplying neither or both fails before
the Surface receives the request.

Install `@rstest/core@0.11.5`, `@rstest/playwright@0.11.5`, and Playwright as
direct development dependencies when using this optional entry point. Doctor
rejects missing or incompatible ambient runner installations and recommends
`test:tap`, `test:tap:list`, and `typecheck:tap` scripts.

In a UI surface manifest, put only actions required for the surface to project
in `authorization.allOf`. Put actions that the mounted surface checks at the
point of use in `authorization.onDemand`. Both fields declare possible calls;
neither grants authority, and every host call still evaluates current policy.

The host API is injected at runtime. Importing an entry point is safe in build
tools and tests; host-backed calls fail when used outside a supported runtime.
`sdk.storage` provides revisioned non-secret JSON scoped by the host to the
active workspace and exact package. `sdk.presence` provides host-stamped,
ephemeral room membership and state; package code never supplies participant
identity. On supported desktop surfaces, `sdk.http` sends bounded HTTP(S)
requests through host consent instead of browser `fetch`, and
`sdk.credentials` lists metadata-only HTTP credential references. Credential
secrets remain in the host vault and are injected only by the native request
authority.

### Authorization checks and consent

Custom surface behavior can ask the exact mounted host for its current
descriptor-declared action decision without executing that action:

```ts
const { allowed } = await sdk.authorization.check({
  actionId: 'example.export',
  autonomy: 'do',
});
```

Use this advisory check to disable or explain custom UI before the user invokes
it; the capability that performs the operation must still enforce authority.
A declared but currently denied action resolves to `{ allowed: false }`. An
action omitted from both this exact surface's `authorization.allOf` and
`authorization.onDemand` rejects as a manifest defect. The check is read-only
and never executes the queried action, creates a grant, or prompts the human.

In an ordinary production surface, persisted grants can satisfy `none`,
exact-channel `channel`, and `reusable` consent. Actions declared with `once`
or `fresh-decision` deliberately resolve to `{ allowed: false }`, even when a
grant row was retained, because they require a call-bound consent attestation.
The public surface SDK does not currently expose a consent request API that can
mint that authority, and calling the protected operation does not prompt
instead. Do not weaken consequential consent to make the check pass.

Use a positive [Surface Test Lab](https://docs.theaiplatform.app/miniapps/testing)
fixture row to validate the intended allowed journey and a denied row to
validate the fallback. Fixture authority validates miniapp behavior; it does
not demonstrate production support for single-use or fresh consent.

## User-selected desktop files

`sdk.files` is an optional desktop-only API for files the user explicitly
selects in a host-owned Open or Save dialog. It is independent of chat and the
package VFS. Miniapps receive an opaque owner-bound handle, safe metadata, and
bytes; native paths and provider credentials never cross the surface boundary.

```ts
if (sdk.files) {
  const [source] = await sdk.files.pickOpen({
    accept: [
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ],
  });
  if (!source) throw new Error('The picker returned no file');

  const metadata = await sdk.files.metadata(source);
  const bytes = await sdk.files.read(metadata.handle, {
    maxBytes: 16 * 1024 * 1024,
  });
  const receipt = await sdk.files.write(metadata.handle, bytes, {
    expectedRevision: metadata.revision,
    idempotencyKey: crypto.randomUUID(),
  });
  console.log(receipt.revision, receipt.contentHash);
}
```

Whole-file reads are capped at 16 MiB. Use `readRange` or
`createReadStream` for bounded reads, and `createWriteStream` for larger
atomic writes. Every write requires the revision previously observed and a
caller-owned idempotency key. An external edit observed by the host's final
revision check rejects with `file_stale`. Strict coordination with a
simultaneous uncooperative external writer remains outside this API's
guarantees. A hard host crash leaves the previous complete destination visible;
the host removes an interrupted stage only when its private journal, locked
marker, and recorded filesystem identities prove exact ownership. Incomplete
ownership proof stays untouched for a later retry, and the lost process-local
receipt is not replayed.
Metadata and watch receipts include a host-issued handle refreshed to their
reported revision. Use that handle for later reads and mutations; do not clone
or edit an older handle to advance its revision. A separately supplied mutation
fence may still use any revision explicitly observed for that selected file.
Pass an `AbortSignal` to stop SDK streaming and staging work, and use
`isMiniAppHostActionError` to branch on the stable `file_*` error codes.
Cancellation applies until a write stream dispatches its final commit. After
`close()` crosses that boundary, await its authoritative receipt or commit
error because publication can no longer be safely canceled.

Each exact file action must be declared once in the desktop UI surface's
`authorization.onDemand` list and permission catalog. The supported action ids
are `files.pick-open`, `files.pick-save`, `files.read`, `files.overwrite`,
`files.rename`, `files.delete`, and `files.watch`; the descriptor also needs one
`user-file` effect whose resources exactly match those declared operations.
Rename and delete require the exact observed revision plus their separately
declared consequential permission. `watch` is one bounded long poll and must be
called again after each result. The host sends metadata, bytes, mutations, and
watch requests through short-lived exact grants on the same-origin JSON Connect
endpoint, never through ordinary `postMessage`. Set
`compatibility.tapSdk` to exclude versions before `0.14.0` and
`compatibility.tapHost` to exclude versions before `2.9.0`. Persistent handle
recovery is reserved by `files.retain-handle` but is not supported by this
host version, so request only session-scoped handles and feature-detect
`sdk.files`.

A Save picker authorizes one exact logical write. The host binds that provenance
to the handle, picker-issued revision, and idempotency key before authorization,
so retry those exact fields when grant minting or its response is uncertain.
Changed write fields require separately granted `files.overwrite` authority.

The catalog also reserves the fresh-decision actions `files.associate`,
`files.import-disclose`, and `files.export-artifact`. They are owner-bound, not
generic `sdk.files` methods: the host owns default-handler settings, while the
collaboration authority owns disclosure and artifact-export decisions. Do not
declare them for ordinary picker, read, or write operations.

Handler registration follows the exact package installation and active
release. Disabling, rolling back, or uninstalling the package removes or
replaces that handler authority without deleting selected user files or
collaboration-owned artifacts; those artifacts retain their owning service's
independent lifecycle.

## Converted VS Code webviews

The `/vscode-webview` entry point mounts a converted, static VS Code webview in
a sandboxed iframe while keeping platform authority in the outer federated
surface. Declarative message bindings persist document data through
`sdk.storage`; the synchronous `localStorage` compatibility facade is backed by
`sdk.session`, so standalone and channel surfaces share sign-in only for the
same account, workspace, installation, and package; and exact HTTPS origins are
mediated through `sdk.http`. The bridge does not expose the host transport, and
undeclared origins fail closed.

Conversion tools should emit `tapVsCodeWebviewRuntimeSource` beside the imported
assets and call `mountVsCodeWebview` from the generated surface. The SDK owns
this compatibility boundary so conversion recipes describe mappings rather
than introducing a second miniapp data plane. TAP's package CSP permits only
same-origin external scripts and forbids `<base>`, so converters must
externalize executable inline scripts. The bridge resolves document resource
URLs against the original entry before mounting and exposes the immutable
`window.__TAP_VSCODE_WEBVIEW_ASSET_BASE_URL__` value for reviewed replacements
of extension-specific asset-root placeholders. Because the CSP forbids `<base>`,
the runtime also replaces `window.Request` with a wrapper that resolves relative
inputs against that same asset base rather than the outer surface URL, so both
`fetch('./data.json')` and `new Request('./data.json')` reach the webview's own
assets. The wrapper preserves `instanceof`, subclassing, and the
`new Request(request, init)` form.

Mediated requests carry text bodies only. The outer bridge is the authority: it
forwards a body only when every declared content type is a text media type —
`text/*`, `application/json`, `application/xml`,
`application/x-www-form-urlencoded`, or a `+json`/`+xml` suffix — so a
hand-serialized multipart or octet-stream payload is rejected even when a guest
replaces the injected runtime and posts an `http.request` envelope directly.
Multipart and binary top-level families are denied ahead of the structured
suffix allowance, so a value like `multipart/form-data+json` cannot pass on its
`+json` ending. The runtime applies the same allowlist inside the iframe for a
fast local failure, and additionally requires a body passed through `fetch`'s
init to be a string or `URLSearchParams`, so binary `BodyInit` values are
refused before anything tries to decode them as text. An explicit empty body
(`body: ''`) is forwarded as an empty string rather than dropped, since a signed
or length-sensitive endpoint can distinguish it from no body at all.

In Surface Test Lab runs, `sdk.http` uses the run's exact-origin policy and the
same bounded native transport, while stored credentials remain unavailable.
That origin list governs host-mediated SDK requests; it is not a browser-wide
network sandbox for trusted Playwright test code.

`sdk.printing` is an optional desktop-only receipt printer API. It discovers
bounded machine-local printer metadata and host-supported 58 mm and 80 mm paper
profiles, then accepts only bounded semantic version-1 receipt rows with
printable ASCII text and a stable job key. The miniapp passes its exact
printer/profile selection to status and submission; the host revalidates that
destination and owns wrapping, feed, cut, and OS spooler submission. Packages
must declare the exact descriptor effect
`{ "kind": "physical-output", "resources": ["receipt-printer"] }` and a
persisted `printing.receipt` action with reusable consent, `do` autonomy, and
consequential risk. Raw ESC/POS, direct USB/network access, and `window.print()`
are not SDK capabilities. Job-key journaling suppresses
routine reconnect duplicates, but physical exactly-once printing is impossible;
every result therefore carries `physicalExactlyOnce: false`.

## Identity and workspace

Three reads answer "who am I", "where am I", and "who else is here". They are
separate calls with deliberately different authority, because the answers expose
different people.

`sdk.user?.current()` returns `{ userId, displayName }` for the person using the
surface. It **requires no permission and prompts for nothing**: the mount context
already carries `userId`, so the name that goes with it is not new authority.
`displayName` is host-derived and cannot be supplied by a package; it is empty
when the signed-in profile has no usable name, so render a fallback rather than an
empty row.

`sdk.workspace?.current()` returns `{ workspaceId, displayName }` for the
workspace the surface is mounted in. `displayName` falls back to the workspace id
rather than an empty string, so a header always has something to print.

`sdk.workspace?.listMembers()` returns `{ members: [{ userId, displayName }] }` —
**joined members only**. An invitation is not a teammate, and pending invites would
reveal hiring before it is announced. The host's own roster also carries email,
role, title, timezone, invitation timestamps and who invited whom; none of it
crosses this boundary, and the result contract is closed so a field added upstream
cannot start flowing to guests.

`workspace.current` requires a persisted `workspace.read` action with `listen`
autonomy; `listMembers` requires `workspace.read-members`, deliberately separate so
an existing structure grant never widens into enumerating people. Both are `listen`
autonomy — the same authority as `listTeams` and `listProjects`. A package that
needs a roster must declare it; feature-detect each API and treat a denial as
"render without names" rather than an error, since a withheld grant is a normal
state and not a failure.

React callers use the hooks instead:

```tsx
import {
  useUser,
  useWorkspace,
  useWorkspaceMembers,
} from '@theaiplatform/miniapp-sdk/react';

const { user } = useUser();
const { workspace } = useWorkspace();
const { members, failure } = useWorkspaceMembers();
```

Each hook reads once on mount, never throws, and reports `isSupported: false` on a
host without the capability. `useWorkspace` and `useWorkspaceMembers` distinguish
`denied` from `request-failed`, because those want different words on screen: one
is "ask an admin", the other is "try again". None of them polls — an identity, a
workspace name, and a roster do not change often enough to justify it, and each
exposes `reload` for the caller that needs to retry.

**There is no avatar.** The generated surface CSP is `img-src 'self' data:`, so a
remote image cannot load whatever a manifest declares. Draw initials, or have your
own backend inline bytes as a `data:` URI.

## Tasks

`sdk.tasks` reads and updates user-authored tasks in the current workspace. The
host omits product-owned tasks and archived tasks by default. A list page
contains 25 tasks by default and accepts a `limit` from 1 through 50.

```ts
const visibleTasks = [];
let cursor: string | undefined;

do {
  const page = await sdk.tasks.list({
    limit: 50,
    ...(cursor === undefined ? {} : { cursor }),
  });
  visibleTasks.push(...page.tasks);
  cursor = page.nextCursor;
} while (cursor !== undefined);
```

Treat each cursor as opaque. Use the same workspace, `includeArchived` value,
and limit for the next call. Pagination is stable when task data does not
change. Pagination is not a snapshot, so a concurrent task change can move an
entry before or after the current position. The React `useTasks` hook follows
all pages before it publishes `data`.

## OS notifications

`sdk.notifications?.show({ message })` is an optional desktop and mobile API
for host-mediated OS notifications. Package code supplies only a non-empty
message of at most 512 Unicode scalar values. The host prefixes it with the
registered surface application name, owns native presentation, respects the
user preference and current OS permission without prompting, and rate-limits
calls per installation and mounted document. Notification messages and the
registered attribution name reject Unicode control, format, line-separator, and
paragraph-separator characters; attribution names are limited to 128 Unicode
scalar values.

Packages must declare the exact descriptor effect
`{ "kind": "user-notification", "resources": ["os"] }` and a persisted
`notifications.show` action with reusable consent, `do` autonomy,
consequential risk, and resource `os`. Feature-detect the API and handle
`shown` or a `suppressed` result with `notifications-disabled`,
`permission-denied`, or `rate-limited`. Test Lab returns a deterministic shown
result without dispatching a real OS notification.

See the
[OS notification API reference](https://docs.theaiplatform.app/miniapps/reference/sdk#os-notifications)
for the complete descriptor and calling contract.

## Terminal sessions in 0.3.0

`sdk.terminal?.v1` is an optional desktop-only terminal session API introduced
in SDK 0.3.0. It exposes
only the fixed `workspace-shell` and `neovim` profiles; package code cannot
choose an executable, environment, native terminal ID, working directory, or
filesystem path. `open()` returns a host-owned session with ordered binary
events, acknowledged binary writes, resize, and idempotent close. A dedicated
per-session channel applies byte credit and bounded buffering, so terminal
output never uses the ordinary miniapp host-action bridge as a data plane.
Packages opening a session declare the selected `terminal` profile and the
`filesystem-mount` resource `conversation-vfs:read-write`, plus the persisted
`terminal.session.open`, `filesystem.read`, and `filesystem.write` actions.
Capability discovery is authoritative: the initial host reports both profiles
unavailable. `workspace-shell` waits for positive conversation-scoped read
isolation, and `neovim` additionally waits for an integrity-locked runtime.

See the
[terminal API reference](https://docs.theaiplatform.app/miniapps/reference/sdk#terminal-sessions)
for capability detection, the required descriptor authorization, session
events, limits, and cleanup.

## Discovery metadata

Descriptor-backed packages may assign up to three unique
`presentation.categories`. Import the `MiniAppCategory` type from the package
root or `/config`; accepted values are `productivity`, `developer-tools`,
`creativity`, `communication`, `data-and-analytics`, `business`, `education`,
`media-and-entertainment`, `utilities`, and `other`.

Do not add an authored `includes` field. The host derives the package's stable,
counted capability summary from its verified `contributions`, so Marketplace
details cannot drift from the code the descriptor is allowed to expose.

## Portable UI

Import the precompiled stylesheet once at the miniapp entry point, then use
components from the public UI entry. React 19 is a peer dependency.

```tsx
import '@theaiplatform/miniapp-sdk/ui/styles.css';

import {
  Button,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  SurfaceViewport,
} from '@theaiplatform/miniapp-sdk/ui';

export function WelcomeCard() {
  return (
    <SurfaceViewport>
      <Card>
        <CardHeader>
          <CardTitle>Ready to build</CardTitle>
        </CardHeader>
        <CardContent>
          <Button>Continue</Button>
        </CardContent>
      </Card>
    </SurfaceViewport>
  );
}
```

The generated surface document fixes `html`, `body`, and `#tap-root` to the
host-provided height and deliberately disables body scrolling. Keep one
`SurfaceViewport` at the full-page root so long content scrolls inside that
boundary with mouse, trackpad, keyboard, and touch input. Existing miniapps
that do not use React can apply the stylesheet's
`.tap-surface-scroll-root` class to their native root element.

Every flex or grid wrapper between `#tap-root` and a nested scroll owner needs
`min-height: 0` (Tailwind `min-h-0`). Use `height: 100%` instead of `100vh`.
For a wide table, put only the table in an
`overflow-x-auto overscroll-x-contain` wrapper so the outer surface remains the
vertical scroll owner. See the
[UI reference](https://docs.theaiplatform.app/miniapps/reference/ui#own-surface-scrolling)
for the complete layout contract.

Tailwind v4 miniapps can instead import the source integration after
`tailwindcss` and `tw-animate-css`; it supplies the platform theme and scans the
SDK's UI bundle for component classes.

```css
@import 'tailwindcss' source(none);
@import 'tw-animate-css';
@import '@theaiplatform/miniapp-sdk/ui/tailwind.css';

@source './src';
```

Use `installMiniAppAppearanceSync()` from the `/web` entry to apply the host's
light/dark theme and bounded UI scale. Tools and agents can read the exported
`ui-components.json` catalog to discover the supported component families and
style entry points without importing application-private packages.

Render authored Markdown through the SDK component so miniapps share the
platform's sanitization and URL policy. It accepts static Markdown, admits only
absolute HTTP(S) links, admits only absolute HTTPS images, and sends only
admitted links to `onOpenLink`:

```tsx
import { Markdown } from '@theaiplatform/miniapp-sdk/ui/markdown';

export function Brief({
  source,
  openExternalLink,
}: {
  source: string;
  openExternalLink: (url: string) => void;
}) {
  return <Markdown onOpenLink={openExternalLink}>{source}</Markdown>;
}
```

### Rust/WASM UI

Rust/WASM miniapps can render the same public component library through the
imperative `/ui/wasm` entry. It accepts a bounded serializable model, owns its
React root and providers, and emits serializable actions with stable control
IDs, entity IDs, model revisions, and unique event IDs.

```ts
import '@theaiplatform/miniapp-sdk/ui/styles.css';
import { createMiniAppUiRoot } from '@theaiplatform/miniapp-sdk/ui/wasm';

const ui = createMiniAppUiRoot(element, model, (action) => {
  wasm.onUiAction(action);
});

ui.update(nextModel);
ui.focus('project-name');
ui.unmount();
```

The bridge synchronizes theme and UI scale, restores focus for controlled
dialogs, exposes a live-announcement method, rejects malformed or stale models,
reports render and dispatch failures explicitly, and makes teardown idempotent.
See [`examples/wasm-ui`](./examples/wasm-ui) for a clean wasm-bindgen consumer
that maps JavaScript exceptions to Rust `Result` values and rejects stale or
replayed actions.

## License and public policies

By downloading, installing, copying, accessing, or using this SDK, you accept
the [Miniapp SDK Proprietary License Agreement](./LICENSE.md). The SDK has no
separate license fee and is provided as-is under that agreement.

- [Acceptable Use Policy](https://theaiplatform.app/acceptable-use)
- [Privacy Notice](https://theaiplatform.app/privacy)
- [Security and Responsible Disclosure Policy](https://theaiplatform.app/security)
- [Developer and Marketplace Policy](https://theaiplatform.app/developer-marketplace-policy)
- [Third-Party Notices](./THIRD_PARTY_NOTICES.md)
