pairling 0.2.3 → 0.2.4
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 +4 -4
- 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 +42 -11
- 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.4",
|
|
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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://pairling
|
|
20
|
+
"url": "https://github.com/mergimg0/pairling-helper"
|
|
21
21
|
},
|
|
22
22
|
"license": "UNLICENSED",
|
|
23
23
|
"author": "Pairling (https://pairling.dev)",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
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.4",
|
|
44
|
+
"@pairling/runtime-darwin-x64": "0.2.4"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
HEAD
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7f88c1b
|
package/payload/mac/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.4
|
|
@@ -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
|
|
|
@@ -1151,6 +1151,7 @@ pair_runtime() {
|
|
|
1151
1151
|
payload_file="$(mktemp)"
|
|
1152
1152
|
if python3 - "$PAIRLING_RUNTIME_PORT" "$ttl" "$REPO_ROOT" >"$payload_file" <<'PY'
|
|
1153
1153
|
import json
|
|
1154
|
+
import ipaddress
|
|
1154
1155
|
import os
|
|
1155
1156
|
import socket
|
|
1156
1157
|
import subprocess
|
|
@@ -1219,6 +1220,37 @@ mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_n
|
|
|
1219
1220
|
# plaintext claim, so this field is the bridge that actually makes WS3 engage.
|
|
1220
1221
|
mac_ake_pub = str(payload.get("mac_ake_pub") or (payload.get("claim") or {}).get("mac_ake_pub") or "")
|
|
1221
1222
|
|
|
1223
|
+
def is_ats_local_ipv4(value: str) -> bool:
|
|
1224
|
+
try:
|
|
1225
|
+
addr = ipaddress.ip_address(value)
|
|
1226
|
+
except ValueError:
|
|
1227
|
+
return False
|
|
1228
|
+
if addr.version != 4 or addr.is_loopback or addr.is_link_local:
|
|
1229
|
+
return False
|
|
1230
|
+
return (
|
|
1231
|
+
value.startswith("10.")
|
|
1232
|
+
or value.startswith("192.168.")
|
|
1233
|
+
or any(value.startswith(f"172.{i}.") for i in range(16, 32))
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
def detected_lan_ip() -> str:
|
|
1237
|
+
override = os.environ.get("PAIRLING_TEST_LAN_IP")
|
|
1238
|
+
if override is not None:
|
|
1239
|
+
value = override.strip()
|
|
1240
|
+
return value if is_ats_local_ipv4(value) else ""
|
|
1241
|
+
if os.environ.get("PAIRLING_DISABLE_LAN") == "1" or os.environ.get("PAIRLING_TEST_DISABLE_LAN") == "1":
|
|
1242
|
+
return ""
|
|
1243
|
+
try:
|
|
1244
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
1245
|
+
try:
|
|
1246
|
+
sock.connect(("8.8.8.8", 80))
|
|
1247
|
+
ip = sock.getsockname()[0]
|
|
1248
|
+
finally:
|
|
1249
|
+
sock.close()
|
|
1250
|
+
return ip if is_ats_local_ipv4(ip) else ""
|
|
1251
|
+
except Exception:
|
|
1252
|
+
return ""
|
|
1253
|
+
|
|
1222
1254
|
def detected_tailnet_ip() -> str:
|
|
1223
1255
|
override = os.environ.get("PAIRLING_TEST_TAILSCALE_IP")
|
|
1224
1256
|
if override is not None:
|
|
@@ -1241,6 +1273,16 @@ def default_pair_route(port_number: int) -> dict:
|
|
|
1241
1273
|
value = os.environ.get(key)
|
|
1242
1274
|
if value:
|
|
1243
1275
|
return {"base_url": value, "source": "explicit_override", "status": "override"}
|
|
1276
|
+
# First-pair QR claims go through iOS URLSession before any persisted
|
|
1277
|
+
# Pairling Connect route exists. TestFlight builds allow local HTTP via
|
|
1278
|
+
# NSAllowsLocalNetworking, but ATS blocks plain HTTP to 100.64/10 tailnet
|
|
1279
|
+
# addresses. Prefer ATS-local LAN/Bonjour bases for bootstrap; remote
|
|
1280
|
+
# tailnet routes are validated and promoted after the claim.
|
|
1281
|
+
lan_ip = detected_lan_ip()
|
|
1282
|
+
if lan_ip:
|
|
1283
|
+
return {"base_url": f"http://{lan_ip}:{port_number}", "source": "lan", "status": "fallback", "kind": "lan"}
|
|
1284
|
+
if os.environ.get("PAIRLING_DISABLE_BONJOUR") != "1" and os.environ.get("PAIRLING_TEST_DISABLE_BONJOUR") != "1":
|
|
1285
|
+
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback", "kind": "bonjour"}
|
|
1244
1286
|
connect_routes = advertised_pairling_connect_routes(fetch_connectd_status(timeout_seconds=0.7))
|
|
1245
1287
|
if connect_routes:
|
|
1246
1288
|
route = connect_routes[0]
|
|
@@ -1253,17 +1295,6 @@ def default_pair_route(port_number: int) -> dict:
|
|
|
1253
1295
|
tailnet_ip = detected_tailnet_ip()
|
|
1254
1296
|
if tailnet_ip:
|
|
1255
1297
|
return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback"}
|
|
1256
|
-
try:
|
|
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
1298
|
return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback"}
|
|
1268
1299
|
|
|
1269
1300
|
pair_route = default_pair_route(int(port))
|
package/payload-manifest.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"connectd": {
|
|
3
3
|
"darwin-arm64": {
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "d77d7299f92da3a72d38981dd8f31f7b5935cd297a29b0231a2b79b8b37ef7f4",
|
|
5
5
|
"team_id": "965AVD34A3"
|
|
6
6
|
},
|
|
7
7
|
"darwin-x64": {
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "393c95dcd6128699614423d6414a21962d796a2baf8f249789d98d247668248e",
|
|
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": "e88a4e5e02bc17d75bd064d71908ab84750f877613ce56e1efb4be3d453a5a81"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "payload/mac/VERSION",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "1725bfa6524c5265e7c171cf06568417d39b947fff49c242f03859479c82334b"
|
|
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": "29e3e3ffd11d1dc5de96a29d5724752c4651168c5cf15b6ab8e5797e85e6a1ee"
|
|
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.4",
|
|
280
280
|
"schema_version": 1,
|
|
281
281
|
"source_dirty": false,
|
|
282
|
-
"source_revision": "
|
|
282
|
+
"source_revision": "7f88c1b"
|
|
283
283
|
}
|