Extensions
Which VS Code extension registry answers, and how to ask it — one choice shared by the picker, the image build and code-server.
Why it exists
code-server resolves --install-extension <id> against one registry. If your extension picker
searches a different one, users pick extensions the build cannot install, and nothing tells them
until the extension is silently missing from their editor.
This module puts one entry point in front of both supported protocols:
- Open VSX — the default, and what code-server uses out of the box;
- an
extensionquerygallery — the protocol code-server switches to when handed anEXTENSIONS_GALLERYJSON blob.
The same operator-supplied blob selects the search client and is what gets handed to code-server, at image-build time and at spawn time. An extension the UI can find stays an extension the worker can install.
Everything is a parameter. Nothing is memoized at import time and nothing is read from the environment: every function takes the already-parsed gallery. That is what lets one control plane scope the setting per organization (a database column) and another per process (an environment variable), and lets one process serve several tenants on several registries. No gallery is hardcoded or special-cased: whatever the operator supplies is the whole configuration.
import { parseGallery, searchExtensions, codeServerGallery } from "@spunto/build/extensions"
const gallery = parseGallery(org.extensionsGallery) // null = Open VSX, the default
const hits = await searchExtensions(gallery, "prettier")
const forCodeServer = codeServerGallery(org.extensionsGallery) // same choice, for the workerThe only I/O in this module is outbound HTTP to a public registry, with no platform credential attached. Both clients time out after 8 seconds.
Choosing the registry
ExtensionGallery
The parts of the operator's gallery blob this package reads itself.
Prop
Type
parseGallery(raw)
function parseGallery(raw: string | null | undefined): ExtensionGallery | nullParses an operator's gallery blob, from wherever you keep it. Anything unusable — empty, not JSON,
not an object, no serviceUrl, a serviceUrl that is not an absolute URL — comes back as null,
meaning Open VSX, with a console.warn.
The fallback is the whole point: a blob that cannot be parsed is a blob the picker would be searching Open VSX against, and shipping it to code-server anyway would recreate exactly the split-brain this module exists to prevent.
parseGallery(null) // null → Open VSX
parseGallery('{"serviceUrl":"https://gallery.example.com/_apis/public/gallery/"}')
// → { serviceUrl: "https://gallery.example.com/_apis/public/gallery", itemUrl: undefined,
// productTarget: undefined, forward: '{"serviceUrl":"https://gallery.example.com/_apis/public/gallery"}' }parseGalleryOrThrow(raw)
function parseGalleryOrThrow(raw: string | null | undefined): ExtensionGallery | nullThe same parse, but it throws on an unusable blob ("not valid JSON", "not a JSON object",
'missing "serviceUrl"', '"serviceUrl" is not an absolute URL'). Empty or blank input still
returns null. Use it to validate what you accept on write, so an unusable value never
reaches storage in the first place.
try {
parseGalleryOrThrow(body.extensionsGallery)
} catch (e) {
return c.json({ error: `Invalid gallery: ${(e as Error).message}` }, 400)
}codeServerGallery(raw)
function codeServerGallery(raw: string | null | undefined): string | nullThe EXTENSIONS_GALLERY value to give code-server, or null to leave it alone (Open VSX). Takes
the raw stored value so callers do not have to parse first. The generators in
./script call it for you when given
extensionsGallery.
const env = codeServerGallery(org.extensionsGallery)
if (env) containerEnv.push(`EXTENSIONS_GALLERY=${env}`)Naming the registry in a UI
ExtensionRegistryInfo and registryInfo(gallery)
function registryInfo(gallery: ExtensionGallery | null): ExtensionRegistryInfoWhat a UI needs to name the active registry instead of hardcoding "Open VSX".
Prop
Type
extensionUrl(gallery, id)
function extensionUrl(gallery: ExtensionGallery | null, id: string): string | undefinedRegistry page for one id. On Open VSX: https://open-vsx.org/extension/<publisher>/<name>. On a
gallery: <itemUrl>?itemName=<id>, or undefined when the gallery has no itemUrl.
withRegistryUrls(gallery, suggestions)
function withRegistryUrls(gallery: ExtensionGallery | null, suggestions: ExtensionSuggestion[]): ExtensionSuggestion[]Re-points registry-agnostic suggestions (ids only) at the active registry by setting each url.
This is how the registry-agnostic SUGGESTED_EXTENSIONS from
./catalogs gets its links:
import { SUGGESTED_EXTENSIONS } from "@spunto/build/catalogs"
const defaults = withRegistryUrls(parseGallery(org.extensionsGallery), SUGGESTED_EXTENSIONS)Querying the registry
RegistryOptions
Options that only apply to the default (Open VSX) branch — a gallery carries its own endpoint in the blob.
Prop
Type
Read your own OPEN_VSX_API-style setting at your boundary and pass it down; the package never
reads the environment.
searchExtensions(gallery, query, size?, opts?)
function searchExtensions(
gallery: ExtensionGallery | null,
query: string,
size?: number,
opts?: RegistryOptions,
): Promise<ExtensionSuggestion[]>Full-text search against the active registry, most downloaded / installed first. size defaults to
20 and is clamped to 1–50. Open VSX results drop deprecated extensions; gallery results drop
unpublished ones. Throws RegistryError if the registry cannot be reached or answers an error
status.
const hits = await searchExtensions(gallery, query, 20, { openVsxApiUrl: mirror })lookupExtension(gallery, id, opts?)
function lookupExtension(
gallery: ExtensionGallery | null,
id: string,
opts?: RegistryOptions,
): Promise<ExtensionSuggestion | null>Exact lookup of publisher.name against the active registry. null is a real verdict — "no
such extension" (or an id that does not have the right shape). A RegistryError means you simply
could not ask.
try {
const ext = await lookupExtension(gallery, "esbenp.prettier-vscode")
if (!ext) return c.json({ error: "Unknown extension" }, 422)
} catch (e) {
if (e instanceof RegistryError) return c.json({ error: "Registry unavailable" }, 503)
throw e
}RegistryError
class RegistryError extends Error {}A registry call that could not be completed: network failure, timeout, non-OK status, malformed gallery response. Shared by both clients so callers can tell "we couldn't ask" apart from "no such extension", whichever registry is active.
RegistryError is an upstream failure: surface it as a 503, never a 500 — and never as "this
extension does not exist".
Extension ids
ExtensionSuggestion
An extension as a picker shows it — a curated default, or a search hit from either registry.
Prop
Type
parseExtensionId(id) / isExtensionId(id)
function parseExtensionId(id: string): { publisher: string; name: string } | null
function isExtensionId(id: string): booleanpublisher.extension-name is the identifier code-server resolves. The check is deliberately
permissive on the characters publishers actually use ([A-Za-z0-9._-], starting alphanumeric) and
strict on the shape: the publisher contains no dot, so the first dot splits. Input is trimmed.
Pure — safe to import from a validation schema or a client bundle.
parseExtensionId("ms-python.python") // → { publisher: "ms-python", name: "python" }
parseExtensionId("prettier") // → null
isExtensionId("rust-lang.rust-analyzer") // → trueA shape check only: it says nothing about the id existing on any registry. Use
lookupExtension for that.
The raw clients
For a caller that already knows which protocol it wants. The functions above are the normal way in — reach for these only to bypass the choice deliberately.
openVsx namespace and DEFAULT_OPEN_VSX_API
const DEFAULT_OPEN_VSX_API = "https://open-vsx.org/api"
type OpenVsxOptions = {
/** Base of the Open VSX REST API, for a mirror. Defaults to DEFAULT_OPEN_VSX_API. */
apiUrl?: string
}
openVsx.searchExtensions(query: string, size?: number, opts?: OpenVsxOptions): Promise<ExtensionSuggestion[]>
openVsx.lookupExtension(id: string, opts?: OpenVsxOptions): Promise<ExtensionSuggestion | null>Open VSX's REST API (/-/search, /<namespace>/<name>). Open VSX answers 200 with an error
payload for unknown namespaces; lookupExtension treats that as null, like a 404.
gallery namespace
gallery.searchExtensions(gallery: ExtensionGallery, query: string, size?: number): Promise<ExtensionSuggestion[]>
gallery.lookupExtension(gallery: ExtensionGallery, id: string): Promise<ExtensionSuggestion | null>The extensionquery protocol: a single POST <serviceUrl>/extensionquery taking a filter
document, sent with accept: application/json;api-version=3.0-preview.1 (without it a gallery
answers 400). This is why an Open VSX mirror URL can never double as a gallery setting — the two
protocols do not share a single route. Lookups re-check the returned id case-insensitively, because
the endpoint happily answers with near-misses.
import { gallery as galleryClient, parseGalleryOrThrow } from "@spunto/build/extensions"
const g = parseGalleryOrThrow(raw)!
const hits = await galleryClient.searchExtensions(g, "python", 10)