import { ServerConfig , ServerConfig_AccessOptions , ServerConfig_PutSaver , Config , Schema , ServerConfig_Controller , ServerConfigSchema , defaultPermissions , ServerConfig_AuthAccountsValue , } from "./server-config" ; import { as } from "./server-types" ; import { type } from 'os' ; interface Description < V > { $description : string ; $items ? : DescObj < V > ; $default ? : NonNullable < V > ; $ref ? : string ; } type Primitive = string | number | symbol | false | true | undefined ; type DescItem < V > = V extends Primitive ? string : V extends Array < infer X > ? Description < NonNullable < X > > : string extends keyof V ? Description < V [ keyof V ] > : Description < V > ; type DescObj < V > = { [ K in keyof V ] - ? : DescItem < NonNullable < V [ K ] > > ; } ; let descriptions = { $schema : "The JSON schema location for this document. This schema is generated directly from the TypeScript interfaces used in TiddlyServer. A text-editor with autocomplete, such as VS code, will make editing this file much simpler. Most fields include a description like this one. \n\nAll relative paths in this file are resolved relative to this file, so `./settings-tree.xml` refers to an XML file in the same folder as this file. All relative paths in included files (such as the XML file) are resolved relative to the included file." , authCookieAge : "Age to set for the auth cookie (default is 30 days)\n- 24 hours: 86400\n- 7 days: 604800\n- 30 days: 2592000\n- 60 days: 5184000\n- 90 days: 7776000\n- 120 days: 10368000\n- 150 days: 12950000\n- 180 days: 15552000" , debugLevel : "- 4: Errors that require the process to exit for restart \n- 3: Major errors that are handled and do not require a server restart \n- 2: Warnings or errors that do not alter the program flow but need to be marked (minimum for status 500) \n- 1: Info - Most startup messages \n- 0: Normal debug messages and all software and request-side error messages \n- -1: Detailed debug messages from high level apis \n- -2: Response status messages and error response data \n- -3: Request and response data for all messages (verbose) \n- -4: Protocol details and full data dump (such as encryption steps and keys)" , maxTransferRequests : "" , tree : { $description : "The tree property accepts one of 3 formats. \n\n- If it is a string ending in .xml, .js, or .json, the tree will be loaded from the specified path. JS and JSON files must export a tree property and XML files must specify a tree element as root. \n\n- A path element (or a string specifying the path) to mount a path as root (a single file is possible but pointless). \n\n- A group element or the children of a group element (which is either an array, or an object with no $element property)" , } , authAccounts : { $description : "" , $items : { clientKeys : { $description : "" , $items : { publicKey : "" , cookieSalt : "" } } , permissions : { $description : "" , $ref : "" } } , } , bindInfo : { $description : "bind address and port info" , $items : { port : "port to listen on, default is 8080 for http and 8443 for https" , _bindLocalhost : "always bind a separate server instance to 127.0.0.1 regardless of any other settings" , bindAddress : { $description : "An array of IP addresses to accept requests on. Can be any IP address assigned to the machine. Default is \"127.0.0.1\".\n\nIf `bindWildcard` is true, each connection is checked individually. Otherwise, the server listens on the specified IP addresses and accepts all connections from the operating system. If an IP address cannot be bound, the server skips it unless `--bindAddressRequired` is specified\n\nIf `filterBindAddress` is true, IPv4 addresses may include a subnet mask, (e.g. `/24`) which matches any interface IP address in that range. Prefix with a minus sign (-) to block requests incoming to that IP address or range." } , bindWildcard : "Bind to the wildcard addresses `0.0.0.0` and `::` (if enabled) in that order. The default is `true`. In many cases this is preferred, however Android does not support this for some reason. On Android, set this to `false` and set host to `[\"0.0.0.0/0\"]` to bind to all IPv4 addresses." , enableIPv6 : "Bind to the IPv6 wildcard as well if `bindWilcard` is true and allow requests incoming to IPv6 addresses if not explicitly denied." , filterBindAddress : "IPv4 addresses may include a subnet mask, (e.g. `/24`) which matches any IP address in that range. Prefix with a minus sign (-) to block requests incoming to that IP address or range." , https : "https-only options: a string to a JavaScript file which exports a function of type `(iface:string) => https.ServerOptions`. Note that the initServer function will change this to a boolean value indicating whether https is in use once inside TiddlyServer." , localAddressPermissions : { $description : "Permissions based on local interface address.  Enter the IP Address and NetMask (`127.0.0.1/8`) as the property key. The keyword \"localhost\" (if specified) matches 127.0.0.0/8 instead of any other specified key.  Keyword \"*\" matches everything that doesn't match another IP address.  This checks the IP address each client connects to (socket.localAddress), not the bind address of the server instance that accepted the request. The keyword defaultPermission does nothing, but auto-complete should give you the default object.  You can then rename it to whatever you need it to be. " , $default : { "writeErrors" : false , "mkdir" : false , "upload" : false , "websockets" : false , "registerNotice" : true , "putsaver" : true , "loginlink" : true , "transfer" : false , "datafolder" : true } , $items : { datafolder : "Whether clients may access data folders, which gives them full access to the system as the user that owns the server process because data folders can be easily modified to execute code on the server. This returns a 403 Access Denied if a data folder is detected. It does not serve the files inside the data folder as a regular folder. For that you need to use the noDataFolder attribute on the folder in the tree." , loginlink : "Whether to include a link to the login page when returning auth errors" , mkdir : "Whether clients may create new directories and datafolders inside existing directories served by TiddlyWiki" , putsaver : "Whether clients may use the put saver, allowing any file served within the tree (not assets) to be overwritten, not just TiddlyWiki files. The put saver cannot save to data folders regardless of this setting." , registerNotice : "Whether login attempts for a public/private key pair which has not been registered will be logged at debug level 2 with the full public key which can be copied into an authAccounts entry. Turn this off if you get spammed with login attempts." , transfer : "Allows clients to use a custom TiddlyServer feature which relays a connection between two clients. " , upload : "Whether clients may upload files to directories (not data folders)." , websockets : "Whether clients may open websocket connections." , writeErrors : "Whether to write status 500 errors to the browser, possibly including stack traces." } } , } } , controllers : { $description : "" } , directoryIndex : { $description : "" } , putsaver : { $description : "" } , datafolder : { $description : "Options object whose properties will be passed to the tiddlywiki server instance using the spread operator. If a property specifies an object instead of a string, the object will be shared between all instances." } , _datafolderclient : "The tiddlywiki folder to serve on `/assets/tiddlywiki/`" , _datafolderserver : "The tiddlywiki folder to use for data folder instances." , _datafoldertarget : "Deprecated: Use _datafolderserver instead." , _devmode : "enables certain expensive per-request checks" , } ; type Reffer = Record < string , ( ) => TypeCheck < any > > ; abstract class TypeCheck < T > { static currentKeyArray : ( string | number ) [ ] = [ ] ; static stack : TypeCheck < any > [ ] = [ ] ; public abstract expectedMessage : string ; public abstract error : Record < string , any > | string | undefined ; protected abstract currentKey : string | number | symbol | undefined ; protected description : string = "" ; protected defData : any = undefined ; /** Chainable method to set schema description */ public describe ( description ) { this . description = description ; return this ; } /** Chainable method to set schema default */ public defaultData ( def ) { this . defData = def ; return this ; } protected abstract _check ( a , reffer ) ; private errMessage = ( err ) => JSON . stringify ( TypeCheck . currentKeyArray ) + " " + err + "\n" ; public check ( a , reffer ) { let parent = TypeCheck . stack [ TypeCheck . stack . length - 1 ] ; TypeCheck . stack . push ( this ) ; this . currentKey = undefined ; let res = false ; if ( ! parent ) { res = this . _check ( a , reffer ) ; } else if ( parent instanceof CheckMultiple || parent instanceof CheckObject || parent instanceof CheckUnion ) { let key = parent . currentKey ; res = this . _check ( a , reffer ) ; if ( ! res && typeof parent . error === "object" ) { parent . error [ key ] = this . error ; } else if ( ! res ) { throw new Error ( "parent.error is a string. This is a bug in one of the instanceof specified classes." ) ; } } else if ( parent instanceof CheckSimple ) { throw new Error ( "CheckSimple instances may not call other checkers. " ) ; } else if ( parent instanceof CheckRef ) { res = this . _check ( a , reffer ) ; if ( ! res ) parent . error = this . error ; } else { throw new Error ( "unhandled instance " + this . toString ( ) ) ; } TypeCheck . stack . pop ( ) ; this . currentKey = undefined ; return res ; } public abstract getSchema ( reffer ) ; static errorMessage ( key ) { switch ( key ) { case CheckObject . wrongUnionKey : return ( co , x ) => "wrong union key " + co . unionKeys ?. map ( k => k + ": " + x [ k ] + " " + co . checkermap [ k ] . expectedMessage ) . join ( ", " ) ; case CheckObject . typeofNotObject : return ( co , x ) => "expected object value but got " + typeof x ; case CheckObject . missingRequired : return ( co , x ) => co . lastMessage ; case CheckObject . unexpectedProperty : return ( co , x ) => co . expectedMessage + " but got " + JSON . stringify ( Object . keys ( x ) ) ; default : return ( tc , x ) => tc . expectedMessage ; } } } class CheckSimple < T > extends TypeCheck < T > { public error : string = "" ; protected currentKey : undefined = undefined ; protected _check : ( a : any ) => a is T ; constructor ( public expectedMessage , check , public schema ) { super ( ) ; this . _check = check ; this . error = this . expectedMessage ; } getSchema ( ) { return { type : this . schema . type , enum : this . schema . enum , default : this . defData , description : this . description || undefined } ; } } export const checkString = ( ) => new CheckSimple ( "expected a string value" , ( a ) : a is string => typeof a === "string" , { type : "string" } ) ; export const checkStringEnum = < T extends string > ( val ) => new CheckSimple ( "expected one string of " + JSON . stringify ( val ) , ( a ) : a is T => typeof a === "string" && ( val as readonly string [ ] ) . indexOf ( a ) !== - 1 , { type : "string" , enum : val } ) ; export const checkStringNotEmpty = ( ) => new CheckSimple ( "expected a string with non-zero length" , ( a ) : a is string => typeof a === "string" && a . length > 0 , { type : "string" , expectNotEmpty : true } ) ; export const checkStringRegex = ( reg ) => new CheckSimple ( "expected a string that matches the regex " + reg . source , ( a ) : a is string => typeof a === "string" && reg . test ( a ) , { type : "string" , regex : reg . source } ) ; export const checkNumber = ( ) => new CheckSimple ( "expected a number value" , ( a ) : a is number => typeof a === "number" , { type : "number" } ) ; export const checkNumberEnum = < T extends number > ( val ) => new CheckSimple ( "expected one number of " + JSON . stringify ( val ) , ( a ) : a is T => typeof a === "number" && ( val as readonly number [ ] ) . indexOf ( a ) !== - 1 , { type : "number" , enum : val } ) ; export const checkBoolean = ( ) => new CheckSimple ( "expected a boolean value" , ( a ) : a is boolean => typeof a === "boolean" , { type : "boolean" } ) ; export const checkBooleanTrue = ( ) => new CheckSimple ( "expected a boolean true" , ( a ) : a is true => typeof a === "boolean" && a === true , { type : "boolean" , enum : [ true ] } ) ; export const checkBooleanFalse = ( ) => new CheckSimple ( "expected a boolean false" , ( a ) : a is false => typeof a === "boolean" && a === false , { type : "boolean" , enum : [ false ] } ) ; export const checkNull = ( ) => new CheckSimple ( "expected a null value" , ( a ) : a is null => typeof a === "object" && a === null , { type : "null" } ) ; export const checkUndefined = ( ) => new CheckSimple ( "expected a null value" , ( a ) : a is undefined => typeof a === "undefined" , { type : "null" } ) ; export const checkAny = ( ) => new CheckSimple ( "expected any value" , ( a ) : a is any => true , { type : "object" } ) ; export class CheckMultiple < T extends { } > extends TypeCheck < T > { error : Record < string , any > | string = { } as any ; currentKey : any ; protected _check = ( a , reffer ) : a is T => { this . currentKey = undefined ; this . error = { } ; let res = this . checkObject ( a , reffer ) ; if ( ! res ) return ( this . error = this . expectedMessage ) , res ; else return this . checkChildren ( a , reffer , k => { this . currentKey = k ; } ) ; } ; constructor ( public expectedMessage , private checkObject , private checkChildren , private schema , private childType , private patterProperties ? ) { super ( ) ; } getSchema ( reffer ) { switch ( this . schema ) { case "record" : return { type : "object" , default : this . defData , description : this . description || undefined , ... ( this . patterProperties ? { patternProperties : { [ this . patterProperties ] : this . childType . getSchema ( reffer ) } } : { additionalProperties : this . childType . getSchema ( reffer ) } ) } ; case "array" : return { items : this . childType . getSchema ( reffer ) , type : "array" , default : this . defData , description : this . description || undefined } ; } } } type ArrayType < T > = T extends Array < infer X > ? X : never ; export const checkArray = < V > ( checker ) => new CheckMultiple ( "expected an array that " + checker . expectedMessage , ( a , reffer ) : a is V [ ] => typeof a === "object" && Array . isArray ( a ) , ( a , reffer , curKey ) : a is V [ ] => a . filter ( ( b , i ) => { curKey ( i ) ; return checker . check ( b , reffer ) ; } ) . length === a . length , "array" , checker ) ; export const checkRecord = < K extends string , V > ( keyChecker , checker ) => new CheckMultiple < { [ k in K ] : V ; } > ( "expected a record that " + checker . expectedMessage , ( a , reffer ) : a is { [ k in K ] : V ; } => typeof a === "object" , ( a , reffer , curKey ) : a is { [ k in K ] : V ; } => { let keys = Object . keys ( a ) ; let arr = keys . filter ( k => { curKey ( k ) ; return keyChecker . check ( k , reffer ) && checker . check ( a [ k ] , reffer ) ; } ) ; return arr . length === keys . length ; } , "record" , checker , keyChecker instanceof CheckSimple ? keyChecker . schema . regex : undefined ) ; class CheckUnionWrapper < A , B > extends TypeCheck < A | B > { currentKey : number | undefined = undefined ; error : any [ ] = [ ] ; _check : ( x : any ) => x is A | B = ( a ) : a is A | B => { throw new Error ( "incorrect usage of CheckUnionWrapper" ) ; } ; get expectedMessage ( ) : string { throw new Error ( "incorrect usage of CheckUnionWrapper" ) ; } getSchema ( ) { throw new Error ( "incorrect usage of CheckUnionWrapper" ) ; } constructor ( public checkerA , public checkerB ) { super ( ) ; } } class CheckUnion < T > extends TypeCheck < T > { currentKey : string | number | undefined = undefined ; error : any [ ] ; expectedMessage : string = "" ; constructor ( public checks , public oneof ) { super ( ) ; this . error = new Array ( this . checks . length ) ; this . expectedMessage = checks . map ( e => e . expectedMessage ) . join ( ", " ) ; if ( this . checks . filter ( e => e instanceof CheckUnion ) . length > 0 ) throw new Error ( "A checkUnion as a direct child of a checkUnion is not supported. Use checkUnion.cu to nest unions instead." ) ; } _check = ( a , reffer ) : a is T => { this . currentKey = undefined ; this . error = new Array ( this . checks . length ) ; let res = this . checks . map ( ( e , i ) => { this . currentKey = i ; let res = e . check ( a , reffer ) ; let err = e . error ; return [ res , err ] as const ; } ) ; let is = res . filter ( e => e [ 0 ] ) . length > 0 ; return is ; } ; getSchema ( reffer ) { return { default : this . defData , description : this . description || undefined , [ this . oneof ? "oneOf" : "anyOf" ] : this . checks . map ( e => e . getSchema ( reffer ) ) } ; } } function flattenWrapper ( c ) { if ( c instanceof CheckUnionWrapper ) return [ ... flattenWrapper ( c . checkerA ) , ... flattenWrapper ( c . checkerB ) ] ; else return [ c ] ; } /**
 * The error message of a union will be an array.
 * False indicates there were no errors for that branch (i.e. it passed)
 */ export const checkUnion = < A , B > ( ca , cb , oneOf = false ) => { let checks = flattenWrapper ( new CheckUnionWrapper ( ca , cb ) ) . filter ( ( e ) : e is NonNullable < typeof e > => ! ! e ) ; return new CheckUnion < A | B > ( checks , oneOf ) ; } ; export const checkUnionArray = < T > ( checks , oneof ) => { return new CheckUnion < T > ( checks , oneof ) ; } ; checkUnion . cu = < A , B > ( ca , cb ) => new CheckUnionWrapper ( ca , cb ) ; class CheckObject < T extends { } > extends TypeCheck < T > { private required = Object . keys ( this . checkermap ) ; private optional = Object . keys ( this . optionalcheckermap ) ; constructor ( public description , public checkermap , public optionalcheckermap , /** if these keys do not pass, the item is assumed to be unrelated */ public unionKeys ? ) { super ( ) ; this . expectedMessage = "expected an object with keys " + [ ... Object . keys ( checkermap ) . map ( e => JSON . stringify ( e ) ) , ... Object . keys ( optionalcheckermap ) . map ( e => JSON . stringify ( e ) + "?" ) , ] . join ( "," ) ; if ( this . unionKeys ) this . unionKeys . forEach ( k => { if ( this . required . indexOf ( k ) === - 1 ) throw new Error ( "unionKey not found in checkermap " + k ) ; } ) ; } getSchema ( reffer ) { const properties = { } ; this . required . forEach ( k => { properties [ k ] = this . checkermap [ k ] . getSchema ( reffer ) ; } ) ; this . optional . forEach ( k => { properties [ k ] = this . optionalcheckermap [ k ] . getSchema ( reffer ) ; } ) ; return { type : "object" , additionalProperties : false , properties , required : this . required , default : this . defData , description : this . description || undefined } ; } public expectedMessage : string ; public lastResult ? : symbol = CheckObject . typeofNotObject ; public lastMessage : string = "" ; public currentKey : string | number | undefined = undefined ; public error : Record < string , any > | string | undefined = { } ; currentKeyArray : string [ ] = [ ] ; private symbolError ( symbol , value , missingkeys ? ) { this . lastResult = symbol ; if ( missingkeys ) this . lastMessage = "missing required keys " + missingkeys . join ( "," ) ; this . error = symbol === CheckObject . wrongUnionKey ? undefined : TypeCheck . errorMessage ( symbol ) ( this , value ) ; return false ; } protected _check ( a , reffer ) { this . lastResult = undefined ; this . currentKey = undefined ; this . lastMessage = "" ; this . error = { } ; if ( typeof a !== "object" ) return this . symbolError ( CheckObject . typeofNotObject , a ) ; const keys = Object . keys ( a ) ; const checkKeys = [ ... this . required ] ; this . optional . forEach ( k => { if ( checkKeys . indexOf ( k ) === - 1 ) checkKeys . push ( k ) ; } ) ; let wrongunionkey = this . unionKeys && ! ( this . unionKeys . filter ( k => { let res = keys . indexOf ( k ) !== - 1 && this . checkermap [ k ] . check ( a [ k ] , reffer ) ; return res ; } ) . length === this . unionKeys . length ) ; if ( wrongunionkey ) return this . symbolError ( CheckObject . wrongUnionKey , a ) ; let missingkeys = this . required . filter ( k => keys . indexOf ( k ) === - 1 ) ; if ( missingkeys . length ) return this . symbolError ( CheckObject . missingRequired , a , missingkeys ) ; let extraKeys = keys . filter ( e => checkKeys . indexOf ( e ) === - 1 ) ; if ( extraKeys . length ) return this . symbolError ( CheckObject . unexpectedProperty , a ) ; return ( keys . filter ( ( k ) : boolean => { const keylog = [ ] ; let res = false ; if ( this . checkermap [ k ] ) { this . currentKey = k ; res = this . checkermap [ k ] . check ( a [ k ] , reffer ) ; } else if ( this . optionalcheckermap [ k ] ) { this . currentKey = k ; res = this . optionalcheckermap [ k ] . check ( a [ k ] , reffer ) ; } else { this . currentKey = k ; res = false ; throw new Error ( "Something went wrong and an extra key was found. This is a bug in the interface checker." ) ; } return res ; } ) . length === keys . length ) ; } static wrongUnionKey = Symbol ( "unrelated union key" ) ; static typeofNotObject = Symbol ( "typeof not object" ) ; static missingRequired = Symbol ( "missing required keys" ) ; static unexpectedProperty = Symbol ( "property is unexpected" ) ; } type OptionalCheckermap < T extends { } , REQUIRED extends keyof T > = { [ KEY in Exclude < keyof T , REQUIRED > ] - ? : TypeCheck < T [ KEY ] > ; } ; export function checkResult ( e , a , reffer ) { let union = new CheckUnion ( [ e ] , false ) ; let res = union . check ( a , reffer ) ; return [ res , union . error ? union . error [ 0 ] : undefined ] as const ; } type Defined < T > = T extends undefined ? never : T ; type ExcludeNever < T > = { [ K in keyof T ] : T [ K ] extends never ? never : K ; } ; type r = NonNullable < ServerConfigSchema [ "directoryIndex" ] > ; type t = ExcludeNever < NonNullable < ServerConfigSchema [ "directoryIndex" ] > > ; /** checkermap parameter is type never if T extends undefined */ export function checkObject < T , REQUIRED extends keyof T = keyof ExcludeNever < T > > ( description , checkermap , optionalcheckermap = { } as any , unionKeys = [ ] ) { return new CheckObject < T > ( description , checkermap , optionalcheckermap , unionKeys ) ; } class CheckRef < T > extends TypeCheck < T > { public error : string | Record < string , any > | undefined = { } ; public currentKey : string | number | symbol | undefined ; protected _check ( a , reffer ) { this . error = { } ; if ( ! reffer [ this . refKey ] ) console . log ( reffer , this . refKey ) ; return reffer [ this . refKey ] ( ) . check ( a , reffer ) ; } constructor ( private refKey , public expectedMessage ) { super ( ) ; } getSchema ( ) { return { $ref : "#/definitions/" + this . refKey } ; } } /**
 * Allows recursive types to be checked. The recursive type
 * should be the result of a function so each checker will
 * be a unique instance. The callback will be called on each
 * recursion.
 */ export const checkRef = < T extends Reffer > ( ) => < K extends keyof T > ( refKey , expected ) => { return new CheckRef < ReturnType < T [ K ] > extends TypeCheck < infer X > ? X : any > ( refKey , expected ) ; } ; export function checkServerConfig ( obj ) { const _checkServerConfig = getServerConfig ( ) ; let [ res , errHash ] = checkResult ( _checkServerConfig [ "ServerConfig" ] ( ) , obj , _checkServerConfig ) ; return [ res , errHash ] as const ; } export function checkServerConfigSchema ( obj ) { const _checkServerConfig = getServerConfig ( ) ; let [ res , errHash ] = checkResult ( _checkServerConfig [ "ServerConfigSchema" ] ( ) , obj , _checkServerConfig ) ; return [ res , errHash ] as const ; } export function generateSchema ( $id ) { const refs = getServerConfig ( ) ; let definitions = { } ; Object . keys ( refs ) . forEach ( ( k ) => { if ( k === "ServerConfig" ) return ; definitions [ k ] = refs [ k as keyof typeof refs ] ( ) . getSchema ( refs ) ; } ) ; return { $id , $ref : "#/definitions/ServerConfigSchema" , $schema : "http://json-schema.org/draft-07/schema#" , definitions } ; } type treeType = { } ; type refsType = { "GroupChild" : ( ) => TypeCheck < Config . FolderElement | Config . GroupElement > ; "TreeOptions" : ( ) => TypeCheck < Config . Options_Auth | Config . Options_Index | Config . Options_Putsaver | Config . Options_Upload > ; "AccessOptions" : ( ) => TypeCheck < ServerConfig_AccessOptions > ; "AuthAccountsValue" : ( ) => TypeCheck < ServerConfig_AuthAccountsValue > ; "ServerConfig" : ( ) => TypeCheck < ServerConfig > ; "SchemaGroupChild" : ( ) => TypeCheck < Schema . GroupChildElements | Record < string , string | object > | string > ; "ServerConfigSchema" : ( ) => TypeCheck < ServerConfigSchema > ; } ; function getSchemaGroupChild ( ) { const groupElement = < AR extends boolean > ( array ) => { return checkObject < Schema . ArrayGroupElement , "$element" | "$children" | "key" > ( "" , { $element : checkStringEnum ( [ "group" ] ) . describe ( "group element" ) , ... ( array ? { key : checkString ( ) } : { } ) as { key : TypeCheck < string > ; } , $children : checkRef < refsType > ( ) ( "SchemaGroupChild" , "expected a repeat of GroupChild" ) as any } , { $options : checkArray ( checkRef < refsType > ( ) ( "TreeOptions" , "expected one of TreeOptions" ) ) , indexPath : checkString ( ) . describe ( "Path of a file to serve as the directory index." ) } , [ "$element" ] ) . describe ( "This is a group element in an " + ( array ? "array" : "object" ) + ". It is used for grouping folders under a mount point. " + ( array ? "The key property is the mount point" : "The key is the mount point" ) ) . defaultData ( { $element : "group" , $children : { } , key : array ? "" : undefined as any } ) ; } ; const folderElement = < AR extends boolean > ( array ) => { return checkObject < Schema . ArrayFolderElement , "$element" | "path" > ( "" , { $element : checkStringEnum ( [ "folder" ] as const ) . describe ( "folder element" ) , path : checkString ( ) . describe ( "Path relative to this file or an absolute path. If NodeJS can access it using stat, readdir, and readFile, then you can put it here" ) } , { $options : checkArray ( checkRef < refsType > ( ) ( "TreeOptions" , "expected one of TreeOptions" ) ) , noTrailingSlash : checkBoolean ( ) . describe ( "Load data folders under this path with no trailing slash. This imitates single-file wikis and allows tiddlers with relative links to be imported directly into a data folder wiki. The source point of the relative link becomes the data folder itself as though it is actually a file. However, this breaks relative links to resources served by the datafolder instance itself, such as the files directory introduced in 5.1.19 and requires the relative link to include the data folder name in the relative link. For this reason, it is better to convert single-file wikis to the datafolder format by putting each wiki inside its own folder as index.html, and a \"files\" folder beside the index.html file, and adding an index option to this element." ) , noDataFolder : checkBoolean ( ) . describe ( "Do not recognize datafolders within this path. Files within data folders will be accessible directly from the web, including the tiddlywiki.info file, instead of treating it as a data folder. This is useful for folders like your downloads folder which might end up containing a tiddlywiki.info file but you do not want it to be a data folder." ) , ... ( array ? { key : checkString ( ) } : { } ) as { key : TypeCheck < string > ; } } , [ "$element" ] ) . describe ( "This is a folder element. The path property specifies the file system path to serve. Any path may be specified, not just a folder. The key is the mount point for this item. If this is in an Array, the key property will be used or the path basename." ) . defaultData ( { $element : "folder" , path : "" , key : array ? "" : undefined as any } ) ; } ; const folderString = ( ) => checkString ( ) . describe ( "A string specifying the file or folder to mount here. If this is in an array the basename will be used, in an object the key will be used." ) . defaultData ( "" ) ; const SchemaGroupChild = ( ) => checkUnionArray ( [ checkRecord < string , string | object > ( checkStringRegex ( /^[^$]+$/ ) , checkUnion ( checkUnion . cu ( folderElement ( false ) , groupElement ( false ) ) , checkUnion . cu ( checkRef < refsType > ( ) ( "SchemaGroupChild" , "expected group child" ) , folderString ( ) ) , false ) ) , checkArray < Schema . ArrayFolderElement | Schema . ArrayGroupElement | string > ( checkUnion ( checkUnion . cu ( folderElement ( true ) , groupElement ( true ) ) , folderString ( ) ) ) ] , false ) . describe ( "This can be an array or object containing tree items. An array cannot use the group shorthand because there is no way to specify the mount point. You can use the advanced element syntax ({\"$element\":\"group\", \"key\":\"mount-point\", \"$children\": Children }) instead." ) . defaultData ( { } ) ; const schemaRef = { "ArrayFolder" : folderElement ( true ) , "ArrayGroup" : groupElement ( true ) , "RecordFolder" : folderElement ( false ) , "RecordGroup" : groupElement ( false ) , "SchemaGroupChild" : SchemaGroupChild ( ) } ; type schemaType = { "ArrayFolder" : TypeCheck < any > ; "ArrayGroup" : TypeCheck < any > ; "RecordFolder" : TypeCheck < any > ; "RecordGroup" : TypeCheck < any > ; "SchemaGroupChild" : TypeCheck < any > ; } ; return SchemaGroupChild ; } const putsaverOptional = ( ) => ( { backupFolder : checkString ( ) . describe ( "Backup folder to store backups in. Multiple folder paths can backup to the same folder if desired." ) , etag : checkStringEnum ( [ "optional" , "required" , "disabled" ] ) . describe ( "Whether to use the etag field -- if not specified then it will check it if presented. This does not affect the backup etagAge option, as the saving mechanism will still send etags back to the browser, regardless of this option." ) , etagAge : checkNumber ( ) . describe ( "Reject an etag with a modified time that is different than the file on disk by this many seconds. Sometimes sync or antivirus sofware will \"touch\" a file and update the modified time without changing anything. Size difference will still cause the request to be rejected." ) , gzipBackups : checkBoolean ( ) . describe ( "GZip backup file to save disk space. Good for larger wikis. Turn this off for experimental wikis that you often need to restore from a backup because of a bad line of code (I speak from experience)." ) , enabled : checkBoolean ( ) . describe ( "If false, disables the put saver globally" ) , } ) ; function getServerConfig ( ) { const AccessOptionsDescription = "Whether the user has access to different features of TiddlyServer beyond static file serving. Using auto-complete here should give you the entire object with defaults prefilled. All options are required." ; type TypeCheckItems < T , X = never > = { [ K in Exclude < keyof T , X > ] : TypeCheck < T [ K ] > ; } ; const refs = { "GroupChild" : ( ) : TypeCheck < Config . FolderElement | Config . GroupElement > => checkUnion ( checkObject < Config . FolderElement > ( "" , { $element : checkStringEnum ( [ "folder" ] as const ) , $options : checkArray ( checkRef < refsType > ( ) ( "TreeOptions" , "expected one of TreeOptions" ) ) , noTrailingSlash : checkBoolean ( ) , noDataFolder : checkBoolean ( ) , path : checkString ( ) , key : checkString ( ) , } , undefined , [ "$element" ] ) , checkObject < Config . GroupElement > ( "" , { $element : checkStringEnum ( [ "group" ] ) , $children : checkArray ( checkRef < refsType > ( ) ( "GroupChild" , "expected a repeat of GroupChild" ) ) , $options : checkArray ( checkRef < refsType > ( ) ( "TreeOptions" , "expected one of TreeOptions" ) ) , indexPath : checkUnion ( checkString ( ) , checkBooleanFalse ( ) ) , key : checkString ( ) , } , undefined , [ "$element" ] ) ) , "TreeOptions" : ( ) => checkUnion ( checkUnion . cu ( checkObject < Config . Options_Auth , "$element" > ( "" , { $element : checkStringEnum ( [ "auth" ] ) . describe ( "Only allow requests using these authAccounts. Option elements affect the group they belong to and all children under that. Each property in an auth element replaces the key from parent auth elements.\n\nAnonymous requests are ALWAYS denied if an auth list applies to the requested path.\n\nNote that this does not change server authentication procedures. Data folders are always given the authenticated username regardless of whether there are auth elements in the tree." ) } , { authError : checkNumberEnum ( [ 403 , 404 ] as const ) . describe ( "Which error code to return for unauthorized (or anonymous) requests\n\n - 403 Access Denied: Client is not granted permission to access this resouce.\n - 404 Not Found: Client is told that the resource does not exist." ) , authList : checkUnion ( checkArray ( checkString ( ) ) , checkNull ( ) ) . describe ( "Array of keys from authAccounts object that can access this resource. `null` allows all requests, including anonymous." ) , } , [ "$element" ] ) . defaultData ( { $element : "auth" } ) , checkObject < Config . Options_Upload , "$element" > ( "" , { $element : checkStringEnum ( [ "upload" ] ) . describe ( "Options related to uploads. Option elements affect the group they belong to and all children under that. Each property in an auth element replaces the key from parent auth elements.\n\nAnonymous requests are ALWAYS denied if an auth list applies to the requested path.\n\nNote that this does not change server authentication procedures. Data folders are always given the authenticated username regardless of whether there are auth elements in the tree." ) } , { maxFileSize : checkNumber ( ) . describe ( "Max file size allowed for upload in bytes" ) } , [ "$element" ] ) . defaultData ( { $element : "upload" } ) ) , checkUnion . cu ( checkObject < Config . Options_Putsaver , "$element" > ( "" , { $element : checkStringEnum ( [ "putsaver" ] as const ) . describe ( "Options related to backups for single-file wikis. Option elements affect the group they belong to and all children under that. Each attribute in a backups element replaces the key from parent backups elements." ) , } , putsaverOptional ( ) , [ "$element" ] ) . defaultData ( { $element : "putsaver" } ) , checkObject < Config . Options_Index , "$element" > ( "" , { $element : checkStringEnum ( [ "index" ] as const ) . describe ( "Options related to the directory index. If you want to specify an index file for a group, use the indexPath attribute on the group element" ) , } , { defaultType : checkUnion ( checkStringEnum ( [ "html" , "json" ] as const ) , checkNumberEnum ( [ 404 , 403 ] as const ) ) . describe ( "The format of the index generated if no index file is found, or \"403\" to return a 403 Access Denied, or 404 to return a 404 Not Found. 403 is the error code used by Apache and Nginx." ) , indexExts : checkArray ( checkString ( ) ) . describe ( 'Extensions to add when looking for an index file. A blank string will set the order to search for the exact indexFile name. The extensions are searched in the order specified. Only applies to folder elements, but may be set on a group element. An empty array disables this feature. The default is `[""]`, which will search for an exact indexFile.' ) , indexFile : checkArray ( checkString ( ) ) . describe ( 'Look for index files named exactly this or with one of the defaultExts added. For example, a defaultFile of ["index"] and a defaultExts of ["htm","",html"] would look for ["index.htm","index","index.html"] in that order. \n\nOnly applies to folder elements, but may be set on a group element to apply to all child folder elements. An empty array disables this feature. To use a .hidden file, put the full filename here, and set indexExts to [""].' ) , } , [ "$element" ] ) . defaultData ( { $element : "index" } ) ) ) , "AccessOptions" : ( ) => checkObject < ServerConfig_AccessOptions > ( "" , { datafolder : checkBoolean ( ) . describe ( "Whether clients may access data folders, which gives them full access to the system as the user that owns the server process because data folders can be easily modified to execute code on the server. This returns a 403 Access Denied if a data folder is detected. It does not serve the files inside the data folder as a regular folder. For that you need to use the noDataFolder attribute on the folder in the tree." ) , loginlink : checkBoolean ( ) . describe ( "Whether to include a link to the login page when returning auth errors" ) , mkdir : checkBoolean ( ) . describe ( "Whether clients may create new directories and datafolders inside existing directories served by TiddlyWiki" ) , putsaver : checkBoolean ( ) . describe ( "Whether clients may use the put saver, allowing any file served within the tree (not assets) to be overwritten, not just TiddlyWiki files. The put saver cannot save to data folders regardless of this setting." ) , registerNotice : checkBoolean ( ) . describe ( "Whether login attempts for a public/private key pair which has not been registered will be logged at debug level 2 with the full public key which can be copied into an authAccounts entry. Turn this off if you get spammed with login attempts." ) , transfer : checkBoolean ( ) . describe ( "Allows clients to use a custom TiddlyServer feature which relays a connection between two clients. " ) , upload : checkBoolean ( ) . describe ( "Whether clients may upload files to directories (not data folders)." ) , websockets : checkBoolean ( ) . describe ( "Whether clients may open websocket connections." ) , writeErrors : checkBoolean ( ) . describe ( "Whether to write status 500 errors to the browser, possibly including stack traces." ) , } ) . describe ( AccessOptionsDescription ) . defaultData ( defaultPermissions ) , AuthAccountsValue : ( ) => checkObject < ServerConfig_AuthAccountsValue > ( "" , { clientKeys : checkRecord ( checkString ( ) , checkObject < ServerConfig_AuthAccountsValue [ "clientKeys" ] [ "" ] > ( "" , { publicKey : checkString ( ) . describe ( "base 64 encoded public key" ) , cookieSalt : checkStringNotEmpty ( ) . describe ( "String which will be added to the cookie by the server. Changing it will invalidate all current cookies for this user, which requires them to login again on each device. `node -e \"console.log(Date.now())\"` will print the current timestamp, which you can use to make sure you get one that you've never used it before." ) , } ) . describe ( "The username the user will login with" ) ) . describe ( "Changing the public key or cookie suffix will require the user to log in again." ) , permissions : checkRef < refsType > ( ) ( "AccessOptions" , "expected AccessOptions object" ) . describe ( AccessOptionsDescription ) . defaultData ( defaultPermissions ) , } ) . describe ( "The authAccount specified in the tree auth options" ) , SchemaGroupChild : getSchemaGroupChild ( ) , "ServerConfig" : ( ) => checkObject < ServerConfig > ( "" , { $schema : checkString ( ) , __assetsDir : checkString ( ) , __dirname : checkString ( ) , __filename : checkString ( ) , __serverTW : checkString ( ) , __clientTW : checkString ( ) , _devmode : checkBoolean ( ) , authCookieAge : checkNumber ( ) , maxTransferRequests : checkNumber ( ) , debugLevel : checkNumber ( ) , tree : checkArray ( checkRef < refsType > ( ) ( "GroupChild" , "expected a GroupChild object" ) ) , authAccounts : checkRecord ( checkString ( ) , checkRef < refsType > ( ) ( "AuthAccountsValue" , "expected AuthAccountsValue" ) ) , bindInfo : checkObject < ServerConfig [ "bindInfo" ] > ( "" , { _bindLocalhost : checkBoolean ( ) , bindAddress : checkArray ( checkString ( ) ) , bindWildcard : checkBoolean ( ) , enableIPv6 : checkBoolean ( ) , filterBindAddress : checkBoolean ( ) , https : checkBoolean ( ) , localAddressPermissions : checkRecord ( checkString ( ) , checkRef < refsType > ( ) ( "AccessOptions" , "expected AccessOptions object" ) ) , port : checkNumber ( ) , } ) , controllers : checkAny ( ) . describe ( "This is not implemented yet" ) , directoryIndex : checkObject < ServerConfig [ "directoryIndex" ] > ( "" , { defaultType : checkStringEnum ( [ "html" , "json" ] as const ) , icons : checkRecord ( checkString ( ) , checkArray ( checkString ( ) ) ) , mimetypes : checkRecord ( checkString ( ) , checkArray ( checkString ( ) ) ) , mixFolders : checkBoolean ( ) , types : checkRecord ( checkString ( ) , checkString ( ) ) , } ) , putsaver : checkObject < ServerConfig [ "putsaver" ] , never > ( "" , { } , putsaverOptional ( ) ) , datafolder : checkRecord ( checkString ( ) , checkAny ( ) ) , } ) , ServerConfigSchema : ( ) => checkObject < Defined < ServerConfigSchema > , "tree" > ( "" , { tree : checkUnion ( checkString ( ) . describe ( "A string ending in `xml`, `js`, or `json` will be parsed and loaded as the tree." ) , checkRef < refsType > ( ) ( "SchemaGroupChild" , "expected group child" ) , true ) } , { _datafolderclient : checkString ( ) . describe ( "The tiddlywiki folder to serve on \"/assets/tiddlywiki/\"" ) , _datafolderserver : checkString ( ) . describe ( "The tiddlywiki folder to use for data folder instances." ) , _datafoldertarget : checkString ( ) . describe ( "Deprecated: Use _datafolderserver instead." ) , _devmode : checkBoolean ( ) , maxTransferRequests : checkNumber ( ) . describe ( "Max concurrent transfer requests" ) , authCookieAge : checkNumber ( ) . describe ( "Age to set for the auth cookie (default is 30 days)\n- 24 hours: 86400\n- 7 days: 604800\n- 30 days: 2592000\n- 60 days: 5184000\n- 90 days: 7776000\n- 120 days: 10368000\n- 150 days: 12950000\n- 180 days: 15552000" ) , debugLevel : checkNumber ( ) . describe ( "- 4: Errors that require the process to exit for restart \n- 3: Major errors that are handled and do not require a server restart \n- 2: Warnings or errors that do not alter the program flow but need to be marked (minimum for status 500) \n- 1: Info - Most startup messages \n- 0: Normal debug messages and all software and request-side error messages \n- -1: Detailed debug messages from high level apis \n- -2: Response status messages and error response data \n- -3: Request and response data for all messages (verbose) \n- -4: Protocol details and full data dump (such as encryption steps and keys)" ) , $schema : checkString ( ) . describe ( "\nThe JSON schema location for this document. This schema is generated\ndirectly from the TypeScript interfaces\nused in TiddlyServer. A text-editor with intellisense, such as VS code,\nwill make editing this file much simpler.\nMost fields include a description like this one.\n\nAll relative paths in this file are resolved relative to this file, so\n`./settings-tree.xml` refers to an XML file in the same folder as this file.\nAll relative paths in included files (such as the XML file) are resolved\nrelative to the included file." ) , bindInfo : checkObject < Defined < ServerConfigSchema [ "bindInfo" ] > , never > ( "" , { } , { _bindLocalhost : checkBoolean ( ) . describe ( "always bind a separate server instance to 127.0.0.1 regardless of any other settings" ) , bindAddress : checkArray ( checkString ( ) ) . describe ( "An array of IP addresses to accept requests on. Can be any IP address\nassigned to the machine. Default is \"127.0.0.1\".\n\nIf `bindWildcard` is true, each connection is checked individually. Otherwise, the server listens\non the specified IP addresses and accepts all connections from the operating system. If an IP address\ncannot be bound, the server skips it unless `--bindAddressRequired` is specified\n\nIf `filterBindAddress` is true, IPv4 addresses may include a subnet mask,\n(e.g. `/24`) which matches any interface IP address in that range. Prefix with a minus sign (-)\nto block requests incoming to that IP address or range." ) , bindWildcard : checkBoolean ( ) . describe ( "Bind to the wildcard addresses `0.0.0.0` and `::` (if enabled) in that order. The default is `true`. In many cases this is preferred, however Android does not support this for some reason. On Android, set this to `false` and set host to `[\"0.0.0.0/0\"]` to bind to all IPv4 addresses." ) , enableIPv6 : checkBoolean ( ) . describe ( "Bind to the IPv6 wildcard as well if `bindWilcard` is true and allow requestsincoming to IPv6 addresses if not explicitly denied" ) , filterBindAddress : checkBoolean ( ) . describe ( "IPv4 addresses may include a subnet mask, (e.g. `/24`) which matches any IP address in that range. Prefix with a minus sign (-) to block requests incoming to that IP address or range." ) , https : checkString ( ) . describe ( "https-only options: a string to a JavaScript file which exports a function of type `(iface:string) => https.ServerOptions`. Developers: Note that the initServer function will change this to a boolean value indicating whether https is in use once inside TiddlyServer." ) , port : checkNumber ( ) . describe ( "port to listen on, default is 8080 for http and 8443 for https" ) , localAddressPermissions : checkRecord ( checkString ( ) , checkRef < refsType > ( ) ( "AccessOptions" , "expected AccessOptions object" ) ) . describe ( "Permissions based on local interface address.  Enter the IP Address and NetMask (`127.0.0.1/8`) as the property key. The keyword \"localhost\" (if specified) matches 127.0.0.0/8 instead of any other specified key.  Keyword \"*\" matches everything that doesn't match another IP address.  This checks the IP address each client connects to (socket.localAddress), not the bind address of the server instance that accepted the request. The keyword defaultPermission does nothing, but auto-complete should give you the defaults (assuming they haven't changed).  You can then rename it to whatever you need it to be. " ) . defaultData ( { defaultPermissions } ) , } ) , authAccounts : checkRecord ( checkString ( ) , checkRef < refsType > ( ) ( "AuthAccountsValue" , "expected AuthAccountsValue" ) ) . describe ( "This is the auth accounts settings related to logins. The keys of this object are the authAccount specifed in the authList under the tree" ) , putsaver : checkObject < ServerConfig [ "putsaver" ] , never > ( "" , { } , putsaverOptional ( ) ) . describe ( "Settings related to the put saver" ) , directoryIndex : checkObject < Defined < ServerConfigSchema [ "directoryIndex" ] > , never > ( "" , { } , { defaultType : checkStringEnum ( [ "html" , "json" ] as const ) . describe ( "default format for the directory index" ) , icons : checkRecord ( checkString ( ) . describe ( "Name of icon file in TiddlyServer/client/icons/files" ) , checkArray ( checkString ( ) . describe ( "File extension to use this icon for." ) ) ) . describe ( "Hashmap of type { \"icon_name.png\": [\"ext\"] } where ext represents the extensions to use this icon for." ) , mimetypes : checkRecord ( checkString ( ) , checkArray ( checkString ( ) ) ) . describe ( "Additional extensions to associate with mime types for content-type header { [\"mime/type\"]: [\"htm\", \"html\"] }." ) , mixFolders : checkBoolean ( ) . describe ( "Sort folders and files together rather than separated" ) } ) , controllers : checkAny ( ) , datafolder : checkRecord ( checkString ( ) . describe ( "Server variable name" ) , checkAny ( ) . describe ( "Server variable value" ) ) . describe ( "Options object whose properties will be passed to the tiddlywiki server instance using the spread operator. This means that if a property specifies an object (or array) instead of a primitive, the same object will be given to all instances." ) , } ) } ; return refs ; }