An admin panel for val-scoped blob storage (std/blob/main.ts) — list,
view, edit (text/JSON), preview images, upload, download, rename, copy, and
delete. Gated behind "Log in with Val Town" and restricted to an owner.
It is an integrations
contract integration, so blobAdmin() returns a callable Integration: the
same value works both as a standalone HTTP val and mounted under a router
alongside other integrations.
main.ts mounts the panel for this val's scoped storage — the panel is
the val's HTTP export and runs its own OAuth. Visit the val's endpoint and log
in.
Scoped blob storage is bound to whichever val's runtime is executing the code, not to where the source lives. So importing this panel into another val makes it read that val's storage automatically — no tokens, no registry:
import { blobAdmin } from "https://esm.town/v/nbbaier/scoped-blob-viewer/mod.ts";
export default blobAdmin({ owner: "nbbaier" });
That one HTTP val is now a full admin panel over the host val's scoped blobs.
Register it with a createRouter and it mounts under /blobs/, sharing one
OAuth gate with every other integration:
import { createRouter } from "https://esm.town/v/nbbaier/integrations/mod.ts";
import { blobAdmin } from "https://esm.town/v/nbbaier/scoped-blob-viewer/mod.ts";
export default createRouter({
integrations: [blobAdmin({})],
});
The router owns authentication; owner remains this panel's own
authorization layer, checked against the router-provided user. A user not on
the owner allowlist gets 403 — including the null user of an ungated router,
which can't satisfy an allowlist.
blobAdmin({
owner: "nbbaier", // string | string[] allowed in; omit = any logged-in VT user
auth: true, // default true (Val Town OAuth). false = no auth, no gating.
title: "My Val Blobs", // header / tab title
});
The OAuth gate — this val's own when standalone, the router's when routed —
needs an OAUTH_STATE_ENCRYPTION_KEY env var (any random 32-byte hex string) to
encrypt session cookies. Generate one with openssl rand -hex 32. The
integration declares it as requiredEnv, so a router's directory page badges it
when unset.
Each val's scoped storage is an isolated namespace reached only with a
project-scoped API token for that specific val (verified: the platform
rejects account tokens for scoped blobs, and v2/blob has no val/project
parameter). There is no API to mint those tokens programmatically. The import
approach above is the clean workaround — the panel runs inside the target val,
so it already holds that val's scope.
mod.ts— public entry point; a JSX-free re-export barrel (keeps the stable import path and type-checks with plaindeno check).app.tsx— theblobAdmin()implementation.main.ts— this val's own standalone consumer.mod_test.ts— dual-mode contract tests (deno task check).