shell-mirror 1.5.131 → 1.5.138
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 +1 -1
- package/public/.htaccess +7 -0
- package/public/app/dashboard.html +4 -3
- package/public/app/dashboard.js +4 -0
- package/public/app/terminal.html +14 -13
- package/public/app/terminal.js +39 -14
- package/public/contact.html +492 -0
- package/public/how-it-works.html +442 -0
- package/public/images/favicon.png +0 -0
- package/public/images/hero_mockup.png +0 -0
- package/public/images/private_by_design.png +0 -0
- package/public/images/real_terminal_view.png +0 -0
- package/public/images/same_session.png +0 -0
- package/public/images/shellmirror_clean_hero.png +0 -0
- package/public/images/shellmirror_macbook_hero.png +0 -0
- package/public/images/terminal-svgrepo-com.svg +7 -0
- package/public/index.html +909 -570
- package/public/privacy.html +362 -0
- package/server.js +9 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<link rel="icon" type="image/png" href="/images/favicon.png">
|
|
7
|
+
<title>>shell-mirror — Privacy</title>
|
|
8
|
+
<meta name="description" content="How >shell-mirror handles your data. Plain-language privacy explanation.">
|
|
9
|
+
|
|
10
|
+
<meta property="og:type" content="website">
|
|
11
|
+
<meta property="og:title" content=">shell-mirror — Privacy">
|
|
12
|
+
<meta property="og:description" content="How >shell-mirror handles your data.">
|
|
13
|
+
<meta property="og:url" content="https://shellmirror.app/privacy">
|
|
14
|
+
|
|
15
|
+
<meta property="twitter:card" content="summary">
|
|
16
|
+
<meta property="twitter:title" content=">shell-mirror — Privacy">
|
|
17
|
+
<meta property="twitter:description" content="How >shell-mirror handles your data.">
|
|
18
|
+
|
|
19
|
+
<!-- Fonts -->
|
|
20
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
21
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
22
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
23
|
+
|
|
24
|
+
<!-- Google Analytics 4 -->
|
|
25
|
+
<script>
|
|
26
|
+
window.dataLayer = window.dataLayer || [];
|
|
27
|
+
function gtag(){dataLayer.push(arguments);}
|
|
28
|
+
gtag('js', new Date());
|
|
29
|
+
gtag('config', 'G-LG7ZGLB8FK');
|
|
30
|
+
(function() {
|
|
31
|
+
var script = document.createElement('script');
|
|
32
|
+
script.async = true;
|
|
33
|
+
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-LG7ZGLB8FK';
|
|
34
|
+
script.onload = function() { window.gtagLoaded = true; };
|
|
35
|
+
script.onerror = function() { window.gtagLoaded = false; };
|
|
36
|
+
document.head.appendChild(script);
|
|
37
|
+
})();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<!-- Microsoft Clarity -->
|
|
41
|
+
<script type="text/javascript">
|
|
42
|
+
(function(c,l,a,r,i,t,y){
|
|
43
|
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
44
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
45
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
46
|
+
})(window, document, "clarity", "script", "sy1w2d7il7");
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
:root {
|
|
51
|
+
--bg-primary: #121315;
|
|
52
|
+
--bg-secondary: #1b1c1e;
|
|
53
|
+
--bg-tertiary: #1f2022;
|
|
54
|
+
--bg-hover: #292a2c;
|
|
55
|
+
--text-primary: #e3e2e5;
|
|
56
|
+
--text-secondary: #8a8f98;
|
|
57
|
+
--text-muted: #5a5f6a;
|
|
58
|
+
--accent: #7c4dff;
|
|
59
|
+
--accent-light: #cdbdff;
|
|
60
|
+
--accent-hover: #6833ea;
|
|
61
|
+
--success: #00c853;
|
|
62
|
+
--border: rgba(73, 68, 85, 0.15);
|
|
63
|
+
--border-visible: rgba(73, 68, 85, 0.3);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
67
|
+
|
|
68
|
+
body {
|
|
69
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
70
|
+
line-height: 1.6;
|
|
71
|
+
color: var(--text-primary);
|
|
72
|
+
background: var(--bg-primary);
|
|
73
|
+
min-height: 100vh;
|
|
74
|
+
-webkit-font-smoothing: antialiased;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
|
|
78
|
+
|
|
79
|
+
/* Header */
|
|
80
|
+
header {
|
|
81
|
+
position: fixed;
|
|
82
|
+
top: 0;
|
|
83
|
+
width: 100%;
|
|
84
|
+
z-index: 100;
|
|
85
|
+
background: rgba(18, 19, 21, 0.6);
|
|
86
|
+
backdrop-filter: blur(20px);
|
|
87
|
+
-webkit-backdrop-filter: blur(20px);
|
|
88
|
+
box-shadow: 0 8px 32px rgba(124, 77, 255, 0.04);
|
|
89
|
+
}
|
|
90
|
+
header .container {
|
|
91
|
+
display: flex;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
align-items: center;
|
|
94
|
+
padding-top: 16px;
|
|
95
|
+
padding-bottom: 16px;
|
|
96
|
+
}
|
|
97
|
+
.logo { font-size: 1.3rem; font-weight: 800; color: #f0eff2; letter-spacing: -0.03em; text-decoration: none; }
|
|
98
|
+
.nav-links { display: flex; gap: 32px; align-items: center; }
|
|
99
|
+
.nav-links a { color: var(--text-muted); text-decoration: none; font-size: 0.9rem; font-weight: 500; transition: color 0.2s ease; }
|
|
100
|
+
.nav-links a:hover { color: var(--text-primary); }
|
|
101
|
+
.header-right { display: flex; align-items: center; gap: 16px; }
|
|
102
|
+
.cta-button {
|
|
103
|
+
background: var(--accent); color: white; padding: 10px 22px;
|
|
104
|
+
text-decoration: none; border-radius: 8px; font-weight: 600;
|
|
105
|
+
border: none; cursor: pointer; transition: all 0.2s ease; font-size: 0.9rem;
|
|
106
|
+
box-shadow: 0 4px 16px rgba(124, 77, 255, 0.2);
|
|
107
|
+
}
|
|
108
|
+
.cta-button:hover { background: var(--accent-hover); box-shadow: 0 4px 20px rgba(124, 77, 255, 0.35); }
|
|
109
|
+
|
|
110
|
+
/* Page content */
|
|
111
|
+
.page-header {
|
|
112
|
+
text-align: center;
|
|
113
|
+
padding: 140px 0 40px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.page-header h1 {
|
|
117
|
+
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
|
118
|
+
font-weight: 800;
|
|
119
|
+
margin-bottom: 20px;
|
|
120
|
+
letter-spacing: -0.02em;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.page-header p {
|
|
124
|
+
color: var(--text-secondary);
|
|
125
|
+
font-size: 1.05rem;
|
|
126
|
+
max-width: 600px;
|
|
127
|
+
margin: 0 auto;
|
|
128
|
+
line-height: 1.7;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.privacy-body {
|
|
132
|
+
max-width: 680px;
|
|
133
|
+
margin: 0 auto;
|
|
134
|
+
padding: 40px 32px 100px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.privacy-body p {
|
|
138
|
+
color: var(--text-secondary);
|
|
139
|
+
font-size: 1rem;
|
|
140
|
+
line-height: 1.7;
|
|
141
|
+
margin-bottom: 16px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.privacy-body h2 {
|
|
145
|
+
font-size: 1.2rem;
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
color: var(--text-primary);
|
|
148
|
+
margin-top: 40px;
|
|
149
|
+
margin-bottom: 12px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.privacy-body h2:first-child { margin-top: 0; }
|
|
153
|
+
|
|
154
|
+
.privacy-body ul {
|
|
155
|
+
list-style: none;
|
|
156
|
+
margin-bottom: 16px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.privacy-body ul li {
|
|
160
|
+
color: var(--text-secondary);
|
|
161
|
+
font-size: 0.95rem;
|
|
162
|
+
line-height: 1.6;
|
|
163
|
+
padding: 6px 0 6px 20px;
|
|
164
|
+
position: relative;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.privacy-body ul li::before {
|
|
168
|
+
content: '';
|
|
169
|
+
position: absolute;
|
|
170
|
+
left: 0;
|
|
171
|
+
top: 14px;
|
|
172
|
+
width: 6px;
|
|
173
|
+
height: 6px;
|
|
174
|
+
background: var(--accent);
|
|
175
|
+
border-radius: 50%;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.privacy-note {
|
|
179
|
+
background: var(--bg-secondary);
|
|
180
|
+
border: 1px solid var(--border-visible);
|
|
181
|
+
border-radius: 12px;
|
|
182
|
+
padding: 24px;
|
|
183
|
+
margin-top: 40px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.privacy-note p {
|
|
187
|
+
color: var(--text-muted);
|
|
188
|
+
font-size: 0.9rem;
|
|
189
|
+
margin-bottom: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Footer */
|
|
193
|
+
footer {
|
|
194
|
+
background: rgba(13, 14, 16, 0.8);
|
|
195
|
+
border-top: 1px solid var(--border);
|
|
196
|
+
padding: 40px 0;
|
|
197
|
+
}
|
|
198
|
+
.footer-inner { display: flex; justify-content: space-between; align-items: center; }
|
|
199
|
+
.footer-brand {
|
|
200
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
201
|
+
font-weight: 700; font-size: 0.85rem; color: var(--text-secondary);
|
|
202
|
+
letter-spacing: 0.15em; text-transform: uppercase;
|
|
203
|
+
}
|
|
204
|
+
.footer-links { display: flex; gap: 28px; }
|
|
205
|
+
.footer-links a {
|
|
206
|
+
color: var(--text-muted); text-decoration: none;
|
|
207
|
+
font-family: 'Space Grotesk', sans-serif; font-size: 0.8rem;
|
|
208
|
+
letter-spacing: 0.1em; text-transform: uppercase; transition: color 0.2s ease;
|
|
209
|
+
}
|
|
210
|
+
.footer-links a:hover { color: var(--accent-light); }
|
|
211
|
+
|
|
212
|
+
@media (max-width: 768px) {
|
|
213
|
+
.container { padding: 0 20px; }
|
|
214
|
+
.page-header { padding: 120px 0 30px; }
|
|
215
|
+
.page-header h1 { font-size: 1.5rem; }
|
|
216
|
+
.header-right .nav-links { display: none; }
|
|
217
|
+
.footer-inner { flex-direction: column; gap: 16px; text-align: center; }
|
|
218
|
+
.privacy-body { padding: 40px 0 80px; }
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
221
|
+
</head>
|
|
222
|
+
<body>
|
|
223
|
+
<header>
|
|
224
|
+
<div class="container">
|
|
225
|
+
<a href="/" class="logo">>shell-mirror</a>
|
|
226
|
+
<div class="header-right">
|
|
227
|
+
<div class="nav-links">
|
|
228
|
+
<a href="/privacy">Privacy</a>
|
|
229
|
+
<a href="/contact">Contact</a>
|
|
230
|
+
</div>
|
|
231
|
+
<div id="header-cta">
|
|
232
|
+
<button class="cta-button" onclick="handleGoogleLogin()">Get Started</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</header>
|
|
237
|
+
|
|
238
|
+
<main>
|
|
239
|
+
<section class="page-header">
|
|
240
|
+
<div class="container">
|
|
241
|
+
<h1>Privacy</h1>
|
|
242
|
+
<p>>shell-mirror is a personal tool for accessing your own terminal session from your own phone.</p>
|
|
243
|
+
</div>
|
|
244
|
+
</section>
|
|
245
|
+
|
|
246
|
+
<div class="privacy-body">
|
|
247
|
+
<p>We keep the wording here simple on purpose. If the product asks you to sign in, it should be clear what that sign-in is for and what it is not for.</p>
|
|
248
|
+
|
|
249
|
+
<h2>Sign-in</h2>
|
|
250
|
+
<p>>shell-mirror uses Google OAuth 2.0 for authentication. When you sign in, we receive your name, email address, and Google profile ID. This information is used to connect your phone and computer to the same session. We do not request access to your Google contacts, files, calendar, or any other data beyond your basic profile.</p>
|
|
251
|
+
|
|
252
|
+
<h2>Terminal data</h2>
|
|
253
|
+
<p>Your terminal input and output are transmitted directly between your computer and your phone using a WebRTC peer-to-peer data channel. The >shell-mirror signaling server handles the initial connection setup (exchanging WebRTC offers, answers, and connection candidates) but does not see or store your terminal content.</p>
|
|
254
|
+
<p>When a direct peer-to-peer connection cannot be established (for example, due to network configuration), a TURN relay server is used to route the connection. In this case, terminal data passes through the relay, but the WebRTC data channel remains encrypted (DTLS-SRTP).</p>
|
|
255
|
+
|
|
256
|
+
<h2>Session information</h2>
|
|
257
|
+
<p>The >shell-mirror agent running on your computer sends a status heartbeat to shellmirror.app approximately every 60 seconds. This heartbeat includes your agent ID, machine name, platform, and the number of active sessions. It does not include any terminal content, commands, or output.</p>
|
|
258
|
+
<!-- TODO: Document whether heartbeat data is persisted server-side or only held in memory. This should be clarified by the maintainer. -->
|
|
259
|
+
|
|
260
|
+
<h2>Data storage</h2>
|
|
261
|
+
<p>>shell-mirror does not use a database for terminal sessions. Your sessions exist in memory on your own computer and are lost when the agent stops. The signaling server holds connection state in memory only.</p>
|
|
262
|
+
<p>Your sign-in session is stored as a browser cookie that expires after 30 days.</p>
|
|
263
|
+
<!-- TODO: Document the PHP backend's session storage and data retention policy. This is not currently documented in the codebase. -->
|
|
264
|
+
|
|
265
|
+
<h2>Analytics</h2>
|
|
266
|
+
<p>The >shell-mirror website (this marketing site and the dashboard) uses Google Analytics 4 and Microsoft Clarity for standard web analytics. These services use cookies. The terminal data path itself does not include analytics tracking.</p>
|
|
267
|
+
|
|
268
|
+
<h2>Third-party services</h2>
|
|
269
|
+
<ul>
|
|
270
|
+
<li>Google OAuth 2.0 — authentication</li>
|
|
271
|
+
<li>Google Analytics 4 — web analytics on the marketing site and dashboard</li>
|
|
272
|
+
<li>Microsoft Clarity — session recording on the marketing site and dashboard</li>
|
|
273
|
+
<li>OpenRelay TURN server — WebRTC relay fallback when direct peer-to-peer is not possible</li>
|
|
274
|
+
</ul>
|
|
275
|
+
|
|
276
|
+
<div class="privacy-note">
|
|
277
|
+
<p>>shell-mirror uses Google sign-in. It requests only your basic profile information (name and email) to authenticate your session. If you want to review or revoke this access, you can do so in your Google account settings.</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<footer>
|
|
282
|
+
<div class="container footer-inner">
|
|
283
|
+
<div class="footer-brand" id="version-info">>shell-mirror</div>
|
|
284
|
+
<div class="footer-links">
|
|
285
|
+
<a href="/how-it-works">How it works</a>
|
|
286
|
+
<a href="/privacy">Privacy</a>
|
|
287
|
+
<a href="/contact">Contact</a>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</footer>
|
|
291
|
+
</main>
|
|
292
|
+
|
|
293
|
+
<script>
|
|
294
|
+
async function checkAuthStatus() {
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetch('/php-backend/api/auth-status.php');
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
if (data.success && data.data && data.data.authenticated)
|
|
299
|
+
return { isAuthenticated: true, user: data.data.user };
|
|
300
|
+
} catch (error) {}
|
|
301
|
+
return { isAuthenticated: false, user: null };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sendGAEvent(n, p) { if (typeof gtag === 'function') gtag('event', n, p); }
|
|
305
|
+
|
|
306
|
+
async function handleGoogleLogin() {
|
|
307
|
+
sendGAEvent('cta_click', { cta_label: 'sign_in', cta_location: 'privacy_page' });
|
|
308
|
+
window.location.href = '/php-backend/api/auth-login.php?return=' + encodeURIComponent('/app/dashboard');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function loadVersionInfo() {
|
|
312
|
+
try {
|
|
313
|
+
const response = await fetch('/build-info.json');
|
|
314
|
+
const buildInfo = await response.json();
|
|
315
|
+
const el = document.getElementById('version-info');
|
|
316
|
+
if (el && buildInfo) el.textContent = `>shell-mirror v${buildInfo.version}`.toUpperCase();
|
|
317
|
+
} catch (e) {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Scroll depth
|
|
321
|
+
const scrollFired = {};
|
|
322
|
+
window.addEventListener('scroll', () => {
|
|
323
|
+
const pct = Math.round((window.scrollY + window.innerHeight) / document.body.scrollHeight * 100);
|
|
324
|
+
[25, 50, 75, 100].forEach(t => {
|
|
325
|
+
if (pct >= t && !scrollFired[t]) { scrollFired[t] = true; sendGAEvent('scroll_depth', { scroll_percent: t, page_title: document.title }); }
|
|
326
|
+
});
|
|
327
|
+
}, { passive: true });
|
|
328
|
+
|
|
329
|
+
// Time on page
|
|
330
|
+
const pageLoadTime = Date.now();
|
|
331
|
+
window.addEventListener('beforeunload', () => {
|
|
332
|
+
sendGAEvent('time_on_page', {
|
|
333
|
+
seconds_on_page: Math.round((Date.now() - pageLoadTime) / 1000),
|
|
334
|
+
page_title: document.title,
|
|
335
|
+
max_scroll_percent: Math.max(...Object.keys(scrollFired).map(Number), 0)
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
async function updateHeaderAndCTA() {
|
|
340
|
+
const authStatus = await checkAuthStatus();
|
|
341
|
+
const headerCta = document.getElementById('header-cta');
|
|
342
|
+
if (authStatus.isAuthenticated) {
|
|
343
|
+
headerCta.innerHTML = `
|
|
344
|
+
<span style="color: var(--text-secondary); font-size: 0.85rem;">Welcome, ${authStatus.user.name || authStatus.user.email}</span>
|
|
345
|
+
<a href="/app/dashboard.html" class="cta-button">Dashboard</a>
|
|
346
|
+
`;
|
|
347
|
+
headerCta.style.display = 'flex';
|
|
348
|
+
headerCta.style.alignItems = 'center';
|
|
349
|
+
headerCta.style.gap = '15px';
|
|
350
|
+
} else {
|
|
351
|
+
headerCta.innerHTML = '<button class="cta-button" onclick="handleGoogleLogin()">Sign In</button>';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
356
|
+
sendGAEvent('page_view', { page_title: document.title, page_location: window.location.href });
|
|
357
|
+
await updateHeaderAndCTA();
|
|
358
|
+
loadVersionInfo();
|
|
359
|
+
});
|
|
360
|
+
</script>
|
|
361
|
+
</body>
|
|
362
|
+
</html>
|
package/server.js
CHANGED
|
@@ -86,6 +86,15 @@ app.get('/', (req, res) => {
|
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
// Marketing pages - clean URLs
|
|
90
|
+
app.get('/how-it-works', (req, res) => {
|
|
91
|
+
res.sendFile(path.join(__dirname, 'public', 'how-it-works.html'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
app.get('/privacy', (req, res) => {
|
|
95
|
+
res.sendFile(path.join(__dirname, 'public', 'privacy.html'));
|
|
96
|
+
});
|
|
97
|
+
|
|
89
98
|
// LOCAL_TESTING_ONLY: Simple bypass for local development
|
|
90
99
|
if (isLocalEnvironment && !isProduction) {
|
|
91
100
|
app.get('/auth/local/bypass', (req, res) => {
|