@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.
@@ -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,3 @@
1
+ export * from './kms';
2
+ export * from './s3';
3
+ export * from './cloudfront';
@@ -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;
@@ -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
@@ -0,0 +1,3 @@
1
+ export * from './kms'
2
+ export * from './s3'
3
+ export * from './cloudfront'
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
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["**/*.spec.ts", "**/*.test.ts"]
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist", // Your outDir,
5
+ "emitDeclarationOnly": true
6
+ },
7
+ "include": ["./src","__tests__/**/**.ts"]
8
+ }