@atproto/ozone 0.1.64 → 0.1.66
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 +24 -0
- package/dist/daemon/blob-diverter.d.ts +26 -11
- package/dist/daemon/blob-diverter.d.ts.map +1 -1
- package/dist/daemon/blob-diverter.js +109 -52
- package/dist/daemon/blob-diverter.js.map +1 -1
- package/dist/util.d.ts +2 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +9 -20
- package/dist/util.js.map +1 -1
- package/package.json +11 -12
- package/src/daemon/blob-diverter.ts +137 -102
- package/src/util.ts +8 -20
- package/tests/_util.ts +46 -0
- package/tests/blob-divert.test.ts +30 -19
- package/tests/moderation.test.ts +3 -1
- package/tests/server.test.ts +23 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @atproto/ozone
|
|
2
2
|
|
|
3
|
+
## 0.1.66
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`1abfd74ec`](https://github.com/bluesky-social/atproto/commit/1abfd74ec7114e5d8e2411f7a4fa10bdce97e277)]:
|
|
8
|
+
- @atproto/crypto@0.4.3
|
|
9
|
+
- @atproto/identity@0.4.5
|
|
10
|
+
- @atproto/xrpc-server@0.7.6
|
|
11
|
+
|
|
12
|
+
## 0.1.65
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#3177](https://github.com/bluesky-social/atproto/pull/3177) [`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on Axios
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`72eba67af`](https://github.com/bluesky-social/atproto/commit/72eba67af1af8320b5400bcb9319d5c3c8407d99)]:
|
|
19
|
+
- @atproto/identity@0.4.4
|
|
20
|
+
- @atproto/api@0.13.26
|
|
21
|
+
- @atproto/common@0.4.6
|
|
22
|
+
- @atproto/lexicon@0.4.5
|
|
23
|
+
- @atproto/crypto@0.4.2
|
|
24
|
+
- @atproto/xrpc-server@0.7.5
|
|
25
|
+
- @atproto/xrpc@0.6.6
|
|
26
|
+
|
|
3
27
|
## 0.1.64
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IdResolver } from '@atproto/identity';
|
|
2
|
-
import { Readable } from 'stream';
|
|
3
|
-
import Database from '../db';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
4
3
|
import { BlobDivertConfig } from '../config';
|
|
4
|
+
import Database from '../db';
|
|
5
5
|
export declare class BlobDiverter {
|
|
6
6
|
db: Database;
|
|
7
7
|
serviceConfig: BlobDivertConfig;
|
|
@@ -10,17 +10,32 @@ export declare class BlobDiverter {
|
|
|
10
10
|
idResolver: IdResolver;
|
|
11
11
|
serviceConfig: BlobDivertConfig;
|
|
12
12
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
15
|
+
*/
|
|
16
|
+
getBlob(options: GetBlobOptions): Promise<Blob>;
|
|
17
|
+
/**
|
|
18
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
19
|
+
*/
|
|
20
|
+
uploadBlob(blob: Blob, report: ReportBlobOptions): Promise<void>;
|
|
21
|
+
uploadBlobOnService({ subjectDid: did, subjectUri: uri, subjectBlobCids, }: {
|
|
21
22
|
subjectDid: string;
|
|
22
23
|
subjectUri: string | null;
|
|
23
24
|
subjectBlobCids: string[];
|
|
24
|
-
}): Promise<
|
|
25
|
+
}): Promise<void>;
|
|
25
26
|
}
|
|
27
|
+
type Blob = {
|
|
28
|
+
type: string;
|
|
29
|
+
stream: Readable;
|
|
30
|
+
};
|
|
31
|
+
type GetBlobOptions = {
|
|
32
|
+
pds: string;
|
|
33
|
+
did: string;
|
|
34
|
+
cid: string;
|
|
35
|
+
};
|
|
36
|
+
type ReportBlobOptions = {
|
|
37
|
+
did: string;
|
|
38
|
+
uri: string | null;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
26
41
|
//# sourceMappingURL=blob-diverter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-diverter.d.ts","sourceRoot":"","sources":["../../src/daemon/blob-diverter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blob-diverter.d.ts","sourceRoot":"","sources":["../../src/daemon/blob-diverter.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAG9C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAItC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,QAAQ,MAAM,OAAO,CAAA;AAG5B,qBAAa,YAAY;IAKd,EAAE,EAAE,QAAQ;IAJrB,aAAa,EAAE,gBAAgB,CAAA;IAC/B,UAAU,EAAE,UAAU,CAAA;gBAGb,EAAE,EAAE,QAAQ,EACnB,QAAQ,EAAE;QACR,UAAU,EAAE,UAAU,CAAA;QACtB,aAAa,EAAE,gBAAgB,CAAA;KAChC;IAMH;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA4CrD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB;IA8BhD,mBAAmB,CAAC,EACxB,UAAU,EAAE,GAAG,EACf,UAAU,EAAE,GAAG,EACf,eAAe,GAChB,EAAE;QACD,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,EAAE,CAAA;KAC1B,GAAG,OAAO,CAAC,IAAI,CAAC;CA0BlB;AAMD,KAAK,IAAI,GAAG;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,QAAQ,CAAA;CACjB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AASD,KAAK,iBAAiB,GAAG;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB,CAAA"}
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
4
24
|
};
|
|
5
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
26
|
exports.BlobDiverter = void 0;
|
|
7
27
|
const common_1 = require("@atproto/common");
|
|
8
|
-
const
|
|
28
|
+
const xrpc_1 = require("@atproto/xrpc");
|
|
9
29
|
const cid_1 = require("multiformats/cid");
|
|
30
|
+
const promises_1 = require("node:stream/promises");
|
|
31
|
+
const undici = __importStar(require("undici"));
|
|
10
32
|
const util_1 = require("../util");
|
|
11
33
|
class BlobDiverter {
|
|
12
34
|
constructor(db, services) {
|
|
@@ -31,70 +53,105 @@ class BlobDiverter {
|
|
|
31
53
|
this.serviceConfig = services.serviceConfig;
|
|
32
54
|
this.idResolver = services.idResolver;
|
|
33
55
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
/**
|
|
57
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
58
|
+
*/
|
|
59
|
+
async getBlob(options) {
|
|
60
|
+
const blobUrl = getBlobUrl(options);
|
|
61
|
+
const blobResponse = await undici
|
|
62
|
+
.request(blobUrl, {
|
|
63
|
+
headersTimeout: 10e3,
|
|
64
|
+
bodyTimeout: 30e3,
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
throw asXrpcClientError(err, `Error fetching blob ${options.cid}`);
|
|
40
68
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
if (blobResponse.statusCode !== 200) {
|
|
70
|
+
blobResponse.body.destroy();
|
|
71
|
+
throw new xrpc_1.XRPCError(blobResponse.statusCode, undefined, `Error downloading blob ${options.cid}`);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const type = blobResponse.headers['content-type'];
|
|
75
|
+
const encoding = blobResponse.headers['content-encoding'];
|
|
76
|
+
const verifier = new common_1.VerifyCidTransform(cid_1.CID.parse(options.cid));
|
|
77
|
+
void (0, promises_1.pipeline)([
|
|
78
|
+
blobResponse.body,
|
|
79
|
+
...(0, common_1.createDecoders)(encoding),
|
|
80
|
+
verifier,
|
|
81
|
+
]).catch((_err) => { });
|
|
82
|
+
return {
|
|
83
|
+
type: typeof type === 'string' ? type : 'application/octet-stream',
|
|
84
|
+
stream: verifier,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
// Typically un-supported content encoding
|
|
89
|
+
blobResponse.body.destroy();
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
48
92
|
}
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
/**
|
|
94
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
95
|
+
*/
|
|
96
|
+
async uploadBlob(blob, report) {
|
|
97
|
+
const uploadUrl = reportBlobUrl(this.serviceConfig.url, report);
|
|
98
|
+
const result = await undici
|
|
99
|
+
.request(uploadUrl, {
|
|
51
100
|
method: 'POST',
|
|
52
|
-
|
|
101
|
+
body: blob.stream,
|
|
102
|
+
headersTimeout: 30e3,
|
|
103
|
+
bodyTimeout: 10e3,
|
|
53
104
|
headers: {
|
|
54
105
|
Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
|
|
55
|
-
'
|
|
106
|
+
'content-type': blob.type,
|
|
56
107
|
},
|
|
108
|
+
})
|
|
109
|
+
.catch((err) => {
|
|
110
|
+
throw asXrpcClientError(err, `Error uploading blob ${report.did}`);
|
|
57
111
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (subjectUri)
|
|
64
|
-
url.searchParams.set('uri', subjectUri);
|
|
65
|
-
const result = await this.sendImage({
|
|
66
|
-
url: url.toString(),
|
|
67
|
-
imageStream,
|
|
68
|
-
contentType,
|
|
69
|
-
});
|
|
70
|
-
return result;
|
|
112
|
+
if (result.statusCode !== 200) {
|
|
113
|
+
result.body.destroy();
|
|
114
|
+
throw new xrpc_1.XRPCError(result.statusCode, undefined, `Error uploading blob ${report.did}`);
|
|
115
|
+
}
|
|
116
|
+
await (0, promises_1.finished)(result.body.resume());
|
|
71
117
|
}
|
|
72
|
-
async uploadBlobOnService({ subjectDid, subjectUri, subjectBlobCids, }) {
|
|
73
|
-
const didDoc = await this.idResolver.did.resolve(
|
|
74
|
-
if (!didDoc)
|
|
118
|
+
async uploadBlobOnService({ subjectDid: did, subjectUri: uri, subjectBlobCids, }) {
|
|
119
|
+
const didDoc = await this.idResolver.did.resolve(did);
|
|
120
|
+
if (!didDoc)
|
|
75
121
|
throw new Error('Error resolving DID');
|
|
76
|
-
}
|
|
77
122
|
const pds = (0, common_1.getPdsEndpoint)(didDoc);
|
|
78
|
-
if (!pds)
|
|
123
|
+
if (!pds)
|
|
79
124
|
throw new Error('Error resolving PDS');
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return this.uploadBlob({ imageStream, contentType }, { subjectDid, subjectUri });
|
|
89
|
-
})));
|
|
90
|
-
if (uploadResult.includes(false)) {
|
|
91
|
-
throw new Error(`Error uploading blob ${subjectUri}`);
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
125
|
+
await (0, common_1.allFulfilled)(subjectBlobCids.map((cid) => (0, util_1.retryHttp)(async () => {
|
|
126
|
+
// attempt to download and upload within the same retry block since
|
|
127
|
+
// the blob stream is not reusable
|
|
128
|
+
const blob = await this.getBlob({ pds, cid, did });
|
|
129
|
+
return this.uploadBlob(blob, { did, uri });
|
|
130
|
+
}))).catch((err) => {
|
|
131
|
+
throw new xrpc_1.XRPCError(xrpc_1.ResponseType.UpstreamFailure, undefined, 'Failed to process blobs', undefined, { cause: err });
|
|
132
|
+
});
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
135
|
exports.BlobDiverter = BlobDiverter;
|
|
97
136
|
const basicAuth = (username, password) => {
|
|
98
137
|
return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
|
99
138
|
};
|
|
139
|
+
function getBlobUrl({ pds, did, cid }) {
|
|
140
|
+
const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds);
|
|
141
|
+
url.searchParams.set('did', did);
|
|
142
|
+
url.searchParams.set('cid', cid);
|
|
143
|
+
return url;
|
|
144
|
+
}
|
|
145
|
+
function reportBlobUrl(service, { did, uri }) {
|
|
146
|
+
const url = new URL(`/xrpc/com.atproto.unspecced.reportBlob`, service);
|
|
147
|
+
url.searchParams.set('did', did);
|
|
148
|
+
if (uri != null)
|
|
149
|
+
url.searchParams.set('uri', uri);
|
|
150
|
+
return url;
|
|
151
|
+
}
|
|
152
|
+
function asXrpcClientError(err, message) {
|
|
153
|
+
return new xrpc_1.XRPCError(xrpc_1.ResponseType.Unknown, undefined, message, undefined, {
|
|
154
|
+
cause: err,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
100
157
|
//# sourceMappingURL=blob-diverter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-diverter.js","sourceRoot":"","sources":["../../src/daemon/blob-diverter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blob-diverter.js","sourceRoot":"","sources":["../../src/daemon/blob-diverter.ts"],"names":[],"mappings":"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"}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { RetryOptions } from '@atproto/common';
|
|
2
1
|
import Database from './db';
|
|
3
2
|
export declare const getSigningKeyId: (db: Database, signingKey: string) => Promise<number>;
|
|
4
|
-
export declare
|
|
5
|
-
export declare
|
|
3
|
+
export declare const RETRYABLE_HTTP_STATUS_CODES: Set<number>;
|
|
4
|
+
export declare const retryHttp: <T>(fn: () => Promise<T>, opts?: import("@atproto/common").RetryOptions) => Promise<T>;
|
|
6
5
|
export type ParsedLabelers = {
|
|
7
6
|
dids: string[];
|
|
8
7
|
redact: Set<string>;
|
package/dist/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,OAAO,QAAQ,MAAM,MAAM,CAAA;AAE3B,eAAO,MAAM,eAAe,OACtB,QAAQ,cACA,MAAM,KACjB,OAAO,CAAC,MAAM,CAehB,CAAA;AAED,eAAO,MAAM,2BAA2B,aAEtC,CAAA;AAEF,eAAO,MAAM,SAAS,wFAMpB,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,mBAAmB,4BAA4B,CAAA;AAE5D,eAAO,MAAM,kBAAkB,WACrB,MAAM,GAAG,SAAS,cACd,MAAM,KACjB,cAAc,GAAG,IAuBnB,CAAA;AAED,eAAO,MAAM,oBAAoB,SAAU,MAAM,EAAE,KAAG,cAKrD,CAAA;AAED,eAAO,MAAM,mBAAmB,WAAY,cAAc,KAAG,MAK5D,CAAA"}
|
package/dist/util.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.getSigningKeyId = void 0;
|
|
4
|
-
exports.retryHttp = retryHttp;
|
|
5
|
-
exports.retryableHttp = retryableHttp;
|
|
6
|
-
const axios_1 = require("axios");
|
|
7
|
-
const structured_headers_1 = require("structured-headers");
|
|
8
|
-
const xrpc_1 = require("@atproto/xrpc");
|
|
3
|
+
exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.retryHttp = exports.RETRYABLE_HTTP_STATUS_CODES = exports.getSigningKeyId = void 0;
|
|
9
4
|
const common_1 = require("@atproto/common");
|
|
5
|
+
const xrpc_1 = require("@atproto/xrpc");
|
|
6
|
+
const structured_headers_1 = require("structured-headers");
|
|
10
7
|
const getSigningKeyId = async (db, signingKey) => {
|
|
11
8
|
const selectRes = await db.db
|
|
12
9
|
.selectFrom('signing_key')
|
|
@@ -24,25 +21,17 @@ const getSigningKeyId = async (db, signingKey) => {
|
|
|
24
21
|
return insertRes.id;
|
|
25
22
|
};
|
|
26
23
|
exports.getSigningKeyId = getSigningKeyId;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
exports.RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
25
|
+
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
26
|
+
]);
|
|
27
|
+
exports.retryHttp = (0, common_1.createRetryable)((err) => {
|
|
31
28
|
if (err instanceof xrpc_1.XRPCError) {
|
|
32
29
|
if (err.status === xrpc_1.ResponseType.Unknown)
|
|
33
30
|
return true;
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
if (err instanceof axios_1.AxiosError) {
|
|
37
|
-
if (!err.response)
|
|
38
|
-
return true;
|
|
39
|
-
return retryableHttpStatusCodes.has(err.response.status);
|
|
31
|
+
return exports.RETRYABLE_HTTP_STATUS_CODES.has(err.status);
|
|
40
32
|
}
|
|
41
33
|
return false;
|
|
42
|
-
}
|
|
43
|
-
const retryableHttpStatusCodes = new Set([
|
|
44
|
-
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
45
|
-
]);
|
|
34
|
+
});
|
|
46
35
|
exports.LABELER_HEADER_NAME = 'atproto-accept-labelers';
|
|
47
36
|
const parseLabelerHeader = (header, ignoreDid) => {
|
|
48
37
|
if (!header)
|
package/dist/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ozone",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.66",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backend service for moderating the Bluesky network.",
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"bin": "dist/bin.js",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@did-plc/lib": "^0.0.1",
|
|
21
|
-
"axios": "^1.6.7",
|
|
22
21
|
"compression": "^1.7.4",
|
|
23
22
|
"cors": "^2.8.5",
|
|
24
23
|
"express": "^4.17.2",
|
|
@@ -32,14 +31,15 @@
|
|
|
32
31
|
"structured-headers": "^1.0.1",
|
|
33
32
|
"typed-emitter": "^2.1.0",
|
|
34
33
|
"uint8arrays": "3.0.0",
|
|
35
|
-
"
|
|
36
|
-
"@atproto/
|
|
37
|
-
"@atproto/
|
|
38
|
-
"@atproto/
|
|
39
|
-
"@atproto/
|
|
34
|
+
"undici": "^6.14.1",
|
|
35
|
+
"@atproto/api": "^0.13.26",
|
|
36
|
+
"@atproto/common": "^0.4.6",
|
|
37
|
+
"@atproto/crypto": "^0.4.3",
|
|
38
|
+
"@atproto/identity": "^0.4.5",
|
|
39
|
+
"@atproto/lexicon": "^0.4.5",
|
|
40
40
|
"@atproto/syntax": "^0.3.1",
|
|
41
|
-
"@atproto/xrpc": "^0.6.
|
|
42
|
-
"@atproto/xrpc-server": "^0.7.
|
|
41
|
+
"@atproto/xrpc": "^0.6.6",
|
|
42
|
+
"@atproto/xrpc-server": "^0.7.6"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@did-plc/server": "^0.0.1",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
"@types/express-serve-static-core": "^4.17.36",
|
|
49
49
|
"@types/pg": "^8.6.6",
|
|
50
50
|
"@types/qs": "^6.9.7",
|
|
51
|
-
"axios": "^0.27.2",
|
|
52
51
|
"jest": "^28.1.2",
|
|
53
52
|
"ts-node": "^10.8.2",
|
|
54
53
|
"typescript": "^5.6.3",
|
|
55
|
-
"@atproto/lex-cli": "^0.5.
|
|
56
|
-
"@atproto/pds": "^0.4.
|
|
54
|
+
"@atproto/lex-cli": "^0.5.5",
|
|
55
|
+
"@atproto/pds": "^0.4.83"
|
|
57
56
|
},
|
|
58
57
|
"scripts": {
|
|
59
58
|
"codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
forwardStreamErrors,
|
|
2
|
+
createDecoders,
|
|
4
3
|
getPdsEndpoint,
|
|
4
|
+
VerifyCidTransform,
|
|
5
|
+
allFulfilled,
|
|
5
6
|
} from '@atproto/common'
|
|
6
7
|
import { IdResolver } from '@atproto/identity'
|
|
7
|
-
import
|
|
8
|
-
import { Readable } from 'stream'
|
|
8
|
+
import { ResponseType, XRPCError } from '@atproto/xrpc'
|
|
9
9
|
import { CID } from 'multiformats/cid'
|
|
10
|
+
import { Readable } from 'node:stream'
|
|
11
|
+
import { finished, pipeline } from 'node:stream/promises'
|
|
12
|
+
import * as undici from 'undici'
|
|
10
13
|
|
|
14
|
+
import { BlobDivertConfig } from '../config'
|
|
11
15
|
import Database from '../db'
|
|
12
16
|
import { retryHttp } from '../util'
|
|
13
|
-
import { BlobDivertConfig } from '../config'
|
|
14
17
|
|
|
15
18
|
export class BlobDiverter {
|
|
16
19
|
serviceConfig: BlobDivertConfig
|
|
@@ -27,126 +30,158 @@ export class BlobDiverter {
|
|
|
27
30
|
this.idResolver = services.idResolver
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
contentType:
|
|
54
|
-
blobResponse.headers['content-type'] || 'application/octet-stream',
|
|
55
|
-
imageStream: imageStream.pipe(verifyCid),
|
|
33
|
+
/**
|
|
34
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
35
|
+
*/
|
|
36
|
+
async getBlob(options: GetBlobOptions): Promise<Blob> {
|
|
37
|
+
const blobUrl = getBlobUrl(options)
|
|
38
|
+
|
|
39
|
+
const blobResponse = await undici
|
|
40
|
+
.request(blobUrl, {
|
|
41
|
+
headersTimeout: 10e3,
|
|
42
|
+
bodyTimeout: 30e3,
|
|
43
|
+
})
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
throw asXrpcClientError(err, `Error fetching blob ${options.cid}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (blobResponse.statusCode !== 200) {
|
|
49
|
+
blobResponse.body.destroy()
|
|
50
|
+
throw new XRPCError(
|
|
51
|
+
blobResponse.statusCode,
|
|
52
|
+
undefined,
|
|
53
|
+
`Error downloading blob ${options.cid}`,
|
|
54
|
+
)
|
|
56
55
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async sendImage({
|
|
60
|
-
url,
|
|
61
|
-
imageStream,
|
|
62
|
-
contentType,
|
|
63
|
-
}: {
|
|
64
|
-
url: string
|
|
65
|
-
imageStream: Readable
|
|
66
|
-
contentType: string
|
|
67
|
-
}) {
|
|
68
|
-
const result = await axios(url, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
data: imageStream,
|
|
71
|
-
headers: {
|
|
72
|
-
Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
|
|
73
|
-
'Content-Type': contentType,
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
56
|
|
|
77
|
-
|
|
57
|
+
try {
|
|
58
|
+
const type = blobResponse.headers['content-type']
|
|
59
|
+
const encoding = blobResponse.headers['content-encoding']
|
|
60
|
+
|
|
61
|
+
const verifier = new VerifyCidTransform(CID.parse(options.cid))
|
|
62
|
+
|
|
63
|
+
void pipeline([
|
|
64
|
+
blobResponse.body,
|
|
65
|
+
...createDecoders(encoding),
|
|
66
|
+
verifier,
|
|
67
|
+
]).catch((_err) => {})
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
type: typeof type === 'string' ? type : 'application/octet-stream',
|
|
71
|
+
stream: verifier,
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Typically un-supported content encoding
|
|
75
|
+
blobResponse.body.destroy()
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @throws {XRPCError} so that retryHttp can handle retries
|
|
82
|
+
*/
|
|
83
|
+
async uploadBlob(blob: Blob, report: ReportBlobOptions) {
|
|
84
|
+
const uploadUrl = reportBlobUrl(this.serviceConfig.url, report)
|
|
85
|
+
|
|
86
|
+
const result = await undici
|
|
87
|
+
.request(uploadUrl, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: blob.stream,
|
|
90
|
+
headersTimeout: 30e3,
|
|
91
|
+
bodyTimeout: 10e3,
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
|
|
94
|
+
'content-type': blob.type,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
throw asXrpcClientError(err, `Error uploading blob ${report.did}`)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (result.statusCode !== 200) {
|
|
102
|
+
result.body.destroy()
|
|
103
|
+
throw new XRPCError(
|
|
104
|
+
result.statusCode,
|
|
105
|
+
undefined,
|
|
106
|
+
`Error uploading blob ${report.did}`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
await finished(result.body.resume())
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
async uploadBlobOnService({
|
|
105
|
-
subjectDid,
|
|
106
|
-
subjectUri,
|
|
114
|
+
subjectDid: did,
|
|
115
|
+
subjectUri: uri,
|
|
107
116
|
subjectBlobCids,
|
|
108
117
|
}: {
|
|
109
118
|
subjectDid: string
|
|
110
119
|
subjectUri: string | null
|
|
111
120
|
subjectBlobCids: string[]
|
|
112
|
-
}): Promise<
|
|
113
|
-
const didDoc = await this.idResolver.did.resolve(
|
|
114
|
-
|
|
115
|
-
if (!didDoc) {
|
|
116
|
-
throw new Error('Error resolving DID')
|
|
117
|
-
}
|
|
121
|
+
}): Promise<void> {
|
|
122
|
+
const didDoc = await this.idResolver.did.resolve(did)
|
|
123
|
+
if (!didDoc) throw new Error('Error resolving DID')
|
|
118
124
|
|
|
119
125
|
const pds = getPdsEndpoint(didDoc)
|
|
126
|
+
if (!pds) throw new Error('Error resolving PDS')
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
throw new Error('Error resolving PDS')
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// attempt to download and upload within the same retry block since the imageStream is not reusable
|
|
126
|
-
const uploadResult = await Promise.all(
|
|
128
|
+
await allFulfilled(
|
|
127
129
|
subjectBlobCids.map((cid) =>
|
|
128
130
|
retryHttp(async () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
return this.uploadBlob(
|
|
135
|
-
{ imageStream, contentType },
|
|
136
|
-
{ subjectDid, subjectUri },
|
|
137
|
-
)
|
|
131
|
+
// attempt to download and upload within the same retry block since
|
|
132
|
+
// the blob stream is not reusable
|
|
133
|
+
const blob = await this.getBlob({ pds, cid, did })
|
|
134
|
+
return this.uploadBlob(blob, { did, uri })
|
|
138
135
|
}),
|
|
139
136
|
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
).catch((err) => {
|
|
138
|
+
throw new XRPCError(
|
|
139
|
+
ResponseType.UpstreamFailure,
|
|
140
|
+
undefined,
|
|
141
|
+
'Failed to process blobs',
|
|
142
|
+
undefined,
|
|
143
|
+
{ cause: err },
|
|
144
|
+
)
|
|
145
|
+
})
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
const basicAuth = (username: string, password: string) => {
|
|
151
150
|
return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
|
|
152
151
|
}
|
|
152
|
+
|
|
153
|
+
type Blob = {
|
|
154
|
+
type: string
|
|
155
|
+
stream: Readable
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type GetBlobOptions = {
|
|
159
|
+
pds: string
|
|
160
|
+
did: string
|
|
161
|
+
cid: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getBlobUrl({ pds, did, cid }: GetBlobOptions): URL {
|
|
165
|
+
const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds)
|
|
166
|
+
url.searchParams.set('did', did)
|
|
167
|
+
url.searchParams.set('cid', cid)
|
|
168
|
+
return url
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type ReportBlobOptions = {
|
|
172
|
+
did: string
|
|
173
|
+
uri: string | null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reportBlobUrl(service: string, { did, uri }: ReportBlobOptions): URL {
|
|
177
|
+
const url = new URL(`/xrpc/com.atproto.unspecced.reportBlob`, service)
|
|
178
|
+
url.searchParams.set('did', did)
|
|
179
|
+
if (uri != null) url.searchParams.set('uri', uri)
|
|
180
|
+
return url
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function asXrpcClientError(err: unknown, message: string) {
|
|
184
|
+
return new XRPCError(ResponseType.Unknown, undefined, message, undefined, {
|
|
185
|
+
cause: err,
|
|
186
|
+
})
|
|
187
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRetryable } from '@atproto/common'
|
|
2
|
+
import { ResponseType, XRPCError } from '@atproto/xrpc'
|
|
2
3
|
import { parseList } from 'structured-headers'
|
|
3
|
-
import { XRPCError, ResponseType } from '@atproto/xrpc'
|
|
4
|
-
import { RetryOptions, retry } from '@atproto/common'
|
|
5
4
|
import Database from './db'
|
|
6
5
|
|
|
7
6
|
export const getSigningKeyId = async (
|
|
@@ -24,28 +23,17 @@ export const getSigningKeyId = async (
|
|
|
24
23
|
return insertRes.id
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
): Promise<T> {
|
|
31
|
-
return retry(fn, { retryable: retryableHttp, ...opts })
|
|
32
|
-
}
|
|
26
|
+
export const RETRYABLE_HTTP_STATUS_CODES = new Set([
|
|
27
|
+
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
28
|
+
])
|
|
33
29
|
|
|
34
|
-
export
|
|
30
|
+
export const retryHttp = createRetryable((err: unknown) => {
|
|
35
31
|
if (err instanceof XRPCError) {
|
|
36
32
|
if (err.status === ResponseType.Unknown) return true
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if (err instanceof AxiosError) {
|
|
40
|
-
if (!err.response) return true
|
|
41
|
-
return retryableHttpStatusCodes.has(err.response.status)
|
|
33
|
+
return RETRYABLE_HTTP_STATUS_CODES.has(err.status)
|
|
42
34
|
}
|
|
43
35
|
return false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const retryableHttpStatusCodes = new Set([
|
|
47
|
-
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
48
|
-
])
|
|
36
|
+
})
|
|
49
37
|
|
|
50
38
|
export type ParsedLabelers = {
|
|
51
39
|
dids: string[]
|
package/tests/_util.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type Express } from 'express'
|
|
2
|
+
import { Server } from 'node:http'
|
|
3
|
+
import { AddressInfo } from 'node:net'
|
|
1
4
|
import { AtUri } from '@atproto/syntax'
|
|
2
5
|
import { lexToJson } from '@atproto/lexicon'
|
|
3
6
|
import { CID } from 'multiformats/cid'
|
|
@@ -195,3 +198,46 @@ export const stripViewerFromThread = <T>(thread: T): T => {
|
|
|
195
198
|
}
|
|
196
199
|
return thread
|
|
197
200
|
}
|
|
201
|
+
|
|
202
|
+
export async function startServer(app: Express) {
|
|
203
|
+
return new Promise<{
|
|
204
|
+
origin: string
|
|
205
|
+
server: Server
|
|
206
|
+
stop: () => Promise<void>
|
|
207
|
+
}>((resolve, reject) => {
|
|
208
|
+
const onListen = () => {
|
|
209
|
+
const port = (server.address() as AddressInfo).port
|
|
210
|
+
resolve({
|
|
211
|
+
server,
|
|
212
|
+
origin: `http://localhost:${port}`,
|
|
213
|
+
stop: () => stopServer(server),
|
|
214
|
+
})
|
|
215
|
+
cleanup()
|
|
216
|
+
}
|
|
217
|
+
const onError = (err: Error) => {
|
|
218
|
+
reject(err)
|
|
219
|
+
cleanup()
|
|
220
|
+
}
|
|
221
|
+
const cleanup = () => {
|
|
222
|
+
server.removeListener('listening', onListen)
|
|
223
|
+
server.removeListener('error', onError)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const server = app
|
|
227
|
+
.listen(0)
|
|
228
|
+
.once('listening', onListen)
|
|
229
|
+
.once('error', onError)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function stopServer(server: Server) {
|
|
234
|
+
return new Promise<void>((resolve, reject) => {
|
|
235
|
+
server.close((err) => {
|
|
236
|
+
if (err) {
|
|
237
|
+
reject(err)
|
|
238
|
+
} else {
|
|
239
|
+
resolve()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
1
|
import {
|
|
3
2
|
ModeratorClient,
|
|
4
3
|
SeedClient,
|
|
5
4
|
TestNetwork,
|
|
6
5
|
basicSeed,
|
|
7
6
|
} from '@atproto/dev-env'
|
|
7
|
+
import { ResponseType, XRPCError } from '@atproto/xrpc'
|
|
8
|
+
import assert from 'node:assert'
|
|
8
9
|
import { forSnapshot } from './_util'
|
|
9
10
|
|
|
10
11
|
describe('blob divert', () => {
|
|
@@ -30,13 +31,16 @@ describe('blob divert', () => {
|
|
|
30
31
|
await network.close()
|
|
31
32
|
})
|
|
32
33
|
|
|
33
|
-
const mockReportServiceResponse = (
|
|
34
|
+
const mockReportServiceResponse = (succeeds: boolean) => {
|
|
34
35
|
const blobDiverter = network.ozone.ctx.blobDiverter
|
|
35
36
|
assert(blobDiverter)
|
|
36
37
|
return jest
|
|
37
|
-
.spyOn(blobDiverter, '
|
|
38
|
+
.spyOn(blobDiverter, 'uploadBlob')
|
|
38
39
|
.mockImplementation(async () => {
|
|
39
|
-
|
|
40
|
+
if (!succeeds) {
|
|
41
|
+
// Using an XRPCError to trigger retries
|
|
42
|
+
throw new XRPCError(ResponseType.Unknown, undefined)
|
|
43
|
+
}
|
|
40
44
|
})
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -46,6 +50,8 @@ describe('blob divert', () => {
|
|
|
46
50
|
cid: sc.posts[sc.dids.carol][0].ref.cidStr,
|
|
47
51
|
})
|
|
48
52
|
|
|
53
|
+
const getImages = () => sc.posts[sc.dids.carol][0].images
|
|
54
|
+
|
|
49
55
|
const emitDivertEvent = async () =>
|
|
50
56
|
modClient.emitEvent(
|
|
51
57
|
{
|
|
@@ -55,9 +61,7 @@ describe('blob divert', () => {
|
|
|
55
61
|
comment: 'Diverting for test',
|
|
56
62
|
},
|
|
57
63
|
createdBy: sc.dids.alice,
|
|
58
|
-
subjectBlobCids:
|
|
59
|
-
img.image.ref.toString(),
|
|
60
|
-
),
|
|
64
|
+
subjectBlobCids: getImages().map((img) => img.image.ref.toString()),
|
|
61
65
|
},
|
|
62
66
|
'moderator',
|
|
63
67
|
)
|
|
@@ -65,25 +69,32 @@ describe('blob divert', () => {
|
|
|
65
69
|
it('fails and keeps attempt count when report service fails to accept upload.', async () => {
|
|
66
70
|
// Simulate failure to fail upload
|
|
67
71
|
const reportServiceRequest = mockReportServiceResponse(false)
|
|
72
|
+
try {
|
|
73
|
+
await expect(emitDivertEvent()).rejects.toThrow('Failed to process blobs')
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// 1 initial attempt + 3 retries
|
|
76
|
+
expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length * 4)
|
|
77
|
+
} finally {
|
|
78
|
+
reportServiceRequest.mockRestore()
|
|
79
|
+
}
|
|
72
80
|
})
|
|
73
81
|
|
|
74
82
|
it('sends blobs to configured divert service and marks divert date', async () => {
|
|
75
|
-
// Simulate
|
|
83
|
+
// Simulate success to accept upload
|
|
76
84
|
const reportServiceRequest = mockReportServiceResponse(true)
|
|
85
|
+
try {
|
|
86
|
+
const divertEvent = await emitDivertEvent()
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length)
|
|
89
|
+
expect(forSnapshot(divertEvent)).toMatchSnapshot()
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const { subjectStatuses } = await modClient.queryStatuses({
|
|
84
|
-
subject: getSubject().uri,
|
|
85
|
-
})
|
|
91
|
+
const { subjectStatuses } = await modClient.queryStatuses({
|
|
92
|
+
subject: getSubject().uri,
|
|
93
|
+
})
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
expect(subjectStatuses[0].takendown).toBe(true)
|
|
96
|
+
} finally {
|
|
97
|
+
reportServiceRequest.mockRestore()
|
|
98
|
+
}
|
|
88
99
|
})
|
|
89
100
|
})
|
package/tests/moderation.test.ts
CHANGED
|
@@ -816,7 +816,9 @@ describe('moderation', () => {
|
|
|
816
816
|
it.skip('prevents image blob from being served, even when cached.', async () => {
|
|
817
817
|
const fetchImage = await fetch(imageUri)
|
|
818
818
|
expect(fetchImage.status).toEqual(404)
|
|
819
|
-
expect(await fetchImage.json()).
|
|
819
|
+
expect(await fetchImage.json()).toMatchObject({
|
|
820
|
+
message: 'Blob not found',
|
|
821
|
+
})
|
|
820
822
|
})
|
|
821
823
|
|
|
822
824
|
it('invalidates the image in the cdn', async () => {
|
package/tests/server.test.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TestNetwork, TestOzone } from '@atproto/dev-env'
|
|
2
2
|
import express from 'express'
|
|
3
|
-
import axios, { AxiosError } from 'axios'
|
|
4
|
-
import { TestOzone, TestNetwork } from '@atproto/dev-env'
|
|
5
3
|
import { handler as errorHandler } from '../src/error'
|
|
4
|
+
import { startServer } from './_util'
|
|
6
5
|
|
|
7
6
|
describe('server', () => {
|
|
8
7
|
let network: TestNetwork
|
|
@@ -20,56 +19,45 @@ describe('server', () => {
|
|
|
20
19
|
})
|
|
21
20
|
|
|
22
21
|
it('preserves 404s.', async () => {
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
const response = await fetch(`${ozone.url}/unknown`)
|
|
23
|
+
expect(response.status).toEqual(404)
|
|
25
24
|
})
|
|
26
25
|
|
|
27
26
|
it('error handler turns unknown errors into 500s.', async () => {
|
|
28
27
|
const app = express()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const promise = axios.get(`http://localhost:${port}/oops`)
|
|
36
|
-
await expect(promise).rejects.toThrow('failed with status code 500')
|
|
37
|
-
srv.close()
|
|
28
|
+
.get('/oops', () => {
|
|
29
|
+
throw new Error('Oops!')
|
|
30
|
+
})
|
|
31
|
+
.use(errorHandler)
|
|
32
|
+
|
|
33
|
+
const { origin, stop } = await startServer(app)
|
|
38
34
|
try {
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
expect(axiosError.response?.status).toEqual(500)
|
|
43
|
-
expect(axiosError.response?.data).toEqual({
|
|
35
|
+
const response = await fetch(new URL(`/oops`, origin))
|
|
36
|
+
expect(response.status).toEqual(500)
|
|
37
|
+
await expect(response.json()).resolves.toEqual({
|
|
44
38
|
error: 'InternalServerError',
|
|
45
39
|
message: 'Internal Server Error',
|
|
46
40
|
})
|
|
41
|
+
} finally {
|
|
42
|
+
await stop()
|
|
47
43
|
}
|
|
48
44
|
})
|
|
49
45
|
|
|
50
46
|
it('healthcheck succeeds when database is available.', async () => {
|
|
51
|
-
const
|
|
52
|
-
expect(status).toEqual(200)
|
|
53
|
-
expect(
|
|
47
|
+
const response = await fetch(`${network.bsky.url}/xrpc/_health`)
|
|
48
|
+
expect(response.status).toEqual(200)
|
|
49
|
+
await expect(response.json()).resolves.toEqual({ version: 'unknown' })
|
|
54
50
|
})
|
|
55
51
|
|
|
56
52
|
it('healthcheck fails when database is unavailable.', async () => {
|
|
57
53
|
// destroy sequencer to release connection that would prevent the db from closing
|
|
58
54
|
await ozone.ctx.sequencer.destroy()
|
|
59
55
|
await ozone.ctx.db.close()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (axios.isAxiosError(err)) {
|
|
66
|
-
error = err
|
|
67
|
-
} else {
|
|
68
|
-
throw err
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
expect(error.response?.status).toEqual(503)
|
|
72
|
-
expect(error.response?.data).toEqual({
|
|
56
|
+
|
|
57
|
+
const res = await fetch(`${ozone.url}/xrpc/_health`)
|
|
58
|
+
|
|
59
|
+
expect(res.status).toEqual(503)
|
|
60
|
+
await expect(res.json()).resolves.toEqual({
|
|
73
61
|
version: '0.0.0',
|
|
74
62
|
error: 'Service Unavailable',
|
|
75
63
|
})
|