octarin-cli 0.3.1 → 0.3.2

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.
@@ -44,6 +44,7 @@ import getpass
44
44
  import json
45
45
  import os
46
46
  import re
47
+ import ssl
47
48
  import subprocess
48
49
  import sys
49
50
  import urllib.error
@@ -940,6 +941,49 @@ def _filter_spans_by_cutoff(event: dict, cutoff: datetime | None) -> dict | None
940
941
  return event
941
942
 
942
943
 
944
+ _SSL_CTX: ssl.SSLContext | None = None
945
+
946
+
947
+ def _ssl_context() -> ssl.SSLContext:
948
+ """A cert-verifying TLS context that also works on Pythons whose default
949
+ trust store is empty — the cause of ``CERTIFICATE_VERIFY_FAILED: unable to
950
+ get local issuer certificate`` on many macOS (python.org) installs. Resolves
951
+ a CA bundle, all options verifying: ``OCTARIN_CA_BUNDLE`` / ``SSL_CERT_FILE``
952
+ env, then ``certifi`` if importable, then known system bundles, then the
953
+ interpreter default. Built once and reused.
954
+ """
955
+ global _SSL_CTX
956
+ if _SSL_CTX is not None:
957
+ return _SSL_CTX
958
+ cafile: str | None = None
959
+ for env in ("OCTARIN_CA_BUNDLE", "SSL_CERT_FILE"):
960
+ p = os.environ.get(env)
961
+ if p and os.path.exists(p):
962
+ cafile = p
963
+ break
964
+ if cafile is None:
965
+ try:
966
+ import certifi # noqa: PLC0415 - optional, used only if present
967
+
968
+ cafile = certifi.where()
969
+ except Exception:
970
+ for p in (
971
+ "/etc/ssl/cert.pem", # macOS base system, many Linux
972
+ "/opt/homebrew/etc/openssl@3/cert.pem", # Apple-silicon Homebrew
973
+ "/usr/local/etc/openssl@3/cert.pem", # Intel Homebrew
974
+ "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
975
+ "/etc/pki/tls/certs/ca-bundle.crt", # RHEL/CentOS
976
+ ):
977
+ if os.path.exists(p):
978
+ cafile = p
979
+ break
980
+ try:
981
+ _SSL_CTX = ssl.create_default_context(cafile=cafile)
982
+ except Exception:
983
+ _SSL_CTX = ssl.create_default_context()
984
+ return _SSL_CTX
985
+
986
+
943
987
  def post_event(
944
988
  url: str, key: str, event: dict, timeout: float = 20.0
945
989
  ) -> tuple[bool, str]:
@@ -955,7 +999,7 @@ def post_event(
955
999
  method="POST",
956
1000
  )
957
1001
  try:
958
- with urllib.request.urlopen(req, timeout=timeout) as resp:
1002
+ with urllib.request.urlopen(req, timeout=timeout, context=_ssl_context()) as resp:
959
1003
  raw = resp.read().decode("utf-8", "replace")
960
1004
  try:
961
1005
  n = json.loads(raw).get("span_count")
@@ -24,6 +24,7 @@ import getpass
24
24
  import hashlib
25
25
  import json
26
26
  import os
27
+ import ssl
27
28
  import subprocess
28
29
  import sys
29
30
  import time
@@ -476,6 +477,46 @@ def user_ref() -> str:
476
477
  return "unknown"
477
478
 
478
479
 
480
+ _SSL_CTX = None
481
+
482
+
483
+ def _ssl_context():
484
+ """Cert-verifying TLS context resilient to empty trust stores (the macOS
485
+ python.org ``CERTIFICATE_VERIFY_FAILED`` issue). Resolves a CA bundle —
486
+ OCTARIN_CA_BUNDLE / SSL_CERT_FILE env, certifi if importable, known system
487
+ bundles, else the interpreter default — and reuses it."""
488
+ global _SSL_CTX
489
+ if _SSL_CTX is not None:
490
+ return _SSL_CTX
491
+ cafile = None
492
+ for env in ("OCTARIN_CA_BUNDLE", "SSL_CERT_FILE"):
493
+ p = os.environ.get(env)
494
+ if p and os.path.exists(p):
495
+ cafile = p
496
+ break
497
+ if cafile is None:
498
+ try:
499
+ import certifi
500
+
501
+ cafile = certifi.where()
502
+ except Exception:
503
+ for p in (
504
+ "/etc/ssl/cert.pem",
505
+ "/opt/homebrew/etc/openssl@3/cert.pem",
506
+ "/usr/local/etc/openssl@3/cert.pem",
507
+ "/etc/ssl/certs/ca-certificates.crt",
508
+ "/etc/pki/tls/certs/ca-bundle.crt",
509
+ ):
510
+ if os.path.exists(p):
511
+ cafile = p
512
+ break
513
+ try:
514
+ _SSL_CTX = ssl.create_default_context(cafile=cafile)
515
+ except Exception:
516
+ _SSL_CTX = ssl.create_default_context()
517
+ return _SSL_CTX
518
+
519
+
479
520
  def post_event(event: dict) -> bool:
480
521
  """POST the IngestEvent. Returns True on 2xx, False otherwise (fail-open)."""
481
522
  url = os.environ.get("OCTARIN_INGEST_URL")
@@ -491,7 +532,7 @@ def post_event(event: dict) -> bool:
491
532
  if api_key:
492
533
  req.add_header("Authorization", f"Bearer {api_key}")
493
534
  try:
494
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp:
535
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S, context=_ssl_context()) as resp:
495
536
  return 200 <= resp.status < 300
496
537
  except Exception:
497
538
  return False
@@ -42,6 +42,7 @@ import getpass
42
42
  import hashlib
43
43
  import json
44
44
  import os
45
+ import ssl
45
46
  import subprocess
46
47
  import sys
47
48
  import time
@@ -547,6 +548,46 @@ def _notify_auth_required_once(project: str) -> None:
547
548
  )
548
549
 
549
550
 
551
+ _SSL_CTX = None
552
+
553
+
554
+ def _ssl_context():
555
+ """Cert-verifying TLS context resilient to empty trust stores (the macOS
556
+ python.org ``CERTIFICATE_VERIFY_FAILED`` issue). Resolves a CA bundle —
557
+ OCTARIN_CA_BUNDLE / SSL_CERT_FILE env, certifi if importable, known system
558
+ bundles, else the interpreter default — and reuses it."""
559
+ global _SSL_CTX
560
+ if _SSL_CTX is not None:
561
+ return _SSL_CTX
562
+ cafile = None
563
+ for env in ("OCTARIN_CA_BUNDLE", "SSL_CERT_FILE"):
564
+ p = os.environ.get(env)
565
+ if p and os.path.exists(p):
566
+ cafile = p
567
+ break
568
+ if cafile is None:
569
+ try:
570
+ import certifi
571
+
572
+ cafile = certifi.where()
573
+ except Exception:
574
+ for p in (
575
+ "/etc/ssl/cert.pem",
576
+ "/opt/homebrew/etc/openssl@3/cert.pem",
577
+ "/usr/local/etc/openssl@3/cert.pem",
578
+ "/etc/ssl/certs/ca-certificates.crt",
579
+ "/etc/pki/tls/certs/ca-bundle.crt",
580
+ ):
581
+ if os.path.exists(p):
582
+ cafile = p
583
+ break
584
+ try:
585
+ _SSL_CTX = ssl.create_default_context(cafile=cafile)
586
+ except Exception:
587
+ _SSL_CTX = ssl.create_default_context()
588
+ return _SSL_CTX
589
+
590
+
550
591
  def post_event(event: dict) -> bool:
551
592
  """POST the IngestEvent. Returns True on 2xx, False otherwise (fail-open).
552
593
 
@@ -585,7 +626,7 @@ def post_event(event: dict) -> bool:
585
626
  elif project:
586
627
  req.add_header("X-Octarin-Project", project)
587
628
  try:
588
- with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp:
629
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S, context=_ssl_context()) as resp:
589
630
  return HTTP_OK <= resp.status < HTTP_MULTIPLE_CHOICES
590
631
  except urllib.error.HTTPError as exc:
591
632
  # Strict-auth signal from the server: print the login.sh hint, once.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octarin-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`) and authorize a machine (`octarin login`). Streams your Claude Code / Cursor / Codex usage to your Octarin workspace.",
5
5
  "keywords": [
6
6
  "octarin",