mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 23:30:09 +02:00
3126363c2c
On-demand daemon spawns when /ios-qa needs it (single-instance flock + readiness protocol). Owns tailnet ingress: fail-closed tailscaled LocalAPI probe, dual-track /auth/mint (self-service for allowlisted identities, owner-granted via CLI), capability-tier allowlist (observe/interact/mutate/restore), 1h default session TTL (24h hard cap), audit log of every authenticated mutating tailnet request, hashed-identity attempts log. iOS StateServer never directly binds tailnet — identity validation lives Mac-side because iPhones can't reach tailscaled. 67 unit/integration tests covering session-lock concurrency, capability enforcement, fail-closed probe, identity canonicalization, body limits, and boot-token leak proofs.
48 lines
2.4 KiB
TypeScript
48 lines
2.4 KiB
TypeScript
// Tailnet endpoint allowlist + capability tier classification tests.
|
|
//
|
|
// Codex flagged: "tailnet listener allowlist is too broad. Remote agents
|
|
// should not get /state/* by default. Split capabilities: observe, interact,
|
|
// mutate state, restore state."
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { classifyRoute } from '../src/proxy';
|
|
|
|
describe('classifyRoute', () => {
|
|
test('healthz, screenshot, elements, snapshot are observe-tier', () => {
|
|
expect(classifyRoute('GET', '/healthz').requiredCapability).toBe('observe');
|
|
expect(classifyRoute('GET', '/screenshot').requiredCapability).toBe('observe');
|
|
expect(classifyRoute('GET', '/elements').requiredCapability).toBe('observe');
|
|
expect(classifyRoute('GET', '/state/snapshot').requiredCapability).toBe('observe');
|
|
expect(classifyRoute('GET', '/state/anyKey').requiredCapability).toBe('observe');
|
|
});
|
|
|
|
test('tap, swipe, type, session ops are interact-tier', () => {
|
|
expect(classifyRoute('POST', '/tap').requiredCapability).toBe('interact');
|
|
expect(classifyRoute('POST', '/swipe').requiredCapability).toBe('interact');
|
|
expect(classifyRoute('POST', '/type').requiredCapability).toBe('interact');
|
|
expect(classifyRoute('POST', '/session/acquire').requiredCapability).toBe('interact');
|
|
expect(classifyRoute('POST', '/session/release').requiredCapability).toBe('interact');
|
|
expect(classifyRoute('POST', '/session/heartbeat').requiredCapability).toBe('interact');
|
|
});
|
|
|
|
test('arbitrary state writes are mutate-tier', () => {
|
|
expect(classifyRoute('POST', '/state/userIsLoggedIn').requiredCapability).toBe('mutate');
|
|
expect(classifyRoute('POST', '/state/anyField').requiredCapability).toBe('mutate');
|
|
});
|
|
|
|
test('state/restore is restore-tier (highest)', () => {
|
|
expect(classifyRoute('POST', '/state/restore').requiredCapability).toBe('restore');
|
|
});
|
|
|
|
test('mint endpoint is observe-tier (minimum bar to attempt mint)', () => {
|
|
expect(classifyRoute('POST', '/auth/mint').requiredCapability).toBe('observe');
|
|
});
|
|
|
|
test('non-allowlisted endpoints return allowed=false', () => {
|
|
expect(classifyRoute('POST', '/auth/sessions').allowed).toBe(false);
|
|
expect(classifyRoute('GET', '/random').allowed).toBe(false);
|
|
expect(classifyRoute('DELETE', '/anything').allowed).toBe(false);
|
|
expect(classifyRoute('GET', '/auth/sessions').allowed).toBe(false); // loopback-only
|
|
});
|
|
});
|