Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7df8a5c
fix(builderlab): share the development server login across features
tellaho Nov 1, 2026
30956c3
fix(builderlab): preserve decisions with the shared development login
tellaho Nov 1, 2026
77929b7
fix(builderlab): settle identity linking before agent requests
tellaho Nov 1, 2026
cc758be
fix(builderlab): accept full agent instructions in the proxy
tellaho Nov 1, 2026
f895349
fix(builderlab): keep pending login owned by the development server
tellaho Nov 1, 2026
7797f5c
test(builderlab): model the shared login in browser fixtures
tellaho Nov 1, 2026
811b852
fix(builderlab): preserve the shared login after an Apps refusal
tellaho Nov 1, 2026
b747ad8
fix(builderlab): report server identity binding failures
tellaho Nov 1, 2026
adf5eae
fix(builderlab): preserve encoded IDs in authenticated requests
tellaho Nov 1, 2026
e0598ef
fix(builderlab): isolate native session exchange routes
tellaho Nov 1, 2026
c7eb819
refactor(builderlab): share fixed native forwarding
tellaho Nov 1, 2026
8c64e7e
fix(builderlab): refresh binding state after link retries
tellaho Nov 1, 2026
aed7f5e
fix(builderlab): retain accepted upload receipts across sign-out
tellaho Nov 1, 2026
2d062bb
fix(builderlab): preserve dev create recovery errors
tellaho Nov 1, 2026
a9828de
fix(builderlab): ignore stale login errors after cancellation
tellaho Nov 1, 2026
ed86423
refactor(builderlab): remove unused dev identity cache
tellaho Nov 1, 2026
0391cf6
fix(builderlab): preserve failures from the current login attempt
tellaho Nov 1, 2026
f59ef1f
fix(builderlab): publish login failures from the shared broker
tellaho Nov 1, 2026
8309d98
fix(browser-tests): declare the builderlab build option
tellaho Nov 1, 2026
3ade460
fix(builderlab): reconcile rejected login dispatches
tellaho Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
468 changes: 389 additions & 79 deletions browser-host/builderlab.mjs

Large diffs are not rendered by default.

495 changes: 481 additions & 14 deletions browser-host/builderlab.test.mjs

Large diffs are not rendered by default.

211 changes: 209 additions & 2 deletions browser-host/relay-broker-api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ test.each([
});
if (path.endsWith("/v1/auth/me"))
return Response.json({
subject: "auth0|owner",
email: "fixture@example.com",
expires_at: "2030",
});
Expand All @@ -185,9 +186,15 @@ test.each([
body: "{}",
});
const opened = new URL(await loginOpened);
const callback = opened.searchParams.get("returnTo");
const callback = opened.searchParams.get("redirect_uri");
expect(callback).toBeTruthy();
expect((await fetch(`${callback}?code=fixture`)).status).toBe(200);
expect(
(
await fetch(
`${callback}?state=${opened.searchParams.get("state")}&code=fixture`,
)
).status,
).toBe(200);
expect((await login).status).toBe(200);
const response = await fetch(`${h.base}/api/builderlab/delete`, {
method: "POST",
Expand Down Expand Up @@ -3910,3 +3917,203 @@ test("inventory rejects foreign pages, ignores invalid heads and follows exact-k
await h.close();
}
});

test("Builderlab HTTP accepts full agent instructions and isolates the fixed native routes", async () => {
let openLogin;
const opened = new Promise((resolve) => {
openLogin = resolve;
});
const instructions = "\\".repeat(20_000);
let received;
const nativeCalls = [];
const h = await harness(() => Response.json([]), {}, fixtureRelayUrl, {
open: async (url) => openLogin(new URL(url)),
fetch: async (url, init) => {
const path = new URL(url).pathname;
if (path.endsWith("/v1/auth/login/exchange"))
return Response.json({
session_credential: "fixture-only",
expires_at: "2030",
});
if (path.endsWith("/v1/auth/me"))
return Response.json({
subject: "fixture-account",
expires_at: "2030",
capabilities: { can_use_remote_agents: true },
});
if (path.endsWith("/nostr-identities/challenge"))
return Response.json({
error: { code: "missing_mapping", setup_needed: true },
});
if (path.endsWith("/v3/beekeeper/update-agent")) {
received = JSON.parse(init.body);
return Response.json({ status: 1 });
}
if (
path.endsWith("/v3/builderlab/decisions") ||
path.endsWith("/v1/auth/hosted-site/native-session")
) {
nativeCalls.push({
path,
body: JSON.parse(init.body),
headers: init.headers,
});
return Response.json({ fixture: "native reply" });
}
throw new Error(`Unexpected Builderlab request ${path}`);
},
});
try {
const login = fetch(`${h.base}/api/builderlab/login`, {
method: "POST",
body: "{}",
});
const authorization = await opened;
await fetch(
`${authorization.searchParams.get("redirect_uri")}?code=fixture&state=${authorization.searchParams.get("state")}`,
);
expect((await login).status).toBe(200);
const stream = await fetch(`${h.base}/api/builderlab/events`);
const reader = stream.body.getReader();
const decoder = new TextDecoder();
let event = "";
try {
while (!event.includes("\n\n")) {
const part = await reader.read();
if (part.done) throw new Error("Missing development session metadata");
event += decoder.decode(part.value, { stream: true });
}
} finally {
await reader.cancel();
}
const { revision } = JSON.parse(
event.slice("data: ".length).split("\n\n")[0],
);
const body = JSON.stringify({
agent_pubkey: "a".repeat(64),
agent_instructions: instructions,
});
const envelope = JSON.stringify({
revision,
path: "/v3/beekeeper/update-agent",
method: "POST",
body,
});
expect(envelope.length).toBeGreaterThan(64 * 1024);
const response = await fetch(`${h.base}/api/builderlab/request`, {
method: "POST",
body: envelope,
});
expect(response.status).toBe(200);
expect((await response.json()).status).toBe(200);
expect(received.agent_instructions).toBe(instructions);
for (const [action, path, header] of [
["decisions", "/v3/builderlab/decisions", "X-BB-Session-Credential"],
[
"viewer",
"/v1/auth/hosted-site/native-session",
"X-Buzz-Builderlab-Revision",
],
]) {
const refused = await fetch(`${h.base}/api/builderlab/request`, {
method: "POST",
body: JSON.stringify({ revision, path, method: "POST", body: "{}" }),
});
expect(await refused.json()).toEqual({
error: "Unsupported Builderlab request",
});
const forwarded = await fetch(`${h.base}/api/builderlab/${action}`, {
method: "POST",
headers: { [header]: revision, "X-Buzz-Community": "buzz.example" },
body: JSON.stringify({ fixture: action }),
});
expect(forwarded.status).toBe(200);
expect(await forwarded.json()).toEqual({ fixture: "native reply" });
expect(nativeCalls.at(-1)).toMatchObject({
path: `/api/goose${path}`,
body: { fixture: action },
headers: { "X-BB-Session-Credential": "fixture-only" },
});
}
expect(nativeCalls).toHaveLength(2);
expect(nativeCalls[0].headers["X-Buzz-Community"]).toBe("buzz.example");
} finally {
await h.close();
}
});

test("Builderlab pending sign-in survives its initiating page disconnecting", async () => {
let openLogin;
const opened = new Promise((resolve) => {
openLogin = resolve;
});
const h = await harness(() => Response.json([]), {}, fixtureRelayUrl, {
open: async (url) => openLogin(new URL(url)),
fetch: async (url) => {
const path = new URL(url).pathname;
if (path.endsWith("/v1/auth/login/exchange"))
return Response.json({
session_credential: "fixture-only",
expires_at: "2030",
});
if (path.endsWith("/v1/auth/me"))
return Response.json({
subject: "fixture-account",
expires_at: "2030",
});
if (path.endsWith("/nostr-identities/challenge"))
return Response.json({
error: { code: "missing_mapping", setup_needed: true },
});
throw new Error(`Unexpected Builderlab request ${path}`);
},
});
const disconnected = new Promise((resolve) => {
h.server.on("request", (req, res) => {
if (req.url === "/api/builderlab/login") res.once("close", resolve);
});
});
const abort = new AbortController();
let reader;
try {
const login = fetch(`${h.base}/api/builderlab/login`, {
method: "POST",
body: "{}",
signal: abort.signal,
}).catch((error) => error);
const authorization = await opened;
abort.abort();
await login;
await disconnected;
const stream = await fetch(`${h.base}/api/builderlab/events`);
reader = stream.body.getReader();
const decoder = new TextDecoder();
let buffered = "";
const next = async () => {
while (!buffered.includes("\n\n")) {
const part = await reader.read();
if (part.done) throw new Error("Development session stream ended");
buffered += decoder.decode(part.value, { stream: true });
}
const end = buffered.indexOf("\n\n");
const event = JSON.parse(buffered.slice("data: ".length, end));
buffered = buffered.slice(end + 2);
return event;
};
expect(await next()).toMatchObject({ pending: true, auth: null });
await fetch(
`${authorization.searchParams.get("redirect_uri")}?code=fixture&state=${authorization.searchParams.get("state")}`,
);
let signedIn;
do {
signedIn = await next();
} while (signedIn.pending);
expect(signedIn.auth).toMatchObject({ subject: "fixture-account" });
const auth = await (await fetch(`${h.base}/api/builderlab/auth`)).json();
expect(auth.auth).toMatchObject({ subject: "fixture-account" });
} finally {
abort.abort();
await reader?.cancel();
await h.close();
}
});
2 changes: 2 additions & 0 deletions browser-host/relay-broker.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export function relayBrokerPlugin(options?: {
upstreamFetch?: typeof fetch;
socketFactory?: (url: string) => WebSocket;
builderlab?: {
api?: string;
appsBase?: string | undefined;
fetch?: typeof fetch;
open?: (url: string) => Promise<void>;
};
Expand Down
76 changes: 69 additions & 7 deletions browser-host/relay-broker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,7 @@ export function relayBrokerPlugin({
...builderlabOptions,
});
server.httpServer?.once("close", () => {
builderlab.signOut();
for (const { close } of streams.values()) close();
archive?.close();
key.fill(0);
Expand Down Expand Up @@ -959,9 +960,21 @@ export function relayBrokerPlugin({
if (res.destroyed) release();
try {
if (url.pathname.startsWith("/api/builderlab/")) {
// Hosted communities plugin only; the session credential never leaves Node.
// One development sign-in for hosting, agents and apps; its credential stays in Node.
const action = url.pathname.slice("/api/builderlab/".length);
try {
if (action === "events" && req.method === "GET") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-store",
});
const send = (state) =>
res.write(`data: ${JSON.stringify(state)}\n\n`);
const stop = builderlab.subscribe(send);
res.once("close", stop);
send(builderlab.snapshot());
return;
}
if (action === "auth" && req.method === "GET")
return json(res, 200, { auth: await builderlab.auth() });
if (req.method !== "POST")
Expand All @@ -970,23 +983,72 @@ export function relayBrokerPlugin({
req.setEncoding("utf8");
for await (const part of req) {
raw += part;
if (raw.length > 4096)
if (
raw.length >
(action === "decisions"
? 128 * 1024
: action === "request"
? 1024 * 1024
: 4096)
)
return json(res, 413, { error: "Request too large" });
}
const input = raw ? JSON.parse(raw) : {};
const nativeRoute = {
// Native Decisions puts the public revision in its existing credential header.
decisions: [
"/v3/builderlab/decisions",
"x-bb-session-credential",
],
viewer: [
"/v1/auth/hosted-site/native-session",
"x-buzz-builderlab-revision",
],
}[action];
if (nativeRoute) {
const [path, header] = nativeRoute;
const reply = await builderlab.request(
{
revision: req.headers[header],
path,
method: "POST",
body: raw,
community: req.headers["x-buzz-community"],
},
cancel.signal,
true,
);
res
.writeHead(reply.status, {
"Content-Type": "application/json",
"Cache-Control": "no-store",
})
.end(reply.body);
return;
}
if (action === "request")
return json(
res,
200,
await builderlab.request(input, cancel.signal),
);
// Scoped callers cannot submit an operation for a replacement account.
if (
input.revision !== undefined &&
input.revision !== builderlab.snapshot().revision
)
return json(res, 409, { error: "Builderlab session changed" });
if (action === "login")
return json(res, 200, {
auth: await builderlab.login(cancel.signal),
auth: await builderlab.login(),
});
if (action === "sign-out") {
builderlab.signOut();
return json(res, 200, {});
}
if (action === "bind")
return json(res, 200, await builderlab.bind());
const result = await builderlab.call(
action,
raw ? JSON.parse(raw) : {},
);
const result = await builderlab.call(action, input);
return result
? json(res, builderlabResponseStatus(result), result)
: json(res, 404, { error: "Unknown Builderlab route" });
Expand Down
4 changes: 4 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ the interface unless an available tool actually supports that action.
When helping someone explore a community, use only your available access and
say when information is incomplete or inaccessible. Never join, invite, share,
or change community content merely to explain how Buzz works.

### Builderlab in development

With the pinned-viewer development server, Settings → Plugins → Builderlab manages one sign-in for hosted communities, remote agents and apps. It survives app reloads while the server runs. Signing out or session expiry signs all three out. The credential stays on the server and is not saved to disk.
Loading
Loading