@atcute/oauth-browser-client 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +296 -6
- package/dist/agents/server-agent.js +7 -6
- package/dist/agents/server-agent.js.map +1 -1
- package/dist/store/db.js +1 -1
- package/dist/store/db.js.map +1 -1
- package/lib/agents/exchange.ts +115 -0
- package/lib/agents/server-agent.ts +149 -0
- package/lib/agents/sessions.ts +142 -0
- package/lib/agents/user-agent.ts +99 -0
- package/lib/constants.ts +1 -0
- package/lib/dpop.ts +154 -0
- package/lib/environment.ts +27 -0
- package/lib/errors.ts +76 -0
- package/lib/index.ts +17 -0
- package/lib/resolvers.ts +222 -0
- package/lib/store/db.ts +184 -0
- package/lib/types/client.ts +82 -0
- package/lib/types/dpop.ts +7 -0
- package/lib/types/identity.ts +7 -0
- package/lib/types/par.ts +4 -0
- package/lib/types/server.ts +67 -0
- package/lib/types/store.ts +6 -0
- package/lib/types/token.ts +46 -0
- package/lib/utils/misc.ts +14 -0
- package/lib/utils/response.ts +3 -0
- package/lib/utils/runtime.ts +55 -0
- package/lib/utils/strings.ts +5 -0
- package/package.json +9 -5
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
|
|
3
|
+
import { database } from '../environment.js';
|
|
4
|
+
import { OAuthResponseError, TokenRefreshError } from '../errors.js';
|
|
5
|
+
import type { Session } from '../types/token.js';
|
|
6
|
+
import { locks } from '../utils/runtime.js';
|
|
7
|
+
|
|
8
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
9
|
+
|
|
10
|
+
export interface SessionGetOptions {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
noCache?: boolean;
|
|
13
|
+
allowStale?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type PendingItem<V> = Promise<{ value: V; isFresh: boolean }>;
|
|
17
|
+
const pending = new Map<At.DID, PendingItem<Session>>();
|
|
18
|
+
|
|
19
|
+
export const getSession = async (sub: At.DID, options?: SessionGetOptions): Promise<Session> => {
|
|
20
|
+
options?.signal?.throwIfAborted();
|
|
21
|
+
|
|
22
|
+
let allowStored = isTokenUsable;
|
|
23
|
+
if (options?.noCache) {
|
|
24
|
+
allowStored = returnFalse;
|
|
25
|
+
} else if (options?.allowStale) {
|
|
26
|
+
allowStored = returnTrue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// As long as concurrent requests are made for the same key, only one
|
|
30
|
+
// request will be made to the cache & getter function at a time. This works
|
|
31
|
+
// because there is no async operation between the while() loop and the
|
|
32
|
+
// pending.set() call. Because of the "single threaded" nature of
|
|
33
|
+
// JavaScript, the pending item will be set before the next iteration of the
|
|
34
|
+
// while loop.
|
|
35
|
+
let previousExecutionFlow: PendingItem<Session> | undefined;
|
|
36
|
+
while ((previousExecutionFlow = pending.get(sub))) {
|
|
37
|
+
try {
|
|
38
|
+
const { isFresh, value } = await previousExecutionFlow;
|
|
39
|
+
|
|
40
|
+
if (isFresh || allowStored(value)) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore errors from previous execution flows (they will have been
|
|
45
|
+
// propagated by that flow).
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
options?.signal?.throwIfAborted();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const run = async (): PendingItem<Session> => {
|
|
52
|
+
const storedSession = database.sessions.get(sub);
|
|
53
|
+
|
|
54
|
+
if (storedSession && allowStored(storedSession)) {
|
|
55
|
+
// Use the stored value as return value for the current execution
|
|
56
|
+
// flow. Notify other concurrent execution flows (that should be
|
|
57
|
+
// "stuck" in the loop before until this promise resolves) that we got
|
|
58
|
+
// a value, but that it came from the store (isFresh = false).
|
|
59
|
+
return { isFresh: false, value: storedSession };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const newSession = await refreshToken(sub, storedSession);
|
|
63
|
+
|
|
64
|
+
await storeSession(sub, newSession);
|
|
65
|
+
return { isFresh: true, value: newSession };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let promise: PendingItem<Session>;
|
|
69
|
+
|
|
70
|
+
if (locks) {
|
|
71
|
+
promise = locks.request(`atcute-oauth:${sub}`, run);
|
|
72
|
+
} else {
|
|
73
|
+
promise = run();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
promise = promise.finally(() => pending.delete(sub));
|
|
77
|
+
|
|
78
|
+
if (pending.has(sub)) {
|
|
79
|
+
// This should never happen. Indeed, there must not be any 'await'
|
|
80
|
+
// statement between this and the loop iteration check meaning that
|
|
81
|
+
// this.pending.get returned undefined. It is there to catch bugs that
|
|
82
|
+
// would occur in future changes to the code.
|
|
83
|
+
throw new Error('concurrent request for the same key');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pending.set(sub, promise);
|
|
87
|
+
|
|
88
|
+
const { value } = await promise;
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const storeSession = async (sub: At.DID, newSession: Session): Promise<void> => {
|
|
93
|
+
try {
|
|
94
|
+
database.sessions.set(sub, newSession);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
await onRefreshError(newSession);
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const deleteStoredSession = (sub: At.DID): void => {
|
|
102
|
+
database.sessions.delete(sub);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const listStoredSessions = (): At.DID[] => {
|
|
106
|
+
return database.sessions.keys();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const returnTrue = () => true;
|
|
110
|
+
const returnFalse = () => false;
|
|
111
|
+
|
|
112
|
+
const refreshToken = async (sub: At.DID, storedSession: Session | undefined): Promise<Session> => {
|
|
113
|
+
if (storedSession === undefined) {
|
|
114
|
+
throw new TokenRefreshError(sub, `session deleted by another tab`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { dpopKey, info, token } = storedSession;
|
|
118
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const newToken = await server.refresh({ sub: info.sub, token });
|
|
122
|
+
|
|
123
|
+
return { dpopKey, info, token: newToken };
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') {
|
|
126
|
+
throw new TokenRefreshError(sub, `session was revoked`, { cause });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw cause;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onRefreshError = async ({ dpopKey, info, token }: Session) => {
|
|
134
|
+
// If the token data cannot be stored, let's revoke it
|
|
135
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
136
|
+
await server.revoke(token.refresh ?? token.access);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const isTokenUsable = ({ token }: Session): boolean => {
|
|
140
|
+
const expires = token.expires_at;
|
|
141
|
+
return expires == null || Date.now() + 60_000 <= expires;
|
|
142
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { FetchHandlerObject } from '@atcute/client';
|
|
2
|
+
import type { At } from '@atcute/client/lexicons';
|
|
3
|
+
|
|
4
|
+
import { createDPoPFetch } from '../dpop.js';
|
|
5
|
+
import { CLIENT_ID } from '../environment.js';
|
|
6
|
+
import type { Session } from '../types/token.js';
|
|
7
|
+
|
|
8
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
9
|
+
import { type SessionGetOptions, deleteStoredSession, getSession } from './sessions.js';
|
|
10
|
+
|
|
11
|
+
export class OAuthUserAgent implements FetchHandlerObject {
|
|
12
|
+
#fetch: typeof fetch;
|
|
13
|
+
#getSessionPromise: Promise<Session> | undefined;
|
|
14
|
+
|
|
15
|
+
constructor(public session: Session) {
|
|
16
|
+
this.#fetch = createDPoPFetch(CLIENT_ID, session.dpopKey, false);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get sub(): At.DID {
|
|
20
|
+
return this.session.info.sub;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getSession(options?: SessionGetOptions): Promise<Session> {
|
|
24
|
+
const promise = getSession(this.session.info.sub, options);
|
|
25
|
+
|
|
26
|
+
promise
|
|
27
|
+
.then((session) => {
|
|
28
|
+
this.session = session;
|
|
29
|
+
})
|
|
30
|
+
.finally(() => {
|
|
31
|
+
this.#getSessionPromise = undefined;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (this.#getSessionPromise = promise);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async signOut(): Promise<void> {
|
|
38
|
+
const sub = this.session.info.sub;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { dpopKey, info, token } = await getSession(sub, { allowStale: true });
|
|
42
|
+
const server = new OAuthServerAgent(info.server, dpopKey);
|
|
43
|
+
|
|
44
|
+
await server.revoke(token.refresh ?? token.access);
|
|
45
|
+
} finally {
|
|
46
|
+
deleteStoredSession(sub);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handle(pathname: string, init?: RequestInit): Promise<Response> {
|
|
51
|
+
await this.#getSessionPromise;
|
|
52
|
+
|
|
53
|
+
const headers = new Headers(init?.headers);
|
|
54
|
+
|
|
55
|
+
let session = this.session;
|
|
56
|
+
let url = new URL(pathname, session.info.aud);
|
|
57
|
+
|
|
58
|
+
headers.set('authorization', `${session.token.type} ${session.token.access}`);
|
|
59
|
+
|
|
60
|
+
let response = await this.#fetch(url, { ...init, headers });
|
|
61
|
+
if (!isInvalidTokenResponse(response)) {
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (this.#getSessionPromise) {
|
|
67
|
+
session = await this.#getSessionPromise;
|
|
68
|
+
} else {
|
|
69
|
+
session = await this.getSession();
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Stream already consumed, can't retry.
|
|
76
|
+
if (init?.body instanceof ReadableStream) {
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
url = new URL(pathname, session.info.aud);
|
|
81
|
+
headers.set('authorization', `${session.token.type} ${session.token.access}`);
|
|
82
|
+
|
|
83
|
+
return await this.#fetch(url, { ...init, headers });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isInvalidTokenResponse = (response: Response) => {
|
|
88
|
+
if (response.status !== 401) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const auth = response.headers.get('www-authenticate');
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
auth != null &&
|
|
96
|
+
(auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) &&
|
|
97
|
+
auth.includes('error="invalid_token"')
|
|
98
|
+
);
|
|
99
|
+
};
|
package/lib/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_APPVIEW_URL = 'https://public.api.bsky.app';
|
package/lib/dpop.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid/non-secure';
|
|
2
|
+
|
|
3
|
+
import { database } from './environment.js';
|
|
4
|
+
import type { DPoPKey } from './types/dpop.js';
|
|
5
|
+
import { extractContentType } from './utils/response.js';
|
|
6
|
+
import { encoder, fromBase64Url, toBase64Url, toSha256 } from './utils/runtime.js';
|
|
7
|
+
|
|
8
|
+
const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
|
|
9
|
+
|
|
10
|
+
export const createES256Key = async (): Promise<DPoPKey> => {
|
|
11
|
+
const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);
|
|
12
|
+
|
|
13
|
+
const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);
|
|
14
|
+
const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
typ: 'ES256',
|
|
18
|
+
key: toBase64Url(new Uint8Array(key)),
|
|
19
|
+
jwt: toBase64Url(encoder.encode(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const createDPoPSignage = (issuer: string, dpopKey: DPoPKey) => {
|
|
24
|
+
const headerString = dpopKey.jwt;
|
|
25
|
+
const keyPromise = crypto.subtle.importKey('pkcs8', fromBase64Url(dpopKey.key), ES256_ALG, true, ['sign']);
|
|
26
|
+
|
|
27
|
+
const constructPayload = (
|
|
28
|
+
method: string,
|
|
29
|
+
url: string,
|
|
30
|
+
nonce: string | undefined,
|
|
31
|
+
ath: string | undefined,
|
|
32
|
+
) => {
|
|
33
|
+
const now = (Date.now() / 1_000) | 0;
|
|
34
|
+
|
|
35
|
+
const payload = {
|
|
36
|
+
iss: issuer,
|
|
37
|
+
iat: now,
|
|
38
|
+
// This seems fine, we can remake the request if it fails.
|
|
39
|
+
jti: nanoid(12),
|
|
40
|
+
htm: method,
|
|
41
|
+
htu: url,
|
|
42
|
+
nonce: nonce,
|
|
43
|
+
ath: ath,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return toBase64Url(encoder.encode(JSON.stringify(payload)));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return async (method: string, url: string, nonce: string | undefined, ath: string | undefined) => {
|
|
50
|
+
const payloadString = constructPayload(method, url, nonce, ath);
|
|
51
|
+
|
|
52
|
+
const signed = await crypto.subtle.sign(
|
|
53
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
54
|
+
await keyPromise,
|
|
55
|
+
encoder.encode(headerString + '.' + payloadString),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const signatureString = toBase64Url(new Uint8Array(signed));
|
|
59
|
+
|
|
60
|
+
return headerString + '.' + payloadString + '.' + signatureString;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const createDPoPFetch = (issuer: string, dpopKey: DPoPKey, isAuthServer?: boolean): typeof fetch => {
|
|
65
|
+
const nonces = database.dpopNonces;
|
|
66
|
+
const sign = createDPoPSignage(issuer, dpopKey);
|
|
67
|
+
|
|
68
|
+
return async (input, init) => {
|
|
69
|
+
const request: Request = init == null && input instanceof Request ? input : new Request(input, init);
|
|
70
|
+
|
|
71
|
+
const authorizationHeader = request.headers.get('authorization');
|
|
72
|
+
const ath = authorizationHeader?.startsWith('DPoP ')
|
|
73
|
+
? await toSha256(authorizationHeader.slice(5))
|
|
74
|
+
: undefined;
|
|
75
|
+
|
|
76
|
+
const { method, url } = request;
|
|
77
|
+
const { origin } = new URL(url);
|
|
78
|
+
|
|
79
|
+
let initNonce: string | undefined;
|
|
80
|
+
try {
|
|
81
|
+
initNonce = nonces.get(origin);
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore get errors, we will just not send a nonce
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const initProof = await sign(method, url, initNonce, ath);
|
|
87
|
+
request.headers.set('dpop', initProof);
|
|
88
|
+
|
|
89
|
+
const initResponse = await fetch(request);
|
|
90
|
+
|
|
91
|
+
const nextNonce = initResponse.headers.get('dpop-nonce');
|
|
92
|
+
if (!nextNonce || nextNonce === initNonce) {
|
|
93
|
+
// No nonce was returned or it is the same as the one we sent. No need to
|
|
94
|
+
// update the nonce store, or retry the request.
|
|
95
|
+
return initResponse;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Store the fresh nonce for future requests
|
|
99
|
+
try {
|
|
100
|
+
nonces.set(origin, nextNonce);
|
|
101
|
+
} catch {
|
|
102
|
+
// Ignore set errors
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
|
|
106
|
+
if (!shouldRetry) {
|
|
107
|
+
// Not a "use_dpop_nonce" error, so there is no need to retry
|
|
108
|
+
return initResponse;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If the input stream was already consumed, we cannot retry the request. A
|
|
112
|
+
// solution would be to clone() the request but that would bufferize the
|
|
113
|
+
// entire stream in memory which can lead to memory starvation. Instead, we
|
|
114
|
+
// will return the original response and let the calling code handle retries.
|
|
115
|
+
|
|
116
|
+
if (input === request || init?.body instanceof ReadableStream) {
|
|
117
|
+
return initResponse;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nextProof = await sign(method, url, nextNonce, ath);
|
|
121
|
+
const nextRequest = new Request(input, init);
|
|
122
|
+
nextRequest.headers.set('dpop', nextProof);
|
|
123
|
+
|
|
124
|
+
return await fetch(nextRequest);
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => {
|
|
129
|
+
// https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
130
|
+
// https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
|
|
131
|
+
if (isAuthServer === undefined || isAuthServer === false) {
|
|
132
|
+
if (response.status === 401) {
|
|
133
|
+
const wwwAuth = response.headers.get('www-authenticate');
|
|
134
|
+
if (wwwAuth?.startsWith('DPoP')) {
|
|
135
|
+
return wwwAuth.includes('error="use_dpop_nonce"');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
|
|
141
|
+
if (isAuthServer === undefined || isAuthServer === true) {
|
|
142
|
+
if (response.status === 400 && extractContentType(response.headers) === 'application/json') {
|
|
143
|
+
try {
|
|
144
|
+
const json = await response.clone().json();
|
|
145
|
+
return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';
|
|
146
|
+
} catch {
|
|
147
|
+
// Response too big (to be "use_dpop_nonce" error) or invalid JSON
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return false;
|
|
154
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createOAuthDatabase, type OAuthDatabase } from './store/db.js';
|
|
2
|
+
|
|
3
|
+
export let CLIENT_ID: string;
|
|
4
|
+
export let REDIRECT_URI: string;
|
|
5
|
+
|
|
6
|
+
export let database: OAuthDatabase;
|
|
7
|
+
|
|
8
|
+
export interface ConfigureOAuthOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Client metadata, necessary to drive the whole request
|
|
11
|
+
*/
|
|
12
|
+
metadata: {
|
|
13
|
+
client_id: string;
|
|
14
|
+
redirect_uri: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Name that will be used as prefix for storage keys needed to persist authentication.
|
|
19
|
+
* @default "atcute-oauth"
|
|
20
|
+
*/
|
|
21
|
+
storageName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const configureOAuth = (options: ConfigureOAuthOptions) => {
|
|
25
|
+
({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI } = options.metadata);
|
|
26
|
+
database = createOAuthDatabase({ name: options.storageName ?? 'atcute-oauth' });
|
|
27
|
+
};
|
package/lib/errors.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
|
|
3
|
+
export class LoginError extends Error {
|
|
4
|
+
override name = 'LoginError';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class AuthorizationError extends Error {
|
|
8
|
+
override name = 'AuthorizationError';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ResolverError extends Error {
|
|
12
|
+
override name = 'ResolverError';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TokenRefreshError extends Error {
|
|
16
|
+
override name = 'TokenRefreshError';
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
public readonly sub: At.DID,
|
|
20
|
+
message: string,
|
|
21
|
+
options?: ErrorOptions,
|
|
22
|
+
) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class OAuthResponseError extends Error {
|
|
28
|
+
override name = 'OAuthResponseError';
|
|
29
|
+
|
|
30
|
+
readonly error: string | undefined;
|
|
31
|
+
readonly description: string | undefined;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly response: Response,
|
|
35
|
+
public readonly data: any,
|
|
36
|
+
) {
|
|
37
|
+
const error = ifString(ifObject(data)?.['error']);
|
|
38
|
+
const errorDescription = ifString(ifObject(data)?.['error_description']);
|
|
39
|
+
|
|
40
|
+
const messageError = error ? `"${error}"` : 'unknown';
|
|
41
|
+
const messageDesc = errorDescription ? `: ${errorDescription}` : '';
|
|
42
|
+
const message = `OAuth ${messageError} error${messageDesc}`;
|
|
43
|
+
|
|
44
|
+
super(message);
|
|
45
|
+
|
|
46
|
+
this.error = error;
|
|
47
|
+
this.description = errorDescription;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get status() {
|
|
51
|
+
return this.response.status;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get headers() {
|
|
55
|
+
return this.response.headers;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class FetchResponseError extends Error {
|
|
60
|
+
override name = 'FetchResponseError';
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
public readonly response: Response,
|
|
64
|
+
public status: number,
|
|
65
|
+
message: string,
|
|
66
|
+
) {
|
|
67
|
+
super(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ifString = (v: unknown): string | undefined => {
|
|
72
|
+
return typeof v === 'string' ? v : undefined;
|
|
73
|
+
};
|
|
74
|
+
const ifObject = (v: unknown): Record<string, unknown> | undefined => {
|
|
75
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as any) : undefined;
|
|
76
|
+
};
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { configureOAuth, type ConfigureOAuthOptions } from './environment.js';
|
|
2
|
+
|
|
3
|
+
export * from './errors.js';
|
|
4
|
+
export * from './resolvers.js';
|
|
5
|
+
|
|
6
|
+
export * from './agents/exchange.js';
|
|
7
|
+
export * from './agents/server-agent.js';
|
|
8
|
+
export * from './agents/sessions.js';
|
|
9
|
+
export * from './agents/user-agent.js';
|
|
10
|
+
|
|
11
|
+
export * from './types/client.js';
|
|
12
|
+
export * from './types/dpop.js';
|
|
13
|
+
export * from './types/identity.js';
|
|
14
|
+
export * from './types/par.js';
|
|
15
|
+
export * from './types/server.js';
|
|
16
|
+
export * from './types/store.js';
|
|
17
|
+
export * from './types/token.js';
|