+6
hosting-service/.env.example
+6
hosting-service/.env.example
+8
hosting-service/.gitignore
+8
hosting-service/.gitignore
+123
hosting-service/EXAMPLE.md
+123
hosting-service/EXAMPLE.md
···+This document demonstrates how HTML path rewriting works when serving sites via the `/s/:identifier/:site/*` route.+When you create a static site with absolute paths like `/style.css` or `/images/logo.png`, these paths work fine when served from the root domain. However, when served from a subdirectory like `/s/alice.bsky.social/mysite/`, these absolute paths break because they resolve to the server root instead of the site root.+The hosting service automatically rewrites absolute paths in HTML files to work correctly in the subdirectory context.+srcset="/s/alice.bsky.social/mysite/images/hero.jpg 1x, /s/alice.bsky.social/mysite/images/hero@2x.jpg 2x"+If you're using relative paths already (like `./style.css` or `../images/logo.png`), they'll work without any rewriting.
+130
hosting-service/README.md
+130
hosting-service/README.md
···+- **Automatic HTML path rewriting**: Absolute paths (`/style.css`) are rewritten to relative paths (`/s/:identifier/:site/style.css`)+When serving sites via the `/s/:identifier/:site/*` route, HTML files are automatically processed to rewrite absolute paths to work correctly in the subdirectory context.+This ensures sites work correctly when served from subdirectories without requiring manual path adjustments.
+60
hosting-service/bun.lock
+60
hosting-service/bun.lock
···+"@atproto/api": ["@atproto/api@0.13.35", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@atproto/lexicon": "^0.4.6", "@atproto/syntax": "^0.3.2", "@atproto/xrpc": "^0.6.8", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g=="],+"@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],+"@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="],+"@atproto/syntax": ["@atproto/syntax@0.3.4", "", {}, "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg=="],+"@atproto/xrpc": ["@atproto/xrpc@0.6.12", "", { "dependencies": { "@atproto/lexicon": "^0.4.10", "zod": "^3.23.8" } }, "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w=="],+"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],+"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],+"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],+"await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="],+"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],+"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],+"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],+"hono": ["hono@4.10.2", "", {}, "sha512-p6fyzl+mQo6uhESLxbF5WlBOAJMDh36PljwlKtP5V1v09NxlqGru3ShK+4wKhSuhuYf8qxMmrivHOa/M7q0sMg=="],+"iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="],+"multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="],+"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],+"tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="],+"uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="],+"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],+"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],+"@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.1", "", {}, "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw=="],
+18
hosting-service/package.json
+18
hosting-service/package.json
···
+59
hosting-service/src/index.ts
+59
hosting-service/src/index.ts
···
+62
hosting-service/src/lib/db.ts
+62
hosting-service/src/lib/db.ts
···+export async function getCustomDomainByHash(hash: string): Promise<CustomDomainLookup | null> {+VALUES (${did}, ${rkey}, ${displayName || null}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW()))
+328
hosting-service/src/lib/firehose.ts
+328
hosting-service/src/lib/firehose.ts
···+const recordUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(site)}`;+const recordUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(site)}`;
+107
hosting-service/src/lib/html-rewriter.test.ts
+107
hosting-service/src/lib/html-rewriter.test.ts
···+expect(result).toBe('<img srcset="/s/did:plc:123/mysite/logo.png 1x, /s/did:plc:123/mysite/logo@2x.png 2x">');
+130
hosting-service/src/lib/html-rewriter.ts
+130
hosting-service/src/lib/html-rewriter.ts
···
+181
hosting-service/src/lib/safe-fetch.ts
+181
hosting-service/src/lib/safe-fetch.ts
···
+27
hosting-service/src/lib/types.ts
+27
hosting-service/src/lib/types.ts
···
+162
hosting-service/src/lib/utils.ts
+162
hosting-service/src/lib/utils.ts
···+export async function fetchSiteRecord(did: string, rkey: string): Promise<WispFsRecord | null> {+const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.fs&rkey=${encodeURIComponent(rkey)}`;+export async function downloadAndCacheSite(did: string, rkey: string, record: WispFsRecord, pdsEndpoint: string): Promise<void> {+const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
+213
hosting-service/src/server.ts
+213
hosting-service/src/server.ts
···+import { resolveDid, getPdsForDid, fetchSiteRecord, downloadAndCacheSite, getCachedFilePath, isCached } from './lib/utils';
+864
-298
public/editor/editor.tsx
+864
-298
public/editor/editor.tsx
············-className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors"+className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors"···············
+4
-1
public/lib/api.ts
+4
-1
public/lib/api.ts
···+const apiHost = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8000'
+12
public/onboarding/index.html
+12
public/onboarding/index.html
···
+412
public/onboarding/onboarding.tsx
+412
public/onboarding/onboarding.tsx
···+<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">+<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-muted-foreground" />+<div className="border-2 border-dashed border-border rounded-lg p-8 text-center hover:border-accent transition-colors">
+2
src/index.ts
+2
src/index.ts
······
+35
-9
src/lib/db.ts
+35
-9
src/lib/db.ts
·········+export const updateWispDomainSite = async (did: string, siteRkey: string | null): Promise<void> => {···-export const claimCustomDomain = async (did: string, domain: string, siteName: string, hash: string) => {+export const claimCustomDomain = async (did: string, domain: string, hash: string, rkey: string = 'self') => {······-VALUES (${did}, ${siteName}, ${displayName || null}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW()))+VALUES (${did}, ${rkey}, ${displayName || null}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW()))
+156
src/lib/dns-verify.ts
+156
src/lib/dns-verify.ts
···
+90
src/lib/sync-sites.ts
+90
src/lib/sync-sites.ts
···+const errorMsg = `Error processing record: ${err instanceof Error ? err.message : 'Unknown error'}`+const errorMsg = `Failed to fetch records from PDS: ${err instanceof Error ? err.message : 'Unknown error'}`
+24
src/routes/auth.ts
+24
src/routes/auth.ts
······
+110
src/routes/domain.ts
+110
src/routes/domain.ts
······throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`);+const hash = createHash('sha256').update(`${auth.did}:${domainLower}`).digest('hex').substring(0, 16);+throw new Error(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`);+throw new Error(`Failed to verify domain: ${err instanceof Error ? err.message : 'Unknown error'}`);+throw new Error(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`);+throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`);+throw new Error(`Failed to map site: ${err instanceof Error ? err.message : 'Unknown error'}`);
+99
src/routes/user.ts
+99
src/routes/user.ts
···+import { getSitesByDid, getDomainByDid, getCustomDomainsByDid, getWispDomainInfo } from '../lib/db'
+111
-9
src/routes/wisp.ts
+111
-9
src/routes/wisp.ts
······console.log('🚀 Starting upload process', { siteName, fileCount: Array.isArray(files) ? files.length : 1 });···-throw new Error('No valid web files found to upload. Allowed types: HTML, CSS, JS, images, fonts, PDFs, and other web assets.');······