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
···
···
+
[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);
-
--card: oklch(0.98 0.01 85);
-
--card-foreground: oklch(0.32 0.04 285);
-
--popover: oklch(0.98 0.01 85);
-
--popover-foreground: oklch(0.32 0.04 285);
-
/* #413C58 - violet primary */
-
--primary: oklch(0.32 0.04 285);
-
--primary-foreground: oklch(0.98 0.01 85);
-
/* #FFAAD2 - pink accent */
--accent: oklch(0.78 0.15 345);
-
--accent-foreground: oklch(0.32 0.04 285);
-
/* #348AA7 - blue secondary */
-
--secondary: oklch(0.56 0.08 220);
-
--secondary-foreground: oklch(0.98 0.01 85);
-
/* #CCD7C5 - ash muted */
-
--muted: oklch(0.85 0.02 130);
-
--muted-foreground: oklch(0.45 0.03 285);
-
--border: oklch(0.75 0.02 285);
-
--input: oklch(0.75 0.02 285);
-
--ring: oklch(0.78 0.15 345);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
···
@apply bg-background text-foreground;
}
}
···
@custom-variant dark (&:is(.dark *));
:root {
+
/* 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);
+
/* Slightly lighter card background */
+
--card: oklch(0.93 0.01 35);
+
--card-foreground: oklch(0.18 0.01 30);
+
--popover: oklch(0.93 0.01 35);
+
--popover-foreground: oklch(0.18 0.01 30);
+
/* Dark brown primary inspired by #645343 */
+
--primary: oklch(0.35 0.02 35);
+
--primary-foreground: oklch(0.95 0.01 35);
+
/* Bright pink accent for links #FFAAD2 */
--accent: oklch(0.78 0.15 345);
+
--accent-foreground: oklch(0.18 0.01 30);
+
/* Medium taupe secondary inspired by #867D76 */
+
--secondary: oklch(0.52 0.015 30);
+
--secondary-foreground: oklch(0.95 0.01 35);
+
/* Light warm muted background */
+
--muted: oklch(0.88 0.01 35);
+
--muted-foreground: oklch(0.42 0.015 30);
+
--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}`,
clientName: Bun.env.CLIENT_NAME ?? 'PDS-View'
}
···
import { adminRoutes } from './routes/admin'
const config: Config = {
+
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'
-
});
const persistKey = async (key: JoseKey) => {
const priv = key.privateJwk;
···
}
};
-
export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => {
const keys = await ensureKeys();
return new NodeOAuthClient({
···
}
};
+
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: `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
return {
client_id: `${config.domain}/client-metadata.json`,
client_name: config.clientName,
-
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 }) => {
const keys = await ensureKeys();
return new NodeOAuthClient({
···
}
};
+
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`,
logo_uri: `${config.domain}/logo.png`,
tos_uri: `${config.domain}/tos`,
policy_uri: `${config.domain}/policy`,
···
}
};
+
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}`,
/** Name of the client application */
clientName: string
};
···
* @typeParam Config
*/
export type Config = {
+
/** 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>