From e1cc82f7bdc3bc12ce404da310aaa6a676a1687e Mon Sep 17 00:00:00 2001 From: ton1c Date: Sat, 26 Apr 2025 17:08:53 +0300 Subject: [PATCH] working on websocket --- server/main.ts | 42 +++++++++---------- server/public/js/shared.bundle.js | 2 +- server/src/lib/context.ts | 13 ++++-- server/src/lib/router.ts | 60 ++++++++++++++++----------- server/src/lib/websocket.ts | 27 ++++++++---- server/src/lib/wsClient.ts | 8 ++-- server/src/middleware/auth.ts | 13 +++--- server/src/middleware/logger.ts | 3 +- server/src/middleware/rateLimiter.ts | 5 ++- server/test.db | Bin 53248 -> 53248 bytes 10 files changed, 99 insertions(+), 74 deletions(-) diff --git a/server/main.ts b/server/main.ts index de9e1e2..c3e1d97 100644 --- a/server/main.ts +++ b/server/main.ts @@ -23,11 +23,16 @@ import { } from "@src/lib/errors.ts"; import devices from "@src/lib/devices.ts"; import { WebSocketClientsGroup } from "@src/lib/websocket.ts"; +import { Option } from "@shared/utils/option.ts"; const AUTH_COOKIE_NAME = "token"; const VERSION = "0.1.0-a.1"; -const router = new HttpRouter(); +export type Variables = { + token: string; +}; + +const router = new HttpRouter(); const views = Deno.cwd() + "/views/"; export const eta = new Eta({ views }); @@ -41,15 +46,15 @@ const cache: Map = new Map(); router.get("/public/*", async (c) => { const filePath = "." + c.path; - //const cached = cache.get(filePath); - // - //if (cached) { - // return cached.clone(); - //} + const cached = cache.get(filePath); + + // if (cached) { + // return cached.clone(); + // } const res = await serveFile(c.req, filePath); - //cache.set(filePath, res.clone()); + // cache.set(filePath, res.clone()); return res; }); @@ -93,30 +98,21 @@ router }); const group = new WebSocketClientsGroup(); +group.onmessage = (e) => { + group.sendToAll("pong"); + console.log("ping"); +}; router.get("/api/admin/ws", (c) => { if (c.req.headers.get("upgrade") != "websocket") { return new Response(null, { status: 501 }); } - const { socket, response } = Deno.upgradeWebSocket(c.req); + const token = c.var.get("token"); - group.addClient(socket); + let { 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("ping"); - socket.send("pong"); - } - }); + socket = group.addClient(token, socket).unwrap(); return response; }); diff --git a/server/public/js/shared.bundle.js b/server/public/js/shared.bundle.js index 059a05d..77b4f27 100644 --- a/server/public/js/shared.bundle.js +++ b/server/public/js/shared.bundle.js @@ -1 +1 @@ -var f=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new h(s)).catch(s=>new S(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new h(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}static from(e){return t.fromPromise(new Promise(e),r=>r)}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 S(r.error):new h(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?T(r.error):new h(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new S(e(r.error)):new h(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?T(await e(r.error)):new h(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?T(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?T(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?g(e):E)}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 b(t){return new f(Promise.resolve(new h(t)))}function T(t){return new f(Promise.resolve(new S(t)))}var h=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 x}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 E}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return f.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof x||this.value instanceof w?u(this.value.map(e)):u(g(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(this.value)}mapErrAsync(e){return b(this.value)}flatten(){return ce(this)}flattenOption(e){return this.value instanceof x||this.value instanceof w?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof x||this.value instanceof w?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof x||this.value instanceof w?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return b(this.value)}void(){return u()}toJSON(){return{tag:"ok",value:this.value}}},S=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: ${ue(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return g(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return T(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return f.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 ce(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 T(this.error)}void(){return o(this.error)}toJSON(){return{tag:"err",error:this.error}}};function u(t){return new h(t)}function o(t){return new S(t)}function Ke(t,e){return(...r)=>{try{let n=t(...r);return u(n)}catch(n){return o(e?e(n):n)}}}function ue(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 ce(t){let e=t;for(;e instanceof h&&(e.value instanceof h||e.value instanceof S);)e=e.value;return e}var O=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function We(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return o(new O(ue(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 w=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)}},x=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 E}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 g(t){return new w(t)}var E=new x;function Xe(t){return t?g(t):E}var N=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 a(t,e){return new N(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()}},V=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")}},M=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})))}},F=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)}},j=class extends i{validateInput(e){return i.validatePrimitive(e,"boolean",this.msg)}},D=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}))})}},C=class extends i{validateInput(e){return i.validatePrimitive(e,"symbol",this.msg)}},q=class extends i{validateInput(e){return i.validatePrimitive(e,"undefined",this.msg)}},$=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}))}},L=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}))}},J=class extends i{validateInput(e){return u(e)}},K=class extends i{validateInput(e){return u(e)}},W=class extends i{validateInput(e){return o(a(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},z=class extends i{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 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={},l=new Set(Object.keys(this.shape));for(let y of Object.keys(n)){let v=this.shape[y];if(v===void 0){if(this.strictMode){let me=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return o(a(r,{kind:"unexpectedProperties",keys:me,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let k=v.parse(n[y]);if(k.isErr())return o(a(r,{kind:"propertyValidation",property:y,detail:k.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));l.delete(y),s[y]=k.value}let m=l.keys().filter(y=>!i.isNullishSchema(this.shape[y])).toArray();return m.length>0?o(a(r,{kind:"missingProperties",keys:m,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)}},G=class t extends i{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 m=l.parse(r);if(m.isOk())return u(m.value);s=m.error.detail?.kind==="typeMismatch"&&s,n.push(m.error.detail)}return s?o(a(r,{kind:"typeMismatch",expected:this.schemas.map(l=>t.getTypeFromSchemaName(l.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}))}},Q=class extends i{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 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(g(s)),s=>o(a(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):i.isNullishSchema(this.schema)?u(g()):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(E);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"}))})}},ee=class extends i{constructor(r,n){super(n);this.entries=r}validateInput(r){for(let n of this.entries)if(r===n)return u(r);return o(a(r,{kind:"typeMismatch",expected:this.entries.map(n=>typeof n=="string"?`"${n}"`:n).join(" | "),received:String(r),msg:this.msg}))}},c={string:t=>new V(t),literal:(t,e)=>new M(t,e),number:t=>new F(t),bigint:t=>new B(t),boolean:t=>new j(t),date:t=>new D(t),symbol:t=>new C(t),undefined:t=>new q(t),null:t=>new $(t),void:t=>new L(t),any:t=>new J(t),unknown:t=>new K(t),never:t=>new W(t),obj:(t,e)=>new z(t,e),union:(t,e)=>new G(t,e),array:(t,e)=>new Q(t,e),optional:(t,e)=>new X(t,e),nullable:(t,e)=>new H(t,e),nullish:(t,e)=>new Z(t,e),result:(t,e)=>new Y(t,e),option:t=>new _(t),enum:(t,e)=>new ee(t,e)};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 A=p("QueryExecutionError"),fr=d(A),he=p("NoAdminEntryError"),Tr=d(he),Ee=p("FailedToReadFileError"),gr=d(Ee),ye=p("InvalidSyntaxError"),vr=d(ye),fe=p("InvalidPathError"),Sr=d(fe),re=p("AdminPasswordNotSetError"),xr=d(re),P=p("RequestValidationError"),le=d(P),Te=p("ResponseValidationError"),pe=d(Te),I=p("FailedToParseRequestAsJSONError"),wr=d(I),U=p("TooManyRequestsError"),Rr=d(U),te=p("UnauthorizedError"),Ur=d(te),ne=p("InvalidPasswordError"),kr=d(ne),se=p("AdminPasswordAlreadySetError"),br=d(se),oe=p("PasswordsMustMatchError"),Or=d(oe),ie=p("CommandExecutionError"),Ar=d(ie),ge=p("DeviceDoesNotExistError"),Pr=d(ge),ve=p("DeviceAlreadyBoundError"),Ir=d(ve),Se=p("DeviceNotBoundError"),Nr=d(Se),ae=p("UsbipUnknownError"),Vr=d(ae),xe=p("NotFoundError"),Mr=d(xe),we=p("NotAllowedError"),Fr=d(we),Re=p("tooManyConnectionError"),Br=d(Re);var R=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,m)=>(l.startsWith(":")&&(s[l.slice(1)]=m),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[v,k]of Object.entries(r))s[this.paramIndexes[v]]=k;let l=s.join("/");console.log(n);let y=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(y).toAsync().map(v=>v).mapErr(v=>pe(v.info))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var Ue={req:c.obj({password:c.string()}),res:c.result(c.void(),c.union([re,A,I,P,U,ne]))},Wr=new R("/login","POST",Ue),ke={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 a(t,{kind:"general",msg:"Passwords must match"})}),res:c.result(c.void(),c.union([oe,se,A,I,P,U]))},zr=new R("/setup","POST",ke),be={req:c.void(),res:c.result(c.void(),c.union([A,U,te,ie,ae]))},Gr=new R("/api/devices/detect","GET",be),Oe={req:c.void(),res:c.result(c.obj({app:c.literal("Keyborg"),version:c.string()}),c.union([U]))},Qr=new R("/api/version","GET",Oe);var Ae=2e3,Pe=1e3,Ie=15e3,Ne=5,Ve=5,de=class{constructor(e,r,n=Ae,s=Pe){this.url=e;this.schema=r;this.timeout=n;this.pingInterval=s}_ws=E;get ws(){return this._ws}set ws(e){this._ws=e,e.isSome()?(e.value.addEventListener("close",this.handleWebSocketClose),e.value.addEventListener("error",this.handleWebSocketError),e.value.addEventListener("message",this.onMessage),this.onConnectSucc(),this.pingAndWait()):this.onDisconnect()}handleWebSocketClose=()=>{this._ws=E,this.connect()};handleWebSocketError=()=>{this._ws=E,this.connect()};pingAndWait=async()=>{(await this.ping()).isErr()&&(clearTimeout(this.pingTimer),this.ws=E),this.pingTimer=setTimeout(this.pingAndWait,Ie)};pingTimer;onConnectInit;onConnectSucc;onConnectFail;onDisconnect;onMessage;isConnecting=!1;ping(){if(this.ws.isNone())return T();let e=this.ws.value;return f.from((r,n)=>{let s,l=v=>{v.data==="pong"&&(e.removeEventListener("message",l),clearTimeout(s),r())};e.addEventListener("message",l);let m=0,y=()=>{++m>Ne&&n(),e.send("ping"),s=setTimeout(y,this.pingInterval)};y()})}createWebSocketConnection(){let e=new WebSocket(this.url);return f.from((r,n)=>{let s=()=>{n()};e.addEventListener("open",()=>{e.removeEventListener("error",s),r(e)}),e.addEventListener("error",s)})}connect(){return this.isConnecting?T():f.fromSafePromise(this.ping().match(()=>b(),()=>{this.isConnecting=!0,this.onConnectInit(),this.tryReconnect()})).flatten()}tryReconnect(){return f.from((e,r)=>{let n=0,s,l=async()=>{if(console.log(`attempt ${n+1}`),++n>=Ve){this.onConnectFail(),this.isConnecting=!1,console.error("Failed to connect"),r();return}let m=await this.createWebSocketConnection();m.isOk()?(this.ws=g(m.value),clearTimeout(s),this.isConnecting=!1,e()):s=setTimeout(l,this.timeout)};l()})}send(e){if(this.ws.isNone())return T()}},rt=c.obj({id:c.number(),kind:c.enum(["up"])});export{i as BaseSchema,ee as EnumSchema,S as Err,M as LiteralSchema,x as None,z as ObjectSchema,h as Ok,_ as OptionSchema,f as ResultAsync,We as ResultFromJSON,Y as ResultSchema,N as SchemaValidationError,w as Some,V as StringSchema,de as WebSocketWrapper,a as createValidationError,o as err,T as errAsync,ce as flattenResult,Xe as fromNullableVal,Ke as fromThrowable,ue as getMessageFromError,Wr as loginApi,E as none,u as ok,b as okAsync,zr as passwordSetupApi,g as some,Gr as updateDevicesApi,Qr as versionApi,c as z}; +var f=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new h(s)).catch(s=>new S(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new h(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}static from(e){return t.fromPromise(new Promise(e),r=>r)}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 S(r.error):new h(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?T(r.error):new h(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new S(e(r.error)):new h(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?T(await e(r.error)):new h(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?T(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?T(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?g(e):E)}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 b(t){return new f(Promise.resolve(new h(t)))}function T(t){return new f(Promise.resolve(new S(t)))}var h=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 x}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 E}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return f.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof x||this.value instanceof w?u(this.value.map(e)):u(g(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(this.value)}mapErrAsync(e){return b(this.value)}flatten(){return ce(this)}flattenOption(e){return this.value instanceof x||this.value instanceof w?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof x||this.value instanceof w?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof x||this.value instanceof w?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return b(this.value)}void(){return u()}toJSON(){return{tag:"ok",value:this.value}}},S=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: ${ue(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return g(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return T(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return f.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 ce(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 T(this.error)}void(){return o(this.error)}toJSON(){return{tag:"err",error:this.error}}};function u(t){return new h(t)}function o(t){return new S(t)}function We(t,e){return(...r)=>{try{let n=t(...r);return u(n)}catch(n){return o(e?e(n):n)}}}function ue(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 ce(t){let e=t;for(;e instanceof h&&(e.value instanceof h||e.value instanceof S);)e=e.value;return e}var O=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function ze(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return o(new O(ue(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 w=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)}},x=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 E}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 g(t){return new w(t)}var E=new x;function He(t){return t?g(t):E}var N=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 a(t,e){return new N(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()}},V=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")}},M=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})))}},F=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)}},j=class extends i{validateInput(e){return i.validatePrimitive(e,"boolean",this.msg)}},D=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}))})}},C=class extends i{validateInput(e){return i.validatePrimitive(e,"symbol",this.msg)}},q=class extends i{validateInput(e){return i.validatePrimitive(e,"undefined",this.msg)}},$=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}))}},L=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}))}},J=class extends i{validateInput(e){return u(e)}},K=class extends i{validateInput(e){return u(e)}},W=class extends i{validateInput(e){return o(a(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},z=class extends i{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 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={},l=new Set(Object.keys(this.shape));for(let y of Object.keys(n)){let v=this.shape[y];if(v===void 0){if(this.strictMode){let me=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return o(a(r,{kind:"unexpectedProperties",keys:me,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let k=v.parse(n[y]);if(k.isErr())return o(a(r,{kind:"propertyValidation",property:y,detail:k.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));l.delete(y),s[y]=k.value}let m=l.keys().filter(y=>!i.isNullishSchema(this.shape[y])).toArray();return m.length>0?o(a(r,{kind:"missingProperties",keys:m,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)}},G=class t extends i{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 m=l.parse(r);if(m.isOk())return u(m.value);s=m.error.detail?.kind==="typeMismatch"&&s,n.push(m.error.detail)}return s?o(a(r,{kind:"typeMismatch",expected:this.schemas.map(l=>t.getTypeFromSchemaName(l.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}))}},Q=class extends i{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 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(g(s)),s=>o(a(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):i.isNullishSchema(this.schema)?u(g()):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(E);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"}))})}},ee=class extends i{constructor(r,n){super(n);this.entries=r}validateInput(r){for(let n of this.entries)if(r===n)return u(r);return o(a(r,{kind:"typeMismatch",expected:this.entries.map(n=>typeof n=="string"?`"${n}"`:n).join(" | "),received:String(r),msg:this.msg}))}},c={string:t=>new V(t),literal:(t,e)=>new M(t,e),number:t=>new F(t),bigint:t=>new B(t),boolean:t=>new j(t),date:t=>new D(t),symbol:t=>new C(t),undefined:t=>new q(t),null:t=>new $(t),void:t=>new L(t),any:t=>new J(t),unknown:t=>new K(t),never:t=>new W(t),obj:(t,e)=>new z(t,e),union:(t,e)=>new G(t,e),array:(t,e)=>new Q(t,e),optional:(t,e)=>new X(t,e),nullable:(t,e)=>new H(t,e),nullish:(t,e)=>new Z(t,e),result:(t,e)=>new Y(t,e),option:t=>new _(t),enum:(t,e)=>new ee(t,e)};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 A=p("QueryExecutionError"),Tr=d(A),he=p("NoAdminEntryError"),gr=d(he),Ee=p("FailedToReadFileError"),vr=d(Ee),ye=p("InvalidSyntaxError"),Sr=d(ye),fe=p("InvalidPathError"),xr=d(fe),re=p("AdminPasswordNotSetError"),wr=d(re),P=p("RequestValidationError"),le=d(P),Te=p("ResponseValidationError"),pe=d(Te),I=p("FailedToParseRequestAsJSONError"),Rr=d(I),U=p("TooManyRequestsError"),Ur=d(U),te=p("UnauthorizedError"),kr=d(te),ne=p("InvalidPasswordError"),br=d(ne),se=p("AdminPasswordAlreadySetError"),Or=d(se),oe=p("PasswordsMustMatchError"),Ar=d(oe),ie=p("CommandExecutionError"),Pr=d(ie),ge=p("DeviceDoesNotExistError"),Ir=d(ge),ve=p("DeviceAlreadyBoundError"),Nr=d(ve),Se=p("DeviceNotBoundError"),Vr=d(Se),ae=p("UsbipUnknownError"),Mr=d(ae),xe=p("NotFoundError"),Fr=d(xe),we=p("NotAllowedError"),Br=d(we),Re=p("tooManyConnectionError"),jr=d(Re),Ue=p("WebSocketMsgSendError"),Dr=d(Ue);var R=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,m)=>(l.startsWith(":")&&(s[l.slice(1)]=m),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[v,k]of Object.entries(r))s[this.paramIndexes[v]]=k;let l=s.join("/");console.log(n);let y=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(y).toAsync().map(v=>v).mapErr(v=>pe(v.info))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var ke={req:c.obj({password:c.string()}),res:c.result(c.void(),c.union([re,A,I,P,U,ne]))},Gr=new R("/login","POST",ke),be={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 a(t,{kind:"general",msg:"Passwords must match"})}),res:c.result(c.void(),c.union([oe,se,A,I,P,U]))},Qr=new R("/setup","POST",be),Oe={req:c.void(),res:c.result(c.void(),c.union([A,U,te,ie,ae]))},Xr=new R("/api/devices/detect","GET",Oe),Ae={req:c.void(),res:c.result(c.obj({app:c.literal("Keyborg"),version:c.string()}),c.union([U]))},Hr=new R("/api/version","GET",Ae);var Pe=2e3,Ie=5e3,Ne=15e3,Ve=5,Me=5,de=class{constructor(e,r,n=Pe,s=Ie){this.url=e;this.schema=r;this.timeout=n;this.pingInterval=s}_ws=E;get ws(){return this._ws}set ws(e){this._ws=e,e.isSome()?(e.value.addEventListener("close",this.handleWebSocketClose),e.value.addEventListener("error",this.handleWebSocketError),e.value.addEventListener("message",this.onMessage),this.onConnectSucc(),this.pingAndWait()):this.onDisconnect()}handleWebSocketClose=()=>{this._ws=E,this.connect()};handleWebSocketError=()=>{this._ws=E,this.connect()};pingAndWait=async()=>{(await this.ping()).isErr()&&(clearTimeout(this.pingTimer),this.ws=E),this.pingTimer=setTimeout(this.pingAndWait,Ne)};pingTimer;onConnectInit;onConnectSucc;onConnectFail;onDisconnect;onMessage;isConnecting=!1;ping(){if(this.ws.isNone())return T();let e=this.ws.value;return f.from((r,n)=>{let s,l=v=>{v.data==="pong"&&(e.removeEventListener("message",l),clearTimeout(s),r())};e.addEventListener("message",l);let m=0,y=()=>{++m>Ve&&n(),e.send("ping"),s=setTimeout(y,this.pingInterval)};y()})}createWebSocketConnection(){let e=new WebSocket(this.url);return f.from((r,n)=>{let s=()=>{n()};e.addEventListener("open",()=>{e.removeEventListener("error",s),r(e)}),e.addEventListener("error",s)})}connect(){return this.isConnecting?T():f.fromSafePromise(this.ping().match(()=>b(),()=>{this.isConnecting=!0,this.onConnectInit(),this.tryReconnect()})).flatten()}tryReconnect(){return f.from((e,r)=>{let n=0,s,l=async()=>{if(console.log(`attempt ${n+1}`),++n>=Me){this.onConnectFail(),this.isConnecting=!1,console.error("Failed to connect"),r();return}let m=await this.createWebSocketConnection();m.isOk()?(this.ws=g(m.value),clearTimeout(s),this.isConnecting=!1,e()):s=setTimeout(l,this.timeout)};l()})}send(e){if(this.ws.isNone())return T()}},nt=c.obj({id:c.number(),kind:c.enum(["up"])});export{i as BaseSchema,ee as EnumSchema,S as Err,M as LiteralSchema,x as None,z as ObjectSchema,h as Ok,_ as OptionSchema,f as ResultAsync,ze as ResultFromJSON,Y as ResultSchema,N as SchemaValidationError,w as Some,V as StringSchema,de as WebSocketWrapper,a as createValidationError,o as err,T as errAsync,ce as flattenResult,He as fromNullableVal,We as fromThrowable,ue as getMessageFromError,Gr as loginApi,E as none,u as ok,b as okAsync,Qr as passwordSetupApi,g as some,Xr as updateDevicesApi,Hr as versionApi,c as z}; diff --git a/server/src/lib/context.ts b/server/src/lib/context.ts index 7a260fc..880642a 100644 --- a/server/src/lib/context.ts +++ b/server/src/lib/context.ts @@ -50,7 +50,10 @@ export class Context< S extends string = string, ReqSchema extends Schema = Schema, ResSchema extends Schema = Schema, - Vars extends Record = Record, + Variables extends Record = Record< + string | number, + any + >, > { private _url?: URL; private _hostname?: string; @@ -80,6 +83,7 @@ export class Context< ctx._cookies = this._cookies; ctx.res = this.res; ctx.schema = schema; + ctx._var = this._var; return ctx as Context & { schema: { req: Req; res: Res } }; } @@ -97,6 +101,7 @@ export class Context< ctx._cookies = this._cookies; ctx.res = this.res; ctx.schema = this.schema; + ctx._var = this._var; return ctx as Context; } @@ -260,14 +265,14 @@ export class Context< }; } - private _var: Vars = {} as Vars; + private _var: Variables = {} as Variables; public get var() { return { - set: (key: keyof Vars, value: Vars[number]) => { + set: (key: K, value: Variables[K]) => { this._var[key] = value; }, - get: (key: K): Vars[K] => { + get: (key: K): Variables[K] => { return this._var[key]; }, }; diff --git a/server/src/lib/router.ts b/server/src/lib/router.ts index 2cff89d..a61d9ad 100644 --- a/server/src/lib/router.ts +++ b/server/src/lib/router.ts @@ -5,14 +5,24 @@ import { Api } from "@src/lib/apiValidator.ts"; import { notAllowedError, notFoundError } from "@src/lib/errors.ts"; import { err } from "@shared/utils/result.ts"; +type VariablesType = Record; + type RequestHandler< S extends string = string, ReqSchema extends Schema = Schema, ResSchema extends Schema = Schema, -> = (c: Context) => Promise | Response; + Variables extends VariablesType = Record< + string | number, + any + >, +> = ( + c: Context, +) => Promise | Response; -export type Middleware = ( - c: Context, +export type Middleware< + Variables extends VariablesType = Partial>, +> = ( + c: Context, next: () => Promise, ) => Promise | Response | void; @@ -35,14 +45,16 @@ const DEFAULT_NOT_ALLOWED_HANDLER = })) as RequestHandler; class HttpRouter< - Variables extends Record = Record< - string | number, - any + Variables extends VariablesType = Partial< + Record< + string | number, + any + > >, > { public readonly routerTree = new RouterTree(); public pathTransformer?: (path: string) => string; - private middlewares: Middleware[] = []; + private middlewares: Middleware[] = []; public notFoundHandler: RequestHandler = DEFAULT_NOT_FOUND_HANDLER; public methodNotAllowedHandler: RequestHandler = DEFAULT_NOT_ALLOWED_HANDLER; @@ -52,7 +64,7 @@ class HttpRouter< return this; } - public use(middleware: Middleware): this { + public use(middleware: Middleware): this { this.middlewares.push(middleware); return this; } @@ -64,9 +76,9 @@ class HttpRouter< >( path: S, method: string, - handler: RequestHandler, + handler: RequestHandler, schema?: { req: ReqSchema; res: ResSchema }, - ): HttpRouter; + ): this; public add< S extends string, ReqSchema extends Schema = Schema, @@ -74,15 +86,15 @@ class HttpRouter< >( path: S[], method: string, - handler: RequestHandler, + handler: RequestHandler, schema?: { req: ReqSchema; res: ResSchema }, - ): HttpRouter; + ): this; public add( path: string | string[], method: string, handler: RequestHandler, schema?: { req: Schema; res: Schema }, - ): HttpRouter { + ): this { const paths = Array.isArray(path) ? path : [path]; for (const p of paths) { @@ -103,16 +115,16 @@ class HttpRouter< public get( path: S, - handler: RequestHandler, - ): HttpRouter; + handler: RequestHandler, + ): this; public get( path: S[], - handler: RequestHandler, - ): HttpRouter; + handler: RequestHandler, + ): this; public get( path: string | string[], - handler: RequestHandler, - ): HttpRouter { + handler: RequestHandler, + ): this { if (Array.isArray(path)) { return this.add(path, "GET", handler); } @@ -121,15 +133,15 @@ class HttpRouter< public post( path: S, - handler: RequestHandler, + handler: RequestHandler, ): HttpRouter; public post( path: string[], - handler: RequestHandler, + handler: RequestHandler, ): HttpRouter; public post( path: string | string[], - handler: RequestHandler, + handler: RequestHandler, ): HttpRouter { if (Array.isArray(path)) { return this.add(path, "POST", handler); @@ -143,7 +155,7 @@ class HttpRouter< ResSchema extends Schema, >( api: Api, - handler: RequestHandler, + handler: RequestHandler, ): HttpRouter { return this.add(api.path, api.method, handler, api.schema); } @@ -171,7 +183,7 @@ class HttpRouter< ): { handler: RequestHandler; params: Record; - ctx: Context; + ctx: Context; } { const routeOption = this.routerTree.find(path); diff --git a/server/src/lib/websocket.ts b/server/src/lib/websocket.ts index c63ceb6..7b89e44 100644 --- a/server/src/lib/websocket.ts +++ b/server/src/lib/websocket.ts @@ -34,13 +34,14 @@ export class WebSocketClientsGroup< public onopen?: EventListenerOrEventListenerObject, public onclose?: EventListenerOrEventListenerObject, public onerror?: EventListenerOrEventListenerObject, - public onmessage?: EventListenerOrEventListenerObject, + public onmessage?: (e: MessageEvent) => any, ) {} public addClient( token: string, socket: WebSocket, - ): Result { + lifetime?: Date, + ): Result { if (this.connectionsCounter > MAX_CONNECTIONS) { return err(tooManyConnectionError("Too many connections")); } @@ -61,18 +62,28 @@ export class WebSocketClientsGroup< clientConnections.set(uuid, socket); socket.addEventListener("close", () => { clientConnections.delete(uuid); + this.connectionsCounter--; }); socket.addEventListener("error", () => { clientConnections.delete(uuid); + this.connectionsCounter--; }); this.connectionsCounter++; - socket.addEventListener("open", this.onopen!); - socket.addEventListener("open", this.onclose!); - socket.addEventListener("open", this.onerror!); - socket.addEventListener("open", this.onmessage!); + if (this.onopen) { + socket.addEventListener("open", this.onopen); + } + if (this.onclose) { + socket.addEventListener("close", this.onclose); + } + if (this.onerror) { + socket.addEventListener("error", this.onerror); + } + if (this.onmessage) { + socket.addEventListener("message", this.onmessage); + } - return ok(); + return ok(socket); } sendToAll( @@ -82,7 +93,7 @@ export class WebSocketClientsGroup< .andThen((msg) => { const errors = []; for (const client of this.clients.values()) { - for (const connection of client) { + for (const connection of client.values()) { try { connection.send(JSON.stringify(msg)); } catch (e) { diff --git a/server/src/lib/wsClient.ts b/server/src/lib/wsClient.ts index be403f5..9571355 100644 --- a/server/src/lib/wsClient.ts +++ b/server/src/lib/wsClient.ts @@ -3,14 +3,14 @@ import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; import { InferSchemaType, Schema, z } from "@shared/utils/validator.ts"; const CONNECTION_TIMEOUT_MS = 2000; -const PING_INTERVAL_MS = 1000; +const PING_INTERVAL_MS = 5000; const PING_CHECK_INTERVAL_MS = 15000; const MAX_PING_ATTEMPTS = 5; const MAX_RECONNECTION_ATTEMPTS = 5; export class WebSocketWrapper< - R extends Schema = Schema, - S extends Schema = Schema, + R extends Schema = Schema, + S extends Schema = Schema, > { private _ws: Option = none; get ws(): Option { @@ -58,7 +58,7 @@ export class WebSocketWrapper< public onDisconnect?: () => void; - public onMessage?: (ev: MessageEvent) => void; + public onMessage?: (ev: MessageEvent) => void; private isConnecting = false; diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 87913c1..b2b288d 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -1,16 +1,12 @@ import { Middleware } from "@lib/router.ts"; import admin from "@lib/admin.ts"; -import { - queryExecutionError, - tooManyRequestsError, - unauthorizedError, -} from "@src/lib/errors.ts"; +import { queryExecutionError, unauthorizedError } from "@lib/errors.ts"; import { err, ok } from "@shared/utils/result.ts"; -import { eta } from "../../main.ts"; +import { eta, Variables } from "../../main.ts"; const EXCLUDE = new Set(["/login", "/setup", "/version"]); -const authMiddleware: Middleware = async (c, next) => { +const authMiddleware: Middleware = async (c, next) => { const token = c.cookies.get("token"); const isValid = token .map((token) => admin.sessions.verifyToken(token)).match( @@ -52,6 +48,9 @@ const authMiddleware: Middleware = async (c, next) => { return c.redirect("/login"); } } + + c.var.set("token", token.unwrapOr("")); + await next(); }; diff --git a/server/src/middleware/logger.ts b/server/src/middleware/logger.ts index 20ede98..b43eef6 100644 --- a/server/src/middleware/logger.ts +++ b/server/src/middleware/logger.ts @@ -1,6 +1,7 @@ import { Middleware } from "@lib/router.ts"; +import { Variables } from "../../main.ts"; -const loggerMiddleware: Middleware = async (c, next) => { +const loggerMiddleware: Middleware = async (c, next) => { console.log("", c.req.method, c.path); await next(); console.log("", c.res.status, "\n"); diff --git a/server/src/middleware/rateLimiter.ts b/server/src/middleware/rateLimiter.ts index e944037..f154dde 100644 --- a/server/src/middleware/rateLimiter.ts +++ b/server/src/middleware/rateLimiter.ts @@ -1,7 +1,8 @@ import { Middleware } from "@lib/router.ts"; import log from "@shared/utils/logger.ts"; import { err } from "@shared/utils/result.ts"; -import { tooManyRequestsError } from "@src/lib/errors.ts"; +import { tooManyRequestsError } from "@lib/errors.ts"; +import { Variables } from "../../main.ts"; const requestCounts: Partial< Record @@ -10,7 +11,7 @@ const requestCounts: Partial< const MAX_REQUESTS_PER_WINDOW = 300; const RATE_LIMIT_WINDOW = 60000; -const rateLimitMiddleware: Middleware = async (c, next) => { +const rateLimitMiddleware: Middleware = async (c, next) => { const hostnameOpt = c.hostname; if (hostnameOpt.isSome()) { diff --git a/server/test.db b/server/test.db index 98ef92f9f0a69ec1757f2964e361f40d63a93a9d..eec38c16ec49601c925e8a3748f7485a96bd6c04 100644 GIT binary patch delta 778 zcmZ9|yKd890EXdK6-;gEzyOq?3oKPD|Nec5BcaM?J5FpT@p%C!KE$y@Y$vIms3L_c z0I@Lk7Es0Yuy7$5Iz%9S(>p!-Zto6m?+!kn?w@~o$?l(jfA`^P|MdRn)vJ@^M~63i zPrAq7j(!~>hc{nO^r!c)b?0z<`grft5qv|@AN=e9d0qRg7%WyYI??lRB}T@=rhC`uGM z?3d=qt65YA#sm%{np;U*!Ae6yDyArc;ui?K0Mr?1wUK7XE*VWiVUsR#Kzbj+iwE>l zKp0P`8M-t z+8azD$#=_c6tY&=nMW$nu>{Y04(SXuh2q!3btJNch;b>H`(AA+aT@wPe$m|t|Ds_V zlDqT=-3*eKNDDOR=MU(Suv`ehY-#x*W8ZJBlo%|Q37?dBfUZQ1;e4Ol2#&2Oli|{U zO3F-@yyKch!Ev!Z+r$K@N12ApjN(Z~sP=>~R_uDyT$>Z2I^-yfSxKCotmOsfn^RG& zmOe$;A@pL`;8<-ZJOkvmjWt*Qr$(Bq8#MwO^|J?cC(}%Io?|2y*3+3Q=u$wk3CeiY zOscG*H&S|DYc24qYm|k%j5ZsAn(JaP6zjer&xG0LJmyOuK5EOgCM$i)=bs#d$$Nai)`zm+uczKoB-9AczPmAPOvMrYC4` zkSlaDy+?1*s}$3P^E|8n>i6(?`S5u8<3sEA=SSGO{q^@u{VelVAoCGaAK)W$%apQJ`4V7`~rn zGd0^2b{I1nKou=BF{2j`yXm}0rgK_yY`m^mp*u>ffETK9kx-&a*Pvnx#svi9ZhXZn zmUUJh7ZB$)vo=wHVs`+&Lx>v`47#1>=cG3Y>o=*5_Hp~-;>Dk5w5fqrpQ;lcU2wic zVkH_0i*1aVwHKB6CzK!vOa7%Wn#n&Gh%24mTHGe&cYKK?G3ufIi1j5v`KBE z+dH8@KBLtzSH}9T@9<>qgvXdz$MRvN#oUHdw0!S5pcCbsu%v}_sP|%QnbC6wEr&G} zB-0lGxv?@)2y#~{GTf+HjWuT+uaBncwpT8bDd8q+?)BVor)cb$+6ZP%?|0MKu|mgQ zG1lQMY|zI60tNs(x04#hn$+%sexq)M&uVC9hLetbbmb`SBt>EATg$0xuYs*Y%$V7l zNXuhg8U+DFM0?6Wj%8VTLRQA&K%}zCFbji|0!SC+VdoGeCG1Ua2-Tny$g@rAZV^`L wX#2}PRJCJ^u>e)+xq2L`$5J^%m!