Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

add deploy wisp spindle, add jacquard submodule for local builds (for now till jacquard 0.9) add local oauth loopback for dev

color rework

pull submodules

init submodules

rustup pls work

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

meow

Changed files
+213 -49
.tangled
workflows
public
styles
src
testDeploy
+3
.gitmodules
··· 1 + [submodule "cli/jacquard"] 2 + path = cli/jacquard 3 + url = https://tangled.org/@nonbinary.computer/jacquard
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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>