mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-17 04:57:20 +02:00
feat(ios): Mac-side daemon (bun/TS) for Tailscale identity gating + USB proxy
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.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// tailscaled LocalAPI client tests. Codex-flagged: identity canonicalization
|
||||
// for user / tag / node-key forms, fail-closed semantics on missing socket
|
||||
// or unparseable response.
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { canonicalize, probeTailscale } from '../src/tailscale-localapi';
|
||||
|
||||
describe('canonicalize', () => {
|
||||
test('returns lowercased user email when UserProfile.LoginName present', () => {
|
||||
const out = canonicalize({
|
||||
Node: { Tags: undefined },
|
||||
UserProfile: { LoginName: 'Alice@Example.COM' },
|
||||
});
|
||||
expect(out).toBe('alice@example.com');
|
||||
});
|
||||
|
||||
test('returns tagged node identity when tags present (prefers tag over user)', () => {
|
||||
const out = canonicalize({
|
||||
Node: { Tags: ['tag:CI'] },
|
||||
UserProfile: { LoginName: 'admin@example.com' },
|
||||
});
|
||||
expect(out).toBe('tag:ci');
|
||||
});
|
||||
|
||||
test('handles tag without prefix', () => {
|
||||
const out = canonicalize({
|
||||
Node: { Tags: ['ci'] },
|
||||
});
|
||||
expect(out).toBe('tag:ci');
|
||||
});
|
||||
|
||||
test('returns node:<key> when no user and no tags', () => {
|
||||
const out = canonicalize({
|
||||
Node: { Key: 'nodekey:abcdef0123' },
|
||||
});
|
||||
expect(out).toBe('node:abcdef0123');
|
||||
});
|
||||
|
||||
test('returns null for unparseable response', () => {
|
||||
expect(canonicalize({})).toBeNull();
|
||||
expect(canonicalize({ Node: {} })).toBeNull();
|
||||
expect(canonicalize({ UserProfile: { LoginName: 'no-at-sign' } })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('probeTailscale', () => {
|
||||
test('fails closed when socket does not exist', async () => {
|
||||
const r = await probeTailscale('/tmp/does-not-exist-' + Math.random());
|
||||
expect(r.ok).toBe(false);
|
||||
// Reason may be 'socket_missing' or 'unreachable' depending on how the
|
||||
// OS/runtime surfaces a missing unix socket. Either is a fail-closed
|
||||
// outcome that prevents the daemon from opening the tailnet listener.
|
||||
expect(['socket_missing', 'unreachable']).toContain(r.reason);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user