@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
@@ -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