Require the daemon secret and loopback Host/Origin on the design and iOS QA daemons (#4117)

* Require the daemon secret and loopback Host/Origin on the design and iOS QA daemons (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Name the changelog fragment after the branch (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Review-Finding: p1/red-team#2 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: refuse cross-site browser requests on the iOS QA loopback, show the expired page for a stale board secret, refuse requests without a Host (GST-1747)

Review-Finding: v1/outside-voice#1 reader=outside-voice
Review-Finding: v1/adversarial-native#2 reader=adversarial-native
Review-Finding: v1/adversarial-native#5 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: show the secret segment in the documented design board URL (GST-1747)

Review-Finding: v1/adversarial-native#1 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: keep the daemon token fixtures on one line so the credential scan reads them as code (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: cover the Sec-Fetch-Site refusal, the expired board page and the no-Host 403 in the changelog fragment (GST-1747)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-07 07:39:51 +00:00
1 parent de5cb247e0
commit 6cf00aee9c
21 files changed
+413 -89

No files matched your search

+1 -1
View File
@@ -148,7 +148,7 @@ export function createDesignReviewPicker({ cwd, deadlineAt }: { cwd: string; dea
throw new Error('Design board submission has no unambiguous offered action');
}
const urls = [...new Set(question.question.match(/https?:\/\/[^\s<>\[\]()]+/g) ?? [])];
if (urls.length !== 1 || !/^http:\/\/127\.0\.0\.1:[1-9][0-9]*\/boards\/[A-Za-z0-9_-]+\/$/.test(urls[0]!)) {
if (urls.length !== 1 || !/^http:\/\/127\.0\.0\.1:[1-9][0-9]*\/[0-9a-f]{64}\/boards\/[A-Za-z0-9_-]+\/$/.test(urls[0]!)) {
throw new Error('Design board submission requires one exact owned board URL');
}
const url = urls[0]!;
+3 -3
View File
@@ -39,10 +39,10 @@ afterEach(async () => {
}
});
async function board(port = daemon.port): Promise<PublishBoardResult> {
async function board(owner: SpawnedDaemon = daemon): Promise<PublishBoardResult> {
const directory = path.join(cwd, `board-${++boardNumber}`);
fs.mkdirSync(directory);
return publishBoard({ port, html: makeBoardHtml(directory) });
return publishBoard({ port: owner.port, token: owner.token, html: makeBoardHtml(directory) });
}
// These are the declared actor records, not relabeled historical questions.
@@ -365,7 +365,7 @@ test('a real foreign daemon port is refused without changing either board', asyn
fs.mkdirSync(path.dirname(otherState), { recursive: true });
const other = await spawnDaemonForTest({ stateFile: otherState });
daemons.push(other);
const foreign = await board(other.port);
const foreign = await board(other);
expect(() => picker()(question(foreign.url))).toThrow();
await expectUnsubmitted(published);
await expectUnsubmitted(foreign);