+3
.gitmodules
+3
.gitmodules
+52
.tangled/workflows/deploy-wisp.yml
+52
.tangled/workflows/deploy-wisp.yml
···
1
+
# Deploy to Wisp.place
2
+
# This workflow builds your site and deploys it to Wisp.place using the wisp-cli
3
+
when:
4
+
- event: ['push']
5
+
branch: ['main']
6
+
- event: ['manual']
7
+
engine: 'nixery'
8
+
clone:
9
+
skip: false
10
+
depth: 1
11
+
submodules: true
12
+
dependencies:
13
+
nixpkgs:
14
+
- git
15
+
- gcc
16
+
github:NixOS/nixpkgs/nixpkgs-unstable:
17
+
- rustc
18
+
- cargo
19
+
environment:
20
+
# Customize these for your project
21
+
SITE_PATH: 'testDeploy'
22
+
SITE_NAME: 'wispPlaceDocs'
23
+
steps:
24
+
- name: 'Initialize submodules'
25
+
command: |
26
+
git submodule update --init --recursive
27
+
28
+
- name: 'Build wisp-cli'
29
+
command: |
30
+
cd cli
31
+
export PATH="$HOME/.nix-profile/bin:$PATH"
32
+
nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
33
+
nix-channel --update
34
+
nix-shell -p pkg-config openssl --run '
35
+
export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)"
36
+
export OPENSSL_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.dev)"
37
+
export OPENSSL_NO_VENDOR=1
38
+
export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -A openssl.out)/lib"
39
+
cargo build --release
40
+
'
41
+
cd ..
42
+
43
+
- name: 'Deploy to Wisp.place'
44
+
command: |
45
+
./cli/target/release/wisp-cli \
46
+
"$WISP_HANDLE" \
47
+
--path "$SITE_PATH" \
48
+
--site "$SITE_NAME" \
49
+
--password "$WISP_APP_PASSWORD"
50
+
environment:
51
+
WISP_HANDLE: '${{ secrets.WISP_HANDLE }}'
52
+
WISP_APP_PASSWORD: '${{ secrets.WISP_APP_PASSWORD }}'
+36
-22
public/styles/global.css
+36
-22
public/styles/global.css
···
4
4
@custom-variant dark (&:is(.dark *));
5
5
6
6
:root {
7
-
/* #F2E7C9 - parchment background */
8
-
--background: oklch(0.93 0.03 85);
9
-
/* #413C58 - violet for text */
10
-
--foreground: oklch(0.32 0.04 285);
7
+
/* Warm beige background inspired by Sunset design #E9DDD8 */
8
+
--background: oklch(0.90 0.012 35);
9
+
/* Very dark brown text for strong contrast #2A2420 */
10
+
--foreground: oklch(0.18 0.01 30);
11
11
12
-
--card: oklch(0.98 0.01 85);
13
-
--card-foreground: oklch(0.32 0.04 285);
12
+
/* Slightly lighter card background */
13
+
--card: oklch(0.93 0.01 35);
14
+
--card-foreground: oklch(0.18 0.01 30);
14
15
15
-
--popover: oklch(0.98 0.01 85);
16
-
--popover-foreground: oklch(0.32 0.04 285);
16
+
--popover: oklch(0.93 0.01 35);
17
+
--popover-foreground: oklch(0.18 0.01 30);
17
18
18
-
/* #413C58 - violet primary */
19
-
--primary: oklch(0.32 0.04 285);
20
-
--primary-foreground: oklch(0.98 0.01 85);
19
+
/* Dark brown primary inspired by #645343 */
20
+
--primary: oklch(0.35 0.02 35);
21
+
--primary-foreground: oklch(0.95 0.01 35);
21
22
22
-
/* #FFAAD2 - pink accent */
23
+
/* Bright pink accent for links #FFAAD2 */
23
24
--accent: oklch(0.78 0.15 345);
24
-
--accent-foreground: oklch(0.32 0.04 285);
25
+
--accent-foreground: oklch(0.18 0.01 30);
25
26
26
-
/* #348AA7 - blue secondary */
27
-
--secondary: oklch(0.56 0.08 220);
28
-
--secondary-foreground: oklch(0.98 0.01 85);
27
+
/* Medium taupe secondary inspired by #867D76 */
28
+
--secondary: oklch(0.52 0.015 30);
29
+
--secondary-foreground: oklch(0.95 0.01 35);
29
30
30
-
/* #CCD7C5 - ash muted */
31
-
--muted: oklch(0.85 0.02 130);
32
-
--muted-foreground: oklch(0.45 0.03 285);
31
+
/* Light warm muted background */
32
+
--muted: oklch(0.88 0.01 35);
33
+
--muted-foreground: oklch(0.42 0.015 30);
33
34
34
-
--border: oklch(0.75 0.02 285);
35
-
--input: oklch(0.75 0.02 285);
36
-
--ring: oklch(0.78 0.15 345);
35
+
--border: oklch(0.75 0.015 30);
36
+
--input: oklch(0.92 0.01 35);
37
+
--ring: oklch(0.72 0.08 15);
37
38
38
39
--destructive: oklch(0.577 0.245 27.325);
39
40
--destructive-foreground: oklch(0.985 0 0);
···
150
151
@apply bg-background text-foreground;
151
152
}
152
153
}
154
+
155
+
@keyframes arrow-bounce {
156
+
0%, 100% {
157
+
transform: translateX(0);
158
+
}
159
+
50% {
160
+
transform: translateX(4px);
161
+
}
162
+
}
163
+
164
+
.arrow-animate {
165
+
animation: arrow-bounce 1.5s ease-in-out infinite;
166
+
}
+1
-1
src/index.ts
+1
-1
src/index.ts
···
24
24
import { adminRoutes } from './routes/admin'
25
25
26
26
const config: Config = {
27
-
domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`,
27
+
domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as Config['domain'],
28
28
clientName: Bun.env.CLIENT_NAME ?? 'PDS-View'
29
29
}
30
30
+49
-20
src/lib/db.ts
+49
-20
src/lib/db.ts
···
337
337
}
338
338
};
339
339
340
-
export const createClientMetadata = (config: { domain: `https://${string}`, clientName: string }): ClientMetadata => ({
341
-
client_id: `${config.domain}/client-metadata.json`,
342
-
client_name: config.clientName,
343
-
client_uri: config.domain,
344
-
logo_uri: `${config.domain}/logo.png`,
345
-
tos_uri: `${config.domain}/tos`,
346
-
policy_uri: `${config.domain}/policy`,
347
-
redirect_uris: [`${config.domain}/api/auth/callback`],
348
-
grant_types: ['authorization_code', 'refresh_token'],
349
-
response_types: ['code'],
350
-
application_type: 'web',
351
-
token_endpoint_auth_method: 'private_key_jwt',
352
-
token_endpoint_auth_signing_alg: "ES256",
353
-
scope: "atproto transition:generic",
354
-
dpop_bound_access_tokens: true,
355
-
jwks_uri: `${config.domain}/jwks.json`,
356
-
subject_type: 'public',
357
-
authorization_signed_response_alg: 'ES256'
358
-
});
340
+
export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => {
341
+
const isLocalDev = process.env.LOCAL_DEV === 'true';
342
+
343
+
if (isLocalDev) {
344
+
// Loopback client for local development
345
+
// For loopback, scopes and redirect_uri must be in client_id query string
346
+
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
347
+
const scope = 'atproto transition:generic';
348
+
const params = new URLSearchParams();
349
+
params.append('redirect_uri', redirectUri);
350
+
params.append('scope', scope);
351
+
352
+
return {
353
+
client_id: `http://localhost?${params.toString()}`,
354
+
client_name: config.clientName,
355
+
client_uri: config.domain,
356
+
redirect_uris: [redirectUri],
357
+
grant_types: ['authorization_code', 'refresh_token'],
358
+
response_types: ['code'],
359
+
application_type: 'web',
360
+
token_endpoint_auth_method: 'none',
361
+
scope: scope,
362
+
dpop_bound_access_tokens: false,
363
+
subject_type: 'public'
364
+
};
365
+
}
366
+
367
+
// Production client with private_key_jwt
368
+
return {
369
+
client_id: `${config.domain}/client-metadata.json`,
370
+
client_name: config.clientName,
371
+
client_uri: config.domain,
372
+
logo_uri: `${config.domain}/logo.png`,
373
+
tos_uri: `${config.domain}/tos`,
374
+
policy_uri: `${config.domain}/policy`,
375
+
redirect_uris: [`${config.domain}/api/auth/callback`],
376
+
grant_types: ['authorization_code', 'refresh_token'],
377
+
response_types: ['code'],
378
+
application_type: 'web',
379
+
token_endpoint_auth_method: 'private_key_jwt',
380
+
token_endpoint_auth_signing_alg: "ES256",
381
+
scope: "atproto transition:generic",
382
+
dpop_bound_access_tokens: true,
383
+
jwks_uri: `${config.domain}/jwks.json`,
384
+
subject_type: 'public',
385
+
authorization_signed_response_alg: 'ES256'
386
+
};
387
+
};
359
388
360
389
const persistKey = async (key: JoseKey) => {
361
390
const priv = key.privateJwk;
···
443
472
}
444
473
};
445
474
446
-
export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => {
475
+
export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => {
447
476
const keys = await ensureKeys();
448
477
449
478
return new NodeOAuthClient({
+30
-4
src/lib/oauth-client.ts
+30
-4
src/lib/oauth-client.ts
···
103
103
}
104
104
};
105
105
106
-
export const createClientMetadata = (config: { domain: `https://${string}`, clientName: string }): ClientMetadata => {
107
-
// Use editor.wisp.place for OAuth endpoints since that's where the API routes live
106
+
export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => {
107
+
const isLocalDev = Bun.env.LOCAL_DEV === 'true';
108
+
109
+
if (isLocalDev) {
110
+
// Loopback client for local development
111
+
// For loopback, scopes and redirect_uri must be in client_id query string
112
+
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
113
+
const scope = 'atproto transition:generic';
114
+
const params = new URLSearchParams();
115
+
params.append('redirect_uri', redirectUri);
116
+
params.append('scope', scope);
117
+
118
+
return {
119
+
client_id: `http://localhost?${params.toString()}`,
120
+
client_name: config.clientName,
121
+
client_uri: `https://wisp.place`,
122
+
redirect_uris: [redirectUri],
123
+
grant_types: ['authorization_code', 'refresh_token'],
124
+
response_types: ['code'],
125
+
application_type: 'web',
126
+
token_endpoint_auth_method: 'none',
127
+
scope: scope,
128
+
dpop_bound_access_tokens: false,
129
+
subject_type: 'public'
130
+
};
131
+
}
132
+
133
+
// Production client with private_key_jwt
108
134
return {
109
135
client_id: `${config.domain}/client-metadata.json`,
110
136
client_name: config.clientName,
111
-
client_uri: `https://wisp.place`,
137
+
client_uri: `https://wisp.place`,
112
138
logo_uri: `${config.domain}/logo.png`,
113
139
tos_uri: `${config.domain}/tos`,
114
140
policy_uri: `${config.domain}/policy`,
···
212
238
}
213
239
};
214
240
215
-
export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => {
241
+
export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => {
216
242
const keys = await ensureKeys();
217
243
218
244
return new NodeOAuthClient({
+2
-2
src/lib/types.ts
+2
-2
src/lib/types.ts
···
3
3
* @typeParam Config
4
4
*/
5
5
export type Config = {
6
-
/** The base domain URL with HTTPS protocol */
7
-
domain: `https://${string}`,
6
+
/** The base domain URL with HTTP or HTTPS protocol */
7
+
domain: `http://${string}` | `https://${string}`,
8
8
/** Name of the client application */
9
9
clientName: string
10
10
};
+40
testDeploy/index.html
+40
testDeploy/index.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.0">
6
+
<title>Wisp.place Test Site</title>
7
+
<style>
8
+
body {
9
+
font-family: system-ui, -apple-system, sans-serif;
10
+
max-width: 800px;
11
+
margin: 4rem auto;
12
+
padding: 0 2rem;
13
+
line-height: 1.6;
14
+
}
15
+
h1 {
16
+
color: #333;
17
+
}
18
+
.info {
19
+
background: #f0f0f0;
20
+
padding: 1rem;
21
+
border-radius: 8px;
22
+
margin: 2rem 0;
23
+
}
24
+
</style>
25
+
</head>
26
+
<body>
27
+
<h1>Hello from Wisp.place!</h1>
28
+
<p>This is a test deployment using the wisp-cli and Tangled Spindles CI/CD.</p>
29
+
30
+
<div class="info">
31
+
<h2>About this deployment</h2>
32
+
<p>This site was deployed to the AT Protocol using:</p>
33
+
<ul>
34
+
<li>Wisp.place CLI (Rust)</li>
35
+
<li>Tangled Spindles CI/CD</li>
36
+
<li>AT Protocol for decentralized hosting</li>
37
+
</ul>
38
+
</div>
39
+
</body>
40
+
</html>