From f2b56d8dde791aae8e27e9ceb6d2bbb0295b2e2d Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Fri, 11 Sep 2026 12:42:25 +0200 Subject: [PATCH] refactor(shared): drop private-repo access from the registry CLI Once databricks/appkit-registry is public, the token/authenticated GitHub Contents API path is dead flexibility: items are fetchable from raw.githubusercontent.com with no auth. Remove it and fetch the public raw URL unconditionally. Removes `resolveToken`, `RegistryToken`, `TOKEN_ENV_VARS`, `registryAuthHeaders`, and the `*_API_*` URL constants; drops the `token` parameter from `resolveItems`/`fetchRegistryItem`/`fetchVerifiedNames` and the token-related branches, messages, and help text in add/list/info. Co-authored-by: Isaac Signed-off-by: MarioCadenas --- .../src/cli/commands/registry/add.test.ts | 20 +++---- .../shared/src/cli/commands/registry/add.ts | 28 ++-------- .../src/cli/commands/registry/client.ts | 56 +++---------------- .../src/cli/commands/registry/constants.ts | 47 +--------------- .../shared/src/cli/commands/registry/info.ts | 4 +- .../shared/src/cli/commands/registry/list.ts | 30 +++------- 6 files changed, 32 insertions(+), 153 deletions(-) diff --git a/packages/shared/src/cli/commands/registry/add.test.ts b/packages/shared/src/cli/commands/registry/add.test.ts index 80b85505f..1c37a71ac 100644 --- a/packages/shared/src/cli/commands/registry/add.test.ts +++ b/packages/shared/src/cli/commands/registry/add.test.ts @@ -90,7 +90,7 @@ function resourceRow(type: string): ResourceRequirementRow { describe("resolveItems", () => { it("returns requested items in order", async () => { const fetch = vi.fn(async (name: string) => item(name)); - const result = await resolveItems(["a", "b"], null, fetch); + const result = await resolveItems(["a", "b"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b"]); }); @@ -101,7 +101,7 @@ describe("resolveItems", () => { c: item("c"), }; const fetch = vi.fn(async (name: string) => graph[name]); - const result = await resolveItems(["a"], null, fetch); + const result = await resolveItems(["a"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b", "c"]); }); @@ -112,7 +112,7 @@ describe("resolveItems", () => { shared: item("shared"), }; const fetch = vi.fn(async (name: string) => graph[name]); - const result = await resolveItems(["a", "b"], null, fetch); + const result = await resolveItems(["a", "b"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b", "shared"]); expect(fetch).toHaveBeenCalledTimes(3); }); @@ -123,7 +123,7 @@ describe("resolveItems", () => { b: item("b", { registryDependencies: ["a"] }), }; const fetch = vi.fn(async (name: string) => graph[name]); - const result = await resolveItems(["a"], null, fetch); + const result = await resolveItems(["a"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b"]); expect(fetch).toHaveBeenCalledTimes(2); }); @@ -134,7 +134,7 @@ describe("resolveItems", () => { b: item("b"), }; const fetch = vi.fn(async (name: string) => graph[name]); - const result = await resolveItems(["a"], null, fetch); + const result = await resolveItems(["a"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b"]); }); @@ -146,7 +146,7 @@ describe("resolveItems", () => { ...item("analytics"), files: [], })); - const result = await resolveItems(["evil"], null, fetch); + const result = await resolveItems(["evil"], fetch); expect(result.map((i) => i.name)).toEqual(["evil"]); }); @@ -155,7 +155,7 @@ describe("resolveItems", () => { it("rejects a top-level ref that is not a plain slug (no fetch)", async () => { const fetch = vi.fn(); await expect( - resolveItems(["../../attacker/repo/payload"], null, fetch), + resolveItems(["../../attacker/repo/payload"], fetch), ).rejects.toThrow(/Invalid registry item name/); expect(fetch).not.toHaveBeenCalled(); }); @@ -165,7 +165,7 @@ describe("resolveItems", () => { a: item("a", { registryDependencies: ["../../evil"] }), }; const fetch = vi.fn(async (name: string) => graph[name]); - await expect(resolveItems(["a"], null, fetch)).rejects.toThrow( + await expect(resolveItems(["a"], fetch)).rejects.toThrow( /Invalid registry item name/, ); }); @@ -180,7 +180,7 @@ describe("resolveItems", () => { active--; return item(name); }); - const result = await resolveItems(["a", "b", "c"], null, fetch); + const result = await resolveItems(["a", "b", "c"], fetch); expect(result.map((i) => i.name)).toEqual(["a", "b", "c"]); expect(maxActive).toBeGreaterThan(1); // ran in parallel, not one-at-a-time }); @@ -216,7 +216,7 @@ describe("partitionVerified", () => { "evil-dep": item("evil-dep"), }; const fetch = vi.fn(async (name: string) => graph[name]); - const items = await resolveItems(["verified-a"], null, fetch); + const items = await resolveItems(["verified-a"], fetch); const res = partitionVerified( items.map((i) => i.name), new Set(["verified-a"]), // only the top-level item is verified diff --git a/packages/shared/src/cli/commands/registry/add.ts b/packages/shared/src/cli/commands/registry/add.ts index 9db613732..f17e97e3b 100644 --- a/packages/shared/src/cli/commands/registry/add.ts +++ b/packages/shared/src/cli/commands/registry/add.ts @@ -24,12 +24,7 @@ import { validateBundle, writeConfig, } from "./config-writer"; -import { - JS_IDENTIFIER, - REGISTRY_REPO, - type RegistryToken, - resolveToken, -} from "./constants"; +import { JS_IDENTIFIER, REGISTRY_REPO } from "./constants"; import { parseEnv } from "./env-reconcile"; import { extractRequirements, @@ -277,11 +272,7 @@ interface PluginSummary { */ export async function resolveItems( names: string[], - token: RegistryToken | null, - fetchItem: ( - name: string, - token: RegistryToken | null, - ) => Promise = fetchRegistryItem, + fetchItem: (name: string) => Promise = fetchRegistryItem, ): Promise { const seen = new Set(); const ordered: RegistryItem[] = []; @@ -307,7 +298,7 @@ export async function resolveItems( while (level.length > 0) { const items = await Promise.all( level.map(async (key) => { - const item = await fetchItem(key, token); + const item = await fetchItem(key); // Pin to the fetch key: the body's self-reported `name` is untrusted // and could claim a verified name to slip past the gate. The key is the // trustworthy identity the index keys `verified` on. @@ -370,20 +361,14 @@ export function partitionVerified( async function runAdd(refs: string[], opts: AddOptions): Promise { const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd(); - const token = resolveToken(); - if (token) { - console.log( - `Using ${token.envName} to fetch from ${REGISTRY_REPO} (private).`, - ); - } // Resolve the full graph and fetch the verified index concurrently (two // independent round-trips). Item resolution is read-only — nothing is written // or installed until after the gate below. const verifiedP = opts.allowUnverified ? Promise.resolve(null) - : fetchVerifiedNames(token); - const items = await resolveItems(refs, token); + : fetchVerifiedNames(); + const items = await resolveItems(refs); // Integrity gate over the *entire resolved set* (not just requested names, so // an unverified transitive dep can't ride in on a verified item). Fails closed: @@ -683,8 +668,7 @@ entries are never clobbered. Interactive by default; pass --yes for agents/CI values non-interactively. Pass --profile to validate the bundle after writing. The frontend/server roots are detected from common layouts, so you can run -this from the repo root. While the registry repo is private, a read token is -resolved from \`gh auth token\` or APPKIT_REGISTRY_TOKEN / GITHUB_TOKEN / GH_TOKEN. +this from the repo root. Examples: $ appkit add metric-card # UI component diff --git a/packages/shared/src/cli/commands/registry/client.ts b/packages/shared/src/cli/commands/registry/client.ts index 789fd96b9..22e272a6c 100644 --- a/packages/shared/src/cli/commands/registry/client.ts +++ b/packages/shared/src/cli/commands/registry/client.ts @@ -1,13 +1,10 @@ import process from "node:process"; import { - REGISTRY_INDEX_API_URL, REGISTRY_INDEX_URL, - REGISTRY_ITEM_API_TEMPLATE, REGISTRY_ITEM_URL_TEMPLATE, REGISTRY_NAMESPACE, REGISTRY_REPO, - type RegistryToken, } from "./constants"; export interface RegistryItemFile { @@ -52,39 +49,15 @@ export function isValidItemName(name: string): boolean { } /** - * Auth headers for a registry request. With a token the GitHub Contents API is - * used and `Accept: raw` makes it return file bytes directly; without one the - * public raw URL needs no headers. Single source for the auth contract shared - * by every registry fetch. + * Fetches and parses a single registry item from the public raw URL. Exits the + * process with a helpful message on failure. */ -export function registryAuthHeaders( - token: RegistryToken | null, -): Record { - if (!token) return {}; - return { - Authorization: `Bearer ${token.value}`, - Accept: "application/vnd.github.raw", - }; -} - -/** - * Fetches and parses a single registry item. When a token is present the GitHub - * Contents API is used (works for the private/internal repo); otherwise the - * public raw URL is used. Exits the process with a helpful message on failure. - */ -export async function fetchRegistryItem( - name: string, - token: RegistryToken | null, -): Promise { - const template = token - ? REGISTRY_ITEM_API_TEMPLATE - : REGISTRY_ITEM_URL_TEMPLATE; - const url = template.replace("{name}", name); - const headers = registryAuthHeaders(token); +export async function fetchRegistryItem(name: string): Promise { + const url = REGISTRY_ITEM_URL_TEMPLATE.replace("{name}", name); let res: Awaited>; try { - res = await fetch(url, { headers }); + res = await fetch(url); } catch (err) { console.error(`Failed to fetch "${name}" from ${url}`); console.error(` ${err instanceof Error ? err.message : String(err)}`); @@ -93,18 +66,6 @@ export async function fetchRegistryItem( if (res.status === 404) { console.error(`"${name}" not found in ${REGISTRY_REPO}.`); - if (!token) { - console.error( - " If the registry repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.", - ); - } - process.exit(1); - } - if (res.status === 401 || res.status === 403) { - console.error( - `Access denied (HTTP ${res.status}) fetching "${name}" from ${REGISTRY_REPO}.`, - ); - console.error(" Check that your token has read access to the repository."); process.exit(1); } if (!res.ok) { @@ -129,12 +90,9 @@ export interface RegistryIndexEntry { * index can't be read, so the caller can tell "nothing verified" apart from * "couldn't check". */ -export async function fetchVerifiedNames( - token: RegistryToken | null, -): Promise | null> { - const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL; +export async function fetchVerifiedNames(): Promise | null> { try { - const res = await fetch(url, { headers: registryAuthHeaders(token) }); + const res = await fetch(REGISTRY_INDEX_URL); if (!res.ok) return null; const data = (await res.json()) as { items?: RegistryIndexEntry[] }; const verified = new Set(); diff --git a/packages/shared/src/cli/commands/registry/constants.ts b/packages/shared/src/cli/commands/registry/constants.ts index 43cda3faf..69dc3c135 100644 --- a/packages/shared/src/cli/commands/registry/constants.ts +++ b/packages/shared/src/cli/commands/registry/constants.ts @@ -1,5 +1,3 @@ -import { spawnSync } from "node:child_process"; - /** shadcn registry namespace consumers reference, e.g. `@databricks-appkit/metric-card`. */ export const REGISTRY_NAMESPACE = "@databricks-appkit"; @@ -15,52 +13,9 @@ export const REGISTRY_REPO = "databricks/appkit-registry"; export const REGISTRY_REF = "main"; /** - * Public hosting: once the repo is public, items are fetchable directly from + * The registry is a public repo, so items are fetched directly from * raw.githubusercontent.com with no auth. */ const PUBLIC_RAW_BASE = `https://raw.githubusercontent.com/${REGISTRY_REPO}/${REGISTRY_REF}`; export const REGISTRY_ITEM_URL_TEMPLATE = `${PUBLIC_RAW_BASE}/public/r/{name}.json`; export const REGISTRY_INDEX_URL = `${PUBLIC_RAW_BASE}/registry.json`; - -/** - * Private/internal hosting: while the repo is internal, files are fetched via - * the GitHub Contents API with a token. `Accept: application/vnd.github.raw` - * makes the API return the file bytes directly (the registry-item JSON). - */ -const GH_CONTENTS_API = `https://api.github.com/repos/${REGISTRY_REPO}/contents`; -export const REGISTRY_ITEM_API_TEMPLATE = `${GH_CONTENTS_API}/public/r/{name}.json?ref=${REGISTRY_REF}`; -export const REGISTRY_INDEX_API_URL = `${GH_CONTENTS_API}/registry.json?ref=${REGISTRY_REF}`; - -/** Env vars checked (in order) for a token granting read access to the repo. */ -export const TOKEN_ENV_VARS = [ - "APPKIT_REGISTRY_TOKEN", - "GITHUB_TOKEN", - "GH_TOKEN", -]; - -export interface RegistryToken { - envName: string; - value: string; -} - -/** - * Resolves a token granting read access to the registry repo: first the env - * vars in {@link TOKEN_ENV_VARS}, then the GitHub CLI (`gh auth token`) if the - * user is logged in. Returns null if none are available. - */ -export function resolveToken( - env: NodeJS.ProcessEnv = process.env, -): RegistryToken | null { - for (const envName of TOKEN_ENV_VARS) { - const value = env[envName]; - if (value) return { envName, value }; - } - try { - const res = spawnSync("gh", ["auth", "token"], { encoding: "utf-8" }); - const value = res.status === 0 ? res.stdout.trim() : ""; - if (value) return { envName: "gh auth token", value }; - } catch { - // gh not installed or not on PATH — fall through. - } - return null; -} diff --git a/packages/shared/src/cli/commands/registry/info.ts b/packages/shared/src/cli/commands/registry/info.ts index f1a37d11b..d9416fc2e 100644 --- a/packages/shared/src/cli/commands/registry/info.ts +++ b/packages/shared/src/cli/commands/registry/info.ts @@ -4,11 +4,9 @@ import { Command } from "commander"; import pc from "picocolors"; import { fetchRegistryItem, isValidItemName, stripNamespace } from "./client"; -import { resolveToken } from "./constants"; import { extractRequirements, renderRequirements } from "./requirements"; async function runInfo(ref: string, opts: { json?: boolean }): Promise { - const token = resolveToken(); // Validate before fetching: the name is interpolated into the fetch path, so // reject non-slug refs (matches the `add` guard) rather than let `/` or `..` // redirect the request to another path in the repo. @@ -17,7 +15,7 @@ async function runInfo(ref: string, opts: { json?: boolean }): Promise { console.error(`Invalid registry item name: ${JSON.stringify(ref)}`); process.exit(1); } - const item = await fetchRegistryItem(name, token); + const item = await fetchRegistryItem(name); const rows = extractRequirements(item); if (opts.json) { diff --git a/packages/shared/src/cli/commands/registry/list.ts b/packages/shared/src/cli/commands/registry/list.ts index d380f97a0..0bec3ba1c 100644 --- a/packages/shared/src/cli/commands/registry/list.ts +++ b/packages/shared/src/cli/commands/registry/list.ts @@ -3,13 +3,7 @@ import process from "node:process"; import { Command } from "commander"; import pc from "picocolors"; -import { registryAuthHeaders } from "./client"; -import { - REGISTRY_INDEX_API_URL, - REGISTRY_INDEX_URL, - REGISTRY_REPO, - resolveToken, -} from "./constants"; +import { REGISTRY_INDEX_URL, REGISTRY_REPO } from "./constants"; interface RegistryIndexItem { name: string; @@ -111,34 +105,24 @@ function printTable(items: RegistryIndexItem[]): void { } } -/** Fetches the registry index (token-aware), or exits with a helpful message. */ +/** Fetches the registry index from the public raw URL, or exits with a message. */ async function fetchIndex(): Promise { - const token = resolveToken(); - const url = token ? REGISTRY_INDEX_API_URL : REGISTRY_INDEX_URL; - let res: Awaited>; try { - res = await fetch(url, { headers: registryAuthHeaders(token) }); + res = await fetch(REGISTRY_INDEX_URL); } catch (err) { - console.error(pc.red(`Failed to reach the registry at ${url}`)); + console.error( + pc.red(`Failed to reach the registry at ${REGISTRY_INDEX_URL}`), + ); console.error(` ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } - if (res.status === 404 || res.status === 401 || res.status === 403) { + if (!res.ok) { console.error( pc.red( `Could not read the registry index from ${REGISTRY_REPO} (HTTP ${res.status}).`, ), ); - if (!token) { - console.error( - " If the repo is private, set APPKIT_REGISTRY_TOKEN (or GITHUB_TOKEN) to a token with read access.", - ); - } - process.exit(1); - } - if (!res.ok) { - console.error(pc.red(`Registry returned HTTP ${res.status} for ${url}`)); process.exit(1); }