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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairling",
3
- "version": "0.2.3",
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.dev"
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.3",
44
- "@pairling/runtime-darwin-x64": "0.2.3"
43
+ "@pairling/runtime-darwin-arm64": "0.2.4",
44
+ "@pairling/runtime-darwin-x64": "0.2.4"
45
45
  }
46
46
  }
@@ -1 +1 @@
1
- feat/onestream-pairling-integration
1
+ HEAD
@@ -1 +1 @@
1
- 6d905a33
1
+ 7f88c1b
@@ -1 +1 @@
1
- 0.2.3
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 == "/pair/claim" && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect) {
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 == "/pair/claim" && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect)
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": true,
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 TestPrePairModeOnlyAllowsHealthManifestAndClaim(t *testing.T) {
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
- if fmt.Sprintf("%+v", forwarded) != "[POST /send-text POST /pair/claim]" {
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 TestPrePairClaimIsRateLimitedAndBodyLimited(t *testing.T) {
446
- var upstreamCalls int
447
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
448
- upstreamCalls++
449
- w.WriteHeader(http.StatusOK)
450
- }))
451
- defer upstream.Close()
452
- limiter := NewMemoryRateLimiter(1, time.Minute)
453
- handler := newTestHandlerWithMode(t, upstream.URL, defaultMaxBodyBytes, nil, ExposureModePrePair, limiter)
454
-
455
- req := httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/pair/claim", strings.NewReader(`{}`))
456
- req.RemoteAddr = "100.64.0.8:12345"
457
- rec := httptest.NewRecorder()
458
- handler.ServeHTTP(rec, req)
459
- if rec.Code != http.StatusOK {
460
- t.Fatalf("first claim status = %d body = %s", rec.Code, rec.Body.String())
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
- req = httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/pair/claim", strings.NewReader(`{}`))
464
- req.RemoteAddr = "100.64.0.8:12345"
465
- rec = httptest.NewRecorder()
466
- handler.ServeHTTP(rec, req)
467
- if rec.Code != http.StatusTooManyRequests {
468
- t.Fatalf("second claim status = %d body = %s", rec.Code, rec.Body.String())
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
- largeBody := strings.NewReader(strings.Repeat("x", int(prePairMaxBodyBytes)+1))
472
- req = httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/pair/claim", largeBody)
473
- req.RemoteAddr = "100.64.0.9:12345"
474
- req.ContentLength = prePairMaxBodyBytes + 1
475
- rec = httptest.NewRecorder()
476
- handler.ServeHTTP(rec, req)
477
- if rec.Code != http.StatusRequestEntityTooLarge {
478
- t.Fatalf("large claim status = %d body = %s", rec.Code, rec.Body.String())
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
- if upstreamCalls != 1 {
482
- t.Fatalf("upstream calls = %d, want 1", upstreamCalls)
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))
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "connectd": {
3
3
  "darwin-arm64": {
4
- "sha256": "4a1ec43865486a1958c11a253f9955542354da58ab81dedfc1828727ff691050",
4
+ "sha256": "d77d7299f92da3a72d38981dd8f31f7b5935cd297a29b0231a2b79b8b37ef7f4",
5
5
  "team_id": "965AVD34A3"
6
6
  },
7
7
  "darwin-x64": {
8
- "sha256": "05da872d33362df876e3fbca2d527f8acd33225917a774068bd3894fe2cbec84",
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": "b7cda4715ebe6bb92ec69b85df0e99668564bde99f03d6e04109b399538000b2"
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": "fbe59df8b5637ce46090bf962b4c7be19a18596f93dc3963f228836be332e593"
23
+ "sha256": "e88a4e5e02bc17d75bd064d71908ab84750f877613ce56e1efb4be3d453a5a81"
24
24
  },
25
25
  {
26
26
  "path": "payload/mac/VERSION",
27
- "sha256": "3ab94c04d24986f3af288ba1cda2c0bbddbc5a89dff097182805f54578e1ea75"
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": "92c8c04def5e166a41926cf97de5dbb285a586f67e0f15eeef873c9f0337cb46"
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": "ce827d08826492193b025738322ae72e3d806604968f4e7d19e3d37c78dce313"
215
+ "sha256": "5da0f96ec8c364d90a7c07e4e968df6e9bd7bfeadf6cbe25d0bb743cdba3a91a"
216
216
  },
217
217
  {
218
218
  "path": "payload/mac/connectd/internal/gateway/proxy_test.go",
219
- "sha256": "f45c74df51939f688bc3b3bb326d9fa103c2b39f81eb589b1e6be06731de9f3a"
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": "45c0295ad0f6e610c0cbf10057fd341754967ffaa5eee403ad1aaa1ea1954a5c"
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.3",
279
+ "package_version": "0.2.4",
280
280
  "schema_version": 1,
281
281
  "source_dirty": false,
282
- "source_revision": "6d905a33"
282
+ "source_revision": "7f88c1b"
283
283
  }