Public
An admin panel for val-scoped blob storage
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

scoped-blob-viewer

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.

Standalone: this val's own panel

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.

Routed: mounted alongside other integrations

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.

Options

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 });

Requirement when auth is on

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.

Why you can't browse arbitrary vals from one panel

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.

Files

  • mod.ts — public entry point; a JSX-free re-export barrel (keeps the stable import path and type-checks with plain deno check).
  • app.tsx — the blobAdmin() implementation.
  • main.ts — this val's own standalone consumer.
  • mod_test.ts — dual-mode contract tests (deno task check).