An application can call another application's API **for a workspace**, and
optionally **for one of its members**, without anyone pasting a key. Connect
issues a five-minute token bound to the provider (`aud`), the workspace and
the scopes the workspace consented to; the call itself goes straight to the
provider, which checks the token locally.

Two things are configured in Console, not in code: the provider's
`exposedApi` (its audience, scopes and events) and the consumer's `consumes`
list (the ceiling on what it may ask for). A workspace admin connects the
pair once; first-party pairs are connected automatically.

## Calling (the consumer)

Your API key needs the `applications:call` scope.

```ts
import { ConnectClient } from "@cosmoops/connect-sdk/server";

const connect = new ConnectClient({ apiKey: process.env.CONNECT_API_KEY! });

// Background work - a sync, a cron, a webhook: an app-only token.
const messaging = connect.applications.fetch("https://messaging.cosmoops.com", {
  workspaceId,
  scope: ["messaging:chat.read"],
});
const res = await messaging("/api/conversations?contact=c_1");
```

For a call a member started, pass their session token instead of the
workspace. Connect runs its entry gate on **both** applications, so a member
without a seat on the provider cannot reach it through you, and only scopes
the provider marked `delegable` may be asked for.

```ts
const messaging = connect.applications.fetch("https://messaging.cosmoops.com", {
  subjectToken: await getConnectSessionToken(),
});
```

`applications.fetch` refuses any URL outside the provider's origin - the
token is bound to that audience. Tokens are cached per (provider, workspace or
member, scope set) until a minute before they expire, one request at a time,
so a burst asks Connect once. A refusal is never cached. To hold the token
yourself, `applications.token({ resource, workspaceId, scope })` and
`applications.tokenForMember({ resource, subjectToken, scope })` return it
directly.

A refusal arrives as a `ConnectError` whose `code` is an RFC 6749 error -
`access_denied` (no connection, or no subscription to both applications),
`invalid_scope`, `invalid_target` (no application exposes that audience) and
so on. `isConnectTokenRefusal(error)` tells those apart from a network fault.

## Being called (the provider)

```ts
import { ConnectTokenError, verifyConnectToken } from "@cosmoops/connect-sdk/next";

export async function GET(request: Request) {
  try {
    const caller = await verifyConnectToken(request, {
      audience: "https://messaging.cosmoops.com",
      scopes: ["messaging:chat.read"],
    });
    // caller.workspaceId, caller.applicationId, caller.memberId (null when app-only)
    return Response.json(await conversationsFor(caller.workspaceId));
  } catch (error) {
    if (error instanceof ConnectTokenError) return error.toResponse();
    throw error;
  }
}
```

Verification is local: the only network read is Connect's
`/.well-known/jwks.json`, cached for ten minutes and refetched early - at most
every 30 seconds - when a token names a key it has not seen. It accepts ES256
only, checks `typ`, the signature, `iss`, `aud` exactly, `exp`/`iat` with 30
seconds of leeway and every required scope. `toResponse()` answers 401 or 403
with the matching `WWW-Authenticate` header, or 503 when Connect's keys could
not be read. `./next`'s `verifyConnectToken` follows `CONNECT_APP_URL`;
`./server`'s takes `connectUrl`.

Connect decides whether the caller may reach your API for this workspace with
these scopes. What `memberId` may do inside it is still yours to decide.

## Events

A provider publishes an event declared in its `exposedApi.events` (API key
scope `events:publish`):

```ts
await connect.events.publish({
  workspaceId,
  type: "messaging.chat.lead_captured",
  data: { contactId },
  idempotencyKey: `lead-${chatId}`,
});
```

Connect delivers it to every connected application holding the event's
`requiredScope`, as an `application.event` webhook, signed and retried like
every other. Receive it with an `"application.event"` handler on
`createConnectWebhookHandler`, or on a dedicated route:

```ts
import { verifyApplicationEvent } from "@cosmoops/connect-sdk/server";

const event = verifyApplicationEvent(await request.text(), request.headers.get("connect-signature"), secret);
// event.id (deduplicate on it), event.type, event.source, event.workspaceId, event.data
```