Skip to content

Commit 41db120

Browse files
committed
Publish the terms of sale and the privacy notice
The order form collected company name, billing address and VAT ID for an annual licence, and neither document was reachable from anywhere: not the site, not the confirmation page, not the fulfilment mail. A buyer first met the licence inside the installed package. Both pages carry the existing text unchanged, on the same shell as the confirmation page. The form now states the renewal arrangement before the submit button and links both documents.
1 parent d8346e6 commit 41db120

3 files changed

Lines changed: 529 additions & 0 deletions

File tree

‎index.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,8 @@ <h3 id="order-title">Request your Pro invoice</h3>
11241124
<p class="field-hint" id="vat-hint">For the invoice</p>
11251125
</div>
11261126
</div>
1127+
<p class="invoice-note" id="renewal-note">$399 per year, per company. The licence renews automatically for successive 12-month terms unless you cancel. You can cancel at any time by emailing <a href="mailto:[email protected]">[email protected]</a>, or through the customer portal on your receipt if your purchase came with one. Cancellation takes effect at the end of the period you have paid for.</p>
1128+
<p class="invoice-note"><a href="terms.html">Licence terms</a> · <a href="privacy.html">Privacy notice</a></p>
11271129
<button class="button button-primary form-submit" type="submit">
11281130
Request invoice
11291131
<svg class="icon" aria-hidden="true"><use href="#arrow-right"/></svg>

‎privacy.html‎

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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">
6+
<title>Privacy. Oxpull</title>
7+
<meta name="description" content="What Oxpull holds, why, and for how long.">
8+
<style>
9+
:root {
10+
color-scheme: dark;
11+
--bg: #0d1410;
12+
--panel: #16201b;
13+
--panel-deep: #101813;
14+
--ink: #eef4ee;
15+
--muted: #a4b4a8;
16+
--subtle: #8ca093;
17+
--accent: #6fc59a;
18+
--accent-hover: #8fd8b3;
19+
--accent-ink: #0c2417;
20+
--accent-soft: rgba(111, 197, 154, .09);
21+
--accent-line: rgba(111, 197, 154, .3);
22+
--line: #2a382e;
23+
--line-soft: rgba(168, 196, 176, .1);
24+
--grid: rgba(168, 196, 176, .055);
25+
--glow: rgba(74, 144, 98, .13);
26+
--nav-bg: rgba(13, 20, 16, .91);
27+
--shadow: 0 24px 80px rgba(0, 0, 0, .22);
28+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
29+
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
30+
--radius: 16px;
31+
--max-width: 1160px;
32+
}
33+
@media (prefers-color-scheme: light) {
34+
:root {
35+
color-scheme: light;
36+
--bg: #f3f6f2;
37+
--panel: #ffffff;
38+
--panel-deep: #f6f8f4;
39+
--ink: #1b2420;
40+
--muted: #526359;
41+
--subtle: #617166;
42+
--accent: #1f6b4b;
43+
--accent-hover: #155339;
44+
--accent-ink: #ffffff;
45+
--accent-soft: rgba(31, 107, 75, .065);
46+
--accent-line: rgba(31, 107, 75, .28);
47+
--line: #d7e0d6;
48+
--line-soft: rgba(35, 72, 47, .09);
49+
--grid: rgba(35, 72, 47, .06);
50+
--glow: rgba(90, 161, 111, .13);
51+
--nav-bg: rgba(243, 246, 242, .92);
52+
--shadow: 0 24px 70px rgba(35, 61, 41, .075);
53+
}
54+
}
55+
*, *::before, *::after { box-sizing: border-box; }
56+
body {
57+
margin: 0;
58+
min-height: 100vh;
59+
min-height: 100svh;
60+
display: flex;
61+
flex-direction: column;
62+
background: var(--bg);
63+
color: var(--ink);
64+
font: 16px/1.6 var(--font);
65+
-webkit-font-smoothing: antialiased;
66+
}
67+
::selection { color: var(--accent-ink); background: var(--accent); }
68+
a { color: inherit; text-decoration: none; }
69+
:focus-visible { outline: 3px solid var(--accent); outline-offset: 5px; }
70+
h1, p { margin: 0; }
71+
svg { display: block; flex: none; }
72+
.svg-library { position: absolute; width: 0; height: 0; overflow: hidden; }
73+
.container { width: min(var(--max-width), calc(100% - 64px)); margin-inline: auto; }
74+
.site-header {
75+
position: sticky;
76+
top: 0;
77+
z-index: 10;
78+
border-bottom: 1px solid var(--line-soft);
79+
background: var(--nav-bg);
80+
backdrop-filter: blur(18px);
81+
-webkit-backdrop-filter: blur(18px);
82+
}
83+
.nav { min-height: 76px; display: flex; align-items: center; }
84+
.wordmark {
85+
display: inline-flex;
86+
align-items: center;
87+
gap: 10px;
88+
font-size: 23px;
89+
font-weight: 720;
90+
letter-spacing: -.055em;
91+
}
92+
.brand-icon { width: 32px; height: 32px; color: var(--accent); }
93+
main {
94+
flex: 1;
95+
padding-block: clamp(48px, 9vw, 104px);
96+
background:
97+
radial-gradient(ellipse at 75% 0, var(--glow), transparent 65%),
98+
linear-gradient(var(--grid) 1px, transparent 1px),
99+
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
100+
background-size: auto, 56px 56px, 56px 56px;
101+
}
102+
.thanks {
103+
max-width: 680px;
104+
margin-inline: auto;
105+
padding: clamp(24px, 5vw, 48px);
106+
border: 1px solid var(--accent-line);
107+
border-radius: var(--radius);
108+
background: linear-gradient(135deg, var(--accent-soft), transparent 65%), var(--panel);
109+
box-shadow: var(--shadow);
110+
}
111+
.check {
112+
width: 44px;
113+
height: 44px;
114+
margin-bottom: 25px;
115+
padding: 11px;
116+
border: 1px solid var(--accent-line);
117+
border-radius: 10px;
118+
color: var(--accent);
119+
background: var(--accent-soft);
120+
}
121+
h1 {
122+
font-size: clamp(2.7rem, 6vw, 4rem);
123+
font-weight: 620;
124+
letter-spacing: -.064em;
125+
line-height: 1.025;
126+
text-wrap: balance;
127+
}
128+
.thanks p { margin-top: 25px; color: var(--muted); }
129+
code {
130+
padding: .1em .35em;
131+
border: 1px solid var(--line);
132+
border-radius: 4px;
133+
background: var(--panel-deep);
134+
color: var(--ink);
135+
font: .88em var(--mono);
136+
overflow-wrap: anywhere;
137+
}
138+
.thanks p a { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
139+
.thanks p a:hover { color: var(--accent-hover); }
140+
.button {
141+
display: inline-flex;
142+
align-items: center;
143+
justify-content: center;
144+
min-height: 46px;
145+
margin-top: 30px;
146+
padding: 11px 19px;
147+
border: 1px solid var(--accent);
148+
border-radius: 7px;
149+
background: var(--accent);
150+
color: var(--accent-ink);
151+
font-size: 14px;
152+
font-weight: 600;
153+
line-height: 1.4;
154+
box-shadow: 0 2px 0 rgba(0, 0, 0, .08);
155+
}
156+
.button:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
157+
.site-footer { padding: 37px 0 28px; border-top: 1px solid var(--line); background: var(--panel-deep); }
158+
.site-footer .wordmark { font-size: 21px; }
159+
.site-footer .brand-icon { width: 28px; height: 28px; }
160+
.footer-bottom {
161+
margin-top: 27px;
162+
padding-top: 27px;
163+
border-top: 1px solid var(--line);
164+
color: var(--subtle);
165+
font: 10px/1.7 var(--mono);
166+
}
167+
@media (max-width: 600px) {
168+
.container { width: calc(100% - 40px); }
169+
.nav { min-height: 66px; }
170+
.wordmark { font-size: 22px; gap: 8px; }
171+
.brand-icon { width: 29px; height: 29px; }
172+
}
173+
</style>
174+
<style>
175+
.legal { max-width: 720px; margin: 0 auto; padding: 56px 0 88px; }
176+
.legal h1 { font-size: 2rem; margin: 0 0 28px; letter-spacing: -.02em; }
177+
.legal h2 { font-size: 1.1rem; margin: 40px 0 12px; letter-spacing: -.01em; }
178+
.legal p, .legal li { color: var(--muted); line-height: 1.7; }
179+
.legal p { margin: 0 0 16px; }
180+
.legal ul { margin: 0 0 16px; padding-left: 20px; }
181+
.legal li { margin: 0 0 8px; }
182+
.legal strong { color: var(--ink); font-weight: 600; }
183+
.legal code { font-family: var(--mono); font-size: .9em; }
184+
.legal pre { background: var(--panel-deep); border: 1px solid var(--line);
185+
border-radius: 10px; padding: 14px 16px; overflow-x: auto; margin: 0 0 16px; }
186+
.legal pre code { color: var(--muted); }
187+
@media (max-width: 640px) { .legal { padding: 32px 16px 64px; } }
188+
</style>
189+
</head>
190+
<body>
191+
<svg class="svg-library" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
192+
<symbol id="brand" viewBox="0 0 32 32">
193+
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
194+
<path d="M10 11C5 11 3 8 3 4M22 11c5 0 7-3 7-7M9 11l2 13 5 4 5-4 2-13-7-4z"/>
195+
<path d="M12 16h1m6 0h1m-7 6h6"/>
196+
</g>
197+
</symbol>
198+
</svg>
199+
200+
<header class="site-header">
201+
<nav class="nav container" aria-label="Main">
202+
<a class="wordmark" href="./">
203+
<svg class="brand-icon" aria-hidden="true"><use href="#brand"/></svg>
204+
oxpull
205+
</a>
206+
</nav>
207+
</header>
208+
209+
<main>
210+
<div class="container">
211+
<article class="legal">
212+
<h1>Privacy</h1>
213+
<p>Last updated 2026-08-20.</p>
214+
<p>This page says exactly what we hold about you and your company. It is short because the list is short.</p>
215+
<h2>What the software sends us: nothing</h2>
216+
<p>Oxpull Pro makes no network call to us. Not at import, not at startup, not on a schedule. There is no licence check, no telemetry and no phone-home. Your task payloads, arguments, results, database contents and queue traffic never leave your infrastructure, and we could not read them if we wanted to.</p>
217+
<p>The package installs as readable Python and the free core is public, so this is checkable rather than asserted.</p>
218+
<p>Everything below is about the two places we do hold data: the record we create when you buy, and the log our package index writes when you install.</p>
219+
<h2>What we hold when you buy</h2>
220+
<p>For each licence:</p>
221+
<ul><li>Your company name.</li><li>One contact email address.</li><li>The order and subscription identifiers from our payment processor.</li><li>The username of your index credential.</li><li>A salted scrypt hash of your credential password.</li><li>Timestamps: when the credential was issued, when it expires, and if it was revoked, when and why.</li><li>A free-text note field we use for operational context, such as &quot;renewed&quot; or &quot;rotated after a leak report&quot;.</li></ul>
222+
<p>We never hold your password. It exists only as a salted hash, which is why a lost password is rotated rather than recovered.</p>
223+
<p>Some purchases go through our payment processor, acting as merchant of record. For those purchases, we do not receive card numbers or expiry dates.</p>
224+
<p>For order-form purchases, we invoice directly and hold the billing details needed for invoicing.</p>
225+
<h2>The index access log</h2>
226+
<p>Our package index writes a log row for every single install request. Each row holds:</p>
227+
<ul><li>The time of the request.</li><li>The credential username, which maps one-to-one to your company.</li><li>The package file requested, so the package name and version.</li><li>The HTTP status we returned.</li><li><strong>Your IP address.</strong></li><li><strong>The installer&#x27;s user-agent string.</strong></li></ul>
228+
<p>That last one is more than a browser string, so here is what it actually contains. pip sends a JSON blob. This is a real one, from our own test install:</p>
229+
<pre><code>{&quot;ci&quot;:null,&quot;cpu&quot;:&quot;arm64&quot;,&quot;distro&quot;:{&quot;name&quot;:&quot;macOS&quot;,&quot;version&quot;:&quot;26.5.2&quot;},
230+
&quot;implementation&quot;:{&quot;name&quot;:&quot;CPython&quot;,&quot;version&quot;:&quot;3.12.13&quot;},
231+
&quot;installer&quot;:{&quot;name&quot;:&quot;pip&quot;,&quot;version&quot;:&quot;26.0&quot;},
232+
&quot;openssl_version&quot;:&quot;OpenSSL 3.6.3 9 Jun 2026&quot;,&quot;python&quot;:&quot;3.12.13&quot;,
233+
&quot;rustc_version&quot;:&quot;1.96.1&quot;,&quot;system&quot;:{&quot;name&quot;:&quot;Darwin&quot;,&quot;release&quot;:&quot;25.5.0&quot;}}</code></pre>
234+
<p>So for every install request, tied to your company and alongside your IP address, we record your operating system and its version, your CPU architecture, your Python version, your OpenSSL version, your rustc version, whether the install ran in CI, and which installer and version you used. pip sends this by default to every index it talks to, including PyPI. We are telling you because we receive it, not because we asked for it.</p>
235+
<h2>Why we keep it, and what it is not for</h2>
236+
<p>One reason: tracing a leaked credential. If a username and password turn up somewhere public, the log tells us which company they belong to, so we rotate one credential instead of forcing a rotation on everyone.</p>
237+
<p>It is not used for analytics, profiling, resale, advertising, usage-based pricing, or checking up on how much you use the product. We do not build a usage profile from it, and there is no product surface anywhere that reads it.</p>
238+
<h2>Who can reach it</h2>
239+
<p>The log lives in one database on one server, in a directory only the index&#x27;s own system account can read. Reaching it means administrative access to that server, which today is held by the operator alone. There is no dashboard, no third-party analytics tool and no export.</p>
240+
<h2>How long we keep it</h2>
241+
<p>We keep the IP address and the installer string for 90 days, then delete them. Ninety days is long enough to investigate a credential leak, which is the only thing we collect them for.</p>
242+
<p>The rest of the row, the time, the package file and the status, has nothing personal in it, and we keep it as operational history.</p>
243+
<p>The licence record itself, in the section above, we keep for as long as you are a customer and afterwards for our own accounting.</p>
244+
<h2>Who else touches your data</h2>
245+
<ul><li><strong>The payment processor</strong>, for merchant-of-record purchases: checkout, payment, invoicing and tax. It holds your billing details under its own terms. For order-form purchases, we invoice directly.</li><li><strong>The company hosting the private index</strong>, which runs the server the index and its log sit on.</li><li><strong>GitHub Pages</strong>, which serves the public website and sees ordinary web request logs that we never see.</li><li><strong>The form provider behind the waitlist form on the website</strong>, which receives the email address you type into it.</li></ul>
246+
<h2>The waitlist</h2>
247+
<p>If you gave us an email address on the website, it is used for one thing: telling you when something ships. Reply with &quot;remove&quot;, or write to [email protected], and it is deleted.</p>
248+
<h2>Cookies</h2>
249+
<p>The website sets none. The package index sets none. There is no analytics script, no tracker and no tag on either.</p>
250+
<h2>Your data, and asking us about it</h2>
251+
<p>Write to [email protected] and we will tell you what we hold about your company, correct it, or delete it.</p>
252+
<h2>Changes</h2>
253+
<p>If this page changes, the date at the top changes with it.</p>
254+
</article>
255+
</div>
256+
</main>
257+
258+
<footer class="site-footer">
259+
<div class="container">
260+
<a class="wordmark" href="./">
261+
<svg class="brand-icon" aria-hidden="true"><use href="#brand"/></svg>
262+
oxpull
263+
</a>
264+
<p class="footer-bottom">No analytics, no trackers, no cookies on this page.</p>
265+
</div>
266+
</footer>
267+
</body>
268+
</html>

0 commit comments

Comments
 (0)