devlinker 1.3.6__tar.gz → 1.3.8__tar.gz

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.
Files changed (33) hide show
  1. devlinker-1.3.8/MANIFEST.in +3 -0
  2. {devlinker-1.3.6 → devlinker-1.3.8}/PKG-INFO +1 -1
  3. devlinker-1.3.8/devlinker/devlinker_loader_instant.html +109 -0
  4. devlinker-1.3.8/devlinker/devlinker_loader_snippet.html +168 -0
  5. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/proxy.py +51 -9
  6. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/PKG-INFO +1 -1
  7. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/SOURCES.txt +3 -0
  8. {devlinker-1.3.6 → devlinker-1.3.8}/pyproject.toml +1 -1
  9. devlinker-1.3.8/setup.py +9 -0
  10. devlinker-1.3.6/setup.py +0 -3
  11. {devlinker-1.3.6 → devlinker-1.3.8}/LICENSE +0 -0
  12. {devlinker-1.3.6 → devlinker-1.3.8}/README.md +0 -0
  13. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/__init__.py +0 -0
  14. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/config.py +0 -0
  15. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/detection_state.py +0 -0
  16. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/detector.py +0 -0
  17. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/detector_ai.py +0 -0
  18. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/doctor.py +0 -0
  19. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/fix.py +0 -0
  20. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/fixer.py +0 -0
  21. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/global_state.py +0 -0
  22. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/inspect.py +0 -0
  23. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/logger.py +0 -0
  24. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/main.py +0 -0
  25. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/monitor.py +0 -0
  26. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/runner.py +0 -0
  27. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/share.py +0 -0
  28. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker/tunnel.py +0 -0
  29. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/dependency_links.txt +0 -0
  30. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/entry_points.txt +0 -0
  31. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/requires.txt +0 -0
  32. {devlinker-1.3.6 → devlinker-1.3.8}/devlinker.egg-info/top_level.txt +0 -0
  33. {devlinker-1.3.6 → devlinker-1.3.8}/setup.cfg +0 -0
@@ -0,0 +1,3 @@
1
+ include devlinker/devlinker_loader_instant.html
2
+ include devlinker/devlinker_loader_minimal.html
3
+ include devlinker/devlinker_loader_snippet.html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
5
  Author-email: Mani <mani1028@users.noreply.github.com>
6
6
  Requires-Python: >=3.7
@@ -0,0 +1,109 @@
1
+ <!-- DevLinker Instant Loader for Local/WLAN Users -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>DevLinker Loading</title>
8
+ <style>
9
+ * { box-sizing: border-box; }
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ background: #0b1220;
14
+ font-family: "Segoe UI", Arial, sans-serif;
15
+ color: #e2e8f0;
16
+ }
17
+ #devlinker-loader-min {
18
+ position: fixed;
19
+ inset: 0;
20
+ z-index: 99999;
21
+ display: grid;
22
+ place-items: center;
23
+ padding: 24px;
24
+ }
25
+ .devlinker-center {
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ gap: 10px;
30
+ }
31
+ .devlinker-logo {
32
+ font-size: 1.15rem;
33
+ font-weight: 700;
34
+ letter-spacing: 0.01em;
35
+ margin-bottom: 4px;
36
+ color: #f8fafc;
37
+ }
38
+ .devlinker-spinner {
39
+ width: 40px;
40
+ height: 40px;
41
+ border: 4px solid rgba(148, 163, 184, 0.25);
42
+ border-top: 4px solid #22d3ee;
43
+ border-radius: 50%;
44
+ animation: devlinker-spin 1s linear infinite;
45
+ }
46
+ .devlinker-text {
47
+ font-size: 1rem;
48
+ color: #cbd5e1;
49
+ font-weight: 600;
50
+ }
51
+ .devlinker-health {
52
+ font-size: 0.84rem;
53
+ color: #94a3b8;
54
+ opacity: 0.9;
55
+ }
56
+ .devlinker-powered {
57
+ position: fixed;
58
+ bottom: 18px;
59
+ left: 0;
60
+ width: 100vw;
61
+ text-align: center;
62
+ color: #94a3b8;
63
+ font-size: 0.92rem;
64
+ font-weight: 500;
65
+ opacity: 0.9;
66
+ pointer-events: none;
67
+ user-select: none;
68
+ }
69
+ @keyframes devlinker-spin {
70
+ to { transform: rotate(360deg); }
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div id="devlinker-loader-min">
76
+ <div class="devlinker-center">
77
+ <div class="devlinker-logo">DevLinker</div>
78
+ <div class="devlinker-spinner"></div>
79
+ <div class="devlinker-text">Loading your app...</div>
80
+ <div class="devlinker-health">Frontend connected • Backend connected</div>
81
+ </div>
82
+ </div>
83
+ <div class="devlinker-powered">Powered by DevLinker</div>
84
+ <script>
85
+ // Fetch the real page and replace the loader with a short minimum display for smoothness.
86
+ (async function() {
87
+ const minTime = 500;
88
+ const start = Date.now();
89
+ try {
90
+ const resp = await fetch(window.location.href, { headers: { "X-DevLinker-Instant": "1" } });
91
+ const html = await resp.text();
92
+ const elapsed = Date.now() - start;
93
+ const delay = Math.max(0, minTime - elapsed);
94
+ setTimeout(() => {
95
+ document.open();
96
+ document.write(html);
97
+ document.close();
98
+ }, delay);
99
+ } catch (e) {
100
+ // Show error if fetch fails
101
+ var loader = document.getElementById("devlinker-loader-min");
102
+ if (loader) {
103
+ loader.innerHTML += '<div style="color:#b91c1c;font-size:1.05rem;margin-top:1.5rem;">Failed to load app. Please check your connection.</div>';
104
+ }
105
+ }
106
+ })();
107
+ </script>
108
+ </body>
109
+ </html>
@@ -0,0 +1,168 @@
1
+ <!-- DevLinker Loader Overlay -->
2
+ <style>
3
+ #devlinker-loader {
4
+ position: fixed;
5
+ z-index: 99999;
6
+ inset: 0;
7
+ display: grid;
8
+ place-items: center;
9
+ min-height: 100vh;
10
+ background:
11
+ radial-gradient(700px 360px at 18% 12%, rgba(34, 211, 238, 0.18), transparent 60%),
12
+ radial-gradient(520px 280px at 82% 18%, rgba(59, 130, 246, 0.16), transparent 58%),
13
+ #0f172a;
14
+ font-family: "Segoe UI", Arial, sans-serif;
15
+ transition: opacity 0.4s cubic-bezier(.4,0,.2,1);
16
+ color: #e2e8f0;
17
+ }
18
+ .devlinker-panel {
19
+ width: min(640px, 92vw);
20
+ border: 1px solid rgba(148, 163, 184, 0.25);
21
+ border-radius: 18px;
22
+ background: rgba(15, 23, 42, 0.75);
23
+ box-shadow: 0 24px 70px rgba(2, 6, 23, 0.55);
24
+ backdrop-filter: blur(8px);
25
+ padding: 24px 22px;
26
+ }
27
+ .devlinker-top {
28
+ font-weight: 700;
29
+ font-size: 1.2rem;
30
+ color: #f8fafc;
31
+ margin-bottom: 12px;
32
+ }
33
+ .devlinker-title {
34
+ font-size: 1.15rem;
35
+ font-weight: 650;
36
+ margin-bottom: 8px;
37
+ color: #f8fafc;
38
+ }
39
+ .devlinker-stage {
40
+ min-height: 1.45rem;
41
+ font-size: 0.98rem;
42
+ color: #a5b4fc;
43
+ margin-bottom: 14px;
44
+ }
45
+ .devlinker-center {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ gap: 10px;
50
+ padding: 14px 0 16px;
51
+ }
52
+ .devlinker-spinner {
53
+ width: 52px;
54
+ height: 52px;
55
+ border: 4px solid rgba(148, 163, 184, 0.25);
56
+ border-top: 4px solid #22d3ee;
57
+ border-right: 4px solid #60a5fa;
58
+ border-radius: 50%;
59
+ animation: devlinker-spin 1s linear infinite;
60
+ }
61
+ .devlinker-text {
62
+ font-size: 1rem;
63
+ color: #cbd5e1;
64
+ font-weight: 600;
65
+ }
66
+ .devlinker-divider {
67
+ height: 1px;
68
+ width: 100%;
69
+ background: rgba(148, 163, 184, 0.22);
70
+ margin: 8px 0 14px;
71
+ }
72
+ .devlinker-info {
73
+ display: grid;
74
+ grid-template-columns: 1fr;
75
+ gap: 10px;
76
+ color: #cbd5e1;
77
+ font-size: 0.92rem;
78
+ }
79
+ .devlinker-tip {
80
+ color: #cbd5e1;
81
+ line-height: 1.4;
82
+ }
83
+ .devlinker-upgrade {
84
+ color: #93c5fd;
85
+ text-decoration: none;
86
+ font-weight: 600;
87
+ display: inline-block;
88
+ margin-top: 2px;
89
+ }
90
+ .devlinker-upgrade:hover {
91
+ color: #bfdbfe;
92
+ }
93
+ .devlinker-powered {
94
+ margin-top: 14px;
95
+ text-align: center;
96
+ color: #94a3b8;
97
+ font-size: 0.9rem;
98
+ font-weight: 500;
99
+ opacity: 0.95;
100
+ }
101
+ @keyframes devlinker-spin {
102
+ to { transform: rotate(360deg); }
103
+ }
104
+ @media (max-width: 640px) {
105
+ .devlinker-panel {
106
+ padding: 18px 16px;
107
+ }
108
+ .devlinker-title {
109
+ font-size: 1.04rem;
110
+ }
111
+ }
112
+ </style>
113
+ <div id="devlinker-loader">
114
+ <div class="devlinker-panel">
115
+ <div class="devlinker-top">DevLinker</div>
116
+ <div class="devlinker-title">Connecting your app...</div>
117
+ <div class="devlinker-stage" id="devlinker-stage">Establishing secure tunnel...</div>
118
+ <div class="devlinker-center">
119
+ <div class="devlinker-spinner"></div>
120
+ <div class="devlinker-text">Preparing full experience</div>
121
+ </div>
122
+ <div class="devlinker-divider"></div>
123
+ <div class="devlinker-info">
124
+ <div class="devlinker-tip">Tip: Share your app instantly with one link. No CORS issues, no port confusion.</div>
125
+ <div class="devlinker-tip">Go Pro: custom domains, faster sharing, and no branding.</div>
126
+ <a class="devlinker-upgrade" href="https://devlinker.app" target="_blank" rel="noopener">Explore Pro at devlinker.app</a>
127
+ </div>
128
+ <div class="devlinker-powered">Powered by DevLinker</div>
129
+ </div>
130
+ </div>
131
+ <script>
132
+ const stageMessages = [
133
+ "Establishing secure tunnel...",
134
+ "Routing frontend assets...",
135
+ "Connecting backend services...",
136
+ "Optimizing connection...",
137
+ "Almost ready..."
138
+ ];
139
+ let stageIndex = 0;
140
+ const stageElement = document.getElementById("devlinker-stage");
141
+ const stageTimer = setInterval(() => {
142
+ if (!stageElement) return;
143
+ stageIndex = (stageIndex + 1) % stageMessages.length;
144
+ stageElement.textContent = stageMessages[stageIndex];
145
+ }, 1000);
146
+
147
+ const start = Date.now();
148
+ const MIN_TIME = 800;
149
+ const MAX_TIME = 5000;
150
+ let hidden = false;
151
+
152
+ function hideLoader() {
153
+ if (hidden) return;
154
+ hidden = true;
155
+ clearInterval(stageTimer);
156
+ const elapsed = Date.now() - start;
157
+ const delay = Math.max(0, MIN_TIME - elapsed);
158
+ const el = document.getElementById("devlinker-loader");
159
+ setTimeout(() => {
160
+ if (el) {
161
+ el.style.opacity = "0";
162
+ setTimeout(() => el.remove(), 400);
163
+ }
164
+ }, delay);
165
+ }
166
+ window.addEventListener("load", hideLoader);
167
+ setTimeout(hideLoader, MAX_TIME);
168
+ </script>
@@ -142,12 +142,17 @@ def _build_target_ws_url(port: int, path: str, query: str) -> str:
142
142
 
143
143
 
144
144
  async def _forward_http(request: Request) -> Response:
145
- # Serve instant loader for local/LAN users unless X-DevLinker-Instant header is present
145
+ # Serve instant loader only for localhost HTML document navigations.
146
146
  client_ip = request.client.host if request.client else None
147
- def is_local_network(ip):
147
+ host_header = request.headers.get("host", "").lower()
148
+
149
+ def is_localhost_ip(ip):
148
150
  if not ip:
149
151
  return False
150
- if ip.startswith("127.") or ip == "localhost" or ip == "::1":
152
+ return ip.startswith("127.") or ip == "localhost" or ip == "::1"
153
+
154
+ def is_lan_ip(ip):
155
+ if not ip:
151
156
  return False
152
157
  if ip.startswith("192.168.") or ip.startswith("10."):
153
158
  return True
@@ -158,9 +163,46 @@ async def _forward_http(request: Request) -> Response:
158
163
  except Exception:
159
164
  return False
160
165
  return False
161
- is_local = is_local_network(client_ip)
166
+
167
+ def classify_mode(host: str, ip: str | None) -> str:
168
+ host_only = host.split(":", 1)[0] if host else ""
169
+ if host_only in ("localhost", "127.0.0.1", "::1"):
170
+ return "localhost"
171
+ if host_only.startswith("192.168.") or host_only.startswith("10."):
172
+ return "lan"
173
+ if host_only.startswith("172."):
174
+ try:
175
+ second = int(host_only.split(".")[1])
176
+ if 16 <= second <= 31:
177
+ return "lan"
178
+ except Exception:
179
+ pass
180
+ if host_only and host_only not in ("", "0.0.0.0"):
181
+ return "public"
182
+ if is_localhost_ip(ip):
183
+ return "localhost"
184
+ if is_lan_ip(ip):
185
+ return "lan"
186
+ return "public" if ip else "unknown"
187
+
188
+ mode = classify_mode(host_header, client_ip)
189
+ is_localhost = mode == "localhost"
190
+ is_lan = mode == "lan"
191
+ is_public = mode == "public"
162
192
  is_instant = request.headers.get("x-devlinker-instant") == "1"
163
- if is_local and not is_instant and request.method == "GET":
193
+ accept_header = request.headers.get("accept", "")
194
+ sec_fetch_dest = request.headers.get("sec-fetch-dest", "")
195
+ is_html_request = "text/html" in accept_header.lower()
196
+ is_document_navigation = sec_fetch_dest in ("", "document")
197
+ is_api_path = request.url.path == "/api" or request.url.path.startswith("/api/")
198
+ if (
199
+ is_localhost
200
+ and not is_instant
201
+ and request.method == "GET"
202
+ and is_html_request
203
+ and is_document_navigation
204
+ and not is_api_path
205
+ ):
164
206
  import os
165
207
  loader_path = os.path.join(os.path.dirname(__file__), "devlinker_loader_instant.html")
166
208
  with open(loader_path, encoding="utf-8") as f:
@@ -231,19 +273,19 @@ async def _forward_http(request: Request) -> Response:
231
273
  for s in ai_suggestions:
232
274
  print_fix(s)
233
275
 
234
- # Only inject loader for HTML responses, not localhost/loopback, but DO inject for LAN/WiFi clients
276
+ # Inject loader overlay only for LAN/WiFi/public HTML responses.
235
277
  headers = _filter_response_headers(dict(upstream.headers))
236
278
  content_type = headers.get("content-type", "")
237
279
  is_html = "text/html" in content_type
238
- is_public = client_ip and not is_local and (not client_ip.startswith("127.") and client_ip != "localhost" and client_ip != "::1")
239
280
  content = upstream.content
240
- if is_html and (is_local or is_public):
281
+ # Only inject loader if NOT an instant loader background fetch
282
+ if is_html and (is_lan or is_public) and not is_instant:
241
283
  try:
242
284
  html = content.decode(upstream.encoding or "utf-8", errors="replace")
243
285
  # Only inject if </body> exists
244
286
  if "</body>" in html:
245
287
  import os
246
- loader_file = "devlinker_loader_snippet.html" if is_public else "devlinker_loader_minimal.html"
288
+ loader_file = "devlinker_loader_snippet.html"
247
289
  with open(os.path.join(os.path.dirname(__file__), loader_file), encoding="utf-8") as f:
248
290
  loader = f.read()
249
291
  html = html.replace("</body>", loader + "</body>")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
5
  Author-email: Mani <mani1028@users.noreply.github.com>
6
6
  Requires-Python: >=3.7
@@ -1,4 +1,5 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  setup.py
@@ -7,6 +8,8 @@ devlinker/config.py
7
8
  devlinker/detection_state.py
8
9
  devlinker/detector.py
9
10
  devlinker/detector_ai.py
11
+ devlinker/devlinker_loader_instant.html
12
+ devlinker/devlinker_loader_snippet.html
10
13
  devlinker/doctor.py
11
14
  devlinker/fix.py
12
15
  devlinker/fixer.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlinker"
7
- version = "1.3.6"
7
+ version = "1.3.8"
8
8
  description = "A lightweight proxy that combines your frontend and backend into one link for easy development and sharing."
9
9
  authors = [
10
10
  { name = "Mani", email = "mani1028@users.noreply.github.com" }
@@ -0,0 +1,9 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="devlinker",
5
+ version="1.3.6",
6
+ packages=find_packages(),
7
+ include_package_data=True,
8
+ # ...existing setup args...
9
+ )
devlinker-1.3.6/setup.py DELETED
@@ -1,3 +0,0 @@
1
- from setuptools import setup
2
-
3
- setup()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes