mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-20 21:17:19 +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,103 @@
|
||||
// /auth/mint endpoint tests. Codex-flagged: identity allowlist, capability
|
||||
// cap, rate-limit cap, self-service vs owner-granted distinction.
|
||||
|
||||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { mintForCaller } from '../src/auth-mint';
|
||||
import { SessionTokenStore } from '../src/session-tokens';
|
||||
import { grantIdentity } from '../src/allowlist';
|
||||
|
||||
let tmpDir: string;
|
||||
let listPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'ios-qa-mint-'));
|
||||
listPath = join(tmpDir, 'allowlist.json');
|
||||
});
|
||||
|
||||
describe('mintForCaller', () => {
|
||||
test('rejects unknown identity', async () => {
|
||||
const store = new SessionTokenStore();
|
||||
const r = await mintForCaller({
|
||||
callerIdentity: 'stranger@example.com',
|
||||
request: { capability: 'observe' },
|
||||
tokenStore: store,
|
||||
allowlistPath: listPath,
|
||||
});
|
||||
expect(r).toEqual({ error: 'identity_not_allowed' });
|
||||
});
|
||||
|
||||
test('mints at the requested tier when allowlisted at that tier', async () => {
|
||||
await grantIdentity({ identity: 'u@e.com', capability: 'mutate', path: listPath });
|
||||
const store = new SessionTokenStore();
|
||||
const r = await mintForCaller({
|
||||
callerIdentity: 'u@e.com',
|
||||
request: { capability: 'interact' },
|
||||
tokenStore: store,
|
||||
allowlistPath: listPath,
|
||||
});
|
||||
expect('error' in r).toBe(false);
|
||||
if ('error' in r) throw new Error('unexpected');
|
||||
expect(r.capability).toBe('mutate'); // returns the granted tier (higher covers interact)
|
||||
expect(r.session_token.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('refuses to mint above the allowlisted tier', async () => {
|
||||
await grantIdentity({ identity: 'observe-only@e.com', capability: 'observe', path: listPath });
|
||||
const store = new SessionTokenStore();
|
||||
const r = await mintForCaller({
|
||||
callerIdentity: 'observe-only@e.com',
|
||||
request: { capability: 'mutate' },
|
||||
tokenStore: store,
|
||||
allowlistPath: listPath,
|
||||
});
|
||||
expect(r).toEqual({ error: 'capability_insufficient' });
|
||||
});
|
||||
|
||||
test('rate limits hit at 11th mint per identity', async () => {
|
||||
await grantIdentity({ identity: 'spammer@e.com', capability: 'observe', path: listPath });
|
||||
const store = new SessionTokenStore();
|
||||
let lastError: unknown = null;
|
||||
let success = 0;
|
||||
for (let i = 0; i < 11; i++) {
|
||||
const r = await mintForCaller({
|
||||
callerIdentity: 'spammer@e.com',
|
||||
request: { capability: 'observe' },
|
||||
tokenStore: store,
|
||||
allowlistPath: listPath,
|
||||
});
|
||||
if ('error' in r) lastError = r;
|
||||
else success++;
|
||||
}
|
||||
expect(success).toBe(10);
|
||||
expect(lastError).toEqual({ error: 'rate_limited' });
|
||||
});
|
||||
|
||||
test('expired allowlist entries reject the mint', async () => {
|
||||
// Write an expired entry directly.
|
||||
const { saveAllowlist } = await import('../src/allowlist');
|
||||
await saveAllowlist({
|
||||
version: 1,
|
||||
entries: [{
|
||||
identity: 'expired@e.com',
|
||||
capabilities: ['restore'],
|
||||
expires_at: new Date(Date.now() - 60_000).toISOString(),
|
||||
}],
|
||||
}, listPath);
|
||||
const store = new SessionTokenStore();
|
||||
const r = await mintForCaller({
|
||||
callerIdentity: 'expired@e.com',
|
||||
request: { capability: 'observe' },
|
||||
tokenStore: store,
|
||||
allowlistPath: listPath,
|
||||
});
|
||||
expect(r).toEqual({ error: 'identity_not_allowed' });
|
||||
});
|
||||
});
|
||||
|
||||
import { afterEach } from 'bun:test';
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
Reference in New Issue
Block a user