···11+/**
22+ * Main app logger using @wisp/observability
33+ *
44+ * Note: This file is kept for backward compatibility.
55+ * New code should import createLogger from @wisp/observability directly.
66+ */
77+import { createLogger } from '@wisp/observability'
88+99+export const logger = createLogger('main-app')
···11+/**
22+ * Shared database utilities for wisp.place
33+ *
44+ * This package provides database query functions that work across both
55+ * main-app (Bun SQL) and hosting-service (postgres) environments.
66+ *
77+ * The actual database client is passed in by the consuming application.
88+ */
99+1010+export * from './types';
1111+1212+// Re-export types
1313+export type {
1414+ DomainLookup,
1515+ CustomDomainLookup,
1616+ SiteRecord,
1717+ OAuthState,
1818+ OAuthSession,
1919+ OAuthKey,
2020+ CookieSecret,
2121+ AdminUser
2222+} from './types';
···11// Quick script to create admin user with randomly generated password
22-import { adminAuth } from './src/lib/admin-auth'
22+import { adminAuth } from '../src/lib/admin-auth'
33import { randomBytes } from 'crypto'
4455// Generate a secure random password
···99 type MethodConfigOrHandler,
1010 createServer as createXrpcServer,
1111} from '@atproto/xrpc-server'
1212-import { schemas } from './lexicons.js'
1212+import { schemas } from './lexicons'
13131414export function createServer(options?: XrpcOptions): Server {
1515 return new Server(options)
···77 ValidationError,
88 type ValidationResult,
99} from '@atproto/lexicon'
1010-import { type $Typed, is$typed, maybe$typed } from './util.js'
1010+import { type $Typed, is$typed, maybe$typed } from './util'
11111212export const schemaDict = {
1313 PlaceWispFs: {
-110
src/lexicons/types/place/wisp/fs.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55-import { CID } from 'multiformats/cid'
66-import { validate as _validate } from '../../../lexicons'
77-import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88-99-const is$typed = _is$typed,
1010- validate = _validate
1111-const id = 'place.wisp.fs'
1212-1313-export interface Main {
1414- $type: 'place.wisp.fs'
1515- site: string
1616- root: Directory
1717- fileCount?: number
1818- createdAt: string
1919- [k: string]: unknown
2020-}
2121-2222-const hashMain = 'main'
2323-2424-export function isMain<V>(v: V) {
2525- return is$typed(v, id, hashMain)
2626-}
2727-2828-export function validateMain<V>(v: V) {
2929- return validate<Main & V>(v, id, hashMain, true)
3030-}
3131-3232-export {
3333- type Main as Record,
3434- isMain as isRecord,
3535- validateMain as validateRecord,
3636-}
3737-3838-export interface File {
3939- $type?: 'place.wisp.fs#file'
4040- type: 'file'
4141- /** Content blob ref */
4242- blob: BlobRef
4343- /** Content encoding (e.g., gzip for compressed files) */
4444- encoding?: 'gzip'
4545- /** Original MIME type before compression */
4646- mimeType?: string
4747- /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */
4848- base64?: boolean
4949-}
5050-5151-const hashFile = 'file'
5252-5353-export function isFile<V>(v: V) {
5454- return is$typed(v, id, hashFile)
5555-}
5656-5757-export function validateFile<V>(v: V) {
5858- return validate<File & V>(v, id, hashFile)
5959-}
6060-6161-export interface Directory {
6262- $type?: 'place.wisp.fs#directory'
6363- type: 'directory'
6464- entries: Entry[]
6565-}
6666-6767-const hashDirectory = 'directory'
6868-6969-export function isDirectory<V>(v: V) {
7070- return is$typed(v, id, hashDirectory)
7171-}
7272-7373-export function validateDirectory<V>(v: V) {
7474- return validate<Directory & V>(v, id, hashDirectory)
7575-}
7676-7777-export interface Entry {
7878- $type?: 'place.wisp.fs#entry'
7979- name: string
8080- node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
8181-}
8282-8383-const hashEntry = 'entry'
8484-8585-export function isEntry<V>(v: V) {
8686- return is$typed(v, id, hashEntry)
8787-}
8888-8989-export function validateEntry<V>(v: V) {
9090- return validate<Entry & V>(v, id, hashEntry)
9191-}
9292-9393-export interface Subfs {
9494- $type?: 'place.wisp.fs#subfs'
9595- type: 'subfs'
9696- /** AT-URI pointing to a place.wisp.subfs record containing this subtree. */
9797- subject: string
9898- /** If true (default), the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false, the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure. */
9999- flat?: boolean
100100-}
101101-102102-const hashSubfs = 'subfs'
103103-104104-export function isSubfs<V>(v: V) {
105105- return is$typed(v, id, hashSubfs)
106106-}
107107-108108-export function validateSubfs<V>(v: V) {
109109- return validate<Subfs & V>(v, id, hashSubfs)
110110-}
-65
src/lexicons/types/place/wisp/settings.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55-import { CID } from 'multiformats/cid'
66-import { validate as _validate } from '../../../lexicons'
77-import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88-99-const is$typed = _is$typed,
1010- validate = _validate
1111-const id = 'place.wisp.settings'
1212-1313-export interface Main {
1414- $type: 'place.wisp.settings'
1515- /** Enable directory listing mode for paths that resolve to directories without an index file. Incompatible with spaMode. */
1616- directoryListing: boolean
1717- /** File to serve for all routes (e.g., 'index.html'). When set, enables SPA mode where all non-file requests are routed to this file. Incompatible with directoryListing and custom404. */
1818- spaMode?: string
1919- /** Custom 404 error page file path. Incompatible with directoryListing and spaMode. */
2020- custom404?: string
2121- /** Ordered list of files to try when serving a directory. Defaults to ['index.html'] if not specified. */
2222- indexFiles?: string[]
2323- /** Enable clean URL routing. When enabled, '/about' will attempt to serve '/about.html' or '/about/index.html' automatically. */
2424- cleanUrls: boolean
2525- /** Custom HTTP headers to set on responses */
2626- headers?: CustomHeader[]
2727- [k: string]: unknown
2828-}
2929-3030-const hashMain = 'main'
3131-3232-export function isMain<V>(v: V) {
3333- return is$typed(v, id, hashMain)
3434-}
3535-3636-export function validateMain<V>(v: V) {
3737- return validate<Main & V>(v, id, hashMain, true)
3838-}
3939-4040-export {
4141- type Main as Record,
4242- isMain as isRecord,
4343- validateMain as validateRecord,
4444-}
4545-4646-/** Custom HTTP header configuration */
4747-export interface CustomHeader {
4848- $type?: 'place.wisp.settings#customHeader'
4949- /** HTTP header name (e.g., 'Cache-Control', 'X-Frame-Options') */
5050- name: string
5151- /** HTTP header value */
5252- value: string
5353- /** Optional glob pattern to apply this header to specific paths (e.g., '*.html', '/assets/*'). If not specified, applies to all paths. */
5454- path?: string
5555-}
5656-5757-const hashCustomHeader = 'customHeader'
5858-5959-export function isCustomHeader<V>(v: V) {
6060- return is$typed(v, id, hashCustomHeader)
6161-}
6262-6363-export function validateCustomHeader<V>(v: V) {
6464- return validate<CustomHeader & V>(v, id, hashCustomHeader)
6565-}
-107
src/lexicons/types/place/wisp/subfs.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-import { type ValidationResult, BlobRef } from '@atproto/lexicon'
55-import { CID } from 'multiformats/cid'
66-import { validate as _validate } from '../../../lexicons'
77-import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util'
88-99-const is$typed = _is$typed,
1010- validate = _validate
1111-const id = 'place.wisp.subfs'
1212-1313-export interface Main {
1414- $type: 'place.wisp.subfs'
1515- root: Directory
1616- fileCount?: number
1717- createdAt: string
1818- [k: string]: unknown
1919-}
2020-2121-const hashMain = 'main'
2222-2323-export function isMain<V>(v: V) {
2424- return is$typed(v, id, hashMain)
2525-}
2626-2727-export function validateMain<V>(v: V) {
2828- return validate<Main & V>(v, id, hashMain, true)
2929-}
3030-3131-export {
3232- type Main as Record,
3333- isMain as isRecord,
3434- validateMain as validateRecord,
3535-}
3636-3737-export interface File {
3838- $type?: 'place.wisp.subfs#file'
3939- type: 'file'
4040- /** Content blob ref */
4141- blob: BlobRef
4242- /** Content encoding (e.g., gzip for compressed files) */
4343- encoding?: 'gzip'
4444- /** Original MIME type before compression */
4545- mimeType?: string
4646- /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */
4747- base64?: boolean
4848-}
4949-5050-const hashFile = 'file'
5151-5252-export function isFile<V>(v: V) {
5353- return is$typed(v, id, hashFile)
5454-}
5555-5656-export function validateFile<V>(v: V) {
5757- return validate<File & V>(v, id, hashFile)
5858-}
5959-6060-export interface Directory {
6161- $type?: 'place.wisp.subfs#directory'
6262- type: 'directory'
6363- entries: Entry[]
6464-}
6565-6666-const hashDirectory = 'directory'
6767-6868-export function isDirectory<V>(v: V) {
6969- return is$typed(v, id, hashDirectory)
7070-}
7171-7272-export function validateDirectory<V>(v: V) {
7373- return validate<Directory & V>(v, id, hashDirectory)
7474-}
7575-7676-export interface Entry {
7777- $type?: 'place.wisp.subfs#entry'
7878- name: string
7979- node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
8080-}
8181-8282-const hashEntry = 'entry'
8383-8484-export function isEntry<V>(v: V) {
8585- return is$typed(v, id, hashEntry)
8686-}
8787-8888-export function validateEntry<V>(v: V) {
8989- return validate<Entry & V>(v, id, hashEntry)
9090-}
9191-9292-export interface Subfs {
9393- $type?: 'place.wisp.subfs#subfs'
9494- type: 'subfs'
9595- /** AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures. */
9696- subject: string
9797-}
9898-9999-const hashSubfs = 'subfs'
100100-101101-export function isSubfs<V>(v: V) {
102102- return is$typed(v, id, hashSubfs)
103103-}
104104-105105-export function validateSubfs<V>(v: V) {
106106- return validate<Subfs & V>(v, id, hashSubfs)
107107-}
-82
src/lexicons/util.ts
···11-/**
22- * GENERATED CODE - DO NOT MODIFY
33- */
44-55-import { type ValidationResult } from '@atproto/lexicon'
66-77-export type OmitKey<T, K extends keyof T> = {
88- [K2 in keyof T as K2 extends K ? never : K2]: T[K2]
99-}
1010-1111-export type $Typed<V, T extends string = string> = V & { $type: T }
1212-export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
1313-1414-export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
1515- ? Id
1616- : `${Id}#${Hash}`
1717-1818-function isObject<V>(v: V): v is V & object {
1919- return v != null && typeof v === 'object'
2020-}
2121-2222-function is$type<Id extends string, Hash extends string>(
2323- $type: unknown,
2424- id: Id,
2525- hash: Hash,
2626-): $type is $Type<Id, Hash> {
2727- return hash === 'main'
2828- ? $type === id
2929- : // $type === `${id}#${hash}`
3030- typeof $type === 'string' &&
3131- $type.length === id.length + 1 + hash.length &&
3232- $type.charCodeAt(id.length) === 35 /* '#' */ &&
3333- $type.startsWith(id) &&
3434- $type.endsWith(hash)
3535-}
3636-3737-export type $TypedObject<
3838- V,
3939- Id extends string,
4040- Hash extends string,
4141-> = V extends {
4242- $type: $Type<Id, Hash>
4343-}
4444- ? V
4545- : V extends { $type?: string }
4646- ? V extends { $type?: infer T extends $Type<Id, Hash> }
4747- ? V & { $type: T }
4848- : never
4949- : V & { $type: $Type<Id, Hash> }
5050-5151-export function is$typed<V, Id extends string, Hash extends string>(
5252- v: V,
5353- id: Id,
5454- hash: Hash,
5555-): v is $TypedObject<V, Id, Hash> {
5656- return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
5757-}
5858-5959-export function maybe$typed<V, Id extends string, Hash extends string>(
6060- v: V,
6161- id: Id,
6262- hash: Hash,
6363-): v is V & object & { $type?: $Type<Id, Hash> } {
6464- return (
6565- isObject(v) &&
6666- ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)
6767- )
6868-}
6969-7070-export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
7171-export type ValidatorParam<V extends Validator> =
7272- V extends Validator<infer R> ? R : never
7373-7474-/**
7575- * Utility function that allows to convert a "validate*" utility function into a
7676- * type predicate.
7777- */
7878-export function asPredicate<V extends Validator>(validate: V) {
7979- return function <T>(v: T): v is T & ValidatorParam<V> {
8080- return validate(v).success
8181- }
8282-}
···77 type UploadedFile,
88 type FileUploadResult,
99 processUploadedFiles,
1010- createManifest,
1110 updateFileBlobs,
1111+ findLargeDirectories,
1212+ replaceDirectoryWithSubfs,
1313+ estimateDirectorySize
1414+} from '@wisp/fs-utils'
1515+import {
1216 shouldCompressFile,
1317 compressFile,
1418 computeCID,
1519 extractBlobMap,
1616- extractSubfsUris,
1717- findLargeDirectories,
1818- replaceDirectoryWithSubfs,
1919- estimateDirectorySize
2020-} from '../lib/wisp-utils'
2020+ extractSubfsUris
2121+} from '@wisp/atproto-utils'
2222+import { createManifest } from '@wisp/fs-utils'
2123import { upsertSite } from '../lib/db'
2222-import { logger } from '../lib/observability'
2323-import { validateRecord } from '../lexicons/types/place/wisp/fs'
2424-import { validateRecord as validateSubfsRecord } from '../lexicons/types/place/wisp/subfs'
2525-import { MAX_SITE_SIZE, MAX_FILE_SIZE, MAX_FILE_COUNT } from '../lib/constants'
2424+import { createLogger } from '@wisp/observability'
2525+import { validateRecord, type Directory } from '@wisp/lexicons/types/place/wisp/fs'
2626+import { validateRecord as validateSubfsRecord } from '@wisp/lexicons/types/place/wisp/subfs'
2727+import { MAX_SITE_SIZE, MAX_FILE_SIZE, MAX_FILE_COUNT } from '@wisp/constants'
2628import {
2729 createUploadJob,
2830 getUploadJob,
···3133 failUploadJob,
3234 addJobListener
3335} from '../lib/upload-jobs'
3636+3737+const logger = createLogger('main-app')
34383539function isValidSiteName(siteName: string): boolean {
3640 if (!siteName || typeof siteName !== 'string') return false;
-40
testDeploy/index.html
···11-<!DOCTYPE html>
22-<html lang="en">
33-<head>
44- <meta charset="UTF-8">
55- <meta name="viewport" content="width=device-width, initial-scale=1.0">
66- <title>Wisp.place Test Site</title>
77- <style>
88- body {
99- font-family: system-ui, -apple-system, sans-serif;
1010- max-width: 800px;
1111- margin: 4rem auto;
1212- padding: 0 2rem;
1313- line-height: 1.6;
1414- }
1515- h1 {
1616- color: #333;
1717- }
1818- .info {
1919- background: #f0f0f0;
2020- padding: 1rem;
2121- border-radius: 8px;
2222- margin: 2rem 0;
2323- }
2424- </style>
2525-</head>
2626-<body>
2727- <h1>Hello from Wisp.place!</h1>
2828- <p>This is a test deployment using the wisp-cli and Tangled Spindles CI/CD.</p>
2929-3030- <div class="info">
3131- <h2>About this deployment</h2>
3232- <p>This site was deployed to the AT Protocol using:</p>
3333- <ul>
3434- <li>Wisp.place CLI (Rust)</li>
3535- <li>Tangled Spindles CI/CD</li>
3636- <li>AT Protocol for decentralized hosting</li>
3737- </ul>
3838- </div>
3939-</body>
4040-</html>
+1-1
tsconfig.json
···2727 /* Modules */
2828 "module": "ES2022" /* Specify what module code is generated. */,
2929 // "rootDir": "./", /* Specify the root folder within your source files. */
3030- "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
3030+ "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
3131 // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
3232 // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
3333 // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */