@atproto/pds 0.4.33 → 0.4.35

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 (227) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/account-manager/db/migrations/004-oauth.d.ts +4 -0
  3. package/dist/account-manager/db/migrations/004-oauth.d.ts.map +1 -0
  4. package/dist/account-manager/db/migrations/004-oauth.js +106 -0
  5. package/dist/account-manager/db/migrations/004-oauth.js.map +1 -0
  6. package/dist/account-manager/db/migrations/index.d.ts +2 -0
  7. package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
  8. package/dist/account-manager/db/migrations/index.js +2 -0
  9. package/dist/account-manager/db/migrations/index.js.map +1 -1
  10. package/dist/account-manager/db/schema/authorization-request.d.ts +19 -0
  11. package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -0
  12. package/dist/account-manager/db/schema/authorization-request.js +5 -0
  13. package/dist/account-manager/db/schema/authorization-request.js.map +1 -0
  14. package/dist/account-manager/db/schema/device-account.d.ts +14 -0
  15. package/dist/account-manager/db/schema/device-account.d.ts.map +1 -0
  16. package/dist/account-manager/db/schema/device-account.js +5 -0
  17. package/dist/account-manager/db/schema/device-account.js.map +1 -0
  18. package/dist/account-manager/db/schema/device.d.ts +16 -0
  19. package/dist/account-manager/db/schema/device.d.ts.map +1 -0
  20. package/dist/account-manager/db/schema/device.js +5 -0
  21. package/dist/account-manager/db/schema/device.js.map +1 -0
  22. package/dist/account-manager/db/schema/index.d.ts +11 -1
  23. package/dist/account-manager/db/schema/index.d.ts.map +1 -1
  24. package/dist/account-manager/db/schema/token.d.ts +24 -0
  25. package/dist/account-manager/db/schema/token.d.ts.map +1 -0
  26. package/dist/account-manager/db/schema/token.js +5 -0
  27. package/dist/account-manager/db/schema/token.js.map +1 -0
  28. package/dist/account-manager/db/schema/used-refresh-token.d.ts +12 -0
  29. package/dist/account-manager/db/schema/used-refresh-token.d.ts.map +1 -0
  30. package/dist/account-manager/db/schema/used-refresh-token.js +5 -0
  31. package/dist/account-manager/db/schema/used-refresh-token.js.map +1 -0
  32. package/dist/account-manager/helpers/account.d.ts +27 -5
  33. package/dist/account-manager/helpers/account.d.ts.map +1 -1
  34. package/dist/account-manager/helpers/account.js +15 -14
  35. package/dist/account-manager/helpers/account.js.map +1 -1
  36. package/dist/account-manager/helpers/authorization-request.d.ts +12 -0
  37. package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -0
  38. package/dist/account-manager/helpers/authorization-request.js +59 -0
  39. package/dist/account-manager/helpers/authorization-request.js.map +1 -0
  40. package/dist/account-manager/helpers/device-account.d.ts +108 -0
  41. package/dist/account-manager/helpers/device-account.d.ts.map +1 -0
  42. package/dist/account-manager/helpers/device-account.js +82 -0
  43. package/dist/account-manager/helpers/device-account.js.map +1 -0
  44. package/dist/account-manager/helpers/device.d.ts +9 -0
  45. package/dist/account-manager/helpers/device.d.ts.map +1 -0
  46. package/dist/account-manager/helpers/device.js +32 -0
  47. package/dist/account-manager/helpers/device.js.map +1 -0
  48. package/dist/account-manager/helpers/token.d.ts +485 -0
  49. package/dist/account-manager/helpers/token.d.ts.map +1 -0
  50. package/dist/account-manager/helpers/token.js +123 -0
  51. package/dist/account-manager/helpers/token.js.map +1 -0
  52. package/dist/account-manager/helpers/used-refresh-token.d.ts +10 -0
  53. package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -0
  54. package/dist/account-manager/helpers/used-refresh-token.js +25 -0
  55. package/dist/account-manager/helpers/used-refresh-token.js.map +1 -0
  56. package/dist/account-manager/index.d.ts +36 -6
  57. package/dist/account-manager/index.d.ts.map +1 -1
  58. package/dist/account-manager/index.js +223 -22
  59. package/dist/account-manager/index.js.map +1 -1
  60. package/dist/actor-store/preference/reader.d.ts +2 -1
  61. package/dist/actor-store/preference/reader.d.ts.map +1 -1
  62. package/dist/actor-store/preference/reader.js +3 -1
  63. package/dist/actor-store/preference/reader.js.map +1 -1
  64. package/dist/actor-store/preference/transactor.d.ts +2 -1
  65. package/dist/actor-store/preference/transactor.d.ts.map +1 -1
  66. package/dist/actor-store/preference/transactor.js +7 -1
  67. package/dist/actor-store/preference/transactor.js.map +1 -1
  68. package/dist/actor-store/preference/util.d.ts +3 -0
  69. package/dist/actor-store/preference/util.d.ts.map +1 -0
  70. package/dist/actor-store/preference/util.js +12 -0
  71. package/dist/actor-store/preference/util.js.map +1 -0
  72. package/dist/actor-store/record/reader.d.ts +1 -1
  73. package/dist/api/app/bsky/actor/getPreferences.d.ts.map +1 -1
  74. package/dist/api/app/bsky/actor/getPreferences.js +1 -6
  75. package/dist/api/app/bsky/actor/getPreferences.js.map +1 -1
  76. package/dist/api/app/bsky/actor/putPreferences.d.ts.map +1 -1
  77. package/dist/api/app/bsky/actor/putPreferences.js +1 -1
  78. package/dist/api/app/bsky/actor/putPreferences.js.map +1 -1
  79. package/dist/api/app/bsky/util/resolver.d.ts +1 -1
  80. package/dist/api/com/atproto/server/createSession.d.ts.map +1 -1
  81. package/dist/api/com/atproto/server/createSession.js +7 -31
  82. package/dist/api/com/atproto/server/createSession.js.map +1 -1
  83. package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
  84. package/dist/api/com/atproto/server/deleteSession.js +14 -13
  85. package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
  86. package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
  87. package/dist/api/com/atproto/server/getSession.js +4 -2
  88. package/dist/api/com/atproto/server/getSession.js.map +1 -1
  89. package/dist/api/com/atproto/server/refreshSession.d.ts.map +1 -1
  90. package/dist/api/com/atproto/server/refreshSession.js +4 -2
  91. package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
  92. package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -1
  93. package/dist/api/com/atproto/sync/getRepoStatus.js +2 -1
  94. package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -1
  95. package/dist/api/com/atproto/sync/listRepos.js +2 -2
  96. package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
  97. package/dist/api/proxy.d.ts.map +1 -1
  98. package/dist/api/proxy.js +15 -2
  99. package/dist/api/proxy.js.map +1 -1
  100. package/dist/auth-routes.d.ts +4 -0
  101. package/dist/auth-routes.d.ts.map +1 -0
  102. package/dist/auth-routes.js +24 -0
  103. package/dist/auth-routes.js.map +1 -0
  104. package/dist/auth-verifier.d.ts +32 -11
  105. package/dist/auth-verifier.d.ts.map +1 -1
  106. package/dist/auth-verifier.js +238 -79
  107. package/dist/auth-verifier.js.map +1 -1
  108. package/dist/config/config.d.ts +12 -0
  109. package/dist/config/config.d.ts.map +1 -1
  110. package/dist/config/config.js +45 -0
  111. package/dist/config/config.js.map +1 -1
  112. package/dist/config/env.d.ts +8 -0
  113. package/dist/config/env.d.ts.map +1 -1
  114. package/dist/config/env.js +10 -0
  115. package/dist/config/env.js.map +1 -1
  116. package/dist/config/secrets.d.ts +1 -0
  117. package/dist/config/secrets.d.ts.map +1 -1
  118. package/dist/config/secrets.js +1 -0
  119. package/dist/config/secrets.js.map +1 -1
  120. package/dist/context.d.ts +6 -0
  121. package/dist/context.d.ts.map +1 -1
  122. package/dist/context.js +71 -13
  123. package/dist/context.js.map +1 -1
  124. package/dist/db/cast.d.ts +15 -0
  125. package/dist/db/cast.d.ts.map +1 -0
  126. package/dist/db/cast.js +66 -0
  127. package/dist/db/cast.js.map +1 -0
  128. package/dist/db/db.d.ts +2 -2
  129. package/dist/db/db.d.ts.map +1 -1
  130. package/dist/db/db.js +9 -7
  131. package/dist/db/db.js.map +1 -1
  132. package/dist/db/index.d.ts +1 -0
  133. package/dist/db/index.d.ts.map +1 -1
  134. package/dist/db/index.js +1 -0
  135. package/dist/db/index.js.map +1 -1
  136. package/dist/error.d.ts.map +1 -1
  137. package/dist/error.js +5 -0
  138. package/dist/error.js.map +1 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +2 -0
  141. package/dist/index.js.map +1 -1
  142. package/dist/lexicon/index.d.ts +4 -0
  143. package/dist/lexicon/index.d.ts.map +1 -1
  144. package/dist/lexicon/index.js +8 -0
  145. package/dist/lexicon/index.js.map +1 -1
  146. package/dist/lexicon/lexicons.d.ts +51 -0
  147. package/dist/lexicon/lexicons.d.ts.map +1 -1
  148. package/dist/lexicon/lexicons.js +51 -0
  149. package/dist/lexicon/lexicons.js.map +1 -1
  150. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  151. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  152. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  153. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts +29 -0
  154. package/dist/lexicon/types/app/bsky/graph/muteThread.d.ts.map +1 -0
  155. package/dist/lexicon/types/app/bsky/graph/muteThread.js +3 -0
  156. package/dist/lexicon/types/app/bsky/graph/muteThread.js.map +1 -0
  157. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts +29 -0
  158. package/dist/lexicon/types/app/bsky/graph/unmuteThread.d.ts.map +1 -0
  159. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js +3 -0
  160. package/dist/lexicon/types/app/bsky/graph/unmuteThread.js.map +1 -0
  161. package/dist/logger.d.ts +13 -11
  162. package/dist/logger.d.ts.map +1 -1
  163. package/dist/logger.js +80 -64
  164. package/dist/logger.js.map +1 -1
  165. package/dist/oauth/detailed-account-store.d.ts +27 -0
  166. package/dist/oauth/detailed-account-store.d.ts.map +1 -0
  167. package/dist/oauth/detailed-account-store.js +76 -0
  168. package/dist/oauth/detailed-account-store.js.map +1 -0
  169. package/dist/oauth/provider.d.ts +16 -0
  170. package/dist/oauth/provider.d.ts.map +1 -0
  171. package/dist/oauth/provider.js +45 -0
  172. package/dist/oauth/provider.js.map +1 -0
  173. package/dist/pipethrough.d.ts.map +1 -1
  174. package/dist/pipethrough.js.map +1 -1
  175. package/dist/sequencer/events.d.ts +2 -2
  176. package/example.env +21 -3
  177. package/package.json +9 -7
  178. package/src/account-manager/db/migrations/004-oauth.ts +122 -0
  179. package/src/account-manager/db/migrations/index.ts +2 -0
  180. package/src/account-manager/db/schema/authorization-request.ts +26 -0
  181. package/src/account-manager/db/schema/device-account.ts +15 -0
  182. package/src/account-manager/db/schema/device.ts +18 -0
  183. package/src/account-manager/db/schema/index.ts +15 -0
  184. package/src/account-manager/db/schema/token.ts +34 -0
  185. package/src/account-manager/db/schema/used-refresh-token.ts +13 -0
  186. package/src/account-manager/helpers/account.ts +16 -21
  187. package/src/account-manager/helpers/authorization-request.ts +82 -0
  188. package/src/account-manager/helpers/device-account.ts +135 -0
  189. package/src/account-manager/helpers/device.ts +45 -0
  190. package/src/account-manager/helpers/token.ts +185 -0
  191. package/src/account-manager/helpers/used-refresh-token.ts +30 -0
  192. package/src/account-manager/index.ts +325 -20
  193. package/src/actor-store/preference/reader.ts +8 -2
  194. package/src/actor-store/preference/transactor.ts +10 -0
  195. package/src/actor-store/preference/util.ts +8 -0
  196. package/src/api/app/bsky/actor/getPreferences.ts +2 -9
  197. package/src/api/app/bsky/actor/putPreferences.ts +5 -1
  198. package/src/api/com/atproto/server/createSession.ts +8 -44
  199. package/src/api/com/atproto/server/deleteSession.ts +14 -20
  200. package/src/api/com/atproto/server/getSession.ts +7 -2
  201. package/src/api/com/atproto/server/refreshSession.ts +6 -2
  202. package/src/api/com/atproto/sync/getRepoStatus.ts +3 -1
  203. package/src/api/com/atproto/sync/listRepos.ts +1 -1
  204. package/src/api/proxy.ts +18 -2
  205. package/src/auth-routes.ts +27 -0
  206. package/src/auth-verifier.ts +312 -92
  207. package/src/config/config.ts +66 -0
  208. package/src/config/env.ts +24 -0
  209. package/src/config/secrets.ts +2 -0
  210. package/src/context.ts +80 -14
  211. package/src/db/cast.ts +59 -0
  212. package/src/db/db.ts +15 -12
  213. package/src/db/index.ts +1 -0
  214. package/src/error.ts +7 -0
  215. package/src/index.ts +2 -0
  216. package/src/lexicon/index.ts +24 -0
  217. package/src/lexicon/lexicons.ts +52 -0
  218. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  219. package/src/lexicon/types/app/bsky/graph/muteThread.ts +38 -0
  220. package/src/lexicon/types/app/bsky/graph/unmuteThread.ts +38 -0
  221. package/src/logger.ts +83 -38
  222. package/src/oauth/detailed-account-store.ts +96 -0
  223. package/src/oauth/provider.ts +77 -0
  224. package/src/pipethrough.ts +3 -2
  225. package/tests/preferences.test.ts +67 -1
  226. package/tests/proxied/__snapshots__/feedgen.test.ts.snap +4 -1
  227. package/tests/proxied/__snapshots__/views.test.ts.snap +116 -38
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+
11
+ export interface QueryParams {}
12
+
13
+ export interface InputSchema {
14
+ root: string
15
+ [k: string]: unknown
16
+ }
17
+
18
+ export interface HandlerInput {
19
+ encoding: 'application/json'
20
+ body: InputSchema
21
+ }
22
+
23
+ export interface HandlerError {
24
+ status: number
25
+ message?: string
26
+ }
27
+
28
+ export type HandlerOutput = HandlerError | void
29
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
30
+ auth: HA
31
+ params: QueryParams
32
+ input: HandlerInput
33
+ req: express.Request
34
+ res: express.Response
35
+ }
36
+ export type Handler<HA extends HandlerAuth = never> = (
37
+ ctx: HandlerReqCtx<HA>,
38
+ ) => Promise<HandlerOutput> | HandlerOutput
package/src/logger.ts CHANGED
@@ -1,8 +1,6 @@
1
- import pino from 'pino'
1
+ import { stdSerializers } from 'pino'
2
2
  import pinoHttp from 'pino-http'
3
3
  import { subsystemLogger } from '@atproto/common'
4
- import * as jose from 'jose'
5
- import { parseBasicAuth } from './auth-verifier'
6
4
 
7
5
  export const dbLogger = subsystemLogger('pds:db')
8
6
  export const didCacheLogger = subsystemLogger('pds:did-cache')
@@ -13,44 +11,91 @@ export const mailerLogger = subsystemLogger('pds:mailer')
13
11
  export const labelerLogger = subsystemLogger('pds:labeler')
14
12
  export const crawlerLogger = subsystemLogger('pds:crawler')
15
13
  export const httpLogger = subsystemLogger('pds')
14
+ export const fetchLogger = subsystemLogger('pds:fetch')
15
+ export const oauthLogger = subsystemLogger('pds:oauth')
16
16
 
17
17
  export const loggerMiddleware = pinoHttp({
18
18
  logger: httpLogger,
19
19
  serializers: {
20
- err: (err) => {
21
- return {
22
- code: err?.code,
23
- message: err?.message,
24
- }
25
- },
26
- req: (req) => {
27
- const serialized = pino.stdSerializers.req(req)
28
- const authHeader = serialized.headers.authorization || ''
29
- let auth: string | undefined = undefined
30
- if (authHeader.startsWith('Bearer ')) {
31
- const token = authHeader.slice('Bearer '.length)
32
- const { sub } = jose.decodeJwt(token)
33
- if (sub) {
34
- auth = 'Bearer ' + sub
35
- } else {
36
- auth = 'Bearer Invalid'
37
- }
38
- }
39
- if (authHeader.startsWith('Basic ')) {
40
- const parsed = parseBasicAuth(authHeader)
41
- if (!parsed) {
42
- auth = 'Basic Invalid'
43
- } else {
44
- auth = 'Basic ' + parsed.username
45
- }
46
- }
47
- return {
48
- ...serialized,
49
- headers: {
50
- ...serialized.headers,
51
- authorization: auth,
52
- },
53
- }
54
- },
20
+ err: errSerializer,
21
+ req: reqSerializer,
55
22
  },
56
23
  })
24
+
25
+ function errSerializer(err: any) {
26
+ return {
27
+ code: err?.code,
28
+ message: err?.message,
29
+ }
30
+ }
31
+
32
+ function reqSerializer(req: any) {
33
+ const serialized = stdSerializers.req(req)
34
+ serialized.headers = obfuscateHeaders(serialized.headers)
35
+ return serialized
36
+ }
37
+
38
+ function obfuscateHeaders(headers: Record<string, string>) {
39
+ const obfuscatedHeaders: Record<string, string> = {}
40
+ for (const key in headers) {
41
+ if (key.toLowerCase() === 'authorization') {
42
+ obfuscatedHeaders[key] = obfuscateAuthHeader(headers[key])
43
+ } else if (key.toLowerCase() === 'dpop') {
44
+ obfuscatedHeaders[key] = obfuscateJws(headers[key]) || 'Invalid'
45
+ } else {
46
+ obfuscatedHeaders[key] = headers[key]
47
+ }
48
+ }
49
+ return obfuscatedHeaders
50
+ }
51
+
52
+ function obfuscateAuthHeader(authHeader: string): string {
53
+ // This is a hot path (runs on every request). Avoid using split() or regex.
54
+
55
+ const spaceIdx = authHeader.indexOf(' ')
56
+ if (spaceIdx === -1) return 'Invalid'
57
+
58
+ const type = authHeader.slice(0, spaceIdx)
59
+ switch (type.toLowerCase()) {
60
+ case 'bearer':
61
+ return `${type} ${obfuscateBearer(authHeader.slice(spaceIdx + 1))}`
62
+ case 'dpop':
63
+ return `${type} ${obfuscateJws(authHeader.slice(spaceIdx + 1)) || 'Invalid'}`
64
+ case 'basic':
65
+ return `${type} ${obfuscateBasic(authHeader.slice(spaceIdx + 1)) || 'Invalid'}`
66
+ default:
67
+ return `Invalid`
68
+ }
69
+ }
70
+
71
+ function obfuscateBasic(token: string): null | string {
72
+ if (!token) return null
73
+ const buffer = Buffer.from(token, 'base64')
74
+ if (!buffer.length) return null // Buffer.from will silently ignore invalid base64 chars
75
+ const authHeader = buffer.toString('utf8')
76
+ const colIdx = authHeader.indexOf(':')
77
+ if (colIdx === -1) return null
78
+ const username = authHeader.slice(0, colIdx)
79
+ return `${username}:***`
80
+ }
81
+
82
+ function obfuscateBearer(token: string): string {
83
+ return obfuscateJws(token) || obfuscateToken(token)
84
+ }
85
+
86
+ function obfuscateToken(token: string): string {
87
+ return token ? '***' : ''
88
+ }
89
+
90
+ function obfuscateJws(token: string): null | string {
91
+ const firstDot = token.indexOf('.')
92
+ if (firstDot === -1) return null
93
+
94
+ const secondDot = token.indexOf('.', firstDot + 1)
95
+ if (secondDot === -1) return null
96
+
97
+ if (token.indexOf('.', secondDot + 1) !== -1) return null
98
+
99
+ // Strip the signature
100
+ return token.slice(0, secondDot) + '.obfuscated'
101
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ AccountInfo,
3
+ AccountStore,
4
+ DeviceId,
5
+ LoginCredentials,
6
+ } from '@atproto/oauth-provider'
7
+
8
+ import { AccountManager } from '../account-manager/index'
9
+ import { ActorStore } from '../actor-store/index'
10
+ import { ProfileViewBasic } from '../lexicon/types/app/bsky/actor/defs'
11
+ import { LocalViewerCreator } from '../read-after-write/index'
12
+
13
+ /**
14
+ * Although the {@link AccountManager} class implements the {@link AccountStore}
15
+ * interface, the accounts it returns do not contain any profile information
16
+ * (display name, avatar, etc). This is due to the fact that the account manager
17
+ * does not have access to the account's repos. The {@link DetailedAccountStore}
18
+ * is a wrapper around the {@link AccountManager} that enriches the accounts
19
+ * with profile information using the account's repos through the
20
+ * {@link ActorStore}.
21
+ */
22
+ export class DetailedAccountStore implements AccountStore {
23
+ constructor(
24
+ private accountManager: AccountManager,
25
+ private actorStore: ActorStore,
26
+ private localViewer: LocalViewerCreator,
27
+ ) {}
28
+
29
+ private async getProfile(did: string): Promise<ProfileViewBasic | null> {
30
+ // TODO: Should we cache this?
31
+ return this.actorStore.read(did, async (actorStoreReader) => {
32
+ const localViewer = this.localViewer(actorStoreReader)
33
+ return localViewer.getProfileBasic()
34
+ })
35
+ }
36
+
37
+ private async enrichAccountInfo(
38
+ accountInfo: AccountInfo,
39
+ ): Promise<AccountInfo> {
40
+ const { account } = accountInfo
41
+ if (!account.picture || !account.name) {
42
+ const profile = await this.getProfile(account.sub)
43
+ if (profile) {
44
+ account.picture ||= profile.avatar
45
+ account.name ||= profile.displayName
46
+ }
47
+ }
48
+
49
+ return accountInfo
50
+ }
51
+
52
+ async authenticateAccount(
53
+ credentials: LoginCredentials,
54
+ deviceId: DeviceId,
55
+ ): Promise<AccountInfo | null> {
56
+ const accountInfo = await this.accountManager.authenticateAccount(
57
+ credentials,
58
+ deviceId,
59
+ )
60
+ if (!accountInfo) return null
61
+ return this.enrichAccountInfo(accountInfo)
62
+ }
63
+
64
+ async addAuthorizedClient(
65
+ deviceId: DeviceId,
66
+ sub: string,
67
+ clientId: string,
68
+ ): Promise<void> {
69
+ return this.accountManager.addAuthorizedClient(deviceId, sub, clientId)
70
+ }
71
+
72
+ async getDeviceAccount(
73
+ deviceId: DeviceId,
74
+ sub: string,
75
+ ): Promise<AccountInfo | null> {
76
+ const accountInfo = await this.accountManager.getDeviceAccount(
77
+ deviceId,
78
+ sub,
79
+ )
80
+ if (!accountInfo) return null
81
+ return this.enrichAccountInfo(accountInfo)
82
+ }
83
+
84
+ async listDeviceAccounts(deviceId: DeviceId): Promise<AccountInfo[]> {
85
+ const accountInfos = await this.accountManager.listDeviceAccounts(deviceId)
86
+ return Promise.all(
87
+ accountInfos.map(async (accountInfo) =>
88
+ this.enrichAccountInfo(accountInfo),
89
+ ),
90
+ )
91
+ }
92
+
93
+ async removeDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {
94
+ return this.accountManager.removeDeviceAccount(deviceId, sub)
95
+ }
96
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ AccessTokenType,
3
+ OAuthProvider,
4
+ OAuthProviderOptions,
5
+ } from '@atproto/oauth-provider'
6
+
7
+ import { AccountManager } from '../account-manager/index'
8
+ import { ActorStore } from '../actor-store/index'
9
+ import { oauthLogger } from '../logger'
10
+ import { LocalViewerCreator } from '../read-after-write/index'
11
+ import { DetailedAccountStore } from './detailed-account-store'
12
+
13
+ export type AuthProviderOptions = {
14
+ accountManager: AccountManager
15
+ actorStore: ActorStore
16
+ localViewer: LocalViewerCreator
17
+ } & Pick<
18
+ OAuthProviderOptions,
19
+ 'issuer' | 'redis' | 'keyset' | 'dpopSecret' | 'customization'
20
+ > &
21
+ Required<Pick<OAuthProviderOptions, 'safeFetch'>>
22
+
23
+ export class PdsOAuthProvider extends OAuthProvider {
24
+ constructor({
25
+ accountManager,
26
+ actorStore,
27
+ localViewer,
28
+ keyset,
29
+ redis,
30
+ dpopSecret,
31
+ issuer,
32
+ customization,
33
+ safeFetch,
34
+ }: AuthProviderOptions) {
35
+ super({
36
+ issuer,
37
+ keyset,
38
+ dpopSecret,
39
+ redis,
40
+ safeFetch,
41
+ customization,
42
+ metadata: {
43
+ // PdsOAuthProvider is used when the PDS is both an authorization server
44
+ // & resource server, in which case the issuer origin is also the
45
+ // resource server uri.
46
+ protected_resources: [new URL(issuer).origin],
47
+ },
48
+
49
+ accountStore: new DetailedAccountStore(
50
+ accountManager,
51
+ actorStore,
52
+ localViewer,
53
+ ),
54
+ requestStore: accountManager,
55
+ deviceStore: accountManager,
56
+ tokenStore: accountManager,
57
+
58
+ // If the PDS is both an authorization server & resource server (no
59
+ // entryway), there is no need to use JWTs as access tokens. Instead,
60
+ // the PDS can use tokenId as access tokens. This allows the PDS to
61
+ // always use up-to-date token data from the token store.
62
+ accessTokenType: AccessTokenType.id,
63
+
64
+ onClientInfo: (clientId) => ({
65
+ isFirstParty: clientId === 'https://bsky.app/',
66
+ // @TODO make client client list configurable:
67
+ isTrusted: undefined,
68
+ }),
69
+ })
70
+ }
71
+
72
+ createRouter() {
73
+ return this.httpHandler({
74
+ onError: (req, res, err, message) => oauthLogger.error({ err }, message),
75
+ })
76
+ }
77
+ }
@@ -22,7 +22,8 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
22
22
  const { url, aud } = await formatUrlAndAud(ctx, req)
23
23
  const auth = await accessStandard({ req })
24
24
  const headers = await formatHeaders(ctx, req, aud, auth.credentials.did)
25
- const body = stream.Readable.toWeb(req)
25
+ const body: webStream.ReadableStream<Uint8Array> =
26
+ stream.Readable.toWeb(req)
26
27
  const reqInit = formatReqInit(req, headers, body)
27
28
  const proxyRes = await makeRequest(url, reqInit)
28
29
  await pipeProxyRes(proxyRes, res)
@@ -113,7 +114,7 @@ export const formatHeaders = async (
113
114
  const formatReqInit = (
114
115
  req: express.Request,
115
116
  headers: Record<string, string>,
116
- body?: Uint8Array | ReadableStream,
117
+ body?: Uint8Array | webStream.ReadableStream<Uint8Array>,
117
118
  ): RequestInit => {
118
119
  if (req.method === 'GET') {
119
120
  return {
@@ -1,11 +1,13 @@
1
1
  import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
2
2
  import AtpAgent from '@atproto/api'
3
3
  import usersSeed from './seeds/users'
4
+ import { AuthScope } from '../dist/auth-verifier'
4
5
 
5
6
  describe('user preferences', () => {
6
7
  let network: TestNetworkNoAppView
7
8
  let agent: AtpAgent
8
9
  let sc: SeedClient
10
+ let appPassHeaders: { authorization: string }
9
11
 
10
12
  beforeAll(async () => {
11
13
  network = await TestNetworkNoAppView.create({
@@ -14,6 +16,16 @@ describe('user preferences', () => {
14
16
  agent = network.pds.getClient()
15
17
  sc = network.getSeedClient()
16
18
  await usersSeed(sc)
19
+ const appPass = await network.pds.ctx.accountManager.createAppPassword(
20
+ sc.dids.alice,
21
+ 'test app pass',
22
+ false,
23
+ )
24
+ const res = await agent.com.atproto.server.createSession({
25
+ identifier: sc.dids.alice,
26
+ password: appPass.password,
27
+ })
28
+ appPassHeaders = { authorization: `Bearer ${res.data.accessJwt}` }
17
29
  })
18
30
 
19
31
  afterAll(async () => {
@@ -46,6 +58,7 @@ describe('user preferences', () => {
46
58
  store.pref.putPreferences(
47
59
  [{ $type: 'com.atproto.server.defs#unknown' }],
48
60
  'com.atproto',
61
+ AuthScope.Access,
49
62
  ),
50
63
  )
51
64
  const { data } = await agent.api.app.bsky.actor.getPreferences(
@@ -96,7 +109,7 @@ describe('user preferences', () => {
96
109
  // Ensure other prefs were not clobbered
97
110
  const otherPrefs = await network.pds.ctx.actorStore.read(
98
111
  sc.dids.alice,
99
- (store) => store.pref.getPreferences('com.atproto'),
112
+ (store) => store.pref.getPreferences('com.atproto', AuthScope.Access),
100
113
  )
101
114
  expect(otherPrefs).toEqual([{ $type: 'com.atproto.server.defs#unknown' }])
102
115
  })
@@ -178,4 +191,57 @@ describe('user preferences', () => {
178
191
  'Input/preferences/1 must be an object which includes the "$type" property',
179
192
  )
180
193
  })
194
+
195
+ it('does not read permissioned preferences with an app password', async () => {
196
+ await agent.api.app.bsky.actor.putPreferences(
197
+ {
198
+ preferences: [
199
+ {
200
+ $type: 'app.bsky.actor.defs#personalDetailsPref',
201
+ birthDate: new Date().toISOString(),
202
+ },
203
+ ],
204
+ },
205
+ { headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },
206
+ )
207
+ const res = await agent.api.app.bsky.actor.getPreferences(
208
+ {},
209
+ { headers: appPassHeaders },
210
+ )
211
+ expect(res.data.preferences).toEqual([])
212
+ })
213
+
214
+ it('does not write permissioned preferences with an app password', async () => {
215
+ const tryPut = agent.api.app.bsky.actor.putPreferences(
216
+ {
217
+ preferences: [
218
+ {
219
+ $type: 'app.bsky.actor.defs#personalDetailsPref',
220
+ birthDate: new Date().toISOString(),
221
+ },
222
+ ],
223
+ },
224
+ { headers: appPassHeaders, encoding: 'application/json' },
225
+ )
226
+ await expect(tryPut).rejects.toThrow(
227
+ /Do not have authorization to set preferences/,
228
+ )
229
+ })
230
+
231
+ it('does not remove permissioned preferences with an app password', async () => {
232
+ await agent.api.app.bsky.actor.putPreferences(
233
+ {
234
+ preferences: [],
235
+ },
236
+ { headers: appPassHeaders, encoding: 'application/json' },
237
+ )
238
+ const res = await agent.api.app.bsky.actor.getPreferences(
239
+ {},
240
+ { headers: sc.getHeaders(sc.dids.alice) },
241
+ )
242
+ const scopedPref = res.data.preferences.find(
243
+ (pref) => pref.$type === 'app.bsky.actor.defs#personalDetailsPref',
244
+ )
245
+ expect(scopedPref).toBeDefined()
246
+ })
181
247
  })
@@ -59,7 +59,9 @@ Object {
59
59
  "replyCount": 0,
60
60
  "repostCount": 0,
61
61
  "uri": "record(0)",
62
- "viewer": Object {},
62
+ "viewer": Object {
63
+ "threadMuted": false,
64
+ },
63
65
  },
64
66
  },
65
67
  Object {
@@ -178,6 +180,7 @@ Object {
178
180
  "uri": "record(2)",
179
181
  "viewer": Object {
180
182
  "like": "record(8)",
183
+ "threadMuted": false,
181
184
  },
182
185
  },
183
186
  },