@atproto/pds 0.4.59 → 0.4.61
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 +48 -0
- package/dist/account-manager/helpers/account.d.ts +1 -0
- package/dist/account-manager/helpers/account.d.ts.map +1 -1
- package/dist/account-manager/helpers/account.js +15 -1
- package/dist/account-manager/helpers/account.js.map +1 -1
- package/dist/account-manager/helpers/invite.d.ts +1 -1
- package/dist/account-manager/helpers/invite.d.ts.map +1 -1
- package/dist/account-manager/helpers/invite.js +20 -9
- package/dist/account-manager/helpers/invite.js.map +1 -1
- package/dist/account-manager/index.d.ts +2 -0
- package/dist/account-manager/index.d.ts.map +1 -1
- package/dist/account-manager/index.js +8 -1
- package/dist/account-manager/index.js.map +1 -1
- package/dist/api/app/bsky/actor/getProfile.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/getProfile.js +2 -9
- package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
- package/dist/api/app/bsky/actor/getProfiles.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/getProfiles.js +2 -6
- package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
- package/dist/api/app/bsky/feed/getActorLikes.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getActorLikes.js +2 -9
- package/dist/api/app/bsky/feed/getActorLikes.js.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.js +2 -9
- package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.js +2 -1
- package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
- package/dist/api/app/bsky/feed/getPostThread.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getPostThread.js +12 -14
- package/dist/api/app/bsky/feed/getPostThread.js.map +1 -1
- package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getTimeline.js +2 -6
- package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
- package/dist/api/com/atproto/admin/getAccountInfo.d.ts.map +1 -1
- package/dist/api/com/atproto/admin/getAccountInfo.js +6 -14
- package/dist/api/com/atproto/admin/getAccountInfo.js.map +1 -1
- package/dist/api/com/atproto/admin/getAccountInfos.d.ts +4 -0
- package/dist/api/com/atproto/admin/getAccountInfos.d.ts.map +1 -0
- package/dist/api/com/atproto/admin/getAccountInfos.js +32 -0
- package/dist/api/com/atproto/admin/getAccountInfos.js.map +1 -0
- package/dist/api/com/atproto/admin/index.d.ts.map +1 -1
- package/dist/api/com/atproto/admin/index.js +2 -0
- package/dist/api/com/atproto/admin/index.js.map +1 -1
- package/dist/api/com/atproto/admin/util.d.ts +17 -0
- package/dist/api/com/atproto/admin/util.d.ts.map +1 -1
- package/dist/api/com/atproto/admin/util.js +27 -1
- package/dist/api/com/atproto/admin/util.js.map +1 -1
- package/dist/api/com/atproto/repo/getRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/getRecord.js +2 -2
- package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
- package/dist/api/com/atproto/server/requestPasswordReset.js +1 -1
- package/dist/api/com/atproto/server/requestPasswordReset.js.map +1 -1
- package/dist/config/config.d.ts +17 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +11 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +9 -1
- package/dist/config/env.js.map +1 -1
- package/dist/context.d.ts +6 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +55 -11
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lexicon/index.d.ts +4 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +8 -0
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +118 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +135 -3
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/profile.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/actor/profile.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/profile.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +13 -2
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js +21 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/repo/getRecord.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/repo/getRecord.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/getRecords.d.ts +39 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRecords.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRecords.js +3 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRecords.js.map +1 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRepos.d.ts +39 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRepos.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRepos.js +3 -0
- package/dist/lexicon/types/tools/ozone/moderation/getRepos.js.map +1 -0
- package/dist/mailer/index.d.ts +1 -1
- package/dist/mailer/index.d.ts.map +1 -1
- package/dist/mailer/index.js.map +1 -1
- package/dist/mailer/templates/confirm-email.js +1 -1
- package/dist/mailer/templates/confirm-email.js.map +2 -2
- package/dist/mailer/templates/delete-account.js +1 -1
- package/dist/mailer/templates/delete-account.js.map +2 -2
- package/dist/mailer/templates/plc-operation.js +1 -1
- package/dist/mailer/templates/plc-operation.js.map +2 -2
- package/dist/mailer/templates/reset-password.js +1 -1
- package/dist/mailer/templates/reset-password.js.map +2 -2
- package/dist/mailer/templates/update-email.js +1 -1
- package/dist/mailer/templates/update-email.js.map +2 -2
- package/dist/pipethrough.d.ts +26 -26
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/pipethrough.js +360 -228
- package/dist/pipethrough.js.map +1 -1
- package/dist/read-after-write/util.d.ts +13 -5
- package/dist/read-after-write/util.d.ts.map +1 -1
- package/dist/read-after-write/util.js +37 -22
- package/dist/read-after-write/util.js.map +1 -1
- package/package.json +15 -14
- package/src/account-manager/helpers/account.ts +22 -0
- package/src/account-manager/helpers/invite.ts +19 -9
- package/src/account-manager/index.ts +13 -1
- package/src/api/app/bsky/actor/getProfile.ts +3 -17
- package/src/api/app/bsky/actor/getProfiles.ts +3 -15
- package/src/api/app/bsky/feed/getActorLikes.ts +3 -19
- package/src/api/app/bsky/feed/getAuthorFeed.ts +3 -17
- package/src/api/app/bsky/feed/getFeed.ts +3 -1
- package/src/api/app/bsky/feed/getPostThread.ts +16 -23
- package/src/api/app/bsky/feed/getTimeline.ts +3 -14
- package/src/api/com/atproto/admin/getAccountInfo.ts +6 -13
- package/src/api/com/atproto/admin/getAccountInfos.ts +33 -0
- package/src/api/com/atproto/admin/index.ts +2 -0
- package/src/api/com/atproto/admin/util.ts +38 -0
- package/src/api/com/atproto/repo/getRecord.ts +5 -2
- package/src/api/com/atproto/server/requestPasswordReset.ts +1 -1
- package/src/config/config.ts +31 -1
- package/src/config/env.ts +22 -2
- package/src/context.ts +62 -17
- package/src/index.ts +1 -0
- package/src/lexicon/index.ts +24 -0
- package/src/lexicon/lexicons.ts +137 -3
- package/src/lexicon/types/app/bsky/actor/defs.ts +2 -0
- package/src/lexicon/types/app/bsky/actor/profile.ts +1 -0
- package/src/lexicon/types/app/bsky/feed/defs.ts +38 -2
- package/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +1 -0
- package/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +2 -0
- package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +2 -0
- package/src/lexicon/types/com/atproto/repo/getRecord.ts +1 -0
- package/src/lexicon/types/tools/ozone/moderation/getRecords.ts +50 -0
- package/src/lexicon/types/tools/ozone/moderation/getRepos.ts +50 -0
- package/src/mailer/index.ts +1 -1
- package/src/mailer/templates/confirm-email.hbs +106 -336
- package/src/mailer/templates/delete-account.hbs +110 -346
- package/src/mailer/templates/plc-operation.hbs +107 -338
- package/src/mailer/templates/reset-password.d.ts +1 -1
- package/src/mailer/templates/reset-password.hbs +108 -344
- package/src/mailer/templates/update-email.hbs +107 -337
- package/src/pipethrough.ts +528 -233
- package/src/read-after-write/util.ts +58 -32
- package/tests/account-deletion.test.ts +1 -1
- package/tests/account.test.ts +2 -2
- package/tests/email-confirmation.test.ts +2 -2
- package/tests/plc-operations.test.ts +1 -1
- package/tests/proxied/proxy-catchall.test.ts +255 -0
- package/tests/proxied/proxy-header.test.ts +31 -1
- package/tests/proxied/read-after-write.test.ts +77 -0
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsonToLex } from '@atproto/lexicon'
|
|
2
|
+
import { HeadersMap } from '@atproto/xrpc'
|
|
3
|
+
import {
|
|
4
|
+
HandlerPipeThrough,
|
|
5
|
+
HandlerPipeThroughBuffer,
|
|
6
|
+
parseReqNsid,
|
|
7
|
+
} from '@atproto/xrpc-server'
|
|
8
|
+
import express from 'express'
|
|
9
|
+
|
|
3
10
|
import AppContext from '../context'
|
|
11
|
+
import { lexicons } from '../lexicon/lexicons'
|
|
12
|
+
import { readStickyLogger as log } from '../logger'
|
|
13
|
+
import {
|
|
14
|
+
asPipeThroughBuffer,
|
|
15
|
+
isJsonContentType,
|
|
16
|
+
pipethrough,
|
|
17
|
+
} from '../pipethrough'
|
|
4
18
|
import { HandlerResponse, LocalRecords, MungeFn } from './types'
|
|
5
19
|
import { getRecordsSinceRev } from './viewer'
|
|
6
|
-
import { HandlerPipeThrough } from '@atproto/xrpc-server'
|
|
7
|
-
import { parseRes } from '../pipethrough'
|
|
8
20
|
|
|
9
21
|
const REPO_REV_HEADER = 'atproto-repo-rev'
|
|
10
22
|
|
|
11
|
-
export const getRepoRev = (headers:
|
|
23
|
+
export const getRepoRev = (headers: HeadersMap): string | undefined => {
|
|
12
24
|
return headers[REPO_REV_HEADER]
|
|
13
25
|
}
|
|
14
26
|
|
|
@@ -23,42 +35,56 @@ export const getLocalLag = (local: LocalRecords): number | undefined => {
|
|
|
23
35
|
return Date.now() - new Date(oldest).getTime()
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
export const
|
|
38
|
+
export const pipethroughReadAfterWrite = async <T>(
|
|
27
39
|
ctx: AppContext,
|
|
28
|
-
|
|
29
|
-
requester: string,
|
|
30
|
-
res: HandlerPipeThrough,
|
|
40
|
+
reqCtx: { req: express.Request; auth: { credentials: { did: string } } },
|
|
31
41
|
munge: MungeFn<T>,
|
|
32
42
|
): Promise<HandlerResponse<T> | HandlerPipeThrough> => {
|
|
43
|
+
const { req, auth } = reqCtx
|
|
44
|
+
const requester = auth.credentials.did
|
|
45
|
+
|
|
46
|
+
const streamRes = await pipethrough(ctx, req, { iss: requester })
|
|
47
|
+
|
|
48
|
+
const rev = getRepoRev(streamRes.headers)
|
|
49
|
+
if (!rev) return streamRes
|
|
50
|
+
|
|
51
|
+
if (isJsonContentType(streamRes.headers['content-type']) === false) {
|
|
52
|
+
// content-type is present but not JSON, we can't munge this
|
|
53
|
+
return streamRes
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// if the munging fails, we can't return the original response because the
|
|
57
|
+
// stream will already have been read. If we end-up buffering the response,
|
|
58
|
+
// we'll return the buffered response in case of an error.
|
|
59
|
+
let bufferRes: HandlerPipeThroughBuffer | undefined
|
|
60
|
+
|
|
33
61
|
try {
|
|
34
|
-
|
|
62
|
+
const lxm = parseReqNsid(req)
|
|
63
|
+
|
|
64
|
+
return await ctx.actorStore.read(requester, async (store) => {
|
|
65
|
+
const local = await getRecordsSinceRev(store, rev)
|
|
66
|
+
if (local.count === 0) return streamRes
|
|
67
|
+
|
|
68
|
+
const { buffer } = (bufferRes = await asPipeThroughBuffer(streamRes))
|
|
69
|
+
|
|
70
|
+
const lex = jsonToLex(JSON.parse(buffer.toString('utf8')))
|
|
71
|
+
|
|
72
|
+
const parsedRes = lexicons.assertValidXrpcOutput(lxm, lex) as T
|
|
73
|
+
|
|
74
|
+
const localViewer = ctx.localViewer(store)
|
|
75
|
+
|
|
76
|
+
const data = await munge(localViewer, parsedRes, local, requester)
|
|
77
|
+
return formatMungedResponse(data, getLocalLag(local))
|
|
78
|
+
})
|
|
35
79
|
} catch (err) {
|
|
80
|
+
// The error occurred while reading the stream, this is non-recoverable
|
|
81
|
+
if (!bufferRes && !streamRes.stream.readable) throw err
|
|
82
|
+
|
|
36
83
|
log.warn({ err, requester }, 'error in read after write munge')
|
|
37
|
-
return
|
|
84
|
+
return bufferRes ?? streamRes
|
|
38
85
|
}
|
|
39
86
|
}
|
|
40
87
|
|
|
41
|
-
export const readAfterWriteInternal = async <T>(
|
|
42
|
-
ctx: AppContext,
|
|
43
|
-
nsid: string,
|
|
44
|
-
requester: string,
|
|
45
|
-
res: HandlerPipeThrough,
|
|
46
|
-
munge: MungeFn<T>,
|
|
47
|
-
): Promise<HandlerResponse<T> | HandlerPipeThrough> => {
|
|
48
|
-
const rev = getRepoRev(res.headers ?? {})
|
|
49
|
-
if (!rev) return res
|
|
50
|
-
return ctx.actorStore.read(requester, async (store) => {
|
|
51
|
-
const local = await getRecordsSinceRev(store, rev)
|
|
52
|
-
if (local.count === 0) {
|
|
53
|
-
return res
|
|
54
|
-
}
|
|
55
|
-
const localViewer = ctx.localViewer(store)
|
|
56
|
-
const parsedRes = parseRes<T>(nsid, res)
|
|
57
|
-
const data = await munge(localViewer, parsedRes, local, requester)
|
|
58
|
-
return formatMungedResponse(data, getLocalLag(local))
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
88
|
export const formatMungedResponse = <T>(
|
|
63
89
|
body: T,
|
|
64
90
|
lag?: number,
|
|
@@ -78,7 +78,7 @@ describe('account deletion', () => {
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
expect(mail.to).toEqual(carol.email)
|
|
81
|
-
expect(mail.html).toContain('
|
|
81
|
+
expect(mail.html).toContain('To permanently delete your account')
|
|
82
82
|
|
|
83
83
|
token = getTokenFromMail(mail)
|
|
84
84
|
if (!token) {
|
package/tests/account.test.ts
CHANGED
|
@@ -403,7 +403,7 @@ describe('account', () => {
|
|
|
403
403
|
)
|
|
404
404
|
|
|
405
405
|
expect(mail.to).toEqual(email)
|
|
406
|
-
expect(mail.html).toContain('Reset
|
|
406
|
+
expect(mail.html).toContain('Reset password')
|
|
407
407
|
expect(mail.html).toContain('alice.test')
|
|
408
408
|
|
|
409
409
|
const token = getTokenFromMail(mail)
|
|
@@ -474,7 +474,7 @@ describe('account', () => {
|
|
|
474
474
|
)
|
|
475
475
|
|
|
476
476
|
expect(mail.to).toEqual(email)
|
|
477
|
-
expect(mail.html).toContain('Reset
|
|
477
|
+
expect(mail.html).toContain('Reset password')
|
|
478
478
|
expect(mail.html).toContain('alice.test')
|
|
479
479
|
|
|
480
480
|
const token = getTokenFromMail(mail)
|
|
@@ -92,7 +92,7 @@ describe('email confirmation', () => {
|
|
|
92
92
|
}),
|
|
93
93
|
)
|
|
94
94
|
expect(mail.to).toEqual(alice.email)
|
|
95
|
-
expect(mail.html).toContain('Confirm your
|
|
95
|
+
expect(mail.html).toContain('Confirm your email')
|
|
96
96
|
confirmToken = getTokenFromMail(mail)
|
|
97
97
|
expect(confirmToken).toBeDefined()
|
|
98
98
|
})
|
|
@@ -164,7 +164,7 @@ describe('email confirmation', () => {
|
|
|
164
164
|
}
|
|
165
165
|
const mail = await getMailFrom(reqUpdate())
|
|
166
166
|
expect(mail.to).toEqual(alice.email)
|
|
167
|
-
expect(mail.html).toContain('Update your
|
|
167
|
+
expect(mail.html).toContain('Update your email')
|
|
168
168
|
updateToken = getTokenFromMail(mail)
|
|
169
169
|
expect(updateToken).toBeDefined()
|
|
170
170
|
})
|
|
@@ -172,7 +172,7 @@ describe('plc operations', () => {
|
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
expect(mail.to).toEqual(sc.accounts[alice].email)
|
|
175
|
-
expect(mail.html).toContain('PLC
|
|
175
|
+
expect(mail.html).toContain('PLC update requested')
|
|
176
176
|
|
|
177
177
|
const gotToken = getTokenFromMail(mail)
|
|
178
178
|
assert(gotToken)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import AtpAgent from '@atproto/api'
|
|
2
|
+
import { Keypair } from '@atproto/crypto'
|
|
3
|
+
import { TestNetworkNoAppView } from '@atproto/dev-env'
|
|
4
|
+
import { LexiconDoc } from '@atproto/lexicon'
|
|
5
|
+
import * as plc from '@did-plc/lib'
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import getPort from 'get-port'
|
|
8
|
+
import { once } from 'node:events'
|
|
9
|
+
import http from 'node:http'
|
|
10
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
11
|
+
|
|
12
|
+
const lexicons = [
|
|
13
|
+
{
|
|
14
|
+
lexicon: 1,
|
|
15
|
+
id: 'com.example.ok',
|
|
16
|
+
defs: {
|
|
17
|
+
main: {
|
|
18
|
+
type: 'query',
|
|
19
|
+
output: {
|
|
20
|
+
encoding: 'application/json',
|
|
21
|
+
schema: { type: 'object', properties: { foo: { type: 'string' } } },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
lexicon: 1,
|
|
28
|
+
id: 'com.example.slow',
|
|
29
|
+
defs: {
|
|
30
|
+
main: {
|
|
31
|
+
type: 'query',
|
|
32
|
+
output: {
|
|
33
|
+
encoding: 'application/json',
|
|
34
|
+
schema: { type: 'object', properties: { foo: { type: 'string' } } },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
lexicon: 1,
|
|
41
|
+
id: 'com.example.abort',
|
|
42
|
+
defs: {
|
|
43
|
+
main: {
|
|
44
|
+
type: 'query',
|
|
45
|
+
output: {
|
|
46
|
+
encoding: 'application/json',
|
|
47
|
+
schema: { type: 'object', properties: { foo: { type: 'string' } } },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
lexicon: 1,
|
|
54
|
+
id: 'com.example.error',
|
|
55
|
+
defs: {
|
|
56
|
+
main: {
|
|
57
|
+
type: 'query',
|
|
58
|
+
output: {
|
|
59
|
+
encoding: 'application/json',
|
|
60
|
+
schema: { type: 'object', properties: { foo: { type: 'string' } } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
] as const satisfies LexiconDoc[]
|
|
66
|
+
|
|
67
|
+
describe('proxy header', () => {
|
|
68
|
+
let network: TestNetworkNoAppView
|
|
69
|
+
let alice: AtpAgent
|
|
70
|
+
|
|
71
|
+
let proxyServer: ProxyServer
|
|
72
|
+
|
|
73
|
+
beforeAll(async () => {
|
|
74
|
+
network = await TestNetworkNoAppView.create({
|
|
75
|
+
dbPostgresSchema: 'proxy_catchall',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const serviceId = 'proxy_test'
|
|
79
|
+
|
|
80
|
+
proxyServer = await ProxyServer.create(
|
|
81
|
+
network.pds.ctx.plcClient,
|
|
82
|
+
network.pds.ctx.plcRotationKey,
|
|
83
|
+
serviceId,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
alice = network.pds.getClient().withProxy(serviceId, proxyServer.did)
|
|
87
|
+
|
|
88
|
+
for (const lex of lexicons) alice.lex.add(lex)
|
|
89
|
+
|
|
90
|
+
await alice.createAccount({
|
|
91
|
+
email: 'alice@test.com',
|
|
92
|
+
handle: 'alice.test',
|
|
93
|
+
password: 'alice-pass',
|
|
94
|
+
})
|
|
95
|
+
await network.processAll()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
afterAll(async () => {
|
|
99
|
+
await proxyServer?.close()
|
|
100
|
+
await network?.close()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects when upstream unavailable', async () => {
|
|
104
|
+
const serviceId = 'foo_bar'
|
|
105
|
+
|
|
106
|
+
const proxyServer = await ProxyServer.create(
|
|
107
|
+
network.pds.ctx.plcClient,
|
|
108
|
+
network.pds.ctx.plcRotationKey,
|
|
109
|
+
serviceId,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Make sure the service is not available
|
|
113
|
+
await proxyServer.close()
|
|
114
|
+
|
|
115
|
+
const client = alice.withProxy(serviceId, proxyServer.did)
|
|
116
|
+
for (const lex of lexicons) client.lex.add(lex)
|
|
117
|
+
|
|
118
|
+
await expect(client.call('com.example.ok')).rejects.toThrow(
|
|
119
|
+
'pipethrough network error',
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('successfully proxies requests', async () => {
|
|
124
|
+
await expect(alice.call('com.example.ok')).resolves.toMatchObject({
|
|
125
|
+
data: { foo: 'ok' },
|
|
126
|
+
success: true,
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('handles cancelled upstream requests', async () => {
|
|
131
|
+
await expect(alice.call('com.example.abort')).rejects.toThrow('terminated')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('handles failing upstream requests', async () => {
|
|
135
|
+
await expect(alice.call('com.example.error')).rejects.toThrowError(
|
|
136
|
+
expect.objectContaining({
|
|
137
|
+
status: 502,
|
|
138
|
+
error: 'FooBar',
|
|
139
|
+
message: 'My message',
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('handles cancelled downstream requests', async () => {
|
|
145
|
+
const ac = new AbortController()
|
|
146
|
+
|
|
147
|
+
setTimeout(() => ac.abort(), 20)
|
|
148
|
+
|
|
149
|
+
await expect(
|
|
150
|
+
alice.call('com.example.slow', {}, undefined, { signal: ac.signal }),
|
|
151
|
+
).rejects.toThrow('This operation was aborted')
|
|
152
|
+
|
|
153
|
+
await expect(alice.call('com.example.slow')).resolves.toMatchObject({
|
|
154
|
+
data: { foo: 'slow' },
|
|
155
|
+
success: true,
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
class ProxyServer {
|
|
161
|
+
constructor(
|
|
162
|
+
private server: http.Server,
|
|
163
|
+
public did: string,
|
|
164
|
+
) {}
|
|
165
|
+
|
|
166
|
+
static async create(
|
|
167
|
+
plcClient: plc.Client,
|
|
168
|
+
keypair: Keypair,
|
|
169
|
+
serviceId: string,
|
|
170
|
+
): Promise<ProxyServer> {
|
|
171
|
+
const app = express()
|
|
172
|
+
|
|
173
|
+
app.get('/xrpc/com.example.ok', (req, res) => {
|
|
174
|
+
res.status(200)
|
|
175
|
+
res.setHeader('content-type', 'application/json')
|
|
176
|
+
res.send('{"foo":"ok"}')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
app.get('/xrpc/com.example.slow', async (req, res) => {
|
|
180
|
+
const wait = async (ms: number) => {
|
|
181
|
+
if (res.destroyed) return
|
|
182
|
+
const ac = new AbortController()
|
|
183
|
+
const abort = () => ac.abort()
|
|
184
|
+
res.on('close', abort)
|
|
185
|
+
try {
|
|
186
|
+
await sleep(ms, undefined, { signal: ac.signal })
|
|
187
|
+
} finally {
|
|
188
|
+
res.off('close', abort)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await wait(50)
|
|
193
|
+
|
|
194
|
+
res.status(200)
|
|
195
|
+
res.setHeader('content-type', 'application/json')
|
|
196
|
+
res.flushHeaders()
|
|
197
|
+
|
|
198
|
+
await wait(50)
|
|
199
|
+
|
|
200
|
+
for (const char of '{"foo":"slow"}') {
|
|
201
|
+
res.write(char)
|
|
202
|
+
await wait(10)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
res.end()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
app.get('/xrpc/com.example.abort', async (req, res) => {
|
|
209
|
+
res.status(200)
|
|
210
|
+
res.setHeader('content-type', 'application/json')
|
|
211
|
+
res.write('{"foo"')
|
|
212
|
+
await sleep(50)
|
|
213
|
+
res.destroy(new Error('abort'))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
app.get('/xrpc/com.example.error', async (req, res) => {
|
|
217
|
+
res.status(500).json({ error: 'FooBar', message: 'My message' })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const port = await getPort()
|
|
221
|
+
const server = app.listen(port)
|
|
222
|
+
server.keepAliveTimeout = 30 * 1000
|
|
223
|
+
server.headersTimeout = 35 * 1000
|
|
224
|
+
await once(server, 'listening')
|
|
225
|
+
|
|
226
|
+
const plcOp = await plc.signOperation(
|
|
227
|
+
{
|
|
228
|
+
type: 'plc_operation',
|
|
229
|
+
rotationKeys: [keypair.did()],
|
|
230
|
+
alsoKnownAs: [],
|
|
231
|
+
verificationMethods: {},
|
|
232
|
+
services: {
|
|
233
|
+
[serviceId]: {
|
|
234
|
+
type: 'TestAtprotoService',
|
|
235
|
+
endpoint: `http://localhost:${port}`,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
prev: null,
|
|
239
|
+
},
|
|
240
|
+
keypair,
|
|
241
|
+
)
|
|
242
|
+
const did = await plc.didForCreateOp(plcOp)
|
|
243
|
+
await plcClient.sendOperation(did, plcOp)
|
|
244
|
+
return new ProxyServer(server, did)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async close() {
|
|
248
|
+
await new Promise<void>((resolve, reject) => {
|
|
249
|
+
this.server.close((err) => {
|
|
250
|
+
if (err) reject(err)
|
|
251
|
+
else resolve()
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -7,6 +7,7 @@ import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'
|
|
|
7
7
|
import getPort from 'get-port'
|
|
8
8
|
import { Keypair } from '@atproto/crypto'
|
|
9
9
|
import { verifyJwt } from '@atproto/xrpc-server'
|
|
10
|
+
import { parseProxyHeader } from '../../src/pipethrough'
|
|
10
11
|
|
|
11
12
|
describe('proxy header', () => {
|
|
12
13
|
let network: TestNetworkNoAppView
|
|
@@ -51,6 +52,35 @@ describe('proxy header', () => {
|
|
|
51
52
|
throw new Error('no error thrown')
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
it('parses proxy header', async () => {
|
|
56
|
+
expect(parseProxyHeader(network.pds.ctx, `#atproto_test`)).rejects.toThrow(
|
|
57
|
+
'no did specified in proxy header',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test#foo`),
|
|
62
|
+
).rejects.toThrow('invalid proxy header format')
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test `),
|
|
66
|
+
).rejects.toThrow('proxy header cannot contain spaces')
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
parseProxyHeader(network.pds.ctx, ` ${proxyServer.did}#atproto_test`),
|
|
70
|
+
).rejects.toThrow('proxy header cannot contain spaces')
|
|
71
|
+
|
|
72
|
+
expect(parseProxyHeader(network.pds.ctx, `foo#bar`)).rejects.toThrow(
|
|
73
|
+
'Poorly formatted DID: foo',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(
|
|
77
|
+
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test`),
|
|
78
|
+
).resolves.toEqual({
|
|
79
|
+
did: proxyServer.did,
|
|
80
|
+
url: proxyServer.url,
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
54
84
|
it('proxies requests based on header', async () => {
|
|
55
85
|
const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`
|
|
56
86
|
await axios.get(`${network.pds.url}${path}`, {
|
|
@@ -93,7 +123,7 @@ describe('proxy header', () => {
|
|
|
93
123
|
'atproto-proxy': proxyServer.did,
|
|
94
124
|
},
|
|
95
125
|
})
|
|
96
|
-
await assertAxiosErr(attempt, 'no service id specified')
|
|
126
|
+
await assertAxiosErr(attempt, 'no service id specified in proxy header')
|
|
97
127
|
expect(proxyServer.requests.length).toBe(1)
|
|
98
128
|
})
|
|
99
129
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import util from 'node:util'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { AtpAgent } from '@atproto/api'
|
|
4
|
+
import { request } from 'undici'
|
|
4
5
|
import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env'
|
|
5
6
|
import basicSeed from '../seeds/basic'
|
|
6
7
|
import { ThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs'
|
|
@@ -266,4 +267,80 @@ describe('proxy read after write', () => {
|
|
|
266
267
|
const parsed = parseInt(lag)
|
|
267
268
|
expect(parsed > 0).toBe(true)
|
|
268
269
|
})
|
|
270
|
+
|
|
271
|
+
it('negotiates encoding', async () => {
|
|
272
|
+
const identity = await agent.api.app.bsky.feed.getTimeline(
|
|
273
|
+
{},
|
|
274
|
+
{ headers: { ...sc.getHeaders(alice), 'accept-encoding': 'identity' } },
|
|
275
|
+
)
|
|
276
|
+
expect(identity.headers['content-encoding']).toBeUndefined()
|
|
277
|
+
|
|
278
|
+
const gzip = await agent.api.app.bsky.feed.getTimeline(
|
|
279
|
+
{},
|
|
280
|
+
{
|
|
281
|
+
headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip, *;q=0' },
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
expect(gzip.headers['content-encoding']).toBe('gzip')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('defaults to identity encoding', async () => {
|
|
288
|
+
// Not using the "agent" because "fetch()" will add "accept-encoding: gzip,
|
|
289
|
+
// deflate" if not "accept-encoding" header is provided
|
|
290
|
+
const res = await request(
|
|
291
|
+
new URL(`/xrpc/app.bsky.feed.getTimeline`, agent.dispatchUrl),
|
|
292
|
+
{
|
|
293
|
+
headers: { ...sc.getHeaders(alice) },
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
expect(res.statusCode).toBe(200)
|
|
297
|
+
expect(res.headers['content-encoding']).toBeUndefined()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('falls back to identity encoding', async () => {
|
|
301
|
+
const invalid = await agent.api.app.bsky.feed.getTimeline(
|
|
302
|
+
{},
|
|
303
|
+
{ headers: { ...sc.getHeaders(alice), 'accept-encoding': 'invalid' } },
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
expect(invalid.headers['content-encoding']).toBeUndefined()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('errors when failing to negotiate encoding', async () => {
|
|
310
|
+
await expect(
|
|
311
|
+
agent.api.app.bsky.feed.getTimeline(
|
|
312
|
+
{},
|
|
313
|
+
{
|
|
314
|
+
headers: {
|
|
315
|
+
...sc.getHeaders(alice),
|
|
316
|
+
'accept-encoding': 'invalid, *;q=0',
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
),
|
|
320
|
+
).rejects.toThrow(
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
status: 406,
|
|
323
|
+
message: 'this service does not support any of the requested encodings',
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('errors on invalid content-encoding format', async () => {
|
|
329
|
+
await expect(
|
|
330
|
+
agent.api.app.bsky.feed.getTimeline(
|
|
331
|
+
{},
|
|
332
|
+
{
|
|
333
|
+
headers: {
|
|
334
|
+
...sc.getHeaders(alice),
|
|
335
|
+
'accept-encoding': ';q=1',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
),
|
|
339
|
+
).rejects.toThrow(
|
|
340
|
+
expect.objectContaining({
|
|
341
|
+
status: 400,
|
|
342
|
+
message: 'Invalid accept-encoding: ";q=1"',
|
|
343
|
+
}),
|
|
344
|
+
)
|
|
345
|
+
})
|
|
269
346
|
})
|