External APIs
Handler-based action() functions can call external HTTP APIs using ctx.fetch(). All outbound requests are restricted to an explicit allowlist declared in lumio.config.json.
Declaring the egress allowlist
Add an egress section to lumio.config.json:
{
"egress": {
"allowHosts": [
"site.api.espn.com",
"api.openweathermap.org",
"*.scdn.co"
]
}
}
Only listed hosts are reachable from ctx.fetch(). Requests to unlisted hosts are rejected before the network layer.
Using ctx.fetch
ctx.fetch has the same interface as the standard fetch() API:
import { action, v } from "@zaflun/lumio-sdk/server";
export const getWeather = action({
args: { city: v.string() },
handler: async (ctx, args) => {
const apiKey = ctx.secrets.get("OPENWEATHER_API_KEY");
const res = await ctx.fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(args.city)}&appid=${apiKey}&units=metric`
);
if (!res.ok) {
throw new Error(`Weather API error: ${res.status} ${res.statusText}`);
}
const data = await res.json();
return {
temperature: data.main.temp,
description: data.weather[0].description,
city: data.name,
};
},
});
Wildcard support
Wildcards (*) match any subdomain at that level only:
| Pattern | Matches | Does not match |
|---|---|---|
*.espn.com | api.espn.com, site.espn.com | espn.com, a.b.espn.com |
*.scdn.co | i.scdn.co, p.scdn.co | scdn.co |
api.example.com | api.example.com exactly | sub.api.example.com |
Security constraints
| Rule | Description |
|---|---|
| HTTPS only | All ctx.fetch() requests must use https:// — HTTP is blocked |
| No private IPs | Requests to RFC 1918 ranges (10.x, 172.16.x, 192.168.x) and localhost are blocked |
| Allowlist required | A ctx.fetch() to any host not in allowHosts throws immediately |
| 10 requests/invocation | Maximum 10 ctx.fetch() calls per action invocation |
| 30-second timeout | Each request times out after 30 seconds |
Setting ports
By default, only port 443 (HTTPS) is allowed. To use a non-standard port, declare it explicitly:
{
"egress": {
"allowHosts": ["my-server.example.com"],
"allowPorts": [8443, 9443]
}
}
Review process
The egress allowlist is reviewed during the admin approval process. Reviewers check:
- Are the listed hosts appropriate for the extension's stated purpose?
- Are wildcards too broad?
- Are the hosts known, reputable services?
Requests to social engineering domains, telemetry/tracking hosts, or hosts unrelated to the extension's function will result in rejection.
Example: Sports scoreboard
{
"extensionId": "...",
"name": "Sports Scoreboard",
"server": true,
"egress": {
"allowHosts": [
"site.api.espn.com",
"cdn.espn.com"
]
}
}
// server/functions.ts
export const getScoreboard = action({
args: { sport: v.string(), league: v.string() },
handler: async (ctx, args) => {
const res = await ctx.fetch(
`https://site.api.espn.com/apis/site/v2/sports/${args.sport}/${args.league}/scoreboard`
);
const data = await res.json();
return data.events ?? [];
},
});