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

Configure Feed

Select the types of activity you want to include in your feed.

the dockerfile mines got hands

+742 -74
+9 -36
Dockerfile
··· 18 18 # Copy source files 19 19 COPY apps/main-app ./apps/main-app 20 20 21 + # Build frontend (CSS + JS bundles) 22 + RUN cd apps/main-app && bun run build 23 + 21 24 # Build compiled server 22 25 RUN bun build \ 23 26 --compile \ ··· 26 29 --outfile server \ 27 30 apps/main-app/src/index.ts 28 31 29 - # Production dependencies stage 30 - FROM oven/bun:1.3 AS prod-deps 31 - 32 - WORKDIR /app 33 - 34 - COPY package.json bunfig.toml tsconfig.json bun.lock* ./ 35 - COPY packages ./packages 36 - COPY apps/main-app/package.json ./apps/main-app/package.json 37 - COPY apps/hosting-service/package.json ./apps/hosting-service/package.json 38 - COPY cli/ ./cli 39 - 40 - # Install only production dependencies 41 - RUN bun install --frozen-lockfile --production 42 - 43 - # Remove unnecessary large packages (bun is already in base image, these are dev tools) 44 - RUN rm -rf /app/node_modules/bun \ 45 - /app/node_modules/@oven \ 46 - /app/node_modules/prettier \ 47 - /app/node_modules/@ts-morph 48 - 49 - # Final stage - use distroless or slim debian-based image 32 + # Final stage - slim image with just the compiled binary 50 33 FROM debian:bookworm-slim 51 34 52 - # Install Bun runtime 53 - COPY --from=oven/bun:1.3 /usr/local/bin/bun /usr/local/bin/bun 54 - 55 35 WORKDIR /app 56 36 57 - # Copy compiled server 37 + # Copy compiled server (standalone binary - no node_modules needed) 58 38 COPY --from=build /app/server /app/server 59 39 60 - # Copy public files 40 + # Copy public static assets 61 41 COPY apps/main-app/public apps/main-app/public 62 42 63 - # Copy production dependencies only 64 - COPY --from=prod-deps /app/node_modules /app/node_modules 43 + # Copy built frontend assets 44 + COPY --from=build /app/apps/main-app/dist apps/main-app/dist 65 45 66 - # Copy configs 67 - COPY package.json bunfig.toml tsconfig.json /app/ 68 - COPY apps/main-app/tsconfig.json /app/apps/main-app/tsconfig.json 69 - COPY apps/main-app/package.json /app/apps/main-app/package.json 70 - 71 - # Create symlink for module resolution 72 - RUN ln -s /app/node_modules /app/apps/main-app/node_modules 73 - 46 + ENV NODE_ENV=production 74 47 ENV PORT=8000 75 48 76 49 EXPOSE 8000
+1
apps/hosting-service/Dockerfile
··· 13 13 COPY packages ./packages 14 14 COPY apps/hosting-service ./apps/hosting-service 15 15 COPY apps/main-app/package.json ./apps/main-app/package.json 16 + COPY cli/package.json ./cli/package.json 16 17 17 18 # Install dependencies 18 19 RUN bun install --frozen-lockfile
+120
apps/main-app/build.ts
··· 1 + #!/usr/bin/env bun 2 + 3 + import { rm, mkdir } from 'fs/promises' 4 + import { existsSync } from 'fs' 5 + import path from 'path' 6 + 7 + console.log('🔨 Building main-app frontend...') 8 + 9 + // Get the directory where this script is located 10 + const scriptDir = import.meta.dir 11 + 12 + const distDir = `${scriptDir}/dist` 13 + const publicDir = `${scriptDir}/public` 14 + 15 + // Clean dist directory 16 + if (existsSync(distDir)) { 17 + await rm(distDir, { recursive: true }) 18 + } 19 + await mkdir(distDir, { recursive: true }) 20 + await mkdir(`${distDir}/editor`, { recursive: true }) 21 + 22 + // Build the editor React app 23 + const editorResult = await Bun.build({ 24 + entrypoints: [`${publicDir}/editor/editor.tsx`], 25 + outdir: `${distDir}/editor`, 26 + target: 'browser', 27 + format: 'esm', 28 + minify: true, 29 + sourcemap: 'none', 30 + splitting: true, 31 + naming: { 32 + entry: '[name].[hash].js', 33 + chunk: '[name].[hash].js', 34 + asset: '[name].[hash][ext]' 35 + } 36 + }) 37 + 38 + if (!editorResult.success) { 39 + console.error('❌ Editor build failed:') 40 + for (const log of editorResult.logs) { 41 + console.error(log) 42 + } 43 + process.exit(1) 44 + } 45 + 46 + // Find the main entry bundle 47 + const editorBundle = editorResult.outputs.find(o => o.path.includes('editor.') && o.path.endsWith('.js')) 48 + 49 + if (!editorBundle) { 50 + console.error('❌ Could not find editor bundle in outputs') 51 + process.exit(1) 52 + } 53 + 54 + const editorBundleName = path.basename(editorBundle.path) 55 + 56 + // Generate the production HTML 57 + const htmlContent = `<!doctype html> 58 + <html lang="en"> 59 + <head> 60 + <meta charset="UTF-8" /> 61 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 62 + <title>wisp.place</title> 63 + <meta name="description" content="Manage your decentralized static sites hosted on AT Protocol." /> 64 + 65 + <!-- Open Graph / Facebook --> 66 + <meta property="og:type" content="website" /> 67 + <meta property="og:url" content="https://wisp.place/editor" /> 68 + <meta property="og:title" content="Editor - wisp.place" /> 69 + <meta property="og:description" content="Manage your decentralized static sites hosted on AT Protocol." /> 70 + <meta property="og:site_name" content="wisp.place" /> 71 + 72 + <!-- Twitter --> 73 + <meta name="twitter:card" content="summary" /> 74 + <meta name="twitter:url" content="https://wisp.place/editor" /> 75 + <meta name="twitter:title" content="Editor - wisp.place" /> 76 + <meta name="twitter:description" content="Manage your decentralized static sites hosted on AT Protocol." /> 77 + 78 + <!-- Theme --> 79 + <meta name="theme-color" content="#7c3aed" /> 80 + 81 + <link rel="icon" type="image/x-icon" href="/favicon.ico"> 82 + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> 83 + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> 84 + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 85 + <link rel="manifest" href="/site.webmanifest"> 86 + <link rel="stylesheet" href="/dist/styles.css"> 87 + <style> 88 + /* Dark theme fallback styles for before JS loads */ 89 + @media (prefers-color-scheme: dark) { 90 + body { 91 + background-color: oklch(0.23 0.015 285); 92 + color: oklch(0.90 0.005 285); 93 + } 94 + 95 + pre { 96 + background-color: oklch(0.33 0.015 285) !important; 97 + color: oklch(0.90 0.005 285) !important; 98 + } 99 + 100 + .bg-muted { 101 + background-color: oklch(0.33 0.015 285) !important; 102 + } 103 + } 104 + </style> 105 + </head> 106 + <body> 107 + <div id="elysia"></div> 108 + <script type="module" src="/editor/${editorBundleName}"></script> 109 + </body> 110 + </html> 111 + ` 112 + 113 + await Bun.write(`${distDir}/editor/index.html`, htmlContent) 114 + 115 + console.log('✅ Build successful!') 116 + console.log(`📦 Generated ${editorResult.outputs.length + 1} file(s):`) 117 + console.log(` - ${distDir}/editor/index.html`) 118 + for (const output of editorResult.outputs) { 119 + console.log(` - ${output.path}`) 120 + }
+4 -2
apps/main-app/package.json
··· 6 6 "test": "bun test", 7 7 "dev": "bun run --watch src/index.ts", 8 8 "start": "bun run src/index.ts", 9 - "build": "bun run build.ts", 9 + "build:css": "bunx @tailwindcss/cli -i public/styles/global.css -o dist/styles.css --minify", 10 + "build": "bun run build.ts && bun run build:css", 10 11 "check": "tsc --noEmit", 11 12 "screenshot": "bun run scripts/screenshot-sites.ts" 12 13 }, ··· 41 42 "bun-plugin-tailwind": "^0.1.2", 42 43 "class-variance-authority": "^0.7.1", 43 44 "clsx": "^2.1.1", 44 - "elysia": "^1.4.18", 45 + "elysia": "^1.4.22", 45 46 "ignore": "^7.0.5", 46 47 "iron-session": "^8.0.4", 47 48 "lucide-react": "^0.546.0", ··· 49 50 "prismjs": "^1.30.0", 50 51 "react": "^19.2.0", 51 52 "react-dom": "^19.2.0", 53 + "react-router-dom": "^7.13.0", 52 54 "tailwind-merge": "^3.3.1", 53 55 "tailwindcss": "4", 54 56 "tw-animate-css": "^1.4.0",
+392 -5
apps/main-app/public/editor/editor.tsx
··· 1 1 import { useState, useEffect } from 'react' 2 2 import { createRoot } from 'react-dom/client' 3 + import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom' 3 4 import { Button } from '@public/components/ui/button' 4 5 import { 5 6 Tabs, ··· 21 22 import { SkeletonShimmer } from '@public/components/ui/skeleton' 22 23 import { Input } from '@public/components/ui/input' 23 24 import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group' 25 + import { Card } from '@public/components/ui/card' 24 26 import { 25 27 Loader2, 26 28 Trash2, 27 - LogOut 29 + LogOut, 30 + ArrowLeft, 31 + Shield, 32 + AlertCircle, 33 + CheckCircle, 34 + Scale 28 35 } from 'lucide-react' 29 36 import Layout from '@public/layouts' 30 37 import { useUserInfo } from './hooks/useUserInfo' ··· 485 492 </a> 486 493 </p> 487 494 <p className="mt-2"> 488 - <a 489 - href="/acceptable-use" 495 + <Link 496 + to="/editor/acceptable-use" 490 497 className="text-accent hover:text-accent/80 transition-colors font-medium" 491 498 > 492 499 Acceptable Use Policy 493 - </a> 500 + </Link> 494 501 </p> 495 502 </div> 496 503 </div> ··· 853 860 ) 854 861 } 855 862 863 + function AcceptableUsePage() { 864 + const navigate = useNavigate() 865 + 866 + return ( 867 + <div className="w-full min-h-screen bg-background flex flex-col"> 868 + {/* Header */} 869 + <header className="w-full border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 870 + <div className="max-w-6xl w-full mx-auto px-4 h-16 flex items-center justify-between"> 871 + <div className="flex items-center gap-2"> 872 + <img src="/transparent-full-size-ico.png" alt="wisp.place" className="w-8 h-8" /> 873 + <span className="text-xl font-semibold text-foreground"> 874 + wisp.place 875 + </span> 876 + </div> 877 + <Button 878 + variant="ghost" 879 + size="sm" 880 + onClick={() => navigate('/editor')} 881 + > 882 + <ArrowLeft className="w-4 h-4 mr-2" /> 883 + Back to Dashboard 884 + </Button> 885 + </div> 886 + </header> 887 + 888 + {/* Hero Section */} 889 + <div className="bg-gradient-to-b from-accent/10 to-background border-b border-border/40"> 890 + <div className="container mx-auto px-4 py-16 max-w-4xl text-center"> 891 + <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-accent/20 mb-6"> 892 + <Shield className="w-8 h-8 text-accent" /> 893 + </div> 894 + <h1 className="text-4xl md:text-5xl font-bold mb-4">Acceptable Use Policy</h1> 895 + <div className="flex items-center justify-center gap-6 text-sm text-muted-foreground"> 896 + <div className="flex items-center gap-2"> 897 + <span className="font-medium">Effective:</span> 898 + <span>November 10, 2025</span> 899 + </div> 900 + <div className="h-4 w-px bg-border"></div> 901 + <div className="flex items-center gap-2"> 902 + <span className="font-medium">Last Updated:</span> 903 + <span>November 10, 2025</span> 904 + </div> 905 + </div> 906 + </div> 907 + </div> 908 + 909 + {/* Content */} 910 + <div className="container mx-auto px-4 py-12 max-w-4xl"> 911 + <article className="space-y-12"> 912 + {/* Our Philosophy */} 913 + <section> 914 + <h2 className="text-3xl font-bold mb-6 text-foreground">Our Philosophy</h2> 915 + <div className="space-y-4 text-lg leading-relaxed text-muted-foreground"> 916 + <p> 917 + wisp.place exists to give you a corner of the internet that's truly yours—a place to create, experiment, and express yourself freely. We believe in the open web and the fundamental importance of free expression. We're not here to police your thoughts, moderate your aesthetics, or judge your taste. 918 + </p> 919 + <p> 920 + That said, we're also real people running real servers in real jurisdictions (the United States and the Netherlands), and there are legal and practical limits to what we can host. This policy aims to be as permissive as possible while keeping the lights on and staying on the right side of the law. 921 + </p> 922 + </div> 923 + </section> 924 + 925 + {/* What You Can Do */} 926 + <Card className="bg-green-500/5 border-green-500/20 p-8"> 927 + <div className="flex items-start gap-4"> 928 + <div className="flex-shrink-0"> 929 + <CheckCircle className="w-8 h-8 text-green-500" /> 930 + </div> 931 + <div className="space-y-4"> 932 + <h2 className="text-3xl font-bold text-foreground">What You Can Do</h2> 933 + <div className="space-y-4 text-lg leading-relaxed text-muted-foreground"> 934 + <p> 935 + <strong className="text-green-600 dark:text-green-400">Almost anything.</strong> Seriously. Build weird art projects. Write controversial essays. Create spaces that would make corporate platforms nervous. Express unpopular opinions. Make things that are strange, provocative, uncomfortable, or just plain yours. 936 + </p> 937 + <p> 938 + We support creative and personal expression in all its forms, including adult content, political speech, counter-cultural work, and experimental projects. 939 + </p> 940 + </div> 941 + </div> 942 + </div> 943 + </Card> 944 + 945 + {/* What You Can't Do */} 946 + <section> 947 + <div className="flex items-center gap-3 mb-6"> 948 + <AlertCircle className="w-8 h-8 text-red-500" /> 949 + <h2 className="text-3xl font-bold text-foreground">What You Can't Do</h2> 950 + </div> 951 + 952 + <div className="space-y-8"> 953 + <Card className="p-6 border-2"> 954 + <h3 className="text-2xl font-semibold mb-4 text-foreground">Illegal Content</h3> 955 + <p className="text-muted-foreground mb-4"> 956 + Don't host content that's illegal in the United States or the Netherlands. This includes but isn't limited to: 957 + </p> 958 + <ul className="space-y-3 text-muted-foreground"> 959 + <li className="flex items-start gap-3"> 960 + <span className="text-red-500 mt-1">•</span> 961 + <span><strong>Child sexual abuse material (CSAM)</strong> involving real minors in any form</span> 962 + </li> 963 + <li className="flex items-start gap-3"> 964 + <span className="text-red-500 mt-1">•</span> 965 + <span><strong>Realistic or AI-generated depictions</strong> of minors in sexual contexts, including photorealistic renders, deepfakes, or AI-generated imagery</span> 966 + </li> 967 + <li className="flex items-start gap-3"> 968 + <span className="text-red-500 mt-1">•</span> 969 + <span><strong>Non-consensual intimate imagery</strong> (revenge porn, deepfakes, hidden camera footage, etc.)</span> 970 + </li> 971 + <li className="flex items-start gap-3"> 972 + <span className="text-red-500 mt-1">•</span> 973 + <span>Content depicting or facilitating human trafficking, sexual exploitation, or sexual violence</span> 974 + </li> 975 + <li className="flex items-start gap-3"> 976 + <span className="text-red-500 mt-1">•</span> 977 + <span>Instructions for manufacturing explosives, biological weapons, or other instruments designed for mass harm</span> 978 + </li> 979 + <li className="flex items-start gap-3"> 980 + <span className="text-red-500 mt-1">•</span> 981 + <span>Content that facilitates imminent violence or terrorism</span> 982 + </li> 983 + <li className="flex items-start gap-3"> 984 + <span className="text-red-500 mt-1">•</span> 985 + <span>Stolen financial information, credentials, or personal data used for fraud</span> 986 + </li> 987 + </ul> 988 + </Card> 989 + 990 + <Card className="p-6 border-2"> 991 + <h3 className="text-2xl font-semibold mb-4 text-foreground">Intellectual Property Violations</h3> 992 + <div className="space-y-4 text-muted-foreground"> 993 + <p> 994 + Don't host content that clearly violates someone else's copyright, trademark, or other intellectual property rights. We're required to respond to valid DMCA takedown notices. 995 + </p> 996 + <p> 997 + We understand that copyright law is complicated and sometimes ridiculous. We're not going to proactively scan your site or nitpick over fair use. But if we receive a legitimate legal complaint, we'll have to act on it. 998 + </p> 999 + </div> 1000 + </Card> 1001 + 1002 + <Card className="p-6 border-2 border-red-500/30 bg-red-500/5"> 1003 + <h3 className="text-2xl font-semibold mb-4 text-foreground">Hate Content</h3> 1004 + <div className="space-y-4 text-muted-foreground"> 1005 + <p> 1006 + You can express controversial ideas. You can be offensive. You can make people uncomfortable. But pure hate—content that exists solely to dehumanize, threaten, or incite violence against people based on race, ethnicity, religion, gender, sexual orientation, disability, or similar characteristics—isn't welcome here. 1007 + </p> 1008 + <p> 1009 + There's a difference between "I have deeply unpopular opinions about X" and "People like X should be eliminated." The former is protected expression. The latter isn't. 1010 + </p> 1011 + <div className="bg-background/50 border-l-4 border-red-500 p-4 rounded"> 1012 + <p className="font-medium text-foreground"> 1013 + <strong>A note on enforcement:</strong> While we're generally permissive and believe in giving people the benefit of the doubt, hate content is where we draw a hard line. I will be significantly more aggressive in moderating this type of content than anything else on this list. If your site exists primarily to spread hate or recruit people into hateful ideologies, you will be removed swiftly and without extensive appeals. This is non-negotiable. 1014 + </p> 1015 + </div> 1016 + </div> 1017 + </Card> 1018 + 1019 + <Card className="p-6 border-2"> 1020 + <h3 className="text-2xl font-semibold mb-4 text-foreground">Adult Content Guidelines</h3> 1021 + <div className="space-y-4 text-muted-foreground"> 1022 + <p> 1023 + Adult content is allowed. This includes sexually explicit material, erotica, adult artwork, and NSFW creative expression. 1024 + </p> 1025 + <p className="font-medium">However:</p> 1026 + <ul className="space-y-2"> 1027 + <li className="flex items-start gap-3"> 1028 + <span className="text-red-500 mt-1">•</span> 1029 + <span>No content involving real minors in any sexual context whatsoever</span> 1030 + </li> 1031 + <li className="flex items-start gap-3"> 1032 + <span className="text-red-500 mt-1">•</span> 1033 + <span>No photorealistic, AI-generated, or otherwise realistic depictions of minors in sexual contexts</span> 1034 + </li> 1035 + <li className="flex items-start gap-3"> 1036 + <span className="text-green-500 mt-1">•</span> 1037 + <span>Clearly stylized drawings and written fiction are permitted, provided they remain obviously non-photographic in nature</span> 1038 + </li> 1039 + <li className="flex items-start gap-3"> 1040 + <span className="text-red-500 mt-1">•</span> 1041 + <span>No non-consensual content (revenge porn, voyeurism, etc.)</span> 1042 + </li> 1043 + <li className="flex items-start gap-3"> 1044 + <span className="text-red-500 mt-1">•</span> 1045 + <span>No content depicting illegal sexual acts (bestiality, necrophilia, etc.)</span> 1046 + </li> 1047 + <li className="flex items-start gap-3"> 1048 + <span className="text-yellow-500 mt-1">•</span> 1049 + <span>Adult content should be clearly marked as such if discoverable through public directories or search</span> 1050 + </li> 1051 + </ul> 1052 + </div> 1053 + </Card> 1054 + 1055 + <Card className="p-6 border-2"> 1056 + <h3 className="text-2xl font-semibold mb-4 text-foreground">Malicious Technical Activity</h3> 1057 + <p className="text-muted-foreground mb-4">Don't use your site to:</p> 1058 + <ul className="space-y-2 text-muted-foreground"> 1059 + <li className="flex items-start gap-3"> 1060 + <span className="text-red-500 mt-1">•</span> 1061 + <span>Distribute malware, viruses, or exploits</span> 1062 + </li> 1063 + <li className="flex items-start gap-3"> 1064 + <span className="text-red-500 mt-1">•</span> 1065 + <span>Conduct phishing or social engineering attacks</span> 1066 + </li> 1067 + <li className="flex items-start gap-3"> 1068 + <span className="text-red-500 mt-1">•</span> 1069 + <span>Launch DDoS attacks or network abuse</span> 1070 + </li> 1071 + <li className="flex items-start gap-3"> 1072 + <span className="text-red-500 mt-1">•</span> 1073 + <span>Mine cryptocurrency without explicit user consent</span> 1074 + </li> 1075 + <li className="flex items-start gap-3"> 1076 + <span className="text-red-500 mt-1">•</span> 1077 + <span>Scrape, spam, or abuse other services</span> 1078 + </li> 1079 + </ul> 1080 + </Card> 1081 + </div> 1082 + </section> 1083 + 1084 + {/* Our Approach to Enforcement */} 1085 + <section> 1086 + <div className="flex items-center gap-3 mb-6"> 1087 + <Scale className="w-8 h-8 text-accent" /> 1088 + <h2 className="text-3xl font-bold text-foreground">Our Approach to Enforcement</h2> 1089 + </div> 1090 + <div className="space-y-6"> 1091 + <div className="space-y-4 text-lg leading-relaxed text-muted-foreground"> 1092 + <p> 1093 + <strong>We actively monitor for obvious violations.</strong> Not to censor your creativity or police your opinions, but to catch the clear-cut stuff that threatens the service's existence and makes this a worse place for everyone. We're looking for the blatantly illegal, the obviously harmful—the stuff that would get servers seized and communities destroyed. 1094 + </p> 1095 + <p> 1096 + We're not reading your blog posts looking for wrongthink. We're making sure this platform doesn't become a haven for the kind of content that ruins good things. 1097 + </p> 1098 + </div> 1099 + 1100 + <Card className="p-6 bg-muted/30"> 1101 + <p className="font-semibold mb-3 text-foreground">We take action when:</p> 1102 + <ol className="space-y-2 text-muted-foreground"> 1103 + <li className="flex items-start gap-3"> 1104 + <span className="font-bold text-accent">1.</span> 1105 + <span>We identify content that clearly violates this policy during routine monitoring</span> 1106 + </li> 1107 + <li className="flex items-start gap-3"> 1108 + <span className="font-bold text-accent">2.</span> 1109 + <span>We receive a valid legal complaint (DMCA, court order, etc.)</span> 1110 + </li> 1111 + <li className="flex items-start gap-3"> 1112 + <span className="font-bold text-accent">3.</span> 1113 + <span>Someone reports content that violates this policy and we can verify the violation</span> 1114 + </li> 1115 + <li className="flex items-start gap-3"> 1116 + <span className="font-bold text-accent">4.</span> 1117 + <span>Your site is causing technical problems for the service or other users</span> 1118 + </li> 1119 + </ol> 1120 + </Card> 1121 + 1122 + <Card className="p-6 bg-muted/30"> 1123 + <p className="font-semibold mb-3 text-foreground">When we do need to take action, we'll try to:</p> 1124 + <ul className="space-y-2 text-muted-foreground"> 1125 + <li className="flex items-start gap-3"> 1126 + <span className="text-accent">•</span> 1127 + <span>Contact you first when legally and practically possible</span> 1128 + </li> 1129 + <li className="flex items-start gap-3"> 1130 + <span className="text-accent">•</span> 1131 + <span>Be transparent about what's happening and why</span> 1132 + </li> 1133 + <li className="flex items-start gap-3"> 1134 + <span className="text-accent">•</span> 1135 + <span>Give you an opportunity to address the issue if appropriate</span> 1136 + </li> 1137 + </ul> 1138 + </Card> 1139 + 1140 + <p className="text-muted-foreground"> 1141 + For serious or repeated violations, we may suspend or terminate your account. 1142 + </p> 1143 + </div> 1144 + </section> 1145 + 1146 + {/* Regional Compliance */} 1147 + <Card className="p-6 bg-blue-500/5 border-blue-500/20"> 1148 + <h2 className="text-2xl font-bold mb-4 text-foreground">Regional Compliance</h2> 1149 + <p className="text-muted-foreground"> 1150 + Our servers are located in the United States and the Netherlands. Content hosted on wisp.place must comply with the laws of both jurisdictions. While we aim to provide broad creative freedom, these legal requirements are non-negotiable. 1151 + </p> 1152 + </Card> 1153 + 1154 + {/* Changes to This Policy */} 1155 + <section> 1156 + <h2 className="text-2xl font-bold mb-4 text-foreground">Changes to This Policy</h2> 1157 + <p className="text-muted-foreground"> 1158 + We may update this policy as legal requirements or service realities change. If we make significant changes, we'll notify active users. 1159 + </p> 1160 + </section> 1161 + 1162 + {/* Questions or Reports */} 1163 + <section> 1164 + <h2 className="text-2xl font-bold mb-4 text-foreground">Questions or Reports</h2> 1165 + <p className="text-muted-foreground"> 1166 + If you have questions about this policy or need to report a violation, contact us at{' '} 1167 + <a 1168 + href="mailto:contact@wisp.place" 1169 + className="text-accent hover:text-accent/80 transition-colors font-medium" 1170 + > 1171 + contact@wisp.place 1172 + </a> 1173 + . 1174 + </p> 1175 + </section> 1176 + 1177 + {/* Final Message */} 1178 + <Card className="p-8 bg-accent/10 border-accent/30 border-2"> 1179 + <p className="text-lg leading-relaxed text-foreground"> 1180 + <strong>Remember:</strong> This policy exists to keep the service running and this community healthy, not to limit your creativity. When in doubt, ask yourself: "Is this likely to get real-world authorities knocking on doors or make this place worse for everyone?" If the answer is yes, it probably doesn't belong here. Everything else? Go wild. 1181 + </p> 1182 + </Card> 1183 + </article> 1184 + </div> 1185 + 1186 + {/* Footer */} 1187 + <footer className="border-t border-border/40 bg-muted/20 mt-auto"> 1188 + <div className="container mx-auto px-4 py-8"> 1189 + <div className="text-center text-sm text-muted-foreground"> 1190 + <p> 1191 + Built by{' '} 1192 + <a 1193 + href="https://bsky.app/profile/nekomimi.pet" 1194 + target="_blank" 1195 + rel="noopener noreferrer" 1196 + className="text-accent hover:text-accent/80 transition-colors font-medium" 1197 + > 1198 + @nekomimi.pet 1199 + </a> 1200 + {' • '} 1201 + Contact:{' '} 1202 + <a 1203 + href="mailto:contact@wisp.place" 1204 + className="text-accent hover:text-accent/80 transition-colors font-medium" 1205 + > 1206 + contact@wisp.place 1207 + </a> 1208 + {' • '} 1209 + Legal/DMCA:{' '} 1210 + <a 1211 + href="mailto:legal@wisp.place" 1212 + className="text-accent hover:text-accent/80 transition-colors font-medium" 1213 + > 1214 + legal@wisp.place 1215 + </a> 1216 + </p> 1217 + <p className="mt-2"> 1218 + <Link 1219 + to="/editor" 1220 + className="text-accent hover:text-accent/80 transition-colors font-medium" 1221 + > 1222 + Back to Dashboard 1223 + </Link> 1224 + </p> 1225 + </div> 1226 + </div> 1227 + </footer> 1228 + </div> 1229 + ) 1230 + } 1231 + 1232 + function App() { 1233 + return ( 1234 + <BrowserRouter> 1235 + <Routes> 1236 + <Route path="/editor" element={<Dashboard />} /> 1237 + <Route path="/editor/acceptable-use" element={<AcceptableUsePage />} /> 1238 + </Routes> 1239 + </BrowserRouter> 1240 + ) 1241 + } 1242 + 856 1243 const root = createRoot(document.getElementById('elysia')!) 857 1244 root.render( 858 1245 <Layout className="gap-6"> 859 - <Dashboard /> 1246 + <App /> 860 1247 </Layout> 861 1248 )
+64 -6
apps/main-app/public/landingpage.html
··· 392 392 height: 1rem; 393 393 } 394 394 395 + /* Light mode terminal */ 396 + @media (prefers-color-scheme: light) { 397 + .terminal { 398 + background: #f6f8fa; 399 + border: 1px solid var(--border-light); 400 + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15); 401 + } 402 + 403 + .terminal-header { 404 + background: #eaeef2; 405 + } 406 + 407 + .terminal-title { 408 + color: #57606a; 409 + } 410 + 411 + .terminal-body { 412 + color: #1f2328; 413 + } 414 + 415 + .t-prompt { color: #6639ba; } 416 + .t-cmd { color: #1f2328; } 417 + .t-muted { color: #656d76; } 418 + .t-success { color: #1a7f37; } 419 + .t-cyan { color: #0969da; } 420 + .t-output { color: #656d76; } 421 + } 422 + 395 423 /* Features */ 396 424 .features { 397 425 padding: 6rem 2rem; ··· 665 693 <span class="hero-badge">Deploy static sites in seconds</span> 666 694 <h1>Ship fast without complexity</h1> 667 695 <p class="hero-desc"> 668 - Static hosting for developers who value simplicity. Your data, your PDS, your rules. 696 + Static hosting for developers who value simplicity. Powered by ATProto! 669 697 </p> 670 698 <div class="cta-group"> 671 699 <div class="cta-buttons"> ··· 684 712 <div class="steps"> 685 713 <div class="step"> 686 714 <div class="step-number">01</div> 687 - <h3>Sign in with Bluesky</h3> 688 - <p>Authenticate with your AT Protocol identity. No new accounts needed.</p> 715 + <h3>Sign in with your AT</h3> 716 + <p>Your Bluesky account is one! Don't have an AT?<br><a href="{{ATPROTO_LOGIN_URL}}" target="_blank">Make one here ^.^!</a></p> 689 717 </div> 690 718 <div class="step"> 691 719 <div class="step-number">02</div> ··· 757 785 </section> 758 786 759 787 <section class="cli-section"> 760 - <div class="cli-inner"> 788 + <div class="cli-inner" style="max-width: 1100px; text-align: left;"> 761 789 <p class="section-label">For power users</p> 762 790 <h2 class="section-title">Command line first</h2> 763 791 <p class="cli-desc">Full control from your terminal. Integrates with your existing workflow.</p> ··· 781 809 </div> 782 810 </div> 783 811 784 - <div class="cta-buttons"> 785 - <a href="https://docs.wisp.place/cli/" target="_blank" class="cta-secondary">Install CLI</a> 812 + <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 3rem; align-items: center; margin-top: 3rem;"> 813 + <div> 814 + <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem;">Self-host anywhere</h3> 815 + <p style="color: var(--text-muted); margin-bottom: 1.5rem;">Deploy your own server anywhere in the world. The CLI watches the firehose and automatically pulls updates when you push changes—complete control over your hosting infrastructure.</p> 816 + <div style="display: flex; align-items: center; gap: 1.5rem;"> 817 + <a href="https://docs.wisp.place/cli/" target="_blank" class="cta-secondary">Install CLI</a> 818 + <code style="color: var(--text); background: var(--code-bg); padding: 0.5rem 0.75rem; border: 1px solid var(--border-light); border-radius: 4px; font-size: 0.875rem;">npm install -g wispctl</code> 819 + </div> 820 + </div> 821 + <div> 822 + <div class="terminal"> 823 + <div class="terminal-header"> 824 + <div class="terminal-dots"> 825 + <span class="terminal-dot red"></span> 826 + <span class="terminal-dot yellow"></span> 827 + <span class="terminal-dot green"></span> 828 + </div> 829 + <span class="terminal-title">terminal</span> 830 + </div> 831 + <div class="terminal-body"> 832 + <div class="terminal-line"><span class="t-prompt">$</span> <span class="t-cmd">wispctl serve nekomimi.pet -s myblog</span></div> 833 + <div class="terminal-spacer"></div> 834 + <div class="terminal-line"><span class="t-muted">◇</span> Serving myblog from nekomimi.pet</div> 835 + <div class="terminal-line"><span class="t-success">✓</span> <span class="t-success">Server running at http://localhost:8080</span></div> 836 + <div class="terminal-line"><span class="t-muted">◇</span> Watching for updates via firehose...</div> 837 + <div class="terminal-spacer"></div> 838 + <div class="terminal-line"><span class="t-muted">◇</span> Site update received, re-pulling...</div> 839 + <div class="terminal-line"><span class="t-muted">◇</span> Files to download: 1, unchanged: 4</div> 840 + <div class="terminal-line"><span class="t-success">✓</span> <span class="t-success">Site reloaded</span></div> 841 + </div> 842 + </div> 843 + </div> 786 844 </div> 787 845 </div> 788 846 </section>
+85 -2
apps/main-app/src/index.ts
··· 1 1 // Fix for Elysia issue with Bun, (see https://github.com/oven-sh/bun/issues/12161) 2 2 process.getBuiltinModule = require; 3 3 4 - import { Elysia } from 'elysia' 4 + import { Elysia, t } from 'elysia' 5 5 import type { Context } from 'elysia' 6 6 import { cors } from '@elysiajs/cors' 7 7 import { staticPlugin } from '@elysiajs/static' ··· 119 119 set.headers['X-XSS-Protection'] = '1; mode=block' 120 120 set.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()' 121 121 }) 122 - .onError(observabilityMiddleware('main-app').onError) 122 + .onError((context) => { 123 + // Call observability error handler first 124 + observabilityMiddleware('main-app').onError(context) 125 + 126 + const { error, set, code } = context as any 127 + 128 + // Determine appropriate status code 129 + let statusCode = 500 130 + let errorMessage = 'Internal server error' 131 + 132 + if (error instanceof Error) { 133 + errorMessage = error.message 134 + 135 + // Map common error patterns to status codes 136 + if (errorMessage.includes('not found') || errorMessage.includes('Not found')) { 137 + statusCode = 404 138 + } else if (errorMessage.includes('Unauthorized') || errorMessage.includes('unauthorized')) { 139 + statusCode = 401 140 + } else if (errorMessage.includes('Forbidden') || errorMessage.includes('forbidden')) { 141 + statusCode = 403 142 + } else if (errorMessage.includes('Invalid') || errorMessage.includes('required') || 143 + errorMessage.includes('validation') || errorMessage.includes('bad request')) { 144 + statusCode = 400 145 + } else if ((error as any).status) { 146 + statusCode = (error as any).status 147 + } 148 + } 149 + 150 + // Handle Elysia error codes 151 + if (code === 'NOT_FOUND') { 152 + statusCode = 404 153 + errorMessage = 'Not found' 154 + } else if (code === 'VALIDATION') { 155 + statusCode = 400 156 + errorMessage = error instanceof Error ? error.message : 'Validation error' 157 + } else if (code === 'PARSE') { 158 + statusCode = 400 159 + errorMessage = 'Invalid request format' 160 + } 161 + 162 + set.status = statusCode 163 + set.headers['Content-Type'] = 'application/json' 164 + 165 + return { 166 + success: false, 167 + error: errorMessage, 168 + statusCode 169 + } 170 + }) 123 171 .use(csrfProtection()) 124 172 .get('/', async ({ set }) => { 125 173 // Build dynamic login URL for AT Protocol OAuth entryway ··· 146 194 prefix: '/' 147 195 }) 148 196 ) 197 + // Production only: serve built assets from dist 198 + .use( 199 + Bun.env.NODE_ENV === 'production' 200 + ? await staticPlugin({ 201 + assets: './apps/main-app/dist', 202 + prefix: '/dist' 203 + }) 204 + : (app) => app 205 + ) 206 + .use( 207 + Bun.env.NODE_ENV === 'production' 208 + ? await staticPlugin({ 209 + assets: './apps/main-app/dist/editor', 210 + prefix: '/editor' 211 + }) 212 + : (app) => app 213 + ) 214 + // Production only: serve built HTML for /editor 215 + .use( 216 + Bun.env.NODE_ENV === 'production' 217 + ? new Elysia() 218 + .get('/editor', async ({ set }) => { 219 + set.headers['Content-Type'] = 'text/html; charset=utf-8' 220 + return await Bun.file('./apps/main-app/dist/editor/index.html').text() 221 + }) 222 + .get('/editor/*', async ({ set }) => { 223 + set.headers['Content-Type'] = 'text/html; charset=utf-8' 224 + return await Bun.file('./apps/main-app/dist/editor/index.html').text() 225 + }) 226 + : (app) => app 227 + ) 228 + // Redirect old acceptable-use URL to new SPA route 229 + .get('/acceptable-use', ({ set }) => { 230 + set.redirect = '/editor/acceptable-use' 231 + }) 149 232 .get('/oauth-client-metadata.json', () => { 150 233 return createClientMetadata(config) 151 234 })
+11 -1
apps/main-app/src/routes/auth.ts
··· 65 65 } catch (err) { 66 66 logger.error('Signin error', err, { handle }) 67 67 console.error('[Auth] Full error:', err) 68 - return { error: 'Authentication failed', details: err instanceof Error ? err.message : String(err) } 68 + c.set.status = 401 69 + return { error: 'Authentication failed', details: err instanceof Error ? err.message : String(err) } 69 70 } 70 71 }) 71 72 /** ··· 153 154 return { success: true } 154 155 } catch (err) { 155 156 logger.error('[Auth] Logout error', err) 157 + c.set.status = 500 156 158 return { error: 'Logout failed' } 157 159 } 160 + }, { 161 + cookie: t.Cookie({ 162 + did: t.Optional(t.String()) 163 + }) 158 164 }) 159 165 /** 160 166 * GET /api/auth/status ··· 179 185 c.cookie.did.remove() 180 186 return { authenticated: false } 181 187 } 188 + }, { 189 + cookie: t.Cookie({ 190 + did: t.Optional(t.String()) 191 + }) 182 192 })
+33 -11
apps/main-app/src/routes/domain.ts
··· 1 - import { Elysia } from 'elysia' 1 + import { Elysia, t } from 'elysia' 2 2 import { requireAuth, type AuthenticatedContext } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 4 import { Agent } from '@atproto/api' ··· 105 105 * POST /api/domain/claim 106 106 * Success: { success: true, domain } 107 107 */ 108 - .post('/claim', async ({ body, auth }) => { 108 + .post('/claim', async ({ body, auth, set }) => { 109 109 try { 110 110 const { handle } = body as { handle?: string }; 111 111 const normalizedHandle = (handle || "").trim().toLowerCase(); 112 112 113 113 if (!isValidHandle(normalizedHandle)) { 114 + set.status = 400 114 115 throw new Error("Invalid handle"); 115 116 } 116 117 ··· 122 123 } catch (err) { 123 124 const message = err instanceof Error ? err.message : 'Unknown error'; 124 125 if (message === 'domain_limit_reached') { 126 + set.status = 400 125 127 throw new Error("Domain limit reached: You can only claim up to 3 wisp.place domains"); 126 128 } 129 + set.status = 409 127 130 throw new Error("Handle taken or error claiming domain"); 128 131 } 129 132 ··· 152 155 * POST /api/domain/update 153 156 * Success: { success: true, domain } 154 157 */ 155 - .post('/update', async ({ body, auth }) => { 158 + .post('/update', async ({ body, auth, set }) => { 156 159 try { 157 160 const { handle } = body as { handle?: string }; 158 161 const normalizedHandle = (handle || "").trim().toLowerCase(); 159 - 162 + 160 163 if (!isValidHandle(normalizedHandle)) { 164 + set.status = 400 161 165 throw new Error("Invalid handle"); 162 166 } 163 167 164 168 const desiredDomain = toDomain(normalizedHandle); 165 169 const current = await getDomainByDid(auth.did); 166 - 170 + 167 171 if (current === desiredDomain) { 168 172 return { success: true, domain: current }; 169 173 } ··· 172 176 try { 173 177 domain = await updateDomain(auth.did, normalizedHandle); 174 178 } catch (err) { 179 + set.status = 409 175 180 throw new Error("Handle taken"); 176 181 } 177 182 ··· 198 203 * POST /api/domain/custom/add 199 204 * Success: { success: true, id, domain, verified: false } 200 205 */ 201 - .post('/custom/add', async ({ body, auth }) => { 206 + .post('/custom/add', async ({ body, auth, set }) => { 202 207 try { 203 208 const { domain } = body as { domain: string }; 204 209 const domainLower = domain.toLowerCase().trim(); ··· 206 211 // Enhanced domain validation 207 212 // 1. Length check (RFC 1035: labels 1-63 chars, total max 253) 208 213 if (!domainLower || domainLower.length < 3 || domainLower.length > 253) { 214 + set.status = 400 209 215 throw new Error('Invalid domain: must be 3-253 characters'); 210 216 } 211 217 ··· 215 221 // - No consecutive dots, no leading/trailing dots or hyphens 216 222 const domainPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/; 217 223 if (!domainPattern.test(domainLower)) { 224 + set.status = 400 218 225 throw new Error('Invalid domain format'); 219 226 } 220 227 ··· 222 229 const labels = domainLower.split('.'); 223 230 for (const label of labels) { 224 231 if (label.length === 0 || label.length > 63) { 232 + set.status = 400 225 233 throw new Error('Invalid domain: label length must be 1-63 characters'); 226 234 } 227 235 if (label.startsWith('-') || label.endsWith('-')) { 236 + set.status = 400 228 237 throw new Error('Invalid domain: labels cannot start or end with hyphen'); 229 238 } 230 239 } ··· 232 241 // 4. TLD validation (require valid TLD, block single-char TLDs and numeric TLDs) 233 242 const tld = labels[labels.length - 1]; 234 243 if (tld.length < 2 || /^\d+$/.test(tld)) { 244 + set.status = 400 235 245 throw new Error('Invalid domain: TLD must be at least 2 characters and not all numeric'); 236 246 } 237 247 238 248 // 5. Homograph attack protection - block domains with mixed scripts or confusables 239 249 // Block non-ASCII characters (Punycode domains should be pre-converted) 240 250 if (!/^[a-z0-9.-]+$/.test(domainLower)) { 251 + set.status = 400 241 252 throw new Error('Invalid domain: only ASCII alphanumeric, dots, and hyphens allowed'); 242 253 } 243 254 ··· 257 268 ]; 258 269 259 270 if (blockedDomains.includes(domainLower)) { 271 + set.status = 400 260 272 throw new Error('Invalid domain: reserved or blocked domain'); 261 273 } 262 274 263 275 for (const pattern of blockedPatterns) { 264 276 if (pattern.test(domainLower)) { 277 + set.status = 400 265 278 throw new Error('Invalid domain: IP addresses not allowed'); 266 279 } 267 280 } ··· 269 282 // Check if already exists and is verified 270 283 const existing = await getCustomDomainInfo(domainLower); 271 284 if (existing && existing.verified) { 285 + set.status = 409 272 286 throw new Error('Domain already verified and claimed'); 273 287 } 274 288 ··· 293 307 * POST /api/domain/custom/verify 294 308 * Success: { success: true, verified, error, found } 295 309 */ 296 - .post('/custom/verify', async ({ body, auth }) => { 310 + .post('/custom/verify', async ({ body, auth, set }) => { 297 311 try { 298 312 const { id } = body as { id: string }; 299 313 300 314 // Get domain from database 301 315 const domainInfo = await getCustomDomainById(id); 302 316 if (!domainInfo) { 317 + set.status = 404 303 318 throw new Error('Domain not found'); 304 319 } 305 320 ··· 325 340 * DELETE /api/domain/custom/:id 326 341 * Success: { success: true } 327 342 */ 328 - .delete('/custom/:id', async ({ params, auth }) => { 343 + .delete('/custom/:id', async ({ params, auth, set }) => { 329 344 try { 330 345 const { id } = params; 331 346 332 347 // Verify ownership before deleting 333 348 const domainInfo = await getCustomDomainById(id); 334 349 if (!domainInfo) { 350 + set.status = 404 335 351 throw new Error('Domain not found'); 336 352 } 337 353 338 354 if (domainInfo.did !== auth.did) { 355 + set.status = 403 339 356 throw new Error('Unauthorized: You do not own this domain'); 340 357 } 341 358 ··· 352 369 * POST /api/domain/wisp/map-site 353 370 * Success: { success: true } 354 371 */ 355 - .post('/wisp/map-site', async ({ body, auth }) => { 372 + .post('/wisp/map-site', async ({ body, auth, set }) => { 356 373 try { 357 374 const { domain, siteRkey } = body as { domain: string; siteRkey: string | null }; 358 375 359 376 if (!domain) { 377 + set.status = 400 360 378 throw new Error('Domain parameter required'); 361 379 } 362 380 ··· 373 391 * DELETE /api/domain/wisp/:domain 374 392 * Success: { success: true } 375 393 */ 376 - .delete('/wisp/:domain', async ({ params, auth }) => { 394 + .delete('/wisp/:domain', async ({ params, auth, set }) => { 377 395 try { 378 396 const { domain } = params; 379 397 ··· 382 400 const info = await isDomainRegistered(domainLower); 383 401 384 402 if (!info.registered || info.type !== 'wisp') { 403 + set.status = 404 385 404 throw new Error('Domain not found'); 386 405 } 387 406 388 407 if (info.did !== auth.did) { 408 + set.status = 403 389 409 throw new Error('Unauthorized: You do not own this domain'); 390 410 } 391 411 ··· 416 436 * POST /api/domain/custom/:id/map-site 417 437 * Success: { success: true } 418 438 */ 419 - .post('/custom/:id/map-site', async ({ params, body, auth }) => { 439 + .post('/custom/:id/map-site', async ({ params, body, auth, set }) => { 420 440 try { 421 441 const { id } = params; 422 442 const { siteRkey } = body as { siteRkey: string | null }; ··· 424 444 // Verify ownership before updating 425 445 const domainInfo = await getCustomDomainById(id); 426 446 if (!domainInfo) { 447 + set.status = 404 427 448 throw new Error('Domain not found'); 428 449 } 429 450 430 451 if (domainInfo.did !== auth.did) { 452 + set.status = 403 431 453 throw new Error('Unauthorized: You do not own this domain'); 432 454 } 433 455
+11 -4
apps/main-app/src/routes/site.ts
··· 1 - import { Elysia } from 'elysia' 1 + import { Elysia, t } from 'elysia' 2 2 import { requireAuth } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 4 import { Agent } from '@atproto/api' ··· 25 25 * Success: { success: true, message } 26 26 * Failure: { success: false, error } 27 27 */ 28 - .delete('/:rkey', async ({ params, auth }) => { 28 + .delete('/:rkey', async ({ params, auth, set }) => { 29 29 const { rkey } = params 30 30 31 31 if (!rkey) { 32 + set.status = 400 32 33 return { 33 34 success: false, 34 35 error: 'Site rkey is required' ··· 119 120 } 120 121 } catch (err) { 121 122 logger.error('[Site] Delete error', err) 123 + set.status = 500 122 124 return { 123 125 success: false, 124 126 error: err instanceof Error ? err.message : 'Failed to delete site' ··· 130 132 * Success: place.wisp.settings record or default settings object. 131 133 * Failure: { success: false, error } 132 134 */ 133 - .get('/:rkey/settings', async ({ params, auth }) => { 135 + .get('/:rkey/settings', async ({ params, auth, set }) => { 134 136 const { rkey } = params 135 137 136 138 if (!rkey) { 139 + set.status = 400 137 140 return { 138 141 success: false, 139 142 error: 'Site rkey is required' ··· 175 178 } 176 179 } catch (err) { 177 180 logger.error('[Site] Get settings error', err) 181 + set.status = 500 178 182 return { 179 183 success: false, 180 184 error: err instanceof Error ? err.message : 'Failed to fetch settings' ··· 186 190 * Success: { success: true, uri, cid } 187 191 * Failure: { success: false, error } 188 192 */ 189 - .post('/:rkey/settings', async ({ params, body, auth }) => { 193 + .post('/:rkey/settings', async ({ params, body, auth, set }) => { 190 194 const { rkey } = params 191 195 192 196 if (!rkey) { 197 + set.status = 400 193 198 return { 194 199 success: false, 195 200 error: 'Site rkey is required' ··· 207 212 ].filter(Boolean) 208 213 209 214 if (modes.length > 1) { 215 + set.status = 400 210 216 return { 211 217 success: false, 212 218 error: 'Only one of spaMode, directoryListing, or custom404 can be enabled' ··· 237 243 } 238 244 } catch (err) { 239 245 logger.error('[Site] Save settings error', err) 246 + set.status = 500 240 247 return { 241 248 success: false, 242 249 error: err instanceof Error ? err.message : 'Failed to save settings'
+1 -1
apps/main-app/src/routes/wisp.ts
··· 1 - import { Elysia } from 'elysia' 1 + import { Elysia, t } from 'elysia' 2 2 import { requireAuth, type AuthenticatedContext } from '../lib/wisp-auth' 3 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 4 import { Agent } from '@atproto/api'
+11 -4
bun.lock
··· 6 6 "name": "@wisp/monorepo", 7 7 "dependencies": { 8 8 "@tailwindcss/cli": "^4.1.17", 9 - "atproto-ui": "^0.12.0", 10 9 "bun-plugin-tailwind": "^0.1.2", 11 - "elysia": "^1.4.18", 12 10 "tailwindcss": "^4.1.17", 13 11 }, 14 12 "devDependencies": { ··· 82 80 "bun-plugin-tailwind": "^0.1.2", 83 81 "class-variance-authority": "^0.7.1", 84 82 "clsx": "^2.1.1", 85 - "elysia": "^1.4.18", 83 + "elysia": "^1.4.22", 86 84 "ignore": "^7.0.5", 87 85 "iron-session": "^8.0.4", 88 86 "lucide-react": "^0.546.0", ··· 90 88 "prismjs": "^1.30.0", 91 89 "react": "^19.2.0", 92 90 "react-dom": "^19.2.0", 91 + "react-router-dom": "^7.13.0", 93 92 "tailwind-merge": "^3.3.1", 94 93 "tailwindcss": "4", 95 94 "tw-animate-css": "^1.4.0", ··· 889 888 890 889 "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 891 890 892 - "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], 891 + "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], 893 892 894 893 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 895 894 ··· 1209 1208 1210 1209 "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], 1211 1210 1211 + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], 1212 + 1213 + "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], 1214 + 1212 1215 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], 1213 1216 1214 1217 "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], ··· 1236 1239 "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], 1237 1240 1238 1241 "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], 1242 + 1243 + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], 1239 1244 1240 1245 "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 1241 1246 ··· 1552 1557 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1553 1558 1554 1559 "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 1560 + 1561 + "@types/bun/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], 1555 1562 1556 1563 "@wisp/lexicons/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1557 1564
-2
package.json
··· 10 10 ], 11 11 "dependencies": { 12 12 "@tailwindcss/cli": "^4.1.17", 13 - "atproto-ui": "^0.12.0", 14 13 "bun-plugin-tailwind": "^0.1.2", 15 - "elysia": "^1.4.18", 16 14 "tailwindcss": "^4.1.17" 17 15 }, 18 16 "scripts": {