Contract and use
Navigation, sign-in and action handoff are different capabilities. A tab request is not a loaded page; a completed browser flow is not an authenticated backend session; an intent receipt is not independent proof of delivery. These excerpts preserve current SDK declarations, including optional methods and target differences.
Request navigation without claiming a load
Use client.capability('links') or await client.require('links'); there is no client.links getter in this SDK. Package IDs such as uk.capp.sidecar.links belong to selection, not capability discovery.
import type { CappClient } from '@capp/app-sdk';
export async function requestDeveloperHelp(client: CappClient): Promise<string> {
const links = await client.require('links', 5000);
if (typeof links.open !== 'function') throw new Error('In-app navigation is unavailable');
await Promise.resolve(links.open('https://c-app.uk/developer-docs/', { mode: 'tab' }));
return 'Help tab requested';
}Invoke this from a button and guard against duplicate pending clicks. A failure may be uncertain; offer a separate ordinary HTTPS link rather than automatically launching another destination. The selected policy can ask, use tabs, require the default browser or disable handling. mode: 'tab' is a request, not authority to override that policy.
Saved links belong to this app on this device. Inspect status().storage: session does not promise persistence, and damaged/unavailable must not be presented as a successful saved collection. Export can include private browsing destinations; review before sharing. Hiding a view does not delete bookmarks or close other features' tabs.
Complete sign-in, then re-read your account
auth.open, start, current, lease, user and status vary by runtime. A status() parameter may be a flow ticket; do not assume it always means “who is signed in?” Prefer the Android Auth guide for the selected build mode.
This application-owned helper uses open() and then your existing authenticated account endpoint. It does not infer identity from an arbitrary flow result or copy credentials into JavaScript.
import type { CappClient } from '@capp/app-sdk';
export async function signInAndReadAccount<T>(
client: CappClient,
readAuthenticatedAccount: () => Promise<T>,
): Promise<T> {
const auth = await client.require('auth', 5000);
if (typeof auth.open !== 'function') throw new Error('This runtime has no supported sign-in flow');
await auth.open({ embed: true, surface: 'sidecar' });
return await readAuthenticatedAccount();
}The callback must reject when its server reports an unauthenticated user; its name is not validation. Keep one sign-in flight at a time. Closing/cancelling a flow is not approval to keep polling forever. A public authenticated indicator is suitable for UI state, not as backend authorization for a write or purchase.
Installation trust and logout
Use await client.security.installIdentity() when an authenticated enrolment flow needs public key material. Verify the result protocol and identity fields; do not supply another product's appId. That public key is not a user session, and possession of an enrolment payload alone must not let a backend accept an arbitrary installation.
The host retains reusable credentials and the private signer. A local clear/logout does not necessarily revoke a remote session, subscription, wallet authority or another device's installation. Your product's logout workflow must define which local and remote state it actually clears. Preserve accurate UI wording when a remote revocation is unconfirmed.
Intent handoff is a separate contract
Use client.intent for supported app-declared handoffs. Keep the original scope/request identity with any returned receipt. An empty object or merely returned promise is not a verified result. current(scope) and receipts() inspect retained outcomes; clear() removes local records, not an external action. Do not retry a purchase or valuable mutation just because the hosted view closed without a reply.
Troubleshooting
| Symptom | Next check |
|---|---|
client.links is missing | Use the capability lookup, not a nonexistent convenience getter. |
| Navigation requested but content blank | Inspect target support, selected link policy and destination embedding restrictions. |
| Sign-in flow returns but protected request is 401 | Refresh account through the existing transport/session path; do not fabricate a bearer. |
| Public identity exists but gateway rejects it | Check authenticated enrolment, expected app/site/binding, revocation and server trust storage. |
| Logout clears UI but another session works | Local clearing and remote revocation have different scope. |
| Newer runtime method absent from these types | Check the exact installed component version; do not advertise it as portable SDK support. |
CappSavedLink
export type CappSavedLink = {
id: string;
url: string;
title: string;
folder: string;
notes: string;
pinned: boolean;
};CappLinksTab
export type CappLinksTab = {
id: string;
title?: string;
kind?: 'app' | 'blank' | 'file' | 'web';
url: string | null;
active: boolean;
canBack: boolean;
canForward: boolean;
};CappLinksStatus
export type CappLinksStatus = {
layout?: 'window-tabs';
activeTab?: string;
nativeBrowsing?: boolean;
linkHandling: 'ask' | 'tab' | 'browser' | 'off';
tabCount?: number;
version: string;
count: number;
limit: number;
storage: 'ready' | 'session' | 'unavailable' | 'damaged';
error: string | null;
open?: boolean;
pageRequested?: boolean;
};CappLinksBridge
export type CappLinksBridge = {
version: '1';
status(): CappLinksStatus;
list(options?: {
query?: string;
folder?: string | null;
}): CappSavedLink[];
save(input: {
url: string;
} & Partial<CappSavedLink>): CappSavedLink;
remove(id: string): {
removed: true;
};
import(json: string): {
added: number;
skipped: number;
};
export(): string;
refresh(): CappLinksStatus;
show(): {
open: boolean;
};
hide(): {
open: boolean;
};
view(url: string): {
requested: boolean;
mode: 'embedded' | 'browser-required' | 'native' | 'file';
};
openExternal(url: string): Promise<unknown>;
open(url: string, options?: {
mode?: 'ask' | 'tab' | 'browser';
}): unknown;
newTab(url?: string | null): {
tabId: string;
};
tabs(): CappLinksTab[];
selectTab(id: string): {
tabId: string;
};
closeTab(id: string): {
closed: true;
};
};CappAuthBridge
export type CappAuthBridge = {
open?: (payload?: Record<string, unknown>) => Promise<CappNativeAuthState | Record<string, unknown> | null>;
start?: (payload?: Record<string, unknown>) => Promise<unknown>;
status?: (ticketOrPayload?: unknown) => Promise<unknown>;
current?: () => CappNativeAuthState | Record<string, unknown> | null | Promise<CappNativeAuthState | Record<string, unknown> | null>;
lease?: () => CappNativeAuthState | Record<string, unknown> | null | Promise<CappNativeAuthState | Record<string, unknown> | null>;
clear?: () => unknown;
logout?: () => Promise<unknown>;
user?: () => Promise<unknown>;
};CappIntentBridge
export type CappIntentBridge = {
open?: (payload?: Record<string, unknown>) => Promise<unknown>;
start?: (payload?: Record<string, unknown>) => Promise<unknown>;
status?: (ticketOrPayload?: unknown) => Promise<unknown>;
current?: (key?: string) => unknown;
receipts?: () => Record<string, unknown>;
clear?: () => unknown;
};CappNativeAuthState
export type CappNativeAuthState = {
protocol: 'capp.native-auth-state.v1' | string;
ok?: boolean;
authenticated: boolean;
lease?: Record<string, unknown> | null;
user?: Record<string, unknown> | null;
storageProtection?: string | null;
hardwareBacked?: boolean | null;
[key: string]: unknown;
};CappInstallIdentity
export type CappInstallIdentity = {
protocol: 'capp.install-identity.v1' | string;
installId: string;
keyId: string;
algorithm: 'Ed25519' | string;
appId?: string | null;
siteId?: string | null;
createdAt?: string | null;
storageProtection?: string | null;
hardwareBacked?: boolean | null;
publicKeyJwk: {
kty: 'OKP' | string;
crv: 'Ed25519' | string;
x: string;
kid?: string;
[key: string]: unknown;
};
[key: string]: unknown;
};