@atproto/aws 0.0.1-beta.0
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/README.md +3 -0
- package/build.js +22 -0
- package/dist/index.js +162357 -0
- package/dist/index.js.map +7 -0
- package/dist/src/cloudfront.d.ts +16 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/kms.d.ts +16 -0
- package/dist/src/s3.d.ts +30 -0
- package/package.json +30 -0
- package/src/cloudfront.ts +41 -0
- package/src/index.ts +3 -0
- package/src/kms.ts +66 -0
- package/src/s3.ts +157 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as aws from '@aws-sdk/client-cloudfront';
|
|
2
|
+
export declare type CloudfrontConfig = {
|
|
3
|
+
distributionId: string;
|
|
4
|
+
pathPrefix?: string;
|
|
5
|
+
} & Omit<aws.CloudFrontClientConfig, 'apiVersion'>;
|
|
6
|
+
export declare class CloudfrontInvalidator implements ImageInvalidator {
|
|
7
|
+
distributionId: string;
|
|
8
|
+
pathPrefix: string;
|
|
9
|
+
client: aws.CloudFront;
|
|
10
|
+
constructor(cfg: CloudfrontConfig);
|
|
11
|
+
invalidate(subject: string, paths: string[]): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export default CloudfrontInvalidator;
|
|
14
|
+
interface ImageInvalidator {
|
|
15
|
+
invalidate(subject: string, paths: string[]): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as aws from '@aws-sdk/client-kms';
|
|
2
|
+
import * as crypto from '@atproto/crypto';
|
|
3
|
+
export declare type KmsConfig = {
|
|
4
|
+
keyId: string;
|
|
5
|
+
} & Omit<aws.KMSClientConfig, 'apiVersion'>;
|
|
6
|
+
export declare class KmsKeypair implements crypto.Keypair {
|
|
7
|
+
private client;
|
|
8
|
+
private keyId;
|
|
9
|
+
private publicKey;
|
|
10
|
+
jwtAlg: string;
|
|
11
|
+
constructor(client: aws.KMS, keyId: string, publicKey: Uint8Array);
|
|
12
|
+
static load(cfg: KmsConfig): Promise<KmsKeypair>;
|
|
13
|
+
did(): string;
|
|
14
|
+
sign(msg: Uint8Array): Promise<Uint8Array>;
|
|
15
|
+
}
|
|
16
|
+
export default KmsKeypair;
|
package/dist/src/s3.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import * as aws from '@aws-sdk/client-s3';
|
|
3
|
+
import { BlobStore } from '@atproto/repo';
|
|
4
|
+
import { CID } from 'multiformats/cid';
|
|
5
|
+
import stream from 'stream';
|
|
6
|
+
export declare type S3Config = {
|
|
7
|
+
bucket: string;
|
|
8
|
+
} & Omit<aws.S3ClientConfig, 'apiVersion'>;
|
|
9
|
+
export declare class S3BlobStore implements BlobStore {
|
|
10
|
+
private client;
|
|
11
|
+
private bucket;
|
|
12
|
+
constructor(cfg: S3Config);
|
|
13
|
+
private genKey;
|
|
14
|
+
private getTmpPath;
|
|
15
|
+
private getStoredPath;
|
|
16
|
+
private getQuarantinedPath;
|
|
17
|
+
putTemp(bytes: Uint8Array | stream.Readable): Promise<string>;
|
|
18
|
+
makePermanent(key: string, cid: CID): Promise<void>;
|
|
19
|
+
putPermanent(cid: CID, bytes: Uint8Array | stream.Readable): Promise<void>;
|
|
20
|
+
quarantine(cid: CID): Promise<void>;
|
|
21
|
+
unquarantine(cid: CID): Promise<void>;
|
|
22
|
+
private getObject;
|
|
23
|
+
getBytes(cid: CID): Promise<Uint8Array>;
|
|
24
|
+
getStream(cid: CID): Promise<stream.Readable>;
|
|
25
|
+
delete(cid: CID): Promise<void>;
|
|
26
|
+
hasStored(cid: CID): Promise<boolean>;
|
|
27
|
+
private deleteKey;
|
|
28
|
+
private move;
|
|
29
|
+
}
|
|
30
|
+
export default S3BlobStore;
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/aws",
|
|
3
|
+
"version": "0.0.1-beta.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/bluesky-social/atproto.git",
|
|
9
|
+
"directory": "packages/aws"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prettier": "prettier --check src/",
|
|
13
|
+
"prettier:fix": "prettier --write src/",
|
|
14
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
15
|
+
"lint:fix": "yarn lint --fix",
|
|
16
|
+
"verify": "run-p prettier lint",
|
|
17
|
+
"verify:fix": "yarn prettier:fix && yarn lint:fix",
|
|
18
|
+
"build": "node ./build.js",
|
|
19
|
+
"postbuild": "tsc --build tsconfig.build.json"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@atproto/crypto": "*",
|
|
23
|
+
"@aws-sdk/client-cloudfront": "^3.261.0",
|
|
24
|
+
"@aws-sdk/client-kms": "^3.196.0",
|
|
25
|
+
"@aws-sdk/client-s3": "^3.224.0",
|
|
26
|
+
"@aws-sdk/lib-storage": "^3.226.0",
|
|
27
|
+
"@noble/secp256k1": "^1.7.0",
|
|
28
|
+
"key-encoder": "^2.0.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as aws from '@aws-sdk/client-cloudfront'
|
|
2
|
+
|
|
3
|
+
export type CloudfrontConfig = {
|
|
4
|
+
distributionId: string
|
|
5
|
+
pathPrefix?: string
|
|
6
|
+
} & Omit<aws.CloudFrontClientConfig, 'apiVersion'>
|
|
7
|
+
|
|
8
|
+
export class CloudfrontInvalidator implements ImageInvalidator {
|
|
9
|
+
distributionId: string
|
|
10
|
+
pathPrefix: string
|
|
11
|
+
client: aws.CloudFront
|
|
12
|
+
constructor(cfg: CloudfrontConfig) {
|
|
13
|
+
const { distributionId, pathPrefix, ...rest } = cfg
|
|
14
|
+
this.distributionId = distributionId
|
|
15
|
+
this.pathPrefix = pathPrefix ?? ''
|
|
16
|
+
this.client = new aws.CloudFront({
|
|
17
|
+
...rest,
|
|
18
|
+
apiVersion: '2020-05-31',
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
async invalidate(subject: string, paths: string[]) {
|
|
22
|
+
await this.client.createInvalidation({
|
|
23
|
+
DistributionId: this.distributionId,
|
|
24
|
+
InvalidationBatch: {
|
|
25
|
+
CallerReference: `cf-invalidator-${subject}-${Date.now()}`,
|
|
26
|
+
Paths: {
|
|
27
|
+
Quantity: paths.length,
|
|
28
|
+
Items: paths.map((path) => this.pathPrefix + path),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default CloudfrontInvalidator
|
|
36
|
+
|
|
37
|
+
// @NOTE keep in sync with same interface in pds/src/image/invalidator.ts
|
|
38
|
+
// this is separate to avoid the dependency on @atproto/pds.
|
|
39
|
+
interface ImageInvalidator {
|
|
40
|
+
invalidate(subject: string, paths: string[]): Promise<void>
|
|
41
|
+
}
|
package/src/index.ts
ADDED
package/src/kms.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as aws from '@aws-sdk/client-kms'
|
|
2
|
+
import * as secp from '@noble/secp256k1'
|
|
3
|
+
import * as crypto from '@atproto/crypto'
|
|
4
|
+
import KeyEncoder from 'key-encoder'
|
|
5
|
+
|
|
6
|
+
const keyEncoder = new KeyEncoder('secp256k1')
|
|
7
|
+
|
|
8
|
+
export type KmsConfig = { keyId: string } & Omit<
|
|
9
|
+
aws.KMSClientConfig,
|
|
10
|
+
'apiVersion'
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export class KmsKeypair implements crypto.Keypair {
|
|
14
|
+
jwtAlg = crypto.SECP256K1_JWT_ALG
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private client: aws.KMS,
|
|
18
|
+
private keyId: string,
|
|
19
|
+
private publicKey: Uint8Array,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
static async load(cfg: KmsConfig) {
|
|
23
|
+
const { keyId, ...rest } = cfg
|
|
24
|
+
const client = new aws.KMS({
|
|
25
|
+
...rest,
|
|
26
|
+
apiVersion: '2014-11-01',
|
|
27
|
+
})
|
|
28
|
+
const res = await client.getPublicKey({ KeyId: keyId })
|
|
29
|
+
if (!res.PublicKey) {
|
|
30
|
+
throw new Error('Could not find public key')
|
|
31
|
+
}
|
|
32
|
+
// public key comes back DER-encoded, so we translate it to raw 65 byte encoding
|
|
33
|
+
const rawPublicKeyHex = keyEncoder.encodePublic(
|
|
34
|
+
Buffer.from(res.PublicKey),
|
|
35
|
+
'der',
|
|
36
|
+
'raw',
|
|
37
|
+
)
|
|
38
|
+
const publicKey = secp.utils.hexToBytes(rawPublicKeyHex)
|
|
39
|
+
return new KmsKeypair(client, keyId, publicKey)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
did(): string {
|
|
43
|
+
return crypto.formatDidKey(this.jwtAlg, this.publicKey)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async sign(msg: Uint8Array): Promise<Uint8Array> {
|
|
47
|
+
const res = await this.client.sign({
|
|
48
|
+
KeyId: this.keyId,
|
|
49
|
+
Message: msg,
|
|
50
|
+
SigningAlgorithm: 'ECDSA_SHA_256',
|
|
51
|
+
})
|
|
52
|
+
if (!res.Signature) {
|
|
53
|
+
throw new Error('Could not get signature')
|
|
54
|
+
}
|
|
55
|
+
// signature comes back DER encoded & not-normalized
|
|
56
|
+
// we translate to raw 64 byte encoding
|
|
57
|
+
// we also normalize s as no more than 1/2 prime order to pass strict verification
|
|
58
|
+
// (prevents duplicating a signature)
|
|
59
|
+
// more: https://github.com/bitcoin-core/secp256k1/blob/a1102b12196ea27f44d6201de4d25926a2ae9640/include/secp256k1.h#L530-L534
|
|
60
|
+
const sig = secp.Signature.fromDER(res.Signature)
|
|
61
|
+
const normalized = sig.normalizeS()
|
|
62
|
+
return normalized.toCompactRawBytes()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default KmsKeypair
|
package/src/s3.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as aws from '@aws-sdk/client-s3'
|
|
2
|
+
import { Upload } from '@aws-sdk/lib-storage'
|
|
3
|
+
import { BlobStore, BlobNotFoundError } from '@atproto/repo'
|
|
4
|
+
import { randomStr } from '@atproto/crypto'
|
|
5
|
+
import { CID } from 'multiformats/cid'
|
|
6
|
+
import stream from 'stream'
|
|
7
|
+
|
|
8
|
+
export type S3Config = { bucket: string } & Omit<
|
|
9
|
+
aws.S3ClientConfig,
|
|
10
|
+
'apiVersion'
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
// @NOTE we use Upload rather than client.putObject because stream
|
|
14
|
+
// length is not known in advance. See also aws/aws-sdk-js-v3#2348.
|
|
15
|
+
|
|
16
|
+
export class S3BlobStore implements BlobStore {
|
|
17
|
+
private client: aws.S3
|
|
18
|
+
private bucket: string
|
|
19
|
+
|
|
20
|
+
constructor(cfg: S3Config) {
|
|
21
|
+
const { bucket, ...rest } = cfg
|
|
22
|
+
this.bucket = bucket
|
|
23
|
+
this.client = new aws.S3({
|
|
24
|
+
...rest,
|
|
25
|
+
apiVersion: '2006-03-01',
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private genKey() {
|
|
30
|
+
return randomStr(32, 'base32')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private getTmpPath(key: string): string {
|
|
34
|
+
return `tmp/${key}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private getStoredPath(cid: CID): string {
|
|
38
|
+
return `blocks/${cid.toString()}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private getQuarantinedPath(cid: CID): string {
|
|
42
|
+
return `quarantine/${cid.toString()}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async putTemp(bytes: Uint8Array | stream.Readable): Promise<string> {
|
|
46
|
+
const key = this.genKey()
|
|
47
|
+
await new Upload({
|
|
48
|
+
client: this.client,
|
|
49
|
+
params: {
|
|
50
|
+
Bucket: this.bucket,
|
|
51
|
+
Body: bytes,
|
|
52
|
+
Key: this.getTmpPath(key),
|
|
53
|
+
},
|
|
54
|
+
}).done()
|
|
55
|
+
return key
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async makePermanent(key: string, cid: CID): Promise<void> {
|
|
59
|
+
const alreadyHas = await this.hasStored(cid)
|
|
60
|
+
if (!alreadyHas) {
|
|
61
|
+
await this.move({
|
|
62
|
+
from: this.getTmpPath(key),
|
|
63
|
+
to: this.getStoredPath(cid),
|
|
64
|
+
})
|
|
65
|
+
} else {
|
|
66
|
+
// already saved, so we no-op & just delete the temp
|
|
67
|
+
await this.deleteKey(this.getTmpPath(key))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async putPermanent(
|
|
72
|
+
cid: CID,
|
|
73
|
+
bytes: Uint8Array | stream.Readable,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
await new Upload({
|
|
76
|
+
client: this.client,
|
|
77
|
+
params: {
|
|
78
|
+
Bucket: this.bucket,
|
|
79
|
+
Body: bytes,
|
|
80
|
+
Key: this.getStoredPath(cid),
|
|
81
|
+
},
|
|
82
|
+
}).done()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async quarantine(cid: CID): Promise<void> {
|
|
86
|
+
await this.move({
|
|
87
|
+
from: this.getStoredPath(cid),
|
|
88
|
+
to: this.getQuarantinedPath(cid),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async unquarantine(cid: CID): Promise<void> {
|
|
93
|
+
await this.move({
|
|
94
|
+
from: this.getQuarantinedPath(cid),
|
|
95
|
+
to: this.getStoredPath(cid),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async getObject(cid: CID) {
|
|
100
|
+
const res = await this.client.getObject({
|
|
101
|
+
Bucket: this.bucket,
|
|
102
|
+
Key: this.getStoredPath(cid),
|
|
103
|
+
})
|
|
104
|
+
if (res.Body) {
|
|
105
|
+
return res.Body
|
|
106
|
+
} else {
|
|
107
|
+
throw new BlobNotFoundError()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getBytes(cid: CID): Promise<Uint8Array> {
|
|
112
|
+
const res = await this.getObject(cid)
|
|
113
|
+
return res.transformToByteArray()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getStream(cid: CID): Promise<stream.Readable> {
|
|
117
|
+
const res = await this.getObject(cid)
|
|
118
|
+
return res as stream.Readable
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async delete(cid: CID): Promise<void> {
|
|
122
|
+
await this.deleteKey(this.getStoredPath(cid))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async hasStored(cid: CID): Promise<boolean> {
|
|
126
|
+
try {
|
|
127
|
+
const res = await this.client.headObject({
|
|
128
|
+
Bucket: this.bucket,
|
|
129
|
+
Key: this.getStoredPath(cid),
|
|
130
|
+
})
|
|
131
|
+
return res.$metadata.httpStatusCode === 200
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async deleteKey(key: string) {
|
|
138
|
+
await this.client.deleteObject({
|
|
139
|
+
Bucket: this.bucket,
|
|
140
|
+
Key: key,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async move(keys: { from: string; to: string }) {
|
|
145
|
+
await this.client.copyObject({
|
|
146
|
+
Bucket: this.bucket,
|
|
147
|
+
CopySource: `${this.bucket}/${keys.from}`,
|
|
148
|
+
Key: keys.to,
|
|
149
|
+
})
|
|
150
|
+
await this.client.deleteObject({
|
|
151
|
+
Bucket: this.bucket,
|
|
152
|
+
Key: keys.from,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default S3BlobStore
|