@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 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
- private getBlob;
14
- sendImage({ url, imageStream, contentType, }: {
15
- url: string;
16
- imageStream: Readable;
17
- contentType: string;
18
- }): Promise<boolean>;
19
- private uploadBlob;
20
- uploadBlobOnService({ subjectDid, subjectUri, subjectBlobCids, }: {
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<boolean>;
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":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAGjC,OAAO,QAAQ,MAAM,OAAO,CAAA;AAE5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,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;YAMW,OAAO;IA6Bf,SAAS,CAAC,EACd,GAAG,EACH,WAAW,EACX,WAAW,GACZ,EAAE;QACD,GAAG,EAAE,MAAM,CAAA;QACX,WAAW,EAAE,QAAQ,CAAA;QACrB,WAAW,EAAE,MAAM,CAAA;KACpB;YAaa,UAAU;IAwBlB,mBAAmB,CAAC,EACxB,UAAU,EACV,UAAU,EACV,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,OAAO,CAAC;CAoCrB"}
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 __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
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 axios_1 = __importDefault(require("axios"));
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
- async getBlob({ pds, did, cid, }) {
35
- const blobResponse = await axios_1.default.get(`${pds}/xrpc/com.atproto.sync.getBlob`, {
36
- params: { did, cid },
37
- decompress: true,
38
- responseType: 'stream',
39
- timeout: 5000, // 5sec of inactivity on the connection
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
- const imageStream = blobResponse.data;
42
- const verifyCid = new common_1.VerifyCidTransform(cid_1.CID.parse(cid));
43
- (0, common_1.forwardStreamErrors)(imageStream, verifyCid);
44
- return {
45
- contentType: blobResponse.headers['content-type'] || 'application/octet-stream',
46
- imageStream: imageStream.pipe(verifyCid),
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
- async sendImage({ url, imageStream, contentType, }) {
50
- const result = await (0, axios_1.default)(url, {
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
- data: imageStream,
101
+ body: blob.stream,
102
+ headersTimeout: 30e3,
103
+ bodyTimeout: 10e3,
53
104
  headers: {
54
105
  Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
55
- 'Content-Type': contentType,
106
+ 'content-type': blob.type,
56
107
  },
108
+ })
109
+ .catch((err) => {
110
+ throw asXrpcClientError(err, `Error uploading blob ${report.did}`);
57
111
  });
58
- return result.status === 200;
59
- }
60
- async uploadBlob({ imageStream, contentType, }, { subjectDid, subjectUri, }) {
61
- const url = new URL(`${this.serviceConfig.url}/xrpc/com.atproto.unspecced.reportBlob`);
62
- url.searchParams.set('did', subjectDid);
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(subjectDid);
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
- // attempt to download and upload within the same retry block since the imageStream is not reusable
82
- const uploadResult = await Promise.all(subjectBlobCids.map((cid) => (0, util_1.retryHttp)(async () => {
83
- const { imageStream, contentType } = await this.getBlob({
84
- pds,
85
- cid,
86
- did: subjectDid,
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":"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"}
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 function retryHttp<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
5
- export declare function retryableHttp(err: unknown): boolean;
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>;
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAS,MAAM,iBAAiB,CAAA;AACrD,OAAO,QAAQ,MAAM,MAAM,CAAA;AAE3B,eAAO,MAAM,eAAe,OACtB,QAAQ,cACA,MAAM,KACjB,OAAO,CAAC,MAAM,CAehB,CAAA;AAED,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,WAUzC;AAMD,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"}
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
- async function retryHttp(fn, opts = {}) {
28
- return (0, common_1.retry)(fn, { retryable: retryableHttp, ...opts });
29
- }
30
- function retryableHttp(err) {
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 retryableHttpStatusCodes.has(err.status);
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":"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"}
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.64",
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
- "@atproto/api": "^0.13.25",
36
- "@atproto/common": "^0.4.5",
37
- "@atproto/crypto": "^0.4.2",
38
- "@atproto/identity": "^0.4.3",
39
- "@atproto/lexicon": "^0.4.4",
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.5",
42
- "@atproto/xrpc-server": "^0.7.4"
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.4",
56
- "@atproto/pds": "^0.4.81"
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
- VerifyCidTransform,
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 axios from 'axios'
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
- private async getBlob({
31
- pds,
32
- did,
33
- cid,
34
- }: {
35
- pds: string
36
- did: string
37
- cid: string
38
- }) {
39
- const blobResponse = await axios.get(
40
- `${pds}/xrpc/com.atproto.sync.getBlob`,
41
- {
42
- params: { did, cid },
43
- decompress: true,
44
- responseType: 'stream',
45
- timeout: 5000, // 5sec of inactivity on the connection
46
- },
47
- )
48
- const imageStream: Readable = blobResponse.data
49
- const verifyCid = new VerifyCidTransform(CID.parse(cid))
50
- forwardStreamErrors(imageStream, verifyCid)
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
- return result.status === 200
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
- private async uploadBlob(
81
- {
82
- imageStream,
83
- contentType,
84
- }: { imageStream: Readable; contentType: string },
85
- {
86
- subjectDid,
87
- subjectUri,
88
- }: { subjectDid: string; subjectUri: string | null },
89
- ) {
90
- const url = new URL(
91
- `${this.serviceConfig.url}/xrpc/com.atproto.unspecced.reportBlob`,
92
- )
93
- url.searchParams.set('did', subjectDid)
94
- if (subjectUri) url.searchParams.set('uri', subjectUri)
95
- const result = await this.sendImage({
96
- url: url.toString(),
97
- imageStream,
98
- contentType,
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
- return result
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<boolean> {
113
- const didDoc = await this.idResolver.did.resolve(subjectDid)
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
- if (!pds) {
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
- const { imageStream, contentType } = await this.getBlob({
130
- pds,
131
- cid,
132
- did: subjectDid,
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
- if (uploadResult.includes(false)) {
143
- throw new Error(`Error uploading blob ${subjectUri}`)
144
- }
145
-
146
- return true
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 { AxiosError } from 'axios'
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 async function retryHttp<T>(
28
- fn: () => Promise<T>,
29
- opts: RetryOptions = {},
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 function retryableHttp(err: unknown) {
30
+ export const retryHttp = createRetryable((err: unknown) => {
35
31
  if (err instanceof XRPCError) {
36
32
  if (err.status === ResponseType.Unknown) return true
37
- return retryableHttpStatusCodes.has(err.status)
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 = (result: boolean) => {
34
+ const mockReportServiceResponse = (succeeds: boolean) => {
34
35
  const blobDiverter = network.ozone.ctx.blobDiverter
35
36
  assert(blobDiverter)
36
37
  return jest
37
- .spyOn(blobDiverter, 'sendImage')
38
+ .spyOn(blobDiverter, 'uploadBlob')
38
39
  .mockImplementation(async () => {
39
- return result
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: sc.posts[sc.dids.carol][0].images.map((img) =>
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
- await expect(emitDivertEvent()).rejects.toThrow()
70
-
71
- expect(reportServiceRequest).toHaveBeenCalled()
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 failure to accept upload
83
+ // Simulate success to accept upload
76
84
  const reportServiceRequest = mockReportServiceResponse(true)
85
+ try {
86
+ const divertEvent = await emitDivertEvent()
77
87
 
78
- const divertEvent = await emitDivertEvent()
88
+ expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length)
89
+ expect(forSnapshot(divertEvent)).toMatchSnapshot()
79
90
 
80
- expect(reportServiceRequest).toHaveBeenCalled()
81
- expect(forSnapshot(divertEvent)).toMatchSnapshot()
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
- expect(subjectStatuses[0].takendown).toBe(true)
95
+ expect(subjectStatuses[0].takendown).toBe(true)
96
+ } finally {
97
+ reportServiceRequest.mockRestore()
98
+ }
88
99
  })
89
100
  })
@@ -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()).toEqual({ message: 'Image not found' })
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 () => {
@@ -1,8 +1,7 @@
1
- import { AddressInfo } from 'net'
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 promise = axios.get(`${ozone.url}/unknown`)
24
- await expect(promise).rejects.toThrow('failed with status code 404')
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
- app.get('/oops', () => {
30
- throw new Error('Oops!')
31
- })
32
- app.use(errorHandler)
33
- const srv = app.listen()
34
- const port = (srv.address() as AddressInfo).port
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 promise
40
- } catch (err: unknown) {
41
- const axiosError = err as AxiosError
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 { data, status } = await axios.get(`${ozone.url}/xrpc/_health`)
52
- expect(status).toEqual(200)
53
- expect(data).toEqual({ version: '0.0.0' })
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
- let error: AxiosError
61
- try {
62
- await axios.get(`${ozone.url}/xrpc/_health`)
63
- throw new Error('Healthcheck should have failed')
64
- } catch (err) {
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
  })