> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Egress

> The HTTPS origin allowlist that ctx.fetch enforces before opening a socket.

Egress is an origin-only HTTPS allowlist. `ctx.fetch` refuses anything outside the list synchronously, before it opens a socket — including redirect targets. It is a security boundary, not a convenience filter.

## Declaring egress

List only the origins you call:

```ts theme={null}
export const integration = defineIntegration({
  slug: 'acme-payments',
  // ...
  egress: [
    'https://api.acme.example', // exact origin
    'https://*.acme-cdn.example', // single leading-label wildcard
    'https://api.acme.example:8443', // explicit port
  ],
});
```

Scheme, host, and optional port only. No path, query, fragment, credentials, or `http:`. Wildcards are a single leading `*.`.

## Validation

`defineIntegration()` rejects invalid entries with the offending entry quoted — the message begins `Egress entry "<entry>" is invalid. Entries are https origins only`:

```ts theme={null}
// ❌ Schema validation error — rejected at import time
egress: [
  'http://api.example', // http: not allowed
  'https://api.example/v1', // path not allowed
  'https://*.*.example', // multiple wildcards
  'https://api.example:not-port', // invalid port
];
```

## Runtime behavior

Denied calls throw `EgressDeniedError` (from `@ekanos/sdk/context`) synchronously:

```ts theme={null}
import { EgressDeniedError } from '@ekanos/sdk/context';

try {
  // Inside egress list
  await ctx.fetch('https://api.acme.example/v1/data'); // ✅ OK

  // Outside egress list
  await ctx.fetch('https://evil.example/steal'); // ❌ Throws synchronously
} catch (cause) {
  if (cause instanceof EgressDeniedError) {
    ctx.logger.error({ origin: cause.origin }, 'Egress denied.');
    return { error: 'Could not reach the API.' };
  }
  throw cause;
}
```

**Redirect targets are checked too.** If `https://api.acme.example/v1/data` redirects to `https://evil.example`, the fetch is denied.

`EgressDeniedError` carries `code: 'EGRESS_DENIED'`, the refused `origin`, and an optional `reason`. The host also refuses an allowlisted name that resolves into a private, loopback, link-local, or other non-public range, on the first request or any redirect hop. Those denials are found after DNS, so they reject the returned promise instead of throwing synchronously, and `reason` names the category (never the address).

## Common patterns

### Single provider

Most integrations call one API:

```ts theme={null}
egress: ['https://api.acme.example'];
```

### Multi-region

If you need multiple regions:

```ts theme={null}
egress: ['https://us-api.acme.example', 'https://eu-api.acme.example'];
```

### Wildcard for CDN

If assets come from a wildcard subdomain:

```ts theme={null}
egress: [
  'https://api.acme.example',
  'https://*.assets.acme.example', // any subdomain of assets.acme.example, not the apex itself
];
```

### Explicit ports

If the API uses a non-standard port:

```ts theme={null}
egress: ['https://api.acme.example:8443'];
```

But `https://api.acme.example:443` is redundant — 443 is the HTTPS default.

## Promotion review

Widening this list is a **security-posture change** that a human reviewer looks at during [promotion](/sdk/surfaces/proposes). The reviewer reads your declaration against what your handlers actually do, so:

**Declare only origins you call.** Do not add `https://*.example` "just in case" — the reviewer will question it, and it weakens your security posture.

**If you call a new origin, add it to the declaration.** `ctx.fetch` denies anything not listed.

## Error handling

```ts theme={null}
const apiKey = await ctx.secrets.get('acme_api_key');
if (!apiKey) {
  return { error: 'API key not configured.' };
}

try {
  const response = await ctx.fetch('https://api.acme.example/v1/data', {
    headers: { authorization: `Bearer ${apiKey}` },
  });

  if (!response.ok) {
    ctx.logger.error({ status: response.status }, 'API request failed.');
    return { error: `Acme returned ${response.status}.` };
  }

  return response.json();
} catch (cause) {
  if (cause instanceof EgressDeniedError) {
    // This should not happen if your declaration is correct
    ctx.logger.error({}, 'Egress denied — declaration missing origin.');
    return { error: 'Internal error.' };
  }
  throw cause;
}
```

## Special case: OAuth

Both OAuth endpoints must be in your egress list. `defineIntegration()` rejects the definition otherwise and tells you which origin to add:

```ts theme={null}
export const integration = defineIntegration({
  slug: 'acme-oauth',
  oauth: {
    provider: {
      authorizationUrl: 'https://connect.acme.example/oauth/authorize',
      tokenUrl: 'https://connect.acme.example/oauth/token',
      // ...
    },
  },
  egress: [
    'https://connect.acme.example', // Covers both endpoints
  ],
});
```

The code exchange runs through your declared egress, so an origin the reviewer never saw is an origin the exchange must not reach.

## Next steps

* [Secrets](/sdk/surfaces/secrets) — credential management
* [Tools](/sdk/surfaces/tools) — use ctx.fetch in a tool
* [OAuth](/sdk/surfaces/oauth) — declare and handle OAuth


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.