@atproto/oauth-client 0.7.5 → 0.7.7

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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # @atproto/oauth-client
2
2
 
3
+ ## 0.7.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
8
+
9
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
10
+
11
+ - Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
12
+ - @atproto-labs/simple-store-memory@0.2.3
13
+ - @atproto-labs/identity-resolver@0.4.3
14
+ - @atproto-labs/handle-resolver@0.4.4
15
+ - @atproto-labs/did-resolver@0.3.4
16
+ - @atproto-labs/simple-store@0.4.3
17
+ - @atproto/oauth-types@0.7.4
18
+ - @atproto-labs/fetch@0.3.3
19
+ - @atproto/jwk@0.7.3
20
+ - @atproto/xrpc@0.8.3
21
+ - @atproto/did@0.5.3
22
+
23
+ ## 0.7.6
24
+
25
+ ### Patch Changes
26
+
27
+ - [#5157](https://github.com/bluesky-social/atproto/pull/5157) [`8bcba69`](https://github.com/bluesky-social/atproto/commit/8bcba69cf1f02d09e07b51ce091918312029df63) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor syntax fix in tests
28
+
3
29
  ## 0.7.5
4
30
 
5
31
  ### Patch Changes
package/package.json CHANGED
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-client",
3
- "version": "0.7.5",
4
- "engines": {
5
- "node": ">=22"
6
- },
3
+ "version": "0.7.7",
7
4
  "license": "MIT",
8
5
  "description": "OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).",
9
6
  "keywords": [
@@ -18,6 +15,11 @@
18
15
  "url": "https://github.com/bluesky-social/atproto",
19
16
  "directory": "packages/oauth/oauth-client"
20
17
  },
18
+ "files": [
19
+ "./dist",
20
+ "./README.md",
21
+ "./CHANGELOG.md"
22
+ ],
21
23
  "type": "module",
22
24
  "exports": {
23
25
  ".": {
@@ -25,20 +27,23 @@
25
27
  "default": "./dist/index.js"
26
28
  }
27
29
  },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
28
33
  "dependencies": {
29
34
  "core-js": "^3",
30
35
  "multiformats": "^13.0.0",
31
36
  "zod": "^3.23.8",
32
- "@atproto-labs/did-resolver": "^0.3.3",
33
- "@atproto-labs/fetch": "^0.3.2",
34
- "@atproto-labs/identity-resolver": "^0.4.2",
35
- "@atproto-labs/simple-store-memory": "^0.2.2",
36
- "@atproto/did": "^0.5.2",
37
- "@atproto/jwk": "^0.7.2",
38
- "@atproto-labs/handle-resolver": "^0.4.3",
39
- "@atproto-labs/simple-store": "^0.4.2",
40
- "@atproto/oauth-types": "^0.7.3",
41
- "@atproto/xrpc": "^0.8.2"
37
+ "@atproto-labs/fetch": "^0.3.3",
38
+ "@atproto-labs/identity-resolver": "^0.4.3",
39
+ "@atproto/jwk": "^0.7.3",
40
+ "@atproto-labs/simple-store": "^0.4.3",
41
+ "@atproto-labs/handle-resolver": "^0.4.4",
42
+ "@atproto/did": "^0.5.3",
43
+ "@atproto-labs/simple-store-memory": "^0.2.3",
44
+ "@atproto-labs/did-resolver": "^0.3.4",
45
+ "@atproto/xrpc": "^0.8.3",
46
+ "@atproto/oauth-types": "^0.7.4"
42
47
  },
43
48
  "devDependencies": {
44
49
  "vitest": "^4.0.16"
package/src/constants.ts DELETED
@@ -1,4 +0,0 @@
1
- /**
2
- * Per ATProto spec (OpenID uses RS256)
3
- */
4
- export const FALLBACK_ALG = 'ES256'
package/src/core-js.d.ts DELETED
@@ -1 +0,0 @@
1
- declare module 'core-js/es/symbol/dispose.js'
@@ -1 +0,0 @@
1
- export class AuthMethodUnsatisfiableError extends Error {}
@@ -1,9 +0,0 @@
1
- export class TokenInvalidError extends Error {
2
- constructor(
3
- public readonly sub: string,
4
- message = `The session for "${sub}" is invalid`,
5
- options?: { cause?: unknown },
6
- ) {
7
- super(message, options)
8
- }
9
- }
@@ -1,9 +0,0 @@
1
- export class TokenRefreshError extends Error {
2
- constructor(
3
- public readonly sub: string,
4
- message: string,
5
- options?: { cause?: unknown },
6
- ) {
7
- super(message, options)
8
- }
9
- }
@@ -1,9 +0,0 @@
1
- export class TokenRevokedError extends Error {
2
- constructor(
3
- public readonly sub: string,
4
- message = `The session for "${sub}" was successfully revoked`,
5
- options?: { cause?: unknown },
6
- ) {
7
- super(message, options)
8
- }
9
- }
package/src/fetch-dpop.ts DELETED
@@ -1,250 +0,0 @@
1
- import { base64url } from 'multiformats/bases/base64'
2
- import { Key } from '@atproto/jwk'
3
- import { Fetch, FetchContext, cancelBody, peekJson } from '@atproto-labs/fetch'
4
- import { SimpleStore } from '@atproto-labs/simple-store'
5
-
6
- // "undefined" in non https environments or environments without crypto
7
- const subtle = globalThis.crypto?.subtle as SubtleCrypto | undefined
8
-
9
- const ReadableStream = globalThis.ReadableStream as
10
- | typeof globalThis.ReadableStream
11
- | undefined
12
-
13
- export type DpopFetchWrapperOptions<C = FetchContext> = {
14
- key: Key
15
- nonces: SimpleStore<string, string>
16
- supportedAlgs?: string[]
17
- sha256?: (input: string) => Promise<string>
18
-
19
- /**
20
- * Is the intended server an authorization server (true) or a resource server
21
- * (false)? Setting this may allow to avoid parsing the response body to
22
- * determine the dpop-nonce.
23
- *
24
- * @default undefined
25
- */
26
- isAuthServer?: boolean
27
- fetch?: Fetch<C>
28
- }
29
-
30
- export function dpopFetchWrapper<C = FetchContext>({
31
- key,
32
- // @TODO we should provide a default based on specs
33
- supportedAlgs,
34
- nonces,
35
- sha256 = typeof subtle !== 'undefined' ? subtleSha256 : undefined,
36
- isAuthServer,
37
- fetch = globalThis.fetch,
38
- }: DpopFetchWrapperOptions<C>): Fetch<C> {
39
- if (!sha256) {
40
- throw new TypeError(
41
- `crypto.subtle is not available in this environment. Please provide a sha256 function.`,
42
- )
43
- }
44
-
45
- // Throws if negotiation fails
46
- const alg = negotiateAlg(key, supportedAlgs)
47
-
48
- return async function (this: C, input, init) {
49
- const request: Request =
50
- init == null && input instanceof Request
51
- ? input
52
- : new Request(input, init)
53
-
54
- const authorizationHeader = request.headers.get('Authorization')
55
- const ath = authorizationHeader?.startsWith('DPoP ')
56
- ? await sha256(authorizationHeader.slice(5))
57
- : undefined
58
-
59
- const { origin } = new URL(request.url)
60
-
61
- const htm = request.method
62
- const htu = buildHtu(request.url)
63
-
64
- let initNonce: string | undefined
65
- try {
66
- initNonce = await nonces.get(origin)
67
- } catch {
68
- // Ignore get errors, we will just not send a nonce
69
- }
70
-
71
- const initProof = await buildProof(key, alg, htm, htu, initNonce, ath)
72
- request.headers.set('DPoP', initProof)
73
-
74
- const initResponse = await fetch.call(this, request)
75
-
76
- // Make sure the response body is consumed. Either by the caller (when the
77
- // response is returned), of if an error is thrown (catch block).
78
-
79
- const nextNonce = initResponse.headers.get('DPoP-Nonce')
80
- if (!nextNonce || nextNonce === initNonce) {
81
- // No nonce was returned or it is the same as the one we sent. No need to
82
- // update the nonce store, or retry the request.
83
- return initResponse
84
- }
85
-
86
- // Store the fresh nonce for future requests
87
- try {
88
- await nonces.set(origin, nextNonce)
89
- } catch {
90
- // Ignore set errors
91
- }
92
-
93
- const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer)
94
- if (!shouldRetry) {
95
- // Not a "use_dpop_nonce" error, so there is no need to retry
96
- return initResponse
97
- }
98
-
99
- // If the input stream was already consumed, we cannot retry the request. A
100
- // solution would be to clone() the request but that would bufferize the
101
- // entire stream in memory which can lead to memory starvation. Instead, we
102
- // will return the original response and let the calling code handle retries.
103
-
104
- if (input === request) {
105
- // The input request body was consumed. We cannot retry the request.
106
- return initResponse
107
- }
108
-
109
- if (ReadableStream && init?.body instanceof ReadableStream) {
110
- // The init body was consumed. We cannot retry the request.
111
- return initResponse
112
- }
113
-
114
- // We will now retry the request with the fresh nonce.
115
-
116
- // The initial response body must be consumed (see cancelBody's doc).
117
- await cancelBody(initResponse, 'log')
118
-
119
- const nextProof = await buildProof(key, alg, htm, htu, nextNonce, ath)
120
- const nextRequest = new Request(input, init)
121
- nextRequest.headers.set('DPoP', nextProof)
122
-
123
- const retryRequest = await fetch.call(this, nextRequest)
124
- const retryNonce = retryRequest.headers.get('DPoP-Nonce')
125
- if (!retryNonce || retryNonce === initNonce) {
126
- // No nonce was returned or it is the same as the one we sent. No need to
127
- // update the nonce store, or retry the request.
128
- return retryRequest
129
- }
130
-
131
- // Store the fresh nonce for future requests
132
- try {
133
- await nonces.set(origin, retryNonce)
134
- } catch {
135
- // Ignore set errors
136
- }
137
-
138
- return retryRequest
139
- }
140
- }
141
-
142
- /**
143
- * Strip query and fragment
144
- *
145
- * @see {@link https://www.rfc-editor.org/rfc/rfc9449.html#section-4.2-4.6}
146
- */
147
- function buildHtu(url: string): string {
148
- const fragmentIndex = url.indexOf('#')
149
- const queryIndex = url.indexOf('?')
150
-
151
- const end =
152
- fragmentIndex === -1
153
- ? queryIndex
154
- : queryIndex === -1
155
- ? fragmentIndex
156
- : Math.min(fragmentIndex, queryIndex)
157
-
158
- return end === -1 ? url : url.slice(0, end)
159
- }
160
-
161
- async function buildProof(
162
- key: Key,
163
- alg: string,
164
- htm: string,
165
- htu: string,
166
- nonce?: string,
167
- ath?: string,
168
- ) {
169
- const jwk = key.bareJwk
170
- if (!jwk) {
171
- throw new Error('Only asymmetric keys can be used as DPoP proofs')
172
- }
173
-
174
- const now = Math.floor(Date.now() / 1e3)
175
-
176
- return key.createJwt(
177
- // https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
178
- {
179
- alg,
180
- typ: 'dpop+jwt',
181
- jwk,
182
- },
183
- {
184
- iat: now,
185
- // Any collision will cause the request to be rejected by the server. no biggie.
186
- jti: Math.random().toString(36).slice(2),
187
- htm,
188
- htu,
189
- nonce,
190
- ath,
191
- },
192
- )
193
- }
194
-
195
- async function isUseDpopNonceError(
196
- response: Response,
197
- isAuthServer?: boolean,
198
- ): Promise<boolean> {
199
- // https://datatracker.ietf.org/doc/html/rfc6750#section-3
200
- // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
201
- if (isAuthServer === undefined || isAuthServer === false) {
202
- if (response.status === 401) {
203
- const wwwAuth = response.headers.get('WWW-Authenticate')
204
- if (wwwAuth?.startsWith('DPoP')) {
205
- return wwwAuth.includes('error="use_dpop_nonce"')
206
- }
207
- }
208
- }
209
-
210
- // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
211
- if (isAuthServer === undefined || isAuthServer === true) {
212
- if (response.status === 400) {
213
- try {
214
- const json = await peekJson(response, 10 * 1024)
215
- return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce'
216
- } catch {
217
- // Response too big (to be "use_dpop_nonce" error) or invalid JSON
218
- return false
219
- }
220
- }
221
- }
222
-
223
- return false
224
- }
225
-
226
- function negotiateAlg(key: Key, supportedAlgs: string[] | undefined): string {
227
- if (supportedAlgs) {
228
- // Use order of supportedAlgs as preference
229
- const alg = supportedAlgs.find((a) => key.algorithms.includes(a))
230
- if (alg) return alg
231
- } else {
232
- const [alg] = key.algorithms
233
- if (alg) return alg
234
- }
235
-
236
- throw new Error('Key does not match any alg supported by the server')
237
- }
238
-
239
- async function subtleSha256(input: string): Promise<string> {
240
- if (subtle == null) {
241
- throw new Error(
242
- `crypto.subtle is not available in this environment. Please provide a sha256 function.`,
243
- )
244
- }
245
-
246
- const bytes = new TextEncoder().encode(input)
247
- const digest = await subtle.digest('SHA-256', bytes)
248
- const digestBytes = new Uint8Array(digest)
249
- return base64url.baseEncode(digestBytes)
250
- }
@@ -1,25 +0,0 @@
1
- export {
2
- type CreateIdentityResolverOptions,
3
- createIdentityResolver,
4
- } from '@atproto-labs/identity-resolver'
5
-
6
- // @TODO Currently, the `OAuthClient`'s `IdentityResolver` is an instance of
7
- // `AtprotoIdentityResolver`, which implements the ATProto Identity resolution
8
- // protocol (did resolution + dns resolution). In the future, we may want to
9
- // allow using a different `IdentityResolver` implementation, such as one based
10
- // on XRPC's "com.atproto.identity.resolveIdentity" method. This would be
11
- // particularly useful for browser based clients, since DNS lookups are not
12
- // available in browser environments (and require an alternative implementation,
13
- // such as one based on the "com.atproto.identity.resolveHandle" XRPC method, or
14
- // using DNS-over-HTTPS). Once we decide to support such a behavior, the
15
- // `identityResolver` option below should be made mandatory, and the code bellow
16
- // should be removed from the @atproto/oauth-client package (and moved to the
17
- // environment specific package, such as @atproto/oauth-client-browser and
18
- // @atproto/oauth-client-node), allowing the dependency graph to be optimized
19
- // for the specific environment. When that is done, the
20
- // `AtprotoIdentityResolver` class should also be moved to its own package.
21
-
22
- // @TODO Once we move to a distinct implementation, we should also introduce a
23
- // caching layer for the `IdentityResolver` to avoid redundant resolution
24
- // requests. Once this is done, the caching layers for the did and handle
25
- // resolvers should be removed as they will be redundant.
package/src/index.ts DELETED
@@ -1,32 +0,0 @@
1
- import 'core-js/es/symbol/dispose.js'
2
-
3
- export * from '@atproto-labs/did-resolver'
4
- export {
5
- FetchError,
6
- FetchRequestError,
7
- FetchResponseError,
8
- } from '@atproto-labs/fetch'
9
- export * from '@atproto-labs/handle-resolver'
10
-
11
- export * from '@atproto/did'
12
- export * from '@atproto/jwk'
13
- export * from '@atproto/oauth-types'
14
-
15
- export * from './lock.js'
16
- export * from './oauth-authorization-server-metadata-resolver.js'
17
- export * from './oauth-callback-error.js'
18
- export * from './oauth-client.js'
19
- export * from './oauth-protected-resource-metadata-resolver.js'
20
- export * from './oauth-resolver-error.js'
21
- export * from './oauth-response-error.js'
22
- export * from './oauth-server-agent.js'
23
- export * from './oauth-server-factory.js'
24
- export * from './oauth-session.js'
25
- export * from './runtime-implementation.js'
26
- export * from './session-getter.js'
27
- export * from './state-store.js'
28
- export * from './types.js'
29
-
30
- export * from './errors/token-invalid-error.js'
31
- export * from './errors/token-refresh-error.js'
32
- export * from './errors/token-revoked-error.js'
package/src/lock.ts DELETED
@@ -1,33 +0,0 @@
1
- import { RuntimeLock } from './runtime-implementation.js'
2
-
3
- const locks = new Map<unknown, Promise<void>>()
4
-
5
- function acquireLocalLock(name: unknown): Promise<() => void> {
6
- return new Promise((resolveAcquire) => {
7
- const prev = locks.get(name) ?? Promise.resolve()
8
- const next = prev.then(() => {
9
- return new Promise<void>((resolveRelease) => {
10
- const release = () => {
11
- // Only delete the lock if it is still the current one
12
- if (locks.get(name) === next) locks.delete(name)
13
-
14
- resolveRelease()
15
- }
16
-
17
- resolveAcquire(release)
18
- })
19
- })
20
-
21
- locks.set(name, next)
22
- })
23
- }
24
-
25
- export const requestLocalLock: RuntimeLock = (name, fn) => {
26
- return acquireLocalLock(name).then(async (release) => {
27
- try {
28
- return await fn()
29
- } finally {
30
- release()
31
- }
32
- })
33
- }
@@ -1,120 +0,0 @@
1
- import {
2
- OAuthAuthorizationServerMetadata,
3
- oauthAuthorizationServerMetadataValidator,
4
- oauthIssuerIdentifierSchema,
5
- } from '@atproto/oauth-types'
6
- import {
7
- Fetch,
8
- FetchResponseError,
9
- bindFetch,
10
- cancelBody,
11
- } from '@atproto-labs/fetch'
12
- import {
13
- CachedGetter,
14
- GetCachedOptions,
15
- SimpleStore,
16
- } from '@atproto-labs/simple-store'
17
- import { contentMime } from './util.js'
18
-
19
- export type { GetCachedOptions, OAuthAuthorizationServerMetadata }
20
-
21
- export type AuthorizationServerMetadataCache = SimpleStore<
22
- string,
23
- OAuthAuthorizationServerMetadata
24
- >
25
-
26
- export type OAuthAuthorizationServerMetadataResolverConfig = {
27
- allowHttpIssuer?: boolean
28
- }
29
-
30
- /**
31
- * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
32
- */
33
- export class OAuthAuthorizationServerMetadataResolver extends CachedGetter<
34
- string,
35
- OAuthAuthorizationServerMetadata
36
- > {
37
- private readonly fetch: Fetch<unknown>
38
- private readonly allowHttpIssuer: boolean
39
-
40
- constructor(
41
- cache: AuthorizationServerMetadataCache,
42
- fetch?: Fetch,
43
- config?: OAuthAuthorizationServerMetadataResolverConfig,
44
- ) {
45
- super(async (issuer, options) => this.fetchMetadata(issuer, options), cache)
46
-
47
- this.fetch = bindFetch(fetch)
48
- this.allowHttpIssuer = config?.allowHttpIssuer === true
49
- }
50
-
51
- async get(
52
- input: URL | string,
53
- options?: GetCachedOptions,
54
- ): Promise<OAuthAuthorizationServerMetadata> {
55
- const issuer = oauthIssuerIdentifierSchema.parse(String(input))
56
- if (!this.allowHttpIssuer && issuer.startsWith('http:')) {
57
- throw new TypeError(
58
- 'Unsecure issuer URL protocol only allowed in development and test environments',
59
- )
60
- }
61
- return super.get(issuer, options)
62
- }
63
-
64
- private async fetchMetadata(
65
- issuer: string,
66
- options?: GetCachedOptions,
67
- ): Promise<OAuthAuthorizationServerMetadata> {
68
- const url = new URL(`/.well-known/oauth-authorization-server`, issuer)
69
- const request = new Request(url, {
70
- headers: { accept: 'application/json' },
71
- cache: options?.noCache ? 'no-cache' : undefined,
72
- signal: options?.signal,
73
- redirect: 'manual', // response must be 200 OK
74
- })
75
-
76
- const response = await this.fetch(request)
77
-
78
- // https://datatracker.ietf.org/doc/html/rfc8414#section-3.2
79
- if (response.status !== 200) {
80
- await cancelBody(response, 'log')
81
- throw await FetchResponseError.from(
82
- response,
83
- `Unexpected status code ${response.status} for "${url}"`,
84
- undefined,
85
- { cause: request },
86
- )
87
- }
88
-
89
- if (contentMime(response.headers) !== 'application/json') {
90
- await cancelBody(response, 'log')
91
- throw await FetchResponseError.from(
92
- response,
93
- `Unexpected content type for "${url}"`,
94
- undefined,
95
- { cause: request },
96
- )
97
- }
98
-
99
- const metadata = oauthAuthorizationServerMetadataValidator.parse(
100
- await response.json(),
101
- )
102
-
103
- // Validate the issuer (MIX-UP attacks)
104
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-mix-up-attacks
105
- // https://datatracker.ietf.org/doc/html/rfc8414#section-2
106
- if (metadata.issuer !== issuer) {
107
- throw new TypeError(`Invalid issuer ${metadata.issuer}`)
108
- }
109
-
110
- // ATPROTO requires client_id_metadata_document
111
- // https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
112
- if (metadata.client_id_metadata_document_supported !== true) {
113
- throw new TypeError(
114
- `Authorization server "${issuer}" does not support client_id_metadata_document`,
115
- )
116
- }
117
-
118
- return metadata
119
- }
120
- }
@@ -1,16 +0,0 @@
1
- export class OAuthCallbackError extends Error {
2
- static from(err: unknown, params: URLSearchParams, state?: string) {
3
- if (err instanceof OAuthCallbackError) return err
4
- const message = err instanceof Error ? err.message : undefined
5
- return new OAuthCallbackError(params, message, state, err)
6
- }
7
-
8
- constructor(
9
- public readonly params: URLSearchParams,
10
- message = params.get('error_description') || 'OAuth callback error',
11
- public readonly state?: string,
12
- cause?: unknown,
13
- ) {
14
- super(message, { cause })
15
- }
16
- }