Deno --allow-net=api.sume.com is not enough to save a finished video
Sume's content URL answers 302 to a file host. A Deno script with a narrow --allow-net fails at the second fetch: read the redirect host, then allow it.

Deno runs a script with no network access until you grant it, and it can limit that access to named hosts. That is a good default for a script that holds an API key, and it has one catch with Sume's video API. A finished job lists unsigned_urls, and the first entry is a /content?index=0 URL on the API that answers 302 with the location of the file. If you allow only the API host, the redirect target is a second host that the permission flag does not cover.
Deno's own documentation describes --allow-net as taking a list of hosts, and says a script with no flag has no network access. This post shows what happens, and a way to handle it without allowing every host.
The script
It submits the cheapest Gemini Omni Flash job, 3 seconds at 360p, which Sume prices at $0.1125. It polls the polling_url from the 202, then requests the content URL with redirect: "manual" so it can read the location header itself. It prints the host, compares it with FILE_HOST, and only then fetches the file, with no Authorization header.
const base = Deno.env.get("SUME_API_BASE") ?? "https://api.sume.com";
const auth = { Authorization: `Bearer ${Deno.env.get("SUME_API_KEY")}` };
const wait = Number(Deno.env.get("POLL_SECONDS") ?? 30) * 1000;
const submit = await fetch(`${base}/v1/videos`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": "deno-omni-0001" },
body: JSON.stringify({ model: "gemini-omni-flash-1.1", prompt: "A lighthouse in a storm",
duration: 3, resolution: "360p", aspect_ratio: "16:9" }),
});
if (submit.status !== 202) throw new Error(await submit.text());
const { polling_url } = await submit.json();
let job;
do {
await new Promise((r) => setTimeout(r, wait));
job = await (await fetch(polling_url, { headers: auth })).json();
if (job.status === "failed" || job.status === "cancelled") throw new Error(job.status);
} while (job.status !== "completed");
const hop = await fetch(job.unsigned_urls[0], { headers: auth, redirect: "manual" });
const target = new URL(hop.headers.get("location")!);
console.log("file host:", target.host);
if (target.host !== Deno.env.get("FILE_HOST")) throw new Error("unexpected file host");
const file = await fetch(target);
await Deno.writeFile("out.mp4", new Uint8Array(await file.arrayBuffer()));
console.log("cost", job.usage.cost);What I saw
I ran it against a local stand-in with the API on one port and the file on another, with a short poll delay. Run with --allow-net for the API host only, it printed the file host and then stopped with a NotCapable error naming the second host. With both hosts allowed it saved the file and printed cost 0.1125. With a wrong FILE_HOST it refused to fetch at all. The stand-in did not receive the key on the second request.
The check is not specific to the stand-in. The Sume guide documents the 302 but not the address it points to, so the script learns the host at run time. The first run prints it, you confirm it is what you expect, and you set FILE_HOST and extend the flag.
Flags for the command line
Each flag grants one thing and no more.
| Flag | Grants | Needed because |
|---|---|---|
| --allow-net=api.sume.com,FILE_HOST | Two named hosts | Submit and poll on the API, then the file |
| --allow-env | Environment reads | The key, base URL and poll delay |
| --allow-write=out.mp4 | One output path | The saved clip |
Why not allow everything
A bare --allow-net would make the script work on the first try, and it would also let any dependency or edited line send your key anywhere. Pinning two hosts is a few seconds of setup that keeps the API key where it belongs. The same reasoning applies when you pin the model: read the catalog at GET /v1/videos/models before you hard-code an id, as in /blog/video-catalog-input-matrix-python-frame-and-reference-fields.
Sources
Related posts
More in Developers
- Derive a Sume idempotency key from tenant, order, Format slug, version
Hash stable ids, not a random UUID, so a retry reuses the key. A failed run needs a new key, and a changed body with the same key returns 409.
- Hash the avatar payload into your Idempotency-Key, adopt the 409 job
Hash the avatar request into the Idempotency-Key: a retry returns the same job (idempotency_hit true); a 409 names the job holding the key.
- Detect a clip's language before dubbing with Sume STT
Run Sume STT with no language hint and read language_code and language_probability from the result. A short Python check that gates a dub on a confident answer.
- Detect Sume API changes in CI: diff the live OpenAPI document
Pull https://api.sume.com/reference/json in CI, reduce it to sorted method and path lines with jq, and fail the build when the route list changes.
Written by Sume