@atproto/oauth-types 0.4.1 → 0.5.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/atproto-loopback-client-id.d.ts +14 -0
  3. package/dist/atproto-loopback-client-id.d.ts.map +1 -0
  4. package/dist/atproto-loopback-client-id.js +43 -0
  5. package/dist/atproto-loopback-client-id.js.map +1 -0
  6. package/dist/atproto-loopback-client-metadata.d.ts +8 -1
  7. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  8. package/dist/atproto-loopback-client-metadata.js +13 -4
  9. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  10. package/dist/atproto-loopback-client-redirect-uris.d.ts +2 -0
  11. package/dist/atproto-loopback-client-redirect-uris.d.ts.map +1 -0
  12. package/dist/atproto-loopback-client-redirect-uris.js +8 -0
  13. package/dist/atproto-loopback-client-redirect-uris.js.map +1 -0
  14. package/dist/atproto-oauth-scope.d.ts +12 -0
  15. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  16. package/dist/atproto-oauth-scope.js +27 -0
  17. package/dist/atproto-oauth-scope.js.map +1 -0
  18. package/dist/atproto-oauth-token-response.d.ts +106 -0
  19. package/dist/atproto-oauth-token-response.d.ts.map +1 -0
  20. package/dist/atproto-oauth-token-response.js +15 -0
  21. package/dist/atproto-oauth-token-response.js.map +1 -0
  22. package/dist/constants.js.map +1 -1
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/oauth-access-token.js.map +1 -1
  28. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
  29. package/dist/oauth-authorization-details.js.map +1 -1
  30. package/dist/oauth-authorization-request-jar.js.map +1 -1
  31. package/dist/oauth-authorization-request-par.d.ts +12 -12
  32. package/dist/oauth-authorization-request-par.js.map +1 -1
  33. package/dist/oauth-authorization-request-parameters.d.ts +12 -12
  34. package/dist/oauth-authorization-request-parameters.js.map +1 -1
  35. package/dist/oauth-authorization-request-query.d.ts +12 -12
  36. package/dist/oauth-authorization-request-query.js.map +1 -1
  37. package/dist/oauth-authorization-request-uri.js.map +1 -1
  38. package/dist/oauth-authorization-response-error.js.map +1 -1
  39. package/dist/oauth-authorization-server-metadata.js +2 -2
  40. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  41. package/dist/oauth-client-credentials-grant-token-request.js.map +1 -1
  42. package/dist/oauth-client-credentials.js.map +1 -1
  43. package/dist/oauth-client-id-discoverable.d.ts +1 -1
  44. package/dist/oauth-client-id-discoverable.js +1 -1
  45. package/dist/oauth-client-id-discoverable.js.map +1 -1
  46. package/dist/oauth-client-id-loopback.d.ts +24 -8
  47. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  48. package/dist/oauth-client-id-loopback.js +97 -60
  49. package/dist/oauth-client-id-loopback.js.map +1 -1
  50. package/dist/oauth-client-id.js.map +1 -1
  51. package/dist/oauth-client-metadata.d.ts +160 -1098
  52. package/dist/oauth-client-metadata.d.ts.map +1 -1
  53. package/dist/oauth-client-metadata.js.map +1 -1
  54. package/dist/oauth-code-challenge-method.js.map +1 -1
  55. package/dist/oauth-endpoint-auth-method.js.map +1 -1
  56. package/dist/oauth-endpoint-name.js.map +1 -1
  57. package/dist/oauth-grant-type.js.map +1 -1
  58. package/dist/oauth-introspection-response.js.map +1 -1
  59. package/dist/oauth-issuer-identifier.js.map +1 -1
  60. package/dist/oauth-par-response.d.ts +2 -2
  61. package/dist/oauth-par-response.js.map +1 -1
  62. package/dist/oauth-password-grant-token-request.js.map +1 -1
  63. package/dist/oauth-protected-resource-metadata.d.ts +1 -1
  64. package/dist/oauth-protected-resource-metadata.js +1 -1
  65. package/dist/oauth-protected-resource-metadata.js.map +1 -1
  66. package/dist/oauth-redirect-uri.d.ts +18 -6
  67. package/dist/oauth-redirect-uri.d.ts.map +1 -1
  68. package/dist/oauth-redirect-uri.js +18 -19
  69. package/dist/oauth-redirect-uri.js.map +1 -1
  70. package/dist/oauth-refresh-token-grant-token-request.js.map +1 -1
  71. package/dist/oauth-refresh-token.js.map +1 -1
  72. package/dist/oauth-request-uri.js.map +1 -1
  73. package/dist/oauth-response-mode.js.map +1 -1
  74. package/dist/oauth-response-type.js.map +1 -1
  75. package/dist/oauth-scope.d.ts +5 -3
  76. package/dist/oauth-scope.d.ts.map +1 -1
  77. package/dist/oauth-scope.js +11 -8
  78. package/dist/oauth-scope.js.map +1 -1
  79. package/dist/oauth-token-identification.js.map +1 -1
  80. package/dist/oauth-token-request.js.map +1 -1
  81. package/dist/oauth-token-response.js.map +1 -1
  82. package/dist/oauth-token-type.js.map +1 -1
  83. package/dist/oidc-authorization-error-response.js.map +1 -1
  84. package/dist/oidc-claims-parameter.js.map +1 -1
  85. package/dist/oidc-claims-properties.js.map +1 -1
  86. package/dist/oidc-entity-type.js.map +1 -1
  87. package/dist/oidc-userinfo.js.map +1 -1
  88. package/dist/uri.d.ts.map +1 -1
  89. package/dist/uri.js +44 -17
  90. package/dist/uri.js.map +1 -1
  91. package/dist/util.d.ts +11 -1
  92. package/dist/util.d.ts.map +1 -1
  93. package/dist/util.js +74 -5
  94. package/dist/util.js.map +1 -1
  95. package/package.json +3 -2
  96. package/src/atproto-loopback-client-id.ts +78 -0
  97. package/src/atproto-loopback-client-metadata.ts +33 -13
  98. package/src/atproto-loopback-client-redirect-uris.ts +4 -0
  99. package/src/atproto-oauth-scope.ts +34 -0
  100. package/src/atproto-oauth-token-response.ts +16 -0
  101. package/src/index.ts +5 -1
  102. package/src/oauth-authorization-server-metadata.ts +2 -2
  103. package/src/oauth-client-id-discoverable.ts +1 -1
  104. package/src/oauth-client-id-loopback.ts +131 -73
  105. package/src/oauth-protected-resource-metadata.ts +1 -1
  106. package/src/oauth-redirect-uri.ts +20 -26
  107. package/src/oauth-scope.ts +13 -7
  108. package/src/uri.ts +54 -18
  109. package/src/util.ts +85 -3
  110. package/tsconfig.build.tsbuildinfo +1 -1
@@ -3,7 +3,7 @@ import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
3
3
  import { webUriSchema } from './uri.js'
4
4
 
5
5
  /**
6
- * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#name-protected-resource-metadata-r}
6
+ * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2}
7
7
  */
8
8
  export const oauthProtectedResourceMetadataSchema = z.object({
9
9
  /**
@@ -6,19 +6,23 @@ import {
6
6
  privateUseUriSchema,
7
7
  } from './uri.js'
8
8
 
9
- export const oauthLoopbackRedirectURISchema = loopbackUriSchema.superRefine(
9
+ /**
10
+ * This is a {@link loopbackUriSchema} with the additional restriction that
11
+ * the hostname `localhost` is not allowed.
12
+ *
13
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.3 Loopback Redirect Considerations} RFC8252
14
+ *
15
+ * > While redirect URIs using localhost (i.e.,
16
+ * > "http://localhost:{port}/{path}") function similarly to loopback IP
17
+ * > redirects described in Section 7.3, the use of localhost is NOT
18
+ * > RECOMMENDED. Specifying a redirect URI with the loopback IP literal rather
19
+ * > than localhost avoids inadvertently listening on network interfaces other
20
+ * > than the loopback interface. It is also less susceptible to client-side
21
+ * > firewalls and misconfigured host name resolution on the user's device.
22
+ */
23
+ export const loopbackRedirectURISchema = loopbackUriSchema.superRefine(
10
24
  (value, ctx): value is Exclude<LoopbackUri, `http://localhost${string}`> => {
11
25
  if (value.startsWith('http://localhost')) {
12
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
13
- //
14
- // > While redirect URIs using localhost (i.e.,
15
- // > "http://localhost:{port}/{path}") function similarly to loopback IP
16
- // > redirects described in Section 7.3, the use of localhost is NOT
17
- // > RECOMMENDED. Specifying a redirect URI with the loopback IP literal
18
- // > rather than localhost avoids inadvertently listening on network
19
- // > interfaces other than the loopback interface. It is also less
20
- // > susceptible to client-side firewalls and misconfigured host name
21
- // > resolution on the user's device.
22
26
  ctx.addIssue({
23
27
  code: ZodIssueCode.custom,
24
28
  message:
@@ -30,27 +34,17 @@ export const oauthLoopbackRedirectURISchema = loopbackUriSchema.superRefine(
30
34
  return true
31
35
  },
32
36
  )
33
- export type OAuthLoopbackRedirectURI = TypeOf<
34
- typeof oauthLoopbackRedirectURISchema
35
- >
36
-
37
- export const oauthHttpsRedirectURISchema = httpsUriSchema
38
- export type OAuthHttpsRedirectURI = TypeOf<typeof oauthHttpsRedirectURISchema>
37
+ export type LoopbackRedirectURI = TypeOf<typeof loopbackRedirectURISchema>
39
38
 
40
- export const oauthPrivateUseRedirectURISchema = privateUseUriSchema
41
- export type OAuthPrivateUseRedirectURI = TypeOf<
42
- typeof oauthPrivateUseRedirectURISchema
39
+ export const oauthLoopbackClientRedirectUriSchema = loopbackRedirectURISchema
40
+ export type OAuthLoopbackRedirectURI = TypeOf<
41
+ typeof oauthLoopbackClientRedirectUriSchema
43
42
  >
44
43
 
45
44
  export const oauthRedirectUriSchema = z.union(
46
- [
47
- oauthLoopbackRedirectURISchema,
48
- oauthHttpsRedirectURISchema,
49
- oauthPrivateUseRedirectURISchema,
50
- ],
45
+ [loopbackRedirectURISchema, httpsUriSchema, privateUseUriSchema],
51
46
  {
52
47
  message: `URL must use the "https:" or "http:" protocol, or a private-use URI scheme (RFC 8252)`,
53
48
  },
54
49
  )
55
-
56
50
  export type OAuthRedirectUri = TypeOf<typeof oauthRedirectUriSchema>
@@ -1,15 +1,21 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ // scope = scope-token *( SP scope-token )
4
+ // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
5
+ export const OAUTH_SCOPE_REGEXP =
6
+ /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/
7
+
8
+ export const isOAuthScope = (input: string): boolean =>
9
+ OAUTH_SCOPE_REGEXP.test(input)
10
+
3
11
  /**
4
- * A space separated list of most non-control ASCII characters except backslash
5
- * and double quote.
12
+ * A (single) space separated list of non empty printable ASCII char string
13
+ * (except backslash and double quote).
6
14
  *
7
15
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1}
8
16
  */
9
- export const oauthScopeSchema = z
10
- .string()
11
- // scope = scope-token *( SP scope-token )
12
- // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
13
- .regex(/^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/)
17
+ export const oauthScopeSchema = z.string().refine(isOAuthScope, {
18
+ message: 'Invalid OAuth scope',
19
+ })
14
20
 
15
21
  export type OAuthScope = z.infer<typeof oauthScopeSchema>
package/src/uri.ts CHANGED
@@ -1,18 +1,10 @@
1
1
  import { TypeOf, ZodIssueCode, z } from 'zod'
2
- import { isHostnameIP, isLoopbackHost } from './util.js'
3
-
4
- const canParseUrl =
5
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
6
- URL.canParse ??
7
- // URL.canParse is not available in Node.js < 18.7.0
8
- ((urlStr: string): boolean => {
9
- try {
10
- new URL(urlStr)
11
- return true
12
- } catch {
13
- return false
14
- }
15
- })
2
+ import {
3
+ canParseUrl,
4
+ isHostnameIP,
5
+ isLocalHostname,
6
+ isLoopbackHost,
7
+ } from './util.js'
16
8
 
17
9
  /**
18
10
  * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
@@ -167,12 +159,56 @@ export const privateUseUriSchema = dangerousUriSchema.superRefine(
167
159
  return false
168
160
  }
169
161
 
170
- if (url.hostname) {
171
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
162
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
163
+ //
164
+ // > When choosing a URI scheme to associate with the app, apps MUST use a
165
+ // > URI scheme based on a domain name under their control, expressed in
166
+ // > reverse order
167
+ //
168
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
169
+ //
170
+ // > In addition to the collision-resistant properties, requiring a URI
171
+ // > scheme based on a domain name that is under the control of the app can
172
+ // > help to prove ownership in the event of a dispute where two apps claim
173
+ // > the same private-use URI scheme (where one app is acting maliciously).
174
+ //
175
+ // We can't check for ownership here (as there is no concept of proven
176
+ // ownership in a generic validation logic), besides excluding local domains
177
+ // as they can't be controlled/owned by the app.
178
+ //
179
+ // https://atproto.com/specs/oauth
180
+ //
181
+ // > Any custom scheme must match the `client_id` hostname in reverse-domain
182
+ // > order.
183
+ //
184
+ // This ATPROTO specific requirement cannot be enforced here, (as there is
185
+ // no concept of `client_id` in this context).
186
+
187
+ const uriScheme = url.protocol.slice(0, -1) // remove trailing ":"
188
+ const urlDomain = uriScheme.split('.').reverse().join('.')
189
+
190
+ if (isLocalHostname(urlDomain)) {
172
191
  ctx.addIssue({
173
192
  code: ZodIssueCode.custom,
174
- message:
175
- 'Private-use URI schemes must not include a hostname (only one "/" is allowed after the protocol, as per RFC 8252)',
193
+ message: `Private-use URI Scheme redirect URI must not be a local hostname`,
194
+ })
195
+ }
196
+
197
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
198
+ //
199
+ // > Following the requirements of Section 3.2 of [RFC3986], as there is no
200
+ // > naming authority for private-use URI scheme redirects, only a single
201
+ // > slash ("/") appears after the scheme component.
202
+ if (
203
+ url.href.startsWith(`${url.protocol}//`) ||
204
+ url.username ||
205
+ url.password ||
206
+ url.hostname ||
207
+ url.port
208
+ ) {
209
+ ctx.addIssue({
210
+ code: ZodIssueCode.custom,
211
+ message: `Private-Use URI Scheme must be in the form ${url.protocol}/<path> (as per RFC 8252)`,
176
212
  })
177
213
  return false
178
214
  }
package/src/util.ts CHANGED
@@ -1,3 +1,16 @@
1
+ export const canParseUrl =
2
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
3
+ URL.canParse?.bind(URL) ??
4
+ // URL.canParse is not available in Node.js < 18.7.0
5
+ ((urlStr: string): boolean => {
6
+ try {
7
+ new URL(urlStr)
8
+ return true
9
+ } catch {
10
+ return false
11
+ }
12
+ })
13
+
1
14
  export function isHostnameIP(hostname: string) {
2
15
  // IPv4
3
16
  if (hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) return true
@@ -14,9 +27,18 @@ export function isLoopbackHost(host: unknown): host is LoopbackHost {
14
27
  return host === 'localhost' || host === '127.0.0.1' || host === '[::1]'
15
28
  }
16
29
 
17
- export function isLoopbackUrl(input: URL | string): boolean {
18
- const url = typeof input === 'string' ? new URL(input) : input
19
- return isLoopbackHost(url.hostname)
30
+ export function isLocalHostname(hostname: string): boolean {
31
+ const parts = hostname.split('.')
32
+ if (parts.length < 2) return true
33
+
34
+ const tld = parts.at(-1)!.toLowerCase()
35
+ return (
36
+ tld === 'test' ||
37
+ tld === 'local' ||
38
+ tld === 'localhost' ||
39
+ tld === 'invalid' ||
40
+ tld === 'example'
41
+ )
20
42
  }
21
43
 
22
44
  export function safeUrl(input: URL | string): URL | null {
@@ -86,3 +108,63 @@ export const numberPreprocess = (val: unknown): unknown => {
86
108
  }
87
109
  return val
88
110
  }
111
+
112
+ /**
113
+ * Returns true if the two arrays contain the same elements, regardless of order
114
+ * or duplicates.
115
+ */
116
+ export function arrayEquivalent<T>(a: readonly T[], b: readonly T[]) {
117
+ if (a === b) return true
118
+ return a.every(includedIn, b) && b.every(includedIn, a)
119
+ }
120
+
121
+ export function includedIn<T>(this: readonly T[], item: T) {
122
+ return this.includes(item)
123
+ }
124
+
125
+ export function asArray<T>(
126
+ value: Iterable<T> | undefined,
127
+ ): undefined | readonly T[] {
128
+ if (value == null) return undefined
129
+ if (Array.isArray(value)) return value // already a (possibly readonly) array
130
+ return Array.from(value)
131
+ }
132
+
133
+ export type SpaceSeparatedValue<Value extends string> =
134
+ `${'' | `${string} `}${Value}${'' | ` ${string}`}`
135
+
136
+ export const isSpaceSeparatedValue = <Value extends string>(
137
+ value: Value,
138
+ input: string,
139
+ ): input is SpaceSeparatedValue<Value> => {
140
+ if (value.length === 0) throw new TypeError('Value cannot be empty')
141
+ if (value.includes(' ')) throw new TypeError('Value cannot contain spaces')
142
+
143
+ // Optimized version of:
144
+ // return input.split(' ').includes(value)
145
+
146
+ const inputLength = input.length
147
+ const valueLength = value.length
148
+
149
+ if (inputLength < valueLength) return false
150
+
151
+ let idx = input.indexOf(value)
152
+ let idxEnd: number
153
+
154
+ while (idx !== -1) {
155
+ idxEnd = idx + valueLength
156
+
157
+ if (
158
+ // at beginning or preceded by space
159
+ (idx === 0 || input.charCodeAt(idx - 1) === 32) &&
160
+ // at end or followed by space
161
+ (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)
162
+ ) {
163
+ return true
164
+ }
165
+
166
+ idx = input.indexOf(value, idxEnd + 1)
167
+ }
168
+
169
+ return false
170
+ }
@@ -1 +1 @@
1
- {"root":["./src/atproto-loopback-client-metadata.ts","./src/constants.ts","./src/index.ts","./src/oauth-access-token.ts","./src/oauth-authorization-code-grant-token-request.ts","./src/oauth-authorization-details.ts","./src/oauth-authorization-request-jar.ts","./src/oauth-authorization-request-par.ts","./src/oauth-authorization-request-parameters.ts","./src/oauth-authorization-request-query.ts","./src/oauth-authorization-request-uri.ts","./src/oauth-authorization-response-error.ts","./src/oauth-authorization-server-metadata.ts","./src/oauth-client-credentials-grant-token-request.ts","./src/oauth-client-credentials.ts","./src/oauth-client-id-discoverable.ts","./src/oauth-client-id-loopback.ts","./src/oauth-client-id.ts","./src/oauth-client-metadata.ts","./src/oauth-code-challenge-method.ts","./src/oauth-endpoint-auth-method.ts","./src/oauth-endpoint-name.ts","./src/oauth-grant-type.ts","./src/oauth-introspection-response.ts","./src/oauth-issuer-identifier.ts","./src/oauth-par-response.ts","./src/oauth-password-grant-token-request.ts","./src/oauth-protected-resource-metadata.ts","./src/oauth-redirect-uri.ts","./src/oauth-refresh-token-grant-token-request.ts","./src/oauth-refresh-token.ts","./src/oauth-request-uri.ts","./src/oauth-response-mode.ts","./src/oauth-response-type.ts","./src/oauth-scope.ts","./src/oauth-token-identification.ts","./src/oauth-token-request.ts","./src/oauth-token-response.ts","./src/oauth-token-type.ts","./src/oidc-authorization-error-response.ts","./src/oidc-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/oidc-userinfo.ts","./src/uri.ts","./src/util.ts"],"version":"5.8.2"}
1
+ {"root":["./src/atproto-loopback-client-id.ts","./src/atproto-loopback-client-metadata.ts","./src/atproto-loopback-client-redirect-uris.ts","./src/atproto-oauth-scope.ts","./src/atproto-oauth-token-response.ts","./src/constants.ts","./src/index.ts","./src/oauth-access-token.ts","./src/oauth-authorization-code-grant-token-request.ts","./src/oauth-authorization-details.ts","./src/oauth-authorization-request-jar.ts","./src/oauth-authorization-request-par.ts","./src/oauth-authorization-request-parameters.ts","./src/oauth-authorization-request-query.ts","./src/oauth-authorization-request-uri.ts","./src/oauth-authorization-response-error.ts","./src/oauth-authorization-server-metadata.ts","./src/oauth-client-credentials-grant-token-request.ts","./src/oauth-client-credentials.ts","./src/oauth-client-id-discoverable.ts","./src/oauth-client-id-loopback.ts","./src/oauth-client-id.ts","./src/oauth-client-metadata.ts","./src/oauth-code-challenge-method.ts","./src/oauth-endpoint-auth-method.ts","./src/oauth-endpoint-name.ts","./src/oauth-grant-type.ts","./src/oauth-introspection-response.ts","./src/oauth-issuer-identifier.ts","./src/oauth-par-response.ts","./src/oauth-password-grant-token-request.ts","./src/oauth-protected-resource-metadata.ts","./src/oauth-redirect-uri.ts","./src/oauth-refresh-token-grant-token-request.ts","./src/oauth-refresh-token.ts","./src/oauth-request-uri.ts","./src/oauth-response-mode.ts","./src/oauth-response-type.ts","./src/oauth-scope.ts","./src/oauth-token-identification.ts","./src/oauth-token-request.ts","./src/oauth-token-response.ts","./src/oauth-token-type.ts","./src/oidc-authorization-error-response.ts","./src/oidc-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/oidc-userinfo.ts","./src/uri.ts","./src/util.ts"],"version":"5.8.2"}