tangled
alpha
login
or
join now
xan.lol
/
wisp.place-monorepo
forked from
nekomimi.pet/wisp.place-monorepo
0
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
0
fork
atom
overview
issues
pulls
pipelines
landing page redesign
nekomimi.pet
2 months ago
79ca02c6
61216fc9
+265
-35
3 changed files
expand all
collapse all
unified
split
apps
main-app
public
index.html
src
index.ts
routes
auth.ts
-35
apps/main-app/public/index.html
···
1
1
-
<!doctype html>
2
2
-
<html lang="en">
3
3
-
<head>
4
4
-
<meta charset="UTF-8" />
5
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
-
<title>wisp.place</title>
7
7
-
<meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." />
8
8
-
9
9
-
<!-- Open Graph / Facebook -->
10
10
-
<meta property="og:type" content="website" />
11
11
-
<meta property="og:url" content="https://wisp.place/" />
12
12
-
<meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" />
13
13
-
<meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." />
14
14
-
<meta property="og:site_name" content="wisp.place" />
15
15
-
16
16
-
<!-- Twitter -->
17
17
-
<meta name="twitter:card" content="summary_large_image" />
18
18
-
<meta name="twitter:url" content="https://wisp.place/" />
19
19
-
<meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" />
20
20
-
<meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." />
21
21
-
22
22
-
<!-- Theme -->
23
23
-
<meta name="theme-color" content="#7c3aed" />
24
24
-
25
25
-
<link rel="icon" type="image/x-icon" href="./favicon.ico">
26
26
-
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
27
27
-
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">
28
28
-
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
29
29
-
<link rel="manifest" href="./site.webmanifest">
30
30
-
</head>
31
31
-
<body>
32
32
-
<div id="elysia"></div>
33
33
-
<script type="module" src="./index.tsx"></script>
34
34
-
</body>
35
35
-
</html>
+236
apps/main-app/src/index.ts
···
121
121
})
122
122
.onError(observabilityMiddleware('main-app').onError)
123
123
.use(csrfProtection())
124
124
+
.get('/', ({ set }) => {
125
125
+
// Build dynamic login URL for AT Protocol OAuth entryway
126
126
+
// atproto.wisp.place will redirect to this endpoint with the saved handle
127
127
+
const isLocalDev = Bun.env.LOCAL_DEV === 'true'
128
128
+
const loginUrl = isLocalDev
129
129
+
? 'http://127.0.0.1:8000/api/auth/login'
130
130
+
: `${config.domain}/api/auth/login`
131
131
+
const atprotoLoginUrl = `https://atproto.wisp.place/?next=${encodeURIComponent(loginUrl)}`
132
132
+
133
133
+
set.headers['Content-Type'] = 'text/html; charset=utf-8'
134
134
+
135
135
+
return `<!doctype html>
136
136
+
<html lang="en">
137
137
+
<head>
138
138
+
<meta charset="UTF-8" />
139
139
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
140
140
+
<title>wisp.place</title>
141
141
+
<meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." />
142
142
+
143
143
+
<!-- Open Graph / Facebook -->
144
144
+
<meta property="og:type" content="website" />
145
145
+
<meta property="og:url" content="https://wisp.place/" />
146
146
+
<meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" />
147
147
+
<meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." />
148
148
+
<meta property="og:site_name" content="wisp.place" />
149
149
+
150
150
+
<!-- Twitter -->
151
151
+
<meta name="twitter:card" content="summary_large_image" />
152
152
+
<meta name="twitter:url" content="https://wisp.place/" />
153
153
+
<meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" />
154
154
+
<meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." />
155
155
+
156
156
+
<!-- Theme -->
157
157
+
<meta name="theme-color" content="#7c3aed" />
158
158
+
159
159
+
<link rel="icon" type="image/x-icon" href="./favicon.ico">
160
160
+
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
161
161
+
<link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">
162
162
+
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
163
163
+
<link rel="manifest" href="./site.webmanifest">
164
164
+
165
165
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
166
166
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
167
167
+
<link
168
168
+
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap"
169
169
+
rel="stylesheet"
170
170
+
/>
171
171
+
<style>
172
172
+
* {
173
173
+
margin: 0;
174
174
+
padding: 0;
175
175
+
box-sizing: border-box;
176
176
+
}
177
177
+
178
178
+
:root {
179
179
+
--bg: #ffffff;
180
180
+
--text: #1a1a1a;
181
181
+
--text-muted: #666;
182
182
+
--link: #0066cc;
183
183
+
--link-hover: #0052a3;
184
184
+
--terminal-bg: #1a1a1a;
185
185
+
--terminal-text: #e0e0e0;
186
186
+
--terminal-cyan: #5fdfdf;
187
187
+
}
188
188
+
189
189
+
@media (prefers-color-scheme: dark) {
190
190
+
:root {
191
191
+
--bg: #121212;
192
192
+
--text: #e0e0e0;
193
193
+
--text-muted: #888;
194
194
+
--link: #5fdfdf;
195
195
+
--link-hover: #7fffff;
196
196
+
--terminal-bg: #0a0a0a;
197
197
+
--terminal-text: #e0e0e0;
198
198
+
}
199
199
+
}
200
200
+
201
201
+
body {
202
202
+
font-family: "Fira Mono", monospace;
203
203
+
font-weight: 400;
204
204
+
font-style: normal;
205
205
+
font-size: 18px;
206
206
+
line-height: 1.6;
207
207
+
padding: 60px 40px;
208
208
+
max-width: 80%;
209
209
+
color: var(--text);
210
210
+
background: var(--bg);
211
211
+
transition:
212
212
+
background 0.2s,
213
213
+
color 0.2s;
214
214
+
}
215
215
+
216
216
+
h1 {
217
217
+
font-size: 1.1em;
218
218
+
font-weight: normal;
219
219
+
margin-bottom: 2em;
220
220
+
}
221
221
+
222
222
+
.cursor {
223
223
+
display: inline-block;
224
224
+
width: 2px;
225
225
+
height: 1.1em;
226
226
+
background: var(--text);
227
227
+
margin-left: 2px;
228
228
+
vertical-align: text-bottom;
229
229
+
animation: blink 1s step-end infinite;
230
230
+
}
231
231
+
232
232
+
@keyframes blink {
233
233
+
0%,
234
234
+
100% {
235
235
+
opacity: 1;
236
236
+
}
237
237
+
50% {
238
238
+
opacity: 0;
239
239
+
}
240
240
+
}
241
241
+
242
242
+
p {
243
243
+
margin-bottom: 0.3em;
244
244
+
}
245
245
+
246
246
+
section {
247
247
+
margin-bottom: 2.5em;
248
248
+
}
249
249
+
250
250
+
a {
251
251
+
color: var(--link);
252
252
+
text-decoration: underline;
253
253
+
text-underline-offset: 2px;
254
254
+
}
255
255
+
256
256
+
a:hover {
257
257
+
color: var(--link-hover);
258
258
+
}
259
259
+
260
260
+
.click-hint {
261
261
+
color: var(--link);
262
262
+
margin-left: 0.5em;
263
263
+
display: inline-flex;
264
264
+
align-items: center;
265
265
+
}
266
266
+
267
267
+
.click-hint .arrow {
268
268
+
display: inline-block;
269
269
+
width: 1.2em;
270
270
+
text-align: center;
271
271
+
animation: nudge 1.2s ease-in-out infinite;
272
272
+
}
273
273
+
274
274
+
@keyframes nudge {
275
275
+
0%,
276
276
+
100% {
277
277
+
transform: translateX(0);
278
278
+
}
279
279
+
50% {
280
280
+
transform: translateX(-4px);
281
281
+
}
282
282
+
}
283
283
+
284
284
+
.terminal-section {
285
285
+
margin-top: 2em;
286
286
+
}
287
287
+
288
288
+
.terminal-label {
289
289
+
margin-bottom: 0.8em;
290
290
+
}
291
291
+
292
292
+
.cmd {
293
293
+
font-family:
294
294
+
ui-monospace, "SF Mono", "Cascadia Code", "Source Code Pro",
295
295
+
Menlo, Consolas, monospace;
296
296
+
font-size: 0.85em;
297
297
+
background: var(--terminal-bg);
298
298
+
color: var(--terminal-text);
299
299
+
border-radius: 4px;
300
300
+
padding: 12px 16px;
301
301
+
display: table;
302
302
+
white-space: nowrap;
303
303
+
margin-bottom: 0.5em;
304
304
+
}
305
305
+
306
306
+
.cmd .highlight {
307
307
+
color: var(--terminal-cyan);
308
308
+
}
309
309
+
310
310
+
.hosting-options {
311
311
+
margin-top: 2.5em;
312
312
+
}
313
313
+
314
314
+
.hosting-options p {
315
315
+
margin-bottom: 0.2em;
316
316
+
}
317
317
+
</style>
318
318
+
</head>
319
319
+
<body>
320
320
+
<h1>wisp.place<span class="cursor"></span></h1>
321
321
+
322
322
+
<section>
323
323
+
<p>the easiest way to get static html going</p>
324
324
+
<p>
325
325
+
just drag n' drop into the dashboard with your
326
326
+
<a href="${atprotoLoginUrl}">AT Protocol account</a>.
327
327
+
<span class="click-hint"
328
328
+
><span class="arrow">←</span> click me!</span
329
329
+
>
330
330
+
</p>
331
331
+
</section>
332
332
+
333
333
+
<section class="terminal-section">
334
334
+
<p class="terminal-label">are you a terminal nerd?</p>
335
335
+
<code class="cmd"
336
336
+
>curl
337
337
+
<span class="highlight"
338
338
+
>https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux</span
339
339
+
>
340
340
+
-o wisp-cli</code
341
341
+
>
342
342
+
<code class="cmd"
343
343
+
>wisp-cli
344
344
+
<span class="highlight">alice.bsky.social</span> --site
345
345
+
MyBlog</code
346
346
+
>
347
347
+
</section>
348
348
+
349
349
+
<div class="hosting-options">
350
350
+
<p>host on our infrastructure for free</p>
351
351
+
<p>
352
352
+
or use wisp-cli to host on your own infra with seamless
353
353
+
deployments
354
354
+
</p>
355
355
+
<p>need docs? <a href="https://docs.wisp.place">docs.wisp.place</a></p>
356
356
+
</div>
357
357
+
</body>
358
358
+
</html>`
359
359
+
})
124
360
.use(authRoutes(client, cookieSecret))
125
361
.use(wispRoutes(client, cookieSecret))
126
362
.use(domainRoutes(client, cookieSecret))
+29
apps/main-app/src/routes/auth.ts
···
13
13
sign: ['did']
14
14
}
15
15
})
16
16
+
.get('/api/auth/login', async (c) => {
17
17
+
// GET endpoint for initiating OAuth via atproto.wisp.place entryway
18
18
+
// Accepts: login_hint (handle) or pds (server)
19
19
+
try {
20
20
+
const query = c.query as { login_hint?: string; pds?: string }
21
21
+
const handle = query.login_hint || ''
22
22
+
const pds = query.pds || ''
23
23
+
24
24
+
// Use login_hint if provided, otherwise use PDS URL
25
25
+
const identifier = handle || (pds ? `https://${pds}` : '')
26
26
+
27
27
+
if (!identifier) {
28
28
+
logger.error('Login attempt with no login_hint or pds')
29
29
+
return c.redirect('/?error=missing_handle')
30
30
+
}
31
31
+
32
32
+
logger.info('Login attempt via entryway', { identifier })
33
33
+
const state = crypto.randomUUID()
34
34
+
const url = await client.authorize(identifier, { state })
35
35
+
logger.info('Authorization URL generated', { identifier })
36
36
+
37
37
+
// Redirect to the OAuth authorization URL
38
38
+
return c.redirect(url.toString())
39
39
+
} catch (err) {
40
40
+
logger.error('Login error', err)
41
41
+
console.error('[Auth] Full error:', err)
42
42
+
return c.redirect('/?error=auth_failed')
43
43
+
}
44
44
+
})
16
45
.post('/api/auth/signin', async (c) => {
17
46
let handle = 'unknown'
18
47
try {