@atproto/pds 0.4.34 → 0.4.35
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +10 -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 +6 -4
- 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
|
+
}
|