@atproto/pds 0.4.33 → 0.4.35

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import assert from 'node:assert'
3
3
  import { DAY, HOUR, SECOND } from '@atproto/common'
4
+ import { Customization } from '@atproto/oauth-provider'
4
5
  import { ServerEnvironment } from './env'
5
6
 
6
7
  // off-config but still from env:
@@ -234,6 +235,54 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
234
235
 
235
236
  const crawlersCfg: ServerConfig['crawlers'] = env.crawlers ?? []
236
237
 
238
+ const fetchCfg: ServerConfig['fetch'] = {
239
+ disableSsrfProtection: env.fetchDisableSsrfProtection ?? false,
240
+ }
241
+
242
+ const oauthCfg: ServerConfig['oauth'] = entrywayCfg
243
+ ? {
244
+ issuer: entrywayCfg.url,
245
+ provider: false,
246
+ }
247
+ : {
248
+ issuer: serviceCfg.publicUrl,
249
+ provider: {
250
+ customization: {
251
+ name: env.serviceName ?? 'Personal PDS',
252
+ logo: env.logoUrl,
253
+ colors: {
254
+ primary: env.primaryColor,
255
+ error: env.errorColor,
256
+ },
257
+ links: [
258
+ {
259
+ title: 'Home',
260
+ href: env.homeUrl,
261
+ rel: 'bookmark',
262
+ },
263
+ {
264
+ title: 'Terms of Service',
265
+ href: env.termsOfServiceUrl,
266
+ rel: 'terms-of-service',
267
+ },
268
+ {
269
+ title: 'Privacy Policy',
270
+ href: env.privacyPolicyUrl,
271
+ rel: 'privacy-policy',
272
+ },
273
+ {
274
+ title: 'Support',
275
+ href: env.supportUrl,
276
+ rel: 'help',
277
+ },
278
+ ].filter(
279
+ (f): f is typeof f & { href: NonNullable<(typeof f)['href']> } =>
280
+ f.href != null,
281
+ ),
282
+ },
283
+ },
284
+ }
285
+
237
286
  return {
238
287
  service: serviceCfg,
239
288
  db: dbCfg,
@@ -251,6 +300,8 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
251
300
  redis: redisCfg,
252
301
  rateLimits: rateLimitsCfg,
253
302
  crawlers: crawlersCfg,
303
+ fetch: fetchCfg,
304
+ oauth: oauthCfg,
254
305
  }
255
306
  }
256
307
 
@@ -271,6 +322,8 @@ export type ServerConfig = {
271
322
  redis: RedisScratchConfig | null
272
323
  rateLimits: RateLimitsConfig
273
324
  crawlers: string[]
325
+ fetch: FetchConfig
326
+ oauth: OAuthConfig
274
327
  }
275
328
 
276
329
  export type ServiceConfig = {
@@ -337,6 +390,19 @@ export type EntrywayConfig = {
337
390
  plcRotationKey: string
338
391
  }
339
392
 
393
+ export type FetchConfig = {
394
+ disableSsrfProtection: boolean
395
+ }
396
+
397
+ export type OAuthConfig = {
398
+ issuer: string
399
+ provider:
400
+ | false
401
+ | {
402
+ customization: Customization
403
+ }
404
+ }
405
+
340
406
  export type InvitesConfig =
341
407
  | {
342
408
  required: true
package/src/config/env.ts CHANGED
@@ -6,14 +6,22 @@ export const readEnv = (): ServerEnvironment => {
6
6
  port: envInt('PDS_PORT'),
7
7
  hostname: envStr('PDS_HOSTNAME'),
8
8
  serviceDid: envStr('PDS_SERVICE_DID'),
9
+ serviceName: envStr('PDS_SERVICE_NAME'),
9
10
  version: envStr('PDS_VERSION'),
11
+ homeUrl: envStr('PDS_HOME_URL'),
12
+ logoUrl: envStr('PDS_LOGO_URL'),
10
13
  privacyPolicyUrl: envStr('PDS_PRIVACY_POLICY_URL'),
14
+ supportUrl: envStr('PDS_SUPPORT_URL'),
11
15
  termsOfServiceUrl: envStr('PDS_TERMS_OF_SERVICE_URL'),
12
16
  contactEmailAddress: envStr('PDS_CONTACT_EMAIL_ADDRESS'),
13
17
  acceptingImports: envBool('PDS_ACCEPTING_REPO_IMPORTS'),
14
18
  blobUploadLimit: envInt('PDS_BLOB_UPLOAD_LIMIT'),
15
19
  devMode: envBool('PDS_DEV_MODE'),
16
20
 
21
+ // branding
22
+ primaryColor: envStr('PDS_PRIMARY_COLOR'),
23
+ errorColor: envStr('PDS_ERROR_COLOR'),
24
+
17
25
  // database
18
26
  dataDirectory: envStr('PDS_DATA_DIRECTORY'),
19
27
  disableWalAutoCheckpoint: envBool('PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT'),
@@ -97,6 +105,7 @@ export const readEnv = (): ServerEnvironment => {
97
105
  crawlers: envList('PDS_CRAWLERS'),
98
106
 
99
107
  // secrets
108
+ dpopSecret: envStr('PDS_DPOP_SECRET'),
100
109
  jwtSecret: envStr('PDS_JWT_SECRET'),
101
110
  adminPassword: envStr('PDS_ADMIN_PASSWORD'),
102
111
 
@@ -106,6 +115,9 @@ export const readEnv = (): ServerEnvironment => {
106
115
  plcRotationKeyK256PrivateKeyHex: envStr(
107
116
  'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX',
108
117
  ),
118
+
119
+ // fetch
120
+ fetchDisableSsrfProtection: envBool('PDS_DISABLE_SSRF_PROTECTION'),
109
121
  }
110
122
  }
111
123
 
@@ -114,14 +126,22 @@ export type ServerEnvironment = {
114
126
  port?: number
115
127
  hostname?: string
116
128
  serviceDid?: string
129
+ serviceName?: string
117
130
  version?: string
131
+ homeUrl?: string
132
+ logoUrl?: string
118
133
  privacyPolicyUrl?: string
134
+ supportUrl?: string
119
135
  termsOfServiceUrl?: string
120
136
  contactEmailAddress?: string
121
137
  acceptingImports?: boolean
122
138
  blobUploadLimit?: number
123
139
  devMode?: boolean
124
140
 
141
+ // branding
142
+ primaryColor?: string
143
+ errorColor?: string
144
+
125
145
  // database
126
146
  dataDirectory?: string
127
147
  disableWalAutoCheckpoint?: boolean
@@ -203,10 +223,14 @@ export type ServerEnvironment = {
203
223
  crawlers?: string[]
204
224
 
205
225
  // secrets
226
+ dpopSecret?: string
206
227
  jwtSecret?: string
207
228
  adminPassword?: string
208
229
 
209
230
  // keys
210
231
  plcRotationKeyKmsKeyId?: string
211
232
  plcRotationKeyK256PrivateKeyHex?: string
233
+
234
+ // fetch
235
+ fetchDisableSsrfProtection?: boolean
212
236
  }
@@ -27,6 +27,7 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => {
27
27
  }
28
28
 
29
29
  return {
30
+ dpopSecret: env.dpopSecret,
30
31
  jwtSecret: env.jwtSecret,
31
32
  adminPassword: env.adminPassword,
32
33
  plcRotationKey,
@@ -34,6 +35,7 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => {
34
35
  }
35
36
 
36
37
  export type ServerSecrets = {
38
+ dpopSecret?: string
37
39
  jwtSecret: string
38
40
  adminPassword: string
39
41
  plcRotationKey: SigningKeyKms | SigningKeyMemory
package/src/context.ts CHANGED
@@ -12,12 +12,21 @@ import {
12
12
  RateLimiterOpts,
13
13
  createServiceAuthHeaders,
14
14
  } from '@atproto/xrpc-server'
15
+ import {
16
+ JoseKey,
17
+ Fetch,
18
+ safeFetchWrap,
19
+ OAuthVerifier,
20
+ } from '@atproto/oauth-provider'
21
+
15
22
  import { ServerConfig, ServerSecrets } from './config'
23
+ import { PdsOAuthProvider } from './oauth/provider'
16
24
  import {
17
25
  AuthVerifier,
18
26
  createPublicKeyObject,
19
27
  createSecretKeyObject,
20
28
  } from './auth-verifier'
29
+ import { fetchLogger } from './logger'
21
30
  import { ServerMailer } from './mailer'
22
31
  import { ModerationMailer } from './mailer/moderation'
23
32
  import { BlobStore } from '@atproto/repo'
@@ -50,6 +59,8 @@ export type AppContextOptions = {
50
59
  moderationAgent?: AtpAgent
51
60
  reportingAgent?: AtpAgent
52
61
  entrywayAgent?: AtpAgent
62
+ safeFetch: Fetch
63
+ authProvider?: PdsOAuthProvider
53
64
  authVerifier: AuthVerifier
54
65
  plcRotationKey: crypto.Keypair
55
66
  cfg: ServerConfig
@@ -74,7 +85,9 @@ export class AppContext {
74
85
  public moderationAgent: AtpAgent | undefined
75
86
  public reportingAgent: AtpAgent | undefined
76
87
  public entrywayAgent: AtpAgent | undefined
88
+ public safeFetch: Fetch
77
89
  public authVerifier: AuthVerifier
90
+ public authProvider?: PdsOAuthProvider
78
91
  public plcRotationKey: crypto.Keypair
79
92
  public cfg: ServerConfig
80
93
 
@@ -97,7 +110,9 @@ export class AppContext {
97
110
  this.moderationAgent = opts.moderationAgent
98
111
  this.reportingAgent = opts.reportingAgent
99
112
  this.entrywayAgent = opts.entrywayAgent
113
+ this.safeFetch = opts.safeFetch
100
114
  this.authVerifier = opts.authVerifier
115
+ this.authProvider = opts.authProvider
101
116
  this.plcRotationKey = opts.plcRotationKey
102
117
  this.cfg = opts.cfg
103
118
  }
@@ -206,7 +221,12 @@ export class AppContext {
206
221
  : undefined
207
222
 
208
223
  const jwtSecretKey = createSecretKeyObject(secrets.jwtSecret)
224
+ const jwtPublicKey = cfg.entryway
225
+ ? createPublicKeyObject(cfg.entryway.jwtPublicKeyHex)
226
+ : null
227
+
209
228
  const accountManager = new AccountManager(
229
+ backgroundQueue,
210
230
  cfg.db.accountDbLoc,
211
231
  jwtSecretKey,
212
232
  cfg.service.did,
@@ -214,20 +234,6 @@ export class AppContext {
214
234
  )
215
235
  await accountManager.migrateOrThrow()
216
236
 
217
- const jwtKey = cfg.entryway
218
- ? createPublicKeyObject(cfg.entryway.jwtPublicKeyHex)
219
- : jwtSecretKey
220
-
221
- const authVerifier = new AuthVerifier(accountManager, idResolver, {
222
- jwtKey, // @TODO support multiple keys?
223
- adminPass: secrets.adminPassword,
224
- dids: {
225
- pds: cfg.service.did,
226
- entryway: cfg.entryway?.did,
227
- modService: cfg.modService?.did,
228
- },
229
- })
230
-
231
237
  const plcRotationKey =
232
238
  secrets.plcRotationKey.provider === 'kms'
233
239
  ? await KmsKeypair.load({
@@ -250,6 +256,64 @@ export class AppContext {
250
256
  appviewCdnUrlPattern: cfg.bskyAppView?.cdnUrlPattern,
251
257
  })
252
258
 
259
+ // A fetch() function that protects against SSRF attacks, large responses &
260
+ // known bad domains. This function can safely be used to fetch user
261
+ // provided URLs (unless "disableSsrfProtection" is true, of course).
262
+ const safeFetch = safeFetchWrap({
263
+ allowHttp: cfg.fetch.disableSsrfProtection,
264
+ responseMaxSize: 512 * 1024, // 512kB
265
+ ssrfProtection: !cfg.fetch.disableSsrfProtection,
266
+ fetch: async (input, init) => {
267
+ const request = input instanceof Request ? input : null
268
+ const method = init?.method ?? request?.method ?? 'GET'
269
+ const uri = request?.url ?? String(input)
270
+ fetchLogger.debug({ method, uri }, 'fetch')
271
+ return globalThis.fetch(input, init)
272
+ },
273
+ })
274
+
275
+ const authProvider = cfg.oauth.provider
276
+ ? new PdsOAuthProvider({
277
+ issuer: cfg.oauth.issuer,
278
+ keyset: [
279
+ // Note: OpenID compatibility would require an RS256 private key in this list
280
+ await JoseKey.fromKeyLike(jwtSecretKey, undefined, 'HS256'),
281
+ ],
282
+ accountManager,
283
+ actorStore,
284
+ localViewer,
285
+ redis: redisScratch,
286
+ dpopSecret: secrets.dpopSecret,
287
+ customization: cfg.oauth.provider.customization,
288
+ safeFetch,
289
+ })
290
+ : undefined
291
+
292
+ const oauthVerifier: OAuthVerifier =
293
+ authProvider ?? // OAuthProvider extends OAuthVerifier
294
+ new OAuthVerifier({
295
+ issuer: cfg.oauth.issuer,
296
+ keyset: [await JoseKey.fromKeyLike(jwtPublicKey!, undefined, 'ES256K')],
297
+ dpopSecret: secrets.dpopSecret,
298
+ redis: redisScratch,
299
+ })
300
+
301
+ const authVerifier = new AuthVerifier(
302
+ accountManager,
303
+ idResolver,
304
+ oauthVerifier,
305
+ {
306
+ publicUrl: cfg.service.publicUrl,
307
+ jwtKey: jwtPublicKey ?? jwtSecretKey,
308
+ adminPass: secrets.adminPassword,
309
+ dids: {
310
+ pds: cfg.service.did,
311
+ entryway: cfg.entryway?.did,
312
+ modService: cfg.modService?.did,
313
+ },
314
+ },
315
+ )
316
+
253
317
  return new AppContext({
254
318
  actorStore,
255
319
  blobstore,
@@ -269,7 +333,9 @@ export class AppContext {
269
333
  moderationAgent,
270
334
  reportingAgent,
271
335
  entrywayAgent,
336
+ safeFetch,
272
337
  authVerifier,
338
+ authProvider,
273
339
  plcRotationKey,
274
340
  cfg,
275
341
  ...(overrides ?? {}),
package/src/db/cast.ts ADDED
@@ -0,0 +1,59 @@
1
+ export type DateISO = `${string}T${string}Z`
2
+ export const toDateISO = (date: Date): DateISO => date.toISOString() as DateISO
3
+ export const fromDateISO = (date: DateISO): Date => new Date(date)
4
+
5
+ export type Json = string
6
+ export const toJson = (obj: unknown): Json => {
7
+ const json = JSON.stringify(obj)
8
+ if (json === undefined) throw new TypeError('Input not JSONifyable')
9
+ return json as Json
10
+ }
11
+ export const fromJson = <T>(json: Json): T => {
12
+ try {
13
+ return JSON.parse(json) as T
14
+ } catch (cause) {
15
+ throw new TypeError('Database contains invalid JSON', { cause })
16
+ }
17
+ }
18
+
19
+ export type JsonArray = `[${string}]`
20
+ export const isJsonArray = (json: string): json is JsonArray =>
21
+ // Although the JSON in the DB should have been encoded using toJson,
22
+ // there should not be any leading or trailing whitespace. We will still trim
23
+ // the string to protect against any manual editing of the DB.
24
+ json.trimStart().startsWith('[') && json.trimEnd().endsWith(']')
25
+ export function assertJsonArray(json: string): asserts json is JsonArray {
26
+ if (!isJsonArray(json)) throw new TypeError('Not an Array')
27
+ }
28
+ export const toJsonArray = (obj: readonly unknown[]): JsonArray => {
29
+ const json = toJson(obj)
30
+ assertJsonArray(json)
31
+ return json as JsonArray
32
+ }
33
+ export const fromJsonArray = <T>(json: JsonArray): T[] => {
34
+ assertJsonArray(json)
35
+ return fromJson(json) as T[]
36
+ }
37
+
38
+ export type JsonObject = `{${string}}`
39
+ const isJsonObject = (json: string): json is JsonObject =>
40
+ // Although the JSON in the DB should have been encoded using toJson,
41
+ // there should not be any leading or trailing whitespace. We will still trim
42
+ // the string to protect against any manual editing of the DB.
43
+ json.trimStart().startsWith('{') && json.trimEnd().endsWith('}')
44
+ function assertJsonObject(json: string): asserts json is JsonObject {
45
+ if (!isJsonObject(json)) throw new TypeError('Not an Object')
46
+ }
47
+ export const toJsonObject = (
48
+ obj: Readonly<Record<string, unknown>>,
49
+ ): JsonObject => {
50
+ const json = toJson(obj)
51
+ assertJsonObject(json)
52
+ return json as JsonObject
53
+ }
54
+ export const fromJsonObject = <T extends Record<string, unknown>>(
55
+ json: JsonObject,
56
+ ): T => {
57
+ assertJsonObject(json)
58
+ return fromJson(json) as T
59
+ }
package/src/db/db.ts CHANGED
@@ -51,7 +51,7 @@ export class Database<Schema> {
51
51
  }
52
52
 
53
53
  async transactionNoRetry<T>(
54
- fn: (db: Database<Schema>) => Promise<T>,
54
+ fn: (db: Database<Schema>) => T | Promise<T>,
55
55
  ): Promise<T> {
56
56
  this.assertNotTransaction()
57
57
  const leakyTxPlugin = new LeakyTxPlugin()
@@ -60,22 +60,25 @@ export class Database<Schema> {
60
60
  .transaction()
61
61
  .execute(async (txn) => {
62
62
  const dbTxn = new Database(txn)
63
- const txRes = await fn(dbTxn)
64
- .catch(async (err) => {
65
- leakyTxPlugin.endTx()
66
- // ensure that all in-flight queries are flushed & the connection is open
67
- await dbTxn.db.getExecutor().provideConnection(async () => {})
68
- throw err
69
- })
70
- .finally(() => leakyTxPlugin.endTx())
71
- const hooks = dbTxn.commitHooks
72
- return { hooks, txRes }
63
+ try {
64
+ const txRes = await fn(dbTxn)
65
+ leakyTxPlugin.endTx()
66
+ const hooks = dbTxn.commitHooks
67
+ return { hooks, txRes }
68
+ } catch (err) {
69
+ leakyTxPlugin.endTx()
70
+ // ensure that all in-flight queries are flushed & the connection is open
71
+ await txn.getExecutor().provideConnection(async () => {})
72
+ throw err
73
+ }
73
74
  })
74
75
  hooks.map((hook) => hook())
75
76
  return txRes
76
77
  }
77
78
 
78
- async transaction<T>(fn: (db: Database<Schema>) => Promise<T>): Promise<T> {
79
+ async transaction<T>(
80
+ fn: (db: Database<Schema>) => T | Promise<T>,
81
+ ): Promise<T> {
79
82
  return retrySqlite(() => this.transactionNoRetry(fn))
80
83
  }
81
84
 
package/src/db/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './db'
2
+ export * from './cast'
2
3
  export * from './migrator'
3
4
  export * from './util'
package/src/error.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import { XRPCError } from '@atproto/xrpc-server'
2
2
  import { ErrorRequestHandler } from 'express'
3
3
  import { httpLogger as log } from './logger'
4
+ import { OAuthError } from '@atproto/oauth-provider'
4
5
 
5
6
  export const handler: ErrorRequestHandler = (err, _req, res, next) => {
6
7
  log.error(err, 'unexpected internal server error')
7
8
  if (res.headersSent) {
8
9
  return next(err)
9
10
  }
11
+
12
+ if (err instanceof OAuthError) {
13
+ res.status(err.status).json(err.toJSON())
14
+ return
15
+ }
16
+
10
17
  const serverError = XRPCError.fromError(err)
11
18
  res.status(serverError.type).json(serverError.payload)
12
19
  }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import events from 'events'
11
11
  import { Options as XrpcServerOptions } from '@atproto/xrpc-server'
12
12
  import { DAY, HOUR, MINUTE, SECOND } from '@atproto/common'
13
13
  import API from './api'
14
+ import * as authRoutes from './auth-routes'
14
15
  import * as basicRoutes from './basic-routes'
15
16
  import * as wellKnown from './well-known'
16
17
  import * as error from './error'
@@ -99,6 +100,7 @@ export class PDS {
99
100
 
100
101
  server = API(server, ctx)
101
102
 
103
+ app.use(authRoutes.createRouter(ctx))
102
104
  app.use(basicRoutes.createRouter(ctx))
103
105
  app.use(wellKnown.createRouter(ctx))
104
106
  app.use(server.xrpc.router)
@@ -120,8 +120,10 @@ import * as AppBskyGraphGetRelationships from './types/app/bsky/graph/getRelatio
120
120
  import * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor'
121
121
  import * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor'
122
122
  import * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList'
123
+ import * as AppBskyGraphMuteThread from './types/app/bsky/graph/muteThread'
123
124
  import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor'
124
125
  import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList'
126
+ import * as AppBskyGraphUnmuteThread from './types/app/bsky/graph/unmuteThread'
125
127
  import * as AppBskyLabelerGetServices from './types/app/bsky/labeler/getServices'
126
128
  import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount'
127
129
  import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications'
@@ -1604,6 +1606,17 @@ export class AppBskyGraphNS {
1604
1606
  return this._server.xrpc.method(nsid, cfg)
1605
1607
  }
1606
1608
 
1609
+ muteThread<AV extends AuthVerifier>(
1610
+ cfg: ConfigOf<
1611
+ AV,
1612
+ AppBskyGraphMuteThread.Handler<ExtractAuth<AV>>,
1613
+ AppBskyGraphMuteThread.HandlerReqCtx<ExtractAuth<AV>>
1614
+ >,
1615
+ ) {
1616
+ const nsid = 'app.bsky.graph.muteThread' // @ts-ignore
1617
+ return this._server.xrpc.method(nsid, cfg)
1618
+ }
1619
+
1607
1620
  unmuteActor<AV extends AuthVerifier>(
1608
1621
  cfg: ConfigOf<
1609
1622
  AV,
@@ -1625,6 +1638,17 @@ export class AppBskyGraphNS {
1625
1638
  const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore
1626
1639
  return this._server.xrpc.method(nsid, cfg)
1627
1640
  }
1641
+
1642
+ unmuteThread<AV extends AuthVerifier>(
1643
+ cfg: ConfigOf<
1644
+ AV,
1645
+ AppBskyGraphUnmuteThread.Handler<ExtractAuth<AV>>,
1646
+ AppBskyGraphUnmuteThread.HandlerReqCtx<ExtractAuth<AV>>
1647
+ >,
1648
+ ) {
1649
+ const nsid = 'app.bsky.graph.unmuteThread' // @ts-ignore
1650
+ return this._server.xrpc.method(nsid, cfg)
1651
+ }
1628
1652
  }
1629
1653
 
1630
1654
  export class AppBskyLabelerNS {
@@ -5102,6 +5102,9 @@ export const schemaDict = {
5102
5102
  type: 'string',
5103
5103
  format: 'at-uri',
5104
5104
  },
5105
+ threadMuted: {
5106
+ type: 'boolean',
5107
+ },
5105
5108
  replyDisabled: {
5106
5109
  type: 'boolean',
5107
5110
  },
@@ -7675,6 +7678,30 @@ export const schemaDict = {
7675
7678
  },
7676
7679
  },
7677
7680
  },
7681
+ AppBskyGraphMuteThread: {
7682
+ lexicon: 1,
7683
+ id: 'app.bsky.graph.muteThread',
7684
+ defs: {
7685
+ main: {
7686
+ type: 'procedure',
7687
+ description:
7688
+ 'Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth.',
7689
+ input: {
7690
+ encoding: 'application/json',
7691
+ schema: {
7692
+ type: 'object',
7693
+ required: ['root'],
7694
+ properties: {
7695
+ root: {
7696
+ type: 'string',
7697
+ format: 'at-uri',
7698
+ },
7699
+ },
7700
+ },
7701
+ },
7702
+ },
7703
+ },
7704
+ },
7678
7705
  AppBskyGraphUnmuteActor: {
7679
7706
  lexicon: 1,
7680
7707
  id: 'app.bsky.graph.unmuteActor',
@@ -7721,6 +7748,29 @@ export const schemaDict = {
7721
7748
  },
7722
7749
  },
7723
7750
  },
7751
+ AppBskyGraphUnmuteThread: {
7752
+ lexicon: 1,
7753
+ id: 'app.bsky.graph.unmuteThread',
7754
+ defs: {
7755
+ main: {
7756
+ type: 'procedure',
7757
+ description: 'Unmutes the specified thread. Requires auth.',
7758
+ input: {
7759
+ encoding: 'application/json',
7760
+ schema: {
7761
+ type: 'object',
7762
+ required: ['root'],
7763
+ properties: {
7764
+ root: {
7765
+ type: 'string',
7766
+ format: 'at-uri',
7767
+ },
7768
+ },
7769
+ },
7770
+ },
7771
+ },
7772
+ },
7773
+ },
7724
7774
  AppBskyLabelerDefs: {
7725
7775
  lexicon: 1,
7726
7776
  id: 'app.bsky.labeler.defs',
@@ -11105,8 +11155,10 @@ export const ids = {
11105
11155
  AppBskyGraphListitem: 'app.bsky.graph.listitem',
11106
11156
  AppBskyGraphMuteActor: 'app.bsky.graph.muteActor',
11107
11157
  AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList',
11158
+ AppBskyGraphMuteThread: 'app.bsky.graph.muteThread',
11108
11159
  AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor',
11109
11160
  AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList',
11161
+ AppBskyGraphUnmuteThread: 'app.bsky.graph.unmuteThread',
11110
11162
  AppBskyLabelerDefs: 'app.bsky.labeler.defs',
11111
11163
  AppBskyLabelerGetServices: 'app.bsky.labeler.getServices',
11112
11164
  AppBskyLabelerService: 'app.bsky.labeler.service',
@@ -49,6 +49,7 @@ export function validatePostView(v: unknown): ValidationResult {
49
49
  export interface ViewerState {
50
50
  repost?: string
51
51
  like?: string
52
+ threadMuted?: boolean
52
53
  replyDisabled?: boolean
53
54
  [k: string]: unknown
54
55
  }
@@ -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