mirror of
https://github.com/block/buzz.git
synced 2026-11-13 04:41:12 +00:00
fix(projects): secure project request routing
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -80,13 +80,58 @@ test("isProjectHomeChannel is false for unbound channels", () => {
|
||||
assert.equal(isProjectHomeChannel(null, [project()]), false);
|
||||
});
|
||||
|
||||
test("findProjectHomeByChannelId prefers the oldest listed home", () => {
|
||||
test("findProjectHomeByChannelId rejects bare and unauthorized route assertions", () => {
|
||||
const base = {
|
||||
createdAt: 0,
|
||||
legacy: false,
|
||||
owner: OWNER,
|
||||
projectChannelId: "channel-a",
|
||||
visibility: "listed",
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
findProjectHomeByChannelId("channel-a", [
|
||||
{ ...base, id: "bare", repositories: [] },
|
||||
{
|
||||
...base,
|
||||
id: "unauthorized",
|
||||
owner: MAINTAINER,
|
||||
repositories: [{ channelId: "channel-a", owner: OWNER }],
|
||||
},
|
||||
]),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test("findProjectHomeByChannelId ignores an older unauthorized competitor", () => {
|
||||
const base = {
|
||||
createdAt: 0,
|
||||
legacy: false,
|
||||
owner: OWNER,
|
||||
projectChannelId: "channel-a",
|
||||
visibility: "listed",
|
||||
};
|
||||
const selected = findProjectHomeByChannelId("channel-a", [
|
||||
{ ...base, createdAt: 50, id: "attacker", repositories: [] },
|
||||
{
|
||||
...base,
|
||||
createdAt: 100,
|
||||
id: "authorized",
|
||||
repositories: [{ channelId: "channel-a", owner: OWNER }],
|
||||
},
|
||||
]);
|
||||
assert.equal(selected?.id, "authorized");
|
||||
});
|
||||
|
||||
test("findProjectHomeByChannelId prefers the oldest listed authoritative home", () => {
|
||||
const base = {
|
||||
createdAt: 0,
|
||||
legacy: false,
|
||||
owner: OWNER,
|
||||
projectChannelId: "channel-a",
|
||||
repositories: [{ channelId: "channel-a", owner: OWNER }],
|
||||
visibility: "listed",
|
||||
};
|
||||
const selected = findProjectHomeByChannelId("channel-a", [
|
||||
{ ...base, createdAt: 200, id: "later" },
|
||||
{ ...base, createdAt: 50, id: "hidden", visibility: "unlisted" },
|
||||
|
||||
@@ -9,7 +9,10 @@ export function findProjectHomeByChannelId(
|
||||
if (!channelId) return null;
|
||||
const matching = projects
|
||||
.filter(
|
||||
(project) => !project.legacy && project.projectChannelId === channelId,
|
||||
(project) =>
|
||||
!project.legacy &&
|
||||
project.projectChannelId === channelId &&
|
||||
hasAuthoritativeHomeBinding(project),
|
||||
)
|
||||
.sort((left, right) => left.createdAt - right.createdAt);
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
advanceProjectChannelRequestQueue,
|
||||
createProjectChannelRequestQueue,
|
||||
enqueueProjectChannelRequest,
|
||||
} from "./projectChannelRequestQueue.ts";
|
||||
|
||||
function candidate(requestId) {
|
||||
return {
|
||||
agentPubkey: `agent-${requestId}`,
|
||||
request: { requestId },
|
||||
};
|
||||
}
|
||||
|
||||
test("accepted requests advance in order while duplicate active requests stay suppressed", () => {
|
||||
const queue = createProjectChannelRequestQueue();
|
||||
const first = candidate("request-a");
|
||||
const second = candidate("request-b");
|
||||
|
||||
assert.equal(enqueueProjectChannelRequest(queue, first), first);
|
||||
assert.equal(enqueueProjectChannelRequest(queue, second), null);
|
||||
assert.equal(enqueueProjectChannelRequest(queue, first), null);
|
||||
assert.equal(advanceProjectChannelRequestQueue(queue), second);
|
||||
assert.equal(enqueueProjectChannelRequest(queue, first), null);
|
||||
assert.equal(advanceProjectChannelRequestQueue(queue), null);
|
||||
});
|
||||
45
desktop/src/features/projects/projectChannelRequestQueue.ts
Normal file
45
desktop/src/features/projects/projectChannelRequestQueue.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ProjectChannelRequest } from "@/features/projects/projectChannelRequest";
|
||||
|
||||
export type AcceptedProjectChannelRequest = {
|
||||
agentPubkey: string;
|
||||
request: ProjectChannelRequest;
|
||||
};
|
||||
|
||||
export type ProjectChannelRequestQueue = {
|
||||
activeRequestId: string | null;
|
||||
pending: AcceptedProjectChannelRequest[];
|
||||
seenRequestIds: Set<string>;
|
||||
};
|
||||
|
||||
export function createProjectChannelRequestQueue(): ProjectChannelRequestQueue {
|
||||
return {
|
||||
activeRequestId: null,
|
||||
pending: [],
|
||||
seenRequestIds: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
export function enqueueProjectChannelRequest(
|
||||
queue: ProjectChannelRequestQueue,
|
||||
candidate: AcceptedProjectChannelRequest,
|
||||
): AcceptedProjectChannelRequest | null {
|
||||
const requestId = candidate.request.requestId;
|
||||
if (queue.seenRequestIds.has(requestId)) return null;
|
||||
queue.seenRequestIds.add(requestId);
|
||||
|
||||
if (queue.activeRequestId === null) {
|
||||
queue.activeRequestId = requestId;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
queue.pending.push(candidate);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function advanceProjectChannelRequestQueue(
|
||||
queue: ProjectChannelRequestQueue,
|
||||
): AcceptedProjectChannelRequest | null {
|
||||
const next = queue.pending.shift() ?? null;
|
||||
queue.activeRequestId = next?.request.requestId ?? null;
|
||||
return next;
|
||||
}
|
||||
@@ -8,6 +8,12 @@ import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks";
|
||||
import { useProjectsQuery } from "@/features/projects/hooks";
|
||||
import type { ProjectChannelRequest } from "@/features/projects/projectChannelRequest";
|
||||
import {
|
||||
advanceProjectChannelRequestQueue,
|
||||
createProjectChannelRequestQueue,
|
||||
enqueueProjectChannelRequest,
|
||||
type AcceptedProjectChannelRequest,
|
||||
} from "@/features/projects/projectChannelRequestQueue";
|
||||
import { useAddProjectChannelMutation } from "@/features/projects/useAddProjectChannel";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
@@ -26,8 +32,7 @@ export function useProjectChannelRequests() {
|
||||
string | null
|
||||
>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const seenRequestIds = React.useRef(new Set<string>());
|
||||
const pendingRequestId = React.useRef<string | null>(null);
|
||||
const acceptedRequests = React.useRef(createProjectChannelRequestQueue());
|
||||
const bufferedRequests = React.useRef<
|
||||
Array<{ agentPubkey: string; request: ProjectChannelRequest }>
|
||||
>([]);
|
||||
@@ -42,17 +47,15 @@ export function useProjectChannelRequests() {
|
||||
channelsRef.current,
|
||||
agentPubkey,
|
||||
next.request.homeChannelId,
|
||||
) !== "accept" ||
|
||||
seenRequestIds.current.has(next.requestId)
|
||||
) !== "accept"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
seenRequestIds.current.add(next.requestId);
|
||||
if (pendingRequestId.current !== null) return;
|
||||
pendingRequestId.current = next.requestId;
|
||||
setError(null);
|
||||
setSourceAgentPubkey(agentPubkey);
|
||||
setRequest(next);
|
||||
const accepted = enqueueProjectChannelRequest(acceptedRequests.current, {
|
||||
agentPubkey,
|
||||
request: next,
|
||||
});
|
||||
if (accepted) showRequest(accepted);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -87,6 +90,16 @@ export function useProjectChannelRequests() {
|
||||
[],
|
||||
);
|
||||
|
||||
function showRequest(candidate: AcceptedProjectChannelRequest | null) {
|
||||
setError(null);
|
||||
setSourceAgentPubkey(candidate?.agentPubkey ?? null);
|
||||
setRequest(candidate?.request ?? null);
|
||||
}
|
||||
|
||||
function advanceRequestQueue() {
|
||||
showRequest(advanceProjectChannelRequestQueue(acceptedRequests.current));
|
||||
}
|
||||
|
||||
const project =
|
||||
request == null
|
||||
? null
|
||||
@@ -109,10 +122,7 @@ export function useProjectChannelRequests() {
|
||||
|
||||
function dismiss() {
|
||||
if (mutation.isPending) return;
|
||||
setRequest(null);
|
||||
setSourceAgentPubkey(null);
|
||||
pendingRequestId.current = null;
|
||||
setError(null);
|
||||
advanceRequestQueue();
|
||||
}
|
||||
|
||||
async function approve() {
|
||||
@@ -148,9 +158,7 @@ export function useProjectChannelRequests() {
|
||||
visibility: request.request.visibility,
|
||||
});
|
||||
toast.success(`Channel "#${request.request.name}" created.`);
|
||||
setRequest(null);
|
||||
setSourceAgentPubkey(null);
|
||||
pendingRequestId.current = null;
|
||||
advanceRequestQueue();
|
||||
} catch (nextError) {
|
||||
setError(
|
||||
nextError instanceof Error
|
||||
|
||||
Reference in New Issue
Block a user