pairling 0.2.2 → 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.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.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.2",
44
- "@pairling/runtime-darwin-x64": "0.2.2"
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
- a1eaa1ac
1
+ 7f88c1b
@@ -1 +1 @@
1
- 0.2.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 == "/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
 
@@ -485,9 +485,10 @@ try:
485
485
  add(
486
486
  "shell_pairling_wrapper",
487
487
  "runtime/current/bin/pairling" in pairling_text
488
+ and "--shim-print-env" in pairling_text
488
489
  and re.search(r"/Users/[^\\s'\"]+/projects/Pairling", pairling_text) is None,
489
490
  "error",
490
- "User pairling command resolves through runtime/current unless PAIRLING_REPO_ROOT is explicitly set.",
491
+ "User pairling command resolves through Pairling without trapping npm setup on stale runtime/current.",
491
492
  str(USER_PAIRLING),
492
493
  )
493
494
  except Exception as exc:
@@ -651,13 +651,40 @@ if [[ -n "${PAIRLING_REPO_ROOT:-}" ]]; then
651
651
  exec "$PAIRLING_REPO_ROOT/mac/packaging/bin/pairling" "$@"
652
652
  fi
653
653
 
654
+ find_npm_pairling_shim() {
655
+ local wrapper_path="$1"
656
+ local old_ifs="$IFS"
657
+ local dir candidate
658
+ IFS=:
659
+ for dir in $PATH; do
660
+ [[ -n "$dir" ]] || dir="."
661
+ candidate="$dir/pairling"
662
+ if [[ -x "$candidate" && "$candidate" != "$wrapper_path" ]] && "$candidate" --shim-print-env >/dev/null 2>&1; then
663
+ IFS="$old_ifs"
664
+ printf '%s\n' "$candidate"
665
+ return 0
666
+ fi
667
+ done
668
+ IFS="$old_ifs"
669
+ return 1
670
+ }
671
+
672
+ case "${1:-}" in
673
+ setup|install|update|upgrade)
674
+ WRAPPER_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
675
+ if NPM_PAIRLING="$(find_npm_pairling_shim "$WRAPPER_PATH")"; then
676
+ exec "$NPM_PAIRLING" "$@"
677
+ fi
678
+ ;;
679
+ esac
680
+
654
681
  APP_SUPPORT="${PAIRLING_APP_SUPPORT_ROOT:-${COMPANION_APP_SUPPORT_ROOT:-$HOME/Library/Application Support/Pairling}}"
655
682
  RUNTIME_PAIRLING="$APP_SUPPORT/runtime/current/bin/pairling"
656
683
  if [[ -x "$RUNTIME_PAIRLING" ]]; then
657
684
  exec "$RUNTIME_PAIRLING" "$@"
658
685
  fi
659
686
 
660
- printf 'Pairling runtime command is not installed. Run: npm install -g pairling && pairling setup (or use a repo-local mac/packaging/bin/pairling).\n' >&2
687
+ printf 'Pairling runtime command is not installed. Run:\n npm install -g pairling\n pairling setup\nor use a repo-local mac/packaging/bin/pairling.\n' >&2
661
688
  exit 127
662
689
  SH
663
690
  chmod 755 "$tmp"
@@ -1060,6 +1087,13 @@ install_runtime() {
1060
1087
  run_doctor || true
1061
1088
  fi
1062
1089
  log "Installed Pairling runtime $RELEASE_NAME"
1090
+ if ! is_dry_run; then
1091
+ log ""
1092
+ if ! pair_runtime --qr; then
1093
+ log "Pairling installed, but setup could not generate a pairing invitation. Run: pairling doctor --json; pairling pair --qr" >&2
1094
+ exit 1
1095
+ fi
1096
+ fi
1063
1097
  }
1064
1098
 
1065
1099
  status_runtime() {
@@ -1117,6 +1151,7 @@ pair_runtime() {
1117
1151
  payload_file="$(mktemp)"
1118
1152
  if python3 - "$PAIRLING_RUNTIME_PORT" "$ttl" "$REPO_ROOT" >"$payload_file" <<'PY'
1119
1153
  import json
1154
+ import ipaddress
1120
1155
  import os
1121
1156
  import socket
1122
1157
  import subprocess
@@ -1185,6 +1220,37 @@ mac_name = str(((payload.get("pair_service") or {}).get("txt") or {}).get("mac_n
1185
1220
  # plaintext claim, so this field is the bridge that actually makes WS3 engage.
1186
1221
  mac_ake_pub = str(payload.get("mac_ake_pub") or (payload.get("claim") or {}).get("mac_ake_pub") or "")
1187
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
+
1188
1254
  def detected_tailnet_ip() -> str:
1189
1255
  override = os.environ.get("PAIRLING_TEST_TAILSCALE_IP")
1190
1256
  if override is not None:
@@ -1207,6 +1273,16 @@ def default_pair_route(port_number: int) -> dict:
1207
1273
  value = os.environ.get(key)
1208
1274
  if value:
1209
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"}
1210
1286
  connect_routes = advertised_pairling_connect_routes(fetch_connectd_status(timeout_seconds=0.7))
1211
1287
  if connect_routes:
1212
1288
  route = connect_routes[0]
@@ -1219,17 +1295,6 @@ def default_pair_route(port_number: int) -> dict:
1219
1295
  tailnet_ip = detected_tailnet_ip()
1220
1296
  if tailnet_ip:
1221
1297
  return {"base_url": f"http://{tailnet_ip}:{port_number}", "source": "standalone_tailnet", "status": "fallback"}
1222
- try:
1223
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1224
- try:
1225
- sock.connect(("8.8.8.8", 80))
1226
- ip = sock.getsockname()[0]
1227
- finally:
1228
- sock.close()
1229
- if ip and not ip.startswith(("127.", "169.254.")):
1230
- return {"base_url": f"http://{ip}:{port_number}", "source": "lan", "status": "fallback"}
1231
- except Exception:
1232
- pass
1233
1298
  return {"base_url": f"http://{socket.gethostname()}.local:{port_number}", "source": "bonjour", "status": "fallback"}
1234
1299
 
1235
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": "3e997fa963920ac130ead94e65a93b62069afc5e4569cf7bd4cfde93ba8dc9ec"
23
+ "sha256": "e88a4e5e02bc17d75bd064d71908ab84750f877613ce56e1efb4be3d453a5a81"
24
24
  },
25
25
  {
26
26
  "path": "payload/mac/VERSION",
27
- "sha256": "998f6b887ed7a6ef44e84210d0237b715bed42ea7254f48dc418afec0a484103"
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",
@@ -248,11 +248,11 @@
248
248
  },
249
249
  {
250
250
  "path": "payload/mac/install/doctor.sh",
251
- "sha256": "b20ad64348c03d33af89dd6ad72177be95a760371aae867cfd976dfe4cac4260"
251
+ "sha256": "1ba291c2c7def20af4ab4c850df0aaf54c41065da7e77255d8a6ff3bdfbeb2cc"
252
252
  },
253
253
  {
254
254
  "path": "payload/mac/install/install-runtime.sh",
255
- "sha256": "c45fec6a414a8b1870e9ea55e82d25b9cc1b786b882ed05f70fc0bff31a864d4"
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.2",
279
+ "package_version": "0.2.4",
280
280
  "schema_version": 1,
281
281
  "source_dirty": false,
282
- "source_revision": "a1eaa1ac"
282
+ "source_revision": "7f88c1b"
283
283
  }