about summary refs log tree commit diff
path: root/kittybox-rs/companion-lite
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/companion-lite')
-rw-r--r--kittybox-rs/companion-lite/index.html127
-rw-r--r--kittybox-rs/companion-lite/src/base64.ts89
-rw-r--r--kittybox-rs/companion-lite/src/indieauth.ts113
-rw-r--r--kittybox-rs/companion-lite/src/main.ts178
-rw-r--r--kittybox-rs/companion-lite/src/micropub_api.ts125
-rw-r--r--kittybox-rs/companion-lite/style.css47
-rw-r--r--kittybox-rs/companion-lite/tsconfig.json104
7 files changed, 0 insertions, 783 deletions
diff --git a/kittybox-rs/companion-lite/index.html b/kittybox-rs/companion-lite/index.html
deleted file mode 100644
index fc99c60..0000000
--- a/kittybox-rs/companion-lite/index.html
+++ /dev/null
@@ -1,127 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <meta charset="utf-8">
-        <title>Kittybox-Micropub debug client</title>
-        <link rel="stylesheet" href="./style.css">
-        <script type="module" src="./main.js"></script>
-        <link rel="jslicense" href="#jslicense">
-    </head>
-    <body>
-      <noscript>
-        <h1 class="header">Kittybox Companion (Lite)</h1>
-        <p>I'm sorry, Kittybox Companion requires JavaScript to work.</p>
-
-        <p>This is a requirement due to multiple interactive features present in Kittybox, such as support for multiple-entry form fields, interactive login sequence and more.</p>
-
-        <p>However, the Micropub standard is extremely flexible, and if you happen to have a token, you can publish articles, notes, likes, follows and more by sending requests directly to the Micropub endpoint.</p>
-
-        <p><a href="https://micropub.spec.indieweb.org/">The Micropub spec is defined here.</a> Good luck!</p>
-      </noscript>
-
-      <div class="view" id="unauthorized" style="display:none">
-        <form action="#" id="indieauth">
-          <label for="me">Your website URL:</label>
-          <input id="me" name="me" type="url">
-          <input type="submit">
-        </form>
-      </div>
-
-      <div class="view" id="authorizing" style="display:none">
-        <p>Performing the authorization dance...</p>
-      </div>
-
-      <div class="view" id="authorized" style="display:none">
-        <form action="/.kittybox/micropub" method="POST" id="micropub">
-          <fieldset>
-            <legend>Post details:</legend>
-            <section>
-              <label for="name">Name (leave blank for an unnamed post):</label>
-              <input id="name" type="text">
-            </section>
-            <section>
-              <label for="content">Content:</label>
-              <textarea id="content" placeholder="Your post's text goes here"></textarea>
-            </section>
-            <section>
-              <label for="category">Categories (separeted by commas):</label>
-              <input id="category" type="text">
-            </section>
-            <fieldset>
-              <legend>Channels</legend>
-              <section>
-                <input type="radio" id="no_channel" name="channel_select" checked value="">
-                <label for="no_channel">Default channel only</label>
-              </section>
-
-              <section>
-                <input type="radio" id="select_channels" name="channel_select" value="on">
-                <label for="select_channels">Select channels manually</label>
-              </section>
-              
-              <fieldset id="channels" style="display: none">
-                <legend>Available channels:</legend>
-                <template id="channel_selector">
-                  <section>
-                    <input type="checkbox" name="channel" id="" value="">
-                    <label for=""></label>
-                  </section>
-                </template>
-                <div id="channels_target"></div>
-              </fieldset>
-            </fieldset>
-          </fieldset>
-          <input type="submit">
-        </form>
-      </div>
-
-      <details id="jslicense">
-        <summary>JavaScript licensing information for this software</summary>
-        <table id="jslicense-labels1">
-          <tr>
-            <td><a href="/.kittybox/micropub/client/main.js">main.js</a></td>
-            <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a></td>
-            <td><a href="https://git.sr.ht/~vikanezrimaya/kittybox/tree/main/item/kittybox-rs/companion-lite/src/main.ts">main.ts (Kittybox source code)</a></td>
-          </tr>
-          <tr>
-            <td><a href="/.kittybox/micropub/client/micropub_api.js">micropub_api.js</a></td>
-            <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a></td>
-            <td><a href="https://git.sr.ht/~vikanezrimaya/kittybox/tree/main/item/kittybox-rs/companion-lite/src/micropub_api.ts">micropub_api.ts (Kittybox source code)</a></td>
-          </tr>
-          <tr>
-            <td><a href="/.kittybox/micropub/client/indieauth.js">indieauth.js</a></td>
-            <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a></td>
-            <td><a href="https://git.sr.ht/~vikanezrimaya/kittybox/tree/main/item/kittybox-rs/companion-lite/src/indieauth.ts">indieauth.ts (Kittybox source code)</a></td>
-          </tr>
-          <tr>
-            <td><a href="/.kittybox/micropub/client/base64.js">base64.js</a></td>
-            <td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0 (Public Domain)</a></td>
-            <td><a href="https://git.sr.ht/~vikanezrimaya/kittybox/tree/main/item/kittybox-rs/companion-lite/src/base64.ts">base64.ts</a>, adapted from <a href="https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_%E2%80%93_rewriting_atob_and_btoa_using_typedarrays_and_utf-8">MDN page on Base64</a></td>
-          </tr>
-          <tr>
-            <td><a href="https://esm.sh/microformats-parser@1.4.1?pin=v96">ESM.sh entrypoint for microformats-parser</a></td>
-            <td><a href="http://www.jclark.com/xml/copying.txt">MIT/Expat</a></td>
-            <td><a href="https://github.com/ije/esm.sh/blob/main/server/handler.go">esm.sh source code</a>
-          </tr>
-          <tr>
-            <td><a href="https://esm.sh/v96/microformats-parser@1.4.1/es2022/microformats-parser.js">microformats-parser</a></td>
-            <td><a href="http://www.jclark.com/xml/copying.txt">MIT/Expat</a></td>
-            <td><a href="https://github.com/microformats/microformats-parser">GitHub repository</a>
-          </tr>
-          <tr>
-            <td><a href="https://esm.sh/v96/parse5@6.0.1/es2022/parse5.js">parse5</a></td>
-            <td>
-              <a href="http://www.jclark.com/xml/copying.txt">MIT/Expat</a>
-              <br>
-              <a href="http://www.freebsd.org/copyright/freebsd-license.html">BSD-2-Clause</a>
-            </td>
-            <td>
-              <a href="https://github.com/inikulin/parse5">GitHub repository for parse5</a>
-              <br>
-              <a href="https://github.com/fb55/entities">GitHub repository for entitites</a>, a parse5 dependency
-            </td>
-          </tr>
-        </table>
-      </details>
-    </body>
-</html>
diff --git a/kittybox-rs/companion-lite/src/base64.ts b/kittybox-rs/companion-lite/src/base64.ts
deleted file mode 100644
index 2429894..0000000
--- a/kittybox-rs/companion-lite/src/base64.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-// Array of bytes to Base64 string decoding
-function b64ToUint6(nChr: number) {
-  return nChr > 64 && nChr < 91
-    ? nChr - 65
-    : nChr > 96 && nChr < 123
-    ? nChr - 71
-    : nChr > 47 && nChr < 58
-    ? nChr + 4
-    : nChr === 43
-    ? 62
-    : nChr === 47
-    ? 63
-    : 0;
-}
-
-export function decode(sBase64: string, nBlocksSize?: number) {
-  const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
-  const nInLen = sB64Enc.length;
-  const nOutLen = nBlocksSize
-    ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
-    : (nInLen * 3 + 1) >> 2;
-  const taBytes = new Uint8Array(nOutLen);
-
-  let nMod3;
-  let nMod4;
-  let nUint24 = 0;
-  let nOutIdx = 0;
-  for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
-    nMod4 = nInIdx & 3;
-    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
-    if (nMod4 === 3 || nInLen - nInIdx === 1) {
-      nMod3 = 0;
-      while (nMod3 < 3 && nOutIdx < nOutLen) {
-        taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
-        nMod3++;
-        nOutIdx++;
-      }
-      nUint24 = 0;
-    }
-  }
-
-  return taBytes;
-}
-
-/* Base64 string to array encoding */
-function uint6ToB64(nUint6: number) {
-  return nUint6 < 26
-    ? nUint6 + 65
-    : nUint6 < 52
-    ? nUint6 + 71
-    : nUint6 < 62
-    ? nUint6 - 4
-    : nUint6 === 62
-    ? 43
-    : nUint6 === 63
-    ? 47
-    : 65;
-}
-
-export function encode(aBytes: Uint8Array) {
-  let nMod3 = 2;
-  let sB64Enc = "";
-
-  const nLen = aBytes.length;
-  let nUint24 = 0;
-  for (let nIdx = 0; nIdx < nLen; nIdx++) {
-    nMod3 = nIdx % 3;
-    if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
-      sB64Enc += "\r\n";
-    }
-
-    nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
-    if (nMod3 === 2 || aBytes.length - nIdx === 1) {
-      sB64Enc += String.fromCodePoint(
-        uint6ToB64((nUint24 >>> 18) & 63),
-        uint6ToB64((nUint24 >>> 12) & 63),
-        uint6ToB64((nUint24 >>> 6) & 63),
-        uint6ToB64(nUint24 & 63)
-      );
-      nUint24 = 0;
-    }
-  }
-  return (
-    sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) +
-      (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
-  );
-}
-
-export default { encode, decode }
diff --git a/kittybox-rs/companion-lite/src/indieauth.ts b/kittybox-rs/companion-lite/src/indieauth.ts
deleted file mode 100644
index 40facab..0000000
--- a/kittybox-rs/companion-lite/src/indieauth.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-// @ts-ignore
-import { mf2 } from "https://esm.sh/microformats-parser@1.4.1?pin=v96"
-import { MF2 } from "./micropub_api.js"
-import base64 from "./base64.js"
-  /*
-  const { mf2 }: {
-    mf2: (html: string, options: {
-      baseUrl: string,
-      experimental?: { lang?: boolean, textContent?: boolean }
-    }) => {
-      items: MF2[],
-      rels: {[key: string]: string[]},
-      "rel-urls": {[key: string]: { rels: string[], text?: string }}
-    }
-  } =
-    // @ts-ignore
-    await import("https://esm.sh/microformats-parser@1.4.1?pin=v96");
-  */
-
-interface IndieauthMetadata {
-  authorization_endpoint: string,
-  token_endpoint: string,
-  issuer: string,
-  introspection_endpoint?: string,
-  introspection_endpoint_auth_methods_supported?: ("Bearer")[],
-  revocation_endpoint?: string,
-  revocation_endpoint_auth_methods_supported?: ["none"],
-  scopes_supported?: string[],
-  response_types_supported: ["code"],
-  grant_types_supported: ("authorization_code" | "refresh_token")[]
-  code_challenge_methods_supported: ("S256")[]
-  authorization_response_iss_parameter_supported: true,
-  userinfo_endpoint?: string
-}
-
-interface MF2ParsedData {
-  items: MF2[],
-  rels: {[key: string]: string[]},
-  "rel-urls": {[key: string]: { rels: string[], text?: string }}
-}
-
-export interface IndiewebEndpoints {
-  authorization_endpoint: URL,
-  token_endpoint: URL,
-  userinfo_endpoint: URL | null,
-  revocation_endpoint: URL | null,
-  micropub: URL,
-  
-}
-
-export function create_verifier() {
-  const array = new Uint8Array(64)
-  crypto.getRandomValues(array)
-
-  return array.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
-}
-
-export async function create_challenge(verifier: string): Promise<string> {
-  return await crypto.subtle.digest('SHA-256', Uint8Array.from(verifier, c => c.charCodeAt(0)))
-    .then((buf) => base64.encode(new Uint8Array(buf)))
-    .then(s => {
-      return s
-        .replaceAll("+", "-")
-        .replaceAll("/", "_")
-        .replaceAll(/=$/g, "")
-    })
-}
-
-export async function discover_endpoints(me: URL): Promise<IndiewebEndpoints | null> {
-  const response = await fetch(me);
-  const data: MF2ParsedData = mf2(await response.text(), { baseUrl: me.toString() });
-  let endpoints: Partial<IndiewebEndpoints> = {};
-  if ("micropub" in data.rels) {
-    endpoints.micropub = new URL(data.rels.micropub[0])
-  } else {
-    return null
-  }
-  if ("indieauth_metadata" in data.rels) {
-    const metadata_response = await fetch(data.rels.indieauth_metadata[0], {
-      headers: {
-        "Accept": "application/json"
-      }
-    });
-
-    const metadata = await metadata_response.json() as IndieauthMetadata;
-    endpoints.authorization_endpoint = new URL(metadata.authorization_endpoint)
-    endpoints.token_endpoint = new URL(metadata.token_endpoint)
-    if (metadata.userinfo_endpoint != null) {
-      endpoints.userinfo_endpoint = new URL(metadata.userinfo_endpoint)
-    } else {
-      endpoints.userinfo_endpoint = null
-    }
-    if (metadata.revocation_endpoint != null) {
-      endpoints.revocation_endpoint = new URL(metadata.revocation_endpoint)
-    } else {
-      endpoints.revocation_endpoint = null
-    }
-
-    return endpoints as IndiewebEndpoints
-  } else if (
-    "authorization_endpoint" in data.rels
-      && "token_endpoint" in data.rels
-  ) {
-    endpoints.authorization_endpoint = new URL(data.rels.authorization_endpoint[0])
-    endpoints.token_endpoint = new URL(data.rels.token_endpoint[0])
-    endpoints.userinfo_endpoint = null
-    endpoints.revocation_endpoint = null
-
-    return endpoints as IndiewebEndpoints
-  } else {
-    return null
-  }
-}
diff --git a/kittybox-rs/companion-lite/src/main.ts b/kittybox-rs/companion-lite/src/main.ts
deleted file mode 100644
index f45cb95..0000000
--- a/kittybox-rs/companion-lite/src/main.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import { Micropub, MicropubChannel, MF2 } from "./micropub_api.js";
-
-const channel_select_radio = document.getElementById("select_channels") as HTMLInputElement;
-channel_select_radio.onclick = async () => {
-  function populate_channel_list(channels: MicropubChannel[]) {
-    (document.getElementById("channels") as HTMLElement).style.display = "block";
-    const channel_list = document.getElementById("channels_target") as HTMLElement;
-    channel_list.innerHTML = "";
-    channels.forEach((channel) => {
-      const template = (document.getElementById("channel_selector") as HTMLTemplateElement).content.cloneNode(true) as HTMLElement;
-      const input = template.querySelector("input") as HTMLInputElement;
-      const label = template.querySelector("label") as HTMLLabelElement;
-      input.id = `channel_selector_option_${channel.uid}`
-      input.value = channel.uid
-      label.htmlFor = input.id
-      label.innerHTML = `<a href="${channel.uid}">${channel.name}</a>`
-
-      channel_list.appendChild(template)
-    })
-  }
-
-  if (micropub == null) {
-    throw new Error("need to authenticate first");
-  }
-  const config = await micropub.config();
-  if (config.channels !== undefined) {
-    populate_channel_list(config.channels)
-  }
-}
-
-const no_channel_radio = document.getElementById("no_channel") as HTMLInputElement;
-no_channel_radio.onclick = () => {
-  (document.getElementById("channels") as HTMLElement).style.display = "none";
-  const channel_list = document.getElementById("channels_target") as HTMLElement
-  channel_list.innerHTML = "";
-}
-
-const main_form = document.getElementById("micropub") as HTMLFormElement;
-main_form.onsubmit = async (event) => {
-  function construct_body(form: HTMLFormElement): MF2 {
-    let content = (form.elements.namedItem("content") as HTMLInputElement).value;
-    let name: string | undefined = (form.elements.namedItem("name") as HTMLInputElement).value || undefined;
-    let category: string[] = (form.elements.namedItem("category") as HTMLInputElement).value
-      .split(",")
-      .map(val => val.trim());
-
-    let channel: string[] | undefined = undefined;
-    let channel_select = (form.elements.namedItem("channel_select") as HTMLInputElement).value;
-    if (channel_select) {
-      let channel_selector = form.elements.namedItem("channel");
-      if (channel_selector instanceof RadioNodeList) {
-        channel = (Array.from(channel_selector) as HTMLInputElement[])
-          .map(i => i.checked ? i.value : false)
-          .filter(i => i) as string[];
-      } else if (channel_selector instanceof HTMLInputElement) {
-        channel = [channel_selector.value]
-      }
-    }
-    return {
-      type: ["h-entry"],
-      properties: {
-        content: [content],
-        name: name ? [name] : undefined,
-        category: category.length ? category : undefined,
-        channel: channel ? channel : undefined
-      }
-    }
-  }
-
-  event.preventDefault()
-  const mf2 = construct_body(main_form);
-  console.log(JSON.stringify(mf2));
-  if (micropub == null) {
-    throw new Error("need to authenticate first");
-  }
-  try {
-    const location = await micropub.submit(mf2);
-    main_form.clear()
-
-    window.open(location, "_blank")
-  } catch (e) {
-    console.error(e)
-    alert(`Error: ${e}`)
-    return
-  }
-
-}
-
-const indieauth_form = document.getElementById("indieauth") as HTMLFormElement;
-indieauth_form.onsubmit = async (event) => {
-  event.preventDefault()
-  const form = event.target as HTMLFormElement;
-  const me = (form.elements.namedItem("me") as HTMLInputElement).value;
-  if (me != null) {
-    const { discover_endpoints, create_verifier, create_challenge } = await import("./indieauth.js");
-
-    const endpoints = await discover_endpoints(new URL(me));
-
-    if (endpoints != null) {
-      localStorage.setItem("micropub_endpoint", endpoints.micropub.toString())
-      localStorage.setItem("token_endpoint", endpoints.token_endpoint.toString())
-      if (endpoints.revocation_endpoint != null) {
-        localStorage.setItem("revocation_endpoint", endpoints.revocation_endpoint.toString())
-      }
-    } else {
-      alert("Your website doesn't support Micropub.")
-      return
-    }
-    (document.getElementById("unauthorized") as HTMLElement).style.display = "none";
-    (document.getElementById("authorizing") as HTMLElement).style.display = "block";
-    const url = endpoints.authorization_endpoint;
-    let params = new URLSearchParams();
-    for (const [key, val] of url.searchParams) {
-      params.append(key, val)
-    }
-    params.set("client_id", window.location.href)
-    params.set("redirect_uri", window.location.href)
-    params.set("response_type", "code")
-    params.set("scope", "profile create media")
-    params.set("state", "awoo")
-    const code_verifier = create_verifier()
-    localStorage.setItem("code_verifier", code_verifier)
-    params.set("code_challenge", await create_challenge(code_verifier))
-    params.set("code_challenge_method", "S256")
-
-    url.search = "?" + params.toString()
-
-    console.log(url)
-
-    window.location.href = url.toString()
-  }
-}
-
-if (window.location.search != "") {
-  (document.getElementById("authorizing") as HTMLElement).style.display = "block";
-  const params = new URLSearchParams(window.location.search)
-  if (params.has("code") && params.has("state")) {
-    const token_endpoint = new URL(localStorage.getItem("token_endpoint")!)
-    const state = params.get("state")
-    // XXX check state
-
-    const client_id = new URL(window.location.href);
-    client_id.search = "";
-    const form = new URLSearchParams();
-    form.set("grant_type", "authorization_code")
-    form.set("code", params.get("code")!)
-    form.set("client_id", client_id.toString())
-    form.set("redirect_uri", client_id.toString())
-    form.set("code_verifier", localStorage.getItem("code_verifier")!)
-
-    const response = await fetch(token_endpoint, {
-      method: "POST",
-      headers: {
-        "Accept": "application/json",
-        "Content-Type": "application/x-www-form-urlencoded"
-      },
-      body: form.toString()
-    });
-
-    const grant = await response.json();
-
-    if ("access_token" in grant) {
-      localStorage.setItem("access_token", grant.access_token);
-      (document.getElementById("authorizing") as HTMLElement).style.display = "none";
-    }
-  }
-}
-
-let micropub: Micropub | null = null;
-const token = localStorage.getItem("access_token")
-const endpoint = localStorage.getItem("micropub_endpoint")
-if (token == null || endpoint == null) {
-  (document.getElementById("unauthorized") as HTMLElement).style.display = "block";
-} else {
-  (document.getElementById("authorized") as HTMLElement).style.display = "block";
-
-  micropub = new Micropub({ endpoint: new URL(endpoint), token });
-}
diff --git a/kittybox-rs/companion-lite/src/micropub_api.ts b/kittybox-rs/companion-lite/src/micropub_api.ts
deleted file mode 100644
index fa1c431..0000000
--- a/kittybox-rs/companion-lite/src/micropub_api.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-export interface MicropubChannel {
-  readonly uid: string,
-  readonly name: string
-}
-
-export interface MF2 {
-  type: string[],
-  properties: { [key:string]: (string | MF2 | {[key:string]: string})[] | undefined }
-}
-
-export interface MicropubConfig {
-  readonly channels?: MicropubChannel[],
-  readonly "media-endpoint"?: string
-}
-
-export interface MicropubErrorMessage {
-  readonly error: string,
-  readonly error_description: string | undefined
-}
-
-export class MicropubError extends Error {
-  readonly status: number | null
-  readonly response: MicropubErrorMessage | null
-
-  constructor(status: number | null, response: MicropubErrorMessage | null, cause: Error | null = null) {
-    // Needs to pass both `message` and `options` to install the "cause" property.
-    if (status == null) {
-      super("Micropub endpoint didn't respond properly", { cause });
-    } else if (response == null) {
-      super(`Micropub endpoint returned HTTP ${status}`, { cause });
-    } else {
-      super(
-        `Micropub endpoint returned ${response.error}: ${response.error_description ?? "(no description was provided)"}`,
-        { cause }
-      )
-    }
-
-    this.status = status;
-    this.response = response;
-  }
-}
-
-export class Micropub {
-  readonly token: string
-  readonly micropub_endpoint: URL
-  private config_response: MicropubConfig | null
-  
-  constructor({ endpoint, token }: { endpoint: URL, token: string }) {
-    this.micropub_endpoint = endpoint;
-    this.token = token;
-    this.config_response = null;
-  }
-
-  async config(): Promise<MicropubConfig> {
-    if (this.config_response != null) {
-      return this.config_response
-    }
-    let url = this.micropub_endpoint;
-    let params = new URLSearchParams();
-    for (const [key, val] of url.searchParams) {
-      params.append(key, val)
-    }
-    params.set("q", "config")
-
-    url.search = "?" + params.toString();
-
-    const response = await fetch(url, {
-      headers: {
-        "Authorization": `Bearer ${this.token}`
-      }
-    });
-    if (response.ok) {
-      const config = await response.json() as MicropubConfig;
-      this.config_response = config
-
-      return config
-    } else {
-      throw new MicropubError(response.status, await response.json() as MicropubErrorMessage);
-    }
-  }
-
-  async submit(mf2: MF2): Promise<URL> {
-    const response = await fetch(this.micropub_endpoint, {
-      method: "POST",
-      headers: {
-        "Authorization": `Bearer ${this.token}`,
-        "Content-Type": "application/json"
-      },
-      body: JSON.stringify(mf2)
-    })
-
-    if (response.status != 201 && response.status != 202) {
-      let err = await response.json() as MicropubErrorMessage;
-
-      throw new MicropubError(response.status, err)
-    } else {
-      return new URL(response.headers.get("Location") as string)
-    }
-  }
-
-  async upload(file: File): Promise<URL> {
-    const config = await this.config();
-    const media = config["media-endpoint"];
-    if (media == null) {
-      throw new Error("Micropub endpoint doesn't support file uploads")
-    }
-
-    const form = new FormData();
-    form.set("file", file);
-
-    const response = await fetch(media, {
-      method: "POST",
-      headers: {
-        "Authorization": `Bearer ${this.token}`,
-      },
-      body: form
-    })
-
-    if (response.ok) {
-      return new URL(response.headers.get("Location") as string)
-    } else {
-      throw new MicropubError(response.status, await response.json());
-    }
-  }
-}
diff --git a/kittybox-rs/companion-lite/style.css b/kittybox-rs/companion-lite/style.css
deleted file mode 100644
index 09ed398..0000000
--- a/kittybox-rs/companion-lite/style.css
+++ /dev/null
@@ -1,47 +0,0 @@
-* {
-    box-sizing: border-box;
-}
-
-:root {
-    font-family: sans-serif;
-}
-
-body {
-    margin: 0;
-}
-
-body > main {
-    margin: auto;
-    max-width: 1024px;
-}
-
-h1.header {
-    margin-top: 0.75em;
-    text-align: center;
-}
-
-fieldset + fieldset,
-fieldset + input,
-section + section,
-section + fieldset
-{
-    margin-top: 0.75em;
-}
-
-input[type="submit"] {
-    margin-left: auto;
-    display: block;
-}
-
-form > fieldset > section > label {
-    width: 100%;
-    display: block;
-}
-
-form > fieldset > section > input, form > fieldset > section > textarea {
-    width: 100%;
-}
-
-textarea {
-    min-height: 10em;
-}
diff --git a/kittybox-rs/companion-lite/tsconfig.json b/kittybox-rs/companion-lite/tsconfig.json
deleted file mode 100644
index 18b94c7..0000000
--- a/kittybox-rs/companion-lite/tsconfig.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
-  "compilerOptions": {
-    /* Visit https://aka.ms/tsconfig to read more about this file */
-
-    /* Projects */
-    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
-    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
-    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
-    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
-    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
-    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
-
-    /* Language and Environment */
-    "target": "es2022",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
-    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
-    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
-    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
-    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
-    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
-    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
-    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
-    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
-    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
-    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
-    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
-
-    /* Modules */
-    "module": "es2022",                                     /* Specify what module code is generated. */
-    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
-    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
-    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
-    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
-    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
-    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
-    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
-    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
-    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
-    // "resolveJsonModule": true,                        /* Enable importing .json files. */
-    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
-
-    /* JavaScript Support */
-    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
-    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
-    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
-
-    /* Emit */
-    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
-    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
-    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
-    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
-    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
-    "outDir": "./dist",                                  /* Specify an output folder for all emitted files. */
-    // "removeComments": true,                           /* Disable emitting comments. */
-    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
-    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
-    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
-    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
-    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
-    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
-    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
-    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
-    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
-    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
-    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
-    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
-    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
-    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
-    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
-    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
-
-    /* Interop Constraints */
-    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
-    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
-    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
-    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
-    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
-
-    /* Type Checking */
-    "strict": true,                                      /* Enable all strict type-checking options. */
-    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
-    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
-    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
-    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
-    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
-    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
-    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
-    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
-    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
-    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
-    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
-    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
-    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
-    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
-    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
-    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
-    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
-    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
-
-    /* Completeness */
-    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
-    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
-  },
-  "include": ["src/**/*"]
-}