@atproto/pds 0.4.34 → 0.4.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/account-manager/db/migrations/004-oauth.d.ts +4 -0
- package/dist/account-manager/db/migrations/004-oauth.d.ts.map +1 -0
- package/dist/account-manager/db/migrations/004-oauth.js +106 -0
- package/dist/account-manager/db/migrations/004-oauth.js.map +1 -0
- package/dist/account-manager/db/migrations/index.d.ts +2 -0
- package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
- package/dist/account-manager/db/migrations/index.js +2 -0
- package/dist/account-manager/db/migrations/index.js.map +1 -1
- package/dist/account-manager/db/schema/authorization-request.d.ts +19 -0
- package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -0
- package/dist/account-manager/db/schema/authorization-request.js +5 -0
- package/dist/account-manager/db/schema/authorization-request.js.map +1 -0
- package/dist/account-manager/db/schema/device-account.d.ts +14 -0
- package/dist/account-manager/db/schema/device-account.d.ts.map +1 -0
- package/dist/account-manager/db/schema/device-account.js +5 -0
- package/dist/account-manager/db/schema/device-account.js.map +1 -0
- package/dist/account-manager/db/schema/device.d.ts +16 -0
- package/dist/account-manager/db/schema/device.d.ts.map +1 -0
- package/dist/account-manager/db/schema/device.js +5 -0
- package/dist/account-manager/db/schema/device.js.map +1 -0
- package/dist/account-manager/db/schema/index.d.ts +11 -1
- package/dist/account-manager/db/schema/index.d.ts.map +1 -1
- package/dist/account-manager/db/schema/token.d.ts +24 -0
- package/dist/account-manager/db/schema/token.d.ts.map +1 -0
- package/dist/account-manager/db/schema/token.js +5 -0
- package/dist/account-manager/db/schema/token.js.map +1 -0
- package/dist/account-manager/db/schema/used-refresh-token.d.ts +12 -0
- package/dist/account-manager/db/schema/used-refresh-token.d.ts.map +1 -0
- package/dist/account-manager/db/schema/used-refresh-token.js +5 -0
- package/dist/account-manager/db/schema/used-refresh-token.js.map +1 -0
- package/dist/account-manager/helpers/account.d.ts +27 -5
- package/dist/account-manager/helpers/account.d.ts.map +1 -1
- package/dist/account-manager/helpers/account.js +15 -14
- package/dist/account-manager/helpers/account.js.map +1 -1
- package/dist/account-manager/helpers/authorization-request.d.ts +12 -0
- package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -0
- package/dist/account-manager/helpers/authorization-request.js +59 -0
- package/dist/account-manager/helpers/authorization-request.js.map +1 -0
- package/dist/account-manager/helpers/device-account.d.ts +108 -0
- package/dist/account-manager/helpers/device-account.d.ts.map +1 -0
- package/dist/account-manager/helpers/device-account.js +82 -0
- package/dist/account-manager/helpers/device-account.js.map +1 -0
- package/dist/account-manager/helpers/device.d.ts +9 -0
- package/dist/account-manager/helpers/device.d.ts.map +1 -0
- package/dist/account-manager/helpers/device.js +32 -0
- package/dist/account-manager/helpers/device.js.map +1 -0
- package/dist/account-manager/helpers/token.d.ts +485 -0
- package/dist/account-manager/helpers/token.d.ts.map +1 -0
- package/dist/account-manager/helpers/token.js +123 -0
- package/dist/account-manager/helpers/token.js.map +1 -0
- package/dist/account-manager/helpers/used-refresh-token.d.ts +10 -0
- package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -0
- package/dist/account-manager/helpers/used-refresh-token.js +25 -0
- package/dist/account-manager/helpers/used-refresh-token.js.map +1 -0
- package/dist/account-manager/index.d.ts +36 -6
- package/dist/account-manager/index.d.ts.map +1 -1
- package/dist/account-manager/index.js +223 -22
- package/dist/account-manager/index.js.map +1 -1
- package/dist/actor-store/preference/reader.js.map +1 -1
- package/dist/actor-store/record/reader.d.ts +1 -1
- package/dist/api/app/bsky/util/resolver.d.ts +1 -1
- package/dist/api/com/atproto/server/createSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/createSession.js +7 -31
- package/dist/api/com/atproto/server/createSession.js.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.js +14 -13
- package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
- package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getSession.js +4 -2
- package/dist/api/com/atproto/server/getSession.js.map +1 -1
- package/dist/api/com/atproto/server/refreshSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/refreshSession.js +4 -2
- package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
- package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getRepoStatus.js +2 -1
- package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -1
- package/dist/api/com/atproto/sync/listRepos.js +2 -2
- package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
- package/dist/api/proxy.d.ts.map +1 -1
- package/dist/api/proxy.js +15 -2
- package/dist/api/proxy.js.map +1 -1
- package/dist/auth-routes.d.ts +4 -0
- package/dist/auth-routes.d.ts.map +1 -0
- package/dist/auth-routes.js +24 -0
- package/dist/auth-routes.js.map +1 -0
- package/dist/auth-verifier.d.ts +32 -11
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +238 -79
- package/dist/auth-verifier.js.map +1 -1
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +45 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +10 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/secrets.d.ts +1 -0
- package/dist/config/secrets.d.ts.map +1 -1
- package/dist/config/secrets.js +1 -0
- package/dist/config/secrets.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +71 -13
- package/dist/context.js.map +1 -1
- package/dist/db/cast.d.ts +15 -0
- package/dist/db/cast.d.ts.map +1 -0
- package/dist/db/cast.js +66 -0
- package/dist/db/cast.js.map +1 -0
- package/dist/db/db.d.ts +2 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +9 -7
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +5 -0
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +13 -11
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +80 -64
- package/dist/logger.js.map +1 -1
- package/dist/oauth/detailed-account-store.d.ts +27 -0
- package/dist/oauth/detailed-account-store.d.ts.map +1 -0
- package/dist/oauth/detailed-account-store.js +76 -0
- package/dist/oauth/detailed-account-store.js.map +1 -0
- package/dist/oauth/provider.d.ts +16 -0
- package/dist/oauth/provider.d.ts.map +1 -0
- package/dist/oauth/provider.js +45 -0
- package/dist/oauth/provider.js.map +1 -0
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/pipethrough.js.map +1 -1
- package/dist/sequencer/events.d.ts +2 -2
- package/example.env +21 -3
- package/package.json +9 -7
- package/src/account-manager/db/migrations/004-oauth.ts +122 -0
- package/src/account-manager/db/migrations/index.ts +2 -0
- package/src/account-manager/db/schema/authorization-request.ts +26 -0
- package/src/account-manager/db/schema/device-account.ts +15 -0
- package/src/account-manager/db/schema/device.ts +18 -0
- package/src/account-manager/db/schema/index.ts +15 -0
- package/src/account-manager/db/schema/token.ts +34 -0
- package/src/account-manager/db/schema/used-refresh-token.ts +13 -0
- package/src/account-manager/helpers/account.ts +16 -21
- package/src/account-manager/helpers/authorization-request.ts +82 -0
- package/src/account-manager/helpers/device-account.ts +135 -0
- package/src/account-manager/helpers/device.ts +45 -0
- package/src/account-manager/helpers/token.ts +185 -0
- package/src/account-manager/helpers/used-refresh-token.ts +30 -0
- package/src/account-manager/index.ts +325 -20
- package/src/actor-store/preference/reader.ts +1 -1
- package/src/api/com/atproto/server/createSession.ts +8 -44
- package/src/api/com/atproto/server/deleteSession.ts +14 -20
- package/src/api/com/atproto/server/getSession.ts +7 -2
- package/src/api/com/atproto/server/refreshSession.ts +6 -2
- package/src/api/com/atproto/sync/getRepoStatus.ts +3 -1
- package/src/api/com/atproto/sync/listRepos.ts +1 -1
- package/src/api/proxy.ts +18 -2
- package/src/auth-routes.ts +27 -0
- package/src/auth-verifier.ts +312 -92
- package/src/config/config.ts +66 -0
- package/src/config/env.ts +24 -0
- package/src/config/secrets.ts +2 -0
- package/src/context.ts +80 -14
- package/src/db/cast.ts +59 -0
- package/src/db/db.ts +15 -12
- package/src/db/index.ts +1 -0
- package/src/error.ts +7 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +83 -38
- package/src/oauth/detailed-account-store.ts +96 -0
- package/src/oauth/provider.ts +77 -0
- package/src/pipethrough.ts +3 -2
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
|
}
|
package/src/config/secrets.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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>(
|
|
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
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)
|
package/src/logger.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import
|
|
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:
|
|
21
|
-
|
|
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
|
+
}
|