mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-24 13:50:59 +02:00
fix(browse): CLI always sends explicit pair scopes via shared DEFAULT_PAIR_SCOPES
The effective pairing default lived in two places: the CLI omitted scopes unless --restrict was passed, and the server filled in its own literal. handlePairAgent now always sends an explicit scopes list and both sides reference one exported constant, DEFAULT_PAIR_SCOPES, so the default cannot silently drift again (pinned by a server-auth source tripwire). Three input traps closed in the same surface: - Bare --restrict (or --restrict swallowing the next flag) parsed as "no restriction" and silently granted FULL access, the opposite of the user's intent. validatePairAgentFlags rejects it pre-server, before any consent gate, so an arg error never boots a daemon. - A scopes list could smuggle the control scope past the explicit flag: --restrict "read,control" minted a control-scoped session with no --control. /pair now 400s on control in a scopes list without the control flag, and the CLI points the user at --control. - Option typos validated only at exchange time: createSetupKey stored any scope string and any rateLimit, so /pair returned 200 with a poisoned setup key whose failure surfaced to the REMOTE agent at /connect as a misleading "Invalid request body". Shared validation now runs in both creators and throws typed InvalidScopeError; /pair and /token 400 with the message, naming the bad scope or negative rateLimit. Also `opts.rateLimit || 10` became `?? 10` so the documented "0 = unlimited" survives the /pair path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f28bfd0158
commit
44e0b6f7be
@@ -409,3 +409,32 @@ describe('Server auth security', () => {
|
||||
expect(routeSrc).toContain('SameSite=Strict');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pair scope defaults and revocation surface', () => {
|
||||
// Regression: the CLI only sent scopes when --restrict was passed, so the
|
||||
// effective pairing default lived in two places (CLI omission + server
|
||||
// fallback) and could silently drift. Both sides must reference the shared
|
||||
// DEFAULT_PAIR_SCOPES constant, and the CLI must send scopes
|
||||
// unconditionally (the old conditional-spread shape is banned).
|
||||
test('/pair default and CLI pairing body share DEFAULT_PAIR_SCOPES', () => {
|
||||
const pairBlock = sliceBetween(SERVER_SRC, "url.pathname === '/pair'", "url.pathname === '/tunnel/start'");
|
||||
expect(pairBlock).toContain('DEFAULT_PAIR_SCOPES');
|
||||
const cliBlock = sliceBetween(CLI_SRC, 'async function handlePairAgent', 'Determine the URL to use');
|
||||
expect(cliBlock).toContain('DEFAULT_PAIR_SCOPES');
|
||||
expect(cliBlock).not.toContain('...(restrict ? { scopes');
|
||||
});
|
||||
|
||||
// control is the only scope behind an explicit flag; a scopes list must
|
||||
// not be able to smuggle it into a pairing grant.
|
||||
test('/pair rejects control inside a scopes list without the control flag', () => {
|
||||
const pairBlock = sliceBetween(SERVER_SRC, "url.pathname === '/pair'", "url.pathname === '/tunnel/start'");
|
||||
expect(pairBlock).toContain("pairBody.scopes.includes('control')");
|
||||
});
|
||||
|
||||
// CLI-encoded clientIds (spaces, UTF-8) must round-trip through the revoke
|
||||
// route; slicing the raw pathname 404s on every encoded name.
|
||||
test('DELETE /token decodes the clientId path segment', () => {
|
||||
const revokeBlock = sliceBetween(SERVER_SRC, "url.pathname.startsWith('/token/')", "url.pathname === '/agents'");
|
||||
expect(revokeBlock).toContain('decodeURIComponent');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user