@atcute/oauth-browser-client 1.0.1

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.
Files changed (69) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +159 -0
  3. package/dist/agents/exchange.d.ts +20 -0
  4. package/dist/agents/exchange.js +87 -0
  5. package/dist/agents/exchange.js.map +1 -0
  6. package/dist/agents/server-agent.d.ts +22 -0
  7. package/dist/agents/server-agent.js +119 -0
  8. package/dist/agents/server-agent.js.map +1 -0
  9. package/dist/agents/sessions.d.ts +11 -0
  10. package/dist/agents/sessions.js +107 -0
  11. package/dist/agents/sessions.js.map +1 -0
  12. package/dist/agents/user-agent.d.ts +13 -0
  13. package/dist/agents/user-agent.js +77 -0
  14. package/dist/agents/user-agent.js.map +1 -0
  15. package/dist/constants.d.ts +1 -0
  16. package/dist/constants.js +2 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/dpop.d.ts +4 -0
  19. package/dist/dpop.js +118 -0
  20. package/dist/dpop.js.map +1 -0
  21. package/dist/environment.d.ts +19 -0
  22. package/dist/environment.js +9 -0
  23. package/dist/environment.js.map +1 -0
  24. package/dist/errors.d.ts +31 -0
  25. package/dist/errors.js +59 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.js +15 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/resolvers.d.ts +52 -0
  31. package/dist/resolvers.js +186 -0
  32. package/dist/resolvers.js.map +1 -0
  33. package/dist/store/db.d.ts +18 -0
  34. package/dist/store/db.js +106 -0
  35. package/dist/store/db.js.map +1 -0
  36. package/dist/types/client.d.ts +37 -0
  37. package/dist/types/client.js +2 -0
  38. package/dist/types/client.js.map +1 -0
  39. package/dist/types/dpop.d.ts +7 -0
  40. package/dist/types/dpop.js +2 -0
  41. package/dist/types/dpop.js.map +1 -0
  42. package/dist/types/identity.d.ts +6 -0
  43. package/dist/types/identity.js +2 -0
  44. package/dist/types/identity.js.map +1 -0
  45. package/dist/types/par.d.ts +4 -0
  46. package/dist/types/par.js +2 -0
  47. package/dist/types/par.js.map +1 -0
  48. package/dist/types/server.d.ts +57 -0
  49. package/dist/types/server.js +2 -0
  50. package/dist/types/server.js.map +1 -0
  51. package/dist/types/store.d.ts +6 -0
  52. package/dist/types/store.js +2 -0
  53. package/dist/types/store.js.map +1 -0
  54. package/dist/types/token.d.ts +38 -0
  55. package/dist/types/token.js +2 -0
  56. package/dist/types/token.js.map +1 -0
  57. package/dist/utils/misc.d.ts +3 -0
  58. package/dist/utils/misc.js +10 -0
  59. package/dist/utils/misc.js.map +1 -0
  60. package/dist/utils/response.d.ts +1 -0
  61. package/dist/utils/response.js +4 -0
  62. package/dist/utils/response.js.map +1 -0
  63. package/dist/utils/runtime.d.ts +12 -0
  64. package/dist/utils/runtime.js +44 -0
  65. package/dist/utils/runtime.js.map +1 -0
  66. package/dist/utils/strings.d.ts +2 -0
  67. package/dist/utils/strings.js +4 -0
  68. package/dist/utils/strings.js.map +1 -0
  69. package/package.json +29 -0
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy
2
+ of this software and associated documentation files (the "Software"), to deal
3
+ in the Software without restriction, including without limitation the rights
4
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5
+ copies of the Software, and to permit persons to whom the Software is
6
+ furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all
9
+ copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @atcute/oauth-browser-client
2
+
3
+ minimal OAuth browser client implementation for AT Protocol.
4
+
5
+ - **only the bare minimum**: enough code to get authentication reasonably working, with only one
6
+ happy path is supported (only ES256 keys for DPoP. PKCE and DPoP-bound PAR is required.)
7
+ - **does not use IndexedDB**: makes the library work under Safari's lockdown mode, and has less
8
+ [maintenance headache][indexeddb-woes] overall, but it also means this is "less secure" (it won't
9
+ be able to use non-exportable keys as recommended by [DPoP specification][idb-dpop-spec].)
10
+ - **no independent DNS/HTTP handle checks**: the default handle resolver makes use of Bluesky's
11
+ AppView to retrieve the correct DID identifier. you should be able to write your own resolver
12
+ function that'll resolve via DNS-over-HTTPS or via other PDSes.
13
+ - **not well-tested**: it has been used in personal projects for quite some time, but hasn't seen
14
+ any use outside of that. using the [reference implementation][oauth-atproto-lib] is recommended if
15
+ you are unsure about the implications presented here.
16
+
17
+ [indexeddb-woes]: https://gist.github.com/pesterhazy/4de96193af89a6dd5ce682ce2adff49a
18
+ [idb-dpop-spec]: https://datatracker.ietf.org/doc/html/rfc9449#section-2-4
19
+ [oauth-atproto-lib]: https://npm.im/@atproto/oauth-client-browser
20
+
21
+ ## usage
22
+
23
+ ### setup
24
+
25
+ initialize the client by importing and calling `configureOAuth` with the client ID and redirect URL.
26
+ this call should be placed before any other calls you make with this library.
27
+
28
+ ```ts
29
+ import { configureOAuth } from '@atcute/oauth-browser-client';
30
+
31
+ configureOAuth({
32
+ metadata: {
33
+ client_id: 'https://example.com/oauth/client-metadata.json',
34
+ redirect_uri: 'https://example.com/oauth/callback',
35
+ },
36
+ });
37
+ ```
38
+
39
+ ### starting an authorization flow
40
+
41
+ if your application involves asking for the user's handle or DID, you can use `resolveFromIdentity`
42
+ which resolves the user's identity to get its PDS, and the metadata of its authorization server.
43
+
44
+ ```ts
45
+ import { resolveFromIdentity } from '@atcute/oauth-browser-client';
46
+
47
+ const { identity, metadata } = await resolveFromIdentity('mary.my.id');
48
+ ```
49
+
50
+ alternatively, if it involves asking for the user's PDS, then you can use `resolveFromService` which
51
+ just grabs the authorization server metadata.
52
+
53
+ ```ts
54
+ import { resolveFromService } from '@atcute/oauth-browser-client';
55
+
56
+ const { metadata } = await resolveFromService('bsky.social');
57
+ ```
58
+
59
+ we can then proceed with authorization by calling `createAuthorizationUrl` with the resolved
60
+ `metadata` (and `identity`, if using `resolveFromIdentity`) along with the scope of the
61
+ authorization, which should either match the one in your client metadata, or a reduced set of it.
62
+
63
+ ```ts
64
+ import { createAuthorizationUrl } from '@atcute/oauth-browser-client';
65
+
66
+ // passing `identity` is optional,
67
+ // it allows for the login form to be autofilled with the user's handle or DID
68
+ const authUrl = await createAuthorizationUrl({
69
+ metadata: metadata,
70
+ identity: identity,
71
+ scope: 'atproto transition:generic transition:chat.bsky',
72
+ });
73
+
74
+ // recommended to wait for the browser to persist local storage before proceeding
75
+ await sleep(200);
76
+
77
+ // redirect the user to sign in and authorize the app
78
+ window.location.assign(authUrl);
79
+
80
+ // if this is on an async function, ideally the function should never ever resolve.
81
+ // the only way it should resolve at this point is if the user aborted the authorization
82
+ // by returning back to this page (thanks to back-forward page caching)
83
+ await new Promise((_resolve, reject) => {
84
+ const listener = () => {
85
+ reject(new Error(`user aborted the login request`));
86
+ };
87
+
88
+ window.addEventListener('pageshow', listener, { once: true });
89
+ });
90
+ ```
91
+
92
+ ### finalizing authorization
93
+
94
+ once the user has been redirected to your redirect URL, we can call `finalizeAuthorization` with the
95
+ parameters that have been provided.
96
+
97
+ ```ts
98
+ import { XRPC } from '@atcute/client';
99
+ import { OAuthUserAgent, finalizeAuthorization } from '@atcute/oauth-browser-client';
100
+
101
+ // `createAuthorizationUrl` asks for the server to redirect here with the
102
+ // parameters assigned in the hash, not the search string.
103
+ const params = new URLSearchParams(location.hash.slice(1));
104
+
105
+ // this is optional, but after retrieving the parameters, we should ideally
106
+ // scrub it from history to prevent this authorization state to be replayed,
107
+ // just for good measure.
108
+ history.replaceState(null, '', location.pathname + location.search);
109
+
110
+ // you'd be given a session object that you can then pass to OAuthUserAgent!
111
+ const session = await finalizeAuthorization(params);
112
+
113
+ // now you can start making requests!
114
+ const agent = new OAuthUserAgent(session);
115
+
116
+ // pass it onto the XRPC so you can make RPC calls with the PDS.
117
+ const rpc = new XRPC({ handler: agent });
118
+ ```
119
+
120
+ the `session` object returned by `finalizeAuthorization` should not be stored anywhere else, as it
121
+ is already persisted in the internal database. you are expected to keep track of who's signed in and
122
+ who was last signed in for your own UI, as the sessions stored by the database is not guaranteed to
123
+ be permanent (mostly if they don't come with a refresh token.)
124
+
125
+ ### resuming existing sessions
126
+
127
+ you can resume existing sessions by calling `getSession` with the DID identifier you intend to
128
+ resume.
129
+
130
+ ```ts
131
+ import { XRPC } from '@atcute/client';
132
+ import { OAuthUserAgent, getSession } from '@atcute/oauth-browser-client';
133
+
134
+ const session = await getSession('did:plc:ia76kvnndjutgedggx2ibrem', { allowStale: true });
135
+
136
+ const agent = new OAuthUserAgent(session);
137
+ const rpc = new XRPC({ handler: agent });
138
+ ```
139
+
140
+ ### removing sessions
141
+
142
+ you can manually remove sessions via `deleteStoredSession`, but ideally, you should revoke the token
143
+ first before doing so.
144
+
145
+ ```ts
146
+ import { OAuthUserAgent, deleteStoredSession, getSession } from '@atcute/oauth-browser-client';
147
+
148
+ const did = 'did:plc:ia76kvnndjutgedggx2ibrem';
149
+
150
+ try {
151
+ const session = await getSession(did, { allowStale: true });
152
+ const agent = new OAuthUserAgent(session);
153
+
154
+ await agent.signOut();
155
+ } catch (err) {
156
+ // `signOut` also deletes the session, we only serve as fallback if it fails.
157
+ deleteStoredSession(did);
158
+ }
159
+ ```
@@ -0,0 +1,20 @@
1
+ import type { IdentityMetadata } from '../types/identity.js';
2
+ import type { AuthorizationServerMetadata } from '../types/server.js';
3
+ import type { Session } from '../types/token.js';
4
+ export interface AuthorizeOptions {
5
+ metadata: AuthorizationServerMetadata;
6
+ identity?: IdentityMetadata;
7
+ scope: string;
8
+ }
9
+ /**
10
+ * Create authentication URL for authorization
11
+ * @param options
12
+ * @returns URL to redirect the user for authorization
13
+ */
14
+ export declare const createAuthorizationUrl: ({ metadata, identity, scope, }: AuthorizeOptions) => Promise<URL>;
15
+ /**
16
+ * Finalize authorization
17
+ * @param params Search params
18
+ * @returns Session object, which you can use to instantiate user agents
19
+ */
20
+ export declare const finalizeAuthorization: (params: URLSearchParams) => Promise<Session>;
@@ -0,0 +1,87 @@
1
+ import { createES256Key } from '../dpop.js';
2
+ import { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';
3
+ import { AuthorizationError, LoginError } from '../errors.js';
4
+ import { generatePKCE, generateState } from '../utils/runtime.js';
5
+ import { OAuthServerAgent } from './server-agent.js';
6
+ import { storeSession } from './sessions.js';
7
+ /**
8
+ * Create authentication URL for authorization
9
+ * @param options
10
+ * @returns URL to redirect the user for authorization
11
+ */
12
+ export const createAuthorizationUrl = async ({ metadata, identity, scope, }) => {
13
+ const state = generateState();
14
+ const pkce = await generatePKCE();
15
+ const dpopKey = await createES256Key();
16
+ const params = {
17
+ redirect_uri: REDIRECT_URI,
18
+ code_challenge: pkce.challenge,
19
+ code_challenge_method: pkce.method,
20
+ state: state,
21
+ login_hint: identity?.raw,
22
+ response_mode: 'fragment',
23
+ response_type: 'code',
24
+ display: 'page',
25
+ // id_token_hint: undefined,
26
+ // max_age: undefined,
27
+ // prompt: undefined,
28
+ scope: scope,
29
+ // ui_locales: undefined,
30
+ };
31
+ database.states.set(state, {
32
+ dpopKey: dpopKey,
33
+ metadata: metadata,
34
+ verifier: pkce.verifier,
35
+ });
36
+ const server = new OAuthServerAgent(metadata, dpopKey);
37
+ const response = await server.request('pushed_authorization_request', params);
38
+ const authUrl = new URL(metadata.authorization_endpoint);
39
+ authUrl.searchParams.set('client_id', CLIENT_ID);
40
+ authUrl.searchParams.set('request_uri', response.request_uri);
41
+ return authUrl;
42
+ };
43
+ /**
44
+ * Finalize authorization
45
+ * @param params Search params
46
+ * @returns Session object, which you can use to instantiate user agents
47
+ */
48
+ export const finalizeAuthorization = async (params) => {
49
+ const issuer = params.get('iss');
50
+ const state = params.get('state');
51
+ const code = params.get('code');
52
+ const error = params.get('error');
53
+ if (!state || !(code || error)) {
54
+ throw new LoginError(`missing parameters`);
55
+ }
56
+ const stored = database.states.get(state);
57
+ if (stored) {
58
+ // Delete now that we've caught it
59
+ database.states.delete(state);
60
+ }
61
+ else {
62
+ throw new LoginError(`unknown state provided`);
63
+ }
64
+ const dpopKey = stored.dpopKey;
65
+ const metadata = stored.metadata;
66
+ if (error) {
67
+ throw new AuthorizationError(params.get('error_description') || error);
68
+ }
69
+ if (!code) {
70
+ throw new LoginError(`missing code parameter`);
71
+ }
72
+ if (issuer === null) {
73
+ throw new LoginError(`missing issuer parameter`);
74
+ }
75
+ else if (issuer !== metadata.issuer) {
76
+ throw new LoginError(`issuer mismatch`);
77
+ }
78
+ // Retrieve authentication tokens
79
+ const server = new OAuthServerAgent(metadata, dpopKey);
80
+ const { info, token } = await server.exchangeCode(code, stored.verifier);
81
+ // We're finished!
82
+ const sub = info.sub;
83
+ const session = { dpopKey, info, token };
84
+ await storeSession(sub, session);
85
+ return session;
86
+ };
87
+ //# sourceMappingURL=exchange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exchange.js","sourceRoot":"","sources":["../../lib/agents/exchange.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { At } from '@atcute/client/lexicons';
2
+ import type { DPoPKey } from '../types/dpop.js';
3
+ import type { OAuthParResponse } from '../types/par.js';
4
+ import type { PersistedAuthorizationServerMetadata } from '../types/server.js';
5
+ import type { ExchangeInfo, OAuthTokenResponse, TokenInfo } from '../types/token.js';
6
+ export declare class OAuthServerAgent {
7
+ #private;
8
+ constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DPoPKey);
9
+ request(endpoint: 'pushed_authorization_request', payload: Record<string, unknown>): Promise<OAuthParResponse>;
10
+ request(endpoint: 'token', payload: Record<string, unknown>): Promise<OAuthTokenResponse>;
11
+ request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>;
12
+ request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>;
13
+ revoke(token: string): Promise<void>;
14
+ exchangeCode(code: string, verifier?: string): Promise<{
15
+ info: ExchangeInfo;
16
+ token: TokenInfo;
17
+ }>;
18
+ refresh({ sub, token }: {
19
+ sub: At.DID;
20
+ token: TokenInfo;
21
+ }): Promise<TokenInfo>;
22
+ }
@@ -0,0 +1,119 @@
1
+ import { createDPoPFetch } from '../dpop.js';
2
+ import { CLIENT_ID, REDIRECT_URI } from '../environment.js';
3
+ import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';
4
+ import { resolveFromIdentity } from '../resolvers.js';
5
+ import { pick } from '../utils/misc.js';
6
+ import { extractContentType } from '../utils/response.js';
7
+ export class OAuthServerAgent {
8
+ #fetch;
9
+ #metadata;
10
+ constructor(metadata, dpopKey) {
11
+ this.#metadata = metadata;
12
+ this.#fetch = createDPoPFetch(CLIENT_ID, dpopKey, true);
13
+ }
14
+ async request(endpoint, payload) {
15
+ const url = this.#metadata[`${endpoint}_endpoint`];
16
+ if (!url) {
17
+ throw new Error(`no endpoint for ${endpoint}`);
18
+ }
19
+ const response = await this.#fetch(url, {
20
+ method: 'post',
21
+ headers: { 'content-type': 'application/json' },
22
+ body: JSON.stringify({ ...payload, client_id: CLIENT_ID }),
23
+ });
24
+ if (extractContentType(response.headers) !== 'application/json') {
25
+ throw new FetchResponseError(response, 2, `unexpected content-type`);
26
+ }
27
+ const json = await response.json();
28
+ if (response.ok) {
29
+ return json;
30
+ }
31
+ else {
32
+ throw new OAuthResponseError(response, json);
33
+ }
34
+ }
35
+ async revoke(token) {
36
+ try {
37
+ await this.request('revocation', { token: token });
38
+ }
39
+ catch { }
40
+ }
41
+ async exchangeCode(code, verifier) {
42
+ const response = await this.request('token', {
43
+ grant_type: 'authorization_code',
44
+ redirect_uri: REDIRECT_URI,
45
+ code: code,
46
+ code_verifier: verifier,
47
+ });
48
+ try {
49
+ return await this.#processExchangeResponse(response);
50
+ }
51
+ catch (err) {
52
+ await this.revoke(response.access_token);
53
+ throw err;
54
+ }
55
+ }
56
+ async refresh({ sub, token }) {
57
+ if (!token.refresh) {
58
+ throw new TokenRefreshError(sub, 'no refresh token available');
59
+ }
60
+ const response = await this.request('token', {
61
+ grant_type: 'refresh_token',
62
+ refresh_token: token.refresh,
63
+ });
64
+ try {
65
+ if (sub !== response.sub) {
66
+ throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`);
67
+ }
68
+ return this.#processTokenResponse(response);
69
+ }
70
+ catch (err) {
71
+ await this.revoke(response.access_token);
72
+ throw err;
73
+ }
74
+ }
75
+ #processTokenResponse(res) {
76
+ const sub = res.sub;
77
+ const scope = res.scope;
78
+ if (!sub) {
79
+ throw new TypeError(`missing sub field in token response`);
80
+ }
81
+ if (!scope) {
82
+ throw new TypeError(`missing scope field in token response`);
83
+ }
84
+ return {
85
+ scope: scope,
86
+ refresh: res.refresh_token,
87
+ access: res.access_token,
88
+ type: res.token_type ?? 'Bearer',
89
+ expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,
90
+ };
91
+ }
92
+ async #processExchangeResponse(res) {
93
+ const sub = res.sub;
94
+ if (!sub) {
95
+ throw new TypeError(`missing sub field in token response`);
96
+ }
97
+ const token = this.#processTokenResponse(res);
98
+ const resolved = await resolveFromIdentity(sub);
99
+ if (resolved.metadata.issuer !== this.#metadata.issuer) {
100
+ throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);
101
+ }
102
+ return {
103
+ token: token,
104
+ info: {
105
+ sub: sub,
106
+ aud: resolved.identity.pds.href,
107
+ server: pick(resolved.metadata, [
108
+ 'issuer',
109
+ 'authorization_endpoint',
110
+ 'introspection_endpoint',
111
+ 'pushed_authorization_request_endpoint',
112
+ 'revocation_endpoint',
113
+ 'token_endpoint',
114
+ ]),
115
+ },
116
+ };
117
+ }
118
+ }
119
+ //# sourceMappingURL=server-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-agent.js","sourceRoot":"","sources":["../../lib/agents/server-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { At } from '@atcute/client/lexicons';
2
+ import type { Session } from '../types/token.js';
3
+ export interface SessionGetOptions {
4
+ signal?: AbortSignal;
5
+ noCache?: boolean;
6
+ allowStale?: boolean;
7
+ }
8
+ export declare const getSession: (sub: At.DID, options?: SessionGetOptions) => Promise<Session>;
9
+ export declare const storeSession: (sub: At.DID, newSession: Session) => Promise<void>;
10
+ export declare const deleteStoredSession: (sub: At.DID) => void;
11
+ export declare const listStoredSessions: () => At.DID[];
@@ -0,0 +1,107 @@
1
+ import { database } from '../environment.js';
2
+ import { OAuthResponseError, TokenRefreshError } from '../errors.js';
3
+ import { OAuthServerAgent } from './server-agent.js';
4
+ const pending = new Map();
5
+ export const getSession = async (sub, options) => {
6
+ options?.signal?.throwIfAborted();
7
+ let allowStored = isTokenUsable;
8
+ if (options?.noCache) {
9
+ allowStored = returnFalse;
10
+ }
11
+ else if (options?.allowStale) {
12
+ allowStored = returnTrue;
13
+ }
14
+ // As long as concurrent requests are made for the same key, only one
15
+ // request will be made to the cache & getter function at a time. This works
16
+ // because there is no async operation between the while() loop and the
17
+ // pending.set() call. Because of the "single threaded" nature of
18
+ // JavaScript, the pending item will be set before the next iteration of the
19
+ // while loop.
20
+ let previousExecutionFlow;
21
+ while ((previousExecutionFlow = pending.get(sub))) {
22
+ try {
23
+ const { isFresh, value } = await previousExecutionFlow;
24
+ if (isFresh || allowStored(value)) {
25
+ return value;
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore errors from previous execution flows (they will have been
30
+ // propagated by that flow).
31
+ }
32
+ options?.signal?.throwIfAborted();
33
+ }
34
+ const lockKey = `atcute-oauth:${sub}`;
35
+ let promise;
36
+ promise = navigator.locks.request(lockKey, async () => {
37
+ const storedSession = database.sessions.get(sub);
38
+ console.log(storedSession, allowStored);
39
+ if (storedSession && allowStored(storedSession)) {
40
+ console.log('true');
41
+ // Use the stored value as return value for the current execution
42
+ // flow. Notify other concurrent execution flows (that should be
43
+ // "stuck" in the loop before until this promise resolves) that we got
44
+ // a value, but that it came from the store (isFresh = false).
45
+ return { isFresh: false, value: storedSession };
46
+ }
47
+ console.log('false');
48
+ const newSession = await refreshToken(sub, storedSession);
49
+ await storeSession(sub, newSession);
50
+ return { isFresh: true, value: newSession };
51
+ });
52
+ promise = promise.finally(() => pending.delete(sub));
53
+ if (pending.has(sub)) {
54
+ // This should never happen. Indeed, there must not be any 'await'
55
+ // statement between this and the loop iteration check meaning that
56
+ // this.pending.get returned undefined. It is there to catch bugs that
57
+ // would occur in future changes to the code.
58
+ throw new Error('concurrent request for the same key');
59
+ }
60
+ pending.set(sub, promise);
61
+ const { value } = await promise;
62
+ return value;
63
+ };
64
+ export const storeSession = async (sub, newSession) => {
65
+ try {
66
+ database.sessions.set(sub, newSession);
67
+ }
68
+ catch (err) {
69
+ await onRefreshError(newSession);
70
+ throw err;
71
+ }
72
+ };
73
+ export const deleteStoredSession = (sub) => {
74
+ database.sessions.delete(sub);
75
+ };
76
+ export const listStoredSessions = () => {
77
+ return database.sessions.keys();
78
+ };
79
+ const returnTrue = () => true;
80
+ const returnFalse = () => false;
81
+ const refreshToken = async (sub, storedSession) => {
82
+ if (storedSession === undefined) {
83
+ throw new TokenRefreshError(sub, `session deleted by another tab`);
84
+ }
85
+ const { dpopKey, info, token } = storedSession;
86
+ const server = new OAuthServerAgent(info.server, dpopKey);
87
+ try {
88
+ const newToken = await server.refresh({ sub: info.sub, token });
89
+ return { dpopKey, info, token: newToken };
90
+ }
91
+ catch (cause) {
92
+ if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') {
93
+ throw new TokenRefreshError(sub, `session was revoked`, { cause });
94
+ }
95
+ throw cause;
96
+ }
97
+ };
98
+ const onRefreshError = async ({ dpopKey, info, token }) => {
99
+ // If the token data cannot be stored, let's revoke it
100
+ const server = new OAuthServerAgent(info.server, dpopKey);
101
+ await server.revoke(token.refresh ?? token.access);
102
+ };
103
+ const isTokenUsable = ({ token }) => {
104
+ const expires = token.expires_at;
105
+ return expires == null || Date.now() + 60_000 <= expires;
106
+ };
107
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../lib/agents/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { FetchHandlerObject } from '@atcute/client';
2
+ import type { At } from '@atcute/client/lexicons';
3
+ import type { Session } from '../types/token.js';
4
+ import { type SessionGetOptions } from './sessions.js';
5
+ export declare class OAuthUserAgent implements FetchHandlerObject {
6
+ #private;
7
+ session: Session;
8
+ constructor(session: Session);
9
+ get sub(): At.DID;
10
+ getSession(options?: SessionGetOptions): Promise<Session>;
11
+ signOut(): Promise<void>;
12
+ handle(pathname: string, init?: RequestInit): Promise<Response>;
13
+ }
@@ -0,0 +1,77 @@
1
+ import { createDPoPFetch } from '../dpop.js';
2
+ import { CLIENT_ID } from '../environment.js';
3
+ import { OAuthServerAgent } from './server-agent.js';
4
+ import { deleteStoredSession, getSession } from './sessions.js';
5
+ export class OAuthUserAgent {
6
+ session;
7
+ #fetch;
8
+ #getSessionPromise;
9
+ constructor(session) {
10
+ this.session = session;
11
+ this.#fetch = createDPoPFetch(CLIENT_ID, session.dpopKey, false);
12
+ }
13
+ get sub() {
14
+ return this.session.info.sub;
15
+ }
16
+ getSession(options) {
17
+ const promise = getSession(this.session.info.sub, options);
18
+ promise
19
+ .then((session) => {
20
+ this.session = session;
21
+ })
22
+ .finally(() => {
23
+ this.#getSessionPromise = undefined;
24
+ });
25
+ return (this.#getSessionPromise = promise);
26
+ }
27
+ async signOut() {
28
+ const sub = this.session.info.sub;
29
+ try {
30
+ const { dpopKey, info, token } = await getSession(sub, { allowStale: true });
31
+ const server = new OAuthServerAgent(info.server, dpopKey);
32
+ await server.revoke(token.refresh ?? token.access);
33
+ }
34
+ finally {
35
+ deleteStoredSession(sub);
36
+ }
37
+ }
38
+ async handle(pathname, init) {
39
+ await this.#getSessionPromise;
40
+ const headers = new Headers(init?.headers);
41
+ let session = this.session;
42
+ let url = new URL(pathname, session.info.aud);
43
+ headers.set('authorization', `${session.token.type} ${session.token.access}`);
44
+ let response = await this.#fetch(url, { ...init, headers });
45
+ if (!isInvalidTokenResponse(response)) {
46
+ return response;
47
+ }
48
+ try {
49
+ if (this.#getSessionPromise) {
50
+ session = await this.#getSessionPromise;
51
+ }
52
+ else {
53
+ session = await this.getSession();
54
+ }
55
+ }
56
+ catch {
57
+ return response;
58
+ }
59
+ // Stream already consumed, can't retry.
60
+ if (init?.body instanceof ReadableStream) {
61
+ return response;
62
+ }
63
+ url = new URL(pathname, session.info.aud);
64
+ headers.set('authorization', `${session.token.type} ${session.token.access}`);
65
+ return await this.#fetch(url, { ...init, headers });
66
+ }
67
+ }
68
+ const isInvalidTokenResponse = (response) => {
69
+ if (response.status !== 401) {
70
+ return false;
71
+ }
72
+ const auth = response.headers.get('www-authenticate');
73
+ return (auth != null &&
74
+ (auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) &&
75
+ auth.includes('error="invalid_token"'));
76
+ };
77
+ //# sourceMappingURL=user-agent.js.map