diff --git a/internal/jws/jws.go b/internal/jws/jws.go
index 89a9d47c98379e3e47c350d1e58a0ac20dfee5ac..97cba34436660117c8d0ff294c8637f9ff129255 100644
--- a/internal/jws/jws.go
+++ b/internal/jws/jws.go
@@ -139,5 +139,8 @@ func LoadKey() {
 	if err = key.Set(jwk.KeyUsageKey, string(jwk.ForSignature)); err != nil {
 		panic(err)
 	}
+	if err = key.Set(jwk.AlgorithmKey, config.Get().Signing.Alg); err != nil {
+		panic(err)
+	}
 	jwks.Add(key)
 }
diff --git a/internal/server/web/partials/create-at.mustache b/internal/server/web/partials/create-at.mustache
index 0a16b54b4218e7d0dda3843175ccf07847e9c591..be84c017d461747749e2b7f74caa6f423346e693 100644
--- a/internal/server/web/partials/create-at.mustache
+++ b/internal/server/web/partials/create-at.mustache
@@ -34,7 +34,8 @@
         <div id="at-pending-spinner" class="spinner-border text-primary d-none" role="status">
             <span class="sr-only">Loading...</span>
         </div>
-        <button class="btn btn-copy" id="at-result-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#at-result-msg">
+        <button class="btn btn-copy copier" id="at-result-copy" data-toggle="tooltip" data-placement="bottom"
+                title="Copy to clipboard" data-clipboard-target="#at-result-msg">
             <i class="far fa-copy"></i>
         </button>
         <pre class="card-text" id="at-result-msg"></pre>
diff --git a/internal/server/web/partials/create-mt.mustache b/internal/server/web/partials/create-mt.mustache
index 87393c73ad98b55dbe5637514d3041037b113262..6a4032c310ad4b77c31c6a9c83c4d0b5751ad457 100644
--- a/internal/server/web/partials/create-mt.mustache
+++ b/internal/server/web/partials/create-mt.mustache
@@ -67,7 +67,7 @@
         <div id="mt-pending-spinner" class="spinner-border text-primary d-none" role="status">
             <span class="sr-only">Loading...</span>
         </div>
-        <button class="btn btn-copy d-none" id="mt-result-copy" data-toggle="tooltip" data-placement="bottom"
+        <button class="btn btn-copy copier d-none" id="mt-result-copy" data-toggle="tooltip" data-placement="bottom"
                 title="Copy to clipboard" data-clipboard-target="#mt-result-msg">
             <i class="far fa-copy"></i>
         </button>
diff --git a/internal/server/web/partials/list-mts.mustache b/internal/server/web/partials/list-mts.mustache
index dbc7cbc60e70db1944528bbb4c1b109eee9f13a2..231827f667a6b048c90ba08ac9dcb4f49078d49f 100644
--- a/internal/server/web/partials/list-mts.mustache
+++ b/internal/server/web/partials/list-mts.mustache
@@ -3,7 +3,7 @@
         title="Reload data">
     <i class="fas fa-sync"></i>
 </button>
-<button class="btn btn-copy d-none" id="list-copy" data-toggle="tooltip" data-placement="bottom"
+<button class="btn btn-copy copier d-none" id="list-copy" data-toggle="tooltip" data-placement="bottom"
         title="Copy to clipboard" data-clipboard-target="#list-msg">
     <i class="far fa-copy"></i>
 </button>
diff --git a/internal/server/web/partials/scripts.mustache b/internal/server/web/partials/scripts.mustache
index df58c3c05c11d49e1238749f920bca48d64bb643..595ca0d6b4318e89bdf0624b8fd8219b6ded3f51 100644
--- a/internal/server/web/partials/scripts.mustache
+++ b/internal/server/web/partials/scripts.mustache
@@ -7,16 +7,18 @@
         <script src="/static/js/login.js"></script>
     {{/logged-in}}
     {{#logged-in}}
+        <script src="/static/js/lib/clipboard.min.js"></script>
+        <script src="/static/js/logged-in-utils.js"></script>
         <script src="/static/js/lib/bootstrap4-toggle.min.js"></script>
         <script src="/static/js/logout.js"></script>
     {{/logged-in}}
     {{#home}}
-        <script src="/static/js/lib/clipboard.min.js"></script>
         <script src="/static/js/lib/user-agent.min.js"></script>
         <script src="/static/js/mt-helper.js"></script>
         <script src="/static/js/home.js"></script>
         <script src="/static/js/create-at.js"></script>
         <script src="/static/js/tokeninfo.js"></script>
+        <script type="module" src="/static/js/tokeninfo-status.js"></script>
         <script src="/static/js/lib/behave.min.js"></script>
         <script src="/static/js/restrictions.js"></script>
         <script src="/static/js/rotation.js"></script>
diff --git a/internal/server/web/partials/tokeninfo.mustache b/internal/server/web/partials/tokeninfo.mustache
index 4928792bbab832d18311168a89a013b93c983ac2..7fa0fdc8b26a3ac187e237dbf07887e53631772c 100644
--- a/internal/server/web/partials/tokeninfo.mustache
+++ b/internal/server/web/partials/tokeninfo.mustache
@@ -1,58 +1,109 @@
 
-<div class="card border-primary">
-    <div class="card-header">
+<div id="tokeninfo-head">
+    <div id="tokeninfo-token-status" class="mb-2">
+        <h4>
+            <span class="badge badge-pill bg-my_green_dark" id="tokeninfo-token-name" data-toggle="tooltip"
+                  data-placement="top" title="Name of this mytoken"></span>
+
+            <span class="ml-2 badge badge-pill bg-my_blue_dark tokeninfo-token-type d-none"
+                  id="tokeninfo-token-type-short">Short Token</span>
+            <span class="ml-2 badge badge-pill badge-success tokeninfo-token-type d-none"
+                  id="tokeninfo-token-type-JWT-valid"
+                data-toggle="tooltip" data-placement="top" title="Signature valid">JWT</span>
+            <span class="ml-2 badge badge-pill badge-danger tokeninfo-token-type d-none"
+                  id="tokeninfo-token-type-JWT-invalid"
+                data-toggle="tooltip" data-placement="top" title="Signature invalid">JWT</span>
+
+            <span class="ml-2 badge badge-pill badge-success" id="tokeninfo-token-valid"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken is valid and can be used">
+                <i class="fas fa-check"></i>
+            </span>
+            <span class="ml-2 badge badge-pill badge-danger d-none" id="tokeninfo-token-invalid"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken is invalid and cannot be used">
+                <i class="fas fa-times"></i>
+            </span>
+
+            <span class="ml-2 badge badge-pill badge-light" id="tokeninfo-token-mytoken-iss"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken is issued by this instance"></span>
+
+            <span class="ml-2 badge badge-pill badge-primary" id="tokeninfo-token-oidc-iss"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken is linked to this OpenID Provider"></span>
+
+            <span class="ml-2 badge badge-pill badge-light" id="tokeninfo-token-iat"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken was issued at this time">
+                <i class="fas fa-clock mr-1"></i><span id="tokeninfo-token-iat-date"></span></span>
+
+            <span class="ml-2 badge badge-pill badge-warning" id="tokeninfo-token-exp"
+                  data-toggle="tooltip" data-placement="top" title="Mytoken expires at this time">
+                <i class="fas fa-stopwatch mr-2"></i><span id="tokeninfo-token-exp-date"></span></span>
+        </h4>
+    </div>
+
+    <div class="input-group">
+        <input type="text" class="form-control" placeholder="Mytoken" id="tokeninfo-token" autofocus
+               style="padding-right: 40px;">
+        <button class="btn copier text-secondary" id="tokeninfo-token-copy" data-toggle="tooltip"
+                data-placement="bottom"
+                title="Copy to clipboard" data-clipboard-target="#tokeninfo-token"
+                style="position: relative; left: -40px;">
+            <i class="far fa-copy"></i>
+        </button>
+        <button type="button" class="btn btn-light ml-n4" id="create-tc">Create Transfercode</button>
+    </div>
+</div>
+
+    <div class="card-header mt-3">
         <ul class="nav nav-tabs card-header-tabs">
             <li class="nav-item">
-                <a class="nav-link active" id="session-info-tab" data-toggle="tab" href="#session" role="tab"
-                   aria-controls="session" aria-selected="true">Session Token Info</a>
+                <a class="nav-link active" id="info-tab" data-toggle="tab" href="#token-info" role="tab"
+                   aria-controls="token-info" aria-selected="true">Info</a>
             </li>
             <li class="nav-item">
-                <a class="nav-link" id="history-tab" data-toggle="tab" href="#history" role="tab"
-                   aria-controls="history" aria-selected="false">Session Token History</a>
+                <a class="nav-link" id="history-tab" data-toggle="tab" href="#token-history" role="tab"
+                   aria-controls="token-history" aria-selected="false">History</a>
             </li>
             <li class="nav-item">
-                <a class="nav-link" id="tree-tab" data-toggle="tab" href="#tree" role="tab" aria-controls="tree"
-                   aria-selected="false">Session Token Subtokens</a>
+                <a class="nav-link" id="tree-tab" data-toggle="tab" href="#token-tree" role="tab"
+                   aria-controls="token-tree" aria-selected="false">Subtokens</a>
             </li>
         </ul>
     </div>
 
     <div class="card-body tab-content">
-        <div class="tab-pane show active" id="session" role="tabpanel" aria-labelledby="session-info-tab">
-            <h4>Information About the Session's Mytoken</h4>
-            <button class="btn btn-reload" id="session-reload" data-toggle="tooltip" data-placement="bottom"
+        <div class="tab-pane show active" id="token-info" role="tabpanel" aria-labelledby="info-tab">
+            <h4>Information About a Mytoken</h4>
+            <button class="btn btn-reload" id="info-reload" data-toggle="tooltip" data-placement="bottom"
                     title="Reload data">
                 <i class="fas fa-sync"></i>
             </button>
-            <button class="btn btn-copy d-none" id="session-copy" data-toggle="tooltip" data-placement="bottom"
-                    title="Copy to clipboard" data-clipboard-target="#session-token-info-msg">
+            <button class="btn btn-copy copier d-none" id="info-copy" data-toggle="tooltip" data-placement="bottom"
+                    title="Copy to clipboard" data-clipboard-target="#tokeninfo-token-content">
                 <i class="far fa-copy"></i>
             </button>
-            <pre class="card-text" id="session-token-info-msg"></pre>
+            <pre class="card-text" id="tokeninfo-token-content"></pre>
         </div>
-        <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="history-tab">
-            <h4>Event History for Session's Mytoken</h4>
+        <div class="tab-pane" id="token-history" role="tabpanel" aria-labelledby="history-tab">
+            <h4>Event History for this Mytoken</h4>
             <button class="btn btn-reload" id="history-reload" data-toggle="tooltip" data-placement="bottom"
                     title="Reload data">
                 <i class="fas fa-sync"></i>
             </button>
-            <button class="btn btn-copy d-none" id="history-copy" data-toggle="tooltip" data-placement="bottom"
+            <button class="btn btn-copy copier d-none" id="history-copy" data-toggle="tooltip" data-placement="bottom"
                     title="Copy to clipboard" data-clipboard-target="#history-msg">
                 <i class="far fa-copy"></i>
             </button>
             <p class="card-text" id="history-msg"></p>
         </div>
-        <div class="tab-pane" id="tree" role="tabpanel" aria-labelledby="tree-tab">
-            <h4>Subtokens for Session's Mytoken</h4>
+        <div class="tab-pane" id="token-tree" role="tabpanel" aria-labelledby="tree-tab">
+            <h4>Subtokens for this Mytoken</h4>
             <button class="btn btn-reload" id="tree-reload" data-toggle="tooltip" data-placement="bottom"
                     title="Reload data">
                 <i class="fas fa-sync"></i>
             </button>
-            <button class="btn btn-copy d-none" id="tree-copy" data-toggle="tooltip" data-placement="bottom"
+            <button class="btn btn-copy copier d-none" id="tree-copy" data-toggle="tooltip" data-placement="bottom"
                     title="Copy to clipboard" data-clipboard-target="#tree-msg">
                 <i class="far fa-copy"></i>
             </button>
             <p class="card-text" id="tree-msg"></p>
         </div>
     </div>
-</div>
diff --git a/internal/server/web/sites/home.mustache b/internal/server/web/sites/home.mustache
index 6aecd8122f52e241e626ec3169c75c6bfd2c0b33..59858aa48a9a718552f744cc1a01fd1bd811f149 100644
--- a/internal/server/web/sites/home.mustache
+++ b/internal/server/web/sites/home.mustache
@@ -12,20 +12,19 @@
 </div>
 
 
+
 <div class="card">
     <div class="card-header">
         <ul class="nav nav-tabs card-header-tabs">
             <li class="nav-item">
-                <a class="nav-link active" id="at-tab" data-toggle="tab" href="#at" role="tab" aria-controls="at"
-                   aria-selected="true">Access Token</a>
+                <a class="nav-link active" id="at-tab" data-toggle="tab" href="#at" role="tab" aria-controls="at" aria-selected="true">Access Token</a>
             </li>
             <li class="nav-item">
                 <a class="nav-link" id="mt-tab" data-toggle="tab" href="#mt" role="tab" aria-controls="mt"
                    aria-selected="false">Create Mytoken</a>
             </li>
             <li class="nav-item">
-                <a class="nav-link" id="info-tab" data-toggle="tab" href="#info" role="tab" aria-controls="info"
-                   aria-selected="false">Tokeninfo</a>
+                <a class="nav-link" id="info-tab" data-toggle="tab" href="#info" role="tab" aria-controls="info" aria-selected="false">Tokeninfo</a>
             </li>
             <li class="nav-item">
                 <a class="nav-link" id="list-mts-tab" data-toggle="tab" href="#list-mts" role="tab"
@@ -38,9 +37,9 @@
             {{> create-at}}
         </div>
         <div class="tab-pane bg-secondary break-out" id="mt" role="tabpanel" aria-labelledby="mt-tab">
-            <div class="bg-secondary ">
+                <div class="bg-secondary ">
                 {{> create-mt}}
-            </div>
+                </div>
         </div>
         <div class="tab-pane" id="info" role="tabpanel" aria-labelledby="info-tab">
             {{> tokeninfo}}
diff --git a/internal/server/web/sites/settings-ssh.mustache b/internal/server/web/sites/settings-ssh.mustache
index c6877756b59dc278b65cf9d095243f45f8e1c5c7..fe8e31de87c7a73ba1a697d8ed3cc646930012f9 100644
--- a/internal/server/web/sites/settings-ssh.mustache
+++ b/internal/server/web/sites/settings-ssh.mustache
@@ -134,7 +134,7 @@
                             </div>
                             <div id="sshHostConfigDiv" class="d-none">
                                 <p>You can save and use the following ssh host entry:</p>
-                                <button class="btn btn-copy" id="sshHostConfigCopy" data-toggle="tooltip"
+                                <button class="btn btn-copy copier" id="sshHostConfigCopy" data-toggle="tooltip"
                                         data-placement="bottom"
                                         title="Copy to clipboard" data-clipboard-target="#sshHostConfig">
                                     <i class="far fa-copy"></i>
diff --git a/internal/server/web/static/js/discovery.js b/internal/server/web/static/js/discovery.js
index 86c548a8ee30d097bd9ffb7b0e4b772faf0bc3bc..a58e6c446e9e9de5c861e7ed611e9b9b96545f0a 100644
--- a/internal/server/web/static/js/discovery.js
+++ b/internal/server/web/static/js/discovery.js
@@ -5,7 +5,8 @@ const configElements = [
     "usersettings_endpoint",
     "revocation_endpoint",
     "tokeninfo_endpoint",
-    "providers_supported"
+    "providers_supported",
+    "jwks_uri"
 ]
 
 function discovery(...next) {
diff --git a/internal/server/web/static/js/home.js b/internal/server/web/static/js/home.js
index e8d2b82270fc7ab18a33fac0ae8d6f402d48ea1f..260b2fb1a8f4b6160d92faf8e2ac8379527c34ff 100644
--- a/internal/server/web/static/js/home.js
+++ b/internal/server/web/static/js/home.js
@@ -1,13 +1,3 @@
-
-let clipboard = new ClipboardJS('.btn-copy');
-clipboard.on('success', function (e) {
-    e.clearSelection();
-    let el = $(e.trigger);
-    let originalText = el.attr('data-original-title');
-    el.attr('data-original-title', 'Copied!').tooltip('show');
-    el.attr('data-original-title', originalText);
-});
-
 $(function (){
     chainFunctions(
         checkIfLoggedIn,
@@ -19,7 +9,7 @@ $(function (){
     let url = document.location.toString();
     if (url.match('#')) {
         let hash = url.split('#')[1];
-        if (['session', 'history', 'tree', 'list'].includes(hash)) {
+        if (['token-info', 'token-history', 'token-tree'].includes(hash)) {
             $('.nav-tabs a[href="#info"]').tab('show') ;
         }
         $('.nav-tabs a[href="#'+hash+'"]').tab('show') ;
diff --git a/internal/server/web/static/js/lib/jose/index.js b/internal/server/web/static/js/lib/jose/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..26d70fbe8b96c1d94fcf0ad3c78b9b3500364130
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/index.js
@@ -0,0 +1,32 @@
+// export { compactDecrypt } from './jwe/compact/decrypt.js';
+// export { flattenedDecrypt } from './jwe/flattened/decrypt.js';
+// export { generalDecrypt } from './jwe/general/decrypt.js';
+// export { GeneralEncrypt } from './jwe/general/encrypt.js';
+// export { compactVerify } from './jws/compact/verify.js';
+// export { flattenedVerify } from './jws/flattened/verify.js';
+// export { generalVerify } from './jws/general/verify.js';
+export { jwtVerify } from './jwt/verify.js';
+// export { jwtDecrypt } from './jwt/decrypt.js';
+// export { CompactEncrypt } from './jwe/compact/encrypt.js';
+// export { FlattenedEncrypt } from './jwe/flattened/encrypt.js';
+// export { CompactSign } from './jws/compact/sign.js';
+// export { FlattenedSign } from './jws/flattened/sign.js';
+// export { GeneralSign } from './jws/general/sign.js';
+// export { SignJWT } from './jwt/sign.js';
+// export { EncryptJWT } from './jwt/encrypt.js';
+// export { calculateJwkThumbprint } from './jwk/thumbprint.js';
+// export { EmbeddedJWK } from './jwk/embedded.js';
+// export { createLocalJWKSet } from './jwks/local.js';
+// export { createRemoteJWKSet } from './jwks/remote.js';
+// export { UnsecuredJWT } from './jwt/unsecured.js';
+// export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js';
+export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js';
+export { decodeProtectedHeader } from './util/decode_protected_header.js';
+export { decodeJwt } from './util/decode_jwt.js';
+import * as errors_1 from './util/errors.js';
+// export { generateKeyPair } from './key/generate_key_pair.js';
+// export { generateSecret } from './key/generate_secret.js';
+import * as base64url_1 from './util/base64url.js';
+
+export { errors_1 as errors };
+export { base64url_1 as base64url };
diff --git a/internal/server/web/static/js/lib/jose/jws/compact/sign.js b/internal/server/web/static/js/lib/jose/jws/compact/sign.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f159015aed696bdb88745ebd61d08be395459e6
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/compact/sign.js
@@ -0,0 +1,18 @@
+import {FlattenedSign} from '../flattened/sign.js';
+
+export class CompactSign {
+    constructor(payload) {
+        this._flattened = new FlattenedSign(payload);
+    }
+    setProtectedHeader(protectedHeader) {
+        this._flattened.setProtectedHeader(protectedHeader);
+        return this;
+    }
+    async sign(key, options) {
+        const jws = await this._flattened.sign(key, options);
+        if (jws.payload === undefined) {
+            throw new TypeError('use the flattened module for creating JWS with b64: false');
+        }
+        return `${jws.protected}.${jws.payload}.${jws.signature}`;
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jws/compact/verify.js b/internal/server/web/static/js/lib/jose/jws/compact/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8d000b7383841fc976f37735e8136b0831c4c22
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/compact/verify.js
@@ -0,0 +1,22 @@
+import {flattenedVerify} from '../flattened/verify.js';
+import {JWSInvalid} from '../../util/errors.js';
+import {decoder} from '../../lib/buffer_utils.js';
+
+export async function compactVerify(jws, key, options) {
+    if (jws instanceof Uint8Array) {
+        jws = decoder.decode(jws);
+    }
+    if (typeof jws !== 'string') {
+        throw new JWSInvalid('Compact JWS must be a string or Uint8Array');
+    }
+    const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');
+    if (length !== 3) {
+        throw new JWSInvalid('Invalid Compact JWS');
+    }
+    const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options);
+    const result = { payload: verified.payload, protectedHeader: verified.protectedHeader };
+    if (typeof key === 'function') {
+        return { ...result, key: verified.key };
+    }
+    return result;
+}
diff --git a/internal/server/web/static/js/lib/jose/jws/flattened/sign.js b/internal/server/web/static/js/lib/jose/jws/flattened/sign.js
new file mode 100644
index 0000000000000000000000000000000000000000..d239dc757346bda42080df1bce02cc5db0de4c91
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/flattened/sign.js
@@ -0,0 +1,82 @@
+import {encode as base64url} from '../../runtime/base64url.js';
+import sign from '../../runtime/sign.js';
+import isDisjoint from '../../lib/is_disjoint.js';
+import {JWSInvalid} from '../../util/errors.js';
+import {concat, decoder, encoder} from '../../lib/buffer_utils.js';
+import checkKeyType from '../../lib/check_key_type.js';
+import validateCrit from '../../lib/validate_crit.js';
+
+export class FlattenedSign {
+    constructor(payload) {
+        if (!(payload instanceof Uint8Array)) {
+            throw new TypeError('payload must be an instance of Uint8Array');
+        }
+        this._payload = payload;
+    }
+    setProtectedHeader(protectedHeader) {
+        if (this._protectedHeader) {
+            throw new TypeError('setProtectedHeader can only be called once');
+        }
+        this._protectedHeader = protectedHeader;
+        return this;
+    }
+    setUnprotectedHeader(unprotectedHeader) {
+        if (this._unprotectedHeader) {
+            throw new TypeError('setUnprotectedHeader can only be called once');
+        }
+        this._unprotectedHeader = unprotectedHeader;
+        return this;
+    }
+    async sign(key, options) {
+        if (!this._protectedHeader && !this._unprotectedHeader) {
+            throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()');
+        }
+        if (!isDisjoint(this._protectedHeader, this._unprotectedHeader)) {
+            throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
+        }
+        const joseHeader = {
+            ...this._protectedHeader,
+            ...this._unprotectedHeader,
+        };
+        const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, this._protectedHeader, joseHeader);
+        let b64 = true;
+        if (extensions.has('b64')) {
+            b64 = this._protectedHeader.b64;
+            if (typeof b64 !== 'boolean') {
+                throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
+            }
+        }
+        const { alg } = joseHeader;
+        if (typeof alg !== 'string' || !alg) {
+            throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
+        }
+        checkKeyType(alg, key, 'sign');
+        let payload = this._payload;
+        if (b64) {
+            payload = encoder.encode(base64url(payload));
+        }
+        let protectedHeader;
+        if (this._protectedHeader) {
+            protectedHeader = encoder.encode(base64url(JSON.stringify(this._protectedHeader)));
+        }
+        else {
+            protectedHeader = encoder.encode('');
+        }
+        const data = concat(protectedHeader, encoder.encode('.'), payload);
+        const signature = await sign(alg, key, data);
+        const jws = {
+            signature: base64url(signature),
+            payload: '',
+        };
+        if (b64) {
+            jws.payload = decoder.decode(payload);
+        }
+        if (this._unprotectedHeader) {
+            jws.header = this._unprotectedHeader;
+        }
+        if (this._protectedHeader) {
+            jws.protected = decoder.decode(protectedHeader);
+        }
+        return jws;
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jws/flattened/verify.js b/internal/server/web/static/js/lib/jose/jws/flattened/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..2f30e86be0d0350be95ec55e4f96ab2dcbb2291a
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/flattened/verify.js
@@ -0,0 +1,105 @@
+import {decode as base64url} from '../../runtime/base64url.js';
+import verify from '../../runtime/verify.js';
+import {JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed} from '../../util/errors.js';
+import {concat, decoder, encoder} from '../../lib/buffer_utils.js';
+import isDisjoint from '../../lib/is_disjoint.js';
+import isObject from '../../lib/is_object.js';
+import checkKeyType from '../../lib/check_key_type.js';
+import validateCrit from '../../lib/validate_crit.js';
+import validateAlgorithms from '../../lib/validate_algorithms.js';
+
+export async function flattenedVerify(jws, key, options) {
+    var _a;
+    if (!isObject(jws)) {
+        throw new JWSInvalid('Flattened JWS must be an object');
+    }
+    if (jws.protected === undefined && jws.header === undefined) {
+        throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members');
+    }
+    if (jws.protected !== undefined && typeof jws.protected !== 'string') {
+        throw new JWSInvalid('JWS Protected Header incorrect type');
+    }
+    if (jws.payload === undefined) {
+        throw new JWSInvalid('JWS Payload missing');
+    }
+    if (typeof jws.signature !== 'string') {
+        throw new JWSInvalid('JWS Signature missing or incorrect type');
+    }
+    if (jws.header !== undefined && !isObject(jws.header)) {
+        throw new JWSInvalid('JWS Unprotected Header incorrect type');
+    }
+    let parsedProt = {};
+    if (jws.protected) {
+        const protectedHeader = base64url(jws.protected);
+        try {
+            parsedProt = JSON.parse(decoder.decode(protectedHeader));
+        }
+        catch (_b) {
+            throw new JWSInvalid('JWS Protected Header is invalid');
+        }
+    }
+    if (!isDisjoint(parsedProt, jws.header)) {
+        throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
+    }
+    const joseHeader = {
+        ...parsedProt,
+        ...jws.header,
+    };
+    const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, parsedProt, joseHeader);
+    let b64 = true;
+    if (extensions.has('b64')) {
+        b64 = parsedProt.b64;
+        if (typeof b64 !== 'boolean') {
+            throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
+        }
+    }
+    const { alg } = joseHeader;
+    if (typeof alg !== 'string' || !alg) {
+        throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
+    }
+    const algorithms = options && validateAlgorithms('algorithms', options.algorithms);
+    if (algorithms && !algorithms.has(alg)) {
+        throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter not allowed');
+    }
+    if (b64) {
+        if (typeof jws.payload !== 'string') {
+            throw new JWSInvalid('JWS Payload must be a string');
+        }
+    }
+    else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) {
+        throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance');
+    }
+    let resolvedKey = false;
+    if (typeof key === 'function') {
+        key = await key(parsedProt, jws);
+        resolvedKey = true;
+    }
+    checkKeyType(alg, key, 'verify');
+    const data = concat(encoder.encode((_a = jws.protected) !== null && _a !== void 0 ? _a : ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload);
+    const signature = base64url(jws.signature);
+    const verified = await verify(alg, key, signature, data);
+    if (!verified) {
+        throw new JWSSignatureVerificationFailed();
+    }
+    let payload;
+    if (b64) {
+        payload = base64url(jws.payload);
+    }
+    else if (typeof jws.payload === 'string') {
+        payload = encoder.encode(jws.payload);
+    }
+    else {
+        payload = jws.payload;
+    }
+    const result = { payload };
+    if (jws.protected !== undefined) {
+        result.protectedHeader = parsedProt;
+    }
+    if (jws.header !== undefined) {
+        result.unprotectedHeader = jws.header;
+    }
+    if (resolvedKey) {
+        return { ...result, key };
+    }
+    return result;
+}
diff --git a/internal/server/web/static/js/lib/jose/jws/general/sign.js b/internal/server/web/static/js/lib/jose/jws/general/sign.js
new file mode 100644
index 0000000000000000000000000000000000000000..3e1edd39be5462e379315fe47bdf69905b212bed
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/general/sign.js
@@ -0,0 +1,68 @@
+import {FlattenedSign} from '../flattened/sign.js';
+import {JWSInvalid} from '../../util/errors.js';
+
+class IndividualSignature {
+    constructor(sig, key, options) {
+        this.parent = sig;
+        this.key = key;
+        this.options = options;
+    }
+    setProtectedHeader(protectedHeader) {
+        if (this.protectedHeader) {
+            throw new TypeError('setProtectedHeader can only be called once');
+        }
+        this.protectedHeader = protectedHeader;
+        return this;
+    }
+    setUnprotectedHeader(unprotectedHeader) {
+        if (this.unprotectedHeader) {
+            throw new TypeError('setUnprotectedHeader can only be called once');
+        }
+        this.unprotectedHeader = unprotectedHeader;
+        return this;
+    }
+    addSignature(...args) {
+        return this.parent.addSignature(...args);
+    }
+    sign(...args) {
+        return this.parent.sign(...args);
+    }
+    done() {
+        return this.parent;
+    }
+}
+export class GeneralSign {
+    constructor(payload) {
+        this._signatures = [];
+        this._payload = payload;
+    }
+    addSignature(key, options) {
+        const signature = new IndividualSignature(this, key, options);
+        this._signatures.push(signature);
+        return signature;
+    }
+    async sign() {
+        if (!this._signatures.length) {
+            throw new JWSInvalid('at least one signature must be added');
+        }
+        const jws = {
+            signatures: [],
+            payload: '',
+        };
+        for (let i = 0; i < this._signatures.length; i++) {
+            const signature = this._signatures[i];
+            const flattened = new FlattenedSign(this._payload);
+            flattened.setProtectedHeader(signature.protectedHeader);
+            flattened.setUnprotectedHeader(signature.unprotectedHeader);
+            const { payload, ...rest } = await flattened.sign(signature.key, signature.options);
+            if (i === 0) {
+                jws.payload = payload;
+            }
+            else if (jws.payload !== payload) {
+                throw new JWSInvalid('inconsistent use of JWS Unencoded Payload Option (RFC7797)');
+            }
+            jws.signatures.push(rest);
+        }
+        return jws;
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jws/general/verify.js b/internal/server/web/static/js/lib/jose/jws/general/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0a73eae57227d762dde890ef3f325684a82dc1f
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jws/general/verify.js
@@ -0,0 +1,25 @@
+import {flattenedVerify} from '../flattened/verify.js';
+import {JWSInvalid, JWSSignatureVerificationFailed} from '../../util/errors.js';
+import isObject from '../../lib/is_object.js';
+
+export async function generalVerify(jws, key, options) {
+    if (!isObject(jws)) {
+        throw new JWSInvalid('General JWS must be an object');
+    }
+    if (!Array.isArray(jws.signatures) || !jws.signatures.every(isObject)) {
+        throw new JWSInvalid('JWS Signatures missing or incorrect type');
+    }
+    for (const signature of jws.signatures) {
+        try {
+            return await flattenedVerify({
+                header: signature.header,
+                payload: jws.payload,
+                protected: signature.protected,
+                signature: signature.signature,
+            }, key, options);
+        }
+        catch (_a) {
+        }
+    }
+    throw new JWSSignatureVerificationFailed();
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/decrypt.js b/internal/server/web/static/js/lib/jose/jwt/decrypt.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ae0014b9aa1575696c561fdb18c0fb4be20b0c4
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/decrypt.js
@@ -0,0 +1,24 @@
+import {compactDecrypt} from '../jwe/compact/decrypt.js';
+import jwtPayload from '../lib/jwt_claims_set.js';
+import {JWTClaimValidationFailed} from '../util/errors.js';
+
+export async function jwtDecrypt(jwt, key, options) {
+    const decrypted = await compactDecrypt(jwt, key, options);
+    const payload = jwtPayload(decrypted.protectedHeader, decrypted.plaintext, options);
+    const { protectedHeader } = decrypted;
+    if (protectedHeader.iss !== undefined && protectedHeader.iss !== payload.iss) {
+        throw new JWTClaimValidationFailed('replicated "iss" claim header parameter mismatch', 'iss', 'mismatch');
+    }
+    if (protectedHeader.sub !== undefined && protectedHeader.sub !== payload.sub) {
+        throw new JWTClaimValidationFailed('replicated "sub" claim header parameter mismatch', 'sub', 'mismatch');
+    }
+    if (protectedHeader.aud !== undefined &&
+        JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud)) {
+        throw new JWTClaimValidationFailed('replicated "aud" claim header parameter mismatch', 'aud', 'mismatch');
+    }
+    const result = { payload, protectedHeader };
+    if (typeof key === 'function') {
+        return { ...result, key: decrypted.key };
+    }
+    return result;
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/encrypt.js b/internal/server/web/static/js/lib/jose/jwt/encrypt.js
new file mode 100644
index 0000000000000000000000000000000000000000..bad7be2e13499a3784ddb8da402e2994ea724368
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/encrypt.js
@@ -0,0 +1,69 @@
+import {CompactEncrypt} from '../jwe/compact/encrypt.js';
+import {encoder} from '../lib/buffer_utils.js';
+import {ProduceJWT} from './produce.js';
+
+export class EncryptJWT extends ProduceJWT {
+    setProtectedHeader(protectedHeader) {
+        if (this._protectedHeader) {
+            throw new TypeError('setProtectedHeader can only be called once');
+        }
+        this._protectedHeader = protectedHeader;
+        return this;
+    }
+    setKeyManagementParameters(parameters) {
+        if (this._keyManagementParameters) {
+            throw new TypeError('setKeyManagementParameters can only be called once');
+        }
+        this._keyManagementParameters = parameters;
+        return this;
+    }
+    setContentEncryptionKey(cek) {
+        if (this._cek) {
+            throw new TypeError('setContentEncryptionKey can only be called once');
+        }
+        this._cek = cek;
+        return this;
+    }
+    setInitializationVector(iv) {
+        if (this._iv) {
+            throw new TypeError('setInitializationVector can only be called once');
+        }
+        this._iv = iv;
+        return this;
+    }
+    replicateIssuerAsHeader() {
+        this._replicateIssuerAsHeader = true;
+        return this;
+    }
+    replicateSubjectAsHeader() {
+        this._replicateSubjectAsHeader = true;
+        return this;
+    }
+    replicateAudienceAsHeader() {
+        this._replicateAudienceAsHeader = true;
+        return this;
+    }
+    async encrypt(key, options) {
+        const enc = new CompactEncrypt(encoder.encode(JSON.stringify(this._payload)));
+        if (this._replicateIssuerAsHeader) {
+            this._protectedHeader = { ...this._protectedHeader, iss: this._payload.iss };
+        }
+        if (this._replicateSubjectAsHeader) {
+            this._protectedHeader = { ...this._protectedHeader, sub: this._payload.sub };
+        }
+        if (this._replicateAudienceAsHeader) {
+            this._protectedHeader = { ...this._protectedHeader, aud: this._payload.aud };
+        }
+        enc.setProtectedHeader(this._protectedHeader);
+        if (this._iv) {
+            enc.setInitializationVector(this._iv);
+        }
+        if (this._cek) {
+            enc.setContentEncryptionKey(this._cek);
+        }
+        if (this._keyManagementParameters) {
+            enc.setKeyManagementParameters(this._keyManagementParameters);
+        }
+        return enc.encrypt(key, options);
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/produce.js b/internal/server/web/static/js/lib/jose/jwt/produce.js
new file mode 100644
index 0000000000000000000000000000000000000000..40a690771863250df00901210c5b43d586ae7d98
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/produce.js
@@ -0,0 +1,55 @@
+import epoch from '../lib/epoch.js';
+import isObject from '../lib/is_object.js';
+import secs from '../lib/secs.js';
+
+export class ProduceJWT {
+    constructor(payload) {
+        if (!isObject(payload)) {
+            throw new TypeError('JWT Claims Set MUST be an object');
+        }
+        this._payload = payload;
+    }
+    setIssuer(issuer) {
+        this._payload = { ...this._payload, iss: issuer };
+        return this;
+    }
+    setSubject(subject) {
+        this._payload = { ...this._payload, sub: subject };
+        return this;
+    }
+    setAudience(audience) {
+        this._payload = { ...this._payload, aud: audience };
+        return this;
+    }
+    setJti(jwtId) {
+        this._payload = { ...this._payload, jti: jwtId };
+        return this;
+    }
+    setNotBefore(input) {
+        if (typeof input === 'number') {
+            this._payload = { ...this._payload, nbf: input };
+        }
+        else {
+            this._payload = { ...this._payload, nbf: epoch(new Date()) + secs(input) };
+        }
+        return this;
+    }
+    setExpirationTime(input) {
+        if (typeof input === 'number') {
+            this._payload = { ...this._payload, exp: input };
+        }
+        else {
+            this._payload = { ...this._payload, exp: epoch(new Date()) + secs(input) };
+        }
+        return this;
+    }
+    setIssuedAt(input) {
+        if (typeof input === 'undefined') {
+            this._payload = { ...this._payload, iat: epoch(new Date()) };
+        }
+        else {
+            this._payload = { ...this._payload, iat: input };
+        }
+        return this;
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/sign.js b/internal/server/web/static/js/lib/jose/jwt/sign.js
new file mode 100644
index 0000000000000000000000000000000000000000..410c8c43e126ba0109df44283f1e5bb58fb9e4a3
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/sign.js
@@ -0,0 +1,22 @@
+import {CompactSign} from '../jws/compact/sign.js';
+import {JWTInvalid} from '../util/errors.js';
+import {encoder} from '../lib/buffer_utils.js';
+import {ProduceJWT} from './produce.js';
+
+export class SignJWT extends ProduceJWT {
+    setProtectedHeader(protectedHeader) {
+        this._protectedHeader = protectedHeader;
+        return this;
+    }
+    async sign(key, options) {
+        var _a;
+        const sig = new CompactSign(encoder.encode(JSON.stringify(this._payload)));
+        sig.setProtectedHeader(this._protectedHeader);
+        if (Array.isArray((_a = this._protectedHeader) === null || _a === void 0 ? void 0 : _a.crit) &&
+            this._protectedHeader.crit.includes('b64') &&
+            this._protectedHeader.b64 === false) {
+            throw new JWTInvalid('JWTs MUST NOT use unencoded payload');
+        }
+        return sig.sign(key, options);
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/unsecured.js b/internal/server/web/static/js/lib/jose/jwt/unsecured.js
new file mode 100644
index 0000000000000000000000000000000000000000..c9611abf3f5491b7ee0e7f3f4a4acf2461b2fb03
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/unsecured.js
@@ -0,0 +1,33 @@
+import * as base64url from '../runtime/base64url.js';
+import {decoder} from '../lib/buffer_utils.js';
+import {JWTInvalid} from '../util/errors.js';
+import jwtPayload from '../lib/jwt_claims_set.js';
+import {ProduceJWT} from './produce.js';
+
+export class UnsecuredJWT extends ProduceJWT {
+    encode() {
+        const header = base64url.encode(JSON.stringify({ alg: 'none' }));
+        const payload = base64url.encode(JSON.stringify(this._payload));
+        return `${header}.${payload}.`;
+    }
+    static decode(jwt, options) {
+        if (typeof jwt !== 'string') {
+            throw new JWTInvalid('Unsecured JWT must be a string');
+        }
+        const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split('.');
+        if (length !== 3 || signature !== '') {
+            throw new JWTInvalid('Invalid Unsecured JWT');
+        }
+        let header;
+        try {
+            header = JSON.parse(decoder.decode(base64url.decode(encodedHeader)));
+            if (header.alg !== 'none')
+                throw new Error();
+        }
+        catch (_a) {
+            throw new JWTInvalid('Invalid Unsecured JWT');
+        }
+        const payload = jwtPayload(header, base64url.decode(encodedPayload), options);
+        return { payload, header };
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/jwt/verify.js b/internal/server/web/static/js/lib/jose/jwt/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..cb991a8e5d7f90e6e2d7e82712f4c38bca2e3de6
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/jwt/verify.js
@@ -0,0 +1,17 @@
+import {compactVerify} from '../jws/compact/verify.js';
+import jwtPayload from '../lib/jwt_claims_set.js';
+import {JWTInvalid} from '../util/errors.js';
+
+export async function jwtVerify(jwt, key, options) {
+    var _a;
+    const verified = await compactVerify(jwt, key, options);
+    if (((_a = verified.protectedHeader.crit) === null || _a === void 0 ? void 0 : _a.includes('b64')) && verified.protectedHeader.b64 === false) {
+        throw new JWTInvalid('JWTs MUST NOT use unencoded payload');
+    }
+    const payload = jwtPayload(verified.protectedHeader, verified.payload, options);
+    const result = { payload, protectedHeader: verified.protectedHeader };
+    if (typeof key === 'function') {
+        return { ...result, key: verified.key };
+    }
+    return result;
+}
diff --git a/internal/server/web/static/js/lib/jose/key/export.js b/internal/server/web/static/js/lib/jose/key/export.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf612ec0de86681fea59128c25be8b1cf6a1af3c
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/key/export.js
@@ -0,0 +1,12 @@
+import {toPKCS8 as exportPrivate, toSPKI as exportPublic} from '../runtime/asn1.js';
+import keyToJWK from '../runtime/key_to_jwk.js';
+
+export async function exportSPKI(key) {
+    return exportPublic(key);
+}
+export async function exportPKCS8(key) {
+    return exportPrivate(key);
+}
+export async function exportJWK(key) {
+    return keyToJWK(key);
+}
diff --git a/internal/server/web/static/js/lib/jose/key/generate_key_pair.js b/internal/server/web/static/js/lib/jose/key/generate_key_pair.js
new file mode 100644
index 0000000000000000000000000000000000000000..88be595489ca0b6563f11bd8fcd257efb0fd1d88
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/key/generate_key_pair.js
@@ -0,0 +1,5 @@
+import {generateKeyPair as generate} from '../runtime/generate.js';
+
+export async function generateKeyPair(alg, options) {
+    return generate(alg, options);
+}
diff --git a/internal/server/web/static/js/lib/jose/key/generate_secret.js b/internal/server/web/static/js/lib/jose/key/generate_secret.js
new file mode 100644
index 0000000000000000000000000000000000000000..d80e08f7f402bdf72b22d78138478b857761d84a
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/key/generate_secret.js
@@ -0,0 +1,5 @@
+import {generateSecret as generate} from '../runtime/generate.js';
+
+export async function generateSecret(alg, options) {
+    return generate(alg, options);
+}
diff --git a/internal/server/web/static/js/lib/jose/key/import.js b/internal/server/web/static/js/lib/jose/key/import.js
new file mode 100644
index 0000000000000000000000000000000000000000..693446d31a59f3f27344ea9ba62463d45559a83a
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/key/import.js
@@ -0,0 +1,120 @@
+import {decode as decodeBase64URL, decodeBase64, encodeBase64} from '../runtime/base64url.js';
+import {fromPKCS8 as importPrivate, fromSPKI as importPublic} from '../runtime/asn1.js';
+import asKeyObject from '../runtime/jwk_to_key.js';
+import {JOSENotSupported} from '../util/errors.js';
+import formatPEM from '../lib/format_pem.js';
+import isObject from '../lib/is_object.js';
+
+function getElement(seq) {
+    let result = [];
+    let next = 0;
+    while (next < seq.length) {
+        let nextPart = parseElement(seq.subarray(next));
+        result.push(nextPart);
+        next += nextPart.byteLength;
+    }
+    return result;
+}
+function parseElement(bytes) {
+    let position = 0;
+    let tag = bytes[0] & 0x1f;
+    position++;
+    if (tag === 0x1f) {
+        tag = 0;
+        while (bytes[position] >= 0x80) {
+            tag = tag * 128 + bytes[position] - 0x80;
+            position++;
+        }
+        tag = tag * 128 + bytes[position] - 0x80;
+        position++;
+    }
+    let length = 0;
+    if (bytes[position] < 0x80) {
+        length = bytes[position];
+        position++;
+    }
+    else {
+        let numberOfDigits = bytes[position] & 0x7f;
+        position++;
+        length = 0;
+        for (let i = 0; i < numberOfDigits; i++) {
+            length = length * 256 + bytes[position];
+            position++;
+        }
+    }
+    if (length === 0x80) {
+        length = 0;
+        while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {
+            length++;
+        }
+        const byteLength = position + length + 2;
+        return {
+            byteLength,
+            contents: bytes.subarray(position, position + length),
+            raw: bytes.subarray(0, byteLength),
+        };
+    }
+    const byteLength = position + length;
+    return {
+        byteLength,
+        contents: bytes.subarray(position, byteLength),
+        raw: bytes.subarray(0, byteLength),
+    };
+}
+function spkiFromX509(buf) {
+    const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents);
+    return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw);
+}
+function getSPKI(x509) {
+    const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '');
+    const raw = decodeBase64(pem);
+    return formatPEM(spkiFromX509(raw), 'PUBLIC KEY');
+}
+export async function importSPKI(spki, alg, options) {
+    if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {
+        throw new TypeError('"spki" must be SPKI formatted string');
+    }
+    return importPublic(spki, alg, options);
+}
+export async function importX509(x509, alg, options) {
+    if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) {
+        throw new TypeError('"x509" must be X.509 formatted string');
+    }
+    const spki = getSPKI(x509);
+    return importPublic(spki, alg, options);
+}
+export async function importPKCS8(pkcs8, alg, options) {
+    if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) {
+        throw new TypeError('"pkcs8" must be PCKS8 formatted string');
+    }
+    return importPrivate(pkcs8, alg, options);
+}
+export async function importJWK(jwk, alg, octAsKeyObject) {
+    if (!isObject(jwk)) {
+        throw new TypeError('JWK must be an object');
+    }
+    alg || (alg = jwk.alg);
+    if (typeof alg !== 'string' || !alg) {
+        throw new TypeError('"alg" argument is required when "jwk.alg" is not present');
+    }
+    switch (jwk.kty) {
+        case 'oct':
+            if (typeof jwk.k !== 'string' || !jwk.k) {
+                throw new TypeError('missing "k" (Key Value) Parameter value');
+            }
+            octAsKeyObject !== null && octAsKeyObject !== void 0 ? octAsKeyObject : (octAsKeyObject = jwk.ext !== true);
+            if (octAsKeyObject) {
+                return asKeyObject({ ...jwk, alg, ext: false });
+            }
+            return decodeBase64URL(jwk.k);
+        case 'RSA':
+            if (jwk.oth !== undefined) {
+                throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported');
+            }
+        case 'EC':
+        case 'OKP':
+            return asKeyObject({ ...jwk, alg });
+        default:
+            throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value');
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js b/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js
new file mode 100644
index 0000000000000000000000000000000000000000..663781ee50c46c332711eb7d1e1a0062582119ed
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js
@@ -0,0 +1,15 @@
+import encrypt from '../runtime/encrypt.js';
+import decrypt from '../runtime/decrypt.js';
+import generateIv from './iv.js';
+import {encode as base64url} from '../runtime/base64url.js';
+
+export async function wrap(alg, key, cek, iv) {
+    const jweAlgorithm = alg.slice(0, 7);
+    iv || (iv = generateIv(jweAlgorithm));
+    const { ciphertext: encryptedKey, tag } = await encrypt(jweAlgorithm, cek, key, iv, new Uint8Array(0));
+    return { encryptedKey, iv: base64url(iv), tag: base64url(tag) };
+}
+export async function unwrap(alg, key, encryptedKey, iv, tag) {
+    const jweAlgorithm = alg.slice(0, 7);
+    return decrypt(jweAlgorithm, key, encryptedKey, iv, tag, new Uint8Array(0));
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/buffer_utils.js b/internal/server/web/static/js/lib/jose/lib/buffer_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..d8a22e79798e5d3f2dea9c7a0b80eb836ca8cc6e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/buffer_utils.js
@@ -0,0 +1,52 @@
+import digest from '../runtime/digest.js';
+
+export const encoder = new TextEncoder();
+export const decoder = new TextDecoder();
+const MAX_INT32 = 2 ** 32;
+export function concat(...buffers) {
+    const size = buffers.reduce((acc, { length }) => acc + length, 0);
+    const buf = new Uint8Array(size);
+    let i = 0;
+    buffers.forEach((buffer) => {
+        buf.set(buffer, i);
+        i += buffer.length;
+    });
+    return buf;
+}
+export function p2s(alg, p2sInput) {
+    return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput);
+}
+function writeUInt32BE(buf, value, offset) {
+    if (value < 0 || value >= MAX_INT32) {
+        throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
+    }
+    buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);
+}
+export function uint64be(value) {
+    const high = Math.floor(value / MAX_INT32);
+    const low = value % MAX_INT32;
+    const buf = new Uint8Array(8);
+    writeUInt32BE(buf, high, 0);
+    writeUInt32BE(buf, low, 4);
+    return buf;
+}
+export function uint32be(value) {
+    const buf = new Uint8Array(4);
+    writeUInt32BE(buf, value);
+    return buf;
+}
+export function lengthAndInput(input) {
+    return concat(uint32be(input.length), input);
+}
+export async function concatKdf(secret, bits, value) {
+    const iterations = Math.ceil((bits >> 3) / 32);
+    const res = new Uint8Array(iterations * 32);
+    for (let iter = 0; iter < iterations; iter++) {
+        const buf = new Uint8Array(4 + secret.length + value.length);
+        buf.set(uint32be(iter + 1));
+        buf.set(secret, 4);
+        buf.set(value, 4 + secret.length);
+        res.set(await digest('sha256', buf), iter * 32);
+    }
+    return res.slice(0, bits >> 3);
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/cek.js b/internal/server/web/static/js/lib/jose/lib/cek.js
new file mode 100644
index 0000000000000000000000000000000000000000..f00c83776f6511db11d23a04d29fb0994395519b
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/cek.js
@@ -0,0 +1,21 @@
+import {JOSENotSupported} from '../util/errors.js';
+import random from '../runtime/random.js';
+
+export function bitLength(alg) {
+    switch (alg) {
+        case 'A128GCM':
+            return 128;
+        case 'A192GCM':
+            return 192;
+        case 'A256GCM':
+        case 'A128CBC-HS256':
+            return 256;
+        case 'A192CBC-HS384':
+            return 384;
+        case 'A256CBC-HS512':
+            return 512;
+        default:
+            throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`);
+    }
+}
+export default (alg) => random(new Uint8Array(bitLength(alg) >> 3));
diff --git a/internal/server/web/static/js/lib/jose/lib/check_iv_length.js b/internal/server/web/static/js/lib/jose/lib/check_iv_length.js
new file mode 100644
index 0000000000000000000000000000000000000000..7e361c25828c0e35cb77266ad7c4f48390e7c48a
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/check_iv_length.js
@@ -0,0 +1,9 @@
+import {JWEInvalid} from '../util/errors.js';
+import {bitLength} from './iv.js';
+
+const checkIvLength = (enc, iv) => {
+    if (iv.length << 3 !== bitLength(enc)) {
+        throw new JWEInvalid('Invalid Initialization Vector length');
+    }
+};
+export default checkIvLength;
diff --git a/internal/server/web/static/js/lib/jose/lib/check_key_type.js b/internal/server/web/static/js/lib/jose/lib/check_key_type.js
new file mode 100644
index 0000000000000000000000000000000000000000..a78030b42b9c840624133c92a7c1a7e675e12f6f
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/check_key_type.js
@@ -0,0 +1,46 @@
+import invalidKeyInput from './invalid_key_input.js';
+import isKeyLike, {types} from '../runtime/is_key_like.js';
+
+const symmetricTypeCheck = (key) => {
+    if (key instanceof Uint8Array)
+        return;
+    if (!isKeyLike(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));
+    }
+    if (key.type !== 'secret') {
+        throw new TypeError(`${types.join(' or ')} instances for symmetric algorithms must be of type "secret"`);
+    }
+};
+const asymmetricTypeCheck = (key, usage) => {
+    if (!isKeyLike(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    if (key.type === 'secret') {
+        throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithms must not be of type "secret"`);
+    }
+    if (usage === 'sign' && key.type === 'public') {
+        throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm signing must be of type "private"`);
+    }
+    if (usage === 'decrypt' && key.type === 'public') {
+        throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm decryption must be of type "private"`);
+    }
+    if (key.algorithm && usage === 'verify' && key.type === 'private') {
+        throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm verifying must be of type "public"`);
+    }
+    if (key.algorithm && usage === 'encrypt' && key.type === 'private') {
+        throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm encryption must be of type "public"`);
+    }
+};
+const checkKeyType = (alg, key, usage) => {
+    const symmetric = alg.startsWith('HS') ||
+        alg === 'dir' ||
+        alg.startsWith('PBES2') ||
+        /^A\d{3}(?:GCM)?KW$/.test(alg);
+    if (symmetric) {
+        symmetricTypeCheck(key);
+    }
+    else {
+        asymmetricTypeCheck(key, usage);
+    }
+};
+export default checkKeyType;
diff --git a/internal/server/web/static/js/lib/jose/lib/check_p2s.js b/internal/server/web/static/js/lib/jose/lib/check_p2s.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffd046584e504bda698a342b88f2dbd820549962
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/check_p2s.js
@@ -0,0 +1,7 @@
+import {JWEInvalid} from '../util/errors.js';
+
+export default function checkP2s(p2s) {
+    if (!(p2s instanceof Uint8Array) || p2s.length < 8) {
+        throw new JWEInvalid('PBES2 Salt Input must be 8 or more octets');
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/crypto_key.js b/internal/server/web/static/js/lib/jose/lib/crypto_key.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1b9edbf7fe2a314c2ea54e42b67fc84724fbe74
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/crypto_key.js
@@ -0,0 +1,146 @@
+import {isCloudflareWorkers} from '../runtime/env.js';
+
+function unusable(name, prop = 'algorithm.name') {
+    return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
+}
+function isAlgorithm(algorithm, name) {
+    return algorithm.name === name;
+}
+function getHashLength(hash) {
+    return parseInt(hash.name.slice(4), 10);
+}
+function getNamedCurve(alg) {
+    switch (alg) {
+        case 'ES256':
+            return 'P-256';
+        case 'ES384':
+            return 'P-384';
+        case 'ES512':
+            return 'P-521';
+        default:
+            throw new Error('unreachable');
+    }
+}
+function checkUsage(key, usages) {
+    if (usages.length && !usages.some((expected) => key.usages.includes(expected))) {
+        let msg = 'CryptoKey does not support this operation, its usages must include ';
+        if (usages.length > 2) {
+            const last = usages.pop();
+            msg += `one of ${usages.join(', ')}, or ${last}.`;
+        }
+        else if (usages.length === 2) {
+            msg += `one of ${usages[0]} or ${usages[1]}.`;
+        }
+        else {
+            msg += `${usages[0]}.`;
+        }
+        throw new TypeError(msg);
+    }
+}
+export function checkSigCryptoKey(key, alg, ...usages) {
+    switch (alg) {
+        case 'HS256':
+        case 'HS384':
+        case 'HS512': {
+            if (!isAlgorithm(key.algorithm, 'HMAC'))
+                throw unusable('HMAC');
+            const expected = parseInt(alg.slice(2), 10);
+            const actual = getHashLength(key.algorithm.hash);
+            if (actual !== expected)
+                throw unusable(`SHA-${expected}`, 'algorithm.hash');
+            break;
+        }
+        case 'RS256':
+        case 'RS384':
+        case 'RS512': {
+            if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5'))
+                throw unusable('RSASSA-PKCS1-v1_5');
+            const expected = parseInt(alg.slice(2), 10);
+            const actual = getHashLength(key.algorithm.hash);
+            if (actual !== expected)
+                throw unusable(`SHA-${expected}`, 'algorithm.hash');
+            break;
+        }
+        case 'PS256':
+        case 'PS384':
+        case 'PS512': {
+            if (!isAlgorithm(key.algorithm, 'RSA-PSS'))
+                throw unusable('RSA-PSS');
+            const expected = parseInt(alg.slice(2), 10);
+            const actual = getHashLength(key.algorithm.hash);
+            if (actual !== expected)
+                throw unusable(`SHA-${expected}`, 'algorithm.hash');
+            break;
+        }
+        case isCloudflareWorkers() && 'EdDSA': {
+            if (!isAlgorithm(key.algorithm, 'NODE-ED25519'))
+                throw unusable('NODE-ED25519');
+            break;
+        }
+        case 'ES256':
+        case 'ES384':
+        case 'ES512': {
+            if (!isAlgorithm(key.algorithm, 'ECDSA'))
+                throw unusable('ECDSA');
+            const expected = getNamedCurve(alg);
+            const actual = key.algorithm.namedCurve;
+            if (actual !== expected)
+                throw unusable(expected, 'algorithm.namedCurve');
+            break;
+        }
+        default:
+            throw new TypeError('CryptoKey does not support this operation');
+    }
+    checkUsage(key, usages);
+}
+export function checkEncCryptoKey(key, alg, ...usages) {
+    switch (alg) {
+        case 'A128GCM':
+        case 'A192GCM':
+        case 'A256GCM': {
+            if (!isAlgorithm(key.algorithm, 'AES-GCM'))
+                throw unusable('AES-GCM');
+            const expected = parseInt(alg.slice(1, 4), 10);
+            const actual = key.algorithm.length;
+            if (actual !== expected)
+                throw unusable(expected, 'algorithm.length');
+            break;
+        }
+        case 'A128KW':
+        case 'A192KW':
+        case 'A256KW': {
+            if (!isAlgorithm(key.algorithm, 'AES-KW'))
+                throw unusable('AES-KW');
+            const expected = parseInt(alg.slice(1, 4), 10);
+            const actual = key.algorithm.length;
+            if (actual !== expected)
+                throw unusable(expected, 'algorithm.length');
+            break;
+        }
+        case 'ECDH':
+            if (!isAlgorithm(key.algorithm, 'ECDH'))
+                throw unusable('ECDH');
+            break;
+        case 'PBES2-HS256+A128KW':
+        case 'PBES2-HS384+A192KW':
+        case 'PBES2-HS512+A256KW':
+            if (!isAlgorithm(key.algorithm, 'PBKDF2'))
+                throw unusable('PBKDF2');
+            break;
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512': {
+            if (!isAlgorithm(key.algorithm, 'RSA-OAEP'))
+                throw unusable('RSA-OAEP');
+            const expected = parseInt(alg.slice(9), 10) || 1;
+            const actual = getHashLength(key.algorithm.hash);
+            if (actual !== expected)
+                throw unusable(`SHA-${expected}`, 'algorithm.hash');
+            break;
+        }
+        default:
+            throw new TypeError('CryptoKey does not support this operation');
+    }
+    checkUsage(key, usages);
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js b/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js
new file mode 100644
index 0000000000000000000000000000000000000000..9252979169a6c41709337e390eba5d36bcd49aee
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js
@@ -0,0 +1,96 @@
+import {unwrap as aesKw} from '../runtime/aeskw.js';
+import * as ECDH from '../runtime/ecdhes.js';
+import {decrypt as pbes2Kw} from '../runtime/pbes2kw.js';
+import {decrypt as rsaEs} from '../runtime/rsaes.js';
+import {decode as base64url} from '../runtime/base64url.js';
+import {JOSENotSupported, JWEInvalid} from '../util/errors.js';
+import {bitLength as cekLength} from '../lib/cek.js';
+import {importJWK} from '../key/import.js';
+import checkKeyType from './check_key_type.js';
+import isObject from './is_object.js';
+import {unwrap as aesGcmKw} from './aesgcmkw.js';
+
+async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
+    checkKeyType(alg, key, 'decrypt');
+    switch (alg) {
+        case 'dir': {
+            if (encryptedKey !== undefined)
+                throw new JWEInvalid('Encountered unexpected JWE Encrypted Key');
+            return key;
+        }
+        case 'ECDH-ES':
+            if (encryptedKey !== undefined)
+                throw new JWEInvalid('Encountered unexpected JWE Encrypted Key');
+        case 'ECDH-ES+A128KW':
+        case 'ECDH-ES+A192KW':
+        case 'ECDH-ES+A256KW': {
+            if (!isObject(joseHeader.epk))
+                throw new JWEInvalid(`JOSE Header "epk" (Ephemeral Public Key) missing or invalid`);
+            if (!ECDH.ecdhAllowed(key))
+                throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime');
+            const epk = await importJWK(joseHeader.epk, alg);
+            let partyUInfo;
+            let partyVInfo;
+            if (joseHeader.apu !== undefined) {
+                if (typeof joseHeader.apu !== 'string')
+                    throw new JWEInvalid(`JOSE Header "apu" (Agreement PartyUInfo) invalid`);
+                partyUInfo = base64url(joseHeader.apu);
+            }
+            if (joseHeader.apv !== undefined) {
+                if (typeof joseHeader.apv !== 'string')
+                    throw new JWEInvalid(`JOSE Header "apv" (Agreement PartyVInfo) invalid`);
+                partyVInfo = base64url(joseHeader.apv);
+            }
+            const sharedSecret = await ECDH.deriveKey(epk, key, alg === 'ECDH-ES' ? joseHeader.enc : alg, alg === 'ECDH-ES' ? cekLength(joseHeader.enc) : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo);
+            if (alg === 'ECDH-ES')
+                return sharedSecret;
+            if (encryptedKey === undefined)
+                throw new JWEInvalid('JWE Encrypted Key missing');
+            return aesKw(alg.slice(-6), sharedSecret, encryptedKey);
+        }
+        case 'RSA1_5':
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512': {
+            if (encryptedKey === undefined)
+                throw new JWEInvalid('JWE Encrypted Key missing');
+            return rsaEs(alg, key, encryptedKey);
+        }
+        case 'PBES2-HS256+A128KW':
+        case 'PBES2-HS384+A192KW':
+        case 'PBES2-HS512+A256KW': {
+            if (encryptedKey === undefined)
+                throw new JWEInvalid('JWE Encrypted Key missing');
+            if (typeof joseHeader.p2c !== 'number')
+                throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`);
+            if (typeof joseHeader.p2s !== 'string')
+                throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`);
+            return pbes2Kw(alg, key, encryptedKey, joseHeader.p2c, base64url(joseHeader.p2s));
+        }
+        case 'A128KW':
+        case 'A192KW':
+        case 'A256KW': {
+            if (encryptedKey === undefined)
+                throw new JWEInvalid('JWE Encrypted Key missing');
+            return aesKw(alg, key, encryptedKey);
+        }
+        case 'A128GCMKW':
+        case 'A192GCMKW':
+        case 'A256GCMKW': {
+            if (encryptedKey === undefined)
+                throw new JWEInvalid('JWE Encrypted Key missing');
+            if (typeof joseHeader.iv !== 'string')
+                throw new JWEInvalid(`JOSE Header "iv" (Initialization Vector) missing or invalid`);
+            if (typeof joseHeader.tag !== 'string')
+                throw new JWEInvalid(`JOSE Header "tag" (Authentication Tag) missing or invalid`);
+            const iv = base64url(joseHeader.iv);
+            const tag = base64url(joseHeader.tag);
+            return aesGcmKw(alg, key, encryptedKey, iv, tag);
+        }
+        default: {
+            throw new JOSENotSupported('Invalid or unsupported "alg" (JWE Algorithm) header value');
+        }
+    }
+}
+export default decryptKeyManagement;
diff --git a/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js b/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ec9c6bc8c85c846fa3105822f910bd15a0ee5a2
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js
@@ -0,0 +1,88 @@
+import {wrap as aesKw} from '../runtime/aeskw.js';
+import * as ECDH from '../runtime/ecdhes.js';
+import {encrypt as pbes2Kw} from '../runtime/pbes2kw.js';
+import {encrypt as rsaEs} from '../runtime/rsaes.js';
+import {encode as base64url} from '../runtime/base64url.js';
+import generateCek, {bitLength as cekLength} from '../lib/cek.js';
+import {JOSENotSupported} from '../util/errors.js';
+import {exportJWK} from '../key/export.js';
+import checkKeyType from './check_key_type.js';
+import {wrap as aesGcmKw} from './aesgcmkw.js';
+
+async function encryptKeyManagement(alg, enc, key, providedCek, providedParameters = {}) {
+    let encryptedKey;
+    let parameters;
+    let cek;
+    checkKeyType(alg, key, 'encrypt');
+    switch (alg) {
+        case 'dir': {
+            cek = key;
+            break;
+        }
+        case 'ECDH-ES':
+        case 'ECDH-ES+A128KW':
+        case 'ECDH-ES+A192KW':
+        case 'ECDH-ES+A256KW': {
+            if (!ECDH.ecdhAllowed(key)) {
+                throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime');
+            }
+            const { apu, apv } = providedParameters;
+            let { epk: ephemeralKey } = providedParameters;
+            ephemeralKey || (ephemeralKey = (await ECDH.generateEpk(key)).privateKey);
+            const { x, y, crv, kty } = await exportJWK(ephemeralKey);
+            const sharedSecret = await ECDH.deriveKey(key, ephemeralKey, alg === 'ECDH-ES' ? enc : alg, alg === 'ECDH-ES' ? cekLength(enc) : parseInt(alg.slice(-5, -2), 10), apu, apv);
+            parameters = { epk: { x, crv, kty } };
+            if (kty === 'EC')
+                parameters.epk.y = y;
+            if (apu)
+                parameters.apu = base64url(apu);
+            if (apv)
+                parameters.apv = base64url(apv);
+            if (alg === 'ECDH-ES') {
+                cek = sharedSecret;
+                break;
+            }
+            cek = providedCek || generateCek(enc);
+            const kwAlg = alg.slice(-6);
+            encryptedKey = await aesKw(kwAlg, sharedSecret, cek);
+            break;
+        }
+        case 'RSA1_5':
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512': {
+            cek = providedCek || generateCek(enc);
+            encryptedKey = await rsaEs(alg, key, cek);
+            break;
+        }
+        case 'PBES2-HS256+A128KW':
+        case 'PBES2-HS384+A192KW':
+        case 'PBES2-HS512+A256KW': {
+            cek = providedCek || generateCek(enc);
+            const { p2c, p2s } = providedParameters;
+            ({ encryptedKey, ...parameters } = await pbes2Kw(alg, key, cek, p2c, p2s));
+            break;
+        }
+        case 'A128KW':
+        case 'A192KW':
+        case 'A256KW': {
+            cek = providedCek || generateCek(enc);
+            encryptedKey = await aesKw(alg, key, cek);
+            break;
+        }
+        case 'A128GCMKW':
+        case 'A192GCMKW':
+        case 'A256GCMKW': {
+            cek = providedCek || generateCek(enc);
+            const { iv } = providedParameters;
+            ({ encryptedKey, ...parameters } = await aesGcmKw(alg, key, cek, iv));
+            break;
+        }
+        default: {
+            throw new JOSENotSupported('Invalid or unsupported "alg" (JWE Algorithm) header value');
+        }
+    }
+    return { cek, encryptedKey, parameters };
+}
+export default encryptKeyManagement;
diff --git a/internal/server/web/static/js/lib/jose/lib/epoch.js b/internal/server/web/static/js/lib/jose/lib/epoch.js
new file mode 100644
index 0000000000000000000000000000000000000000..e405e4b2df8336a5c54452047c187ede4fb168d6
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/epoch.js
@@ -0,0 +1 @@
+export default (date) => Math.floor(date.getTime() / 1000);
diff --git a/internal/server/web/static/js/lib/jose/lib/format_pem.js b/internal/server/web/static/js/lib/jose/lib/format_pem.js
new file mode 100644
index 0000000000000000000000000000000000000000..81673f256f3e36c16d32648a91be96b430d4728e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/format_pem.js
@@ -0,0 +1,4 @@
+export default (b64, descriptor) => {
+    const newlined = (b64.match(/.{1,64}/g) || []).join('\n');
+    return `-----BEGIN ${descriptor}-----\n${newlined}\n-----END ${descriptor}-----`;
+};
diff --git a/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js b/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js
new file mode 100644
index 0000000000000000000000000000000000000000..468ad288b4d1cd2714057919f9d4ca9e91c11f5c
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js
@@ -0,0 +1,25 @@
+export default (actual, ...types) => {
+    let msg = 'Key must be ';
+    if (types.length > 2) {
+        const last = types.pop();
+        msg += `one of type ${types.join(', ')}, or ${last}.`;
+    }
+    else if (types.length === 2) {
+        msg += `one of type ${types[0]} or ${types[1]}.`;
+    }
+    else {
+        msg += `of type ${types[0]}.`;
+    }
+    if (actual == null) {
+        msg += ` Received ${actual}`;
+    }
+    else if (typeof actual === 'function' && actual.name) {
+        msg += ` Received function ${actual.name}`;
+    }
+    else if (typeof actual === 'object' && actual != null) {
+        if (actual.constructor && actual.constructor.name) {
+            msg += ` Received an instance of ${actual.constructor.name}`;
+        }
+    }
+    return msg;
+};
diff --git a/internal/server/web/static/js/lib/jose/lib/is_disjoint.js b/internal/server/web/static/js/lib/jose/lib/is_disjoint.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f643502dcffd4fa684c6554fd715a97b1a7113b
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/is_disjoint.js
@@ -0,0 +1,22 @@
+const isDisjoint = (...headers) => {
+    const sources = headers.filter(Boolean);
+    if (sources.length === 0 || sources.length === 1) {
+        return true;
+    }
+    let acc;
+    for (const header of sources) {
+        const parameters = Object.keys(header);
+        if (!acc || acc.size === 0) {
+            acc = new Set(parameters);
+            continue;
+        }
+        for (const parameter of parameters) {
+            if (acc.has(parameter)) {
+                return false;
+            }
+            acc.add(parameter);
+        }
+    }
+    return true;
+};
+export default isDisjoint;
diff --git a/internal/server/web/static/js/lib/jose/lib/is_object.js b/internal/server/web/static/js/lib/jose/lib/is_object.js
new file mode 100644
index 0000000000000000000000000000000000000000..4955e93225d63bfc83b9805cf71872ba2d158eb3
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/is_object.js
@@ -0,0 +1,16 @@
+function isObjectLike(value) {
+    return typeof value === 'object' && value !== null;
+}
+export default function isObject(input) {
+    if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') {
+        return false;
+    }
+    if (Object.getPrototypeOf(input) === null) {
+        return true;
+    }
+    let proto = input;
+    while (Object.getPrototypeOf(proto) !== null) {
+        proto = Object.getPrototypeOf(proto);
+    }
+    return Object.getPrototypeOf(input) === proto;
+}
diff --git a/internal/server/web/static/js/lib/jose/lib/iv.js b/internal/server/web/static/js/lib/jose/lib/iv.js
new file mode 100644
index 0000000000000000000000000000000000000000..e477b54896b37777fa7183325ac8a334f94e139b
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/iv.js
@@ -0,0 +1,21 @@
+import {JOSENotSupported} from '../util/errors.js';
+import random from '../runtime/random.js';
+
+export function bitLength(alg) {
+    switch (alg) {
+        case 'A128GCM':
+        case 'A128GCMKW':
+        case 'A192GCM':
+        case 'A192GCMKW':
+        case 'A256GCM':
+        case 'A256GCMKW':
+            return 96;
+        case 'A128CBC-HS256':
+        case 'A192CBC-HS384':
+        case 'A256CBC-HS512':
+            return 128;
+        default:
+            throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`);
+    }
+}
+export default (alg) => random(new Uint8Array(bitLength(alg) >> 3));
diff --git a/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js b/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js
new file mode 100644
index 0000000000000000000000000000000000000000..56feec48171d871db39cf4c540fd153e2b5ea793
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js
@@ -0,0 +1,92 @@
+import {JWTClaimValidationFailed, JWTExpired, JWTInvalid} from '../util/errors.js';
+import {decoder} from './buffer_utils.js';
+import epoch from './epoch.js';
+import secs from './secs.js';
+import isObject from './is_object.js';
+
+const normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, '');
+const checkAudiencePresence = (audPayload, audOption) => {
+    if (typeof audPayload === 'string') {
+        return audOption.includes(audPayload);
+    }
+    if (Array.isArray(audPayload)) {
+        return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
+    }
+    return false;
+};
+export default (protectedHeader, encodedPayload, options = {}) => {
+    const { typ } = options;
+    if (typ &&
+        (typeof protectedHeader.typ !== 'string' ||
+            normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
+        throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', 'typ', 'check_failed');
+    }
+    let payload;
+    try {
+        payload = JSON.parse(decoder.decode(encodedPayload));
+    }
+    catch (_a) {
+    }
+    if (!isObject(payload)) {
+        throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');
+    }
+    const { issuer } = options;
+    if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
+        throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed');
+    }
+    const { subject } = options;
+    if (subject && payload.sub !== subject) {
+        throw new JWTClaimValidationFailed('unexpected "sub" claim value', 'sub', 'check_failed');
+    }
+    const { audience } = options;
+    if (audience &&
+        !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {
+        throw new JWTClaimValidationFailed('unexpected "aud" claim value', 'aud', 'check_failed');
+    }
+    let tolerance;
+    switch (typeof options.clockTolerance) {
+        case 'string':
+            tolerance = secs(options.clockTolerance);
+            break;
+        case 'number':
+            tolerance = options.clockTolerance;
+            break;
+        case 'undefined':
+            tolerance = 0;
+            break;
+        default:
+            throw new TypeError('Invalid clockTolerance option type');
+    }
+    const { currentDate } = options;
+    const now = epoch(currentDate || new Date());
+    if ((payload.iat !== undefined || options.maxTokenAge) && typeof payload.iat !== 'number') {
+        throw new JWTClaimValidationFailed('"iat" claim must be a number', 'iat', 'invalid');
+    }
+    if (payload.nbf !== undefined) {
+        if (typeof payload.nbf !== 'number') {
+            throw new JWTClaimValidationFailed('"nbf" claim must be a number', 'nbf', 'invalid');
+        }
+        if (payload.nbf > now + tolerance) {
+            throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', 'nbf', 'check_failed');
+        }
+    }
+    if (payload.exp !== undefined) {
+        if (typeof payload.exp !== 'number') {
+            throw new JWTClaimValidationFailed('"exp" claim must be a number', 'exp', 'invalid');
+        }
+        if (payload.exp <= now - tolerance) {
+            throw new JWTExpired('"exp" claim timestamp check failed', 'exp', 'check_failed');
+        }
+    }
+    if (options.maxTokenAge) {
+        const age = now - payload.iat;
+        const max = typeof options.maxTokenAge === 'number' ? options.maxTokenAge : secs(options.maxTokenAge);
+        if (age - tolerance > max) {
+            throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', 'iat', 'check_failed');
+        }
+        if (age < 0 - tolerance) {
+            throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed');
+        }
+    }
+    return payload;
+};
diff --git a/internal/server/web/static/js/lib/jose/lib/secs.js b/internal/server/web/static/js/lib/jose/lib/secs.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf470ed8ad45c393b273f25562b8d06d2a642b59
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/secs.js
@@ -0,0 +1,44 @@
+const minute = 60;
+const hour = minute * 60;
+const day = hour * 24;
+const week = day * 7;
+const year = day * 365.25;
+const REGEX = /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i;
+export default (str) => {
+    const matched = REGEX.exec(str);
+    if (!matched) {
+        throw new TypeError('Invalid time period format');
+    }
+    const value = parseFloat(matched[1]);
+    const unit = matched[2].toLowerCase();
+    switch (unit) {
+        case 'sec':
+        case 'secs':
+        case 'second':
+        case 'seconds':
+        case 's':
+            return Math.round(value);
+        case 'minute':
+        case 'minutes':
+        case 'min':
+        case 'mins':
+        case 'm':
+            return Math.round(value * minute);
+        case 'hour':
+        case 'hours':
+        case 'hr':
+        case 'hrs':
+        case 'h':
+            return Math.round(value * hour);
+        case 'day':
+        case 'days':
+        case 'd':
+            return Math.round(value * day);
+        case 'week':
+        case 'weeks':
+        case 'w':
+            return Math.round(value * week);
+        default:
+            return Math.round(value * year);
+    }
+};
diff --git a/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js b/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6a7918571f55526070266826dd48dd6e165c38e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js
@@ -0,0 +1,11 @@
+const validateAlgorithms = (option, algorithms) => {
+    if (algorithms !== undefined &&
+        (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) {
+        throw new TypeError(`"${option}" option must be an array of strings`);
+    }
+    if (!algorithms) {
+        return undefined;
+    }
+    return new Set(algorithms);
+};
+export default validateAlgorithms;
diff --git a/internal/server/web/static/js/lib/jose/lib/validate_crit.js b/internal/server/web/static/js/lib/jose/lib/validate_crit.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebbf4354a70b4615cb8b3d40c74ed02d78c6425e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/lib/validate_crit.js
@@ -0,0 +1,35 @@
+import {JOSENotSupported} from '../util/errors.js';
+
+function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
+    if (joseHeader.crit !== undefined && protectedHeader.crit === undefined) {
+        throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
+    }
+    if (!protectedHeader || protectedHeader.crit === undefined) {
+        return new Set();
+    }
+    if (!Array.isArray(protectedHeader.crit) ||
+        protectedHeader.crit.length === 0 ||
+        protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) {
+        throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
+    }
+    let recognized;
+    if (recognizedOption !== undefined) {
+        recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
+    }
+    else {
+        recognized = recognizedDefault;
+    }
+    for (const parameter of protectedHeader.crit) {
+        if (!recognized.has(parameter)) {
+            throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
+        }
+        if (joseHeader[parameter] === undefined) {
+            throw new Err(`Extension Header Parameter "${parameter}" is missing`);
+        }
+        else if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {
+            throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
+        }
+    }
+    return new Set(protectedHeader.crit);
+}
+export default validateCrit;
diff --git a/internal/server/web/static/js/lib/jose/runtime/aeskw.js b/internal/server/web/static/js/lib/jose/runtime/aeskw.js
new file mode 100644
index 0000000000000000000000000000000000000000..242723997d6687e55c70ff92f803287601d8fe9d
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/aeskw.js
@@ -0,0 +1,33 @@
+import bogusWebCrypto from './bogus.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+function checkKeySize(key, alg) {
+    if (key.algorithm.length !== parseInt(alg.slice(1, 4), 10)) {
+        throw new TypeError(`Invalid key size for alg: ${alg}`);
+    }
+}
+function getCryptoKey(key, alg, usage) {
+    if (isCryptoKey(key)) {
+        checkEncCryptoKey(key, alg, usage);
+        return key;
+    }
+    if (key instanceof Uint8Array) {
+        return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage]);
+    }
+    throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));
+}
+export const wrap = async (alg, key, cek) => {
+    const cryptoKey = await getCryptoKey(key, alg, 'wrapKey');
+    checkKeySize(cryptoKey, alg);
+    const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, ...bogusWebCrypto);
+    return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, cryptoKey, 'AES-KW'));
+};
+export const unwrap = async (alg, key, encryptedKey) => {
+    const cryptoKey = await getCryptoKey(key, alg, 'unwrapKey');
+    checkKeySize(cryptoKey, alg);
+    const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, cryptoKey, 'AES-KW', ...bogusWebCrypto);
+    return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek));
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/asn1.js b/internal/server/web/static/js/lib/jose/runtime/asn1.js
new file mode 100644
index 0000000000000000000000000000000000000000..a33a21690d8fae504e20ccd772cfd916c00690b1
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/asn1.js
@@ -0,0 +1,119 @@
+import {isCloudflareWorkers} from './env.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {encodeBase64} from './base64url.js';
+import formatPEM from '../lib/format_pem.js';
+import {JOSENotSupported} from '../util/errors.js';
+import {types} from './is_key_like.js';
+
+const genericExport = async (keyType, keyFormat, key) => {
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    if (!key.extractable) {
+        throw new TypeError('CryptoKey is not extractable');
+    }
+    if (key.type !== keyType) {
+        throw new TypeError(`key is not a ${keyType} key`);
+    }
+    return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
+};
+export const toSPKI = (key) => {
+    return genericExport('public', 'spki', key);
+};
+export const toPKCS8 = (key) => {
+    return genericExport('private', 'pkcs8', key);
+};
+const findOid = (keyData, oid, from = 0) => {
+    if (from === 0) {
+        oid.unshift(oid.length);
+        oid.unshift(0x06);
+    }
+    let i = keyData.indexOf(oid[0], from);
+    if (i === -1)
+        return false;
+    const sub = keyData.subarray(i, i + oid.length);
+    if (sub.length !== oid.length)
+        return false;
+    return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);
+};
+const getNamedCurve = (keyData) => {
+    switch (true) {
+        case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]):
+            return 'P-256';
+        case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]):
+            return 'P-384';
+        case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):
+            return 'P-521';
+        case isCloudflareWorkers() && findOid(keyData, [0x2b, 0x65, 0x70]):
+            return 'Ed25519';
+        default:
+            throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type');
+    }
+};
+const genericImport = async (replace, keyFormat, pem, alg, options) => {
+    var _a;
+    let algorithm;
+    let keyUsages;
+    const keyData = new Uint8Array(atob(pem.replace(replace, ''))
+        .split('')
+        .map((c) => c.charCodeAt(0)));
+    const isPublic = keyFormat === 'spki';
+    switch (alg) {
+        case 'PS256':
+        case 'PS384':
+        case 'PS512':
+            algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        case 'RS256':
+        case 'RS384':
+        case 'RS512':
+            algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512':
+            algorithm = {
+                name: 'RSA-OAEP',
+                hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
+            };
+            keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
+            break;
+        case 'ES256':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        case 'ES384':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        case 'ES512':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        case 'ECDH-ES':
+        case 'ECDH-ES+A128KW':
+        case 'ECDH-ES+A192KW':
+        case 'ECDH-ES+A256KW':
+            algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) };
+            keyUsages = isPublic ? [] : ['deriveBits'];
+            break;
+        case isCloudflareWorkers() && 'EdDSA':
+            const namedCurve = getNamedCurve(keyData).toUpperCase();
+            algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` };
+            keyUsages = isPublic ? ['verify'] : ['sign'];
+            break;
+        default:
+            throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value');
+    }
+    return crypto.subtle.importKey(keyFormat, keyData, algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages);
+};
+export const fromPKCS8 = (pem, alg, options) => {
+    return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, 'pkcs8', pem, alg, options);
+};
+export const fromSPKI = (pem, alg, options) => {
+    return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options);
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/base64url.js b/internal/server/web/static/js/lib/jose/runtime/base64url.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9eba39089441a4c7358ecd9660f88e924fe7513
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/base64url.js
@@ -0,0 +1,38 @@
+import {decoder, encoder} from '../lib/buffer_utils.js';
+
+export const encodeBase64 = (input) => {
+    let unencoded = input;
+    if (typeof unencoded === 'string') {
+        unencoded = encoder.encode(unencoded);
+    }
+    const CHUNK_SIZE = 0x8000;
+    const arr = [];
+    for (let i = 0; i < unencoded.length; i += CHUNK_SIZE) {
+        arr.push(String.fromCharCode.apply(null, unencoded.subarray(i, i + CHUNK_SIZE)));
+    }
+    return btoa(arr.join(''));
+};
+export const encode = (input) => {
+    return encodeBase64(input).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
+};
+export const decodeBase64 = (encoded) => {
+    const binary = atob(encoded);
+    const bytes = new Uint8Array(binary.length);
+    for (let i = 0; i < binary.length; i++) {
+        bytes[i] = binary.charCodeAt(i);
+    }
+    return bytes;
+};
+export const decode = (input) => {
+    let encoded = input;
+    if (encoded instanceof Uint8Array) {
+        encoded = decoder.decode(encoded);
+    }
+    encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
+    try {
+        return decodeBase64(encoded);
+    }
+    catch (_a) {
+        throw new TypeError('The input to be decoded is not correctly encoded.');
+    }
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/bogus.js b/internal/server/web/static/js/lib/jose/runtime/bogus.js
new file mode 100644
index 0000000000000000000000000000000000000000..8fde604ebfc8874cda1333ffa5074aeb734faf4e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/bogus.js
@@ -0,0 +1,6 @@
+const bogusWebCrypto = [
+    { hash: 'SHA-256', name: 'HMAC' },
+    true,
+    ['sign'],
+];
+export default bogusWebCrypto;
diff --git a/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js b/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf64b7f3300a45e0a8a71dd3894724ad29b358d6
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js
@@ -0,0 +1,8 @@
+import {JWEInvalid} from '../util/errors.js';
+
+const checkCekLength = (cek, expected) => {
+    if (cek.length << 3 !== expected) {
+        throw new JWEInvalid('Invalid Content Encryption Key length');
+    }
+};
+export default checkCekLength;
diff --git a/internal/server/web/static/js/lib/jose/runtime/check_key_length.js b/internal/server/web/static/js/lib/jose/runtime/check_key_length.js
new file mode 100644
index 0000000000000000000000000000000000000000..33970068fea16908676079c14afb6f1157a3aa85
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/check_key_length.js
@@ -0,0 +1,8 @@
+export default (alg, key) => {
+    if (alg.startsWith('RS') || alg.startsWith('PS')) {
+        const { modulusLength } = key.algorithm;
+        if (typeof modulusLength !== 'number' || modulusLength < 2048) {
+            throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
+        }
+    }
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/decrypt.js b/internal/server/web/static/js/lib/jose/runtime/decrypt.js
new file mode 100644
index 0000000000000000000000000000000000000000..404a2401592d044bd0a4a29bd0e45c873ef79a60
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/decrypt.js
@@ -0,0 +1,86 @@
+import {concat, uint64be} from '../lib/buffer_utils.js';
+import checkIvLength from '../lib/check_iv_length.js';
+import checkCekLength from './check_cek_length.js';
+import timingSafeEqual from './timing_safe_equal.js';
+import {JOSENotSupported, JWEDecryptionFailed} from '../util/errors.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
+    if (!(cek instanceof Uint8Array)) {
+        throw new TypeError(invalidKeyInput(cek, 'Uint8Array'));
+    }
+    const keySize = parseInt(enc.slice(1, 4), 10);
+    const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, ['decrypt']);
+    const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), {
+        hash: `SHA-${keySize << 1}`,
+        name: 'HMAC',
+    }, false, ['sign']);
+    const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3));
+    const expectedTag = new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3));
+    let macCheckPassed;
+    try {
+        macCheckPassed = timingSafeEqual(tag, expectedTag);
+    }
+    catch (_a) {
+    }
+    if (!macCheckPassed) {
+        throw new JWEDecryptionFailed();
+    }
+    let plaintext;
+    try {
+        plaintext = new Uint8Array(await crypto.subtle.decrypt({ iv, name: 'AES-CBC' }, encKey, ciphertext));
+    }
+    catch (_b) {
+    }
+    if (!plaintext) {
+        throw new JWEDecryptionFailed();
+    }
+    return plaintext;
+}
+async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
+    let encKey;
+    if (cek instanceof Uint8Array) {
+        encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['decrypt']);
+    }
+    else {
+        checkEncCryptoKey(cek, enc, 'decrypt');
+        encKey = cek;
+    }
+    try {
+        return new Uint8Array(await crypto.subtle.decrypt({
+            additionalData: aad,
+            iv,
+            name: 'AES-GCM',
+            tagLength: 128,
+        }, encKey, concat(ciphertext, tag)));
+    }
+    catch (_a) {
+        throw new JWEDecryptionFailed();
+    }
+}
+const decrypt = async (enc, cek, ciphertext, iv, tag, aad) => {
+    if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
+        throw new TypeError(invalidKeyInput(cek, ...types, 'Uint8Array'));
+    }
+    checkIvLength(enc, iv);
+    switch (enc) {
+        case 'A128CBC-HS256':
+        case 'A192CBC-HS384':
+        case 'A256CBC-HS512':
+            if (cek instanceof Uint8Array)
+                checkCekLength(cek, parseInt(enc.slice(-3), 10));
+            return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad);
+        case 'A128GCM':
+        case 'A192GCM':
+        case 'A256GCM':
+            if (cek instanceof Uint8Array)
+                checkCekLength(cek, parseInt(enc.slice(1, 4), 10));
+            return gcmDecrypt(enc, cek, ciphertext, iv, tag, aad);
+        default:
+            throw new JOSENotSupported('Unsupported JWE Content Encryption Algorithm');
+    }
+};
+export default decrypt;
diff --git a/internal/server/web/static/js/lib/jose/runtime/digest.js b/internal/server/web/static/js/lib/jose/runtime/digest.js
new file mode 100644
index 0000000000000000000000000000000000000000..505620bbb3e44aa499ee333f381188f3cabbf4d6
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/digest.js
@@ -0,0 +1,7 @@
+import crypto from './webcrypto.js';
+
+const digest = async (algorithm, data) => {
+    const subtleDigest = `SHA-${algorithm.slice(-3)}`;
+    return new Uint8Array(await crypto.subtle.digest(subtleDigest, data));
+};
+export default digest;
diff --git a/internal/server/web/static/js/lib/jose/runtime/ecdhes.js b/internal/server/web/static/js/lib/jose/runtime/ecdhes.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ad1b94401a1bb8ac7c7408c9bd91fbf713a1bcb
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/ecdhes.js
@@ -0,0 +1,34 @@
+import {concat, concatKdf, encoder, lengthAndInput, uint32be} from '../lib/buffer_utils.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+export async function deriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(0), apv = new Uint8Array(0)) {
+    if (!isCryptoKey(publicKey)) {
+        throw new TypeError(invalidKeyInput(publicKey, ...types));
+    }
+    checkEncCryptoKey(publicKey, 'ECDH');
+    if (!isCryptoKey(privateKey)) {
+        throw new TypeError(invalidKeyInput(privateKey, ...types));
+    }
+    checkEncCryptoKey(privateKey, 'ECDH', 'deriveBits');
+    const value = concat(lengthAndInput(encoder.encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength));
+    const sharedSecret = new Uint8Array(await crypto.subtle.deriveBits({
+        name: 'ECDH',
+        public: publicKey,
+    }, privateKey, Math.ceil(parseInt(privateKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3));
+    return concatKdf(sharedSecret, keyLength, value);
+}
+export async function generateEpk(key) {
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    return crypto.subtle.generateKey(key.algorithm, true, ['deriveBits']);
+}
+export function ecdhAllowed(key) {
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    return ['P-256', 'P-384', 'P-521'].includes(key.algorithm.namedCurve);
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/encrypt.js b/internal/server/web/static/js/lib/jose/runtime/encrypt.js
new file mode 100644
index 0000000000000000000000000000000000000000..4212092de67f6e034464a5cdf30f714b37bad519
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/encrypt.js
@@ -0,0 +1,69 @@
+import {concat, uint64be} from '../lib/buffer_utils.js';
+import checkIvLength from '../lib/check_iv_length.js';
+import checkCekLength from './check_cek_length.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {JOSENotSupported} from '../util/errors.js';
+import {types} from './is_key_like.js';
+
+async function cbcEncrypt(enc, plaintext, cek, iv, aad) {
+    if (!(cek instanceof Uint8Array)) {
+        throw new TypeError(invalidKeyInput(cek, 'Uint8Array'));
+    }
+    const keySize = parseInt(enc.slice(1, 4), 10);
+    const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, ['encrypt']);
+    const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), {
+        hash: `SHA-${keySize << 1}`,
+        name: 'HMAC',
+    }, false, ['sign']);
+    const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
+        iv,
+        name: 'AES-CBC',
+    }, encKey, plaintext));
+    const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3));
+    const tag = new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3));
+    return { ciphertext, tag };
+}
+async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
+    let encKey;
+    if (cek instanceof Uint8Array) {
+        encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
+    }
+    else {
+        checkEncCryptoKey(cek, enc, 'encrypt');
+        encKey = cek;
+    }
+    const encrypted = new Uint8Array(await crypto.subtle.encrypt({
+        additionalData: aad,
+        iv,
+        name: 'AES-GCM',
+        tagLength: 128,
+    }, encKey, plaintext));
+    const tag = encrypted.slice(-16);
+    const ciphertext = encrypted.slice(0, -16);
+    return { ciphertext, tag };
+}
+const encrypt = async (enc, plaintext, cek, iv, aad) => {
+    if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
+        throw new TypeError(invalidKeyInput(cek, ...types, 'Uint8Array'));
+    }
+    checkIvLength(enc, iv);
+    switch (enc) {
+        case 'A128CBC-HS256':
+        case 'A192CBC-HS384':
+        case 'A256CBC-HS512':
+            if (cek instanceof Uint8Array)
+                checkCekLength(cek, parseInt(enc.slice(-3), 10));
+            return cbcEncrypt(enc, plaintext, cek, iv, aad);
+        case 'A128GCM':
+        case 'A192GCM':
+        case 'A256GCM':
+            if (cek instanceof Uint8Array)
+                checkCekLength(cek, parseInt(enc.slice(1, 4), 10));
+            return gcmEncrypt(enc, plaintext, cek, iv, aad);
+        default:
+            throw new JOSENotSupported('Unsupported JWE Content Encryption Algorithm');
+    }
+};
+export default encrypt;
diff --git a/internal/server/web/static/js/lib/jose/runtime/env.js b/internal/server/web/static/js/lib/jose/runtime/env.js
new file mode 100644
index 0000000000000000000000000000000000000000..6751cbb2918385b42cf6eb0a0f7d20100088b6eb
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/env.js
@@ -0,0 +1,3 @@
+export function isCloudflareWorkers() {
+    return typeof WebSocketPair === 'function';
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js b/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f20b3877c79b26b58857a3259aac3a572e0e214
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js
@@ -0,0 +1,35 @@
+import {JOSEError, JWKSTimeout} from '../util/errors.js';
+
+const fetchJwks = async (url, timeout, options) => {
+    let controller;
+    let id;
+    let timedOut = false;
+    if (typeof AbortController === 'function') {
+        controller = new AbortController();
+        id = setTimeout(() => {
+            timedOut = true;
+            controller.abort();
+        }, timeout);
+    }
+    const response = await fetch(url.href, {
+        signal: controller ? controller.signal : undefined,
+        redirect: 'manual',
+        headers: options.headers,
+    }).catch((err) => {
+        if (timedOut)
+            throw new JWKSTimeout();
+        throw err;
+    });
+    if (id !== undefined)
+        clearTimeout(id);
+    if (response.status !== 200) {
+        throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response');
+    }
+    try {
+        return await response.json();
+    }
+    catch (_a) {
+        throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON');
+    }
+};
+export default fetchJwks;
diff --git a/internal/server/web/static/js/lib/jose/runtime/generate.js b/internal/server/web/static/js/lib/jose/runtime/generate.js
new file mode 100644
index 0000000000000000000000000000000000000000..932bfda8a2d99f585dd4073cfd1fc38fdc6a8dad
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/generate.js
@@ -0,0 +1,127 @@
+import {isCloudflareWorkers} from './env.js';
+import crypto from './webcrypto.js';
+import {JOSENotSupported} from '../util/errors.js';
+import random from './random.js';
+
+export async function generateSecret(alg, options) {
+    var _a;
+    let length;
+    let algorithm;
+    let keyUsages;
+    switch (alg) {
+        case 'HS256':
+        case 'HS384':
+        case 'HS512':
+            length = parseInt(alg.slice(-3), 10);
+            algorithm = { name: 'HMAC', hash: `SHA-${length}`, length };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case 'A128CBC-HS256':
+        case 'A192CBC-HS384':
+        case 'A256CBC-HS512':
+            length = parseInt(alg.slice(-3), 10);
+            return random(new Uint8Array(length >> 3));
+        case 'A128KW':
+        case 'A192KW':
+        case 'A256KW':
+            length = parseInt(alg.slice(1, 4), 10);
+            algorithm = { name: 'AES-KW', length };
+            keyUsages = ['wrapKey', 'unwrapKey'];
+            break;
+        case 'A128GCMKW':
+        case 'A192GCMKW':
+        case 'A256GCMKW':
+        case 'A128GCM':
+        case 'A192GCM':
+        case 'A256GCM':
+            length = parseInt(alg.slice(1, 4), 10);
+            algorithm = { name: 'AES-GCM', length };
+            keyUsages = ['encrypt', 'decrypt'];
+            break;
+        default:
+            throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+    }
+    return crypto.subtle.generateKey(algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages);
+}
+function getModulusLengthOption(options) {
+    var _a;
+    const modulusLength = (_a = options === null || options === void 0 ? void 0 : options.modulusLength) !== null && _a !== void 0 ? _a : 2048;
+    if (typeof modulusLength !== 'number' || modulusLength < 2048) {
+        throw new JOSENotSupported('Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used');
+    }
+    return modulusLength;
+}
+export async function generateKeyPair(alg, options) {
+    var _a, _b;
+    let algorithm;
+    let keyUsages;
+    switch (alg) {
+        case 'PS256':
+        case 'PS384':
+        case 'PS512':
+            algorithm = {
+                name: 'RSA-PSS',
+                hash: `SHA-${alg.slice(-3)}`,
+                publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+                modulusLength: getModulusLengthOption(options),
+            };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case 'RS256':
+        case 'RS384':
+        case 'RS512':
+            algorithm = {
+                name: 'RSASSA-PKCS1-v1_5',
+                hash: `SHA-${alg.slice(-3)}`,
+                publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+                modulusLength: getModulusLengthOption(options),
+            };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512':
+            algorithm = {
+                name: 'RSA-OAEP',
+                hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
+                publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+                modulusLength: getModulusLengthOption(options),
+            };
+            keyUsages = ['decrypt', 'unwrapKey', 'encrypt', 'wrapKey'];
+            break;
+        case 'ES256':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case 'ES384':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case 'ES512':
+            algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
+            keyUsages = ['sign', 'verify'];
+            break;
+        case isCloudflareWorkers() && 'EdDSA':
+            switch (options === null || options === void 0 ? void 0 : options.crv) {
+                case undefined:
+                case 'Ed25519':
+                    algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' };
+                    keyUsages = ['sign', 'verify'];
+                    break;
+                default:
+                    throw new JOSENotSupported('Invalid or unsupported crv option provided');
+            }
+            break;
+        case 'ECDH-ES':
+        case 'ECDH-ES+A128KW':
+        case 'ECDH-ES+A192KW':
+        case 'ECDH-ES+A256KW':
+            algorithm = { name: 'ECDH', namedCurve: (_a = options === null || options === void 0 ? void 0 : options.crv) !== null && _a !== void 0 ? _a : 'P-256' };
+            keyUsages = ['deriveKey', 'deriveBits'];
+            break;
+        default:
+            throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+    }
+    return (crypto.subtle.generateKey(algorithm, (_b = options === null || options === void 0 ? void 0 : options.extractable) !== null && _b !== void 0 ? _b : false, keyUsages));
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js b/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js
new file mode 100644
index 0000000000000000000000000000000000000000..197904dbfd2492b086fe7e6e9977aa1387ebd4bd
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js
@@ -0,0 +1,18 @@
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkSigCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+export default function getCryptoKey(alg, key, usage) {
+    if (isCryptoKey(key)) {
+        checkSigCryptoKey(key, alg, usage);
+        return key;
+    }
+    if (key instanceof Uint8Array) {
+        if (!alg.startsWith('HS')) {
+            throw new TypeError(invalidKeyInput(key, ...types));
+        }
+        return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]);
+    }
+    throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/is_key_like.js b/internal/server/web/static/js/lib/jose/runtime/is_key_like.js
new file mode 100644
index 0000000000000000000000000000000000000000..87192e9a37ba27843f90005e5496f8acc551d91f
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/is_key_like.js
@@ -0,0 +1,6 @@
+import {isCryptoKey} from './webcrypto.js';
+
+export default (key) => {
+    return isCryptoKey(key);
+};
+export const types = ['CryptoKey'];
diff --git a/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js b/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js
new file mode 100644
index 0000000000000000000000000000000000000000..626f4840e3b0109163d2ac68b7ef3b5fb81d0c0f
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js
@@ -0,0 +1,136 @@
+import {isCloudflareWorkers} from './env.js';
+import crypto from './webcrypto.js';
+import {JOSENotSupported} from '../util/errors.js';
+import {decode as base64url} from './base64url.js';
+
+function subtleMapping(jwk) {
+    let algorithm;
+    let keyUsages;
+    switch (jwk.kty) {
+        case 'oct': {
+            switch (jwk.alg) {
+                case 'HS256':
+                case 'HS384':
+                case 'HS512':
+                    algorithm = { name: 'HMAC', hash: `SHA-${jwk.alg.slice(-3)}` };
+                    keyUsages = ['sign', 'verify'];
+                    break;
+                case 'A128CBC-HS256':
+                case 'A192CBC-HS384':
+                case 'A256CBC-HS512':
+                    throw new JOSENotSupported(`${jwk.alg} keys cannot be imported as CryptoKey instances`);
+                case 'A128GCM':
+                case 'A192GCM':
+                case 'A256GCM':
+                case 'A128GCMKW':
+                case 'A192GCMKW':
+                case 'A256GCMKW':
+                    algorithm = { name: 'AES-GCM' };
+                    keyUsages = ['encrypt', 'decrypt'];
+                    break;
+                case 'A128KW':
+                case 'A192KW':
+                case 'A256KW':
+                    algorithm = { name: 'AES-KW' };
+                    keyUsages = ['wrapKey', 'unwrapKey'];
+                    break;
+                case 'PBES2-HS256+A128KW':
+                case 'PBES2-HS384+A192KW':
+                case 'PBES2-HS512+A256KW':
+                    algorithm = { name: 'PBKDF2' };
+                    keyUsages = ['deriveBits'];
+                    break;
+                default:
+                    throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+            }
+            break;
+        }
+        case 'RSA': {
+            switch (jwk.alg) {
+                case 'PS256':
+                case 'PS384':
+                case 'PS512':
+                    algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                case 'RS256':
+                case 'RS384':
+                case 'RS512':
+                    algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                case 'RSA-OAEP':
+                case 'RSA-OAEP-256':
+                case 'RSA-OAEP-384':
+                case 'RSA-OAEP-512':
+                    algorithm = {
+                        name: 'RSA-OAEP',
+                        hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`,
+                    };
+                    keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey'];
+                    break;
+                default:
+                    throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+            }
+            break;
+        }
+        case 'EC': {
+            switch (jwk.alg) {
+                case 'ES256':
+                    algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                case 'ES384':
+                    algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                case 'ES512':
+                    algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                case 'ECDH-ES':
+                case 'ECDH-ES+A128KW':
+                case 'ECDH-ES+A192KW':
+                case 'ECDH-ES+A256KW':
+                    algorithm = { name: 'ECDH', namedCurve: jwk.crv };
+                    keyUsages = jwk.d ? ['deriveBits'] : [];
+                    break;
+                default:
+                    throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+            }
+            break;
+        }
+        case isCloudflareWorkers() && 'OKP':
+            if (jwk.alg !== 'EdDSA') {
+                throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
+            }
+            switch (jwk.crv) {
+                case 'Ed25519':
+                    algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' };
+                    keyUsages = jwk.d ? ['sign'] : ['verify'];
+                    break;
+                default:
+                    throw new JOSENotSupported('Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value');
+            }
+            break;
+        default:
+            throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value');
+    }
+    return { algorithm, keyUsages };
+}
+const parse = async (jwk) => {
+    var _a, _b;
+    const { algorithm, keyUsages } = subtleMapping(jwk);
+    const rest = [
+        algorithm,
+        (_a = jwk.ext) !== null && _a !== void 0 ? _a : false,
+        (_b = jwk.key_ops) !== null && _b !== void 0 ? _b : keyUsages,
+    ];
+    if (algorithm.name === 'PBKDF2') {
+        return crypto.subtle.importKey('raw', base64url(jwk.k), ...rest);
+    }
+    const keyData = { ...jwk };
+    delete keyData.alg;
+    return crypto.subtle.importKey('jwk', keyData, ...rest);
+};
+export default parse;
diff --git a/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js b/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js
new file mode 100644
index 0000000000000000000000000000000000000000..549fd13b5efd03f8df6899e49b7c58d7f39446f0
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js
@@ -0,0 +1,22 @@
+import crypto, {isCryptoKey} from './webcrypto.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {encode as base64url} from './base64url.js';
+import {types} from './is_key_like.js';
+
+const keyToJWK = async (key) => {
+    if (key instanceof Uint8Array) {
+        return {
+            kty: 'oct',
+            k: base64url(key),
+        };
+    }
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));
+    }
+    if (!key.extractable) {
+        throw new TypeError('non-extractable CryptoKey cannot be exported as a JWK');
+    }
+    const { ext, key_ops, alg, use, ...jwk } = await crypto.subtle.exportKey('jwk', key);
+    return jwk;
+};
+export default keyToJWK;
diff --git a/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js b/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js
new file mode 100644
index 0000000000000000000000000000000000000000..e05d40b2f3e65c6f8dcd6d74d7cd090b79ac0e8c
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js
@@ -0,0 +1,52 @@
+import random from './random.js';
+import {p2s as concatSalt} from '../lib/buffer_utils.js';
+import {encode as base64url} from './base64url.js';
+import {unwrap, wrap} from './aeskw.js';
+import checkP2s from '../lib/check_p2s.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+function getCryptoKey(key, alg) {
+    if (key instanceof Uint8Array) {
+        return crypto.subtle.importKey('raw', key, 'PBKDF2', false, ['deriveBits']);
+    }
+    if (isCryptoKey(key)) {
+        checkEncCryptoKey(key, alg, 'deriveBits', 'deriveKey');
+        return key;
+    }
+    throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array'));
+}
+async function deriveKey(p2s, alg, p2c, key) {
+    checkP2s(p2s);
+    const salt = concatSalt(alg, p2s);
+    const keylen = parseInt(alg.slice(13, 16), 10);
+    const subtleAlg = {
+        hash: `SHA-${alg.slice(8, 11)}`,
+        iterations: p2c,
+        name: 'PBKDF2',
+        salt,
+    };
+    const wrapAlg = {
+        length: keylen,
+        name: 'AES-KW',
+    };
+    const cryptoKey = await getCryptoKey(key, alg);
+    if (cryptoKey.usages.includes('deriveBits')) {
+        return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen));
+    }
+    if (cryptoKey.usages.includes('deriveKey')) {
+        return crypto.subtle.deriveKey(subtleAlg, cryptoKey, wrapAlg, false, ['wrapKey', 'unwrapKey']);
+    }
+    throw new TypeError('PBKDF2 key "usages" must include "deriveBits" or "deriveKey"');
+}
+export const encrypt = async (alg, key, cek, p2c = 2048, p2s = random(new Uint8Array(16))) => {
+    const derived = await deriveKey(p2s, alg, p2c, key);
+    const encryptedKey = await wrap(alg.slice(-6), derived, cek);
+    return { encryptedKey, p2c, p2s: base64url(p2s) };
+};
+export const decrypt = async (alg, key, encryptedKey, p2c, p2s) => {
+    const derived = await deriveKey(p2s, alg, p2c, key);
+    return unwrap(alg.slice(-6), derived, encryptedKey);
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/random.js b/internal/server/web/static/js/lib/jose/runtime/random.js
new file mode 100644
index 0000000000000000000000000000000000000000..7082da0031bd4e5e0fcbe022ff7ab74bb501c657
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/random.js
@@ -0,0 +1,3 @@
+import crypto from './webcrypto.js';
+
+export default crypto.getRandomValues.bind(crypto);
diff --git a/internal/server/web/static/js/lib/jose/runtime/rsaes.js b/internal/server/web/static/js/lib/jose/runtime/rsaes.js
new file mode 100644
index 0000000000000000000000000000000000000000..a81f7af83df76166dd8f337e5685d297e96dd9ba
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/rsaes.js
@@ -0,0 +1,38 @@
+import subtleAlgorithm from './subtle_rsaes.js';
+import bogusWebCrypto from './bogus.js';
+import crypto, {isCryptoKey} from './webcrypto.js';
+import {checkEncCryptoKey} from '../lib/crypto_key.js';
+import checkKeyLength from './check_key_length.js';
+import invalidKeyInput from '../lib/invalid_key_input.js';
+import {types} from './is_key_like.js';
+
+export const encrypt = async (alg, key, cek) => {
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    checkEncCryptoKey(key, alg, 'encrypt', 'wrapKey');
+    checkKeyLength(alg, key);
+    if (key.usages.includes('encrypt')) {
+        return new Uint8Array(await crypto.subtle.encrypt(subtleAlgorithm(alg), key, cek));
+    }
+    if (key.usages.includes('wrapKey')) {
+        const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, ...bogusWebCrypto);
+        return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, key, subtleAlgorithm(alg)));
+    }
+    throw new TypeError('RSA-OAEP key "usages" must include "encrypt" or "wrapKey" for this operation');
+};
+export const decrypt = async (alg, key, encryptedKey) => {
+    if (!isCryptoKey(key)) {
+        throw new TypeError(invalidKeyInput(key, ...types));
+    }
+    checkEncCryptoKey(key, alg, 'decrypt', 'unwrapKey');
+    checkKeyLength(alg, key);
+    if (key.usages.includes('decrypt')) {
+        return new Uint8Array(await crypto.subtle.decrypt(subtleAlgorithm(alg), key, encryptedKey));
+    }
+    if (key.usages.includes('unwrapKey')) {
+        const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, key, subtleAlgorithm(alg), ...bogusWebCrypto);
+        return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek));
+    }
+    throw new TypeError('RSA-OAEP key "usages" must include "decrypt" or "unwrapKey" for this operation');
+};
diff --git a/internal/server/web/static/js/lib/jose/runtime/sign.js b/internal/server/web/static/js/lib/jose/runtime/sign.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ac57c36258ba2851da3f48380b6a0bfcb0fdfae
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/sign.js
@@ -0,0 +1,12 @@
+import subtleAlgorithm from './subtle_dsa.js';
+import crypto from './webcrypto.js';
+import checkKeyLength from './check_key_length.js';
+import getSignKey from './get_sign_verify_key.js';
+
+const sign = async (alg, key, data) => {
+    const cryptoKey = await getSignKey(alg, key, 'sign');
+    checkKeyLength(alg, cryptoKey);
+    const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
+    return new Uint8Array(signature);
+};
+export default sign;
diff --git a/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js b/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js
new file mode 100644
index 0000000000000000000000000000000000000000..14ee42e4103aa65f6bb97ec7cb8c40c6ca970c88
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js
@@ -0,0 +1,29 @@
+import {isCloudflareWorkers} from './env.js';
+import {JOSENotSupported} from '../util/errors.js';
+
+export default function subtleDsa(alg, algorithm) {
+    const hash = `SHA-${alg.slice(-3)}`;
+    switch (alg) {
+        case 'HS256':
+        case 'HS384':
+        case 'HS512':
+            return { hash, name: 'HMAC' };
+        case 'PS256':
+        case 'PS384':
+        case 'PS512':
+            return { hash, name: 'RSA-PSS', saltLength: alg.slice(-3) >> 3 };
+        case 'RS256':
+        case 'RS384':
+        case 'RS512':
+            return { hash, name: 'RSASSA-PKCS1-v1_5' };
+        case 'ES256':
+        case 'ES384':
+        case 'ES512':
+            return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve };
+        case isCloudflareWorkers() && 'EdDSA':
+            const { namedCurve } = algorithm;
+            return { name: namedCurve, namedCurve };
+        default:
+            throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js b/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbf870dde121cb373fc3c7ffcaa08db524665a34
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js
@@ -0,0 +1,13 @@
+import {JOSENotSupported} from '../util/errors.js';
+
+export default function subtleRsaEs(alg) {
+    switch (alg) {
+        case 'RSA-OAEP':
+        case 'RSA-OAEP-256':
+        case 'RSA-OAEP-384':
+        case 'RSA-OAEP-512':
+            return 'RSA-OAEP';
+        default:
+            throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js b/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js
new file mode 100644
index 0000000000000000000000000000000000000000..442cdccd20673435ff02530df7d8fb202d16b10e
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js
@@ -0,0 +1,19 @@
+const timingSafeEqual = (a, b) => {
+    if (!(a instanceof Uint8Array)) {
+        throw new TypeError('First argument must be a buffer');
+    }
+    if (!(b instanceof Uint8Array)) {
+        throw new TypeError('Second argument must be a buffer');
+    }
+    if (a.length !== b.length) {
+        throw new TypeError('Input buffers must have the same length');
+    }
+    const len = a.length;
+    let out = 0;
+    let i = -1;
+    while (++i < len) {
+        out |= a[i] ^ b[i];
+    }
+    return out === 0;
+};
+export default timingSafeEqual;
diff --git a/internal/server/web/static/js/lib/jose/runtime/verify.js b/internal/server/web/static/js/lib/jose/runtime/verify.js
new file mode 100644
index 0000000000000000000000000000000000000000..982340e52805d84c21a440fdb624099e37fcf878
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/verify.js
@@ -0,0 +1,17 @@
+import subtleAlgorithm from './subtle_dsa.js';
+import crypto from './webcrypto.js';
+import checkKeyLength from './check_key_length.js';
+import getVerifyKey from './get_sign_verify_key.js';
+
+const verify = async (alg, key, signature, data) => {
+    const cryptoKey = await getVerifyKey(alg, key, 'verify');
+    checkKeyLength(alg, cryptoKey);
+    const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
+    try {
+        return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
+    }
+    catch (_a) {
+        return false;
+    }
+};
+export default verify;
diff --git a/internal/server/web/static/js/lib/jose/runtime/webcrypto.js b/internal/server/web/static/js/lib/jose/runtime/webcrypto.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9e1e915390c4ebf7cf0581f4b183cae253ae402
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/webcrypto.js
@@ -0,0 +1,2 @@
+export default crypto;
+export const isCryptoKey = (key) => key instanceof CryptoKey;
diff --git a/internal/server/web/static/js/lib/jose/runtime/zlib.js b/internal/server/web/static/js/lib/jose/runtime/zlib.js
new file mode 100644
index 0000000000000000000000000000000000000000..d912bdb72f5db4ac08655af15657313cf98ab4be
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/runtime/zlib.js
@@ -0,0 +1,8 @@
+import {JOSENotSupported} from '../util/errors.js';
+
+export const inflate = async () => {
+    throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported by your javascript runtime. You need to use the `inflateRaw` decrypt option to provide Inflate Raw implementation.');
+};
+export const deflate = async () => {
+    throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported by your javascript runtime. You need to use the `deflateRaw` encrypt option to provide Deflate Raw implementation.');
+};
diff --git a/internal/server/web/static/js/lib/jose/util/base64url.js b/internal/server/web/static/js/lib/jose/util/base64url.js
new file mode 100644
index 0000000000000000000000000000000000000000..d28523e861ff89837a9e2d5937cd6258689d3e8c
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/util/base64url.js
@@ -0,0 +1,4 @@
+import * as base64url from '../runtime/base64url.js';
+
+export const encode = base64url.encode;
+export const decode = base64url.decode;
diff --git a/internal/server/web/static/js/lib/jose/util/decode_jwt.js b/internal/server/web/static/js/lib/jose/util/decode_jwt.js
new file mode 100644
index 0000000000000000000000000000000000000000..09741c36730a3dc65e49ee21c7ae988de00480d5
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/util/decode_jwt.js
@@ -0,0 +1,33 @@
+import {decode as base64url} from './base64url.js';
+import {decoder} from '../lib/buffer_utils.js';
+import isObject from '../lib/is_object.js';
+import {JWTInvalid} from './errors.js';
+
+export function decodeJwt(jwt) {
+    if (typeof jwt !== 'string')
+        throw new JWTInvalid('JWTs must use Compact JWS serialization, JWT must be a string');
+    const { 1: payload, length } = jwt.split('.');
+    if (length === 5)
+        throw new JWTInvalid('Only JWTs using Compact JWS serialization can be decoded');
+    if (length !== 3)
+        throw new JWTInvalid('Invalid JWT');
+    if (!payload)
+        throw new JWTInvalid('JWTs must contain a payload');
+    let decoded;
+    try {
+        decoded = base64url(payload);
+    }
+    catch (_a) {
+        throw new JWTInvalid('Failed to parse the base64url encoded payload');
+    }
+    let result;
+    try {
+        result = JSON.parse(decoder.decode(decoded));
+    }
+    catch (_b) {
+        throw new JWTInvalid('Failed to parse the decoded payload as JSON');
+    }
+    if (!isObject(result))
+        throw new JWTInvalid('Invalid JWT Claims Set');
+    return result;
+}
diff --git a/internal/server/web/static/js/lib/jose/util/decode_protected_header.js b/internal/server/web/static/js/lib/jose/util/decode_protected_header.js
new file mode 100644
index 0000000000000000000000000000000000000000..437d82098841aab167ab24dffbcce358e6a191c5
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/util/decode_protected_header.js
@@ -0,0 +1,35 @@
+import {decode as base64url} from './base64url.js';
+import {decoder} from '../lib/buffer_utils.js';
+import isObject from '../lib/is_object.js';
+
+export function decodeProtectedHeader(token) {
+    let protectedB64u;
+    if (typeof token === 'string') {
+        const parts = token.split('.');
+        if (parts.length === 3 || parts.length === 5) {
+            ;
+            [protectedB64u] = parts;
+        }
+    }
+    else if (typeof token === 'object' && token) {
+        if ('protected' in token) {
+            protectedB64u = token.protected;
+        }
+        else {
+            throw new TypeError('Token does not contain a Protected Header');
+        }
+    }
+    try {
+        if (typeof protectedB64u !== 'string' || !protectedB64u) {
+            throw new Error();
+        }
+        const result = JSON.parse(decoder.decode(base64url(protectedB64u)));
+        if (!isObject(result)) {
+            throw new Error();
+        }
+        return result;
+    }
+    catch (_a) {
+        throw new TypeError('Invalid Token or Protected Header formatting');
+    }
+}
diff --git a/internal/server/web/static/js/lib/jose/util/errors.js b/internal/server/web/static/js/lib/jose/util/errors.js
new file mode 100644
index 0000000000000000000000000000000000000000..777ce985b8f60980b67fb25a7f7d2324d6024b71
--- /dev/null
+++ b/internal/server/web/static/js/lib/jose/util/errors.js
@@ -0,0 +1,147 @@
+export class JOSEError extends Error {
+    constructor(message) {
+        var _a;
+        super(message);
+        this.code = 'ERR_JOSE_GENERIC';
+        this.name = this.constructor.name;
+        (_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, this, this.constructor);
+    }
+    static get code() {
+        return 'ERR_JOSE_GENERIC';
+    }
+}
+export class JWTClaimValidationFailed extends JOSEError {
+    constructor(message, claim = 'unspecified', reason = 'unspecified') {
+        super(message);
+        this.code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';
+        this.claim = claim;
+        this.reason = reason;
+    }
+    static get code() {
+        return 'ERR_JWT_CLAIM_VALIDATION_FAILED';
+    }
+}
+export class JWTExpired extends JOSEError {
+    constructor(message, claim = 'unspecified', reason = 'unspecified') {
+        super(message);
+        this.code = 'ERR_JWT_EXPIRED';
+        this.claim = claim;
+        this.reason = reason;
+    }
+    static get code() {
+        return 'ERR_JWT_EXPIRED';
+    }
+}
+export class JOSEAlgNotAllowed extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JOSE_ALG_NOT_ALLOWED';
+    }
+    static get code() {
+        return 'ERR_JOSE_ALG_NOT_ALLOWED';
+    }
+}
+export class JOSENotSupported extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JOSE_NOT_SUPPORTED';
+    }
+    static get code() {
+        return 'ERR_JOSE_NOT_SUPPORTED';
+    }
+}
+export class JWEDecryptionFailed extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWE_DECRYPTION_FAILED';
+        this.message = 'decryption operation failed';
+    }
+    static get code() {
+        return 'ERR_JWE_DECRYPTION_FAILED';
+    }
+}
+export class JWEInvalid extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWE_INVALID';
+    }
+    static get code() {
+        return 'ERR_JWE_INVALID';
+    }
+}
+export class JWSInvalid extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWS_INVALID';
+    }
+    static get code() {
+        return 'ERR_JWS_INVALID';
+    }
+}
+export class JWTInvalid extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWT_INVALID';
+    }
+    static get code() {
+        return 'ERR_JWT_INVALID';
+    }
+}
+export class JWKInvalid extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWK_INVALID';
+    }
+    static get code() {
+        return 'ERR_JWK_INVALID';
+    }
+}
+export class JWKSInvalid extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWKS_INVALID';
+    }
+    static get code() {
+        return 'ERR_JWKS_INVALID';
+    }
+}
+export class JWKSNoMatchingKey extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWKS_NO_MATCHING_KEY';
+        this.message = 'no applicable key found in the JSON Web Key Set';
+    }
+    static get code() {
+        return 'ERR_JWKS_NO_MATCHING_KEY';
+    }
+}
+export class JWKSMultipleMatchingKeys extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';
+        this.message = 'multiple matching keys found in the JSON Web Key Set';
+    }
+    static get code() {
+        return 'ERR_JWKS_MULTIPLE_MATCHING_KEYS';
+    }
+}
+export class JWKSTimeout extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWKS_TIMEOUT';
+        this.message = 'request timed out';
+    }
+    static get code() {
+        return 'ERR_JWKS_TIMEOUT';
+    }
+}
+export class JWSSignatureVerificationFailed extends JOSEError {
+    constructor() {
+        super(...arguments);
+        this.code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
+        this.message = 'signature verification failed';
+    }
+    static get code() {
+        return 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
+    }
+}
diff --git a/internal/server/web/static/js/logged-in-utils.js b/internal/server/web/static/js/logged-in-utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..c694f042007ab32637466ea1989b0093345e6971
--- /dev/null
+++ b/internal/server/web/static/js/logged-in-utils.js
@@ -0,0 +1,9 @@
+
+let clipboard = new ClipboardJS('.copier');
+clipboard.on('success', function (e) {
+    e.clearSelection();
+    let el = $(e.trigger);
+    let originalText = el.attr('data-original-title');
+    el.attr('data-original-title', 'Copied!').tooltip('show');
+    el.attr('data-original-title', originalText);
+});
diff --git a/internal/server/web/static/js/ssh.js b/internal/server/web/static/js/ssh.js
index b21218124049c5cce9c065ea3dbe0ccc3f110ce7..40c74635a41771b6606170370f00787e6d1a719b 100644
--- a/internal/server/web/static/js/ssh.js
+++ b/internal/server/web/static/js/ssh.js
@@ -19,16 +19,6 @@ const $hostConfigDiv = $('#sshHostConfigDiv');
 const $sshFollowInstructions = $('#follow-instructions');
 const $noSSHKeyEntry = $('#noSSHKeyEntry')
 
-
-let clipboard = new ClipboardJS('.btn-copy');
-clipboard.on('success', function (e) {
-    e.clearSelection();
-    let el = $(e.trigger);
-    let originalText = el.attr('data-original-title');
-    el.attr('data-original-title', 'Copied!').tooltip('show');
-    el.attr('data-original-title', originalText);
-});
-
 $('#addModal').on('hidden.bs.modal', function () {
     window.clearInterval(intervalID);
     $sshResult.hideB();
diff --git a/internal/server/web/static/js/tokeninfo-status.js b/internal/server/web/static/js/tokeninfo-status.js
new file mode 100644
index 0000000000000000000000000000000000000000..2c6b58b59abe0cfcb35ff1f1c93fcf06e325c7f8
--- /dev/null
+++ b/internal/server/web/static/js/tokeninfo-status.js
@@ -0,0 +1,123 @@
+import * as jose from './lib/jose/index.js';
+
+const $tokeninfoBadgeName = $('#tokeninfo-token-name');
+const $tokeninfoBadgeTypeShort = $('#tokeninfo-token-type-short');
+const $tokeninfoBadgeTypeJWTValid = $('#tokeninfo-token-type-JWT-valid');
+const $tokeninfoBadgeTypeJWTInvalid = $('#tokeninfo-token-type-JWT-invalid');
+const $tokeninfoBadgeValid = $('#tokeninfo-token-valid');
+const $tokeninfoBadgeInvalid = $('#tokeninfo-token-invalid');
+const $tokeninfoBadgeMytokenIss = $('#tokeninfo-token-mytoken-iss');
+const $tokeninfoBadgeOIDCIss = $('#tokeninfo-token-oidc-iss');
+const $tokeninfoBadgeIat = $('#tokeninfo-token-iat');
+const $tokeninfoBadgeExp = $('#tokeninfo-token-exp');
+const $tokeninfoBadgeIatDate = $('#tokeninfo-token-iat-date');
+const $tokeninfoBadgeExpDate = $('#tokeninfo-token-exp-date');
+
+const $tokeninfoTypeBadges = $('.tokeninfo-token-type');
+
+async function update_tokeninfo() {
+    let token = $tokenInput.val();
+    let payload = {};
+    let tokeninfoEndpoint = storageGet('tokeninfo_endpoint');
+    let jwksUri = storageGet('jwks_uri');
+    try {
+        payload =  jose.decodeJwt(token);
+        let mytokenIss = payload['iss'];
+        if (!mytokenIss.startsWith(window.location.href)) {
+     await       $.ajax({
+                type: "Get",
+                url: "/.well-known/mytoken-configuration",
+                success: function(res){
+                   tokeninfoEndpoint = res['tokeninfo_endpoint'];
+                   jwksUri = res['jwks_uri'];
+                },
+                error: function (errRes) {
+                    $errorModalMsg.text(getErrorMessage(errRes));
+                    $errorModal.modal();
+                }
+            });
+        }
+        $tokeninfoTypeBadges.hideB();
+
+        let jwks = await $.ajax({url: jwksUri, type:"GET"});
+        try {
+            const pubKey = await jose.importJWK(jwks['keys'][0]);
+            await jose.jwtVerify(token, pubKey);
+            $tokeninfoBadgeTypeJWTValid.showB();
+        } catch (e) {
+           if (e instanceof jose.errors.JWSSignatureVerificationFailed) {
+               $tokeninfoBadgeTypeJWTInvalid.showB();
+           } else {
+               throw e;
+           }
+        }
+
+    } catch (e) {
+        if (e instanceof jose.errors.JWTInvalid) {
+            $tokeninfoTypeBadges.hideB();
+            $tokeninfoBadgeTypeShort.showB();
+        } else {
+            throw e;
+        }
+    }
+    try {
+        await $.ajax({
+            type: "POST",
+            url: tokeninfoEndpoint,
+            data: JSON.stringify({
+                'action': 'introspect',
+                'mytoken': token,
+            }),
+            dataType: "json",
+            contentType: "application/json",
+            success: function (res) {
+                payload = res['token'];
+                if (res['valid']) {
+                    $tokeninfoBadgeValid.showB();
+                    $tokeninfoBadgeInvalid.hideB();
+                } else {
+                    $tokeninfoBadgeValid.hideB();
+                    $tokeninfoBadgeInvalid.showB();
+                }
+            },
+            error: function (errRes) {
+                $tokeninfoBadgeValid.hideB();
+                $tokeninfoBadgeInvalid.hideB();
+                if (errRes.responseJSON['error'] === 'insufficient_capabilities') {
+                    $tokeninfoBadgeValid.showB();
+                } else {
+                    $tokeninfoBadgeInvalid.showB();
+                }
+            }
+        });
+    } catch(e) {
+        console.log(e);
+    }
+
+    let oidcIss = payload['oidc_iss'];
+    let mytokenIss = payload['iss'];
+    let name = payload['name'];
+    $tokeninfoBadgeOIDCIss.text(oidcIss!==undefined?oidcIss:"");
+    $tokeninfoBadgeMytokenIss.text(mytokenIss!==undefined?mytokenIss:"");
+    $tokeninfoBadgeName.text(name!==undefined?name:"");
+    let exp = payload['exp'];
+    if (exp===undefined) {
+        $tokeninfoBadgeExp.hideB();
+    } else {
+        $tokeninfoBadgeExpDate.text(formatTime(exp));
+        $tokeninfoBadgeExp.showB();
+    }
+    let iat = payload['iat'];
+    if (iat===undefined) {
+        $tokeninfoBadgeIat.hideB();
+    } else {
+        $tokeninfoBadgeIatDate.text(formatTime(iat));
+        $tokeninfoBadgeIat.showB();
+    }
+}
+
+$tokenInput.on('change', update_tokeninfo);
+
+$(function (){
+    update_tokeninfo();
+});
\ No newline at end of file
diff --git a/internal/server/web/static/js/tokeninfo.js b/internal/server/web/static/js/tokeninfo.js
index 2e56518a84c32e942fa94f065f8c9cf58a30d861..7b86acafe7d2d9ff126b7417b62f4b1eaf67f0c3 100644
--- a/internal/server/web/static/js/tokeninfo.js
+++ b/internal/server/web/static/js/tokeninfo.js
@@ -1,3 +1,4 @@
+const $tokenInput = $('#tokeninfo-token');
 
 function _tokeninfo(action, successFnc, errorFnc, token=undefined) {
     let data = {
@@ -18,11 +19,10 @@ function _tokeninfo(action, successFnc, errorFnc, token=undefined) {
     });
 }
 
-
-function getSessionTokenInfo(e) {
+function getTokenInfo(e) {
     e.preventDefault();
-    let msg = $('#session-token-info-msg');
-    let copy = $('#session-copy');
+    let msg = $('#tokeninfo-token-content');
+    let copy = $('#info-copy');
     _tokeninfo('introspect',
         function (res) {
             let token = res['token'];
@@ -40,7 +40,7 @@ function getSessionTokenInfo(e) {
             msg.text(getErrorMessage(errRes));
             msg.addClass('text-danger');
             copy.removeClass('d-none');
-        })
+        }, $tokenInput.val())
     return false;
 }
 
@@ -53,7 +53,7 @@ function historyToHTML(events) {
     let tableEntries = [];
     events.forEach(function (event) {
         let comment = event['comment'] || '';
-        let time = new Date(event['time']*1000).toLocaleString();
+        let time = formatTime(event['time']);
         let agentIcons = userAgentToHTMLIcons(event['user_agent']);
         let entry = '<tr>' +
             '<td>'+event['event']+'</td>' +
@@ -86,7 +86,7 @@ function _tokenTreeToHTML(tree, depth) {
     if (depth > 0) {
         name = arrowI.repeat(depth-1) + lastArrowI + name
     }
-    let time = new Date(token['created']*1000).toLocaleString();
+    let time = formatTime(token['created']);
     let tableEntries = ['<tr>' +
         '<td>'+name+'</td>' +
         '<td>'+time+'</td>' +
@@ -131,7 +131,7 @@ function getHistoryTokenInfo(e) {
             msg.text(getErrorMessage(errRes));
             msg.addClass('text-danger');
             copy.removeClass('d-none');
-        })
+        }, $tokenInput.val())
     return false;
 }
 
@@ -149,7 +149,7 @@ function getTreeTokenInfo(e) {
             msg.text(getErrorMessage(errRes));
             msg.addClass('text-danger');
             copy.removeClass('d-none');
-        })
+        }, $tokenInput.val())
     return false;
 }
 
@@ -182,11 +182,12 @@ function getListTokenInfo(e) {
     return false;
 }
 
-$('#session-info-tab').on('shown.bs.tab', getSessionTokenInfo)
-$('#session-reload').on('click', getSessionTokenInfo)
+$('#info-tab').on('shown.bs.tab', getTokenInfo)
+$('#info-reload').on('click', getTokenInfo)
 $('#history-tab').on('shown.bs.tab', getHistoryTokenInfo)
 $('#history-reload').on('click', getHistoryTokenInfo)
 $('#tree-tab').on('shown.bs.tab', getTreeTokenInfo)
 $('#tree-reload').on('click', getTreeTokenInfo)
+
 $('#list-mts-tab').on('shown.bs.tab', getListTokenInfo)
-$('#list-reload').on('click', getListTokenInfo)
+$('#list-reload').on('click', getListTokenInfo)
\ No newline at end of file
diff --git a/internal/server/web/static/js/utils.js b/internal/server/web/static/js/utils.js
index 6825a5f8cfdb6a4aea6e844115a61a458af32587..de9ac37ee2e99f20f6164db9d88a86fcafcb89ca 100644
--- a/internal/server/web/static/js/utils.js
+++ b/internal/server/web/static/js/utils.js
@@ -96,4 +96,8 @@ function extractMaxScopesFromToken(token) {
        scopes.push(...s.split(' '));
     }
     return scopes.filter(onlyUnique).join(" ")
+}
+
+function formatTime(t) {
+    return new Date(t*1000).toLocaleString()
 }
\ No newline at end of file