types.ts
91 lines · ios-qa/daemon/src/types.ts at 3f79d81
1// Shared types for the ios-qa daemon.
3export type Capability = 'observe' | 'interact' | 'mutate' | 'restore';
5export const CAPABILITY_ORDER: Record<Capability, number> = {
6 observe: 0,
7 interact: 1,
8 mutate: 2,
9 restore: 3,
10};
12export function capabilityCovers(have: Capability, need: Capability): boolean {
13 return CAPABILITY_ORDER[have] >= CAPABILITY_ORDER[need];
14}
16export interface AllowlistEntry {
17 identity: string;
18 capabilities: Capability[];
19 expires_at: string | null;
20 note?: string;
21}
23export interface Allowlist {
24 version: 1;
25 entries: AllowlistEntry[];
26}
28export interface SessionToken {
29 token: string;
30 identity: string;
31 capability: Capability;
32 expires_at: number; // epoch ms
33 device_udid: string | null;
34 origin: 'self_service' | 'owner_granted';
35}
37export interface AuditRow {
38 ts: string;
39 identity: string;
40 device_udid: string;
41 endpoint: string;
42 session_id: string;
43 capability: Capability;
44 request_id: string;
45 status: number;
46}
48export interface AttemptRow {
49 ts: string;
50 identity_canon: string; // sha256 salted — never the raw identity
51 endpoint: string;
52 reason: 'no_token' | 'invalid_token' | 'expired_token' | 'identity_not_allowed' |
53 'capability_insufficient' | 'rate_limited' | 'allowlist_violation' |
54 'tailnet_socket_missing' | 'whois_unparseable';
55}
57export interface WhoIsResult {
58 identity: string; // canonicalized: "user@example.com" or "tag:<name>" or "node:<key>"
59 raw: unknown;
60}
62// Path allowlist for tailnet listener — by capability tier.
63// Each endpoint is mapped to the MINIMUM tier required.
64export const TAILNET_ENDPOINT_TIERS: Record<string, Capability> = {
65 'GET /healthz': 'observe',
66 'POST /auth/mint': 'observe', // any allowlisted caller can attempt; daemon then filters by tier
67 'POST /auth/revoke': 'observe', // own-session revoke
68 'GET /screenshot': 'observe',
69 'GET /elements': 'observe',
70 'GET /state/snapshot': 'observe',
71 'GET /state/*': 'observe',
72 'POST /session/acquire': 'interact',
73 'POST /session/release': 'interact',
74 'POST /session/heartbeat': 'interact',
75 'POST /tap': 'interact',
76 'POST /swipe': 'interact',
77 'POST /type': 'interact',
78 'POST /state/*': 'mutate',
79 'POST /state/restore': 'restore',
80};
82export function tierForRoute(method: string, path: string): Capability | null {
83 const exact = `${method} ${path}`;
84 if (TAILNET_ENDPOINT_TIERS[exact]) return TAILNET_ENDPOINT_TIERS[exact];
85 // Wildcard /state/*
86 if (path.startsWith('/state/') && path !== '/state/snapshot' && path !== '/state/restore') {
87 if (method === 'GET') return 'observe';
88 if (method === 'POST') return 'mutate';
89 }
90 return null; // not allowlisted on tailnet
91}