diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..784460f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cli/jacquard"] + path = cli/jacquard + url = https://tangled.org/@nonbinary.computer/jacquard diff --git a/.tangled/workflows/deploy-wisp.yml b/.tangled/workflows/deploy-wisp.yml new file mode 100644 index 0000000..f77c379 --- /dev/null +++ b/.tangled/workflows/deploy-wisp.yml @@ -0,0 +1,52 @@ +# Deploy to Wisp.place +# This workflow builds your site and deploys it to Wisp.place using the wisp-cli +when: + - event: ['push'] + branch: ['main'] + - event: ['manual'] +engine: 'nixery' +clone: + skip: false + depth: 1 + submodules: true +dependencies: + nixpkgs: + - git + - gcc + github:NixOS/nixpkgs/nixpkgs-unstable: + - rustc + - cargo +environment: + # Customize these for your project + SITE_PATH: 'testDeploy' + SITE_NAME: 'wispPlaceDocs' +steps: + - name: 'Initialize submodules' + command: | + git submodule update --init --recursive + + - name: 'Build wisp-cli' + command: | + cd cli + export PATH="$HOME/.nix-profile/bin:$PATH" + nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs + nix-channel --update + nix-shell -p pkg-config openssl --run ' + export PKG_CONFIG_PATH="$(pkg-config --variable pc_path pkg-config)" + export OPENSSL_DIR="$(nix-build --no-out-link "" -A openssl.dev)" + export OPENSSL_NO_VENDOR=1 + export OPENSSL_LIB_DIR="$(nix-build --no-out-link "" -A openssl.out)/lib" + cargo build --release + ' + cd .. + + - name: 'Deploy to Wisp.place' + command: | + ./cli/target/release/wisp-cli \ + "$WISP_HANDLE" \ + --path "$SITE_PATH" \ + --site "$SITE_NAME" \ + --password "$WISP_APP_PASSWORD" + environment: + WISP_HANDLE: '${{ secrets.WISP_HANDLE }}' + WISP_APP_PASSWORD: '${{ secrets.WISP_APP_PASSWORD }}' diff --git a/cli/jacquard b/cli/jacquard new file mode 160000 index 0000000..d533482 --- /dev/null +++ b/cli/jacquard @@ -0,0 +1 @@ +Subproject commit d533482a61f540586b1eea620b8e9a01a59d5650 diff --git a/public/styles/global.css b/public/styles/global.css index 5a01356..a0e817d 100644 --- a/public/styles/global.css +++ b/public/styles/global.css @@ -4,36 +4,37 @@ @custom-variant dark (&:is(.dark *)); :root { - /* #F2E7C9 - parchment background */ - --background: oklch(0.93 0.03 85); - /* #413C58 - violet for text */ - --foreground: oklch(0.32 0.04 285); + /* Warm beige background inspired by Sunset design #E9DDD8 */ + --background: oklch(0.90 0.012 35); + /* Very dark brown text for strong contrast #2A2420 */ + --foreground: oklch(0.18 0.01 30); - --card: oklch(0.98 0.01 85); - --card-foreground: oklch(0.32 0.04 285); + /* Slightly lighter card background */ + --card: oklch(0.93 0.01 35); + --card-foreground: oklch(0.18 0.01 30); - --popover: oklch(0.98 0.01 85); - --popover-foreground: oklch(0.32 0.04 285); + --popover: oklch(0.93 0.01 35); + --popover-foreground: oklch(0.18 0.01 30); - /* #413C58 - violet primary */ - --primary: oklch(0.32 0.04 285); - --primary-foreground: oklch(0.98 0.01 85); + /* Dark brown primary inspired by #645343 */ + --primary: oklch(0.35 0.02 35); + --primary-foreground: oklch(0.95 0.01 35); - /* #FFAAD2 - pink accent */ + /* Bright pink accent for links #FFAAD2 */ --accent: oklch(0.78 0.15 345); - --accent-foreground: oklch(0.32 0.04 285); + --accent-foreground: oklch(0.18 0.01 30); - /* #348AA7 - blue secondary */ - --secondary: oklch(0.56 0.08 220); - --secondary-foreground: oklch(0.98 0.01 85); + /* Medium taupe secondary inspired by #867D76 */ + --secondary: oklch(0.52 0.015 30); + --secondary-foreground: oklch(0.95 0.01 35); - /* #CCD7C5 - ash muted */ - --muted: oklch(0.85 0.02 130); - --muted-foreground: oklch(0.45 0.03 285); + /* Light warm muted background */ + --muted: oklch(0.88 0.01 35); + --muted-foreground: oklch(0.42 0.015 30); - --border: oklch(0.75 0.02 285); - --input: oklch(0.75 0.02 285); - --ring: oklch(0.78 0.15 345); + --border: oklch(0.75 0.015 30); + --input: oklch(0.92 0.01 35); + --ring: oklch(0.72 0.08 15); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.985 0 0); @@ -150,3 +151,16 @@ @apply bg-background text-foreground; } } + +@keyframes arrow-bounce { + 0%, 100% { + transform: translateX(0); + } + 50% { + transform: translateX(4px); + } +} + +.arrow-animate { + animation: arrow-bounce 1.5s ease-in-out infinite; +} diff --git a/src/index.ts b/src/index.ts index 40201c5..ba58063 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ import { promptAdminSetup } from './lib/admin-auth' import { adminRoutes } from './routes/admin' const config: Config = { - domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`, + domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as Config['domain'], clientName: Bun.env.CLIENT_NAME ?? 'PDS-View' } diff --git a/src/lib/db.ts b/src/lib/db.ts index f13664e..f86fa4c 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -337,25 +337,54 @@ export const cleanupExpiredSessions = async () => { } }; -export const createClientMetadata = (config: { domain: `https://${string}`, clientName: string }): ClientMetadata => ({ - client_id: `${config.domain}/client-metadata.json`, - client_name: config.clientName, - client_uri: config.domain, - logo_uri: `${config.domain}/logo.png`, - tos_uri: `${config.domain}/tos`, - policy_uri: `${config.domain}/policy`, - redirect_uris: [`${config.domain}/api/auth/callback`], - grant_types: ['authorization_code', 'refresh_token'], - response_types: ['code'], - application_type: 'web', - token_endpoint_auth_method: 'private_key_jwt', - token_endpoint_auth_signing_alg: "ES256", - scope: "atproto transition:generic", - dpop_bound_access_tokens: true, - jwks_uri: `${config.domain}/jwks.json`, - subject_type: 'public', - authorization_signed_response_alg: 'ES256' -}); +export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => { + const isLocalDev = process.env.LOCAL_DEV === 'true'; + + if (isLocalDev) { + // Loopback client for local development + // For loopback, scopes and redirect_uri must be in client_id query string + const redirectUri = 'http://127.0.0.1:8000/api/auth/callback'; + const scope = 'atproto transition:generic'; + const params = new URLSearchParams(); + params.append('redirect_uri', redirectUri); + params.append('scope', scope); + + return { + client_id: `http://localhost?${params.toString()}`, + client_name: config.clientName, + client_uri: config.domain, + redirect_uris: [redirectUri], + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + application_type: 'web', + token_endpoint_auth_method: 'none', + scope: scope, + dpop_bound_access_tokens: false, + subject_type: 'public' + }; + } + + // Production client with private_key_jwt + return { + client_id: `${config.domain}/client-metadata.json`, + client_name: config.clientName, + client_uri: config.domain, + logo_uri: `${config.domain}/logo.png`, + tos_uri: `${config.domain}/tos`, + policy_uri: `${config.domain}/policy`, + redirect_uris: [`${config.domain}/api/auth/callback`], + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + application_type: 'web', + token_endpoint_auth_method: 'private_key_jwt', + token_endpoint_auth_signing_alg: "ES256", + scope: "atproto transition:generic", + dpop_bound_access_tokens: true, + jwks_uri: `${config.domain}/jwks.json`, + subject_type: 'public', + authorization_signed_response_alg: 'ES256' + }; +}; const persistKey = async (key: JoseKey) => { const priv = key.privateJwk; @@ -443,7 +472,7 @@ export const rotateKeysIfNeeded = async (): Promise => { } }; -export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => { +export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => { const keys = await ensureKeys(); return new NodeOAuthClient({ diff --git a/src/lib/oauth-client.ts b/src/lib/oauth-client.ts index da0a1dc..a087c80 100644 --- a/src/lib/oauth-client.ts +++ b/src/lib/oauth-client.ts @@ -103,12 +103,38 @@ export const cleanupExpiredSessions = async () => { } }; -export const createClientMetadata = (config: { domain: `https://${string}`, clientName: string }): ClientMetadata => { - // Use editor.wisp.place for OAuth endpoints since that's where the API routes live +export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => { + const isLocalDev = Bun.env.LOCAL_DEV === 'true'; + + if (isLocalDev) { + // Loopback client for local development + // For loopback, scopes and redirect_uri must be in client_id query string + const redirectUri = 'http://127.0.0.1:8000/api/auth/callback'; + const scope = 'atproto transition:generic'; + const params = new URLSearchParams(); + params.append('redirect_uri', redirectUri); + params.append('scope', scope); + + return { + client_id: `http://localhost?${params.toString()}`, + client_name: config.clientName, + client_uri: `https://wisp.place`, + redirect_uris: [redirectUri], + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + application_type: 'web', + token_endpoint_auth_method: 'none', + scope: scope, + dpop_bound_access_tokens: false, + subject_type: 'public' + }; + } + + // Production client with private_key_jwt return { client_id: `${config.domain}/client-metadata.json`, client_name: config.clientName, - client_uri: `https://wisp.place`, + client_uri: `https://wisp.place`, logo_uri: `${config.domain}/logo.png`, tos_uri: `${config.domain}/tos`, policy_uri: `${config.domain}/policy`, @@ -212,7 +238,7 @@ export const rotateKeysIfNeeded = async (): Promise => { } }; -export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => { +export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => { const keys = await ensureKeys(); return new NodeOAuthClient({ diff --git a/src/lib/types.ts b/src/lib/types.ts index c37918c..4453a26 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,8 +3,8 @@ * @typeParam Config */ export type Config = { - /** The base domain URL with HTTPS protocol */ - domain: `https://${string}`, + /** The base domain URL with HTTP or HTTPS protocol */ + domain: `http://${string}` | `https://${string}`, /** Name of the client application */ clientName: string }; diff --git a/testDeploy/index.html b/testDeploy/index.html new file mode 100644 index 0000000..181af0c --- /dev/null +++ b/testDeploy/index.html @@ -0,0 +1,40 @@ + + + + + + Wisp.place Test Site + + + +

Hello from Wisp.place!

+

This is a test deployment using the wisp-cli and Tangled Spindles CI/CD.

+ +
+

About this deployment

+

This site was deployed to the AT Protocol using:

+
    +
  • Wisp.place CLI (Rust)
  • +
  • Tangled Spindles CI/CD
  • +
  • AT Protocol for decentralized hosting
  • +
+
+ +