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
}
}
|