+9
README.md
+9
README.md
···
6
6
7
7
A demo server is running at https://tapapp.lol.
8
8
9
+
### Authentication
10
+
11
+
Tap uses Bluesky App Passwords (not your main account password).
12
+
13
+
- Enter your Bluesky handle and an App Password on the home page to sign in.
14
+
- The server stores your access and refresh tokens in memory for the duration of your session.
15
+
- Tokens are refreshed automatically via `com.atproto.server.refreshSession`.
16
+
- You can revoke the App Password any time in your Bluesky account settings.
17
+
9
18
### Export features
10
19
11
20
- <strong>Export</strong> builds a `.fountain` file in plain text format and triggers a download.
+22
-6
server/main.go
+22
-6
server/main.go
···
69
69
// 1) Read record metadata
70
70
getRecURL := "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=" + s.DID + "&collection=lol.tapapp.tap.doc&rkey=current"
71
71
req, _ := http.NewRequest(http.MethodGet, getRecURL, nil)
72
-
req.Header.Set("Authorization", "Bearer " + s.AccessJWT)
72
+
req.Header.Set("Authorization", "Bearer "+s.AccessJWT)
73
73
resp, err := authedDo(w, r, req)
74
74
if err != nil {
75
75
http.Error(w, "getRecord failed", http.StatusBadGateway)
···
111
111
// 2) Download blob (retry once on transient errors)
112
112
blobURL := "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + s.DID + "&cid=" + cid
113
113
bReq, _ := http.NewRequest(http.MethodGet, blobURL, nil)
114
-
bReq.Header.Set("Authorization", "Bearer " + s.AccessJWT)
114
+
bReq.Header.Set("Authorization", "Bearer "+s.AccessJWT)
115
115
bRes, err := authedDo(w, r, bReq)
116
116
if err != nil {
117
117
http.Error(w, "getBlob failed", http.StatusBadGateway)
···
706
706
// Routes
707
707
mux.HandleFunc("/", handleIndex)
708
708
mux.HandleFunc("/about", handleAbout)
709
+
mux.HandleFunc("/privacy", handlePrivacy)
710
+
mux.HandleFunc("/terms", handleTerms)
709
711
mux.HandleFunc("/health", handleHealth)
710
712
mux.HandleFunc("/preview", handlePreview)
711
713
// Multi-doc (ATProto-backed)
···
715
717
mux.HandleFunc("/atp/session", handleATPSession)
716
718
mux.HandleFunc("/atp/post", handleATPPost)
717
719
mux.HandleFunc("/atp/doc", handleATPDoc)
718
-
// OAuth routes disabled (app-passwords in use)
719
-
// mux.HandleFunc("/oauth/login", handleOAuthLogin)
720
-
// mux.HandleFunc("/oauth/callback", handleOAuthCallback)
721
720
722
721
addr := getEnv("PORT", "8088")
723
722
log.Printf("tap (Go) server listening on http://localhost:%s", addr)
···
767
766
Title: "About Tap",
768
767
}
769
768
render(w, "about.html", data)
769
+
}
770
+
771
+
func handlePrivacy(w http.ResponseWriter, r *http.Request) {
772
+
data := struct {
773
+
Title string
774
+
}{
775
+
Title: "Privacy Policy",
776
+
}
777
+
render(w, "privacy.html", data)
778
+
}
779
+
780
+
func handleTerms(w http.ResponseWriter, r *http.Request) {
781
+
data := struct {
782
+
Title string
783
+
}{
784
+
Title: "Terms of Service",
785
+
}
786
+
render(w, "terms.html", data)
770
787
}
771
788
772
789
func handleHealth(w http.ResponseWriter, r *http.Request) {
···
934
951
}
935
952
return res, nil
936
953
}
937
-
+1
-1
server/templates/about.html
+1
-1
server/templates/about.html
···
39
39
40
40
</span>
41
41
<span class="footer-right">
42
-
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a>
42
+
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a>
43
43
</span>
44
44
</p>
45
45
</footer>
+1
-1
server/templates/index.html
+1
-1
server/templates/index.html
···
58
58
</span>
59
59
</span>
60
60
<span class="footer-right">
61
-
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a>
61
+
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a>
62
62
</span>
63
63
</p>
64
64
</footer>
+135
server/templates/privacy.html
+135
server/templates/privacy.html
···
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>{{ .Title }}</title>
7
+
<link rel="stylesheet" href="/static/styles.css"/>
8
+
<script data-goatcounter="https://tap-editor.goatcounter.com/count"
9
+
async src="//gc.zgo.at/count.js"></script>
10
+
</head>
11
+
<body>
12
+
<header class="container header">
13
+
<h1>Tap Privacy Policy</h1>
14
+
<nav>
15
+
<a href="/">Home</a>
16
+
<span id="header-user" class="sp" style="margin-left:12px; opacity:.85; display:none"></span>
17
+
<button id="header-logout" class="sp" style="display:none">Logout</button>
18
+
</nav>
19
+
</header>
20
+
21
+
<main class="container prose">
22
+
<h2>Overview</h2>
23
+
<p>Limeleaf Worker Collective, LLC ("we", "our", or "the Service") is committed to protecting your privacy. This Privacy Policy explains how we handle information when you use our Tap service.</p>
24
+
25
+
<h2>Information We Don't Collect</h2>
26
+
<p>We are a privacy-first service, which we define as:</p>
27
+
<ul>
28
+
<li>No personal data is stored on our servers</li>
29
+
<li>No tracking cookies</li>
30
+
<li>No advertising or marketing data collection</li>
31
+
<li>No user behavior tracking</li>
32
+
<li>No data sharing with third parties</li>
33
+
</ul>
34
+
35
+
<h2>Authentication</h2>
36
+
<p>When you sign in, Tap uses Bluesky App Passwords (not your main account password):</p>
37
+
<ul>
38
+
<li>We never ask for or store your main Bluesky password</li>
39
+
<li>The server stores your access and refresh tokens in memory for your session</li>
40
+
<li>No user profile data is persisted to disk on our servers</li>
41
+
<li>You can revoke the App Password at any time in your Bluesky account settings</li>
42
+
</ul>
43
+
44
+
<h2>Tap Document Data</h2>
45
+
<p>When you create or view documents in Tap:</p>
46
+
<ul>
47
+
<li>Data is stored only in your AT Protocol Personal Data Server (PDS)</li>
48
+
<li>We act only as a viewer/interface for your AT Protocol data</li>
49
+
<li>No AT Protocol content is cached or stored by our service</li>
50
+
<li>Content permissions are determined by the AT Protocol network</li>
51
+
</ul>
52
+
53
+
<h2>Third-Party Services</h2>
54
+
<p>We use minimal third-party services:</p>
55
+
<ul>
56
+
<li>Fly.io: For hosting and content delivery. Fly.io may collect basic access logs (IP addresses, user agents) for security and performance purposes. See Fly.io's Privacy Policy</li>
57
+
<li>Bluesky: For authentication and authorization</li>
58
+
</ul>
59
+
60
+
<h2>Your Rights</h2>
61
+
<p>You have the right to:</p>
62
+
<ul>
63
+
<li>Access the service without providing personal information</li>
64
+
<li>Use the service without creating an account</li>
65
+
<li>Revoke your Bluesky App Password at any time</li>
66
+
<li>Clear your browser data to remove any local session information</li>
67
+
</ul>
68
+
69
+
<h2>Data Security</h2>
70
+
<p>Since we don't collect or store personal data, there is minimal security risk. However, we implement standard web security practices including HTTPS encryption for all connections.</p>
71
+
72
+
<h2>Children's Privacy</h2>
73
+
<p>Our service does not knowingly collect any information from children under 13. The service is designed to be used without providing any personal information.</p>
74
+
75
+
<h2>Changes to This Policy</h2>
76
+
<p>We may update this Privacy Policy from time to time. Changes will be posted on this page with an updated "Last updated" date.</p>
77
+
78
+
<h2>Contact</h2>
79
+
<p>If you have questions about this Privacy Policy, you can contact us at: <a href="mailto:info@limeleaf.coop">info@limeleaf.coop</a></p>
80
+
</main>
81
+
82
+
<footer class="container footer">
83
+
<p>
84
+
<span class="footer-left">
85
+
86
+
</span>
87
+
<span class="footer-right">
88
+
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a>
89
+
</span>
90
+
</p>
91
+
</footer>
92
+
<script type="module">
93
+
(async () => {
94
+
const userEl = document.getElementById('header-user');
95
+
const btn = document.getElementById('header-logout');
96
+
if (!userEl || !btn) return;
97
+
98
+
const show = (handle) => {
99
+
userEl.textContent = handle || '';
100
+
userEl.style.display = handle ? 'inline' : 'none';
101
+
btn.style.display = handle ? 'inline-block' : 'none';
102
+
const sampleWrap = document.getElementById('footer-sample-wrap');
103
+
if (sampleWrap) sampleWrap.style.display = handle ? 'inline' : 'none';
104
+
};
105
+
const hide = () => show('');
106
+
107
+
const sampleWrap = document.getElementById('footer-sample-wrap');
108
+
try {
109
+
const res = await fetch('/atp/session', { method: 'GET' });
110
+
if (res.ok && res.status !== 204) {
111
+
const s = await res.json();
112
+
show(s?.handle);
113
+
} else {
114
+
hide();
115
+
}
116
+
} catch { hide(); }
117
+
118
+
window.addEventListener('atp-login', (e) => {
119
+
try { show(e?.detail?.session?.handle || ''); if (sampleWrap) sampleWrap.style.display = 'inline'; } catch {}
120
+
});
121
+
window.addEventListener('atp-logout', () => { hide(); if (sampleWrap) sampleWrap.style.display = 'none'; });
122
+
123
+
if (!('___tapHeaderLogoutBound' in window)) {
124
+
(window).___tapHeaderLogoutBound = true;
125
+
btn.addEventListener('click', async () => {
126
+
try { await fetch('/atp/session', { method: 'DELETE' }); } catch {}
127
+
window.dispatchEvent(new CustomEvent('atp-logout'));
128
+
});
129
+
}
130
+
})();
131
+
</script>
132
+
</body>
133
+
</html>
134
+
135
+
+155
server/templates/terms.html
+155
server/templates/terms.html
···
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>{{ .Title }}</title>
7
+
<link rel="stylesheet" href="/static/styles.css"/>
8
+
<script data-goatcounter="https://tap-editor.goatcounter.com/count"
9
+
async src="//gc.zgo.at/count.js"></script>
10
+
</head>
11
+
<body>
12
+
<header class="container header">
13
+
<h1>Tap Terms of Service</h1>
14
+
<nav>
15
+
<a href="/">Home</a>
16
+
<span id="header-user" class="sp" style="margin-left:12px; opacity:.85; display:none"></span>
17
+
<button id="header-logout" class="sp" style="display:none">Logout</button>
18
+
</nav>
19
+
</header>
20
+
21
+
<main class="container prose">
22
+
<p>Last updated: September 5, 2025</p>
23
+
24
+
<h2>1. Acceptance of Terms</h2>
25
+
26
+
<p>By accessing or using Tap ("the Service"), you agree to be bound by these Terms of Service as set forth by Limeleaf Worker Collective, LLC ("Limeleaf"). If you do not agree to these terms, please do not use the Service.</p>
27
+
28
+
<h2>2. Description of Service</h2>
29
+
30
+
<p>Tap is a web-based editor and viewer for screenplay files in the Fountain format. The Service allows users to:</p>
31
+
<ul>
32
+
<li>Create and edit Fountain screenplays</li>
33
+
<li>Export Fountain screenplays</li>
34
+
<li>Store Fountain screenplays in AT Protocol collections</li>
35
+
</ul>
36
+
37
+
<h2>3. Acceptable Use</h2>
38
+
39
+
<p>You agree to use the Service only for lawful purposes and in accordance with these Terms. You agree not to:</p>
40
+
<ul>
41
+
<li>Use the Service in any way that violates applicable laws or regulations</li>
42
+
<li>Attempt to gain unauthorized access to any portion of the Service</li>
43
+
<li>Interfere with or disrupt the Service or servers hosting the Service</li>
44
+
<li>Use automated means to access the Service without our express permission</li>
45
+
<li>Impersonate any person or entity</li>
46
+
</ul>
47
+
48
+
<h2>4. Content and Intellectual Property</h2>
49
+
50
+
<p>The Service displays content that you, the user, create. We do not claim ownership of any user-generated content. The Service interface and code are open source. You must respect the intellectual property rights of content creators.</p>
51
+
52
+
<h2>5. Privacy and Data</h2>
53
+
54
+
<p>Your use of the Service is subject to our Privacy Policy. By using the Service, you consent to our practices described in the Privacy Policy.</p>
55
+
56
+
<h2>6. Authentication</h2>
57
+
58
+
<p>You authenticate using Bluesky App Passwords (not your main account password):</p>
59
+
60
+
<ul>
61
+
<li>You can create and revoke an App Password at any time in your Bluesky settings</li>
62
+
<li>The Service uses your App Password to obtain access and refresh tokens from Bluesky</li>
63
+
<li>Tokens are stored in memory on the server for the duration of your session</li>
64
+
<li>We request only the minimum permissions needed to manage your Tap documents</li>
65
+
</ul>
66
+
67
+
<h2>7. Disclaimers and Limitations</h2>
68
+
69
+
<p>THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND:</p>
70
+
71
+
<ul>
72
+
<li>We do not guarantee uninterrupted or error-free service</li>
73
+
<li>We are not responsible for content stored by users</li>
74
+
<li>Use of the Service is at your own risk</li>
75
+
</ul>
76
+
77
+
<h2>8. Limitation of Liability</h2>
78
+
79
+
<p>To the maximum extent permitted by law, we shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the Service.</p>
80
+
81
+
<h2>9. Indemnification</h2>
82
+
83
+
<p>You agree to indemnify and hold harmless Limeleaf Worker Collective and its operators from any claims, damages, losses, liabilities, costs, and expenses arising from your use of the Service or violation of these Terms.</p>
84
+
85
+
<h2>10. Changes to Terms</h2>
86
+
87
+
<p>We may modify these Terms at any time. Changes will be effective immediately upon posting to this page. Your continued use of the Service after any changes indicates your acceptance of the modified Terms.</p>
88
+
89
+
<h2>11. Termination</h2>
90
+
91
+
<p>We may terminate or suspend access to the Service immediately, without prior notice, for any reason, including breach of these Terms.</p>
92
+
93
+
<h2>12. Governing Law</h2>
94
+
95
+
<p>These Terms shall be governed by the laws of the jurisdiction of the state of New York in the United States, without regard to conflict of law principles.</p>
96
+
97
+
<h2>13. Contact Information</h2>
98
+
99
+
<p>For questions about these Terms of Service, please write to info@limeleaf.coop.
100
+
</main>
101
+
102
+
<footer class="container footer">
103
+
<p>
104
+
<span class="footer-left">
105
+
106
+
</span>
107
+
<span class="footer-right">
108
+
© <a href="https://limeleaf.coop">Limeleaf Worker Collective</a> • <a href="/privacy">Privacy Policy</a> • <a href="/terms">Terms of Service</a>
109
+
</span>
110
+
</p>
111
+
</footer>
112
+
<script type="module">
113
+
(async () => {
114
+
const userEl = document.getElementById('header-user');
115
+
const btn = document.getElementById('header-logout');
116
+
if (!userEl || !btn) return;
117
+
118
+
const show = (handle) => {
119
+
userEl.textContent = handle || '';
120
+
userEl.style.display = handle ? 'inline' : 'none';
121
+
btn.style.display = handle ? 'inline-block' : 'none';
122
+
const sampleWrap = document.getElementById('footer-sample-wrap');
123
+
if (sampleWrap) sampleWrap.style.display = handle ? 'inline' : 'none';
124
+
};
125
+
const hide = () => show('');
126
+
127
+
const sampleWrap = document.getElementById('footer-sample-wrap');
128
+
try {
129
+
const res = await fetch('/atp/session', { method: 'GET' });
130
+
if (res.ok && res.status !== 204) {
131
+
const s = await res.json();
132
+
show(s?.handle);
133
+
} else {
134
+
hide();
135
+
}
136
+
} catch { hide(); }
137
+
138
+
window.addEventListener('atp-login', (e) => {
139
+
try { show(e?.detail?.session?.handle || ''); if (sampleWrap) sampleWrap.style.display = 'inline'; } catch {}
140
+
});
141
+
window.addEventListener('atp-logout', () => { hide(); if (sampleWrap) sampleWrap.style.display = 'none'; });
142
+
143
+
if (!('___tapHeaderLogoutBound' in window)) {
144
+
(window).___tapHeaderLogoutBound = true;
145
+
btn.addEventListener('click', async () => {
146
+
try { await fetch('/atp/session', { method: 'DELETE' }); } catch {}
147
+
window.dispatchEvent(new CustomEvent('atp-logout'));
148
+
});
149
+
}
150
+
})();
151
+
</script>
152
+
</body>
153
+
</html>
154
+
155
+