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

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
···
+
[submodule "cli/jacquard"]
+
path = cli/jacquard
+
url = https://tangled.org/@nonbinary.computer/jacquard
+52
.tangled/workflows/deploy-wisp.yml
···
+
# 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 "<nixpkgs>" -A openssl.dev)"
+
export OPENSSL_NO_VENDOR=1
+
export OPENSSL_LIB_DIR="$(nix-build --no-out-link "<nixpkgs>" -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 }}'
+36 -22
public/styles/global.css
···
@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);
···
@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;
+
}
+1 -1
src/index.ts
···
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'
}
+49 -20
src/lib/db.ts
···
}
};
-
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;
···
}
};
-
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({
+30 -4
src/lib/oauth-client.ts
···
}
};
-
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`,
···
}
};
-
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({
+2 -2
src/lib/types.ts
···
* @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
};
+40
testDeploy/index.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
<title>Wisp.place Test Site</title>
+
<style>
+
body {
+
font-family: system-ui, -apple-system, sans-serif;
+
max-width: 800px;
+
margin: 4rem auto;
+
padding: 0 2rem;
+
line-height: 1.6;
+
}
+
h1 {
+
color: #333;
+
}
+
.info {
+
background: #f0f0f0;
+
padding: 1rem;
+
border-radius: 8px;
+
margin: 2rem 0;
+
}
+
</style>
+
</head>
+
<body>
+
<h1>Hello from Wisp.place!</h1>
+
<p>This is a test deployment using the wisp-cli and Tangled Spindles CI/CD.</p>
+
+
<div class="info">
+
<h2>About this deployment</h2>
+
<p>This site was deployed to the AT Protocol using:</p>
+
<ul>
+
<li>Wisp.place CLI (Rust)</li>
+
<li>Tangled Spindles CI/CD</li>
+
<li>AT Protocol for decentralized hosting</li>
+
</ul>
+
</div>
+
</body>
+
</html>