diff --git a/site/src/content/blog/devblog-2026-w27.mdx b/site/src/content/blog/devblog-2026-w27.mdx new file mode 100644 index 0000000..e69de29 diff --git a/web/standalone/src/App.tsx b/web/standalone/src/App.tsx index 2708238..b22ae98 100644 --- a/web/standalone/src/App.tsx +++ b/web/standalone/src/App.tsx @@ -10,9 +10,12 @@ export default function App() { <> } /> - } /> - } /> - } /> + {/* scope/kind/name grammar (see @pcbjam/shared routes.ts). The tool is + inferred from the file (or lib kind); `-/:tool` boots a fileless tool. */} + } /> + } /> + } /> + } /> {/* Version + source link, bottom-right on every route (home + editor). */} diff --git a/web/standalone/src/components/NewFileDialog.tsx b/web/standalone/src/components/NewFileDialog.tsx index ba8a5a0..2f4a69e 100644 --- a/web/standalone/src/components/NewFileDialog.tsx +++ b/web/standalone/src/components/NewFileDialog.tsx @@ -1,7 +1,13 @@ import * as React from "react"; -import { TOOL_LABELS, type Tool } from "@pcbjam/shared"; +import { + LOCAL_SCOPE, + TOOL_LABELS, + type Tool, + projectPath, +} from "@pcbjam/shared"; import { Loader2 } from "lucide-react"; import { uploadFileBytes } from "@/lib/api"; +import { currentScope } from "@/lib/config"; import { localProjectStore } from "@/lib/project-source"; import { useLocalProjects } from "@/lib/api"; import { @@ -92,9 +98,10 @@ export function NewFileDialog({ await uploadFileBytes(slug, finalName, bytes); } // Full navigation so Emscripten boots into a clean page opening the file. - window.location.assign( - `/p/${encodeURIComponent(slug)}/${tool}/${finalName}`, - ); + // A home-created project is browser-local (@local scope); an in-project new + // file keeps the current scope. The tool is inferred from the file's ext. + const scope = homeMode ? LOCAL_SCOPE : currentScope(); + window.location.assign(projectPath(scope, slug, finalName)); } catch (e) { setBusy(false); setError(e instanceof Error ? e.message : String(e)); diff --git a/web/standalone/src/components/ProjectsSection.tsx b/web/standalone/src/components/ProjectsSection.tsx index 60d1d55..35c990d 100644 --- a/web/standalone/src/components/ProjectsSection.tsx +++ b/web/standalone/src/components/ProjectsSection.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { Link } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; -import type { Project } from "@pcbjam/shared"; +import { type Project, projectPath } from "@pcbjam/shared"; import { Download, Loader2, Pencil, Trash2 } from "lucide-react"; import { useLocalProjects, useProjects } from "@/lib/api"; import { PROJECT_SOURCE_KIND } from "@/lib/config"; @@ -149,7 +149,7 @@ function ProjectRow({
{children}
diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 463d3f2..644f095 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -5,6 +5,8 @@ import { EXTENSION_TOOL, FILELESS_TOOLS, fileToDoc, + projectPath, + projectToolPath, toolSchema, ydocHasState, yToDoc, @@ -13,6 +15,7 @@ import { } from "@pcbjam/shared"; import { ChevronDown, ChevronUp, Loader2 } from "lucide-react"; import { + currentScope, libsSourceConfig, yjsProviderConfig, type DocSource, @@ -160,10 +163,6 @@ function chooseToolFile( return candidates[0]?.path; } -function encodeRelPath(path: string): string { - return path.split("/").map(encodeURIComponent).join("/"); -} - function installToolNavigationHook( win: ToolWindow, opts: { @@ -191,10 +190,13 @@ function installToolNavigationHook( return false; } + // Scope/kind/name grammar: a fileless tool boots at `…/-/:tool`; a file route + // carries the path (its tool is inferred). Scope = the current URL's scope. + const scope = currentScope(); const url = - `/p/${encodeURIComponent(opts.slug)}/${nextTool}` + - (nextPath ? `/${encodeRelPath(nextPath)}` : "") + - win.location.search; + (FILELESS_TOOLS.has(nextTool) + ? projectToolPath(scope, opts.slug, nextTool) + : projectPath(scope, opts.slug, nextPath)) + win.location.search; opts.log(`[nav] ${rawToolName} ${rawFileName || "(no file)"} -> ${url}`); win.location.assign(url); diff --git a/web/standalone/src/lib/api.ts b/web/standalone/src/lib/api.ts index ca7c12d..ca98c09 100644 --- a/web/standalone/src/lib/api.ts +++ b/web/standalone/src/lib/api.ts @@ -1,6 +1,6 @@ import type { DriftReportBody, Project } from "@pcbjam/shared"; import { useQuery } from "@tanstack/react-query"; -import { API_BASE_URL, libsSourceConfig } from "./config"; +import { API_BASE_URL, currentScope, libsSourceConfig } from "./config"; import { client } from "./contract-client"; import type { LibInfo } from "@/wasm/libs/source"; import { downloadBytes } from "./download"; @@ -119,7 +119,10 @@ export async function reportDrift( slug: string, body: DriftReportBody, ): Promise { - await client.reportDrift({ params: { project: slug }, body }); + await client.reportDrift({ + params: { scope: currentScope(), project: slug }, + body, + }); } /** @@ -128,7 +131,7 @@ export async function reportDrift( * a keepalive `fetch` is the fallback when the beacon is rejected (too large). */ export function reportDriftBeacon(slug: string, body: DriftReportBody): void { - const url = `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/drift`; + const url = `${API_BASE_URL}/api/scopes/${encodeURIComponent(currentScope())}/projects/${encodeURIComponent(slug)}/drift`; const blob = new Blob([JSON.stringify(body)], { type: "application/json" }); try { if (navigator.sendBeacon(url, blob)) return; diff --git a/web/standalone/src/lib/config.ts b/web/standalone/src/lib/config.ts index bc1f8f3..3ec5de9 100644 --- a/web/standalone/src/lib/config.ts +++ b/web/standalone/src/lib/config.ts @@ -151,16 +151,38 @@ export function docSourceConfig(): DocSource { * "off" — disable libs (empty sym-lib-table). */ /** - * The (thin, pre-auth) owner the editor writes libs as, sent on every lib - * request via OWNER_HEADER. `?libowner=` (e2e isolation) wins over - * `VITE_LIBS_OWNER`, else a stable local default. + * The (thin, pre-auth) current user — sent on every request via USER_HEADER and + * doubling as the personal scope slug. `?user=`/`?libowner=` (e2e isolation) win + * over `VITE_USER`/`VITE_LIBS_OWNER`, else a stable local default. */ -export function libsOwner(): string { +export function userSlug(): string { if (typeof window !== "undefined") { - const p = new URLSearchParams(window.location.search).get("libowner"); + const q = new URLSearchParams(window.location.search); + const p = q.get("user") ?? q.get("libowner"); if (p) return p; } - return import.meta.env.VITE_LIBS_OWNER ?? "local-user"; + return ( + import.meta.env.VITE_USER ?? import.meta.env.VITE_LIBS_OWNER ?? "local-user" + ); +} + +/** + * The active scope (first URL segment) for API calls. Mirrors how `userSlug()` + * reads the URL, so the source layer scopes requests without threading scope + * through every signature. Falls back to `?scope=` / `VITE_SCOPE` / the personal + * scope (the user slug). Client-only scopes (e.g. `@local`) are routed by the + * project source and never sent to a backend. + */ +export function currentScope(): string { + if (typeof window !== "undefined") { + const seg = window.location.pathname.split("/").filter(Boolean)[0]; + if (seg && seg !== "projects" && seg !== "libs") { + return decodeURIComponent(seg); + } + const q = new URLSearchParams(window.location.search).get("scope"); + if (q) return q; + } + return import.meta.env.VITE_SCOPE ?? userSlug(); } /** Full URL of the CDN libs top manifest (required for VITE_LIBS_SOURCE=cdn), @@ -187,7 +209,7 @@ export function libsSourceConfig(projectId?: string): LibsSource | null { ? CDN_LIBS_MANIFEST_URL ? cdnLibsSource(CDN_LIBS_MANIFEST_URL) : staticLibsSource() // misconfigured cdn ⇒ offline fallback - : remoteLibsSource(API_BASE_URL, libsOwner(), project); + : remoteLibsSource(API_BASE_URL, currentScope(), userSlug(), project); // 0004-A spike: `?libwrite=1` adds one in-memory writable user SYMBOL lib so the // editor save path works with no backend (a dev/test aid). The real remote @@ -221,7 +243,8 @@ export function libsSourceForLib( if (import.meta.env.VITE_LIBS_SOURCE === "synced") { return syncedLibsSource(libId, { apiBase: API_BASE_URL, - owner: libsOwner(), + scope: currentScope(), + user: userSlug(), project, log: (m) => console.log(m), }); diff --git a/web/standalone/src/lib/contract-client.ts b/web/standalone/src/lib/contract-client.ts index c56f963..4f804ce 100644 --- a/web/standalone/src/lib/contract-client.ts +++ b/web/standalone/src/lib/contract-client.ts @@ -1,14 +1,15 @@ -import { contract } from "@pcbjam/shared"; +import { contract, USER_HEADER } from "@pcbjam/shared"; import { initClient } from "@ts-rest/core"; -import { API_BASE_URL } from "./config"; +import { API_BASE_URL, userSlug } from "./config"; /** * ts-rest client over the shared contract (the REST backend). Shared by the * remote project source (lib/project-source.ts) and the lib/drift endpoints * (lib/api.ts). Kept in its own module so project-source and api don't import - * each other (avoids a cycle). + * each other (avoids a cycle). The scope is a PATH param (per call); the user is + * a header (the thin pre-auth identity, used for per-user lib pins). */ export const client = initClient(contract, { baseUrl: API_BASE_URL, - baseHeaders: {}, + baseHeaders: { [USER_HEADER]: userSlug() }, }); diff --git a/web/standalone/src/lib/idb-project-store.ts b/web/standalone/src/lib/idb-project-store.ts index 450470e..3cda6bf 100644 --- a/web/standalone/src/lib/idb-project-store.ts +++ b/web/standalone/src/lib/idb-project-store.ts @@ -1,4 +1,9 @@ -import type { Project, ProjectFile, ProjectWithFiles } from "@pcbjam/shared"; +import { + LOCAL_SCOPE, + type Project, + type ProjectFile, + type ProjectWithFiles, +} from "@pcbjam/shared"; import type { ProjectSource } from "./project-source"; import { SOURCE_DESCRIPTORS, @@ -114,6 +119,7 @@ function slugify(name: string): string { function toProject(r: ProjectRecord): Project { return { id: deterministicUuid(`local:project:${r.slug}`), + scope: LOCAL_SCOPE, slug: r.slug, name: r.name, createdAt: r.createdAt, diff --git a/web/standalone/src/lib/project-source-shared.ts b/web/standalone/src/lib/project-source-shared.ts index a35d43a..b15142f 100644 --- a/web/standalone/src/lib/project-source-shared.ts +++ b/web/standalone/src/lib/project-source-shared.ts @@ -4,11 +4,11 @@ * both can import the descriptor types + the deterministic-uuid helper without a * runtime import cycle. * - * A source has one of three KINDS, surfaced verbatim in the UI so the user always - * knows where their edits go: - * - "remote-rw" — a backend the editor reads AND writes (saves upload). - * - "remote-ro" — a read-only remote (the demo's CDN gallery); saves download. - * - "local" — this browser's IndexedDB; saves persist locally, export by zip. + * A source has one of three KINDS, surfaced in the UI so the user always knows + * where their edits go: + * - "remote-rw" — a backend the editor reads AND writes (Cloud; saves upload). + * - "remote-ro" — the curated CDN gallery (Demo); editing auto-forks a local copy. + * - "local" — this browser's IndexedDB (Local); saves persist, export by zip. */ export type SourceKind = "remote-rw" | "remote-ro" | "local"; @@ -26,22 +26,24 @@ export const SOURCE_DESCRIPTORS: Record = { "remote-rw": { kind: "remote-rw", writable: true, - label: "Remote · editable", + label: "Cloud", description: "Stored on the backend — your saves are uploaded there.", }, "remote-ro": { kind: "remote-ro", + // Not directly writable, but editing transparently forks a local copy, so + // the UI never shows a scary "read-only" — it's just a Demo you can edit. writable: false, - label: "Remote · read-only", + label: "Demo", description: - "A read-only example from the server — edits aren't saved back; Save downloads the file to your machine.", + "A demo project — open it and your edits become a local copy you can save and export.", }, local: { kind: "local", writable: true, - label: "Local (this browser)", + label: "Local", description: - "Stored in this browser only — saves persist here across visits. Export with Download .zip (nothing is uploaded).", + "Stored in this browser — saves persist here across visits. Export with Download .zip.", }, }; diff --git a/web/standalone/src/lib/project-source.test.ts b/web/standalone/src/lib/project-source.test.ts index 53b0f87..f61b656 100644 --- a/web/standalone/src/lib/project-source.test.ts +++ b/web/standalone/src/lib/project-source.test.ts @@ -28,6 +28,9 @@ async function loadStatic() { // Local IDB store off ⇒ the active source is the plain static gallery // (no composite), which is what these read-only assertions cover. LOCAL_PROJECTS_ENABLED: false, + // The contract client (imported transitively) reads these at module load. + userSlug: () => "test-user", + currentScope: () => "demo", })); return (await import("./project-source")).projectSource; } diff --git a/web/standalone/src/lib/project-source.ts b/web/standalone/src/lib/project-source.ts index 736357d..202ea29 100644 --- a/web/standalone/src/lib/project-source.ts +++ b/web/standalone/src/lib/project-source.ts @@ -1,9 +1,15 @@ -import type { Project, ProjectFile, ProjectWithFiles } from "@pcbjam/shared"; +import { + DEMO_SCOPE, + type Project, + type ProjectFile, + type ProjectWithFiles, +} from "@pcbjam/shared"; import { API_BASE_URL, LOCAL_PROJECTS_ENABLED, PROJECT_MANIFEST_URL, PROJECT_SOURCE_KIND, + currentScope, } from "./config"; import { client } from "./contract-client"; import { idbProjectStore, type LocalProjectStore } from "./idb-project-store"; @@ -51,18 +57,24 @@ function encodePath(relPath: string): string { } function remoteProjectSource(): ProjectSource { + // The active scope is the URL's first segment (config.currentScope), read at + // call time so a single source instance serves whatever scope is open. + const projectsBase = () => + `${API_BASE_URL}/api/scopes/${encodeURIComponent(currentScope())}/projects`; const fileUrl = (slug: string, relPath: string) => - `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files/${encodePath(relPath)}`; + `${projectsBase()}/${encodeURIComponent(slug)}/files/${encodePath(relPath)}`; return { descriptor: SOURCE_DESCRIPTORS["remote-rw"], readOnly: false, async listProjects() { - const res = await client.listProjects(); + const res = await client.listProjects({ params: { scope: currentScope() } }); if (res.status !== 200) throw new Error("failed to list projects"); return res.body; }, async getProject(slug) { - const res = await client.getProject({ params: { project: slug } }); + const res = await client.getProject({ + params: { scope: currentScope(), project: slug }, + }); if (res.status === 404) throw new Error("project not found"); if (res.status !== 200) throw new Error("failed to load project"); return res.body; @@ -78,10 +90,10 @@ function remoteProjectSource(): ProjectSource { // The form FIELD NAME carries the project-relative path (upsert by // (project, path)) — same convention as the management app's folder upload. form.append(relPath, new File([bytes as BlobPart], name)); - const res = await fetch( - `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files`, - { method: "POST", body: form }, - ); + const res = await fetch(`${projectsBase()}/${encodeURIComponent(slug)}/files`, { + method: "POST", + body: form, + }); if (!res.ok) throw new Error(`upload failed (${res.status}): ${relPath}`); }, }; @@ -125,6 +137,8 @@ function staticProjectSource(manifestUrl: string): ProjectSource { const toProject = (p: StaticManifestProject, ts: string): Project => ({ id: deterministicUuid(`project:${p.slug}`), + // The curated gallery lives under the reserved `demo` scope. + scope: DEMO_SCOPE, slug: p.slug, name: p.name, createdAt: ts, diff --git a/web/standalone/src/pages/HomePage.tsx b/web/standalone/src/pages/HomePage.tsx index 4b56165..a9af4f6 100644 --- a/web/standalone/src/pages/HomePage.tsx +++ b/web/standalone/src/pages/HomePage.tsx @@ -1,10 +1,14 @@ import * as React from "react"; import { useNavigate } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; -import type { Tool } from "@pcbjam/shared"; +import { type Tool, libPath, projectPath } from "@pcbjam/shared"; import { FolderOpen, Library, Loader2, Package } from "lucide-react"; import { useLibs } from "@/lib/api"; -import { LOCAL_PROJECTS_ENABLED, PROJECT_SOURCE_KIND } from "@/lib/config"; +import { + LOCAL_PROJECTS_ENABLED, + PROJECT_SOURCE_KIND, + currentScope, +} from "@/lib/config"; import { localFileLibsSource } from "@/wasm/libs/local-file-source"; import type { LibsSource } from "@/wasm/libs/source"; import { downloadBytes } from "@/lib/download"; @@ -151,7 +155,7 @@ export function HomePage() { if (!store) return; const project = await store.createProject(imported.name, imported.files); await queryClient.invalidateQueries({ queryKey: ["local-projects"] }); - navigate(`/p/${project.slug}`); + navigate(projectPath(project.scope, project.slug)); }; // Tool launched from the home page: no project, no files. File-less editors @@ -388,7 +392,7 @@ function LibGroup({ {shown.map((lib) => ( diff --git a/web/standalone/src/pages/LibToolPage.tsx b/web/standalone/src/pages/LibToolPage.tsx index 27133ff..5f5c04b 100644 --- a/web/standalone/src/pages/LibToolPage.tsx +++ b/web/standalone/src/pages/LibToolPage.tsx @@ -1,37 +1,31 @@ -import { useParams } from "react-router-dom"; -import { toolSchema } from "@pcbjam/shared"; +import { useParams, useSearchParams } from "react-router-dom"; +import { parseToolParam } from "@pcbjam/shared"; import { libsSourceForLib } from "@/lib/config"; import { WasmTool } from "@/components/WasmTool"; import { PreflightGate } from "@/preflight/PreflightGate"; /** - * Open one BACKEND library scoped to itself in its editor, addressed by URL: - * /l// e.g. /l/Diode/symbol_editor + * Open one library scoped to itself in its editor, addressed by URL: + * /:scope/libs/ (symbol_editor — the default) + * /:scope/libs/?tool=footprint_editor * - * Deep-linkable + reload-safe (unlike the home page's in-place lib launch). The - * editor boots with no project/files and a `scopedLibsSource` so its lib tree - * shows exactly this one library. (Local lib FILES can't be URL-addressed — they - * stay an in-place launch on the home page.) + * The lib's editor is chosen by `?tool=` (the home page appends it from the lib's + * kind); absent ⇒ the symbol editor. `` is the lib's opaque token (its CDN + * name or backend id). Deep-linkable + reload-safe. The editor boots with no + * project/files and a `scopedLibsSource` showing exactly this one library. */ export function LibToolPage() { const params = useParams(); - const libId = params.lib ?? ""; - const parsedTool = toolSchema.safeParse(params.tool); - - if (!parsedTool.success) { - return ( -
- Unknown tool: {params.tool} -
- ); - } + const [search] = useSearchParams(); + const libId = params.name ?? ""; + const tool = parseToolParam(search.get("tool")) ?? "symbol_editor"; const libsSource = libsSourceForLib(libId, "local"); return ( { if (FILELESS_TOOLS.has(tool)) { - window.location.assign(`/p/${encodeURIComponent(slug)}/${tool}/`); + window.location.assign(projectToolPath(scope, slug, tool)); } else { setNewFileTool(tool); } @@ -128,7 +130,7 @@ export function ProjectView() { {tool && (
Open in {TOOL_LABELS[tool]} @@ -161,7 +163,9 @@ export function ProjectView() { {data.project.name} {descriptor && } -

/p/{data.project.slug}

+

+ {data.project.scope}/projects/{data.project.slug} +

{movingToLocal ? ( diff --git a/web/standalone/src/pages/ToolPage.tsx b/web/standalone/src/pages/ToolPage.tsx index eac15bd..d007419 100644 --- a/web/standalone/src/pages/ToolPage.tsx +++ b/web/standalone/src/pages/ToolPage.tsx @@ -1,5 +1,5 @@ -import { useParams } from "react-router-dom"; -import { toolSchema } from "@pcbjam/shared"; +import { useParams, useSearchParams } from "react-router-dom"; +import { parseToolParam, toolForFile, type Tool } from "@pcbjam/shared"; import { fetchFileBytes, uploadFileBytes, @@ -12,17 +12,24 @@ import { PreflightGate } from "@/preflight/PreflightGate"; export function ToolPage() { const params = useParams(); - const slug = params.project ?? ""; - const targetPath = params["*"] || undefined; + const [search] = useSearchParams(); + const slug = params.name ?? ""; + // Two shapes render here: a fileless tool boot (`…/-/:tool`) sets params.tool; + // a file route (`…/*`) sets the splat — the tool is inferred from its extension + // unless `?tool=` overrides it. + const splat = params["*"] || undefined; + const tool: Tool | null = params.tool + ? parseToolParam(params.tool) + : (parseToolParam(search.get("tool")) ?? (splat ? toolForFile(splat) : null)); + const targetPath = params.tool ? undefined : splat; - const parsedTool = toolSchema.safeParse(params.tool); const { data, isLoading, error } = useProject(slug); const { data: sourceDescriptor } = useSourceDescriptor(slug); - if (!parsedTool.success) { + if (!tool) { return (
- Unknown tool: {params.tool} + Unknown tool: {params.tool ?? splat}
); } @@ -54,7 +61,7 @@ export function ToolPage() { return ( = { - ...(owner ? { [OWNER_HEADER]: owner } : {}), + [SCOPE_HEADER]: scope, + ...(user ? { [USER_HEADER]: user } : {}), ...(project ? { [PROJECT_HEADER]: project } : {}), }; const client = initClient(contract, { baseUrl: apiBase, baseHeaders: reqHeaders, }); + const enc = encodeURIComponent; const itemUrl = (libId: string, kind: string, name: string) => - `${apiBase}/api/libs/${encodeURIComponent(libId)}/items/` + - `${encodeURIComponent(kind)}/${encodeURIComponent(name)}`; + `${apiBase}/api/scopes/${enc(scope)}/libs/${enc(libId)}/items/` + + `${enc(kind)}/${enc(name)}`; return { async listLibs(kind?: string): Promise { - const res = await client.listLibs({ query: { kind } }); + const res = await client.listLibs({ params: { scope }, query: { kind } }); if (res.status !== 200) return []; return res.body.map((l) => ({ id: l.id, @@ -43,7 +50,7 @@ export function remoteLibsSource( }, async listItems(libId: string): Promise { - const res = await client.listLibItems({ params: { lib: libId } }); + const res = await client.listLibItems({ params: { scope, lib: libId } }); if (res.status !== 200) return []; return res.body.map((i) => ({ kind: i.kind, name: i.name })); }, @@ -75,7 +82,7 @@ export function remoteLibsSource( }, async createLib(name: string): Promise { - const res = await client.createLib({ body: { name } }); + const res = await client.createLib({ params: { scope }, body: { name } }); if (res.status !== 201) return null; return { id: res.body.id, diff --git a/web/standalone/src/wasm/libs/synced-source.ts b/web/standalone/src/wasm/libs/synced-source.ts index 95e6d7b..8685dbb 100644 --- a/web/standalone/src/wasm/libs/synced-source.ts +++ b/web/standalone/src/wasm/libs/synced-source.ts @@ -1,4 +1,4 @@ -import { OWNER_HEADER, PROJECT_HEADER } from "@pcbjam/shared"; +import { PROJECT_HEADER, SCOPE_HEADER, USER_HEADER } from "@pcbjam/shared"; import { SyncStack, type LayerDescriptor } from "@pcbjam/sync-client"; import type { LibInfo, LibItemInfo, LibsSource } from "./source"; @@ -18,7 +18,8 @@ export function syncedLibsSource( libId: string, opts: { apiBase: string; - owner?: string; + scope: string; + user?: string; project?: string; log?: (msg: string) => void; }, @@ -85,15 +86,16 @@ export function syncedLibsSource( async function resolveAndOpen( libId: string, - opts: { apiBase: string; owner?: string; project?: string }, + opts: { apiBase: string; scope: string; user?: string; project?: string }, log: (msg: string) => void, ): Promise<{ stack: SyncStack; info: LibInfo }> { const headers: Record = { - ...(opts.owner ? { [OWNER_HEADER]: opts.owner } : {}), + [SCOPE_HEADER]: opts.scope, + ...(opts.user ? { [USER_HEADER]: opts.user } : {}), ...(opts.project ? { [PROJECT_HEADER]: opts.project } : {}), }; const res = await fetch( - `${opts.apiBase}/api/libs/${encodeURIComponent(libId)}/sync-stack`, + `${opts.apiBase}/api/scopes/${encodeURIComponent(opts.scope)}/libs/${encodeURIComponent(libId)}/sync-stack`, { method: "POST", headers }, ); if (!res.ok) throw new Error(`sync-stack resolve failed: HTTP ${res.status}`);