@atproto/oauth-client 0.5.13 → 0.6.0
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/CHANGELOG.md +25 -0
- package/LICENSE.txt +1 -1
- package/dist/oauth-client.d.ts +7 -8
- package/dist/oauth-client.d.ts.map +1 -1
- package/dist/oauth-client.js +27 -26
- package/dist/oauth-client.js.map +1 -1
- package/dist/oauth-server-factory.d.ts +1 -1
- package/dist/oauth-server-factory.d.ts.map +1 -1
- package/dist/oauth-server-factory.js +0 -7
- package/dist/oauth-server-factory.js.map +1 -1
- package/dist/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +1 -4
- package/dist/oauth-session.js.map +1 -1
- package/dist/session-getter.d.ts +16 -21
- package/dist/session-getter.d.ts.map +1 -1
- package/dist/session-getter.js +65 -60
- package/dist/session-getter.js.map +1 -1
- package/dist/state-store.d.ts +13 -3
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js.map +1 -1
- package/dist/util.d.ts +0 -10
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -64
- package/dist/util.js.map +1 -1
- package/package.json +11 -11
- package/src/oauth-client.ts +47 -50
- package/src/oauth-server-factory.ts +2 -16
- package/src/oauth-session.ts +1 -4
- package/src/session-getter.ts +85 -102
- package/src/state-store.ts +13 -3
- package/src/util.ts +0 -67
package/src/oauth-client.ts
CHANGED
|
@@ -42,36 +42,38 @@ import { OAuthSession } from './oauth-session.js'
|
|
|
42
42
|
import { RuntimeImplementation } from './runtime-implementation.js'
|
|
43
43
|
import { Runtime } from './runtime.js'
|
|
44
44
|
import {
|
|
45
|
-
SessionEventMap,
|
|
46
45
|
SessionGetter,
|
|
46
|
+
SessionHooks,
|
|
47
47
|
SessionStore,
|
|
48
|
+
isExpectedSessionError,
|
|
48
49
|
} from './session-getter.js'
|
|
49
50
|
import { InternalStateData, StateStore } from './state-store.js'
|
|
50
51
|
import { AuthorizeOptions, CallbackOptions, ClientMetadata } from './types.js'
|
|
51
|
-
import { CustomEventTarget } from './util.js'
|
|
52
52
|
import { validateClientMetadata } from './validate-client-metadata.js'
|
|
53
53
|
|
|
54
54
|
// Export all types needed to construct OAuthClientOptions
|
|
55
|
-
export {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
export type {
|
|
56
|
+
AuthorizationServerMetadataCache,
|
|
57
|
+
CreateIdentityResolverOptions,
|
|
58
|
+
DidCache,
|
|
59
|
+
DpopNonceCache,
|
|
60
|
+
Fetch,
|
|
61
|
+
HandleCache,
|
|
62
|
+
HandleResolver,
|
|
63
|
+
InternalStateData,
|
|
64
|
+
OAuthClientMetadata,
|
|
65
|
+
OAuthClientMetadataInput,
|
|
66
|
+
OAuthResponseMode,
|
|
67
|
+
ProtectedResourceMetadataCache,
|
|
68
|
+
RuntimeImplementation,
|
|
69
|
+
SessionHooks,
|
|
70
|
+
SessionStore,
|
|
71
|
+
StateStore,
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export
|
|
74
|
+
export { Key, Keyset }
|
|
75
|
+
|
|
76
|
+
export type OAuthClientOptions = {
|
|
75
77
|
// Config
|
|
76
78
|
responseMode: OAuthResponseMode
|
|
77
79
|
clientMetadata: Readonly<OAuthClientMetadataInput>
|
|
@@ -102,9 +104,8 @@ export type OAuthClientOptions = CreateIdentityResolverOptions & {
|
|
|
102
104
|
// Services
|
|
103
105
|
runtimeImplementation: RuntimeImplementation
|
|
104
106
|
fetch?: Fetch
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export type OAuthClientEventMap = SessionEventMap
|
|
107
|
+
} & CreateIdentityResolverOptions &
|
|
108
|
+
SessionHooks
|
|
108
109
|
|
|
109
110
|
export type OAuthClientFetchMetadataOptions = {
|
|
110
111
|
clientId: OAuthClientIdDiscoverable
|
|
@@ -112,7 +113,7 @@ export type OAuthClientFetchMetadataOptions = {
|
|
|
112
113
|
signal?: AbortSignal
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
export class OAuthClient
|
|
116
|
+
export class OAuthClient {
|
|
116
117
|
static async fetchMetadata({
|
|
117
118
|
clientId,
|
|
118
119
|
fetch = globalThis.fetch,
|
|
@@ -181,8 +182,6 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
181
182
|
keyset,
|
|
182
183
|
} = options
|
|
183
184
|
|
|
184
|
-
super()
|
|
185
|
-
|
|
186
185
|
this.keyset = keyset
|
|
187
186
|
? keyset instanceof Keyset
|
|
188
187
|
? keyset
|
|
@@ -215,21 +214,13 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
215
214
|
dpopNonceCache,
|
|
216
215
|
)
|
|
217
216
|
|
|
217
|
+
this.stateStore = stateStore
|
|
218
218
|
this.sessionGetter = new SessionGetter(
|
|
219
219
|
sessionStore,
|
|
220
220
|
this.serverFactory,
|
|
221
221
|
this.runtime,
|
|
222
|
+
options,
|
|
222
223
|
)
|
|
223
|
-
this.stateStore = stateStore
|
|
224
|
-
|
|
225
|
-
// Proxy sessionGetter events
|
|
226
|
-
for (const type of ['deleted', 'updated'] as const) {
|
|
227
|
-
this.sessionGetter.addEventListener(type, (event) => {
|
|
228
|
-
if (!this.dispatchCustomEvent(type, event.detail)) {
|
|
229
|
-
event.preventDefault()
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
224
|
}
|
|
234
225
|
|
|
235
226
|
// Exposed as public API for convenience
|
|
@@ -411,8 +402,7 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
411
402
|
|
|
412
403
|
const server = await this.serverFactory.fromIssuer(
|
|
413
404
|
stateData.iss,
|
|
414
|
-
|
|
415
|
-
stateData.authMethod ?? 'legacy',
|
|
405
|
+
stateData.authMethod,
|
|
416
406
|
stateData.dpopKey,
|
|
417
407
|
)
|
|
418
408
|
|
|
@@ -446,6 +436,15 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
446
436
|
stateData.verifier,
|
|
447
437
|
options?.redirect_uri ?? server.clientMetadata.redirect_uris[0],
|
|
448
438
|
)
|
|
439
|
+
|
|
440
|
+
// We revoke any existing session first to avoid leaving orphaned sessions
|
|
441
|
+
// on the AS.
|
|
442
|
+
try {
|
|
443
|
+
await this.revoke(tokenSet.sub)
|
|
444
|
+
} catch {
|
|
445
|
+
// No existing session, or failed to get it. This is fine.
|
|
446
|
+
}
|
|
447
|
+
|
|
449
448
|
try {
|
|
450
449
|
await this.sessionGetter.setStored(tokenSet.sub, {
|
|
451
450
|
dpopKey: stateData.dpopKey,
|
|
@@ -472,7 +471,7 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
472
471
|
* Load a stored session. This will refresh the token only if needed (about to
|
|
473
472
|
* expire) by default.
|
|
474
473
|
*
|
|
475
|
-
* @
|
|
474
|
+
* @see {@link SessionGetter.restore}
|
|
476
475
|
*/
|
|
477
476
|
async restore(
|
|
478
477
|
sub: string,
|
|
@@ -481,11 +480,8 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
481
480
|
// sub arg is lightly typed for convenience of library user
|
|
482
481
|
assertAtprotoDid(sub)
|
|
483
482
|
|
|
484
|
-
const {
|
|
485
|
-
|
|
486
|
-
authMethod = 'legacy',
|
|
487
|
-
tokenSet,
|
|
488
|
-
} = await this.sessionGetter.getSession(sub, refresh)
|
|
483
|
+
const { dpopKey, authMethod, tokenSet } =
|
|
484
|
+
await this.sessionGetter.getSession(sub, refresh)
|
|
489
485
|
|
|
490
486
|
try {
|
|
491
487
|
const server = await this.serverFactory.fromIssuer(
|
|
@@ -512,14 +508,15 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
|
|
|
512
508
|
// sub arg is lightly typed for convenience of library user
|
|
513
509
|
assertAtprotoDid(sub)
|
|
514
510
|
|
|
515
|
-
const {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
tokenSet,
|
|
519
|
-
} = await this.sessionGetter.get(sub, {
|
|
520
|
-
allowStale: true,
|
|
511
|
+
const res = await this.sessionGetter.getSession(sub, false).catch((err) => {
|
|
512
|
+
if (isExpectedSessionError(err)) return null
|
|
513
|
+
throw err
|
|
521
514
|
})
|
|
522
515
|
|
|
516
|
+
if (!res) return
|
|
517
|
+
|
|
518
|
+
const { dpopKey, authMethod, tokenSet } = res
|
|
519
|
+
|
|
523
520
|
// NOT using `;(await this.restore(sub, false)).signOut()` because we want
|
|
524
521
|
// the tokens to be deleted even if it was not possible to fetch the issuer
|
|
525
522
|
// data.
|
|
@@ -2,10 +2,7 @@ import { Key, Keyset } from '@atproto/jwk'
|
|
|
2
2
|
import { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'
|
|
3
3
|
import { Fetch } from '@atproto-labs/fetch'
|
|
4
4
|
import { GetCachedOptions } from './oauth-authorization-server-metadata-resolver.js'
|
|
5
|
-
import {
|
|
6
|
-
ClientAuthMethod,
|
|
7
|
-
negotiateClientAuthMethod,
|
|
8
|
-
} from './oauth-client-auth.js'
|
|
5
|
+
import { ClientAuthMethod } from './oauth-client-auth.js'
|
|
9
6
|
import { OAuthResolver } from './oauth-resolver.js'
|
|
10
7
|
import { DpopNonceCache, OAuthServerAgent } from './oauth-server-agent.js'
|
|
11
8
|
import { Runtime } from './runtime.js'
|
|
@@ -32,7 +29,7 @@ export class OAuthServerFactory {
|
|
|
32
29
|
*/
|
|
33
30
|
async fromIssuer(
|
|
34
31
|
issuer: string,
|
|
35
|
-
authMethod:
|
|
32
|
+
authMethod: ClientAuthMethod,
|
|
36
33
|
dpopKey: Key,
|
|
37
34
|
options?: GetCachedOptions,
|
|
38
35
|
) {
|
|
@@ -41,17 +38,6 @@ export class OAuthServerFactory {
|
|
|
41
38
|
options,
|
|
42
39
|
)
|
|
43
40
|
|
|
44
|
-
if (authMethod === 'legacy') {
|
|
45
|
-
// @NOTE Because we were previously not storing the authMethod in the
|
|
46
|
-
// session data, we provide a backwards compatible implementation by
|
|
47
|
-
// computing it here.
|
|
48
|
-
authMethod = negotiateClientAuthMethod(
|
|
49
|
-
serverMetadata,
|
|
50
|
-
this.clientMetadata,
|
|
51
|
-
this.keyset,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
return this.fromMetadata(serverMetadata, authMethod, dpopKey)
|
|
56
42
|
}
|
|
57
43
|
|
package/src/oauth-session.ts
CHANGED
|
@@ -58,10 +58,7 @@ export class OAuthSession {
|
|
|
58
58
|
* if, and only if, they are (about to be) expired. Defaults to `undefined`.
|
|
59
59
|
*/
|
|
60
60
|
protected async getTokenSet(refresh: boolean | 'auto'): Promise<TokenSet> {
|
|
61
|
-
const { tokenSet } = await this.sessionGetter.
|
|
62
|
-
noCache: refresh === true,
|
|
63
|
-
allowStale: refresh === false,
|
|
64
|
-
})
|
|
61
|
+
const { tokenSet } = await this.sessionGetter.getSession(this.sub, refresh)
|
|
65
62
|
|
|
66
63
|
return tokenSet
|
|
67
64
|
}
|
package/src/session-getter.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Key } from '@atproto/jwk'
|
|
|
3
3
|
import {
|
|
4
4
|
CachedGetter,
|
|
5
5
|
GetCachedOptions,
|
|
6
|
+
GetOptions,
|
|
6
7
|
SimpleStore,
|
|
7
8
|
} from '@atproto-labs/simple-store'
|
|
8
9
|
import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'
|
|
@@ -14,32 +15,35 @@ import { OAuthResponseError } from './oauth-response-error.js'
|
|
|
14
15
|
import { TokenSet } from './oauth-server-agent.js'
|
|
15
16
|
import { OAuthServerFactory } from './oauth-server-factory.js'
|
|
16
17
|
import { Runtime } from './runtime.js'
|
|
17
|
-
import {
|
|
18
|
+
import { combineSignals } from './util.js'
|
|
18
19
|
|
|
19
20
|
export type Session = {
|
|
20
21
|
dpopKey: Key
|
|
21
|
-
|
|
22
|
-
* Previous implementation of this lib did not define an `authMethod`
|
|
23
|
-
*/
|
|
24
|
-
authMethod?: ClientAuthMethod
|
|
22
|
+
authMethod: ClientAuthMethod
|
|
25
23
|
tokenSet: TokenSet
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export type SessionStore = SimpleStore<string, Session>
|
|
29
27
|
|
|
30
|
-
export type
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown
|
|
37
|
-
}
|
|
28
|
+
export type SessionHooks = {
|
|
29
|
+
onUpdate?: (sub: AtprotoDid, session: Session) => void
|
|
30
|
+
onDelete?: (
|
|
31
|
+
sub: AtprotoDid,
|
|
32
|
+
cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown,
|
|
33
|
+
) => void
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
export function isExpectedSessionError(err: unknown) {
|
|
37
|
+
return (
|
|
38
|
+
err instanceof TokenRefreshError ||
|
|
39
|
+
err instanceof TokenRevokedError ||
|
|
40
|
+
err instanceof TokenInvalidError ||
|
|
41
|
+
err instanceof AuthMethodUnsatisfiableError ||
|
|
42
|
+
// The stored session is invalid (e.g. missing properties) and cannot
|
|
43
|
+
// be used properly
|
|
44
|
+
err instanceof TypeError
|
|
45
|
+
)
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* There are several advantages to wrapping the sessionStore in a (single)
|
|
@@ -49,15 +53,14 @@ export type SessionEventListener<
|
|
|
49
53
|
* localStorage/indexedDB, will sync across multiple tabs (for a given sub).
|
|
50
54
|
*/
|
|
51
55
|
export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
52
|
-
private readonly eventTarget = new CustomEventTarget<SessionEventMap>()
|
|
53
|
-
|
|
54
56
|
constructor(
|
|
55
57
|
sessionStore: SessionStore,
|
|
56
58
|
serverFactory: OAuthServerFactory,
|
|
57
59
|
private readonly runtime: Runtime,
|
|
60
|
+
private readonly hooks: SessionHooks = {},
|
|
58
61
|
) {
|
|
59
62
|
super(
|
|
60
|
-
async (sub,
|
|
63
|
+
async (sub, { signal }, storedSession) => {
|
|
61
64
|
// There needs to be a previous session to be able to refresh. If
|
|
62
65
|
// storedSession is undefined, it means that the store does not contain
|
|
63
66
|
// a session for the given sub.
|
|
@@ -69,17 +72,15 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
69
72
|
// make sure an event is dispatched here if this occurs.
|
|
70
73
|
const msg = 'The session was deleted by another process'
|
|
71
74
|
const cause = new TokenRefreshError(sub, msg)
|
|
72
|
-
|
|
75
|
+
await hooks.onDelete?.call(null, sub, cause)
|
|
73
76
|
throw cause
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// concurrent access (which, normally, should not happen if a proper
|
|
80
|
-
// runtime lock was provided).
|
|
79
|
+
// @NOTE Throwing a TokenRefreshError (or any other error class defined
|
|
80
|
+
// in the deleteOnError options) will result in this.delStored() being
|
|
81
|
+
// called.
|
|
81
82
|
|
|
82
|
-
const { dpopKey, authMethod
|
|
83
|
+
const { dpopKey, authMethod, tokenSet } = storedSession
|
|
83
84
|
|
|
84
85
|
if (sub !== tokenSet.sub) {
|
|
85
86
|
// Fool-proofing (e.g. against invalid session storage)
|
|
@@ -90,16 +91,6 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
90
91
|
throw new TokenRefreshError(sub, 'No refresh token available')
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
// Since refresh tokens can only be used once, we might run into
|
|
94
|
-
// concurrency issues if multiple instances (e.g. browser tabs) are
|
|
95
|
-
// trying to refresh the same token simultaneously. The chances of this
|
|
96
|
-
// happening when multiple instances are started simultaneously is
|
|
97
|
-
// reduced by randomizing the expiry time (see isStale() below). The
|
|
98
|
-
// best solution is to use a mutex/lock to ensure that only one instance
|
|
99
|
-
// is refreshing the token at a time (runtime.usingLock) but that is not
|
|
100
|
-
// always possible. If no lock implementation is provided, we will use
|
|
101
|
-
// the store to check if a concurrent refresh occurred.
|
|
102
|
-
|
|
103
94
|
const server = await serverFactory.fromIssuer(
|
|
104
95
|
tokenSet.iss,
|
|
105
96
|
authMethod,
|
|
@@ -111,7 +102,7 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
111
102
|
// point. Any thrown error beyond this point will prevent the
|
|
112
103
|
// TokenGetter from obtaining, and storing, the new token set,
|
|
113
104
|
// effectively rendering the currently saved session unusable.
|
|
114
|
-
|
|
105
|
+
signal?.throwIfAborted()
|
|
115
106
|
|
|
116
107
|
try {
|
|
117
108
|
const newTokenSet = await server.refresh(tokenSet)
|
|
@@ -127,9 +118,16 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
127
118
|
authMethod: server.authMethod,
|
|
128
119
|
}
|
|
129
120
|
} catch (cause) {
|
|
130
|
-
//
|
|
131
|
-
// concurrency issues
|
|
132
|
-
//
|
|
121
|
+
// Since refresh tokens can only be used once, we might run into
|
|
122
|
+
// concurrency issues if multiple instances (e.g. browser tabs) are
|
|
123
|
+
// trying to refresh the same token simultaneously. The chances of
|
|
124
|
+
// this happening when multiple instances are started simultaneously
|
|
125
|
+
// is reduced by randomizing the expiry time (see isStale() below).
|
|
126
|
+
// The best solution is to use a mutex/lock to ensure that only one
|
|
127
|
+
// instance is refreshing the token at a time (runtime.usingLock) but
|
|
128
|
+
// that is not always possible. Let's try to recover from concurrency
|
|
129
|
+
// issues, or force the session to be deleted by throwing a
|
|
130
|
+
// TokenRefreshError.
|
|
133
131
|
if (
|
|
134
132
|
cause instanceof OAuthResponseError &&
|
|
135
133
|
cause.status === 400 &&
|
|
@@ -187,91 +185,63 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
187
185
|
30e3 * Math.random()
|
|
188
186
|
)
|
|
189
187
|
},
|
|
190
|
-
onStoreError: async (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Let the original error propagate
|
|
210
|
-
}
|
|
188
|
+
onStoreError: async (err, sub, { tokenSet, dpopKey, authMethod }) => {
|
|
189
|
+
// If the token data cannot be stored, let's revoke it
|
|
190
|
+
try {
|
|
191
|
+
const server = await serverFactory.fromIssuer(
|
|
192
|
+
tokenSet.iss,
|
|
193
|
+
authMethod,
|
|
194
|
+
dpopKey,
|
|
195
|
+
)
|
|
196
|
+
await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token)
|
|
197
|
+
} catch {
|
|
198
|
+
// At least we tried...
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Attempt to delete the session from the store. Note that this might
|
|
202
|
+
// fail if the store is not available, which is fine.
|
|
203
|
+
try {
|
|
204
|
+
await this.delStored(sub, err)
|
|
205
|
+
} catch {
|
|
206
|
+
// Ignore (better to propagate the original storage error)
|
|
211
207
|
}
|
|
212
208
|
|
|
213
209
|
throw err
|
|
214
210
|
},
|
|
215
|
-
deleteOnError:
|
|
216
|
-
err instanceof TokenRefreshError ||
|
|
217
|
-
err instanceof TokenRevokedError ||
|
|
218
|
-
err instanceof TokenInvalidError ||
|
|
219
|
-
err instanceof AuthMethodUnsatisfiableError,
|
|
211
|
+
deleteOnError: isExpectedSessionError,
|
|
220
212
|
},
|
|
221
213
|
)
|
|
222
214
|
}
|
|
223
215
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.eventTarget.addEventListener(type, callback, options)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
removeEventListener<T extends keyof SessionEventMap>(
|
|
233
|
-
type: T,
|
|
234
|
-
callback: SessionEventListener<T>,
|
|
235
|
-
options?: EventListenerOptions | boolean,
|
|
236
|
-
) {
|
|
237
|
-
this.eventTarget.removeEventListener(type, callback, options)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
dispatchEvent<T extends keyof SessionEventMap>(
|
|
241
|
-
type: T,
|
|
242
|
-
detail: SessionEventMap[T],
|
|
243
|
-
): boolean {
|
|
244
|
-
return this.eventTarget.dispatchCustomEvent(type, detail)
|
|
216
|
+
override async getStored(
|
|
217
|
+
sub: AtprotoDid,
|
|
218
|
+
options?: GetOptions,
|
|
219
|
+
): Promise<Session | undefined> {
|
|
220
|
+
return super.getStored(sub, options)
|
|
245
221
|
}
|
|
246
222
|
|
|
247
|
-
async setStored(sub:
|
|
223
|
+
override async setStored(sub: AtprotoDid, session: Session) {
|
|
248
224
|
// Prevent tampering with the stored value
|
|
249
225
|
if (sub !== session.tokenSet.sub) {
|
|
250
226
|
throw new TypeError('Token set does not match the expected sub')
|
|
251
227
|
}
|
|
252
228
|
await super.setStored(sub, session)
|
|
253
|
-
this.
|
|
229
|
+
await this.hooks.onUpdate?.call(null, sub, session)
|
|
254
230
|
}
|
|
255
231
|
|
|
256
232
|
override async delStored(sub: AtprotoDid, cause?: unknown): Promise<void> {
|
|
257
233
|
await super.delStored(sub, cause)
|
|
258
|
-
this.
|
|
234
|
+
await this.hooks.onDelete?.call(null, sub, cause)
|
|
259
235
|
}
|
|
260
236
|
|
|
261
237
|
/**
|
|
262
|
-
* @
|
|
263
|
-
*
|
|
264
|
-
* if they are expired. When `undefined`, the credentials will be refreshed
|
|
265
|
-
* if, and only if, they are (about to be) expired. Defaults to `undefined`.
|
|
238
|
+
* @deprecated Use {@link getSession} instead
|
|
239
|
+
* @internal (not really deprecated)
|
|
266
240
|
*/
|
|
267
|
-
async
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async get(sub: AtprotoDid, options?: GetCachedOptions): Promise<Session> {
|
|
241
|
+
override async get(
|
|
242
|
+
sub: AtprotoDid,
|
|
243
|
+
options?: GetCachedOptions,
|
|
244
|
+
): Promise<Session> {
|
|
275
245
|
const session = await this.runtime.usingLock(
|
|
276
246
|
`@atproto-oauth-client-${sub}`,
|
|
277
247
|
async () => {
|
|
@@ -295,4 +265,17 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
|
295
265
|
|
|
296
266
|
return session
|
|
297
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param refresh When `true`, the credentials will be refreshed even if they
|
|
271
|
+
* are not expired. When `false`, the credentials will not be refreshed even
|
|
272
|
+
* if they are expired. When `undefined`, the credentials will be refreshed
|
|
273
|
+
* if, and only if, they are (about to be) expired. Defaults to `undefined`.
|
|
274
|
+
*/
|
|
275
|
+
async getSession(sub: AtprotoDid, refresh: boolean | 'auto' = 'auto') {
|
|
276
|
+
return this.get(sub, {
|
|
277
|
+
noCache: refresh === true,
|
|
278
|
+
allowStale: refresh === false,
|
|
279
|
+
})
|
|
280
|
+
}
|
|
298
281
|
}
|
package/src/state-store.ts
CHANGED
|
@@ -5,10 +5,20 @@ import { ClientAuthMethod } from './oauth-client-auth.js'
|
|
|
5
5
|
export type InternalStateData = {
|
|
6
6
|
iss: string
|
|
7
7
|
dpopKey: Key
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
verifier?: string
|
|
8
|
+
authMethod: ClientAuthMethod
|
|
9
|
+
verifier: string
|
|
11
10
|
appState?: string
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A store pending oauth authorization flows. The key is the "state" parameter
|
|
15
|
+
* used in the authorization request, and the value is an object containing the
|
|
16
|
+
* necessary information to complete the flow once the user is redirected back
|
|
17
|
+
* to the client.
|
|
18
|
+
*
|
|
19
|
+
* @note The data stored in this store is typically short-lived. It should be
|
|
20
|
+
* automatically cleared after a certain period of time (e.g. 1 hour) to prevent
|
|
21
|
+
* the store from growing indefinitely. It is up to the implementation to
|
|
22
|
+
* implement this cleanup mechanism.
|
|
23
|
+
*/
|
|
14
24
|
export type StateStore = SimpleStore<string, InternalStateData>
|
package/src/util.ts
CHANGED
|
@@ -7,73 +7,6 @@ export function contentMime(headers: Headers): string | undefined {
|
|
|
7
7
|
return headers.get('content-type')?.split(';')[0]!.trim()
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Ponyfill for `CustomEvent` constructor.
|
|
12
|
-
*/
|
|
13
|
-
export const CustomEvent: typeof globalThis.CustomEvent =
|
|
14
|
-
globalThis.CustomEvent ??
|
|
15
|
-
(() => {
|
|
16
|
-
class CustomEvent<T> extends Event {
|
|
17
|
-
#detail: T | null
|
|
18
|
-
constructor(type: string, options?: CustomEventInit<T>) {
|
|
19
|
-
if (!arguments.length) throw new TypeError('type argument is required')
|
|
20
|
-
super(type, options)
|
|
21
|
-
this.#detail = options?.detail ?? null
|
|
22
|
-
}
|
|
23
|
-
get detail() {
|
|
24
|
-
return this.#detail
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
Object.defineProperties(CustomEvent.prototype, {
|
|
29
|
-
[Symbol.toStringTag]: {
|
|
30
|
-
writable: false,
|
|
31
|
-
enumerable: false,
|
|
32
|
-
configurable: true,
|
|
33
|
-
value: 'CustomEvent',
|
|
34
|
-
},
|
|
35
|
-
detail: {
|
|
36
|
-
enumerable: true,
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return CustomEvent
|
|
41
|
-
})()
|
|
42
|
-
|
|
43
|
-
export class CustomEventTarget<EventDetailMap extends Record<string, unknown>> {
|
|
44
|
-
readonly eventTarget = new EventTarget()
|
|
45
|
-
|
|
46
|
-
addEventListener<T extends Extract<keyof EventDetailMap, string>>(
|
|
47
|
-
type: T,
|
|
48
|
-
callback: (event: CustomEvent<EventDetailMap[T]>) => void,
|
|
49
|
-
options?: AddEventListenerOptions | boolean,
|
|
50
|
-
): void {
|
|
51
|
-
this.eventTarget.addEventListener(type, callback as EventListener, options)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
removeEventListener<T extends Extract<keyof EventDetailMap, string>>(
|
|
55
|
-
type: T,
|
|
56
|
-
callback: (event: CustomEvent<EventDetailMap[T]>) => void,
|
|
57
|
-
options?: EventListenerOptions | boolean,
|
|
58
|
-
): void {
|
|
59
|
-
this.eventTarget.removeEventListener(
|
|
60
|
-
type,
|
|
61
|
-
callback as EventListener,
|
|
62
|
-
options,
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
dispatchCustomEvent<T extends Extract<keyof EventDetailMap, string>>(
|
|
67
|
-
type: T,
|
|
68
|
-
detail: EventDetailMap[T],
|
|
69
|
-
init?: EventInit,
|
|
70
|
-
): boolean {
|
|
71
|
-
return this.eventTarget.dispatchEvent(
|
|
72
|
-
new CustomEvent(type, { ...init, detail }),
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
10
|
export function combineSignals(
|
|
78
11
|
signals: readonly (AbortSignal | undefined)[],
|
|
79
12
|
): AbortController & Disposable {
|