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

update place.wisp.fs lexicon to support subfs nodes

Adds 'subfs' as a new node type in directory entries. A subfs node
contains a subject URI pointing to a place.wisp.subfs record that
holds the actual directory content.

This allows the main manifest to reference external records instead
of embedding large directory trees directly.

Changed files
+350 -17
hosting-service
src
lexicon
types
place
wisp
lexicons
src
lexicons
types
place
wisp
+135 -2
hosting-service/src/lexicon/lexicons.ts
··· 51 51 blob: { 52 52 type: 'blob', 53 53 accept: ['*/*'], 54 - maxSize: 1000000, 54 + maxSize: 1000000000, 55 55 description: 'Content blob ref', 56 56 }, 57 57 encoding: { ··· 98 98 }, 99 99 node: { 100 100 type: 'union', 101 - refs: ['lex:place.wisp.fs#file', 'lex:place.wisp.fs#directory'], 101 + refs: [ 102 + 'lex:place.wisp.fs#file', 103 + 'lex:place.wisp.fs#directory', 104 + 'lex:place.wisp.fs#subfs', 105 + ], 106 + }, 107 + }, 108 + }, 109 + subfs: { 110 + type: 'object', 111 + required: ['type', 'subject'], 112 + properties: { 113 + type: { 114 + type: 'string', 115 + const: 'subfs', 116 + }, 117 + subject: { 118 + type: 'string', 119 + format: 'at-uri', 120 + description: 121 + 'AT-URI pointing to a place.wisp.subfs record containing this subtree', 122 + }, 123 + }, 124 + }, 125 + }, 126 + }, 127 + PlaceWispSubfs: { 128 + lexicon: 1, 129 + id: 'place.wisp.subfs', 130 + defs: { 131 + main: { 132 + type: 'record', 133 + description: 134 + 'Virtual filesystem manifest within a place.wisp.fs record', 135 + record: { 136 + type: 'object', 137 + required: ['root', 'createdAt'], 138 + properties: { 139 + root: { 140 + type: 'ref', 141 + ref: 'lex:place.wisp.subfs#directory', 142 + }, 143 + fileCount: { 144 + type: 'integer', 145 + minimum: 0, 146 + maximum: 1000, 147 + }, 148 + createdAt: { 149 + type: 'string', 150 + format: 'datetime', 151 + }, 152 + }, 153 + }, 154 + }, 155 + file: { 156 + type: 'object', 157 + required: ['type', 'blob'], 158 + properties: { 159 + type: { 160 + type: 'string', 161 + const: 'file', 162 + }, 163 + blob: { 164 + type: 'blob', 165 + accept: ['*/*'], 166 + maxSize: 1000000000, 167 + description: 'Content blob ref', 168 + }, 169 + encoding: { 170 + type: 'string', 171 + enum: ['gzip'], 172 + description: 'Content encoding (e.g., gzip for compressed files)', 173 + }, 174 + mimeType: { 175 + type: 'string', 176 + description: 'Original MIME type before compression', 177 + }, 178 + base64: { 179 + type: 'boolean', 180 + description: 181 + 'True if blob content is base64-encoded (used to bypass PDS content sniffing)', 182 + }, 183 + }, 184 + }, 185 + directory: { 186 + type: 'object', 187 + required: ['type', 'entries'], 188 + properties: { 189 + type: { 190 + type: 'string', 191 + const: 'directory', 192 + }, 193 + entries: { 194 + type: 'array', 195 + maxLength: 500, 196 + items: { 197 + type: 'ref', 198 + ref: 'lex:place.wisp.subfs#entry', 199 + }, 200 + }, 201 + }, 202 + }, 203 + entry: { 204 + type: 'object', 205 + required: ['name', 'node'], 206 + properties: { 207 + name: { 208 + type: 'string', 209 + maxLength: 255, 210 + }, 211 + node: { 212 + type: 'union', 213 + refs: [ 214 + 'lex:place.wisp.subfs#file', 215 + 'lex:place.wisp.subfs#directory', 216 + 'lex:place.wisp.subfs#subfs', 217 + ], 218 + }, 219 + }, 220 + }, 221 + subfs: { 222 + type: 'object', 223 + required: ['type', 'subject'], 224 + properties: { 225 + type: { 226 + type: 'string', 227 + const: 'subfs', 228 + }, 229 + subject: { 230 + type: 'string', 231 + format: 'at-uri', 232 + description: 233 + 'AT-URI pointing to another place.wisp.subfs record for nested subtrees', 102 234 }, 103 235 }, 104 236 }, ··· 138 270 139 271 export const ids = { 140 272 PlaceWispFs: 'place.wisp.fs', 273 + PlaceWispSubfs: 'place.wisp.subfs', 141 274 } as const
+31 -8
hosting-service/src/lexicon/types/place/wisp/fs.ts
··· 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 4 import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats' 5 + import { CID } from 'multiformats/cid' 6 6 import { validate as _validate } from '../../../lexicons' 7 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 8 ··· 10 10 validate = _validate 11 11 const id = 'place.wisp.fs' 12 12 13 - export interface Record { 13 + export interface Main { 14 14 $type: 'place.wisp.fs' 15 15 site: string 16 16 root: Directory ··· 19 19 [k: string]: unknown 20 20 } 21 21 22 - const hashRecord = 'main' 22 + const hashMain = 'main' 23 23 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 24 + export function isMain<V>(v: V) { 25 + return is$typed(v, id, hashMain) 26 26 } 27 27 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 28 + export function validateMain<V>(v: V) { 29 + return validate<Main & V>(v, id, hashMain, true) 30 + } 31 + 32 + export { 33 + type Main as Record, 34 + isMain as isRecord, 35 + validateMain as validateRecord, 30 36 } 31 37 32 38 export interface File { ··· 71 77 export interface Entry { 72 78 $type?: 'place.wisp.fs#entry' 73 79 name: string 74 - node: $Typed<File> | $Typed<Directory> | { $type: string } 80 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 75 81 } 76 82 77 83 const hashEntry = 'entry' ··· 83 89 export function validateEntry<V>(v: V) { 84 90 return validate<Entry & V>(v, id, hashEntry) 85 91 } 92 + 93 + export interface Subfs { 94 + $type?: 'place.wisp.fs#subfs' 95 + type: 'subfs' 96 + /** AT-URI pointing to a place.wisp.subfs record containing this subtree */ 97 + subject: string 98 + } 99 + 100 + const hashSubfs = 'subfs' 101 + 102 + export function isSubfs<V>(v: V) { 103 + return is$typed(v, id, hashSubfs) 104 + } 105 + 106 + export function validateSubfs<V>(v: V) { 107 + return validate<Subfs & V>(v, id, hashSubfs) 108 + }
+11 -4
lexicons/fs.json
··· 21 21 "required": ["type", "blob"], 22 22 "properties": { 23 23 "type": { "type": "string", "const": "file" }, 24 - "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000, "description": "Content blob ref" }, 24 + "blob": { "type": "blob", "accept": ["*/*"], "maxSize": 1000000000, "description": "Content blob ref" }, 25 25 "encoding": { "type": "string", "enum": ["gzip"], "description": "Content encoding (e.g., gzip for compressed files)" }, 26 26 "mimeType": { "type": "string", "description": "Original MIME type before compression" }, 27 - "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } 28 - } 27 + "base64": { "type": "boolean", "description": "True if blob content is base64-encoded (used to bypass PDS content sniffing)" } } 29 28 }, 30 29 "directory": { 31 30 "type": "object", ··· 44 43 "required": ["name", "node"], 45 44 "properties": { 46 45 "name": { "type": "string", "maxLength": 255 }, 47 - "node": { "type": "union", "refs": ["#file", "#directory"] } 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 a place.wisp.subfs record containing this subtree" } 48 55 } 49 56 } 50 57 }
+149 -2
src/lexicons/lexicons.ts
··· 51 51 blob: { 52 52 type: 'blob', 53 53 accept: ['*/*'], 54 - maxSize: 1000000, 54 + maxSize: 1000000000, 55 55 description: 'Content blob ref', 56 + }, 57 + encoding: { 58 + type: 'string', 59 + enum: ['gzip'], 60 + description: 'Content encoding (e.g., gzip for compressed files)', 61 + }, 62 + mimeType: { 63 + type: 'string', 64 + description: 'Original MIME type before compression', 65 + }, 66 + base64: { 67 + type: 'boolean', 68 + description: 69 + 'True if blob content is base64-encoded (used to bypass PDS content sniffing)', 56 70 }, 57 71 }, 58 72 }, ··· 84 98 }, 85 99 node: { 86 100 type: 'union', 87 - refs: ['lex:place.wisp.fs#file', 'lex:place.wisp.fs#directory'], 101 + refs: [ 102 + 'lex:place.wisp.fs#file', 103 + 'lex:place.wisp.fs#directory', 104 + 'lex:place.wisp.fs#subfs', 105 + ], 106 + }, 107 + }, 108 + }, 109 + subfs: { 110 + type: 'object', 111 + required: ['type', 'subject'], 112 + properties: { 113 + type: { 114 + type: 'string', 115 + const: 'subfs', 116 + }, 117 + subject: { 118 + type: 'string', 119 + format: 'at-uri', 120 + description: 121 + 'AT-URI pointing to a place.wisp.subfs record containing this subtree', 122 + }, 123 + }, 124 + }, 125 + }, 126 + }, 127 + PlaceWispSubfs: { 128 + lexicon: 1, 129 + id: 'place.wisp.subfs', 130 + defs: { 131 + main: { 132 + type: 'record', 133 + description: 134 + 'Virtual filesystem manifest within a place.wisp.fs record', 135 + record: { 136 + type: 'object', 137 + required: ['root', 'createdAt'], 138 + properties: { 139 + root: { 140 + type: 'ref', 141 + ref: 'lex:place.wisp.subfs#directory', 142 + }, 143 + fileCount: { 144 + type: 'integer', 145 + minimum: 0, 146 + maximum: 1000, 147 + }, 148 + createdAt: { 149 + type: 'string', 150 + format: 'datetime', 151 + }, 152 + }, 153 + }, 154 + }, 155 + file: { 156 + type: 'object', 157 + required: ['type', 'blob'], 158 + properties: { 159 + type: { 160 + type: 'string', 161 + const: 'file', 162 + }, 163 + blob: { 164 + type: 'blob', 165 + accept: ['*/*'], 166 + maxSize: 1000000000, 167 + description: 'Content blob ref', 168 + }, 169 + encoding: { 170 + type: 'string', 171 + enum: ['gzip'], 172 + description: 'Content encoding (e.g., gzip for compressed files)', 173 + }, 174 + mimeType: { 175 + type: 'string', 176 + description: 'Original MIME type before compression', 177 + }, 178 + base64: { 179 + type: 'boolean', 180 + description: 181 + 'True if blob content is base64-encoded (used to bypass PDS content sniffing)', 182 + }, 183 + }, 184 + }, 185 + directory: { 186 + type: 'object', 187 + required: ['type', 'entries'], 188 + properties: { 189 + type: { 190 + type: 'string', 191 + const: 'directory', 192 + }, 193 + entries: { 194 + type: 'array', 195 + maxLength: 500, 196 + items: { 197 + type: 'ref', 198 + ref: 'lex:place.wisp.subfs#entry', 199 + }, 200 + }, 201 + }, 202 + }, 203 + entry: { 204 + type: 'object', 205 + required: ['name', 'node'], 206 + properties: { 207 + name: { 208 + type: 'string', 209 + maxLength: 255, 210 + }, 211 + node: { 212 + type: 'union', 213 + refs: [ 214 + 'lex:place.wisp.subfs#file', 215 + 'lex:place.wisp.subfs#directory', 216 + 'lex:place.wisp.subfs#subfs', 217 + ], 218 + }, 219 + }, 220 + }, 221 + subfs: { 222 + type: 'object', 223 + required: ['type', 'subject'], 224 + properties: { 225 + type: { 226 + type: 'string', 227 + const: 'subfs', 228 + }, 229 + subject: { 230 + type: 'string', 231 + format: 'at-uri', 232 + description: 233 + 'AT-URI pointing to another place.wisp.subfs record for nested subtrees', 88 234 }, 89 235 }, 90 236 }, ··· 124 270 125 271 export const ids = { 126 272 PlaceWispFs: 'place.wisp.fs', 273 + PlaceWispSubfs: 'place.wisp.subfs', 127 274 } as const
+24 -1
src/lexicons/types/place/wisp/fs.ts
··· 40 40 type: 'file' 41 41 /** Content blob ref */ 42 42 blob: BlobRef 43 + /** Content encoding (e.g., gzip for compressed files) */ 44 + encoding?: 'gzip' 45 + /** Original MIME type before compression */ 46 + mimeType?: string 47 + /** True if blob content is base64-encoded (used to bypass PDS content sniffing) */ 48 + base64?: boolean 43 49 } 44 50 45 51 const hashFile = 'file' ··· 71 77 export interface Entry { 72 78 $type?: 'place.wisp.fs#entry' 73 79 name: string 74 - node: $Typed<File> | $Typed<Directory> | { $type: string } 80 + node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string } 75 81 } 76 82 77 83 const hashEntry = 'entry' ··· 83 89 export function validateEntry<V>(v: V) { 84 90 return validate<Entry & V>(v, id, hashEntry) 85 91 } 92 + 93 + export interface Subfs { 94 + $type?: 'place.wisp.fs#subfs' 95 + type: 'subfs' 96 + /** AT-URI pointing to a place.wisp.subfs record containing this subtree */ 97 + subject: string 98 + } 99 + 100 + const hashSubfs = 'subfs' 101 + 102 + export function isSubfs<V>(v: V) { 103 + return is$typed(v, id, hashSubfs) 104 + } 105 + 106 + export function validateSubfs<V>(v: V) { 107 + return validate<Subfs & V>(v, id, hashSubfs) 108 + }