pairling 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +5 -1
  2. package/payload/mac/SOURCE_BRANCH +1 -0
  3. package/payload/mac/SOURCE_DIRTY +1 -0
  4. package/payload/mac/SOURCE_REVISION +1 -0
  5. package/payload/mac/VERSION +1 -0
  6. package/payload/mac/companiond/integrations/__init__.py +1 -0
  7. package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
  8. package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
  9. package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
  10. package/payload/mac/companiond/live_activity_publisher.py +380 -0
  11. package/payload/mac/companiond/llm_route.py +108 -0
  12. package/payload/mac/companiond/local_mcp_bridge.py +156 -0
  13. package/payload/mac/companiond/model_status_contract.py +101 -0
  14. package/payload/mac/companiond/pairdrop_store.py +920 -0
  15. package/payload/mac/companiond/pairling_connectd_status.py +149 -0
  16. package/payload/mac/companiond/pairling_devices.py +459 -0
  17. package/payload/mac/companiond/pairling_pairing.py +404 -0
  18. package/payload/mac/companiond/pairling_relay_claims.py +232 -0
  19. package/payload/mac/companiond/pairling_tools.py +706 -0
  20. package/payload/mac/companiond/pairlingd.py +18438 -0
  21. package/payload/mac/companiond/providers/__init__.py +1 -0
  22. package/payload/mac/companiond/providers/base.py +255 -0
  23. package/payload/mac/companiond/providers/claude.py +127 -0
  24. package/payload/mac/companiond/providers/codex.py +124 -0
  25. package/payload/mac/companiond/providers/external.py +46 -0
  26. package/payload/mac/companiond/providers/registry.py +70 -0
  27. package/payload/mac/companiond/pty_broker.py +887 -0
  28. package/payload/mac/companiond/push_dispatcher.py +1990 -0
  29. package/payload/mac/companiond/push_event_catalog.py +566 -0
  30. package/payload/mac/companiond/request_proof.py +142 -0
  31. package/payload/mac/companiond/runtime_contract.py +47 -0
  32. package/payload/mac/companiond/runtime_manifest.py +197 -0
  33. package/payload/mac/companiond/runtime_paths.py +87 -0
  34. package/payload/mac/companiond/safety_monitor.py +542 -0
  35. package/payload/mac/companiond/sentinel_notifications.py +491 -0
  36. package/payload/mac/companiond/standard_push_publisher.py +516 -0
  37. package/payload/mac/companiond/substrate_status_contract.py +139 -0
  38. package/payload/mac/companiond/terminal_screen_backend.py +332 -0
  39. package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
  40. package/payload/mac/companiond/workstate_feed_contract.py +108 -0
  41. package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
  42. package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
  43. package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
  44. package/payload/mac/connectd/go.mod +51 -0
  45. package/payload/mac/connectd/go.sum +229 -0
  46. package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
  47. package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
  48. package/payload/mac/connectd/internal/runtime/config.go +99 -0
  49. package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
  50. package/payload/mac/connectd/internal/status/status.go +300 -0
  51. package/payload/mac/connectd/internal/status/status_test.go +263 -0
  52. package/payload/mac/guardian/companion-power-guardian.py +613 -0
  53. package/payload/mac/guardian/guardian_contract.py +67 -0
  54. package/payload/mac/install/bootstrap-first-run.sh +206 -0
  55. package/payload/mac/install/doctor.sh +660 -0
  56. package/payload/mac/install/install-runtime.sh +1241 -0
  57. package/payload/mac/install/render-launchd.py +119 -0
  58. package/payload/mac/install/uninstall-runtime.sh +136 -0
  59. package/payload/mac/mcp/phone_tools.py +210 -0
  60. package/payload/mac/packaging/bin/pairling +63 -0
  61. package/payload-manifest.json +255 -0
@@ -0,0 +1,300 @@
1
+ package status
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/http"
7
+ "net/url"
8
+ "regexp"
9
+ "strings"
10
+ "sync"
11
+ "time"
12
+ )
13
+
14
+ const (
15
+ SchemaVersion = 2
16
+ DefaultListenPort = 7773
17
+ DefaultConnectdVersion = "2026-05-24"
18
+ DefaultControlURLMode = "tailscale_saas"
19
+ CustomControlURLMode = "custom"
20
+ RouteIDPairlingConnect = "pairling-connect-tailnet"
21
+ RouteSourceConnectd = "pairling_connectd"
22
+ RouteKindTailnet = "tailnet"
23
+ RouteStatusReady = "ready"
24
+ )
25
+
26
+ type AdvertisedRoute struct {
27
+ ID string `json:"id"`
28
+ Kind string `json:"kind"`
29
+ Source string `json:"source"`
30
+ Priority int `json:"priority"`
31
+ BaseURL string `json:"base_url"`
32
+ Host string `json:"host"`
33
+ Port int `json:"port"`
34
+ Status string `json:"status"`
35
+ }
36
+
37
+ type Snapshot struct {
38
+ OK bool `json:"ok"`
39
+ SchemaVersion int `json:"schema_version"`
40
+ AuthState string `json:"auth_state"`
41
+ Hostname string `json:"hostname"`
42
+ TailnetIP string `json:"tailnet_ip,omitempty"`
43
+ TailnetIPCount int `json:"tailnet_ip_count"`
44
+ AuthURLPresent bool `json:"auth_url_present"`
45
+ ControlURLMode string `json:"control_url_mode"`
46
+ UpstreamReachable bool `json:"upstream_reachable"`
47
+ ListenerRunning bool `json:"listener_running"`
48
+ GatewayHealthy bool `json:"gateway_healthy"`
49
+ ListenPort int `json:"listen_port"`
50
+ ConnectdVersion string `json:"connectd_version"`
51
+ AdvertisedRoutes []AdvertisedRoute `json:"advertised_routes"`
52
+ LastError string `json:"last_error,omitempty"`
53
+ LastGatewayFailure string `json:"last_gateway_failure,omitempty"`
54
+ LastGatewayFailureAt string `json:"last_gateway_failure_at,omitempty"`
55
+ UpdatedAt string `json:"updated_at"`
56
+ }
57
+
58
+ type Store struct {
59
+ mu sync.RWMutex
60
+ snapshot Snapshot
61
+ authURL string
62
+ gatewaySuccessStreak int
63
+ }
64
+
65
+ func NewStore(hostname string) *Store {
66
+ return &Store{
67
+ snapshot: Snapshot{
68
+ OK: true,
69
+ SchemaVersion: SchemaVersion,
70
+ AuthState: "starting",
71
+ Hostname: hostname,
72
+ ControlURLMode: DefaultControlURLMode,
73
+ GatewayHealthy: true,
74
+ ListenPort: DefaultListenPort,
75
+ ConnectdVersion: DefaultConnectdVersion,
76
+ AdvertisedRoutes: []AdvertisedRoute{},
77
+ UpdatedAt: nowString(),
78
+ },
79
+ }
80
+ }
81
+
82
+ func (s *Store) SetControlURLMode(mode string) {
83
+ s.update(func(snapshot *Snapshot) {
84
+ if mode == "" {
85
+ mode = DefaultControlURLMode
86
+ }
87
+ snapshot.ControlURLMode = mode
88
+ })
89
+ }
90
+
91
+ func (s *Store) SetListenPort(port int) {
92
+ s.update(func(snapshot *Snapshot) {
93
+ if port <= 0 {
94
+ port = DefaultListenPort
95
+ }
96
+ snapshot.ListenPort = port
97
+ })
98
+ }
99
+
100
+ func (s *Store) SetConnectdVersion(version string) {
101
+ s.update(func(snapshot *Snapshot) {
102
+ if version == "" {
103
+ version = DefaultConnectdVersion
104
+ }
105
+ snapshot.ConnectdVersion = version
106
+ })
107
+ }
108
+
109
+ func (s *Store) SetAuthPending(message string) {
110
+ s.update(func(snapshot *Snapshot) {
111
+ snapshot.AuthState = "pending"
112
+ if authURL, ok := extractAuthURL(message); ok {
113
+ s.authURL = authURL
114
+ snapshot.AuthURLPresent = true
115
+ }
116
+ })
117
+ }
118
+
119
+ func (s *Store) SetAuthenticated() {
120
+ s.update(func(snapshot *Snapshot) {
121
+ snapshot.AuthState = "authenticated"
122
+ s.authURL = ""
123
+ snapshot.AuthURLPresent = false
124
+ })
125
+ }
126
+
127
+ func (s *Store) SetTailnetIP(ip string) {
128
+ s.update(func(snapshot *Snapshot) {
129
+ snapshot.TailnetIP = ip
130
+ if ip == "" {
131
+ snapshot.TailnetIPCount = 0
132
+ } else {
133
+ snapshot.TailnetIPCount = 1
134
+ }
135
+ })
136
+ }
137
+
138
+ func (s *Store) SetUpstreamReachable(reachable bool) {
139
+ s.update(func(snapshot *Snapshot) {
140
+ snapshot.UpstreamReachable = reachable
141
+ })
142
+ }
143
+
144
+ func (s *Store) SetListenerRunning(running bool) {
145
+ s.update(func(snapshot *Snapshot) {
146
+ snapshot.ListenerRunning = running
147
+ })
148
+ }
149
+
150
+ func (s *Store) RecordGatewayEvent(method, path string, status int, outcome string) {
151
+ s.update(func(snapshot *Snapshot) {
152
+ method = strings.ToUpper(strings.TrimSpace(method))
153
+ path = strings.TrimSpace(path)
154
+ outcome = strings.TrimSpace(outcome)
155
+ if gatewayEventIsFailure(path, status, outcome) {
156
+ s.gatewaySuccessStreak = 0
157
+ snapshot.GatewayHealthy = false
158
+ snapshot.LastGatewayFailure = redact(fmt.Sprintf("%s %s status=%d outcome=%s", method, path, status, outcome))
159
+ snapshot.LastGatewayFailureAt = nowString()
160
+ return
161
+ }
162
+ if gatewayEventIsRecoveryProbe(path, status, outcome) {
163
+ s.gatewaySuccessStreak = 1
164
+ snapshot.GatewayHealthy = true
165
+ snapshot.LastGatewayFailure = ""
166
+ snapshot.LastGatewayFailureAt = ""
167
+ return
168
+ }
169
+ })
170
+ }
171
+
172
+ func (s *Store) SetLastError(message string) {
173
+ s.update(func(snapshot *Snapshot) {
174
+ snapshot.LastError = redact(message)
175
+ if message != "" {
176
+ snapshot.OK = false
177
+ }
178
+ })
179
+ }
180
+
181
+ func (s *Store) Snapshot() Snapshot {
182
+ s.mu.RLock()
183
+ defer s.mu.RUnlock()
184
+ return s.snapshot
185
+ }
186
+
187
+ func (s *Store) AuthURLForOpen() (string, bool) {
188
+ s.mu.RLock()
189
+ defer s.mu.RUnlock()
190
+ if s.authURL == "" || !s.snapshot.AuthURLPresent {
191
+ return "", false
192
+ }
193
+ return validateAuthURL(s.authURL)
194
+ }
195
+
196
+ func (s *Store) Handler() http.Handler {
197
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
198
+ if r.Method != http.MethodGet {
199
+ http.Error(w, "GET required", http.StatusMethodNotAllowed)
200
+ return
201
+ }
202
+ w.Header().Set("Content-Type", "application/json")
203
+ _ = json.NewEncoder(w).Encode(s.Snapshot())
204
+ })
205
+ }
206
+
207
+ func (s *Store) update(fn func(*Snapshot)) {
208
+ s.mu.Lock()
209
+ defer s.mu.Unlock()
210
+ fn(&s.snapshot)
211
+ s.snapshot.AdvertisedRoutes = advertisedRoutes(s.snapshot)
212
+ s.snapshot.UpdatedAt = nowString()
213
+ }
214
+
215
+ func advertisedRoutes(snapshot Snapshot) []AdvertisedRoute {
216
+ if snapshot.AuthState != "authenticated" || snapshot.TailnetIP == "" || !snapshot.ListenerRunning || !snapshot.UpstreamReachable || !snapshot.GatewayHealthy {
217
+ return []AdvertisedRoute{}
218
+ }
219
+ port := snapshot.ListenPort
220
+ if port <= 0 {
221
+ port = DefaultListenPort
222
+ }
223
+ return []AdvertisedRoute{{
224
+ ID: RouteIDPairlingConnect,
225
+ Kind: RouteKindTailnet,
226
+ Source: RouteSourceConnectd,
227
+ Priority: 100,
228
+ BaseURL: fmt.Sprintf("http://%s:%d", snapshot.TailnetIP, port),
229
+ Host: snapshot.TailnetIP,
230
+ Port: port,
231
+ Status: RouteStatusReady,
232
+ }}
233
+ }
234
+
235
+ func gatewayEventIsFailure(path string, status int, outcome string) bool {
236
+ path = strings.TrimSpace(path)
237
+ if path != "/routez" {
238
+ return false
239
+ }
240
+ outcome = strings.ToLower(strings.TrimSpace(outcome))
241
+ if outcome == "upstream_error" || outcome == "validation_failed" || outcome == "timeout" {
242
+ return true
243
+ }
244
+ if status >= 500 {
245
+ return true
246
+ }
247
+ return false
248
+ }
249
+
250
+ func gatewayEventIsRecoveryProbe(path string, status int, outcome string) bool {
251
+ outcome = strings.ToLower(strings.TrimSpace(outcome))
252
+ return path == "/routez" && outcome == "forwarded" && status >= 200 && status < 400
253
+ }
254
+
255
+ func nowString() string {
256
+ return time.Now().UTC().Format(time.RFC3339)
257
+ }
258
+
259
+ var secretPattern = regexp.MustCompile(`(?i)(sk-[A-Za-z0-9._-]+|[A-Za-z0-9._-]*secret[A-Za-z0-9._-]*|[A-Za-z0-9._-]*token[A-Za-z0-9._-]*)`)
260
+ var authURLPattern = regexp.MustCompile(`https://login\.tailscale\.com/a/[A-Za-z0-9._~!$&'()*+,;=:@%/?-]+`)
261
+ var bearerPattern = regexp.MustCompile(`(?i)Bearer\s+[A-Za-z0-9._~+/=-]+`)
262
+ var tailscaleAuthKeyPattern = regexp.MustCompile(`(?i)tskey-[A-Za-z0-9._-]+`)
263
+
264
+ func extractAuthURL(value string) (string, bool) {
265
+ raw := authURLPattern.FindString(value)
266
+ if raw == "" {
267
+ return "", false
268
+ }
269
+ return validateAuthURL(raw)
270
+ }
271
+
272
+ func ValidAuthURL(value string) bool {
273
+ _, ok := validateAuthURL(value)
274
+ return ok
275
+ }
276
+
277
+ func validateAuthURL(raw string) (string, bool) {
278
+ parsed, err := url.Parse(raw)
279
+ if err != nil {
280
+ return "", false
281
+ }
282
+ if parsed.Scheme != "https" || strings.ToLower(parsed.Host) != "login.tailscale.com" {
283
+ return "", false
284
+ }
285
+ if parsed.User != nil || parsed.Fragment != "" || !strings.HasPrefix(parsed.Path, "/a/") || len(parsed.Path) <= len("/a/") {
286
+ return "", false
287
+ }
288
+ return raw, true
289
+ }
290
+
291
+ func redact(value string) string {
292
+ value = authURLPattern.ReplaceAllString(value, "https://login.tailscale.com/a/[redacted]")
293
+ value = bearerPattern.ReplaceAllString(value, "Bearer [redacted]")
294
+ value = tailscaleAuthKeyPattern.ReplaceAllString(value, "[redacted]")
295
+ return secretPattern.ReplaceAllString(value, "[redacted]")
296
+ }
297
+
298
+ func Redact(value string) string {
299
+ return redact(value)
300
+ }
@@ -0,0 +1,263 @@
1
+ package status
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ func TestStoreServesHelperReadableSnapshotWithoutSecrets(t *testing.T) {
12
+ store := NewStore("pairling-inst-abcdef")
13
+ store.SetAuthPending("https://login.tailscale.com/a/secret-auth-token")
14
+ store.SetTailnetIP("100.64.0.10")
15
+ store.SetUpstreamReachable(true)
16
+ store.SetListenerRunning(true)
17
+ store.SetLastError("provider key sk-secret leaked elsewhere")
18
+
19
+ rec := httptest.NewRecorder()
20
+ store.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
21
+
22
+ if rec.Code != http.StatusOK {
23
+ t.Fatalf("status = %d, want 200", rec.Code)
24
+ }
25
+ var body Snapshot
26
+ if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
27
+ t.Fatal(err)
28
+ }
29
+ if body.Hostname != "pairling-inst-abcdef" || body.TailnetIP != "100.64.0.10" {
30
+ t.Fatalf("bad snapshot: %+v", body)
31
+ }
32
+ if body.SchemaVersion != 2 || body.AuthState != "pending" || !body.AuthURLPresent || !body.UpstreamReachable || !body.ListenerRunning {
33
+ t.Fatalf("bad status fields: %+v", body)
34
+ }
35
+ if body.TailnetIPCount != 1 || body.ControlURLMode != DefaultControlURLMode || body.ListenPort != DefaultListenPort {
36
+ t.Fatalf("bad v2 status fields: %+v", body)
37
+ }
38
+ if len(body.AdvertisedRoutes) != 0 {
39
+ t.Fatalf("pending auth should not advertise routes: %+v", body.AdvertisedRoutes)
40
+ }
41
+ raw := rec.Body.String()
42
+ for _, forbidden := range []string{"secret-auth-token", "sk-secret"} {
43
+ if strings.Contains(raw, forbidden) {
44
+ t.Fatalf("status leaked %q: %s", forbidden, raw)
45
+ }
46
+ }
47
+ }
48
+
49
+ func TestStoreAdvertisesRouteOnlyWhenReady(t *testing.T) {
50
+ store := NewStore("pairling-inst-abcdef")
51
+ store.SetAuthPending("open https://login.tailscale.com/a/example-token")
52
+ store.SetListenPort(7773)
53
+ store.SetTailnetIP("100.64.0.10")
54
+ store.SetListenerRunning(true)
55
+ store.SetUpstreamReachable(true)
56
+
57
+ if got := store.Snapshot().AdvertisedRoutes; len(got) != 0 {
58
+ t.Fatalf("unauthenticated status advertised routes: %+v", got)
59
+ }
60
+
61
+ store.SetAuthenticated()
62
+ snapshot := store.Snapshot()
63
+ if len(snapshot.AdvertisedRoutes) != 1 {
64
+ t.Fatalf("advertised routes = %+v, want one ready route", snapshot.AdvertisedRoutes)
65
+ }
66
+ route := snapshot.AdvertisedRoutes[0]
67
+ if route.ID != RouteIDPairlingConnect || route.Source != RouteSourceConnectd || route.Kind != RouteKindTailnet {
68
+ t.Fatalf("bad route identity: %+v", route)
69
+ }
70
+ if route.BaseURL != "http://100.64.0.10:7773" || route.Host != "100.64.0.10" || route.Port != 7773 || route.Status != RouteStatusReady {
71
+ t.Fatalf("bad route fields: %+v", route)
72
+ }
73
+ if snapshot.AuthURLPresent {
74
+ t.Fatalf("authenticated snapshot should clear auth URL presence: %+v", snapshot)
75
+ }
76
+ if authURL, ok := store.AuthURLForOpen(); ok || authURL != "" {
77
+ t.Fatalf("authenticated status should clear raw auth URL, got ok=%t url=%q", ok, authURL)
78
+ }
79
+ }
80
+
81
+ func TestStoreSuppressesAdvertisedRouteAfterRecentGatewayValidationFailure(t *testing.T) {
82
+ store := NewStore("pairling-inst-abcdef")
83
+ store.SetAuthenticated()
84
+ store.SetTailnetIP("100.64.0.10")
85
+ store.SetListenerRunning(true)
86
+ store.SetUpstreamReachable(true)
87
+
88
+ store.RecordGatewayEvent("GET", "/routez", 502, "upstream_error")
89
+ snapshot := store.Snapshot()
90
+ if snapshot.GatewayHealthy {
91
+ t.Fatalf("gateway should be unhealthy after route validation failure: %+v", snapshot)
92
+ }
93
+ if snapshot.LastGatewayFailure == "" {
94
+ t.Fatalf("gateway failure should be exposed as redacted diagnostic: %+v", snapshot)
95
+ }
96
+ if got := snapshot.AdvertisedRoutes; len(got) != 0 {
97
+ t.Fatalf("gateway failure advertised routes: %+v", got)
98
+ }
99
+
100
+ store.RecordGatewayEvent("GET", "/routez", 200, "forwarded")
101
+ snapshot = store.Snapshot()
102
+ if !snapshot.GatewayHealthy {
103
+ t.Fatalf("gateway should recover after fresh successful routez proof: %+v", snapshot)
104
+ }
105
+ if len(snapshot.AdvertisedRoutes) != 1 {
106
+ t.Fatalf("recovered gateway did not advertise route: %+v", snapshot.AdvertisedRoutes)
107
+ }
108
+ }
109
+
110
+ func TestStoreDoesNotPoisonRouteHealthForProductEndpointFailures(t *testing.T) {
111
+ store := NewStore("pairling-inst-abcdef")
112
+ store.SetAuthenticated()
113
+ store.SetTailnetIP("100.64.0.10")
114
+ store.SetListenerRunning(true)
115
+ store.SetUpstreamReachable(true)
116
+
117
+ store.RecordGatewayEvent("POST", "/push/test", 500, "upstream_error")
118
+ snapshot := store.Snapshot()
119
+ if !snapshot.GatewayHealthy {
120
+ t.Fatalf("product endpoint 5xx should not poison route health: %+v", snapshot)
121
+ }
122
+ if snapshot.LastGatewayFailure != "" {
123
+ t.Fatalf("product endpoint failure should not be stored as route failure: %+v", snapshot)
124
+ }
125
+ if len(snapshot.AdvertisedRoutes) != 1 {
126
+ t.Fatalf("product endpoint failure should not suppress advertised routes: %+v", snapshot.AdvertisedRoutes)
127
+ }
128
+ }
129
+
130
+ func TestStoreRecoversRouteHealthAfterFreshSuccessfulRouteProof(t *testing.T) {
131
+ store := NewStore("pairling-inst-abcdef")
132
+ store.SetAuthenticated()
133
+ store.SetTailnetIP("100.64.0.10")
134
+ store.SetListenerRunning(true)
135
+ store.SetUpstreamReachable(true)
136
+
137
+ store.RecordGatewayEvent("GET", "/routez", 502, "upstream_error")
138
+ if store.Snapshot().GatewayHealthy {
139
+ t.Fatalf("route proof failure should mark route unhealthy")
140
+ }
141
+
142
+ store.RecordGatewayEvent("GET", "/routez", 200, "forwarded")
143
+ snapshot := store.Snapshot()
144
+ if !snapshot.GatewayHealthy {
145
+ t.Fatalf("fresh routez proof should recover route health: %+v", snapshot)
146
+ }
147
+ if len(snapshot.AdvertisedRoutes) != 1 {
148
+ t.Fatalf("fresh routez proof should restore advertised route: %+v", snapshot.AdvertisedRoutes)
149
+ }
150
+ }
151
+
152
+ func TestStoreKeepsRawAuthURLPrivateForLocalOpenOnly(t *testing.T) {
153
+ store := NewStore("pairling-inst-abcdef")
154
+ rawAuthURL := "https://login.tailscale.com/a/secret-auth-token?next=pairling"
155
+ store.SetAuthPending("Approve Pairling Connect at " + rawAuthURL)
156
+
157
+ authURL, ok := store.AuthURLForOpen()
158
+ if !ok || authURL != rawAuthURL {
159
+ t.Fatalf("auth URL for local open = %q, %t; want raw in-memory URL", authURL, ok)
160
+ }
161
+
162
+ rec := httptest.NewRecorder()
163
+ store.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
164
+ if strings.Contains(rec.Body.String(), "secret-auth-token") || strings.Contains(rec.Body.String(), "login.tailscale.com/a/") {
165
+ t.Fatalf("status response leaked raw auth URL: %s", rec.Body.String())
166
+ }
167
+ }
168
+
169
+ func TestStoreRejectsInvalidAuthURLForLocalOpen(t *testing.T) {
170
+ store := NewStore("pairling-inst-abcdef")
171
+ store.SetAuthPending("Approve Pairling Connect at http://login.tailscale.com/a/not-secure")
172
+
173
+ if authURL, ok := store.AuthURLForOpen(); ok || authURL != "" {
174
+ t.Fatalf("invalid auth URL should not be available for local open, got ok=%t url=%q", ok, authURL)
175
+ }
176
+
177
+ snapshot := store.Snapshot()
178
+ if snapshot.AuthURLPresent {
179
+ t.Fatalf("invalid auth URL should not set presence: %+v", snapshot)
180
+ }
181
+ if !ValidAuthURL("https://login.tailscale.com/a/example-token?next=pairling") {
182
+ t.Fatal("valid auth URL was rejected")
183
+ }
184
+ if ValidAuthURL("https://login.tailscale.com.evil/a/example-token") {
185
+ t.Fatal("evil host was accepted")
186
+ }
187
+ }
188
+
189
+ func TestStoreSuppressesAdvertisedRouteForDegradedStates(t *testing.T) {
190
+ cases := []struct {
191
+ name string
192
+ configure func(*Store)
193
+ }{
194
+ {
195
+ name: "auth pending",
196
+ configure: func(store *Store) {
197
+ store.SetAuthPending("open https://login.tailscale.com/a/example")
198
+ store.SetTailnetIP("100.64.0.10")
199
+ store.SetListenerRunning(true)
200
+ store.SetUpstreamReachable(true)
201
+ },
202
+ },
203
+ {
204
+ name: "missing tailnet IP",
205
+ configure: func(store *Store) {
206
+ store.SetAuthenticated()
207
+ store.SetListenerRunning(true)
208
+ store.SetUpstreamReachable(true)
209
+ },
210
+ },
211
+ {
212
+ name: "listener down",
213
+ configure: func(store *Store) {
214
+ store.SetAuthenticated()
215
+ store.SetTailnetIP("100.64.0.10")
216
+ store.SetUpstreamReachable(true)
217
+ },
218
+ },
219
+ {
220
+ name: "upstream down",
221
+ configure: func(store *Store) {
222
+ store.SetAuthenticated()
223
+ store.SetTailnetIP("100.64.0.10")
224
+ store.SetListenerRunning(true)
225
+ },
226
+ },
227
+ }
228
+
229
+ for _, tc := range cases {
230
+ t.Run(tc.name, func(t *testing.T) {
231
+ store := NewStore("pairling-inst-abcdef")
232
+ tc.configure(store)
233
+ if got := store.Snapshot().AdvertisedRoutes; len(got) != 0 {
234
+ t.Fatalf("degraded state advertised routes: %+v", got)
235
+ }
236
+ })
237
+ }
238
+ }
239
+
240
+ func TestRedactRemovesAuthURLsAndBearerMaterial(t *testing.T) {
241
+ cases := map[string][]string{
242
+ "open https://login.tailscale.com/a/abc123DEF456 to authenticate": {
243
+ "abc123DEF456",
244
+ },
245
+ "Authorization: Bearer device-token-value": {
246
+ "device-token-value",
247
+ },
248
+ "auth key tskey-auth-k9uFq_secret_part": {
249
+ "tskey-auth-k9uFq_secret_part",
250
+ },
251
+ }
252
+
253
+ for input, forbidden := range cases {
254
+ t.Run(input, func(t *testing.T) {
255
+ redacted := Redact(input)
256
+ for _, value := range forbidden {
257
+ if strings.Contains(redacted, value) {
258
+ t.Fatalf("redacted output leaked %q: %s", value, redacted)
259
+ }
260
+ }
261
+ })
262
+ }
263
+ }