Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
160
browse/test/sse-session-cookie.test.ts
Normal file
160
browse/test/sse-session-cookie.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Unit tests for the view-only SSE session cookie module.
|
||||
*
|
||||
* Verifies the registry lifecycle (mint/validate/expire), cookie flag
|
||||
* invariants (HttpOnly, SameSite=Strict, no Secure), token entropy, and
|
||||
* that scope is implicit (the registry has no cross-endpoint footprint
|
||||
* that could be used to escalate the cookie to a scoped token).
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
mintSseSessionToken, validateSseSessionToken, extractSseCookie,
|
||||
buildSseSetCookie, buildSseClearCookie, SSE_COOKIE_NAME,
|
||||
__resetSseSessions,
|
||||
} from '../src/sse-session-cookie';
|
||||
|
||||
const MODULE_SRC = fs.readFileSync(
|
||||
path.join(import.meta.dir, '../src/sse-session-cookie.ts'), 'utf-8'
|
||||
);
|
||||
|
||||
beforeEach(() => __resetSseSessions());
|
||||
|
||||
describe('SSE session cookie: mint + validate', () => {
|
||||
test('mint returns a token and an expiry', () => {
|
||||
const { token, expiresAt } = mintSseSessionToken();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.length).toBeGreaterThan(20);
|
||||
expect(expiresAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
test('mint uses 32 random bytes (256-bit entropy)', () => {
|
||||
// base64url of 32 bytes is 43 chars (no padding)
|
||||
const { token } = mintSseSessionToken();
|
||||
expect(token).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
||||
});
|
||||
|
||||
test('two mint calls produce different tokens', () => {
|
||||
const a = mintSseSessionToken();
|
||||
const b = mintSseSessionToken();
|
||||
expect(a.token).not.toBe(b.token);
|
||||
});
|
||||
|
||||
test('validate returns true for a just-minted token', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
expect(validateSseSessionToken(token)).toBe(true);
|
||||
});
|
||||
|
||||
test('validate returns false for an unknown token', () => {
|
||||
expect(validateSseSessionToken('not-a-real-token')).toBe(false);
|
||||
});
|
||||
|
||||
test('validate returns false for null/undefined/empty', () => {
|
||||
expect(validateSseSessionToken(null)).toBe(false);
|
||||
expect(validateSseSessionToken(undefined)).toBe(false);
|
||||
expect(validateSseSessionToken('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSE session cookie: TTL enforcement', () => {
|
||||
test('TTL is 30 minutes', () => {
|
||||
// Assert via source — the actual constant is module-private
|
||||
expect(MODULE_SRC).toContain('const TTL_MS = 30 * 60 * 1000');
|
||||
});
|
||||
|
||||
test('a token with artificially rewound expiry is rejected', () => {
|
||||
// Mint a token, then monkey-patch Date.now to simulate 31 minutes elapsed.
|
||||
const { token, expiresAt } = mintSseSessionToken();
|
||||
const originalNow = Date.now;
|
||||
try {
|
||||
Date.now = () => expiresAt + 1;
|
||||
expect(validateSseSessionToken(token)).toBe(false);
|
||||
} finally {
|
||||
Date.now = originalNow;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSE session cookie: cookie flag invariants', () => {
|
||||
test('Set-Cookie is HttpOnly', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
expect(buildSseSetCookie(token)).toContain('HttpOnly');
|
||||
});
|
||||
|
||||
test('Set-Cookie is SameSite=Strict', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
expect(buildSseSetCookie(token)).toContain('SameSite=Strict');
|
||||
});
|
||||
|
||||
test('Set-Cookie includes the token value', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
expect(buildSseSetCookie(token)).toContain(`${SSE_COOKIE_NAME}=${token}`);
|
||||
});
|
||||
|
||||
test('Set-Cookie Max-Age matches TTL', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
// 30 minutes = 1800 seconds
|
||||
expect(buildSseSetCookie(token)).toContain('Max-Age=1800');
|
||||
});
|
||||
|
||||
test('Set-Cookie does NOT set Secure (local HTTP daemon)', () => {
|
||||
const { token } = mintSseSessionToken();
|
||||
// Adding Secure would block the browser from ever sending the cookie
|
||||
// back to a 127.0.0.1 daemon over HTTP. If gstack ever moves to HTTPS,
|
||||
// add Secure then.
|
||||
expect(buildSseSetCookie(token)).not.toContain('Secure');
|
||||
});
|
||||
|
||||
test('Clear-Cookie has Max-Age=0', () => {
|
||||
expect(buildSseClearCookie()).toContain('Max-Age=0');
|
||||
expect(buildSseClearCookie()).toContain('HttpOnly');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSE session cookie: extract from request', () => {
|
||||
function mockReq(cookieHeader: string | null): Request {
|
||||
const headers = new Headers();
|
||||
if (cookieHeader !== null) headers.set('cookie', cookieHeader);
|
||||
return new Request('http://127.0.0.1/activity/stream', { headers });
|
||||
}
|
||||
|
||||
test('extracts the token when cookie is present', () => {
|
||||
const req = mockReq(`${SSE_COOKIE_NAME}=abc123`);
|
||||
expect(extractSseCookie(req)).toBe('abc123');
|
||||
});
|
||||
|
||||
test('returns null when no cookie header', () => {
|
||||
const req = mockReq(null);
|
||||
expect(extractSseCookie(req)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when cookie header has no gstack_sse', () => {
|
||||
const req = mockReq('other=x; unrelated=y');
|
||||
expect(extractSseCookie(req)).toBeNull();
|
||||
});
|
||||
|
||||
test('extracts gstack_sse from a multi-cookie header', () => {
|
||||
const req = mockReq(`other=x; ${SSE_COOKIE_NAME}=real-token; trailing=y`);
|
||||
expect(extractSseCookie(req)).toBe('real-token');
|
||||
});
|
||||
|
||||
test('handles tokens with base64url padding-like chars', () => {
|
||||
// real tokens contain A-Z, a-z, 0-9, _, -
|
||||
const req = mockReq(`${SSE_COOKIE_NAME}=AbCd-_xyz`);
|
||||
expect(extractSseCookie(req)).toBe('AbCd-_xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSE session cookie: scope isolation (prior learning cookie-picker-auth-isolation)', () => {
|
||||
test('the module exposes ONLY view-only functions, no scoped-token hooks', () => {
|
||||
// This is a contract guard: if someone later makes SSE session tokens
|
||||
// valid as scoped tokens (e.g., by exporting a helper that registers
|
||||
// them in the main token registry), a leaked cookie could execute
|
||||
// /command. The module must not import from token-registry.
|
||||
expect(MODULE_SRC).not.toContain("from './token-registry'");
|
||||
expect(MODULE_SRC).not.toContain('createToken');
|
||||
expect(MODULE_SRC).not.toContain('initRegistry');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user