about summary refs log tree commit diff
path: root/kittybox-rs/companion-lite/src/indieauth.ts
blob: 40facabd959f862f956dfa6e3525e05312a6fe7e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// @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
  }
}