@atproto/aws 0.3.1 → 0.3.3

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,33 @@
1
1
  # @atproto/aws
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
8
+
9
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
10
+
11
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build with `noImplicitAny` enabled
12
+
13
+ - Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
14
+ - @atproto/common-web@0.5.3
15
+ - @atproto/common@0.6.5
16
+ - @atproto/crypto@0.5.3
17
+ - @atproto/repo@0.10.3
18
+
19
+ ## 0.3.2
20
+
21
+ ### Patch Changes
22
+
23
+ - [#5151](https://github.com/bluesky-social/atproto/pull/5151) [`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update dependencies
24
+
25
+ - Updated dependencies [[`f2cf8f7`](https://github.com/bluesky-social/atproto/commit/f2cf8f7fc5f3a10847f2e6d785e5fa2244ee8cfb), [`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7), [`f2cf8f7`](https://github.com/bluesky-social/atproto/commit/f2cf8f7fc5f3a10847f2e6d785e5fa2244ee8cfb)]:
26
+ - @atproto/common@0.6.4
27
+ - @atproto/common-web@0.5.2
28
+ - @atproto/crypto@0.5.2
29
+ - @atproto/repo@0.10.2
30
+
3
31
  ## 0.3.1
4
32
 
5
33
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/aws",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "description": "Shared AWS cloud API helpers for atproto services",
6
6
  "keywords": [
@@ -13,6 +13,18 @@
13
13
  "url": "https://github.com/bluesky-social/atproto",
14
14
  "directory": "packages/aws"
15
15
  },
16
+ "files": [
17
+ "./dist",
18
+ "./README.md",
19
+ "./CHANGELOG.md"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
16
28
  "engines": {
17
29
  "node": ">=22"
18
30
  },
@@ -25,18 +37,10 @@
25
37
  "key-encoder": "^2.0.3",
26
38
  "multiformats": "^13.0.0",
27
39
  "uint8arrays": "^5.0.0",
28
- "@atproto/crypto": "^0.5.1",
29
- "@atproto/common-web": "^0.5.1",
30
- "@atproto/repo": "^0.10.1",
31
- "@atproto/common": "^0.6.3"
32
- },
33
- "devDependencies": {},
34
- "type": "module",
35
- "exports": {
36
- ".": {
37
- "types": "./dist/index.d.ts",
38
- "default": "./dist/index.js"
39
- }
40
+ "@atproto/common": "^0.6.5",
41
+ "@atproto/common-web": "^0.5.3",
42
+ "@atproto/crypto": "^0.5.3",
43
+ "@atproto/repo": "^0.10.3"
40
44
  },
41
45
  "scripts": {
42
46
  "build": "tsgo --build tsconfig.build.json"
package/src/bunny.ts DELETED
@@ -1,35 +0,0 @@
1
- import { allFulfilled } from '@atproto/common'
2
- import { ImageInvalidator } from './types.js'
3
-
4
- export type BunnyConfig = {
5
- accessKey: string
6
- urlPrefix: string
7
- }
8
-
9
- const API_PURGE_URL = 'https://api.bunny.net/purge'
10
-
11
- export class BunnyInvalidator implements ImageInvalidator {
12
- constructor(public cfg: BunnyConfig) {}
13
- async invalidate(_subject: string, paths: string[]) {
14
- await allFulfilled(
15
- paths.map(async (path) =>
16
- purgeUrl({
17
- url: this.cfg.urlPrefix + path,
18
- accessKey: this.cfg.accessKey,
19
- }),
20
- ),
21
- )
22
- }
23
- }
24
-
25
- export default BunnyInvalidator
26
-
27
- async function purgeUrl(opts: { accessKey: string; url: string }) {
28
- const search = new URLSearchParams()
29
- search.set('async', 'true')
30
- search.set('url', opts.url)
31
- await fetch(API_PURGE_URL + '?' + search.toString(), {
32
- method: 'post',
33
- headers: { AccessKey: opts.accessKey },
34
- })
35
- }
package/src/cloudfront.ts DELETED
@@ -1,36 +0,0 @@
1
- import * as aws from '@aws-sdk/client-cloudfront'
2
- import { ImageInvalidator } from './types.js'
3
-
4
- export type CloudfrontConfig = {
5
- distributionId: string
6
- pathPrefix?: string
7
- } & Omit<aws.CloudFrontClientConfig, 'apiVersion'>
8
-
9
- export class CloudfrontInvalidator implements ImageInvalidator {
10
- distributionId: string
11
- pathPrefix: string
12
- client: aws.CloudFront
13
- constructor(cfg: CloudfrontConfig) {
14
- const { distributionId, pathPrefix, ...rest } = cfg
15
- this.distributionId = distributionId
16
- this.pathPrefix = pathPrefix ?? ''
17
- this.client = new aws.CloudFront({
18
- ...rest,
19
- apiVersion: '2020-05-31',
20
- })
21
- }
22
- async invalidate(subject: string, paths: string[]) {
23
- await this.client.createInvalidation({
24
- DistributionId: this.distributionId,
25
- InvalidationBatch: {
26
- CallerReference: `cf-invalidator-${subject}-${Date.now()}`,
27
- Paths: {
28
- Quantity: paths.length,
29
- Items: paths.map((path) => this.pathPrefix + path),
30
- },
31
- },
32
- })
33
- }
34
- }
35
-
36
- export default CloudfrontInvalidator
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './kms.js'
2
- export * from './s3.js'
3
- export * from './cloudfront.js'
4
- export * from './bunny.js'
5
- export * from './util.js'
6
- export * from './types.js'
package/src/kms.ts DELETED
@@ -1,70 +0,0 @@
1
- import * as aws from '@aws-sdk/client-kms'
2
- import { secp256k1 as noble } from '@noble/curves/secp256k1'
3
- import KeyEncoderModule from 'key-encoder'
4
- import * as ui8 from 'uint8arrays'
5
- import * as crypto from '@atproto/crypto'
6
-
7
- // key-encoder is CJS with exports.default; Node ESM interop wraps it as { default: Class }
8
- const KeyEncoder = ((m) => m.default ?? m)(KeyEncoderModule)
9
-
10
- const keyEncoder = new KeyEncoder('secp256k1')
11
-
12
- export type KmsConfig = { keyId: string } & Omit<
13
- aws.KMSClientConfig,
14
- 'apiVersion'
15
- >
16
-
17
- export class KmsKeypair implements crypto.Keypair {
18
- jwtAlg = crypto.SECP256K1_JWT_ALG
19
-
20
- constructor(
21
- private client: aws.KMS,
22
- private keyId: string,
23
- private publicKey: Uint8Array,
24
- ) {}
25
-
26
- static async load(cfg: KmsConfig) {
27
- const { keyId, ...rest } = cfg
28
- const client = new aws.KMS({
29
- ...rest,
30
- apiVersion: '2014-11-01',
31
- })
32
- const res = await client.getPublicKey({ KeyId: keyId })
33
- if (!res.PublicKey) {
34
- throw new Error('Could not find public key')
35
- }
36
- // public key comes back DER-encoded, so we translate it to raw 65 byte encoding
37
- const rawPublicKeyHex = keyEncoder.encodePublic(
38
- Buffer.from(res.PublicKey),
39
- 'der',
40
- 'raw',
41
- )
42
- const publicKey = ui8.fromString(rawPublicKeyHex, 'hex')
43
- return new KmsKeypair(client, keyId, publicKey)
44
- }
45
-
46
- did(): string {
47
- return crypto.formatDidKey(this.jwtAlg, this.publicKey)
48
- }
49
-
50
- async sign(msg: Uint8Array): Promise<Uint8Array> {
51
- const res = await this.client.sign({
52
- KeyId: this.keyId,
53
- Message: msg,
54
- SigningAlgorithm: 'ECDSA_SHA_256',
55
- })
56
- if (!res.Signature) {
57
- throw new Error('Could not get signature')
58
- }
59
- // signature comes back DER encoded & not-normalized
60
- // we translate to raw 64 byte encoding
61
- // we also normalize s as no more than 1/2 prime order to pass strict verification
62
- // (prevents duplicating a signature)
63
- // more: https://github.com/bitcoin-core/secp256k1/blob/a1102b12196ea27f44d6201de4d25926a2ae9640/include/secp256k1.h#L530-L534
64
- const sig = noble.Signature.fromDER(res.Signature)
65
- const normalized = sig.normalizeS()
66
- return normalized.toCompactRawBytes()
67
- }
68
- }
69
-
70
- export default KmsKeypair
package/src/s3.ts DELETED
@@ -1,267 +0,0 @@
1
- import stream from 'node:stream'
2
- import { NoSuchKey, S3, S3ClientConfig } from '@aws-sdk/client-s3'
3
- import { Upload } from '@aws-sdk/lib-storage'
4
- import { CID } from 'multiformats/cid'
5
- import { SECOND, aggregateErrors, chunkArray } from '@atproto/common-web'
6
- import { randomStr } from '@atproto/crypto'
7
- import { BlobNotFoundError, BlobStore } from '@atproto/repo'
8
-
9
- export type S3Config = {
10
- bucket: string
11
- /**
12
- * The maximum time any request to S3 (including individual blob chunks
13
- * uploads) can take, in milliseconds.
14
- */
15
- requestTimeoutMs?: number
16
- /**
17
- * The maximum total time a blob upload can take, in milliseconds.
18
- */
19
- uploadTimeoutMs?: number
20
- } & Omit<S3ClientConfig, 'apiVersion' | 'requestHandler'>
21
-
22
- export class S3BlobStore implements BlobStore {
23
- private client: S3
24
- private bucket: string
25
- private uploadTimeoutMs: number
26
-
27
- constructor(
28
- public did: string,
29
- cfg: S3Config,
30
- ) {
31
- const {
32
- bucket,
33
- uploadTimeoutMs = 10 * SECOND,
34
- requestTimeoutMs = uploadTimeoutMs,
35
- ...rest
36
- } = cfg
37
- this.bucket = bucket
38
- this.uploadTimeoutMs = uploadTimeoutMs
39
- this.client = new S3({
40
- ...rest,
41
- apiVersion: '2006-03-01',
42
- // Ensures that all requests timeout under "requestTimeoutMs".
43
- //
44
- // @NOTE This will also apply to the upload of each individual chunk
45
- // when using Upload from @aws-sdk/lib-storage.
46
- requestHandler: { requestTimeout: requestTimeoutMs },
47
- })
48
- }
49
-
50
- static creator(cfg: S3Config) {
51
- return (did: string) => {
52
- return new S3BlobStore(did, cfg)
53
- }
54
- }
55
-
56
- private genKey() {
57
- return randomStr(32, 'base32')
58
- }
59
-
60
- private getTmpPath(key: string): string {
61
- return `tmp/${this.did}/${key}`
62
- }
63
-
64
- private getStoredPath(cid: CID): string {
65
- return `blocks/${this.did}/${cid.toString()}`
66
- }
67
-
68
- private getQuarantinedPath(cid: CID): string {
69
- return `quarantine/${this.did}/${cid.toString()}`
70
- }
71
-
72
- private async uploadBytes(path: string, bytes: Uint8Array | stream.Readable) {
73
- // @NOTE we use Upload rather than client.putObject because stream length is
74
- // not known in advance. See also aws/aws-sdk-js-v3#2348.
75
- //
76
- // See also https://github.com/aws/aws-sdk-js-v3/issues/6426, wherein Upload
77
- // may hang the s3 connection under certain circumstances. We don't have a
78
- // good way to avoid this, so we use timeouts defensively on all s3
79
- // requests.
80
-
81
- const abortSignal = AbortSignal.timeout(this.uploadTimeoutMs)
82
- const abortController = new AbortController()
83
- abortSignal.addEventListener('abort', () => abortController.abort())
84
-
85
- const upload = new Upload({
86
- client: this.client,
87
- params: {
88
- Bucket: this.bucket,
89
- Body: bytes,
90
- Key: path,
91
- },
92
- // @ts-ignore native implementation fine in node >=15
93
- abortController,
94
- })
95
-
96
- try {
97
- await upload.done()
98
- } catch (err) {
99
- // Translate aws-sdk's abort error to something more specific
100
- if (err instanceof Error && err.name === 'AbortError') {
101
- throw new Error('Blob upload timed out', { cause: err })
102
- }
103
-
104
- throw err
105
- }
106
- }
107
-
108
- async putTemp(bytes: Uint8Array | stream.Readable): Promise<string> {
109
- const key = this.genKey()
110
- await this.uploadBytes(this.getTmpPath(key), bytes)
111
- return key
112
- }
113
-
114
- async makePermanent(key: string, cid: CID): Promise<void> {
115
- try {
116
- // @NOTE we normally call this method when we know the file is temporary.
117
- // Because of this, we optimistically move the file, allowing to make
118
- // fewer network requests in the happy path.
119
- await this.move({
120
- from: this.getTmpPath(key),
121
- to: this.getStoredPath(cid),
122
- })
123
- } catch (err) {
124
- // If the optimistic move failed because the temp file was not found,
125
- // check if the permanent file already exists. If it does, we can assume
126
- // that another process made the file permanent concurrently, and we can
127
- // no-op.
128
- if (err instanceof BlobNotFoundError) {
129
- // Blob was not found from temp storage...
130
- const alreadyHas = await this.hasStored(cid)
131
- // already saved, so we no-op
132
- if (alreadyHas) return
133
- }
134
-
135
- throw err
136
- }
137
- }
138
-
139
- async putPermanent(
140
- cid: CID,
141
- bytes: Uint8Array | stream.Readable,
142
- ): Promise<void> {
143
- await this.uploadBytes(this.getStoredPath(cid), bytes)
144
- }
145
-
146
- async quarantine(cid: CID): Promise<void> {
147
- await this.move({
148
- from: this.getStoredPath(cid),
149
- to: this.getQuarantinedPath(cid),
150
- })
151
- }
152
-
153
- async unquarantine(cid: CID): Promise<void> {
154
- await this.move({
155
- from: this.getQuarantinedPath(cid),
156
- to: this.getStoredPath(cid),
157
- })
158
- }
159
-
160
- private async getObject(cid: CID) {
161
- const res = await this.client.getObject({
162
- Bucket: this.bucket,
163
- Key: this.getStoredPath(cid),
164
- })
165
- if (res.Body) {
166
- return res.Body
167
- } else {
168
- throw new BlobNotFoundError()
169
- }
170
- }
171
-
172
- async getBytes(cid: CID): Promise<Uint8Array> {
173
- const res = await this.getObject(cid)
174
- return res.transformToByteArray()
175
- }
176
-
177
- async getStream(cid: CID): Promise<stream.Readable> {
178
- const res = await this.getObject(cid)
179
- return res as stream.Readable
180
- }
181
-
182
- async delete(cid: CID): Promise<void> {
183
- await this.deleteKey(this.getStoredPath(cid))
184
- }
185
-
186
- async deleteMany(cids: CID[]): Promise<void> {
187
- const errors: unknown[] = []
188
- for (const chunk of chunkArray(cids, 500)) {
189
- try {
190
- const keys = chunk.map((cid) => this.getStoredPath(cid))
191
- await this.deleteManyKeys(keys)
192
- } catch (err) {
193
- errors.push(err)
194
- }
195
- }
196
- if (errors.length) throw aggregateErrors(errors)
197
- }
198
-
199
- async hasStored(cid: CID): Promise<boolean> {
200
- return this.hasKey(this.getStoredPath(cid))
201
- }
202
-
203
- async hasTemp(key: string): Promise<boolean> {
204
- return this.hasKey(this.getTmpPath(key))
205
- }
206
-
207
- private async hasKey(key: string) {
208
- try {
209
- const res = await this.client.headObject({
210
- Bucket: this.bucket,
211
- Key: key,
212
- })
213
- return res.$metadata.httpStatusCode === 200
214
- } catch (err) {
215
- return false
216
- }
217
- }
218
-
219
- private async deleteKey(key: string) {
220
- await this.client.deleteObject({
221
- Bucket: this.bucket,
222
- Key: key,
223
- })
224
- }
225
-
226
- private async deleteManyKeys(keys: string[]) {
227
- await this.client.deleteObjects({
228
- Bucket: this.bucket,
229
- Delete: {
230
- Objects: keys.map((k) => ({ Key: k })),
231
- },
232
- })
233
- }
234
-
235
- private async move(keys: { from: string; to: string }) {
236
- try {
237
- await this.client.copyObject({
238
- Bucket: this.bucket,
239
- CopySource: `${this.bucket}/${keys.from}`,
240
- Key: keys.to,
241
- })
242
- } catch (cause) {
243
- if (cause instanceof NoSuchKey) {
244
- // Already deleted, possibly by a concurrently running process
245
- throw new BlobNotFoundError(undefined, { cause })
246
- }
247
-
248
- throw cause
249
- }
250
-
251
- try {
252
- await this.client.deleteObject({
253
- Bucket: this.bucket,
254
- Key: keys.from,
255
- })
256
- } catch (err) {
257
- if (err instanceof NoSuchKey) {
258
- // Already deleted, possibly by a concurrently running process
259
- return
260
- }
261
-
262
- throw err
263
- }
264
- }
265
- }
266
-
267
- export default S3BlobStore
package/src/types.ts DELETED
@@ -1,5 +0,0 @@
1
- // @NOTE keep in sync with same interface in bsky/src/image/invalidator.ts
2
- // this is separate to avoid the dependency on @atproto/bsky.
3
- export interface ImageInvalidator {
4
- invalidate(subject: string, paths: string[]): Promise<void>
5
- }
package/src/util.ts DELETED
@@ -1,13 +0,0 @@
1
- import { allFulfilled } from '@atproto/common'
2
- import { ImageInvalidator } from './types.js'
3
-
4
- export class MultiImageInvalidator implements ImageInvalidator {
5
- constructor(public invalidators: ImageInvalidator[]) {}
6
- async invalidate(subject: string, paths: string[]) {
7
- await allFulfilled(
8
- this.invalidators.map((invalidator) =>
9
- invalidator.invalidate(subject, paths),
10
- ),
11
- )
12
- }
13
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig/isomorphic.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- },
7
- "include": ["./src"],
8
- }
@@ -1 +0,0 @@
1
- {"version":"7.0.0-dev.20260614.1","root":["./src/bunny.ts","./src/cloudfront.ts","./src/index.ts","./src/kms.ts","./src/s3.ts","./src/types.ts","./src/util.ts"]}
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }],
4
- }