about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-05-20 18:12:09 +0200
committerbptato <nincsnevem662@gmail.com>2024-05-20 18:12:09 +0200
commit7c4bb940410c8f5ad59e1d21d5565364a9a0cd71 (patch)
treed46d682ce9d4f308232c961985d8411c2a70197c
parent723613b0a02605dbf715d74c70b9ec29f1092c76 (diff)
downloadchawan-7c4bb940410c8f5ad59e1d21d5565364a9a0cd71.tar.gz
html: improve Request, derive Client from Window
* make Client an instance of Window (for less special casing)
* misc work on Request & fetch
* improve origin comparison (opaque origins of same URLs are now
  considered the same)
-rw-r--r--src/html/chadombuilder.nim10
-rw-r--r--src/html/dom.nim54
-rw-r--r--src/html/env.nim48
-rw-r--r--src/html/script.nim34
-rw-r--r--src/html/xmlhttprequest.nim14
-rw-r--r--src/js/javascript.nim8
-rw-r--r--src/loader/cgi.nim2
-rw-r--r--src/loader/loader.nim22
-rw-r--r--src/loader/request.nim261
-rw-r--r--src/loader/response.nim12
-rw-r--r--src/local/client.nim37
-rw-r--r--src/local/container.nim4
-rw-r--r--src/local/pager.nim2
-rw-r--r--src/server/buffer.nim11
-rw-r--r--src/types/referrer.nim65
-rw-r--r--src/types/url.nim77
-rw-r--r--src/utils/twtstr.nim16
17 files changed, 362 insertions, 315 deletions
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index 92effe3a..3c5b2c12 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -352,18 +352,14 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string):
   case t
   of "text/html":
     let global = JS_GetGlobalObject(ctx)
-    let window = if ctx.hasClass(Window):
-      fromJS[Window](ctx, global).get(nil)
-    else:
-      Window(nil)
+    let window = fromJS[Window](ctx, global).get
     JS_FreeValue(ctx, global)
-    let url = if window != nil and window.document != nil:
+    let url = if window.document != nil:
       window.document.url
     else:
       newURL("about:blank").get
     #TODO this is probably broken in client (or at least sub-optimal)
-    let factory = if window != nil: window.factory else: newCAtomFactory()
-    let builder = newChaDOMBuilder(url, window, factory, ccIrrelevant)
+    let builder = newChaDOMBuilder(url, window, window.factory, ccIrrelevant)
     var parser = initHTML5Parser(builder, HTML5ParserOpts[Node, CAtom]())
     let res = parser.parseChunk(str)
     assert res == PRES_CONTINUE
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 5833142b..14d46037 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -71,11 +71,11 @@ type
 
   Window* = ref object of EventTarget
     attrs*: WindowAttributes
-    console* {.jsget.}: Console
+    internalConsole*: Console
     navigator* {.jsget.}: Navigator
     screen* {.jsget.}: Screen
     settings*: EnvironmentSettings
-    loader*: Option[FileLoader]
+    internalLoader*: Option[FileLoader]
     location* {.jsget.}: Location
     jsrt*: JSRuntime
     jsctx*: JSContext
@@ -1512,7 +1512,7 @@ proc reload(location: Location) {.jsuffunc.} =
   location.document.window.navigate(location.url)
 
 func origin(location: Location): string {.jsuffget.} =
-  return location.url.origin
+  return location.url.jsOrigin
 
 func protocol(location: Location): string {.jsuffget.} =
   return location.url.protocol
@@ -2185,14 +2185,14 @@ func outerHTML(element: Element): string {.jsfget.} =
 
 func crossOrigin0(element: HTMLElement): CORSAttribute =
   if not element.attrb(satCrossorigin):
-    return NO_CORS
+    return caNoCors
   case element.attr(satCrossorigin)
   of "anonymous", "":
-    return ANONYMOUS
+    return caAnonymous
   of "use-credentials":
-    return USE_CREDENTIALS
+    return caUseCredentials
   else:
-    return ANONYMOUS
+    return caAnonymous
 
 func crossOrigin(element: HTMLScriptElement): CORSAttribute {.jsfget.} =
   return element.crossOrigin0
@@ -2201,7 +2201,7 @@ func crossOrigin(element: HTMLImageElement): CORSAttribute {.jsfget.} =
   return element.crossOrigin0
 
 func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] =
-  getReferrerPolicy(element.attr(satReferrerpolicy))
+  return strictParseEnum[ReferrerPolicy](element.attr(satReferrerpolicy))
 
 proc sheets*(document: Document): seq[CSSStylesheet] =
   if document.cachedSheetsInvalid:
@@ -2613,14 +2613,9 @@ func newDocument*(factory: CAtomFactory): Document =
 
 func newDocument(ctx: JSContext): Document {.jsctor.} =
   let global = JS_GetGlobalObject(ctx)
-  let window = if ctx.hasClass(Window):
-    fromJS[Window](ctx, global).get(nil)
-  else:
-    Window(nil)
+  let window = fromJS[Window](ctx, global).get
   JS_FreeValue(ctx, global)
-  #TODO this is probably broken in client (or at least sub-optimal)
-  let factory = if window != nil: window.factory else: newCAtomFactory()
-  return newDocument(factory)
+  return newDocument(window.factory)
 
 func newDocumentType*(document: Document; name, publicId, systemId: string):
     DocumentType =
@@ -2815,6 +2810,12 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} =
 var appliesFwdDecl*: proc(mqlist: MediaQueryList; window: Window): bool
   {.nimcall, noSideEffect.}
 
+func loader*(window: Window): Option[FileLoader] {.inline.} =
+  return window.internalLoader
+
+func console(window: Window): Console =
+  return window.internalConsole
+
 # see https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
 #TODO make this somewhat compliant with ^this
 proc loadResource(window: Window; link: HTMLLinkElement) =
@@ -3565,19 +3566,20 @@ proc createClassicScript(ctx: JSContext; source: string; baseURL: URL;
 type OnCompleteProc = proc(element: HTMLScriptElement, res: ScriptResult)
 
 proc fetchClassicScript(element: HTMLScriptElement; url: URL;
-    options: ScriptOptions; cors: CORSAttribute; cs: Charset,
+    options: ScriptOptions; cors: CORSAttribute; cs: Charset;
     onComplete: OnCompleteProc) =
   let window = element.document.window
   if not element.scriptingEnabled or window.loader.isNone:
     element.onComplete(ScriptResult(t: RESULT_NULL))
     return
   let loader = window.loader.get
-  let request = createPotentialCORSRequest(url, RequestDestination.SCRIPT, cors)
-  let response = loader.doRequest(request)
+  let request = createPotentialCORSRequest(url, rdScript, cors)
+  request.client = some(window.settings)
+  #TODO make this non-blocking somehow
+  let response = loader.doRequest(request.request)
   if response.res != 0:
     element.onComplete(ScriptResult(t: RESULT_NULL))
     return
-  #TODO make this non-blocking somehow
   let s = response.body.recvAll()
   let source = if cs in {CHARSET_UNKNOWN, CHARSET_UTF_8}:
     s.toValidUTF8()
@@ -3603,7 +3605,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL;
   window.importMapsAllowed = false
   element.fetchSingleModule(
     url,
-    RequestDestination.SCRIPT,
+    rdScript,
     options,
     parseURL("about:client").get,
     isTopLevel = true,
@@ -3611,8 +3613,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL;
       if res.t == RESULT_NULL:
         element.onComplete(res)
       else:
-        element.fetchDescendantsAndLink(res.script, RequestDestination.SCRIPT,
-          onComplete)
+        element.fetchDescendantsAndLink(res.script, rdScript, onComplete)
   )
 
 proc fetchDescendantsAndLink(element: HTMLScriptElement; script: Script;
@@ -3623,6 +3624,8 @@ proc fetchDescendantsAndLink(element: HTMLScriptElement; script: Script;
 proc fetchSingleModule(element: HTMLScriptElement; url: URL;
     destination: RequestDestination; options: ScriptOptions,
     referrer: URL; isTopLevel: bool; onComplete: OnCompleteProc) =
+  discard #TODO implement
+  #[
   let moduleType = "javascript"
   #TODO moduleRequest
   let settings = element.document.window.settings
@@ -3634,10 +3637,10 @@ proc fetchSingleModule(element: HTMLScriptElement; url: URL;
     element.onComplete(settings.moduleMap[i].value)
     return
   let destination = fetchDestinationFromModuleType(destination, moduleType)
-  let mode = if destination in {WORKER, SHAREDWORKER, SERVICEWORKER}:
-    RequestMode.SAME_ORIGIN
+  let mode = if destination in {rdWorker, rdSharedworker, rdServiceworker}:
+    rmSameOrigin
   else:
-    RequestMode.CORS
+    rmCors
   #TODO client
   #TODO initiator type
   let request = newRequest(
@@ -3647,6 +3650,7 @@ proc fetchSingleModule(element: HTMLScriptElement; url: URL;
     destination = destination
   )
   discard request #TODO
+  ]#
 
 proc execute*(element: HTMLScriptElement) =
   let document = element.document
diff --git a/src/html/env.nim b/src/html/env.nim
index dc63f790..76aa2007 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -91,17 +91,24 @@ proc height(screen: var Screen): int64 {.jsfget.} =
 proc colorDepth(screen: var Screen): int64 {.jsfget.} = 24
 proc pixelDepth(screen: var Screen): int64 {.jsfget.} = screen.colorDepth
 
-proc addNavigatorModule(ctx: JSContext) =
+proc addNavigatorModule*(ctx: JSContext) =
   ctx.registerType(Navigator)
   ctx.registerType(PluginArray)
   ctx.registerType(MimeTypeArray)
   ctx.registerType(Screen)
 
-proc fetch[T: Request|string](window: Window; req: T; init = none(RequestInit)):
-    JSResult[FetchPromise] {.jsfunc.} =
+proc fetch[T: JSRequest|string](window: Window; input: T;
+    init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} =
   if window.loader.isSome:
-    let req = ?newRequest(window.jsctx, req, init)
-    return ok(window.loader.get.fetch(req))
+    let input = ?newRequest(window.jsctx, input, init)
+    #TODO cors requests?
+    if not window.settings.origin.isSameOrigin(input.request.url.origin):
+      let promise = FetchPromise()
+      let err = newTypeError("NetworkError when attempting to fetch resource")
+      promise.resolve(JSResult[Response].err(err))
+      return ok(promise)
+    return ok(window.loader.get.fetch(input.request))
+  return ok(nil)
 
 proc setTimeout[T: JSValue|string](window: Window; handler: T;
     timeout = 0i32): int32 {.jsfunc.} =
@@ -117,6 +124,9 @@ proc clearTimeout(window: Window; id: int32) {.jsfunc.} =
 proc clearInterval(window: Window; id: int32) {.jsfunc.} =
   window.timeouts.clearInterval(id)
 
+func console(window: Window): Console {.jsfget.} =
+  return window.internalConsole
+
 proc screenX(window: Window): int64 {.jsfget.} = 0
 proc screenY(window: Window): int64 {.jsfget.} = 0
 proc screenLeft(window: Window): int64 {.jsfget.} = 0
@@ -170,6 +180,17 @@ proc setOnLoad(ctx: JSContext; window: Window; val: JSValue)
     doAssert ctx.addEventListener(window, "load", val).isSome
     JS_FreeValue(ctx, this)
 
+proc addWindowModule*(ctx: JSContext) =
+  ctx.addEventModule()
+  let eventTargetCID = ctx.getClass("EventTarget")
+  ctx.registerType(Window, parent = eventTargetCID, asglobal = true)
+
+proc addWindowModule2*(ctx: JSContext) =
+  ctx.addEventModule()
+  let eventTargetCID = ctx.getClass("EventTarget")
+  ctx.registerType(Window, parent = eventTargetCID, asglobal = true,
+    globalparent = true)
+
 proc addScripting*(window: Window; selector: Selector[int]) =
   let rt = newJSRuntime()
   let ctx = rt.newJSContext()
@@ -189,12 +210,8 @@ proc addScripting*(window: Window; selector: Selector[int]) =
         JS_FreeValue(ctx, ret)
     )
   )
-  var global = JS_GetGlobalObject(ctx)
-  ctx.addEventModule()
-  let eventTargetCID = ctx.getClass("EventTarget")
-  ctx.registerType(Window, asglobal = true, parent = eventTargetCID)
+  ctx.addWindowModule()
   ctx.setGlobal(window)
-  JS_FreeValue(ctx, global)
   ctx.addDOMExceptionModule()
   ctx.addConsoleModule()
   ctx.addNavigatorModule()
@@ -219,17 +236,18 @@ proc runJSJobs*(window: Window) =
     ctx.writeException(window.console.err)
 
 proc newWindow*(scripting, images: bool; selector: Selector[int];
-    attrs: WindowAttributes; factory: CAtomFactory;
-    navigate: proc(url: URL) = nil, loader = none(FileLoader)): Window =
+    attrs: WindowAttributes; factory: CAtomFactory; navigate: proc(url: URL);
+    loader: FileLoader; url: URL): Window =
   let err = newDynFileStream(stderr)
   let window = Window(
     attrs: attrs,
-    console: newConsole(err),
+    internalConsole: newConsole(err),
     navigator: Navigator(),
-    loader: loader,
+    internalLoader: some(loader),
     images: images,
     settings: EnvironmentSettings(
-      scripting: scripting
+      scripting: scripting,
+      origin: url.origin
     ),
     navigate: navigate,
     factory: factory
diff --git a/src/html/script.nim b/src/html/script.nim
index 61066abe..c0962b13 100644
--- a/src/html/script.nim
+++ b/src/html/script.nim
@@ -1,5 +1,4 @@
 import js/javascript
-import loader/request
 import types/referrer
 import types/url
 
@@ -13,10 +12,39 @@ type
   ScriptResultType* = enum
     RESULT_NULL, RESULT_SCRIPT, RESULT_IMPORT_MAP_PARSE, RESULT_FETCHING
 
+  RequestDestination* = enum
+    rdNone = ""
+    rdAudio = "audio"
+    rdAudioworklet = "audioworklet"
+    rdDocument = "document"
+    rdEmbed = "embed"
+    rdFont = "font"
+    rdFrame = "frame"
+    rdIframe = "iframe"
+    rdImage = "image"
+    rdJson = "json"
+    rdManifest = "manifest"
+    rdObject = "object"
+    rdPaintworklet = "paintworklet"
+    rdReport = "report"
+    rdScript = "script"
+    rdServiceworker = "serviceworker"
+    rdSharedworker = "sharedworker"
+    rdStyle = "style"
+    rdTrack = "track"
+    rdWorker = "worker"
+    rdXslt = "xslt"
+
+  CredentialsMode* = enum
+    cmSameOrigin = "same-origin"
+    cmOmit = "omit"
+    cmInclude = "include"
+
 type
   EnvironmentSettings* = ref object
     scripting*: bool
     moduleMap*: ModuleMap
+    origin*: Origin
 
   Script* = object
     #TODO setings
@@ -61,7 +89,7 @@ proc find*(moduleMap: ModuleMap; url: URL; moduleType: string): int =
 func fetchDestinationFromModuleType*(default: RequestDestination;
     moduleType: string): RequestDestination =
   if moduleType == "json":
-    return RequestDestination.JSON
+    return rdJson
   if moduleType == "css":
-    return RequestDestination.STYLE
+    return rdStyle
   return default
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
index 69fe157d..33f1e61c 100644
--- a/src/html/xmlhttprequest.nim
+++ b/src/html/xmlhttprequest.nim
@@ -69,13 +69,13 @@ func readyState(this: XMLHttpRequest): uint16 {.jsfget.} =
 
 proc parseMethod(s: string): DOMResult[HttpMethod] =
   return case s.toLowerAscii()
-  of "get": ok(HTTP_GET)
-  of "delete": ok(HTTP_DELETE)
-  of "head": ok(HTTP_HEAD)
-  of "options": ok(HTTP_OPTIONS)
-  of "patch": ok(HTTP_PATCH)
-  of "post": ok(HTTP_POST)
-  of "put": ok(HTTP_PUT)
+  of "get": ok(hmGet)
+  of "delete": ok(hmDelete)
+  of "head": ok(hmHead)
+  of "options": ok(hmOptions)
+  of "patch": ok(hmPatch)
+  of "post": ok(hmPost)
+  of "put": ok(hmPut)
   of "connect", "trace", "track":
     errDOMException("Forbidden method", "SecurityError")
   else:
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index bfe45aae..a697bb2f 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -1601,8 +1601,9 @@ proc bindEndStmts(endstmts: NimNode; info: RegistryInfo) =
       let `classDef` = JSClassDefConst(addr cd))
 
 macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0;
-    asglobal: static bool = false; nointerface = false;
-    name: static string = ""; has_extra_getset: static bool = false;
+    asglobal: static bool = false; globalparent: static bool = false;
+    nointerface = false; name: static string = "";
+    has_extra_getset: static bool = false;
     extra_getset: static openArray[TabGetSet] = []; namespace = JS_NULL;
     errid = opt(JSErrorEnum); ishtmldda = false): JSClassID =
   var stmts = newStmtList()
@@ -1636,9 +1637,10 @@ macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0;
   let tname = info.tname
   let unforgeable = info.tabUnforgeable
   let staticfuns = info.tabStatic
+  let global = asglobal and not globalparent
   endstmts.add(quote do:
     `ctx`.newJSClass(`classDef`, `tname`, getTypePtr(`t`), `sctr`, `tabList`,
-      `parent`, bool(`asglobal`), `nointerface`, `finName`, `namespace`,
+      `parent`, bool(`global`), `nointerface`, `finName`, `namespace`,
       `errid`, `unforgeable`, `staticfuns`, `ishtmldda`)
   )
   stmts.add(newBlockStmt(endstmts))
diff --git a/src/loader/cgi.nim b/src/loader/cgi.nim
index 347855ac..f3c5b1e3 100644
--- a/src/loader/cgi.nim
+++ b/src/loader/cgi.nim
@@ -41,7 +41,7 @@ proc setupEnv(cmd, scriptName, pathInfo, requestURI, myDir: string;
     putEnv("PATH_INFO", pathInfo)
   if url.query.isSome:
     putEnv("QUERY_STRING", url.query.get)
-  if request.httpMethod == HTTP_POST:
+  if request.httpMethod == hmPost:
     if request.multipart.isSome:
       putEnv("CONTENT_TYPE", request.multipart.get.getContentType())
     else:
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index c378523b..8bc23c16 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -854,18 +854,18 @@ proc getRedirect*(response: Response; request: Request): Request =
       let location = response.headers.table["Location"][0]
       let url = parseURL(location, option(request.url))
       if url.isSome:
-        if (response.status == 303 and
-            request.httpMethod notin {HTTP_GET, HTTP_HEAD}) or
-            (response.status == 301 or response.status == 302 and
-            request.httpMethod == HTTP_POST):
-          return newRequest(url.get, HTTP_GET,
-            mode = request.mode, credentialsMode = request.credentialsMode,
-            destination = request.destination)
+        let status = response.status
+        if status == 303 and request.httpMethod notin {hmGet, hmHead} or
+            status == 301 or
+            status == 302 and request.httpMethod == hmPost:
+          return newRequest(url.get, hmGet)
         else:
-          return newRequest(url.get, request.httpMethod,
-            body = request.body, multipart = request.multipart,
-            mode = request.mode, credentialsMode = request.credentialsMode,
-            destination = request.destination)
+          return newRequest(
+            url.get,
+            request.httpMethod,
+            body = request.body,
+            multipart = request.multipart
+          )
   return nil
 
 template withLoaderPacketWriter(stream: SocketStream; loader: FileLoader;
diff --git a/src/loader/request.nim b/src/loader/request.nim
index 8eeb17b8..1398d25f 100644
--- a/src/loader/request.nim
+++ b/src/loader/request.nim
@@ -3,9 +3,10 @@ import std/strutils
 import std/tables
 
 import bindings/quickjs
-import js/jserror
+import html/script
 import js/fromjs
 import js/javascript
+import js/jserror
 import js/jstypes
 import loader/headers
 import types/blob
@@ -15,82 +16,83 @@ import types/url
 
 type
   HttpMethod* = enum
-    HTTP_GET = "GET"
-    HTTP_CONNECT = "CONNECT"
-    HTTP_DELETE = "DELETE"
-    HTTP_HEAD = "HEAD"
-    HTTP_OPTIONS = "OPTIONS"
-    HTTP_PATCH = "PATCH"
-    HTTP_POST = "POST"
-    HTTP_PUT = "PUT"
-    HTTP_TRACE = "TRACE"
+    hmGet = "GET"
+    hmConnect = "CONNECT"
+    hmDelete = "DELETE"
+    hmHead = "HEAD"
+    hmOptions = "OPTIONS"
+    hmPatch = "PATCH"
+    hmPost = "POST"
+    hmPut = "PUT"
+    hmTrace = "TRACE"
 
   RequestMode* = enum
-    NO_CORS = "no-cors"
-    SAME_ORIGIN = "same-origin"
-    CORS = "cors"
-    NAVIGATE = "navigate"
-    WEBSOCKET = "websocket"
-
-  RequestDestination* = enum
-    NO_DESTINATION = ""
-    AUDIO = "audio"
-    AUDIOWORKLET = "audioworklet"
-    DOCUMENT = "document"
-    EMBED = "embed"
-    FONT = "font"
-    FRAME = "frame"
-    IFRAME = "iframe"
-    IMAGE = "image"
-    JSON = "json"
-    MANIFEST = "manifest"
-    OBJECT = "object"
-    PAINTWORKLET = "paintworklet"
-    REPORT = "report"
-    SCRIPT = "script"
-    SERVICEWORKER = "serviceworker"
-    SHAREDWORKER = "sharedworker"
-    STYLE = "style"
-    TRACK = "track"
-    WORKER = "worker"
-    XSLT = "xslt"
-
-  CredentialsMode* = enum
-    SAME_ORIGIN = "same-origin"
-    OMIT = "omit"
-    INCLUDE = "include"
+    rmNoCors = "no-cors"
+    rmSameOrigin = "same-origin"
+    rmCors = "cors"
+    rmNavigate = "navigate"
+    rmWebsocket = "websocket"
 
   CORSAttribute* = enum
-    NO_CORS = "no-cors"
-    ANONYMOUS = "anonymous"
-    USE_CREDENTIALS = "use-credentials"
+    caNoCors = "no-cors"
+    caAnonymous = "anonymous"
+    caUseCredentials = "use-credentials"
 
 type
-  Request* = ref RequestObj
-  RequestObj* = object
+  RequestOriginType* = enum
+    rotClient, rotOrigin
+
+  RequestOrigin* = object
+    case t*: RequestOriginType
+    of rotClient: discard
+    of rotOrigin:
+      origin*: Origin
+
+  RequestWindowType* = enum
+    rwtClient, rwtNoWindow, rwtWindow
+
+  RequestWindow* = object
+    case t*: RequestWindowType
+    of rwtClient, rwtNoWindow: discard
+    of rwtWindow:
+      window*: EnvironmentSettings
+
+  Request* = ref object
     httpMethod*: HttpMethod
     url*: URL
-    headers* {.jsget.}: Headers
+    headers*: Headers
     body*: Option[string]
     multipart*: Option[FormData]
     referrer*: URL
-    mode* {.jsget.}: RequestMode
-    destination* {.jsget.}: RequestDestination
-    credentialsMode* {.jsget.}: CredentialsMode
     proxy*: URL #TODO do something with this
     # when set to true, the loader will not write data from the body (not
     # headers!) into the output until a resume is received.
     suspended*: bool
 
-jsDestructor(Request)
+  JSRequest* = ref object
+    request*: Request
+    mode* {.jsget.}: RequestMode
+    destination* {.jsget.}: RequestDestination
+    credentialsMode* {.jsget.}: CredentialsMode
+    origin*: RequestOrigin
+    window*: RequestWindow
+    client*: Option[EnvironmentSettings]
+
+jsDestructor(JSRequest)
 
-proc js_url(this: Request): string {.jsfget: "url".} =
+func headers(this: JSRequest): Headers {.jsfget.} =
+  return this.request.headers
+
+func url(this: JSRequest): URL =
+  return this.request.url
+
+proc jsUrl(this: JSRequest): string {.jsfget: "url".} =
   return $this.url
 
 #TODO pretty sure this is incorrect
-proc js_referrer(this: Request): string {.jsfget: "referrer".} =
-  if this.referrer != nil:
-    return $this.referrer
+proc jsReferrer(this: JSRequest): string {.jsfget: "referrer".} =
+  if this.request.referrer != nil:
+    return $this.request.referrer
   return ""
 
 iterator pairs*(headers: Headers): (string, string) =
@@ -98,10 +100,8 @@ iterator pairs*(headers: Headers): (string, string) =
     for v in vs:
       yield (k, v)
 
-func newRequest*(url: URL; httpMethod = HTTP_GET; headers = newHeaders();
-    body = none(string); multipart = none(FormData); mode = RequestMode.NO_CORS;
-    credentialsMode = CredentialsMode.SAME_ORIGIN;
-    destination = RequestDestination.NO_DESTINATION; proxy: URL = nil;
+func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders();
+    body = none(string); multipart = none(FormData); proxy: URL = nil;
     referrer: URL = nil; suspended = false): Request =
   return Request(
     url: url,
@@ -109,18 +109,14 @@ func newRequest*(url: URL; httpMethod = HTTP_GET; headers = newHeaders();
     headers: headers,
     body: body,
     multipart: multipart,
-    mode: mode,
-    credentialsMode: credentialsMode,
-    destination: destination,
     referrer: referrer,
     proxy: proxy,
     suspended: suspended
   )
 
-func newRequest*(url: URL; httpMethod = HTTP_GET;
+func newRequest*(url: URL; httpMethod = hmGet;
     headers: seq[(string, string)] = @[]; body = none(string);
-    multipart = none(FormData); mode = RequestMode.NO_CORS; proxy: URL = nil):
-    Request =
+    multipart = none(FormData); proxy: URL = nil): Request =
   let hl = newHeaders()
   for pair in headers:
     let (k, v) = pair
@@ -131,43 +127,39 @@ func newRequest*(url: URL; httpMethod = HTTP_GET;
     hl,
     body,
     multipart,
-    mode,
     proxy = proxy
   )
 
 func createPotentialCORSRequest*(url: URL; destination: RequestDestination;
-    cors: CORSAttribute; fallbackFlag = false): Request =
-  var mode = if cors == NO_CORS:
-    RequestMode.NO_CORS
+    cors: CORSAttribute; fallbackFlag = false): JSRequest =
+  var mode = if cors == caNoCors:
+    rmNoCors
   else:
-    RequestMode.CORS
-  if fallbackFlag and mode == NO_CORS:
-    mode = SAME_ORIGIN
-  let credentialsMode = if cors == ANONYMOUS:
-    CredentialsMode.SAME_ORIGIN
-  else: CredentialsMode.INCLUDE
-  return newRequest(
-    url,
-    destination = destination,
-    mode = mode,
-    credentialsMode = credentialsMode
+    rmCors
+  if fallbackFlag and mode == rmNoCors:
+    mode = rmSameOrigin
+  let credentialsMode = if cors == caAnonymous: cmSameOrigin else: cmInclude
+  return JSRequest(
+    request: newRequest(url),
+    destination: destination,
+    mode: mode,
+    credentialsMode: credentialsMode
   )
 
 type
   BodyInitType = enum
-    BODY_INIT_BLOB, BODY_INIT_FORM_DATA, BODY_INIT_URL_SEARCH_PARAMS,
-    BODY_INIT_STRING
+    bitBlob, bitFormData, bitUrlSearchParams, bitString
 
   BodyInit = object
     #TODO ReadableStream, BufferSource
     case t: BodyInitType
-    of BODY_INIT_BLOB:
+    of bitBlob:
       blob: Blob
-    of BODY_INIT_FORM_DATA:
+    of bitFormData:
       formData: FormData
-    of BODY_INIT_URL_SEARCH_PARAMS:
+    of bitUrlSearchParams:
       searchParams: URLSearchParams
-    of BODY_INIT_STRING:
+    of bitString:
       str: string
 
   RequestInit* = object of JSDict
@@ -180,6 +172,7 @@ type
     credentials: Option[CredentialsMode]
     proxyUrl: URL
     mode: Option[RequestMode]
+    window: Option[JSValue]
 
 proc fromJSBodyInit(ctx: JSContext; val: JSValue): JSResult[BodyInit] =
   if JS_IsUndefined(val) or JS_IsNull(val):
@@ -187,67 +180,72 @@ proc fromJSBodyInit(ctx: JSContext; val: JSValue): JSResult[BodyInit] =
   block formData:
     let x = fromJS[FormData](ctx, val)
     if x.isSome:
-      return ok(BodyInit(t: BODY_INIT_FORM_DATA, formData: x.get))
+      return ok(BodyInit(t: bitFormData, formData: x.get))
   block blob:
     let x = fromJS[Blob](ctx, val)
     if x.isSome:
-      return ok(BodyInit(t: BODY_INIT_BLOB, blob: x.get))
+      return ok(BodyInit(t: bitBlob, blob: x.get))
   block searchParams:
     let x = fromJS[URLSearchParams](ctx, val)
     if x.isSome:
-      return ok(BodyInit(t: BODY_INIT_URL_SEARCH_PARAMS, searchParams: x.get))
+      return ok(BodyInit(t: bitUrlSearchParams, searchParams: x.get))
   block str:
     let x = fromJS[string](ctx, val)
     if x.isSome:
-      return ok(BodyInit(t: BODY_INIT_STRING, str: x.get))
+      return ok(BodyInit(t: bitString, str: x.get))
   return err(newTypeError("Invalid body init type"))
 
-func newRequest*[T: string|Request](ctx: JSContext; resource: T;
-    init = none(RequestInit)): JSResult[Request] {.jsctor.} =
+func newRequest*[T: string|JSRequest](ctx: JSContext; resource: T;
+    init = none(RequestInit)): JSResult[JSRequest] {.jsctor.} =
+  defer:
+    if init.isSome and init.get.window.isSome:
+      JS_FreeValue(ctx, init.get.window.get)
   when T is string:
     let url = ?newURL(resource)
     if url.username != "" or url.password != "":
       return err(newTypeError("Input URL contains a username or password"))
-    var httpMethod = HTTP_GET
+    var httpMethod = hmGet
     var headers = newHeaders()
     let referrer: URL = nil
-    var credentials = CredentialsMode.SAME_ORIGIN
+    var credentials = cmSameOrigin
     var body: Option[string]
     var multipart: Option[FormData]
     var proxyUrl: URL #TODO?
-    let fallbackMode = opt(RequestMode.CORS)
+    let fallbackMode = opt(rmCors)
+    var window = RequestWindow(t: rwtClient)
   else:
     let url = resource.url
-    var httpMethod = resource.httpMethod
+    var httpMethod = resource.request.httpMethod
     var headers = resource.headers.clone()
-    let referrer = resource.referrer
+    let referrer = resource.request.referrer
     var credentials = resource.credentialsMode
-    var body = resource.body
-    var multipart = resource.multipart
-    var proxyUrl = resource.proxy #TODO?
+    var body = resource.request.body
+    var multipart = resource.request.multipart
+    var proxyUrl = resource.request.proxy #TODO?
     let fallbackMode = none(RequestMode)
-    #TODO window
-  var mode = fallbackMode.get(RequestMode.NO_CORS)
-  let destination = NO_DESTINATION
+    var window = resource.window
+  var mode = fallbackMode.get(rmNoCors)
+  let destination = rdNone
   #TODO origin, window
-  if init.isSome:
-    if mode == RequestMode.NAVIGATE:
-      mode = RequestMode.SAME_ORIGIN
+  if init.isSome: #TODO spec wants us to check if it's "not empty"...
+    let init = init.get
+    if init.window.isSome:
+      if not JS_IsNull(init.window.get):
+        return errTypeError("Expected window to be null")
+      window = RequestWindow(t: rwtNoWindow)
+    if mode == rmNavigate:
+      mode = rmSameOrigin
     #TODO flags?
     #TODO referrer
-    let init = init.get
     httpMethod = init.`method`
     if init.body.isSome:
       let ibody = init.body.get
       case ibody.t
-      of BODY_INIT_FORM_DATA:
-        multipart = some(ibody.formData)
-      of BODY_INIT_STRING:
-        body = some(ibody.str)
-      else:
-        discard #TODO
-      if httpMethod in {HTTP_GET, HTTP_HEAD}:
-        return err(newTypeError("HEAD or GET Request cannot have a body."))
+      of bitFormData: multipart = some(ibody.formData)
+      of bitString: body = some(ibody.str)
+      else: discard #TODO
+      if httpMethod in {hmGet, hmHead}:
+        return errTypeError("HEAD or GET Request cannot have a body.")
     if init.headers.isSome:
       headers.fill(init.headers.get)
     if init.credentials.isSome:
@@ -256,25 +254,28 @@ func newRequest*[T: string|Request](ctx: JSContext; resource: T;
       mode = init.mode.get
     #TODO find a standard compatible way to implement this
     proxyUrl = init.proxyUrl
-  return ok(Request(
-    url: url,
-    httpMethod: httpMethod,
-    headers: headers,
-    body: body,
-    multipart: multipart,
+  return ok(JSRequest(
+    request: newRequest(
+      url,
+      httpMethod,
+      headers,
+      body,
+      multipart,
+      proxy = proxyUrl,
+      referrer = referrer
+    ),
     mode: mode,
     credentialsMode: credentials,
     destination: destination,
-    proxy: proxyUrl,
-    referrer: referrer
+    window: window
   ))
 
 func credentialsMode*(attribute: CORSAttribute): CredentialsMode =
   case attribute
-  of NO_CORS, ANONYMOUS:
-    return SAME_ORIGIN
-  of USE_CREDENTIALS:
-    return INCLUDE
+  of caNoCors, caAnonymous:
+    return cmSameOrigin
+  of caUseCredentials:
+    return cmInclude
 
 proc addRequestModule*(ctx: JSContext) =
-  ctx.registerType(Request)
+  ctx.registerType(JSRequest, name = "Request")
diff --git a/src/loader/response.nim b/src/loader/response.nim
index 63b07cf1..56e955a3 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -28,11 +28,11 @@ type
 
   #TODO fully implement headers guards
   HeadersGuard* = enum
-    GUARD_IMMUTABLE = "immutable"
-    GUARD_REQUEST = "request"
-    GUARD_REQUEST_NO_CORS = "request-no-cors"
-    GUARD_RESPONSE = "response"
-    GUARD_NONE = "none"
+    hgImmutable = "immutable"
+    hgRequest = "request"
+    hgRequestNoCors = "request-no-cors"
+    hgResponse = "response"
+    hgNone = "none"
 
   Response* = ref object
     responseType* {.jsget: "type".}: ResponseType
@@ -67,7 +67,7 @@ func makeNetworkError*(): Response {.jsstfunc: "Response:error".} =
     responseType: TYPE_ERROR,
     status: 0,
     headers: newHeaders(),
-    headersGuard: GUARD_IMMUTABLE
+    headersGuard: hgImmutable
   )
 
 func sok(response: Response): bool {.jsfget: "ok".} =
diff --git a/src/local/client.nim b/src/local/client.nim
index 07a7c523..19c31263 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -12,9 +12,10 @@ import std/unicode
 import bindings/constcharp
 import bindings/quickjs
 import config/config
+import html/catom
 import html/chadombuilder
 import html/dom
-import html/event
+import html/env
 import html/formdata
 import html/xmlhttprequest
 import io/bufstream
@@ -29,14 +30,14 @@ import js/base64
 import js/console
 import js/domexception
 import js/encoding
-import js/jserror
 import js/fromjs
 import js/intl
 import js/javascript
-import js/jstypes
-import js/jsutils
+import js/jserror
 import js/jsmodule
 import js/jsopaque
+import js/jstypes
+import js/jsutils
 import js/timeout
 import js/tojs
 import loader/headers
@@ -57,16 +58,13 @@ import utils/twtstr
 import chagashi/charset
 
 type
-  Client* = ref object
+  Client* = ref object of Window
     alive: bool
     config {.jsget.}: Config
     consoleWrapper: ConsoleWrapper
     fdmap: Table[int, Container]
     feednext: bool
-    jsctx: JSContext
-    jsrt: JSRuntime
     pager {.jsget.}: Pager
-    timeouts: TimeoutState
     pressed: tuple[col: int; row: int]
     exitCode: int
     inEval: bool
@@ -91,11 +89,6 @@ template forkserver(client: Client): ForkServer =
 template readChar(client: Client): char =
   client.pager.term.readChar()
 
-proc fetch[T: Request|string](client: Client; req: T;
-    init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} =
-  let req = ?newRequest(client.jsctx, req, init)
-  return ok(client.loader.fetch(req))
-
 proc interruptHandler(rt: JSRuntime; opaque: pointer): cint {.cdecl.} =
   let client = cast[Client](opaque)
   if client.console == nil or client.pager.term.istream == nil:
@@ -805,12 +798,12 @@ func line(client: Client): LineEdit {.jsfget.} =
   return client.pager.lineedit.get(nil)
 
 proc addJSModules(client: Client; ctx: JSContext) =
+  ctx.addWindowModule2()
   ctx.addDOMExceptionModule()
   ctx.addConsoleModule()
-  ctx.addCookieModule()
-  ctx.addURLModule()
-  ctx.addEventModule()
+  ctx.addNavigatorModule()
   ctx.addDOMModule()
+  ctx.addURLModule()
   ctx.addHTMLModule()
   ctx.addIntlModule()
   ctx.addBlobModule()
@@ -819,11 +812,12 @@ proc addJSModules(client: Client; ctx: JSContext) =
   ctx.addHeadersModule()
   ctx.addRequestModule()
   ctx.addResponseModule()
+  ctx.addEncodingModule()
   ctx.addLineEditModule()
   ctx.addConfigModule()
   ctx.addPagerModule()
   ctx.addContainerModule()
-  ctx.addEncodingModule()
+  ctx.addCookieModule()
 
 func getClient(client: Client): Client {.jsfget: "client".} =
   return client
@@ -850,14 +844,17 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext;
     jsctx: jsctx,
     pager: pager,
     exitCode: -1,
-    alive: true
+    alive: true,
+    factory: newCAtomFactory(),
+    internalLoader: some(loader)
   )
   jsrt.setInterruptHandler(interruptHandler, cast[pointer](client))
-  jsctx.registerType(Client, asglobal = true)
-  jsctx.setGlobal(client)
   let global = JS_GetGlobalObject(jsctx)
+  jsctx.setGlobal(client)
   jsctx.definePropertyE(global, "cmd", config.cmd.jsObj)
   JS_FreeValue(jsctx, global)
   config.cmd.jsObj = JS_NULL
   client.addJSModules(jsctx)
+  let windowCID = jsctx.getClass("Window")
+  jsctx.registerType(Client, asglobal = true, parent = windowCID)
   return client
diff --git a/src/local/container.nim b/src/local/container.nim
index 75eba41b..00553190 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -1405,7 +1405,7 @@ proc extractCookies(response: Response): seq[Cookie] =
 
 proc extractReferrerPolicy(response: Response): Option[ReferrerPolicy] =
   if "Referrer-Policy" in response.headers:
-    return getReferrerPolicy(response.headers["Referrer-Policy"])
+    return strictParseEnum[ReferrerPolicy](response.headers["Referrer-Policy"])
   return none(ReferrerPolicy)
 
 # Apply data received in response.
@@ -1423,7 +1423,7 @@ proc applyResponse*(container: Container; response: Response;
     if referrerPolicy.isSome:
       container.loaderConfig.referrerPolicy = referrerPolicy.get
   else:
-    container.loaderConfig.referrerPolicy = NO_REFERRER
+    container.loaderConfig.referrerPolicy = rpNoReferrer
   # setup content type; note that isSome means an override so we skip it
   if container.contentType.isNone:
     var contentType = response.getContentType()
diff --git a/src/local/pager.nim b/src/local/pager.nim
index b2cd779c..3a30a8f8 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -962,7 +962,7 @@ proc gotoURL(pager: Pager; request: Request; prevurl = none(URL);
   var loaderConfig: LoaderClientConfig
   var bufferConfig = pager.applySiteconf(request.url, cs, loaderConfig)
   if prevurl.isNone or not prevurl.get.equals(request.url, true) or
-      request.url.hash == "" or request.httpMethod != HTTP_GET:
+      request.url.hash == "" or request.httpMethod != hmGet:
     # Basically, we want to reload the page *only* when
     # a) we force a reload (by setting prevurl to none)
     # b) or the new URL isn't just the old URL + an anchor
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 54c32de3..178dd94b 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -887,7 +887,8 @@ proc setHTML(buffer: Buffer) =
     buffer.attrs,
     factory,
     navigate,
-    some(buffer.loader)
+    buffer.loader,
+    buffer.url
   )
   let confidence = if buffer.config.charsetOverride == CHARSET_UNKNOWN:
     ccTentative
@@ -1317,10 +1318,10 @@ proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request] =
     #TODO
     return none(Request)
   let httpmethod = if formmethod == fmGet:
-    HTTP_GET
+    hmGet
   else:
     assert formmethod == fmPost
-    HTTP_POST
+    hmPost
 
   #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"):
   #  submitter.attr("formtarget")
@@ -1549,7 +1550,7 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult =
         repaint = true
         if s.isSome:
           let url = newURL("data:text/html," & s.get).get
-          let req = newRequest(url, HTTP_GET)
+          let req = newRequest(url, hmGet)
           return ClickResult(
             repaint: repaint,
             open: some(req)
@@ -1559,7 +1560,7 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult =
       )
     return ClickResult(
       repaint: repaint,
-      open: some(newRequest(url, HTTP_GET))
+      open: some(newRequest(url, hmGet))
     )
   return ClickResult(repaint: repaint)
 
diff --git a/src/types/referrer.nim b/src/types/referrer.nim
index ec9acc76..7c7b6ffe 100644
--- a/src/types/referrer.nim
+++ b/src/types/referrer.nim
@@ -1,72 +1,51 @@
-import std/options
-
 import types/url
 
 type ReferrerPolicy* = enum
-  STRICT_ORIGIN_WHEN_CROSS_ORIGIN
-  NO_REFERRER
-  NO_REFERRER_WHEN_DOWNGRADE
-  STRICT_ORIGIN
-  ORIGIN
-  SAME_ORIGIN
-  ORIGIN_WHEN_CROSS_ORIGIN
-  UNSAFE_URL
-
-const DefaultPolicy* = STRICT_ORIGIN_WHEN_CROSS_ORIGIN
+  rpStrictOriginWhenCrossOrigin = "strict-origin-when-cross-origin"
+  rpNoReferrer = "no-referrer"
+  rpNoReferrerWhenDowngrade = "no-referrer-when-downgrade"
+  rpStrictOrigin = "strict-origin"
+  rpOrigin = "origin"
+  rpSameOrigin = "same-origin"
+  rpOriginWhenCrossOrigin = "origin-when-cross-origin"
+  rpUnsafeURL = "unsafe-url"
 
-proc getReferrerPolicy*(s: string): Option[ReferrerPolicy] =
-  case s
-  of "no-referrer":
-    return some(NO_REFERRER)
-  of "no-referrer-when-downgrade":
-    return some(NO_REFERRER_WHEN_DOWNGRADE)
-  of "origin":
-    return some(ORIGIN)
-  of "origin-when-cross-origin":
-    return some(ORIGIN_WHEN_CROSS_ORIGIN)
-  of "same-origin":
-    return some(SAME_ORIGIN)
-  of "strict-origin":
-    return some(STRICT_ORIGIN)
-  of "strict-origin-when-cross-origin":
-    return some(STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
-  of "unsafe-url":
-    return some(UNSAFE_URL)
+const DefaultPolicy* = rpStrictOriginWhenCrossOrigin
 
 proc getReferrer*(prev, target: URL; policy: ReferrerPolicy): string =
-  let origin = prev.origin0
-  if origin.isNone:
+  let origin = prev.origin
+  if origin.t == otOpaque:
     return ""
   if prev.scheme != "http" and prev.scheme != "https":
     return ""
   if target.scheme != "http" and target.scheme != "https":
     return ""
   case policy
-  of NO_REFERRER:
+  of rpNoReferrer:
     return ""
-  of NO_REFERRER_WHEN_DOWNGRADE:
+  of rpNoReferrerWhenDowngrade:
     if prev.scheme == "https" and target.scheme == "http":
       return ""
     return $origin & prev.pathname & prev.search
-  of SAME_ORIGIN:
-    if origin == target.origin0:
+  of rpSameOrigin:
+    if origin.isSameOrigin(target.origin):
       return $origin
     return ""
-  of ORIGIN:
+  of rpOrigin:
     return $origin
-  of STRICT_ORIGIN:
+  of rpStrictOrigin:
     if prev.scheme == "https" and target.scheme == "http":
       return ""
     return $origin
-  of ORIGIN_WHEN_CROSS_ORIGIN:
-    if origin != target.origin0:
+  of rpOriginWhenCrossOrigin:
+    if not origin.isSameOrigin(target.origin):
       return $origin
     return $origin & prev.pathname & prev.search
-  of STRICT_ORIGIN_WHEN_CROSS_ORIGIN:
+  of rpStrictOriginWhenCrossOrigin:
     if prev.scheme == "https" and target.scheme == "http":
       return $origin
-    if origin != target.origin0:
+    if not origin.isSameOrigin(target.origin):
       return $origin
     return $origin & prev.pathname & prev.search
-  of UNSAFE_URL:
+  of rpUnsafeURL:
     return $origin & prev.pathname & prev.search
diff --git a/src/types/url.nim b/src/types/url.nim
index abb76f56..585bb64b 100644
--- a/src/types/url.nim
+++ b/src/types/url.nim
@@ -58,12 +58,21 @@ type
     blob: Option[BlobURLEntry]
     searchParams* {.jsget.}: URLSearchParams
 
-  Origin* = Option[tuple[
-    scheme: string,
-    host: Host,
-    port: Option[uint16],
+  OriginType* = enum
+    otOpaque, otTuple
+
+  TupleOrigin* = tuple
+    scheme: string
+    host: Host
+    port: Option[uint16]
     domain: Option[string]
-  ]]
+
+  Origin* = ref object
+    case t*: OriginType
+    of otOpaque:
+      s: string
+    of otTuple:
+      tup: TupleOrigin
 
 jsDestructor(URL)
 jsDestructor(URLSearchParams)
@@ -876,9 +885,11 @@ func serialize(host: Host): string =
 func serialize*(path: URLPath): string {.inline.} =
   if path.opaque:
     return path.s
+  var buf = ""
   for s in path.ss:
-    result &= '/'
-    result &= s
+    buf &= '/'
+    buf &= s
+  return buf
 
 when defined(windows) or defined(OS2) or defined(DOS):
   func serialize_unicode_dos(path: URLPath): string =
@@ -1081,7 +1092,7 @@ proc newURL*(s: string; base: Option[string] = none(string)):
   url.searchParams.initURLSearchParams(url.query.get(""))
   return ok(url)
 
-proc origin0*(url: URL): Origin =
+proc origin*(url: URL): Origin =
   case url.scheme
   of "blob":
     if url.blob.isSome:
@@ -1089,33 +1100,43 @@ proc origin0*(url: URL): Origin =
       discard
     let pathURL = parseURL($url.path)
     if pathURL.isNone:
-      return # opaque
-    return pathURL.get.origin0
+      return Origin(t: otOpaque, s: $url)
+    return pathURL.get.origin
   of "ftp", "http", "https", "ws", "wss":
-    return some((url.scheme, url.host.get, url.port, none(string)))
+    return Origin(
+      t: otTuple,
+      tup: (url.scheme, url.host.get, url.port, none(string))
+    )
   of "file":
-    #???
-    return # opaque
+    return Origin(t: otOpaque, s: $url)
   else:
-    return # opaque
+    return Origin(t: otOpaque, s: $url)
 
-proc `==`*(a, b: Origin): bool =
-  if a.isNone or b.isNone: return false
-  return a.get == b.get
+proc `==`*(a, b: Origin): bool {.error.} =
+  discard
+
+proc isSameOrigin*(a, b: Origin): bool =
+  if a.t != b.t:
+    return false
+  case a.t
+  of otOpaque:
+    return a.s == b.s
+  of otTuple:
+    return a.tup == b.tup
 
 proc `$`*(origin: Origin): string =
-  if origin.isNone:
+  if origin.t == otOpaque:
     return "null"
-  let origin = origin.get
-  result = origin.scheme
-  result &= "://"
-  result &= origin.host.serialize()
-  if origin.port.isSome:
-    result &= ':'
-    result &= $origin.port.get
-
-proc origin*(url: URL): string {.jsfget.} =
-  return $url.origin0
+  var s = origin.tup.scheme
+  s &= "://"
+  s &= origin.tup.host.serialize()
+  if origin.tup.port.isSome:
+    s &= ':'
+    s &= $origin.tup.port.get
+  return s
+
+proc jsOrigin*(url: URL): string {.jsfget: "origin".} =
+  return $url.origin
 
 proc protocol*(url: URL): string {.jsfget.} =
   return url.scheme & ':'
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index c657d15b..3cbcfcd3 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -552,12 +552,12 @@ proc makeCRLF*(s: string): string =
     else:
       result &= s[i]
 
-func strictParseEnum*[T: enum](s: string): Opt[T] =
+func strictParseEnum*[T: enum](s: string): Option[T] =
   # cmp when len is small enough, otherwise hashmap
   when {T.low..T.high}.len <= 4:
     for e in T.low .. T.high:
       if $e == s:
-        return ok(e)
+        return some(e)
   else:
     const tab = (func(): Table[string, T] =
       result = initTable[string, T]()
@@ -565,15 +565,15 @@ func strictParseEnum*[T: enum](s: string): Opt[T] =
         result[$e] = e
     )()
     if s in tab:
-      return ok(tab[s])
-  return err()
+      return some(tab[s])
+  return none(T)
 
-func parseEnumNoCase*[T: enum](s: string): Opt[T] =
+func parseEnumNoCase*[T: enum](s: string): Option[T] =
   # cmp when len is small enough, otherwise hashmap
   when {T.low..T.high}.len <= 4:
     for e in T.low .. T.high:
       if ($e).equalsIgnoreCase(s):
-        return ok(e)
+        return some(e)
   else:
     const tab = (func(): Table[string, T] =
       result = initTable[string, T]()
@@ -581,8 +581,8 @@ func parseEnumNoCase*[T: enum](s: string): Opt[T] =
         result[$e] = e
     )()
     if s in tab:
-      return ok(tab[s])
-  return err()
+      return some(tab[s])
+  return none(T)
 
 proc getContentTypeAttr*(contentType, attrname: string): string =
   var i = contentType.find(';')