diff --git a/server/api.ts b/server/api.ts index 339d8f3..a79a6b9 100644 --- a/server/api.ts +++ b/server/api.ts @@ -3,12 +3,15 @@ import { createValidationError, z } from "@shared/utils/validator.ts"; import { adminPasswordAlreadySetErrorSchema, adminPasswordNotSetErrorSchema, + commandExecutionErrorSchema, failedToParseRequestAsJSONErrorSchema, invalidPasswordErrorSchema, passwordsMustMatchErrorSchema, queryExecutionErrorSchema, requestValidationErrorSchema, tooManyRequestsErrorSchema, + unauthorizedErrorSchema, + usbipUnknownErrorSchema, } from "@src/lib/errors.ts"; const loginApiSchema = { @@ -66,3 +69,42 @@ export const passwordSetupApi = new Api( "POST", passwordSetupApiSchema, ); + +const updateDevicesApiSchema = { + req: z.void(), + res: z.result( + z.void(), + z.union([ + queryExecutionErrorSchema, + tooManyRequestsErrorSchema, + unauthorizedErrorSchema, + commandExecutionErrorSchema, + usbipUnknownErrorSchema, + ]), + ), +}; + +export const updateDevicesApi = new Api( + "/api/updateDevices", + "POST", + updateDevicesApiSchema, +); + +const versionApiSchema = { + req: z.void(), + res: z.result( + z.obj({ + app: z.literal("Keyborg"), + version: z.string(), + }), + z.union([ + tooManyRequestsErrorSchema, + ]), + ), +}; + +export const versionApi = new Api( + "/version", + "POST", + versionApiSchema, +); diff --git a/server/main.ts b/server/main.ts index 0c7b19e..74924c9 100644 --- a/server/main.ts +++ b/server/main.ts @@ -4,8 +4,12 @@ import { serveFile } from "jsr:@std/http/file-server"; import rateLimitMiddleware from "@src/middleware/rateLimiter.ts"; import authMiddleware from "@src/middleware/auth.ts"; import loggerMiddleware from "@src/middleware/logger.ts"; -import { SchemaValidationError, z } from "@shared/utils/validator.ts"; -import { loginApi, passwordSetupApi } from "./api.ts"; +import { + loginApi, + passwordSetupApi, + updateDevicesApi, + versionApi, +} from "./api.ts"; import { err, ok } from "@shared/utils/result.ts"; import admin from "@src/lib/admin.ts"; import { Context } from "@src/lib/context.ts"; @@ -20,6 +24,7 @@ import { import devices from "@src/lib/devices.ts"; const AUTH_COOKIE_NAME = "token"; +const VERSION = "0.1.0"; const router = new HttpRouter(); @@ -50,9 +55,9 @@ router.get("/public/*", async (c) => { router .get(["", "/index.html"], (c) => { - console.log(devices.list()); + const devicesList = devices.list().unwrap().unwrap(); - return c.html(eta.render("./index.html", {})); + return c.html(eta.render("./index.html", { devices: devicesList })); }) .get(["/login", "/login.html"], (c) => { const isSet = admin.isPasswordSet(); @@ -88,63 +93,104 @@ router ); }); -router.api(loginApi, async (c) => { - const r = await c - .parseBody() - .andThenAsync( - ({ password }) => admin.verifyPassword(password), - ); +router.get("ws", (c) => { + if (c.req.headers.get("upgrade") != "websocket") { + return new Response(null, { status: 501 }); + } - if (r.isErr()) { - if (r.error.type === "AdminPasswordNotSetError") { - return c.json400( - err({ - type: r.error.type, - info: r.error.info, - }), + const { socket, response } = Deno.upgradeWebSocket(c.req); + + socket.addEventListener("open", () => { + console.log("a client connected!"); + }); + + socket.addEventListener("close", () => { + console.log("client disconnected"); + }); + + socket.addEventListener("message", (event) => { + if (event.data === "ping") { + console.log("pinged!"); + socket.send("pong"); + } + }); + + return response; +}); + +router + .api(loginApi, async (c) => { + const r = await c + .parseBody() + .andThenAsync( + ({ password }) => admin.verifyPassword(password), + ); + + if (r.isErr()) { + if (r.error.type === "AdminPasswordNotSetError") { + return c.json400( + err({ + type: r.error.type, + info: r.error.info, + }), + ); + } + return handleCommonErrors(c, r.error); + } + + const isMatch = r.value; + + if (isMatch) { + return admin.sessions.create() + .map(({ value, expires }) => { + c.cookies.set({ + name: AUTH_COOKIE_NAME, + value, + expires, + }); + return ok(); + }).match( + (v) => c.json(v), + (e) => handleCommonErrors(c, e), + ); + } else { + return c.json( + err(invalidPasswordError("Invalid login or password")), ); } - return handleCommonErrors(c, r.error); - } + }) + .api(passwordSetupApi, async (c) => { + const r = await c.parseBody(); - const isMatch = r.value; + if (r.isErr()) { + return handleCommonErrors(c, r.error); + } - if (isMatch) { - return admin.sessions.create() - .map(({ value, expires }) => { - c.cookies.set({ - name: AUTH_COOKIE_NAME, - value, - expires, - }); - return ok(); - }).match( - (v) => c.json(v), - (e) => handleCommonErrors(c, e), + const v = r.value; + + if (v.password !== v.passwordRepeat) { + return c.json400( + err(passwordsMustMatchError("Passwords must match")), ); - } else { - return c.json(err(invalidPasswordError("Invalid login or password"))); - } -}); + } -router.api(passwordSetupApi, async (c) => { - const r = await c.parseBody(); - - if (r.isErr()) { - return handleCommonErrors(c, r.error); - } - - const v = r.value; - - if (v.password !== v.passwordRepeat) { - return c.json400(err(passwordsMustMatchError("Passwords must match"))); - } - - return admin.setPassword(v.password).match( - () => c.json(ok()), - (e) => c.json400(err(e)), - ); -}); + return admin.setPassword(v.password).match( + () => c.json(ok()), + (e) => c.json400(err(e)), + ); + }) + .api(updateDevicesApi, (c) => { + return devices.updateDevices().match( + () => c.json(ok()), + (e) => c.json500(err(e)), + ); + }) + .api(versionApi, (c) => { + return c.json(ok({ + app: "Keyborg", + version: VERSION, + })); + }); function handleCommonErrors( c: Context, diff --git a/server/public/js/index.js b/server/public/js/index.js index e69de29..cdd291e 100644 --- a/server/public/js/index.js +++ b/server/public/js/index.js @@ -0,0 +1 @@ +class c{ws=null;url;reconnectInterval;maxReconnectInterval;reconnectDecay;timeout;forcedClose=!1;onmessage;constructor(e,t={}){this.url=e,this.reconnectInterval=t.reconnectInterval??1e3,this.maxReconnectInterval=t.maxReconnectInterval??3e4,this.reconnectDecay=t.reconnectDecay??1.5,this.timeout=t.timeout??2e3,this.connect()}connect(e=!1){console.log(`Connecting to ${this.url}...`),this.ws=new WebSocket(this.url);let t=setTimeout(()=>{console.warn("Connection timeout, closing socket."),this.ws?.close()},this.timeout);this.ws.onopen=n=>{clearTimeout(t),console.log("WebSocket connected."),this.onmessage&&this.ws?.addEventListener("message",this.onmessage)},this.ws.onerror=n=>{console.error("WebSocket error:",n)},this.ws.onclose=n=>{clearTimeout(t),console.log("WebSocket closed:",n.reason),this.forcedClose||setTimeout(()=>{this.reconnectInterval=Math.min(this.reconnectInterval*this.reconnectDecay,this.maxReconnectInterval),this.connect(!0)},this.reconnectInterval)}}onMessage(e){this.ws&&this.ws.addEventListener("message",e),this.onmessage=e}send(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):console.error("WebSocket is not open. Message not sent.")}close(){this.forcedClose=!0,this.ws?.close()}}const s=new c("/ws");s.onMessage(o=>{console.log(o.data)});const i=document.getElementById("ping");i.onclick=()=>{s.send("ping")}; diff --git a/server/public/js/shared.bundle.js b/server/public/js/shared.bundle.js index cfbfd2b..f561e17 100644 --- a/server/public/js/shared.bundle.js +++ b/server/public/js/shared.bundle.js @@ -1 +1 @@ -var U=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new m(s)).catch(s=>new f(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new m(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}async unwrap(){let e=await this._promise;if(e.isErr())throw e.error;return e.value}async match(e,r){let n=await this._promise;return n.isErr()?r(n.error):e(n.value)}map(e){return new t(this._promise.then(r=>r.isErr()?new f(r.error):new m(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?R(r.error):new m(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new f(e(r.error)):new m(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?R(await e(r.error)):new m(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?R(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?R(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?y(e):v)}flatten(){return new t(this._promise.then(e=>e.flatten()))}flattenOption(e){return new t(this._promise.then(r=>r.flattenOption(e)))}flattenOptionOrDefault(e){return new t(this._promise.then(r=>r.flattenOptionOrDefault(e)))}matchOption(e,r){return new t(this._promise.then(n=>n.matchOption(e,r)))}matchOptionAndFlatten(e,r){return new t(this._promise.then(n=>n.matchOptionAndFlatten(e,r)))}then(e,r){return this._promise.then(e,r)}};function I(t){return new U(Promise.resolve(new m(t)))}function R(t){return new U(Promise.resolve(new f(t)))}var m=class t{constructor(e){this.value=e;this.value=e,Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="ok";isErr(){return!1}ifErr(e){return this}isErrOrNone(){return this.value instanceof T}isOk(){return!0}ifOk(e){return e(this.value),this}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}unwrapErr(){return v}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return U.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof T||this.value instanceof S?u(this.value.map(e)):u(y(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(this.value)}mapErrAsync(e){return I(this.value)}flatten(){return ie(this)}flattenOption(e){return this.value instanceof T||this.value instanceof S?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof T||this.value instanceof S?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof T||this.value instanceof S?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return I(this.value)}void(){return u()}toJSON(){return{tag:"ok",value:this.value}}},f=class t{constructor(e){this.error=e;this.error=e,Object.defineProperties(this,{tag:{writable:!1,configurable:!1,enumerable:!1}})}tag="err";isErr(){return!0}ifErr(e){return e(this.error),this}isOk(){return!1}ifOk(e){return this}isErrOrNone(){return!0}unwrap(){let e=`Tried to unwrap error: ${se(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return y(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return R(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return U.fromPromise(new Promise(()=>{throw""}),()=>e(this.error))}mapOption(e){return i(this.error)}andThen(e){return new t(this.error)}andThenAsync(e){return new t(this.error).toAsync()}flatten(){return ie(this)}flattenOption(e){return new t(this.error)}flattenOptionOr(e){return new t(this.error)}matchOption(e,r){return i(this.error)}toNullable(){return null}toAsync(){return R(this.error)}void(){return i(this.error)}toJSON(){return{tag:"err",error:this.error}}};function u(t){return new m(t)}function i(t){return new f(t)}function Oe(t,e){return(...r)=>{try{let n=t(...r);return u(n)}catch(n){return i(e?e(n):n)}}}function se(t){if(t instanceof Error)return t.message?t.message:"code"in t&&typeof t.code=="string"?t.code:"An unknown error occurred";if(typeof t=="string")return t;if(typeof t=="object"&&t!==null&&"message"in t){let e=t;return typeof e.message=="string"?e.message:String(e.message)}return"An unknown error occurred"}function ie(t){let e=t;for(;e instanceof m&&(e.value instanceof m||e.value instanceof f);)e=e.value;return e}var x=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function be(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return i(new x(se(n)))}else e=t;if(typeof e!="object"||e===null)return i(new x("Expected an object but received type ${typeof data}."));let r=e;if("tag"in r)switch(r.tag){case"ok":{if("value"in r)return u(r.value);break}case"err":{if("error"in r)return i(r.error);break}}return i(new x("Object does not contain 'tag' and 'value' or 'error' property"))}var S=class t{constructor(e){this.value=e;Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="some";isSome(){return!0}ifSome(e){return e(this.value),this}isNone(){return!1}ifNone(e){return this}map(e){return new t(e(this.value))}flatMap(e){return e(this.value)}andThen(e){return e(this.value)}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}or(e){return this}orElse(e){return this}match(e,r){return e(this.value)}toString(){return`Some(${this.value})`}toJSON(){return{tag:"some",value:this.value}}toNullable(){return this.value}toBoolean(){return!0}okOrElse(e){return u(this.value)}},T=class t{tag="none";constructor(){Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}isSome(){return!1}ifSome(e){return this}isNone(){return!0}ifNone(e){return e(),this}map(e){return new t}andThen(e){return v}flatMap(e){return new t}unwrap(){throw new Error("Tried to unwrap a non-existent value")}unwrapOr(e){return e}unwrapOrElse(e){return e()}or(e){return e}orElse(e){return e()}match(e,r){return r()}toString(){return"None"}toJSON(){return{tag:"none"}}toNullable(){return null}toBoolean(){return!1}okOrElse(e){return i(e())}};function y(t){return new S(t)}var v=new T;function Ve(t){return t?y(t):v}var V=class t extends Error{constructor(r,n){super(t.getBestMsg(n)||"Schema validation error");this.input=r;this.detail=n;this.name="SchemaValidationError"}type="SchemaValiationError";format(){return{input:this.input,error:t.formatDetail(this.detail)}}get info(){return t.getBestMsg(this.detail)}static getBestMsg(r){switch(r.kind){case"typeMismatch":case"unexpectedProperties":case"missingProperties":case"general":case"unionValidation":return t.formatMsg(r);case"propertyValidation":case"arrayElement":return r.msg||t.getBestMsg(r.detail);default:return"Unknown error"}}static formatDetail(r){switch(r.kind){case"general":case"typeMismatch":return t.formatMsg(r);case"propertyValidation":return{[r.property]:r.msg||this.formatDetail(r.detail)};case"unexpectedProperties":case"missingProperties":{let n=r.msg||(r.kind==="unexpectedProperties"?"Property is not allowed in a strict schema object":"Property is required, but missing");return r.keys.reduce((s,l)=>(s[l]=n,s),{})}case"arrayElement":{let n={};return r.msg&&(n.msg=r.msg),n[`index_${r.index}`]=this.formatDetail(r.detail),n}case"unionValidation":{let n=r.details?.map(s=>this.formatDetail(s));return r.msg&&n.unshift("Msg: "+r.msg),n}default:return"Unknown error type"}}static formatMsg(r){if(r.msg||r.kind==="general")return r.msg||"Unknown error";switch(r.kind){case"typeMismatch":return`Expected ${r.expected}, but received ${r.received}`;case"unexpectedProperties":return`Properties not allowed: ${r.keys.join(", ")}`;case"missingProperties":return`Missing required properties: ${r.keys.join(", ")}`;case"unionValidation":return"Input did not match any union member";default:return"Unknown error"}}};function o(t,e){return new V(t,e)}var a=class{constructor(e){this.msg=e}checks=[];parse(e){return this.validateInput(e).andThen(r=>this.applyValidationChecks(r))}static validatePrimitive(e,r,n){let s=typeof e;return s===r?u(e):i(o(e,{kind:"typeMismatch",expected:r,received:s,msg:n}))}addCheck(e){return this.checks.push(e),this}applyValidationChecks(e){for(let r of this.checks){let n=r(e);if(n)return i(n)}return u(e)}static isNullishSchema(e){return e.parse(null).isOk()||e.parse(void 0).isOk()}},F=class t extends a{static emailRegex=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;static ipRegex=/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;validateInput(e){return a.validatePrimitive(e,"string",this.msg)}max(e,r){return this.addCheck(n=>{if(n.length>e)return o(n,{kind:"general",msg:r||`String must be at most ${e} characters long`})})}min(e,r){return this.addCheck(n=>{if(n.length{if(!e.test(n))return o(n,{kind:"general",msg:r||`String must match pattern ${String(e)}`})})}email(e){return this.regex(t.emailRegex,e||"String must be a valid email address")}ip(e){return this.regex(t.ipRegex,e||"String must be a valid ip address")}},N=class extends a{constructor(r,n){super(n);this.literal=r}validateInput(r){return a.validatePrimitive(r,"string",this.msg).andThen(n=>n===this.literal?u(n):i(o(r,{kind:"typeMismatch",expected:this.literal,received:n,msg:this.msg})))}},M=class extends a{validateInput(e){return a.validatePrimitive(e,"number",this.msg)}gt(e,r){return this.addCheck(n=>{if(n<=e)return o(n,{kind:"general",msg:r||`Number must be greater than ${e}`})})}gte(e,r){return this.addCheck(n=>{if(n{if(n>=e)return o(n,{kind:"general",msg:r||`Number must be less than ${e}`})})}lte(e,r){return this.addCheck(n=>{if(n>e)return o(n,{kind:"general",msg:r||`Number must be less than or equal to ${e}`})})}int(e){return this.addCheck(r=>{if(!Number.isInteger(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}positive(e){return this.gt(0,e||"Number must be positive")}nonnegative(e){return this.gte(0,e||"Number must be nonnegative")}negative(e){return this.lt(0,e||"Number must be negative")}nonpositive(e){return this.lte(0,e||"Number must be nonpositive")}finite(e){return this.addCheck(r=>{if(!Number.isFinite(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}safe(e){return this.addCheck(r=>{if(!Number.isSafeInteger(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}multipleOf(e,r){return this.addCheck(n=>{if(n%e!==0)return o(n,{kind:"general",msg:r||`Number must be a multiple of ${e}`})})}},j=class extends a{validateInput(e){return a.validatePrimitive(e,"bigint",this.msg)}},B=class extends a{validateInput(e){return a.validatePrimitive(e,"boolean",this.msg)}},q=class extends a{validateInput(e){return a.validatePrimitive(e,"object",this.msg).andThen(r=>{if(r instanceof Date)return u(r);let n=r?.constructor?.name??"unknown";return i(o(e,{kind:"typeMismatch",expected:"Date instance",received:n,msg:this.msg}))})}},D=class extends a{validateInput(e){return a.validatePrimitive(e,"symbol",this.msg)}},$=class extends a{validateInput(e){return a.validatePrimitive(e,"undefined",this.msg)}},J=class extends a{validateInput(e){if(e===null)return u(e);let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(o(e,{kind:"typeMismatch",expected:"null",received:r,msg:this.msg}))}},C=class extends a{validateInput(e){if(e==null)return u();let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(o(e,{kind:"typeMismatch",expected:"void (undefined/null)",received:r,msg:this.msg}))}},K=class extends a{validateInput(e){return u(e)}},L=class extends a{validateInput(e){return u(e)}},z=class extends a{validateInput(e){return i(o(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},Q=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.shape=r;this.objectMsg=l}strictMode=!1;objectMsg;validateInput(r){return a.validatePrimitive(r,"object",this.msg||this.objectMsg?.mismatch).andThen(n=>{if(n===null)return i(o(r,{kind:"typeMismatch",expected:"Non-null object",received:"null",msg:this.msg||this.objectMsg?.nullObject}));let s={},l=new Set(Object.keys(this.shape));for(let E of Object.keys(n)){let g=this.shape[E];if(g===void 0){if(this.strictMode){let ue=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return i(o(r,{kind:"unexpectedProperties",keys:ue,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let w=g.parse(n[E]);if(w.isErr())return i(o(r,{kind:"propertyValidation",property:E,detail:w.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));l.delete(E),s[E]=w.value}let h=l.keys().filter(E=>!a.isNullishSchema(this.shape[E])).toArray();return h.length>0?i(o(r,{kind:"missingProperties",keys:h,msg:this.msg||this.objectMsg?.missingProperty})):u(s)})}strict(){return this.strictMode=!0,this}pick(r){let n={};for(let s of Object.keys(r))r[s]&&(n[s]=this.shape[s]);return c.obj(n)}},W=class t extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schemas=r;this.unionMsg=l}unionMsg;static getTypeFromSchemaName(r){switch(r){case"StringSchema":case"LiteralSchema":return"string";case"NumberSchema":return"number";case"BigintSchema":return"bigint";case"BooleanSchema":return"boolean";case"UndefinedSchema":return"undefined";case"SymbolSchema":return"symbol";default:return"object"}}validateInput(r){let n=[],s=!0;for(let l of this.schemas){let h=l.parse(r);if(h.isOk())return u(h.value);s=h.error.detail?.kind==="typeMismatch"&&s,n.push(h.error.detail)}return s?i(o(r,{kind:"typeMismatch",expected:this.schemas.map(l=>t.getTypeFromSchemaName(l.constructor.name)).join(" | "),received:typeof r,msg:this.msg||this.unionMsg?.mismatch})):i(o(r,{kind:"unionValidation",msg:this.msg||this.unionMsg?.unionValidation||"Input did not match any union member",details:n}))}},Z=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schema=r;this.arrayMsg=l}arrayMsg;validateInput(r){if(!Array.isArray(r))return i(o(r,{kind:"typeMismatch",expected:"Array",received:"Non-array",msg:this.msg||this.arrayMsg?.mismatch}));for(let n=0;n{if("tag"in n)switch(n.tag){case"ok":return"value"in n?this.okSchema.parse(n.value).match(s=>u(u(s)),s=>i(o(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.okSchema)?u(u()):i(o(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'ok', than result must contain a 'value' property"}));case"err":return"error"in n?this.errSchema.parse(n.error).match(s=>u(i(s)),s=>i(o(r,{kind:"propertyValidation",property:"error",detail:s.detail}))):a.isNullishSchema(this.errSchema)?u(i()):i(o(r,{kind:"missingProperties",keys:["error"],msg:"If tag is set to 'err', than result must contain a 'error' property"}));default:return i(o(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'ok' or 'err'",received:`'${n.tag}'`}}))}else return i(o(r,{kind:"missingProperties",keys:["tag"],msg:"Result must contain a tag property"}))})}},_=class extends a{constructor(r){super();this.schema=r}validateInput(r){return a.validatePrimitive(r,"object").andThen(n=>{if("tag"in n)switch(n.tag){case"some":return"value"in n?this.schema.parse(n.value).match(s=>u(y(s)),s=>i(o(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.schema)?u(y()):i(o(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'some', than option must contain a 'value' property"}));case"none":return u(v);default:return i(o(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'some' or 'none'",received:`'${n.tag}'`}}))}else return i(o(r,{kind:"missingProperties",keys:["tag"],msg:"Option must contain a tag property"}))})}},c={string:t=>new F(t),literal:(t,e)=>new N(t,e),number:t=>new M(t),bigint:t=>new j(t),boolean:t=>new B(t),date:t=>new q(t),symbol:t=>new D(t),undefined:t=>new $(t),null:t=>new J(t),void:t=>new C(t),any:t=>new K(t),unknown:t=>new L(t),never:t=>new z(t),obj:(t,e)=>new Q(t,e),union:(t,e)=>new W(t,e),array:(t,e)=>new Z(t,e),optional:(t,e)=>new G(t,e),nullable:(t,e)=>new H(t,e),nullish:(t,e)=>new X(t,e),result:(t,e)=>new Y(t,e),option:t=>new _(t)};function p(t,e){return c.obj({type:c.literal(t),info:e??c.string()})}function d(t){return e=>({type:t.shape.type.literal,info:e})}var O=p("QueryExecutionError"),er=d(O),le=p("NoAdminEntryError"),rr=d(le),ce=p("FailedToReadFileError"),tr=d(ce),pe=p("InvalidSyntaxError"),nr=d(pe),de=p("InvalidPathError"),sr=d(de),ee=p("AdminPasswordNotSetError"),ir=d(ee),b=p("RequestValidationError"),ae=d(b),me=p("ResponseValidationError"),oe=d(me),A=p("FailedToParseRequestAsJSONError"),ar=d(A),P=p("TooManyRequestsError"),or=d(P),he=p("UnauthorizedError"),ur=d(he),re=p("InvalidPasswordError"),lr=d(re),te=p("AdminPasswordAlreadySetError"),cr=d(te),ne=p("PasswordsMustMatchError"),pr=d(ne);var k=class{constructor(e,r,n){this.path=e;this.method=r;this.schema=n;this.pathSplitted=e.split("/"),this.paramIndexes=this.pathSplitted.reduce((s,l,h)=>(l.startsWith(":")&&(s[l.slice(1)]=h),s),{})}pathSplitted;paramIndexes;makeRequest(e,r){return this.schema.req.parse(e).toAsync().mapErr(n=>ae(n.info)).andThenAsync(async n=>{let s=this.pathSplitted;for(let[g,w]of Object.entries(r))s[this.paramIndexes[g]]=w;let l=s.join("/"),E=await(await fetch(l,{method:this.method,headers:{"Content-Type":"application/json",Accept:"application/json; charset=utf-8"},body:JSON.stringify(n)})).json();return this.schema.res.parse(E).toAsync().map(g=>g).mapErr(g=>oe(g.info))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var Ee={req:c.obj({password:c.string()}),res:c.result(c.void(),c.union([ee,O,A,b,P,re]))},vr=new k("/login","POST",Ee),fe={req:c.obj({password:c.string().min(10,"Password must be at least 10 characters long").regex(/^[a-zA-Z0-9]+$/,"Password must consist of lower or upper case latin letters and numbers"),passwordRepeat:c.string()}).addCheck(t=>{if(t.passwordRepeat!==t.password)return o(t,{kind:"general",msg:"Passwords must match"})}),res:c.result(c.void(),c.union([ne,te,O,A,b,P]))},Sr=new k("/setup","POST",fe);export{a as BaseSchema,f as Err,N as LiteralSchema,T as None,Q as ObjectSchema,m as Ok,_ as OptionSchema,U as ResultAsync,be as ResultFromJSON,Y as ResultSchema,V as SchemaValidationError,S as Some,F as StringSchema,o as createValidationError,i as err,R as errAsync,ie as flattenResult,Ve as fromNullableVal,Oe as fromThrowable,se as getMessageFromError,vr as loginApi,v as none,u as ok,I as okAsync,Sr as passwordSetupApi,y as some,c as z}; +var R=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new m(s)).catch(s=>new y(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new m(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}async unwrap(){let e=await this._promise;if(e.isErr())throw e.error;return e.value}async match(e,r){let n=await this._promise;return n.isErr()?r(n.error):e(n.value)}map(e){return new t(this._promise.then(r=>r.isErr()?new y(r.error):new m(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?x(r.error):new m(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new y(e(r.error)):new m(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?x(await e(r.error)):new m(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?x(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?x(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?f(e):v)}flatten(){return new t(this._promise.then(e=>e.flatten()))}flattenOption(e){return new t(this._promise.then(r=>r.flattenOption(e)))}flattenOptionOrDefault(e){return new t(this._promise.then(r=>r.flattenOptionOrDefault(e)))}matchOption(e,r){return new t(this._promise.then(n=>n.matchOption(e,r)))}matchOptionAndFlatten(e,r){return new t(this._promise.then(n=>n.matchOptionAndFlatten(e,r)))}then(e,r){return this._promise.then(e,r)}};function I(t){return new R(Promise.resolve(new m(t)))}function x(t){return new R(Promise.resolve(new y(t)))}var m=class t{constructor(e){this.value=e;this.value=e,Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="ok";isErr(){return!1}ifErr(e){return this}isErrOrNone(){return this.value instanceof T}isOk(){return!0}ifOk(e){return e(this.value),this}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}unwrapErr(){return v}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return R.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof T||this.value instanceof S?u(this.value.map(e)):u(f(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(this.value)}mapErrAsync(e){return I(this.value)}flatten(){return ue(this)}flattenOption(e){return this.value instanceof T||this.value instanceof S?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof T||this.value instanceof S?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof T||this.value instanceof S?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return I(this.value)}void(){return u()}toJSON(){return{tag:"ok",value:this.value}}},y=class t{constructor(e){this.error=e;this.error=e,Object.defineProperties(this,{tag:{writable:!1,configurable:!1,enumerable:!1}})}tag="err";isErr(){return!0}ifErr(e){return e(this.error),this}isOk(){return!1}ifOk(e){return this}isErrOrNone(){return!0}unwrap(){let e=`Tried to unwrap error: ${ae(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return f(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return x(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return R.fromPromise(new Promise(()=>{throw""}),()=>e(this.error))}mapOption(e){return o(this.error)}andThen(e){return new t(this.error)}andThenAsync(e){return new t(this.error).toAsync()}flatten(){return ue(this)}flattenOption(e){return new t(this.error)}flattenOptionOr(e){return new t(this.error)}matchOption(e,r){return o(this.error)}toNullable(){return null}toAsync(){return x(this.error)}void(){return o(this.error)}toJSON(){return{tag:"err",error:this.error}}};function u(t){return new m(t)}function o(t){return new y(t)}function Be(t,e){return(...r)=>{try{let n=t(...r);return u(n)}catch(n){return o(e?e(n):n)}}}function ae(t){if(t instanceof Error)return t.message?t.message:"code"in t&&typeof t.code=="string"?t.code:"An unknown error occurred";if(typeof t=="string")return t;if(typeof t=="object"&&t!==null&&"message"in t){let e=t;return typeof e.message=="string"?e.message:String(e.message)}return"An unknown error occurred"}function ue(t){let e=t;for(;e instanceof m&&(e.value instanceof m||e.value instanceof y);)e=e.value;return e}var O=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function De(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return o(new O(ae(n)))}else e=t;if(typeof e!="object"||e===null)return o(new O("Expected an object but received type ${typeof data}."));let r=e;if("tag"in r)switch(r.tag){case"ok":{if("value"in r)return u(r.value);break}case"err":{if("error"in r)return o(r.error);break}}return o(new O("Object does not contain 'tag' and 'value' or 'error' property"))}var S=class t{constructor(e){this.value=e;Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="some";isSome(){return!0}ifSome(e){return e(this.value),this}isNone(){return!1}ifNone(e){return this}map(e){return new t(e(this.value))}flatMap(e){return e(this.value)}andThen(e){return e(this.value)}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}or(e){return this}orElse(e){return this}match(e,r){return e(this.value)}toString(){return`Some(${this.value})`}toJSON(){return{tag:"some",value:this.value}}toNullable(){return this.value}toBoolean(){return!0}okOrElse(e){return u(this.value)}},T=class t{tag="none";constructor(){Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}isSome(){return!1}ifSome(e){return this}isNone(){return!0}ifNone(e){return e(),this}map(e){return new t}andThen(e){return v}flatMap(e){return new t}unwrap(){throw new Error("Tried to unwrap a non-existent value")}unwrapOr(e){return e}unwrapOrElse(e){return e()}or(e){return e}orElse(e){return e()}match(e,r){return r()}toString(){return"None"}toJSON(){return{tag:"none"}}toNullable(){return null}toBoolean(){return!1}okOrElse(e){return o(e())}};function f(t){return new S(t)}var v=new T;function Ce(t){return t?f(t):v}var V=class t extends Error{constructor(r,n){super(t.getBestMsg(n)||"Schema validation error");this.input=r;this.detail=n;this.name="SchemaValidationError"}type="SchemaValiationError";format(){return{input:this.input,error:t.formatDetail(this.detail)}}get info(){return t.getBestMsg(this.detail)}static getBestMsg(r){switch(r.kind){case"typeMismatch":case"unexpectedProperties":case"missingProperties":case"general":case"unionValidation":return t.formatMsg(r);case"propertyValidation":case"arrayElement":return r.msg||t.getBestMsg(r.detail);default:return"Unknown error"}}static formatDetail(r){switch(r.kind){case"general":case"typeMismatch":return t.formatMsg(r);case"propertyValidation":return{[r.property]:r.msg||this.formatDetail(r.detail)};case"unexpectedProperties":case"missingProperties":{let n=r.msg||(r.kind==="unexpectedProperties"?"Property is not allowed in a strict schema object":"Property is required, but missing");return r.keys.reduce((s,c)=>(s[c]=n,s),{})}case"arrayElement":{let n={};return r.msg&&(n.msg=r.msg),n[`index_${r.index}`]=this.formatDetail(r.detail),n}case"unionValidation":{let n=r.details?.map(s=>this.formatDetail(s));return r.msg&&n.unshift("Msg: "+r.msg),n}default:return"Unknown error type"}}static formatMsg(r){if(r.msg||r.kind==="general")return r.msg||"Unknown error";switch(r.kind){case"typeMismatch":return`Expected ${r.expected}, but received ${r.received}`;case"unexpectedProperties":return`Properties not allowed: ${r.keys.join(", ")}`;case"missingProperties":return`Missing required properties: ${r.keys.join(", ")}`;case"unionValidation":return"Input did not match any union member";default:return"Unknown error"}}};function a(t,e){return new V(t,e)}var i=class{constructor(e){this.msg=e}checks=[];parse(e){return this.validateInput(e).andThen(r=>this.applyValidationChecks(r))}static validatePrimitive(e,r,n){let s=typeof e;return s===r?u(e):o(a(e,{kind:"typeMismatch",expected:r,received:s,msg:n}))}addCheck(e){return this.checks.push(e),this}applyValidationChecks(e){for(let r of this.checks){let n=r(e);if(n)return o(n)}return u(e)}static isNullishSchema(e){return e.parse(null).isOk()||e.parse(void 0).isOk()}},N=class t extends i{static emailRegex=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;static ipRegex=/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;validateInput(e){return i.validatePrimitive(e,"string",this.msg)}max(e,r){return this.addCheck(n=>{if(n.length>e)return a(n,{kind:"general",msg:r||`String must be at most ${e} characters long`})})}min(e,r){return this.addCheck(n=>{if(n.length{if(!e.test(n))return a(n,{kind:"general",msg:r||`String must match pattern ${String(e)}`})})}email(e){return this.regex(t.emailRegex,e||"String must be a valid email address")}ip(e){return this.regex(t.ipRegex,e||"String must be a valid ip address")}},F=class extends i{constructor(r,n){super(n);this.literal=r}validateInput(r){return i.validatePrimitive(r,"string",this.msg).andThen(n=>n===this.literal?u(n):o(a(r,{kind:"typeMismatch",expected:this.literal,received:n,msg:this.msg})))}},M=class extends i{validateInput(e){return i.validatePrimitive(e,"number",this.msg)}gt(e,r){return this.addCheck(n=>{if(n<=e)return a(n,{kind:"general",msg:r||`Number must be greater than ${e}`})})}gte(e,r){return this.addCheck(n=>{if(n{if(n>=e)return a(n,{kind:"general",msg:r||`Number must be less than ${e}`})})}lte(e,r){return this.addCheck(n=>{if(n>e)return a(n,{kind:"general",msg:r||`Number must be less than or equal to ${e}`})})}int(e){return this.addCheck(r=>{if(!Number.isInteger(r))return a(r,{kind:"general",msg:e||"Number must be an integer"})})}positive(e){return this.gt(0,e||"Number must be positive")}nonnegative(e){return this.gte(0,e||"Number must be nonnegative")}negative(e){return this.lt(0,e||"Number must be negative")}nonpositive(e){return this.lte(0,e||"Number must be nonpositive")}finite(e){return this.addCheck(r=>{if(!Number.isFinite(r))return a(r,{kind:"general",msg:e||"Number must be an integer"})})}safe(e){return this.addCheck(r=>{if(!Number.isSafeInteger(r))return a(r,{kind:"general",msg:e||"Number must be an integer"})})}multipleOf(e,r){return this.addCheck(n=>{if(n%e!==0)return a(n,{kind:"general",msg:r||`Number must be a multiple of ${e}`})})}},B=class extends i{validateInput(e){return i.validatePrimitive(e,"bigint",this.msg)}},D=class extends i{validateInput(e){return i.validatePrimitive(e,"boolean",this.msg)}},j=class extends i{validateInput(e){return i.validatePrimitive(e,"object",this.msg).andThen(r=>{if(r instanceof Date)return u(r);let n=r?.constructor?.name??"unknown";return o(a(e,{kind:"typeMismatch",expected:"Date instance",received:n,msg:this.msg}))})}},q=class extends i{validateInput(e){return i.validatePrimitive(e,"symbol",this.msg)}},$=class extends i{validateInput(e){return i.validatePrimitive(e,"undefined",this.msg)}},C=class extends i{validateInput(e){if(e===null)return u(e);let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return o(a(e,{kind:"typeMismatch",expected:"null",received:r,msg:this.msg}))}},J=class extends i{validateInput(e){if(e==null)return u();let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return o(a(e,{kind:"typeMismatch",expected:"void (undefined/null)",received:r,msg:this.msg}))}},K=class extends i{validateInput(e){return u(e)}},L=class extends i{validateInput(e){return u(e)}},z=class extends i{validateInput(e){return o(a(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},Q=class extends i{constructor(r,n){let s,c;typeof n=="string"?s=n:typeof n=="object"&&(c=n);super(s);this.shape=r;this.objectMsg=c}strictMode=!1;objectMsg;validateInput(r){return i.validatePrimitive(r,"object",this.msg||this.objectMsg?.mismatch).andThen(n=>{if(n===null)return o(a(r,{kind:"typeMismatch",expected:"Non-null object",received:"null",msg:this.msg||this.objectMsg?.nullObject}));let s={},c=new Set(Object.keys(this.shape));for(let E of Object.keys(n)){let g=this.shape[E];if(g===void 0){if(this.strictMode){let pe=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return o(a(r,{kind:"unexpectedProperties",keys:pe,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let k=g.parse(n[E]);if(k.isErr())return o(a(r,{kind:"propertyValidation",property:E,detail:k.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));c.delete(E),s[E]=k.value}let h=c.keys().filter(E=>!i.isNullishSchema(this.shape[E])).toArray();return h.length>0?o(a(r,{kind:"missingProperties",keys:h,msg:this.msg||this.objectMsg?.missingProperty})):u(s)})}strict(){return this.strictMode=!0,this}pick(r){let n={};for(let s of Object.keys(r))r[s]&&(n[s]=this.shape[s]);return l.obj(n)}},W=class t extends i{constructor(r,n){let s,c;typeof n=="string"?s=n:typeof n=="object"&&(c=n);super(s);this.schemas=r;this.unionMsg=c}unionMsg;static getTypeFromSchemaName(r){switch(r){case"StringSchema":case"LiteralSchema":return"string";case"NumberSchema":return"number";case"BigintSchema":return"bigint";case"BooleanSchema":return"boolean";case"UndefinedSchema":return"undefined";case"SymbolSchema":return"symbol";default:return"object"}}validateInput(r){let n=[],s=!0;for(let c of this.schemas){let h=c.parse(r);if(h.isOk())return u(h.value);s=h.error.detail?.kind==="typeMismatch"&&s,n.push(h.error.detail)}return s?o(a(r,{kind:"typeMismatch",expected:this.schemas.map(c=>t.getTypeFromSchemaName(c.constructor.name)).join(" | "),received:typeof r,msg:this.msg||this.unionMsg?.mismatch})):o(a(r,{kind:"unionValidation",msg:this.msg||this.unionMsg?.unionValidation||"Input did not match any union member",details:n}))}},Z=class extends i{constructor(r,n){let s,c;typeof n=="string"?s=n:typeof n=="object"&&(c=n);super(s);this.schema=r;this.arrayMsg=c}arrayMsg;validateInput(r){if(!Array.isArray(r))return o(a(r,{kind:"typeMismatch",expected:"Array",received:"Non-array",msg:this.msg||this.arrayMsg?.mismatch}));for(let n=0;n{if("tag"in n)switch(n.tag){case"ok":return"value"in n?this.okSchema.parse(n.value).match(s=>u(u(s)),s=>o(a(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):i.isNullishSchema(this.okSchema)?u(u()):o(a(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'ok', than result must contain a 'value' property"}));case"err":return"error"in n?this.errSchema.parse(n.error).match(s=>u(o(s)),s=>o(a(r,{kind:"propertyValidation",property:"error",detail:s.detail}))):i.isNullishSchema(this.errSchema)?u(o()):o(a(r,{kind:"missingProperties",keys:["error"],msg:"If tag is set to 'err', than result must contain a 'error' property"}));default:return o(a(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'ok' or 'err'",received:`'${n.tag}'`}}))}else return o(a(r,{kind:"missingProperties",keys:["tag"],msg:"Result must contain a tag property"}))})}},_=class extends i{constructor(r){super();this.schema=r}validateInput(r){return i.validatePrimitive(r,"object").andThen(n=>{if("tag"in n)switch(n.tag){case"some":return"value"in n?this.schema.parse(n.value).match(s=>u(f(s)),s=>o(a(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):i.isNullishSchema(this.schema)?u(f()):o(a(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'some', than option must contain a 'value' property"}));case"none":return u(v);default:return o(a(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'some' or 'none'",received:`'${n.tag}'`}}))}else return o(a(r,{kind:"missingProperties",keys:["tag"],msg:"Option must contain a tag property"}))})}},l={string:t=>new N(t),literal:(t,e)=>new F(t,e),number:t=>new M(t),bigint:t=>new B(t),boolean:t=>new D(t),date:t=>new j(t),symbol:t=>new q(t),undefined:t=>new $(t),null:t=>new C(t),void:t=>new J(t),any:t=>new K(t),unknown:t=>new L(t),never:t=>new z(t),obj:(t,e)=>new Q(t,e),union:(t,e)=>new W(t,e),array:(t,e)=>new Z(t,e),optional:(t,e)=>new G(t,e),nullable:(t,e)=>new H(t,e),nullish:(t,e)=>new X(t,e),result:(t,e)=>new Y(t,e),option:t=>new _(t)};function p(t,e){return l.obj({type:l.literal(t),info:e??l.string()})}function d(t){return e=>({type:t.shape.type.literal,info:e})}var b=p("QueryExecutionError"),lr=d(b),de=p("NoAdminEntryError"),cr=d(de),me=p("FailedToReadFileError"),pr=d(me),he=p("InvalidSyntaxError"),dr=d(he),Ee=p("InvalidPathError"),mr=d(Ee),ee=p("AdminPasswordNotSetError"),hr=d(ee),A=p("RequestValidationError"),le=d(A),ye=p("ResponseValidationError"),ce=d(ye),P=p("FailedToParseRequestAsJSONError"),Er=d(P),U=p("TooManyRequestsError"),yr=d(U),re=p("UnauthorizedError"),fr=d(re),te=p("InvalidPasswordError"),Tr=d(te),ne=p("AdminPasswordAlreadySetError"),gr=d(ne),se=p("PasswordsMustMatchError"),vr=d(se),oe=p("CommandExecutionError"),Sr=d(oe),fe=p("DeviceDoesNotExistError"),xr=d(fe),Te=p("DeviceAlreadyBoundError"),Rr=d(Te),ge=p("DeviceNotBoundError"),wr=d(ge),ie=p("UsbipUnknownError"),Ur=d(ie),ve=p("NotFoundError"),kr=d(ve),Se=p("NotAllowedError"),Or=d(Se);var w=class{constructor(e,r,n){this.path=e;this.method=r;this.schema=n;this.pathSplitted=e.split("/"),this.paramIndexes=this.pathSplitted.reduce((s,c,h)=>(c.startsWith(":")&&(s[c.slice(1)]=h),s),{})}pathSplitted;paramIndexes;makeRequest(e,r){return this.schema.req.parse(e).toAsync().mapErr(n=>le(n.info)).andThenAsync(async n=>{let s=this.pathSplitted;for(let[g,k]of Object.entries(r))s[this.paramIndexes[g]]=k;let c=s.join("/");console.log(n);let E=await(await fetch(c,{method:this.method,headers:{"Content-Type":"application/json",Accept:"application/json; charset=utf-8"},body:JSON.stringify(n)})).json();return this.schema.res.parse(E).toAsync().map(g=>g).mapErr(g=>ce(g.info))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var xe={req:l.obj({password:l.string()}),res:l.result(l.void(),l.union([ee,b,P,A,U,te]))},Br=new w("/login","POST",xe),Re={req:l.obj({password:l.string().min(10,"Password must be at least 10 characters long").regex(/^[a-zA-Z0-9]+$/,"Password must consist of lower or upper case latin letters and numbers"),passwordRepeat:l.string()}).addCheck(t=>{if(t.passwordRepeat!==t.password)return a(t,{kind:"general",msg:"Passwords must match"})}),res:l.result(l.void(),l.union([se,ne,b,P,A,U]))},Dr=new w("/setup","POST",Re),we={req:l.void(),res:l.result(l.void(),l.union([b,U,re,oe,ie]))},jr=new w("/api/updateDevices","POST",we),Ue={req:l.void(),res:l.result(l.obj({app:l.literal("Keyborg"),version:l.string()}),l.union([U]))},qr=new w("/version","POST",Ue);export{i as BaseSchema,y as Err,F as LiteralSchema,T as None,Q as ObjectSchema,m as Ok,_ as OptionSchema,R as ResultAsync,De as ResultFromJSON,Y as ResultSchema,V as SchemaValidationError,S as Some,N as StringSchema,a as createValidationError,o as err,x as errAsync,ue as flattenResult,Ce as fromNullableVal,Be as fromThrowable,ae as getMessageFromError,Br as loginApi,v as none,u as ok,I as okAsync,Dr as passwordSetupApi,f as some,jr as updateDevicesApi,qr as versionApi,l as z}; diff --git a/server/src/js/index.ts b/server/src/js/index.ts index 8b13789..064a1c1 100644 --- a/server/src/js/index.ts +++ b/server/src/js/index.ts @@ -1 +1,103 @@ +interface ReconnectOptions { + reconnectInterval?: number; // Initial reconnect delay (ms) + maxReconnectInterval?: number; // Maximum delay (ms) + reconnectDecay?: number; // Exponential backoff multiplier + timeout?: number; // Connection timeout (ms) +} +class ReconnectingWebSocketClient { + private ws: WebSocket | null = null; + private url: string; + private reconnectInterval: number; + private maxReconnectInterval: number; + private reconnectDecay: number; + private timeout: number; + private forcedClose: boolean = false; + private onmessage?: (ev: MessageEvent) => any; + + constructor( + url: string, + options: ReconnectOptions = {}, + ) { + this.url = url; + this.reconnectInterval = options.reconnectInterval ?? 1000; // 1 second + this.maxReconnectInterval = options.maxReconnectInterval ?? 30000; // 30 seconds + this.reconnectDecay = options.reconnectDecay ?? 1.5; + this.timeout = options.timeout ?? 2000; // 2 seconds + this.connect(); + } + + private connect(isReconnect: boolean = false): void { + console.log(`Connecting to ${this.url}...`); + + this.ws = new WebSocket(this.url); + let connectionTimeout = setTimeout(() => { + console.warn("Connection timeout, closing socket."); + this.ws?.close(); + }, this.timeout); + + this.ws.onopen = (event: Event) => { + clearTimeout(connectionTimeout); + console.log("WebSocket connected."); + + if (this.onmessage) { + this.ws?.addEventListener("message", this.onmessage); + } + + // On connection, send login credentials + // Optionally, if this is a reconnection, you could dispatch a custom event or handle state changes. + }; + + this.ws.onerror = (event: Event) => { + console.error("WebSocket error:", event); + }; + + this.ws.onclose = (event: CloseEvent) => { + clearTimeout(connectionTimeout); + console.log("WebSocket closed:", event.reason); + if (!this.forcedClose) { + // Schedule reconnection with exponential backoff + setTimeout(() => { + this.reconnectInterval = Math.min( + this.reconnectInterval * this.reconnectDecay, + this.maxReconnectInterval, + ); + this.connect(true); + }, this.reconnectInterval); + } + }; + } + + public onMessage(fn: (e: MessageEvent) => void) { + if (this.ws) { + this.ws.addEventListener("message", fn); + } + + this.onmessage = fn; + } + + public send(data: any): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(data); + } else { + console.error("WebSocket is not open. Message not sent."); + } + } + + public close(): void { + this.forcedClose = true; + this.ws?.close(); + } +} + +const ws = new ReconnectingWebSocketClient("/ws"); + +ws.onMessage((e) => { + console.log(e.data); +}); + +const pingBtn = document.getElementById("ping") as HTMLButtonElement; + +pingBtn.onclick = () => { + ws.send("ping"); +}; diff --git a/server/src/lib/apiValidator.ts b/server/src/lib/apiValidator.ts index 7280a3a..e753cf7 100644 --- a/server/src/lib/apiValidator.ts +++ b/server/src/lib/apiValidator.ts @@ -65,6 +65,8 @@ export class Api< } const path = pathSplitted.join("/"); + console.log(data); + const response = await fetch( path, { diff --git a/server/src/lib/devices.ts b/server/src/lib/devices.ts index fa97464..48795ad 100644 --- a/server/src/lib/devices.ts +++ b/server/src/lib/devices.ts @@ -1,15 +1,15 @@ -import usbip, { - CommandExecutionError, - DeviceDetailed, - DeviceDoesNotExistError, - deviceDoesNotExistError, - UsbipUnknownError, -} from "@src/lib/usbip.ts"; +import usbip, { DeviceDetailed } from "@src/lib/usbip.ts"; import { none, Option, some } from "@shared/utils/option.ts"; import { InferSchemaType, z } from "@shared/utils/validator.ts"; import log from "@shared/utils/logger.ts"; import { ResultAsync } from "@shared/utils/resultasync.ts"; import { err, Ok, ok, Result } from "@shared/utils/result.ts"; +import { + CommandExecutionError, + DeviceDoesNotExistError, + deviceDoesNotExistError, + UsbipUnknownError, +} from "@src/lib/errors.ts"; type FailedToAccessDevices = CommandExecutionError | UsbipUnknownError; diff --git a/server/src/lib/errors.ts b/server/src/lib/errors.ts index 1a890b8..a150b44 100644 --- a/server/src/lib/errors.ts +++ b/server/src/lib/errors.ts @@ -116,3 +116,51 @@ export const passwordsMustMatchError = createErrorFactory( export type PasswordsMustMatchError = InferSchemaType< typeof passwordsMustMatchErrorSchema >; + +export const commandExecutionErrorSchema = defineError("CommandExecutionError"); +export const commandExecutionError = createErrorFactory( + commandExecutionErrorSchema, +); +export type CommandExecutionError = InferSchemaType< + typeof commandExecutionErrorSchema +>; + +export const deviceDoesNotExistErrorSchema = defineError( + "DeviceDoesNotExistError", +); +export const deviceDoesNotExistError = createErrorFactory( + deviceDoesNotExistErrorSchema, +); +export type DeviceDoesNotExistError = InferSchemaType< + typeof deviceDoesNotExistErrorSchema +>; + +export const deviceAlreadyBoundErrorSchema = defineError( + "DeviceAlreadyBoundError", +); +export const deviceAlreadyBoundError = createErrorFactory( + deviceAlreadyBoundErrorSchema, +); +export type DeviceAlreadyBoundError = InferSchemaType< + typeof deviceAlreadyBoundErrorSchema +>; + +export const deviceNotBoundErrorSchema = defineError("DeviceNotBoundError"); +export const deviceNotBoundError = createErrorFactory( + deviceNotBoundErrorSchema, +); +export type DeviceNotBoundError = InferSchemaType< + typeof deviceNotBoundErrorSchema +>; + +export const usbipUnknownErrorSchema = defineError("UsbipUnknownError"); +export const usbipUnknownError = createErrorFactory(usbipUnknownErrorSchema); +export type UsbipUnknownError = InferSchemaType; + +export const notFoundErrorSchema = defineError("NotFoundError"); +export const notFoundError = createErrorFactory(notFoundErrorSchema); +export type NotFoundError = InferSchemaType; + +export const notAllowedErrorSchema = defineError("NotAllowedError"); +export const notAllowedError = createErrorFactory(notAllowedErrorSchema); +export type NotAllowedError = InferSchemaType; diff --git a/server/src/lib/router.ts b/server/src/lib/router.ts index 6e7309d..efef64b 100644 --- a/server/src/lib/router.ts +++ b/server/src/lib/router.ts @@ -1,8 +1,10 @@ -import { RouterTree } from "@lib/routerTree.ts"; -import { none, Option, some } from "@shared/utils/option.ts"; -import { Context } from "@lib/context.ts"; +import { RouterTree } from "@src/lib/routerTree.ts"; +import { none, some } from "@shared/utils/option.ts"; +import { Context } from "@src/lib/context.ts"; import { Schema } from "@shared/utils/validator.ts"; import { Api } from "@src/lib/apiValidator.ts"; +import { notAllowedError, notFoundError } from "@src/lib/errors.ts"; +import { err } from "@shared/utils/result.ts"; type RequestHandler< S extends string, @@ -10,44 +12,39 @@ type RequestHandler< ResSchema extends Schema = Schema, > = (c: Context) => Promise | Response; -type RequestHandlerWithSchema = { - handler: RequestHandler; - schema?: { - res: Schema; - req: Schema; - }; -}; export type Middleware = ( c: Context, next: () => Promise, ) => Promise | Response | void; +type MethodHandler = { + handler: RequestHandler; + schema?: { req: Schema; res: Schema }; +}; + type MethodHandlers = Partial< - Record; - schema?: { - res: Schema; - req: Schema; - }; - }> + Record> >; -const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found"); +const DEFAULT_NOT_FOUND_HANDLER = + (() => new Response("404 Not found", { status: 404 })) as RequestHandler< + any + >; class HttpRouter { public readonly routerTree = new RouterTree>(); - public pathPreprocessor?: (path: string) => string; + public pathTransformer?: (path: string) => string; private middlewares: Middleware[] = []; public defaultNotFoundHandler: RequestHandler = DEFAULT_NOT_FOUND_HANDLER; - public setPathProcessor(processor: (path: string) => string) { - this.pathPreprocessor = processor; + public setPathTransformer(transformer: (path: string) => string) { + this.pathTransformer = transformer; return this; } - public use(mw: Middleware): this { - this.middlewares.push(mw); + public use(middleware: Middleware): this { + this.middlewares.push(middleware); return this; } @@ -61,7 +58,6 @@ class HttpRouter { handler: RequestHandler, schema?: { req: ReqSchema; res: ResSchema }, ): HttpRouter; - public add< S extends string, ReqSchema extends Schema = Schema, @@ -72,7 +68,6 @@ class HttpRouter { handler: RequestHandler, schema?: { req: ReqSchema; res: ResSchema }, ): HttpRouter; - public add( path: string | string[], method: string, @@ -83,13 +78,13 @@ class HttpRouter { for (const p of paths) { this.routerTree.getHandler(p).match( - (mth) => { - mth[method] = { handler, schema }; + (existingHandlers) => { + existingHandlers[method] = { handler, schema }; }, () => { - const mth: MethodHandlers = {}; - mth[method] = { handler, schema }; - this.routerTree.add(p, mth); + const newHandlers: MethodHandlers = {}; + newHandlers[method] = { handler, schema }; + this.routerTree.add(p, newHandlers); }, ); } @@ -149,19 +144,46 @@ class HttpRouter { connInfo: Deno.ServeHandlerInfo, ): Promise { let ctx = new Context(req, connInfo, {}); + const path = this.pathTransformer + ? this.pathTransformer(ctx.path) + : ctx.path; let routeParams: Record = {}; - const path = this.pathPreprocessor - ? this.pathPreprocessor(ctx.path) - : ctx.path; const handler = this.routerTree .find(path) .andThen((match) => { const { value: methodHandler, params: params } = match; routeParams = params; - const route = methodHandler[req.method]; - if (!route) return none; + + let route = methodHandler[req.method]; + + if (!route) { + if (req.method === "HEAD") { + const getHandler = methodHandler["GET"]; + if (!getHandler) { + return none; + } + route = getHandler; + } else if ( + ctx.preferredType.map((v) => v === "json") + .toBoolean() && + req.method !== "GET" + ) { + return some( + (() => + ctx.json( + err(notAllowedError( + "405 Not allowed", + )), + { + status: 405, + }, + )) as RequestHandler, + ); + } + return none; + } if (route.schema) { ctx = ctx.setSchema(route.schema); } @@ -169,7 +191,22 @@ class HttpRouter { return some(handler); }) - .unwrapOrElse(() => this.defaultNotFoundHandler); + .unwrapOrElse(() => { + switch (ctx.preferredType.unwrapOr("other")) { + case "json": + return (() => + ctx.json(err(notFoundError("404 Not found")), { + status: 404, + })) as RequestHandler; + case "html": + return (() => + ctx.html("404 Not found", { + status: 404, + })) as RequestHandler; + case "other": + return DEFAULT_NOT_FOUND_HANDLER; + } + }); const res = (await this.executeMiddlewareChain( this.middlewares, @@ -177,9 +214,27 @@ class HttpRouter { ctx = ctx.setParams(routeParams), )).res; + if (req.method === "HEAD") { + const headers = new Headers(res.headers); + headers.set("Content-Length", "0"); + return new Response(null, { + headers, + status: res.status, + statusText: res.statusText, + }); + } + return res; } + private resolveRoute( + ctx: Context, + req: Request, + path: string, + ): { handler: RequestHandler; params: Record } { + const routeOption = this.routerTree.find(path); + } + private async executeMiddlewareChain( middlewares: Middleware[], handler: RequestHandler, diff --git a/server/src/lib/usbip.ts b/server/src/lib/usbip.ts index 977d5c7..93ea2d9 100644 --- a/server/src/lib/usbip.ts +++ b/server/src/lib/usbip.ts @@ -8,48 +8,18 @@ import { type Option, some, } from "@shared/utils/option.ts"; -import { createErrorFactory, defineError } from "@shared/utils/errors.ts"; -import { InferSchemaType } from "@shared/utils/validator.ts"; - -export const commandExecutionErrorSchema = defineError("CommandExecutionError"); -export const commandExecutionError = createErrorFactory( - commandExecutionErrorSchema, -); -export type CommandExecutionError = InferSchemaType< - typeof commandExecutionErrorSchema ->; - -export const deviceDoesNotExistErrorSchema = defineError( - "DeviceDoesNotExistError", -); -export const deviceDoesNotExistError = createErrorFactory( - deviceDoesNotExistErrorSchema, -); -export type DeviceDoesNotExistError = InferSchemaType< - typeof deviceDoesNotExistErrorSchema ->; - -export const deviceAlreadyBoundErrorSchema = defineError( - "DeviceAlreadyBoundError", -); -export const deviceAlreadyBoundError = createErrorFactory( - deviceAlreadyBoundErrorSchema, -); -export type DeviceAlreadyBoundError = InferSchemaType< - typeof deviceAlreadyBoundErrorSchema ->; - -export const deviceNotBoundErrorSchema = defineError("DeviceNotBoundError"); -export const deviceNotBoundError = createErrorFactory( - deviceNotBoundErrorSchema, -); -export type DeviceNotBoundError = InferSchemaType< - typeof deviceNotBoundErrorSchema ->; - -export const usbipUnknownErrorSchema = defineError("UsbipUnknownError"); -export const usbipUnknownError = createErrorFactory(usbipUnknownErrorSchema); -export type UsbipUnknownError = InferSchemaType; +import { + CommandExecutionError, + commandExecutionError, + DeviceAlreadyBoundError, + deviceAlreadyBoundError, + DeviceDoesNotExistError, + deviceDoesNotExistError, + DeviceNotBoundError, + deviceNotBoundError, + UsbipUnknownError, + usbipUnknownError, +} from "@src/lib/errors.ts"; type UsbipCommonError = DeviceDoesNotExistError | UsbipUnknownError; diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 56437d2..87913c1 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -8,8 +8,7 @@ import { import { err, ok } from "@shared/utils/result.ts"; import { eta } from "../../main.ts"; -const LOGIN_PATH = "/login"; -const SETUP_PATH = "/setup"; +const EXCLUDE = new Set(["/login", "/setup", "/version"]); const authMiddleware: Middleware = async (c, next) => { const token = c.cookies.get("token"); @@ -33,8 +32,7 @@ const authMiddleware: Middleware = async (c, next) => { const path = c.path; if ( - !isValid.value && !path.startsWith("/public") && path !== LOGIN_PATH && - path !== SETUP_PATH + !isValid.value && !path.startsWith("/public") && !EXCLUDE.has(path) ) { if (!isValid.value) { c.cookies.delete("token"); diff --git a/server/src/views/index.html b/server/src/views/index.html index 846e40c..55f6df4 100644 --- a/server/src/views/index.html +++ b/server/src/views/index.html @@ -1,3 +1,13 @@ <% layout("./layouts/layout.html") %> + devices: + <% it.devices.forEach(function(device){ %> +
+ name: <%= device.name %> | <%= device.vendor %> + busid: <%= device.busid %> +
+ <%= device.busid %> + <% }) %> - + + + diff --git a/server/test.db b/server/test.db index 1b91532..7d2fd6d 100644 Binary files a/server/test.db and b/server/test.db differ diff --git a/server/views/index.html b/server/views/index.html index bc1eab5..ecdd061 100644 --- a/server/views/index.html +++ b/server/views/index.html @@ -1,6 +1 @@ -<% layout("./layouts/layout.html") %> - - devices: -
- - +<% layout("./layouts/layout.html") %> devices: <% it.devices.forEach(function(device){ %>
name: <%= device.name %> | <%= device.vendor %> busid: <%= device.busid %>
<%= device.busid %> <% }) %> \ No newline at end of file