Surfaces and Lifecycle

A ui.surface expose exports a mount function. The host calls it with a container owned by the current realm and a TapFederatedSurfaceMountContext for the exact package release, installation, contribution, and instance.

import type {
  TapFederatedSurfaceMount,
  TapFederatedSurfaceMountContext,
} from '@theaiplatform/miniapp-sdk/surface';

export function mount(
  container: HTMLElement,
  context: TapFederatedSurfaceMountContext,
): TapFederatedSurfaceMount {
  // Render into container and attach listeners here.
  return {
    unmount() {
      // Remove listeners, subscriptions, timers, and rendered content here.
    },
  };
}

Mount Context

The context contains read-only identity and scope fields:

  • package, namespace, release, installation, contribution, and instance IDs;
  • the verified host origin and package asset base URL;
  • optional workspace, channel, and conversation IDs;
  • the declared package event publisher; and
  • the current host-authority snapshot.

Optional scope fields are absent when the placement does not supply that scope. Validate required fields at mount and render a useful empty or unavailable state instead of guessing an ID.

Host Authority

A candidate realm can mount before it is allowed to perform host-backed work. Read context.hostAuthority.getSnapshot() and subscribe to changes:

const renderAuthority = () => {
  const ready = context.hostAuthority.getSnapshot();
  container.toggleAttribute('aria-busy', !ready);
};

renderAuthority();
const unsubscribeAuthority = context.hostAuthority.subscribe(renderAuthority);

Do not use a delay as a readiness signal. Disable privileged actions until authority is available, and handle authority being removed while the surface remains visible.

Package Events

Event names and schemas belong in manifest.tap.json. The host scopes delivery to the verified package session.

const unsubscribeRefresh = context.events.subscribe(
  'catalog.refresh-requested',
  async (payload) => {
    if (typeof payload !== 'object' || payload === null) return;
    await refreshCatalog();
  },
);

await context.events.publish('catalog.refreshed', {
  contributionId: context.contributionId,
});

Validate received payloads even when you own both publisher and subscriber. Releases can overlap briefly during activation, and an older compatible release may use a previous payload version.

Package Assets

Use resolvePackageAssetUrl for files declared inside the target graph:

import { resolvePackageAssetUrl } from '@theaiplatform/miniapp-sdk/surface';

const helpUrl = resolvePackageAssetUrl(context, 'assets/help.html');

The helper rejects absolute paths, traversal, encoded separator aliases, query strings, fragments, credentials, and paths outside the target directory.

Cleanup

Make unmount idempotent because cleanup can follow user navigation, release activation, permission changes, or an error boundary. Unmount should:

  1. unsubscribe from package events and host-authority changes;
  2. remove global and DOM event listeners;
  3. abort pending work that your code owns;
  4. stop timers, observers, media, and workers; and
  5. remove rendered content.
let mounted = true;

return {
  unmount() {
    if (!mounted) return;
    mounted = false;
    unsubscribeRefresh();
    unsubscribeAuthority();
    container.replaceChildren();
  },
};

SDK requests already accepted by the host can still settle during cleanup. Ignore their UI result after unmount rather than assuming every request can be cancelled.