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

add place.wisp.subfs lexicon for large site support

Introduces a new lexicon type to split large sites into multiple records.
The subfs (sub-filesystem) record contains a directory tree that can be
referenced from the main place.wisp.fs manifest via a subfs node.

This enables sites with 400+ files or manifests >140KB to bypass PDS
record size limits by splitting directories into separate records.

Changed files
+273
hosting-service
src
lexicon
types
place
wisp
lexicons
src
lexicons
types
place
wisp
+107
hosting-service/src/lexicon/types/place/wisp/subfs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'place.wisp.subfs' 12 + 13 + export interface Main { 14 + $type: 'place.wisp.subfs' 15 + root: Directory 16 + fileCount?: number 17 + createdAt: string 18 + [k: string]: unknown 19 + } 20 + 21 + const hashMain = 'main' 22 + 23 + export function isMain<V>(v: V) { 24 + return is$typed(v, id, hashMain) 25 + } 26 + 27 + export function validateMain<V>(v: V) { 28 + return validate<Main & V>(v, id, hashMain, true) 29 + } 30 + 31 + export { 32 + type Main as Record, 33 + isMain as isRecord, 34 + validateMain as validateRecord, 35 + } 36 + 37 + export interface File { 38 + $type?: 'place.wisp.subfs#file' 39 + type: 'file' 40 + /** Content blob ref */ 41 + blob: BlobRef 42 + /** Content encoding (e.g., gzip for compressed files) */ 43 + encoding?: 'gzip' 44 + /** Original MIME type before compression */ 45 + mimeType?: string 46 + /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */ 47 + base64?: boolean 48 + } 49 + 50 + const hashFile = 'file' 51 + 52 + export function isFile<V>(v: V) { 53 + return is$typed(v, id, hashFile) 54 + } 55 + 56 + export function validateFile<V>(v: V) { 57 + return validate<File & V>(v, id, hashFile) 58 + } 59 + 60 + export interface Directory { 61 + $type?: 'place.wisp.subfs#directory' 62 + type: 'directory' 63 + entries: Entry[] 64 + } 65 + 66 + const hashDirectory = 'directory' 67 + 68 + export function isDirectory<V>(v: V) { 69 + return is$typed(v, id, hashDirectory) 70 + } 71 + 72 + export function validateDirectory<V>(v: V) { 73 + return validate<Directory & V>(v, id, hashDirectory) 74 + } 75 + 76 + export interface Entry { 77 + $type?: 'place.wisp.subfs#entry' 78 + name: string 79 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 80 + } 81 + 82 + const hashEntry = 'entry' 83 + 84 + export function isEntry<V>(v: V) { 85 + return is$typed(v, id, hashEntry) 86 + } 87 + 88 + export function validateEntry<V>(v: V) { 89 + return validate<Entry & V>(v, id, hashEntry) 90 + } 91 + 92 + export interface Subfs { 93 + $type?: 'place.wisp.subfs#subfs' 94 + type: 'subfs' 95 + /** AT-URI pointing to another place.wisp.subfs record for nested subtrees */ 96 + subject: string 97 + } 98 + 99 + const hashSubfs = 'subfs' 100 + 101 + export function isSubfs<V>(v: V) { 102 + return is$typed(v, id, hashSubfs) 103 + } 104 + 105 + export function validateSubfs<V>(v: V) { 106 + return validate<Subfs & V>(v, id, hashSubfs) 107 + }
+59
lexicons/subfs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.subfs", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Virtual filesystem manifest within a place.wisp.fs record", 8 + "record": { 9 + "type": "object", 10 + "required": ["root", "createdAt"], 11 + "properties": { 12 + "root": { "type": "ref", "ref": "#directory" }, 13 + "fileCount": { "type": "integer", "minimum": 0, "maximum": 1000 }, 14 + "createdAt": { "type": "string", "format": "datetime" } 15 + } 16 + } 17 + }, 18 + "file": { 19 + "type": "object", 20 + "required": ["type", "blob"], 21 + "properties": { 22 + "type": { "type": "string", "const": "file" }, 23 + "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 24 + "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 25 + "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 26 + "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } 27 + } 28 + }, 29 + "directory": { 30 + "type": "object", 31 + "required": ["type", "entries"], 32 + "properties": { 33 + "type": { "type": "string", "const": "directory" }, 34 + "entries": { 35 + "type": "array", 36 + "maxLength": 500, 37 + "items": { "type": "ref", "ref": "#entry" } 38 + } 39 + } 40 + }, 41 + "entry": { 42 + "type": "object", 43 + "required": ["name", "node"], 44 + "properties": { 45 + "name": { "type": "string", "maxLength": 255 }, 46 + "node": { "type": "union", "refs": ["#file", "#directory", "#subfs"] } 47 + } 48 + }, 49 + "subfs": { 50 + "type": "object", 51 + "required": ["type", "subject"], 52 + "properties": { 53 + "type": { "type": "string", "const": "subfs" }, 54 + "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to another place.wisp.subfs record for nested subtrees" } 55 + } 56 + } 57 + } 58 + } 59 +
+107
src/lexicons/types/place/wisp/subfs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'place.wisp.subfs' 12 + 13 + export interface Main { 14 + $type: 'place.wisp.subfs' 15 + root: Directory 16 + fileCount?: number 17 + createdAt: string 18 + [k: string]: unknown 19 + } 20 + 21 + const hashMain = 'main' 22 + 23 + export function isMain<V>(v: V) { 24 + return is$typed(v, id, hashMain) 25 + } 26 + 27 + export function validateMain<V>(v: V) { 28 + return validate<Main & V>(v, id, hashMain, true) 29 + } 30 + 31 + export { 32 + type Main as Record, 33 + isMain as isRecord, 34 + validateMain as validateRecord, 35 + } 36 + 37 + export interface File { 38 + $type?: 'place.wisp.subfs#file' 39 + type: 'file' 40 + /** Content blob ref */ 41 + blob: BlobRef 42 + /** Content encoding (e.g., gzip for compressed files) */ 43 + encoding?: 'gzip' 44 + /** Original MIME type before compression */ 45 + mimeType?: string 46 + /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */ 47 + base64?: boolean 48 + } 49 + 50 + const hashFile = 'file' 51 + 52 + export function isFile<V>(v: V) { 53 + return is$typed(v, id, hashFile) 54 + } 55 + 56 + export function validateFile<V>(v: V) { 57 + return validate<File & V>(v, id, hashFile) 58 + } 59 + 60 + export interface Directory { 61 + $type?: 'place.wisp.subfs#directory' 62 + type: 'directory' 63 + entries: Entry[] 64 + } 65 + 66 + const hashDirectory = 'directory' 67 + 68 + export function isDirectory<V>(v: V) { 69 + return is$typed(v, id, hashDirectory) 70 + } 71 + 72 + export function validateDirectory<V>(v: V) { 73 + return validate<Directory & V>(v, id, hashDirectory) 74 + } 75 + 76 + export interface Entry { 77 + $type?: 'place.wisp.subfs#entry' 78 + name: string 79 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 80 + } 81 + 82 + const hashEntry = 'entry' 83 + 84 + export function isEntry<V>(v: V) { 85 + return is$typed(v, id, hashEntry) 86 + } 87 + 88 + export function validateEntry<V>(v: V) { 89 + return validate<Entry & V>(v, id, hashEntry) 90 + } 91 + 92 + export interface Subfs { 93 + $type?: 'place.wisp.subfs#subfs' 94 + type: 'subfs' 95 + /** AT-URI pointing to another place.wisp.subfs record for nested subtrees */ 96 + subject: string 97 + } 98 + 99 + const hashSubfs = 'subfs' 100 + 101 + export function isSubfs<V>(v: V) { 102 + return is$typed(v, id, hashSubfs) 103 + } 104 + 105 + export function validateSubfs<V>(v: V) { 106 + return validate<Subfs & V>(v, id, hashSubfs) 107 + }