SpuntoSpunto/@spunto/build

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 extensionquery gallery — the protocol code-server switches to when handed an EXTENSIONS_GALLERY JSON 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 worker

The 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 | null

Parses 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 | null

The 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 | null

The 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): ExtensionRegistryInfo

What 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 | undefined

Registry 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): boolean

publisher.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") // → true

A 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.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)
Edit on GitHub

On this page