Skip to content

Network and credentials API

Apps have no network access of their own. client.network lets the user configure one endpoint per named slot, keeps the API key outside your app, and posts JSON to exactly that endpoint.

Availability

Requires the system.network permission and the native desktop — the browser preview has no credential store, so the service reports unavailable there. No workspace connection is needed: this is about the internet, not the host you are connected to.

Interface

ts
interface AppConnection {
  endpoint: string; // what the user configured
  revision: string; // pin this when sending a request
  hasKey: boolean; // whether a key is stored — never the key itself
  remembered: boolean; // saved in the OS credential store, or session-only
}

interface AppHTTPResponse {
  status: number;
  contentType: string;
  read(signal?: AbortSignal): Promise<Uint8Array | null>; // null ends the body
  close(): Promise<void>;
}

interface AppNetworkAPI {
  profile(slot: string, signal?: AbortSignal): Promise<AppConnection | null>;
  configure(
    options: { slot: string; suggestedEndpoint?: string },
    signal?: AbortSignal,
  ): Promise<AppConnection | null>;
  forget(slot: string, signal?: AbortSignal): Promise<void>;
  postJSON(
    options: { slot: string; revision: string; body: Json },
    signal?: AbortSignal,
  ): Promise<AppHTTPResponse>;
}

A slot is your own name for a connection — "model", "webhook" — matching ^[a-zA-Z0-9_-]{1,64}$. One app can hold several.

configure() opens the desktop's trusted connection dialog. The user enters the endpoint and, optionally, the key; the key goes to the operating system's credential store (Windows Credential Manager, macOS Keychain, Linux Secret Service) or is kept for the session only. Your app receives the endpoint, a revision and the two booleans, and never the secret. The dialog resolves to null if the user dismisses it.

Endpoints must be complete http:// or https:// URLs of at most 2048 characters, with no embedded credentials, query string or fragment. Requests are POSTs with a JSON body to exactly that URL: no other method, no extra headers, no redirects followed.

Examples

Configure on demand, then send a request:

ts
async function connection(): Promise<AppConnection> {
  const existing = await client.network.profile("model");
  if (existing?.hasKey) return existing;
  const configured = await client.network.configure({
    slot: "model",
    suggestedEndpoint: "https://api.example.com/v1/messages",
  });
  if (!configured) throw new Error("No endpoint configured.");
  return configured;
}

async function ask(question: string): Promise<string> {
  const { revision } = await connection();
  const response = await client.network.postJSON({
    slot: "model",
    revision,
    body: { prompt: question },
  });
  try {
    if (response.status >= 400)
      throw new Error(`Endpoint returned ${response.status}`);
    const decoder = new TextDecoder();
    let text = "";
    for (;;) {
      const chunk = await response.read();
      if (chunk === null) break;
      text += decoder.decode(chunk, { stream: true });
    }
    return text;
  } finally {
    await response.close();
  }
}

The body arrives in chunks of at most 32 KiB, so a streaming endpoint can be consumed as it arrives. Always close() in a finally.

Let users disconnect:

ts
await client.network.forget("model"); // removes the stored key and endpoint for this slot

Show status honestly in your interface: hasKey tells you whether a key is stored, and remembered whether it survives a restart.

Errors

CodeMessageMeaning
invalidInvalid connection slot.The slot name breaks the pattern.
invalidJSON request exceeds 3 MiB.Body too large.
invalidDuplicate HTTP request.A request with that identity is already running.
busyClose an existing HTTP request first.More than four concurrent requests.
busyWait for the current response read.Two reads on one response.
busyFinish the open connection dialog first.configure() called twice.
closedHTTP response closed.Used after close().
abortedHTTP request canceled. / Connection setup canceled.A signal fired, or the user cancelled.

Errors raised by the desktop while saving a credential are shown in the dialog rather than to your app — for example when the OS credential store is locked, or when an endpoint changes and the key must be entered again.

Lifecycle and permissions

Connections belong to the installation of your app, like storage: remembered connections survive updates and restarts; session-only connections do not survive restarting the desktop. Removal retires the installation identity, so reinstalling cannot access its old connections. Removal does not itself delete the old credential-store entries; use forget(slot) before removal to delete a saved connection. Every window of your app sees the same slots.

The revision pins the endpoint and credential version that a request uses. If the user reconfigures the slot, old revisions stop working — call profile() again and use the fresh revision rather than caching one indefinitely.

system.network grants nothing else: it does not give access to the connected host's files or console, and it cannot reach arbitrary URLs. Responses are capped at 16 MiB.

ShellCanvas documentation