@atproto/bsky 0.0.36 → 0.0.37
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 +9 -0
- package/dist/auth-verifier.d.ts +9 -8
- package/dist/config.d.ts +4 -0
- package/dist/index.js +85 -24
- package/dist/index.js.map +2 -2
- package/dist/lexicon/lexicons.d.ts +5 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -1
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +1 -0
- package/package.json +5 -5
- package/src/api/blob-resolver.ts +32 -2
- package/src/api/com/atproto/admin/getAccountInfos.ts +1 -1
- package/src/api/com/atproto/admin/getSubjectStatus.ts +1 -1
- package/src/api/com/atproto/admin/updateSubjectStatus.ts +1 -1
- package/src/auth-verifier.ts +42 -21
- package/src/config.ts +21 -0
- package/src/index.ts +1 -1
- package/src/lexicon/lexicons.ts +5 -0
- package/src/lexicon/types/app/bsky/actor/defs.ts +1 -1
- package/src/lexicon/types/com/atproto/admin/defs.ts +2 -0
|
@@ -779,6 +779,10 @@ export declare const schemaDict: {
|
|
|
779
779
|
type: string;
|
|
780
780
|
description: string;
|
|
781
781
|
};
|
|
782
|
+
content: {
|
|
783
|
+
type: string;
|
|
784
|
+
description: string;
|
|
785
|
+
};
|
|
782
786
|
comment: {
|
|
783
787
|
type: string;
|
|
784
788
|
description: string;
|
|
@@ -4731,6 +4735,7 @@ export declare const schemaDict: {
|
|
|
4731
4735
|
hideRepliesByUnfollowed: {
|
|
4732
4736
|
type: string;
|
|
4733
4737
|
description: string;
|
|
4738
|
+
default: boolean;
|
|
4734
4739
|
};
|
|
4735
4740
|
hideRepliesByLikeCount: {
|
|
4736
4741
|
type: string;
|
|
@@ -88,7 +88,7 @@ export declare function validatePersonalDetailsPref(v: unknown): ValidationResul
|
|
|
88
88
|
export interface FeedViewPref {
|
|
89
89
|
feed: string;
|
|
90
90
|
hideReplies?: boolean;
|
|
91
|
-
hideRepliesByUnfollowed
|
|
91
|
+
hideRepliesByUnfollowed: boolean;
|
|
92
92
|
hideRepliesByLikeCount?: number;
|
|
93
93
|
hideReposts?: boolean;
|
|
94
94
|
hideQuotePosts?: boolean;
|
|
@@ -313,6 +313,7 @@ export declare function isModEventUnmute(v: unknown): v is ModEventUnmute;
|
|
|
313
313
|
export declare function validateModEventUnmute(v: unknown): ValidationResult;
|
|
314
314
|
export interface ModEventEmail {
|
|
315
315
|
subjectLine: string;
|
|
316
|
+
content?: string;
|
|
316
317
|
comment?: string;
|
|
317
318
|
[k: string]: unknown;
|
|
318
319
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/bsky",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Reference implementation of app.bsky App View (Bluesky API)",
|
|
6
6
|
"keywords": [
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"sharp": "^0.32.6",
|
|
42
42
|
"typed-emitter": "^2.1.0",
|
|
43
43
|
"uint8arrays": "3.0.0",
|
|
44
|
+
"@atproto/api": "^0.10.5",
|
|
44
45
|
"@atproto/common": "^0.3.3",
|
|
45
|
-
"@atproto/api": "^0.10.4",
|
|
46
46
|
"@atproto/crypto": "^0.3.0",
|
|
47
47
|
"@atproto/identity": "^0.3.2",
|
|
48
48
|
"@atproto/lexicon": "^0.3.2",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"@types/qs": "^6.9.7",
|
|
63
63
|
"axios": "^0.27.2",
|
|
64
64
|
"http2-express-bridge": "^1.0.7",
|
|
65
|
-
"@atproto/api": "^0.10.
|
|
66
|
-
"@atproto/dev-env": "^0.2.
|
|
65
|
+
"@atproto/api": "^0.10.5",
|
|
66
|
+
"@atproto/dev-env": "^0.2.37",
|
|
67
67
|
"@atproto/lex-cli": "^0.3.1",
|
|
68
|
-
"@atproto/pds": "^0.4.
|
|
68
|
+
"@atproto/pds": "^0.4.5",
|
|
69
69
|
"@atproto/xrpc": "^0.4.2"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
package/src/api/blob-resolver.ts
CHANGED
|
@@ -106,7 +106,9 @@ export async function resolveBlob(ctx: AppContext, did: string, cid: CID) {
|
|
|
106
106
|
throw createError(404, 'Blob not found')
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const blobResult = await retryHttp(() =>
|
|
109
|
+
const blobResult = await retryHttp(() =>
|
|
110
|
+
getBlob(ctx, { pds, did, cid: cidStr }),
|
|
111
|
+
)
|
|
110
112
|
const imageStream: Readable = blobResult.data
|
|
111
113
|
const verifyCid = new VerifyCidTransform(cid)
|
|
112
114
|
|
|
@@ -119,12 +121,40 @@ export async function resolveBlob(ctx: AppContext, did: string, cid: CID) {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
async function getBlob(
|
|
124
|
+
async function getBlob(
|
|
125
|
+
ctx: AppContext,
|
|
126
|
+
opts: { pds: string; did: string; cid: string },
|
|
127
|
+
) {
|
|
123
128
|
const { pds, did, cid } = opts
|
|
124
129
|
return axios.get(`${pds}/xrpc/com.atproto.sync.getBlob`, {
|
|
125
130
|
params: { did, cid },
|
|
126
131
|
decompress: true,
|
|
127
132
|
responseType: 'stream',
|
|
128
133
|
timeout: 5000, // 5sec of inactivity on the connection
|
|
134
|
+
headers: getRateLimitBypassHeaders(ctx, pds),
|
|
129
135
|
})
|
|
130
136
|
}
|
|
137
|
+
|
|
138
|
+
function getRateLimitBypassHeaders(
|
|
139
|
+
ctx: AppContext,
|
|
140
|
+
pds: string,
|
|
141
|
+
): { 'x-ratelimit-bypass'?: string } {
|
|
142
|
+
const {
|
|
143
|
+
blobRateLimitBypassKey: bypassKey,
|
|
144
|
+
blobRateLimitBypassHostname: bypassHostname,
|
|
145
|
+
} = ctx.cfg
|
|
146
|
+
if (!bypassKey || !bypassHostname) {
|
|
147
|
+
return {}
|
|
148
|
+
}
|
|
149
|
+
const url = new URL(pds)
|
|
150
|
+
if (bypassHostname.startsWith('.')) {
|
|
151
|
+
if (url.hostname.endsWith(bypassHostname)) {
|
|
152
|
+
return { 'x-ratelimit-bypass': bypassKey }
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
if (url.hostname === bypassHostname) {
|
|
156
|
+
return { 'x-ratelimit-bypass': bypassKey }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {}
|
|
160
|
+
}
|
|
@@ -5,7 +5,7 @@ import { INVALID_HANDLE } from '@atproto/syntax'
|
|
|
5
5
|
|
|
6
6
|
export default function (server: Server, ctx: AppContext) {
|
|
7
7
|
server.com.atproto.admin.getAccountInfos({
|
|
8
|
-
auth: ctx.authVerifier.
|
|
8
|
+
auth: ctx.authVerifier.roleOrModService,
|
|
9
9
|
handler: async ({ params }) => {
|
|
10
10
|
const { dids } = params
|
|
11
11
|
const actors = await ctx.hydrator.actor.getActors(dids, true)
|
|
@@ -5,7 +5,7 @@ import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSub
|
|
|
5
5
|
|
|
6
6
|
export default function (server: Server, ctx: AppContext) {
|
|
7
7
|
server.com.atproto.admin.getSubjectStatus({
|
|
8
|
-
auth: ctx.authVerifier.
|
|
8
|
+
auth: ctx.authVerifier.roleOrModService,
|
|
9
9
|
handler: async ({ params }) => {
|
|
10
10
|
const { did, uri, blob } = params
|
|
11
11
|
|
|
@@ -10,7 +10,7 @@ import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/rep
|
|
|
10
10
|
|
|
11
11
|
export default function (server: Server, ctx: AppContext) {
|
|
12
12
|
server.com.atproto.admin.updateSubjectStatus({
|
|
13
|
-
auth: ctx.authVerifier.
|
|
13
|
+
auth: ctx.authVerifier.roleOrModService,
|
|
14
14
|
handler: async ({ input, auth }) => {
|
|
15
15
|
const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth)
|
|
16
16
|
if (!canPerformTakedown) {
|
package/src/auth-verifier.ts
CHANGED
|
@@ -25,7 +25,7 @@ export enum RoleStatus {
|
|
|
25
25
|
|
|
26
26
|
type NullOutput = {
|
|
27
27
|
credentials: {
|
|
28
|
-
type: '
|
|
28
|
+
type: 'none'
|
|
29
29
|
iss: null
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -45,9 +45,9 @@ type RoleOutput = {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
type
|
|
48
|
+
type ModServiceOutput = {
|
|
49
49
|
credentials: {
|
|
50
|
-
type: '
|
|
50
|
+
type: 'mod_service'
|
|
51
51
|
aud: string
|
|
52
52
|
iss: string
|
|
53
53
|
}
|
|
@@ -55,18 +55,18 @@ type AdminServiceOutput = {
|
|
|
55
55
|
|
|
56
56
|
export type AuthVerifierOpts = {
|
|
57
57
|
ownDid: string
|
|
58
|
-
|
|
58
|
+
modServiceDid: string
|
|
59
59
|
adminPasses: string[]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export class AuthVerifier {
|
|
63
63
|
public ownDid: string
|
|
64
|
-
public
|
|
64
|
+
public modServiceDid: string
|
|
65
65
|
private adminPasses: Set<string>
|
|
66
66
|
|
|
67
67
|
constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) {
|
|
68
68
|
this.ownDid = opts.ownDid
|
|
69
|
-
this.
|
|
69
|
+
this.modServiceDid = opts.modServiceDid
|
|
70
70
|
this.adminPasses = new Set(opts.adminPasses)
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -83,13 +83,21 @@ export class AuthVerifier {
|
|
|
83
83
|
if (!this.parseRoleCreds(ctx.req).admin) {
|
|
84
84
|
throw new AuthRequiredError('bad credentials')
|
|
85
85
|
}
|
|
86
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
credentials: { type: 'standard', iss, aud },
|
|
88
|
+
}
|
|
87
89
|
}
|
|
88
90
|
const { iss, aud } = await this.verifyServiceJwt(ctx, {
|
|
89
91
|
aud: this.ownDid,
|
|
90
92
|
iss: null,
|
|
91
93
|
})
|
|
92
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
credentials: {
|
|
96
|
+
type: 'standard',
|
|
97
|
+
iss,
|
|
98
|
+
aud,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
standardOptional = async (
|
|
@@ -159,19 +167,19 @@ export class AuthVerifier {
|
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
modService = async (reqCtx: ReqCtx): Promise<ModServiceOutput> => {
|
|
163
171
|
const { iss, aud } = await this.verifyServiceJwt(reqCtx, {
|
|
164
172
|
aud: this.ownDid,
|
|
165
|
-
iss: [this.
|
|
173
|
+
iss: [this.modServiceDid, `${this.modServiceDid}#atproto_labeler`],
|
|
166
174
|
})
|
|
167
|
-
return { credentials: { type: '
|
|
175
|
+
return { credentials: { type: 'mod_service', aud, iss } }
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
|
|
178
|
+
roleOrModService = async (
|
|
171
179
|
reqCtx: ReqCtx,
|
|
172
|
-
): Promise<RoleOutput |
|
|
180
|
+
): Promise<RoleOutput | ModServiceOutput> => {
|
|
173
181
|
if (isBearerToken(reqCtx.req)) {
|
|
174
|
-
return this.
|
|
182
|
+
return this.modService(reqCtx)
|
|
175
183
|
} else {
|
|
176
184
|
return this.role(reqCtx)
|
|
177
185
|
}
|
|
@@ -195,12 +203,15 @@ export class AuthVerifier {
|
|
|
195
203
|
opts: { aud: string | null; iss: string[] | null },
|
|
196
204
|
) {
|
|
197
205
|
const getSigningKey = async (
|
|
198
|
-
|
|
206
|
+
iss: string,
|
|
199
207
|
_forceRefresh: boolean, // @TODO consider propagating to dataplane
|
|
200
208
|
): Promise<string> => {
|
|
201
|
-
if (opts.iss !== null && !opts.iss.includes(
|
|
209
|
+
if (opts.iss !== null && !opts.iss.includes(iss)) {
|
|
202
210
|
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
|
|
203
211
|
}
|
|
212
|
+
const [did, serviceId] = iss.split('#')
|
|
213
|
+
const keyId =
|
|
214
|
+
serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto'
|
|
204
215
|
let identity: GetIdentityByDidResponse
|
|
205
216
|
try {
|
|
206
217
|
identity = await this.dataplane.getIdentityByDid({ did })
|
|
@@ -211,7 +222,7 @@ export class AuthVerifier {
|
|
|
211
222
|
throw err
|
|
212
223
|
}
|
|
213
224
|
const keys = unpackIdentityKeys(identity.keys)
|
|
214
|
-
const didKey = getKeyAsDidKey(keys, { id:
|
|
225
|
+
const didKey = getKeyAsDidKey(keys, { id: keyId })
|
|
215
226
|
if (!didKey) {
|
|
216
227
|
throw new AuthRequiredError('missing or bad key')
|
|
217
228
|
}
|
|
@@ -226,26 +237,36 @@ export class AuthVerifier {
|
|
|
226
237
|
return { iss: payload.iss, aud: payload.aud }
|
|
227
238
|
}
|
|
228
239
|
|
|
240
|
+
isModService(iss: string): boolean {
|
|
241
|
+
return [
|
|
242
|
+
this.modServiceDid,
|
|
243
|
+
`${this.modServiceDid}#atproto_labeler`,
|
|
244
|
+
].includes(iss)
|
|
245
|
+
}
|
|
246
|
+
|
|
229
247
|
nullCreds(): NullOutput {
|
|
230
248
|
return {
|
|
231
249
|
credentials: {
|
|
232
|
-
type: '
|
|
250
|
+
type: 'none',
|
|
233
251
|
iss: null,
|
|
234
252
|
},
|
|
235
253
|
}
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
parseCreds(
|
|
239
|
-
creds: StandardOutput | RoleOutput |
|
|
257
|
+
creds: StandardOutput | RoleOutput | ModServiceOutput | NullOutput,
|
|
240
258
|
) {
|
|
241
259
|
const viewer =
|
|
242
260
|
creds.credentials.type === 'standard' ? creds.credentials.iss : null
|
|
243
261
|
const canViewTakedowns =
|
|
244
262
|
(creds.credentials.type === 'role' && creds.credentials.admin) ||
|
|
245
|
-
creds.credentials.type === '
|
|
263
|
+
creds.credentials.type === 'mod_service' ||
|
|
264
|
+
(creds.credentials.type === 'standard' &&
|
|
265
|
+
this.isModService(creds.credentials.iss))
|
|
246
266
|
const canPerformTakedown =
|
|
247
267
|
(creds.credentials.type === 'role' && creds.credentials.admin) ||
|
|
248
|
-
creds.credentials.type === '
|
|
268
|
+
creds.credentials.type === 'mod_service'
|
|
269
|
+
|
|
249
270
|
return {
|
|
250
271
|
viewer,
|
|
251
272
|
canViewTakedowns,
|
package/src/config.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ServerConfigValues {
|
|
|
21
21
|
courierIgnoreBadTls?: boolean
|
|
22
22
|
searchUrl?: string
|
|
23
23
|
cdnUrl?: string
|
|
24
|
+
blobRateLimitBypassKey?: string
|
|
25
|
+
blobRateLimitBypassHostname?: string
|
|
24
26
|
// identity
|
|
25
27
|
didPlcUrl: string
|
|
26
28
|
handleResolveNameservers?: string[]
|
|
@@ -76,6 +78,15 @@ export class ServerConfig {
|
|
|
76
78
|
const courierIgnoreBadTls =
|
|
77
79
|
process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true'
|
|
78
80
|
assert(courierHttpVersion === '1.1' || courierHttpVersion === '2')
|
|
81
|
+
const blobRateLimitBypassKey =
|
|
82
|
+
process.env.BSKY_BLOB_RATE_LIMIT_BYPASS_KEY || undefined
|
|
83
|
+
// single domain would be e.g. "mypds.com", subdomains are supported with a leading dot e.g. ".mypds.com"
|
|
84
|
+
const blobRateLimitBypassHostname =
|
|
85
|
+
process.env.BSKY_BLOB_RATE_LIMIT_BYPASS_HOSTNAME || undefined
|
|
86
|
+
assert(
|
|
87
|
+
!blobRateLimitBypassKey || blobRateLimitBypassHostname,
|
|
88
|
+
'must specify a hostname when using a blob rate limit bypass key',
|
|
89
|
+
)
|
|
79
90
|
const adminPasswords = envList(
|
|
80
91
|
process.env.BSKY_ADMIN_PASSWORDS || process.env.BSKY_ADMIN_PASSWORD,
|
|
81
92
|
)
|
|
@@ -106,6 +117,8 @@ export class ServerConfig {
|
|
|
106
117
|
courierApiKey,
|
|
107
118
|
courierHttpVersion,
|
|
108
119
|
courierIgnoreBadTls,
|
|
120
|
+
blobRateLimitBypassKey,
|
|
121
|
+
blobRateLimitBypassHostname,
|
|
109
122
|
adminPasswords,
|
|
110
123
|
modServiceDid,
|
|
111
124
|
...stripUndefineds(overrides ?? {}),
|
|
@@ -197,6 +210,14 @@ export class ServerConfig {
|
|
|
197
210
|
return this.cfg.cdnUrl
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
get blobRateLimitBypassKey() {
|
|
214
|
+
return this.cfg.blobRateLimitBypassKey
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get blobRateLimitBypassHostname() {
|
|
218
|
+
return this.cfg.blobRateLimitBypassHostname
|
|
219
|
+
}
|
|
220
|
+
|
|
200
221
|
get didPlcUrl() {
|
|
201
222
|
return this.cfg.didPlcUrl
|
|
202
223
|
}
|
package/src/index.ts
CHANGED
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -897,6 +897,10 @@ export const schemaDict = {
|
|
|
897
897
|
type: 'string',
|
|
898
898
|
description: 'The subject line of the email sent to the user.',
|
|
899
899
|
},
|
|
900
|
+
content: {
|
|
901
|
+
type: 'string',
|
|
902
|
+
description: 'The content of the email sent to the user.',
|
|
903
|
+
},
|
|
900
904
|
comment: {
|
|
901
905
|
type: 'string',
|
|
902
906
|
description: 'Additional comment about the outgoing comm.',
|
|
@@ -5180,6 +5184,7 @@ export const schemaDict = {
|
|
|
5180
5184
|
type: 'boolean',
|
|
5181
5185
|
description:
|
|
5182
5186
|
'Hide replies in the feed if they are not by followed users.',
|
|
5187
|
+
default: true,
|
|
5183
5188
|
},
|
|
5184
5189
|
hideRepliesByLikeCount: {
|
|
5185
5190
|
type: 'integer',
|
|
@@ -197,7 +197,7 @@ export interface FeedViewPref {
|
|
|
197
197
|
/** Hide replies in the feed. */
|
|
198
198
|
hideReplies?: boolean
|
|
199
199
|
/** Hide replies in the feed if they are not by followed users. */
|
|
200
|
-
hideRepliesByUnfollowed
|
|
200
|
+
hideRepliesByUnfollowed: boolean
|
|
201
201
|
/** Hide replies in the feed if they do not have this number of likes. */
|
|
202
202
|
hideRepliesByLikeCount?: number
|
|
203
203
|
/** Hide reposts in the feed. */
|
|
@@ -704,6 +704,8 @@ export function validateModEventUnmute(v: unknown): ValidationResult {
|
|
|
704
704
|
export interface ModEventEmail {
|
|
705
705
|
/** The subject line of the email sent to the user. */
|
|
706
706
|
subjectLine: string
|
|
707
|
+
/** The content of the email sent to the user. */
|
|
708
|
+
content?: string
|
|
707
709
|
/** Additional comment about the outgoing comm. */
|
|
708
710
|
comment?: string
|
|
709
711
|
[k: string]: unknown
|