pairling 0.2.3 → 0.2.5
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.
- package/package.json +7 -7
- package/payload/mac/SOURCE_BRANCH +1 -1
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/runtime_manifest.py +1 -1
- package/payload/mac/connectd/internal/gateway/proxy.go +10 -3
- package/payload/mac/connectd/internal/gateway/proxy_test.go +49 -37
- package/payload/mac/install/install-runtime.sh +154 -22
- package/payload-manifest.json +11 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairling",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Pair your iPhone with the AI coding agents running on your Mac. CLI and local runtime installer for the Pairling iOS app.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pairling",
|
|
@@ -15,10 +15,6 @@
|
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://pairling.dev/support"
|
|
17
17
|
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "https://pairling.dev"
|
|
21
|
-
},
|
|
22
18
|
"license": "UNLICENSED",
|
|
23
19
|
"author": "Pairling (https://pairling.dev)",
|
|
24
20
|
"type": "module",
|
|
@@ -39,8 +35,12 @@
|
|
|
39
35
|
"publishConfig": {
|
|
40
36
|
"access": "public"
|
|
41
37
|
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/mergimg0/pairling-helper"
|
|
41
|
+
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@pairling/runtime-darwin-arm64": "0.2.
|
|
44
|
-
"@pairling/runtime-darwin-x64": "0.2.
|
|
43
|
+
"@pairling/runtime-darwin-arm64": "0.2.5",
|
|
44
|
+
"@pairling/runtime-darwin-x64": "0.2.5"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
HEAD
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2f094bd
|
package/payload/mac/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.5
|
|
@@ -165,7 +165,7 @@ def build_manifest_payload(
|
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
167
|
"endpoints": {
|
|
168
|
-
"public": ["/health", "/manifest", "/pair/start", "/pair/claim"],
|
|
168
|
+
"public": ["/health", "/manifest", "/pair/start", "/pair/claim", "/pair/psk-claim"],
|
|
169
169
|
"authenticated": [
|
|
170
170
|
"/manifest",
|
|
171
171
|
"/sessions",
|
|
@@ -235,7 +235,7 @@ func (h *Handler) allowedForAnyMethod(path string, header http.Header) bool {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
func (h *Handler) requestBodyLimit(method, path string) int64 {
|
|
238
|
-
if method == http.MethodPost && path
|
|
238
|
+
if method == http.MethodPost && isPrePairClaimPath(path) && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect) {
|
|
239
239
|
if h.maxBodyBytes <= 0 || prePairMaxBodyBytes < h.maxBodyBytes {
|
|
240
240
|
return prePairMaxBodyBytes
|
|
241
241
|
}
|
|
@@ -259,7 +259,7 @@ func (h *Handler) requestBodyLimit(method, path string) int64 {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
func (h *Handler) rateLimitPath(method, path string) bool {
|
|
262
|
-
return method == http.MethodPost && path
|
|
262
|
+
return method == http.MethodPost && isPrePairClaimPath(path) && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect)
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
func prePairAllowed(method, path string) bool {
|
|
@@ -273,6 +273,10 @@ func prePairAllowed(method, path string) bool {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
func isPrePairClaimPath(path string) bool {
|
|
277
|
+
return path == "/pair/claim" || path == "/pair/psk-claim"
|
|
278
|
+
}
|
|
279
|
+
|
|
276
280
|
func hasBearer(header http.Header) bool {
|
|
277
281
|
return strings.HasPrefix(header.Get("Authorization"), "Bearer ")
|
|
278
282
|
}
|
|
@@ -491,6 +495,7 @@ var postPaths = map[string]bool{
|
|
|
491
495
|
"/open": true,
|
|
492
496
|
"/orchestrations": true,
|
|
493
497
|
"/pair/claim": true,
|
|
498
|
+
"/pair/psk-claim": true,
|
|
494
499
|
"/pair/revoke": true,
|
|
495
500
|
"/pair/rotate-token": true,
|
|
496
501
|
"/pair/start": true,
|
|
@@ -524,11 +529,13 @@ var prePairGetPaths = map[string]bool{
|
|
|
524
529
|
"/health": true,
|
|
525
530
|
"/healthz": true,
|
|
526
531
|
"/readyz": true,
|
|
532
|
+
"/routez": true,
|
|
527
533
|
"/manifest": true,
|
|
528
534
|
}
|
|
529
535
|
|
|
530
536
|
var prePairPostPaths = map[string]bool{
|
|
531
|
-
"/pair/claim":
|
|
537
|
+
"/pair/claim": true,
|
|
538
|
+
"/pair/psk-claim": true,
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
type MemoryRateLimiter struct {
|
|
@@ -308,7 +308,7 @@ func TestNewHandlerAcceptsLoopbackUpstream(t *testing.T) {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
func
|
|
311
|
+
func TestPrePairModeOnlyAllowsHealthManifestRoutezAndClaims(t *testing.T) {
|
|
312
312
|
var forwarded []string
|
|
313
313
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
314
314
|
forwarded = append(forwarded, r.Method+" "+r.URL.Path)
|
|
@@ -324,8 +324,10 @@ func TestPrePairModeOnlyAllowsHealthManifestAndClaim(t *testing.T) {
|
|
|
324
324
|
}{
|
|
325
325
|
{http.MethodGet, "/health"},
|
|
326
326
|
{http.MethodGet, "/healthz"},
|
|
327
|
+
{http.MethodGet, "/routez"},
|
|
327
328
|
{http.MethodGet, "/manifest"},
|
|
328
329
|
{http.MethodPost, "/pair/claim"},
|
|
330
|
+
{http.MethodPost, "/pair/psk-claim"},
|
|
329
331
|
}
|
|
330
332
|
for _, tc := range allowed {
|
|
331
333
|
t.Run("allows "+tc.method+" "+tc.path, func(t *testing.T) {
|
|
@@ -407,7 +409,13 @@ func TestPairlingConnectModeRequiresBearerForPostPairEndpointsAndRejectsRemotePa
|
|
|
407
409
|
t.Fatalf("pre-pair claim status = %d body = %s", rec.Code, rec.Body.String())
|
|
408
410
|
}
|
|
409
411
|
|
|
410
|
-
|
|
412
|
+
rec = httptest.NewRecorder()
|
|
413
|
+
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/pair/psk-claim", strings.NewReader(`{}`)))
|
|
414
|
+
if rec.Code != http.StatusOK {
|
|
415
|
+
t.Fatalf("pre-pair PSK claim status = %d body = %s", rec.Code, rec.Body.String())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if fmt.Sprintf("%+v", forwarded) != "[POST /send-text POST /pair/claim POST /pair/psk-claim]" {
|
|
411
419
|
t.Fatalf("forwarded = %+v", forwarded)
|
|
412
420
|
}
|
|
413
421
|
}
|
|
@@ -442,44 +450,48 @@ func TestPairlingConnectModeMatchesEndpointContract(t *testing.T) {
|
|
|
442
450
|
}
|
|
443
451
|
}
|
|
444
452
|
|
|
445
|
-
func
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
453
|
+
func TestPrePairClaimsAreRateLimitedAndBodyLimited(t *testing.T) {
|
|
454
|
+
for _, path := range []string{"/pair/claim", "/pair/psk-claim"} {
|
|
455
|
+
t.Run(path, func(t *testing.T) {
|
|
456
|
+
var upstreamCalls int
|
|
457
|
+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
458
|
+
upstreamCalls++
|
|
459
|
+
w.WriteHeader(http.StatusOK)
|
|
460
|
+
}))
|
|
461
|
+
defer upstream.Close()
|
|
462
|
+
limiter := NewMemoryRateLimiter(1, time.Minute)
|
|
463
|
+
handler := newTestHandlerWithMode(t, upstream.URL, defaultMaxBodyBytes, nil, ExposureModePrePair, limiter)
|
|
464
|
+
|
|
465
|
+
req := httptest.NewRequest(http.MethodPost, "http://pairling-connect.local"+path, strings.NewReader(`{}`))
|
|
466
|
+
req.RemoteAddr = "100.64.0.8:12345"
|
|
467
|
+
rec := httptest.NewRecorder()
|
|
468
|
+
handler.ServeHTTP(rec, req)
|
|
469
|
+
if rec.Code != http.StatusOK {
|
|
470
|
+
t.Fatalf("first claim status = %d body = %s", rec.Code, rec.Body.String())
|
|
471
|
+
}
|
|
462
472
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
473
|
+
req = httptest.NewRequest(http.MethodPost, "http://pairling-connect.local"+path, strings.NewReader(`{}`))
|
|
474
|
+
req.RemoteAddr = "100.64.0.8:12345"
|
|
475
|
+
rec = httptest.NewRecorder()
|
|
476
|
+
handler.ServeHTTP(rec, req)
|
|
477
|
+
if rec.Code != http.StatusTooManyRequests {
|
|
478
|
+
t.Fatalf("second claim status = %d body = %s", rec.Code, rec.Body.String())
|
|
479
|
+
}
|
|
470
480
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
481
|
+
largeBody := strings.NewReader(strings.Repeat("x", int(prePairMaxBodyBytes)+1))
|
|
482
|
+
req = httptest.NewRequest(http.MethodPost, "http://pairling-connect.local"+path, largeBody)
|
|
483
|
+
req.RemoteAddr = "100.64.0.9:12345"
|
|
484
|
+
req.ContentLength = prePairMaxBodyBytes + 1
|
|
485
|
+
rec = httptest.NewRecorder()
|
|
486
|
+
handler.ServeHTTP(rec, req)
|
|
487
|
+
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
488
|
+
t.Fatalf("large claim status = %d body = %s", rec.Code, rec.Body.String())
|
|
489
|
+
}
|
|
480
490
|
|
|
481
|
-
|
|
482
|
-
|
|
491
|
+
if upstreamCalls != 1 {
|
|
492
|
+
t.Fatalf("upstream calls = %d, want 1", upstreamCalls)
|
|
493
|
+
}
|
|
494
|
+
})
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
497
|
|
|
@@ -747,7 +747,20 @@ ptybroker_live_session_count() {
|
|
|
747
747
|
import json
|
|
748
748
|
import sys
|
|
749
749
|
|
|
750
|
-
|
|
750
|
+
def load_json_arg(raw):
|
|
751
|
+
text = str(raw or "").strip()
|
|
752
|
+
decoder = json.JSONDecoder()
|
|
753
|
+
for index, char in enumerate(text):
|
|
754
|
+
if char not in "{[":
|
|
755
|
+
continue
|
|
756
|
+
try:
|
|
757
|
+
value, _ = decoder.raw_decode(text[index:])
|
|
758
|
+
return value
|
|
759
|
+
except json.JSONDecodeError:
|
|
760
|
+
continue
|
|
761
|
+
return {}
|
|
762
|
+
|
|
763
|
+
payload = load_json_arg(sys.argv[1])
|
|
751
764
|
status = payload.get("status") if isinstance(payload.get("status"), dict) else {}
|
|
752
765
|
print(status.get("live_session_count", "unknown"))
|
|
753
766
|
PY
|
|
@@ -799,7 +812,20 @@ ptybroker_live_revision() {
|
|
|
799
812
|
import json
|
|
800
813
|
import sys
|
|
801
814
|
|
|
802
|
-
|
|
815
|
+
def load_json_arg(raw):
|
|
816
|
+
text = str(raw or "").strip()
|
|
817
|
+
decoder = json.JSONDecoder()
|
|
818
|
+
for index, char in enumerate(text):
|
|
819
|
+
if char not in "{[":
|
|
820
|
+
continue
|
|
821
|
+
try:
|
|
822
|
+
value, _ = decoder.raw_decode(text[index:])
|
|
823
|
+
return value
|
|
824
|
+
except json.JSONDecodeError:
|
|
825
|
+
continue
|
|
826
|
+
return {}
|
|
827
|
+
|
|
828
|
+
payload = load_json_arg(sys.argv[1])
|
|
803
829
|
status = payload.get("status") if isinstance(payload.get("status"), dict) else payload
|
|
804
830
|
print(status.get("source_revision") or "")
|
|
805
831
|
PY
|
|
@@ -813,7 +839,20 @@ import sys
|
|
|
813
839
|
from pathlib import Path
|
|
814
840
|
|
|
815
841
|
current = Path(sys.argv[1])
|
|
816
|
-
|
|
842
|
+
def load_json_arg(raw):
|
|
843
|
+
text = str(raw or "").strip()
|
|
844
|
+
decoder = json.JSONDecoder()
|
|
845
|
+
for index, char in enumerate(text):
|
|
846
|
+
if char not in "{[":
|
|
847
|
+
continue
|
|
848
|
+
try:
|
|
849
|
+
value, _ = decoder.raw_decode(text[index:])
|
|
850
|
+
return value
|
|
851
|
+
except json.JSONDecodeError:
|
|
852
|
+
continue
|
|
853
|
+
return {}
|
|
854
|
+
|
|
855
|
+
payload = load_json_arg(sys.argv[2])
|
|
817
856
|
live = payload.get("status") if isinstance(payload.get("status"), dict) else payload
|
|
818
857
|
|
|
819
858
|
def read_revision(root: Path):
|
|
@@ -881,7 +920,20 @@ ptybroker_report_deferred_restart() {
|
|
|
881
920
|
import json
|
|
882
921
|
import sys
|
|
883
922
|
|
|
884
|
-
|
|
923
|
+
def load_json_arg(raw):
|
|
924
|
+
text = str(raw or "").strip()
|
|
925
|
+
decoder = json.JSONDecoder()
|
|
926
|
+
for index, char in enumerate(text):
|
|
927
|
+
if char not in "{[":
|
|
928
|
+
continue
|
|
929
|
+
try:
|
|
930
|
+
value, _ = decoder.raw_decode(text[index:])
|
|
931
|
+
return value
|
|
932
|
+
except json.JSONDecodeError:
|
|
933
|
+
continue
|
|
934
|
+
return {}
|
|
935
|
+
|
|
936
|
+
state = load_json_arg(sys.argv[1])
|
|
885
937
|
if state.get("state") != "stale_deferred":
|
|
886
938
|
raise SystemExit(0)
|
|
887
939
|
live = state.get("live") if isinstance(state.get("live"), dict) else {}
|
|
@@ -902,7 +954,20 @@ ptybroker_state_field() {
|
|
|
902
954
|
import json
|
|
903
955
|
import sys
|
|
904
956
|
|
|
905
|
-
|
|
957
|
+
def load_json_arg(raw):
|
|
958
|
+
text = str(raw or "").strip()
|
|
959
|
+
decoder = json.JSONDecoder()
|
|
960
|
+
for index, char in enumerate(text):
|
|
961
|
+
if char not in "{[":
|
|
962
|
+
continue
|
|
963
|
+
try:
|
|
964
|
+
value, _ = decoder.raw_decode(text[index:])
|
|
965
|
+
return value
|
|
966
|
+
except json.JSONDecodeError:
|
|
967
|
+
continue
|
|
968
|
+
return {}
|
|
969
|
+
|
|
970
|
+
payload = load_json_arg(sys.argv[1])
|
|
906
971
|
value = payload
|
|
907
972
|
for part in sys.argv[2].split("."):
|
|
908
973
|
if isinstance(value, dict):
|
|
@@ -1089,7 +1154,7 @@ install_runtime() {
|
|
|
1089
1154
|
log "Installed Pairling runtime $RELEASE_NAME"
|
|
1090
1155
|
if ! is_dry_run; then
|
|
1091
1156
|
log ""
|
|
1092
|
-
if ! pair_runtime --qr; then
|
|
1157
|
+
if ! PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS="${PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS:-35}" pair_runtime --qr; then
|
|
1093
1158
|
log "Pairling installed, but setup could not generate a pairing invitation. Run: pairling doctor --json; pairling pair --qr" >&2
|
|
1094
1159
|
exit 1
|
|
1095
1160
|
fi
|
|
@@ -1151,10 +1216,12 @@ pair_runtime() {
|
|
|
1151
1216
|
payload_file="$(mktemp)"
|
|
1152
1217
|
if python3 - "$PAIRLING_RUNTIME_PORT" "$ttl" "$REPO_ROOT" >"$payload_file" <<'PY'
|
|
1153
1218
|
import json
|
|
1219
|
+
import ipaddress
|
|
1154
1220
|
import os
|
|
1155
1221
|
import socket
|
|
1156
1222
|
import subprocess
|
|
1157
1223
|
import sys
|
|
1224
|
+
import time
|
|
1158
1225
|
import urllib.parse
|
|
1159
1226
|
import urllib.error
|
|
1160
1227
|
import urllib.request
|
|
@@ -1219,6 +1286,37 @@ mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_n
|
|
|
1219
1286
|
# plaintext claim, so this field is the bridge that actually makes WS3 engage.
|
|
1220
1287
|
mac_ake_pub = str(payload.get("mac_ake_pub") or (payload.get("claim") or {}).get("mac_ake_pub") or "")
|
|
1221
1288
|
|
|
1289
|
+
def is_ats_local_ipv4(value: str) -> bool:
|
|
1290
|
+
try:
|
|
1291
|
+
addr = ipaddress.ip_address(value)
|
|
1292
|
+
except ValueError:
|
|
1293
|
+
return False
|
|
1294
|
+
if addr.version != 4 or addr.is_loopback or addr.is_link_local:
|
|
1295
|
+
return False
|
|
1296
|
+
return (
|
|
1297
|
+
value.startswith("10.")
|
|
1298
|
+
or value.startswith("192.168.")
|
|
1299
|
+
or any(value.startswith(f"172.{i}.") for i in range(16, 32))
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
def detected_lan_ip() -> str:
|
|
1303
|
+
override = os.environ.get("PAIRLING_TEST_LAN_IP")
|
|
1304
|
+
if override is not None:
|
|
1305
|
+
value = override.strip()
|
|
1306
|
+
return value if is_ats_local_ipv4(value) else ""
|
|
1307
|
+
if os.environ.get("PAIRLING_DISABLE_LAN") == "1" or os.environ.get("PAIRLING_TEST_DISABLE_LAN") == "1":
|
|
1308
|
+
return ""
|
|
1309
|
+
try:
|
|
1310
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
1311
|
+
try:
|
|
1312
|
+
sock.connect(("8.8.8.8", 80))
|
|
1313
|
+
ip = sock.getsockname()[0]
|
|
1314
|
+
finally:
|
|
1315
|
+
sock.close()
|
|
1316
|
+
return ip if is_ats_local_ipv4(ip) else ""
|
|
1317
|
+
except Exception:
|
|
1318
|
+
return ""
|
|
1319
|
+
|
|
1222
1320
|
def detected_tailnet_ip() -> str:
|
|
1223
1321
|
override = os.environ.get("PAIRLING_TEST_TAILSCALE_IP")
|
|
1224
1322
|
if override is not None:
|
|
@@ -1236,35 +1334,64 @@ def detected_tailnet_ip() -> str:
|
|
|
1236
1334
|
return ip
|
|
1237
1335
|
return ""
|
|
1238
1336
|
|
|
1337
|
+
def connectd_route_wait_seconds() -> float:
|
|
1338
|
+
try:
|
|
1339
|
+
return min(max(float(os.environ.get("PAIRLING_CONNECTD_ROUTE_WAIT_SECONDS") or "0"), 0.0), 60.0)
|
|
1340
|
+
except ValueError:
|
|
1341
|
+
return 0.0
|
|
1342
|
+
|
|
1343
|
+
def connectd_route_poll_seconds() -> float:
|
|
1344
|
+
try:
|
|
1345
|
+
return min(max(float(os.environ.get("PAIRLING_CONNECTD_ROUTE_POLL_SECONDS") or "0.5"), 0.1), 2.0)
|
|
1346
|
+
except ValueError:
|
|
1347
|
+
return 0.5
|
|
1348
|
+
|
|
1349
|
+
def status_could_be_ready_soon(status: dict) -> bool:
|
|
1350
|
+
if not status:
|
|
1351
|
+
return True
|
|
1352
|
+
if status.get("auth_url_present"):
|
|
1353
|
+
return False
|
|
1354
|
+
return True
|
|
1355
|
+
|
|
1356
|
+
def ready_connectd_route():
|
|
1357
|
+
wait_seconds = connectd_route_wait_seconds()
|
|
1358
|
+
poll_seconds = connectd_route_poll_seconds()
|
|
1359
|
+
deadline = time.monotonic() + wait_seconds
|
|
1360
|
+
while True:
|
|
1361
|
+
status = fetch_connectd_status(timeout_seconds=0.7)
|
|
1362
|
+
connect_routes = advertised_pairling_connect_routes(status)
|
|
1363
|
+
if connect_routes:
|
|
1364
|
+
return connect_routes[0]
|
|
1365
|
+
if wait_seconds <= 0 or time.monotonic() >= deadline or not status_could_be_ready_soon(status):
|
|
1366
|
+
return None
|
|
1367
|
+
time.sleep(min(poll_seconds, max(0.0, deadline - time.monotonic())))
|
|
1368
|
+
|
|
1239
1369
|
def default_pair_route(port_number: int) -> dict:
|
|
1240
1370
|
for key in ("PAIRLING_PAIR_BASE_URL", "PAIRLING_PUBLIC_BASE_URL"):
|
|
1241
1371
|
value = os.environ.get(key)
|
|
1242
1372
|
if value:
|
|
1243
1373
|
return {"base_url": value, "source": "explicit_override", "status": "override"}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1374
|
+
# Remote-first pairing: if connectd reports a ready Pairling Connect route,
|
|
1375
|
+
# the QR advertises that route and the iOS app claims it through the
|
|
1376
|
+
# embedded pre-pair transport. LAN/Bonjour are explicit degraded fallbacks
|
|
1377
|
+
# when Pairling Connect is not ready.
|
|
1378
|
+
route = ready_connectd_route()
|
|
1379
|
+
if route:
|
|
1247
1380
|
return {
|
|
1248
1381
|
"base_url": route["base_url"],
|
|
1249
1382
|
"source": route["source"],
|
|
1250
1383
|
"status": route["status"],
|
|
1251
1384
|
"kind": route["kind"],
|
|
1252
1385
|
}
|
|
1386
|
+
lan_ip = detected_lan_ip()
|
|
1387
|
+
if lan_ip:
|
|
1388
|
+
return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
|
|
1389
|
+
if os.environ.get("PAIRLING_DISABLE_BONJOUR") != "1" and os.environ.get("PAIRLING_TEST_DISABLE_BONJOUR") != "1":
|
|
1390
|
+
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
|
|
1253
1391
|
tailnet_ip = detected_tailnet_ip()
|
|
1254
1392
|
if tailnet_ip:
|
|
1255
|
-
return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback"}
|
|
1256
|
-
|
|
1257
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
1258
|
-
try:
|
|
1259
|
-
sock.connect(("8.8.8.8", 80))
|
|
1260
|
-
ip = sock.getsockname()[0]
|
|
1261
|
-
finally:
|
|
1262
|
-
sock.close()
|
|
1263
|
-
if ip and not ip.startswith(("127.", "169.254.")):
|
|
1264
|
-
return {"base_url": f"http://{ip}:{port_number}", "source": "lan", "status": "fallback"}
|
|
1265
|
-
except Exception:
|
|
1266
|
-
pass
|
|
1267
|
-
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback"}
|
|
1393
|
+
return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback", "kind": "standalone_tailnet"}
|
|
1394
|
+
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
|
|
1268
1395
|
|
|
1269
1396
|
pair_route = default_pair_route(int(port))
|
|
1270
1397
|
base_url = str(pair_route.get("base_url") or "")
|
|
@@ -1285,6 +1412,11 @@ if pair_id and secret:
|
|
|
1285
1412
|
pair_params["route_status"] = "ready"
|
|
1286
1413
|
pair_params["route_kind"] = str(pair_route.get("kind") or "tailnet")
|
|
1287
1414
|
pair_params["route_contract"] = "pairling-runtime-v1"
|
|
1415
|
+
elif pair_route.get("status") == "fallback":
|
|
1416
|
+
pair_params["route_source"] = "local_fallback"
|
|
1417
|
+
pair_params["route_status"] = "degraded"
|
|
1418
|
+
pair_params["route_kind"] = str(pair_route.get("kind") or pair_route.get("source") or "local")
|
|
1419
|
+
pair_params["route_contract"] = "pairling-runtime-v1"
|
|
1288
1420
|
manual = {
|
|
1289
1421
|
"base_url": base_url,
|
|
1290
1422
|
"pair_id": pair_id,
|
package/payload-manifest.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"connectd": {
|
|
3
3
|
"darwin-arm64": {
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "96400014bc7d32bd1b976216eb6dc76a6621d9da34a2ef02da87c72957dea170",
|
|
5
5
|
"team_id": "965AVD34A3"
|
|
6
6
|
},
|
|
7
7
|
"darwin-x64": {
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "bb4df01a72f6c2032c8d7341f5d243fa3e6c99295c033dbf3f99a7d7eed650b7",
|
|
9
9
|
"team_id": "965AVD34A3"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
{
|
|
14
14
|
"path": "payload/mac/SOURCE_BRANCH",
|
|
15
|
-
"sha256": "
|
|
15
|
+
"sha256": "34d6a94dacb895403529caac12a19aed745c6caca7a8d0f4ed631999044f76e8"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "payload/mac/SOURCE_DIRTY",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "payload/mac/SOURCE_REVISION",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "6fba526ba8b27c395d71b9d6067bb09fe9458c5d650a92af80541ad7971a3ba3"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "payload/mac/VERSION",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "e959f750e92dbb7614ae28ac96f0a37272caed81d8bbb758791af7bfbb6106a0"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "payload/mac/companiond/app_attest_lan.py",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "payload/mac/companiond/runtime_manifest.py",
|
|
155
|
-
"sha256": "
|
|
155
|
+
"sha256": "72d61833a74e8171157784bee252f3e0f0bb15d77b740c2ffb2d82214d3b4933"
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"path": "payload/mac/companiond/runtime_paths.py",
|
|
@@ -212,11 +212,11 @@
|
|
|
212
212
|
},
|
|
213
213
|
{
|
|
214
214
|
"path": "payload/mac/connectd/internal/gateway/proxy.go",
|
|
215
|
-
"sha256": "
|
|
215
|
+
"sha256": "5da0f96ec8c364d90a7c07e4e968df6e9bd7bfeadf6cbe25d0bb743cdba3a91a"
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"path": "payload/mac/connectd/internal/gateway/proxy_test.go",
|
|
219
|
-
"sha256": "
|
|
219
|
+
"sha256": "1eefdfc331e97033d124cd627dd6e456f6de71e3a28b8d05aa6ede9faa3ff663"
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
"path": "payload/mac/connectd/internal/runtime/config.go",
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
"path": "payload/mac/install/install-runtime.sh",
|
|
255
|
-
"sha256": "
|
|
255
|
+
"sha256": "87c1db89b281b78d0809b4360c54c5c57b8e4f25a473496373315236ffeb6117"
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "payload/mac/install/psk_dependency_check.py",
|
|
@@ -276,8 +276,8 @@
|
|
|
276
276
|
}
|
|
277
277
|
],
|
|
278
278
|
"package": "pairling",
|
|
279
|
-
"package_version": "0.2.
|
|
279
|
+
"package_version": "0.2.5",
|
|
280
280
|
"schema_version": 1,
|
|
281
281
|
"source_dirty": false,
|
|
282
|
-
"source_revision": "
|
|
282
|
+
"source_revision": "2f094bd"
|
|
283
283
|
}
|