@cuboapp/api-backend 1.0.30 → 1.0.31
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/package.json +2 -1
- package/src/index.ts +5 -0
- package/src/s3/index.ts +104 -0
- package/src/s3/types/index.ts +19 -0
- package/src/s3/utils/index.ts +74 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/api-backend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Backend Api for CuboApp",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"repository": "git@github.com:cuboapp/api-backend.git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"@cuboapp/types": "2.0.13",
|
|
15
15
|
"@cuboapp/utils": "1.0.10",
|
|
16
16
|
"@cuboapp/ws": "1.0.6",
|
|
17
|
+
"mime-types": "^3.0.2",
|
|
17
18
|
"pg": "^8.17.2",
|
|
18
19
|
"yjs": "^13.6.29"
|
|
19
20
|
},
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { cloneDeep } from '@cuboapp/utils'
|
|
|
4
4
|
|
|
5
5
|
import { ApiHelpers } from './helpers'
|
|
6
6
|
import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
|
|
7
|
+
import { CuboS3UploadFunctionOptions, CuboS3UploadFunctionResult, uploadToS3 } from './s3'
|
|
7
8
|
|
|
8
9
|
export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown> {
|
|
9
10
|
constructor(public options: CuboBackendApiOptions<T, A>) {}
|
|
@@ -121,6 +122,10 @@ export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown>
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
public async uploadToS3(buffer: Buffer, opts: CuboS3UploadFunctionOptions): Promise<CuboS3UploadFunctionResult> {
|
|
126
|
+
return uploadToS3(buffer, opts)
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
private async getEntity(entityAlias: Extract<keyof T, string>) {
|
|
125
130
|
const entity = await this.helpers.getEntityByAlias(entityAlias)
|
|
126
131
|
if (!entity) {
|
package/src/s3/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CuboS3UploadFunction } from './types'
|
|
2
|
+
import {
|
|
3
|
+
defaultFileName,
|
|
4
|
+
encodeRfc3986,
|
|
5
|
+
encodeS3Key,
|
|
6
|
+
toAmzDate,
|
|
7
|
+
sha256Hex,
|
|
8
|
+
normalizeHeaderValue,
|
|
9
|
+
getSignatureKey,
|
|
10
|
+
hmacHex,
|
|
11
|
+
buildS3Key
|
|
12
|
+
} from './utils'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upload buffer to S3 using PUT Object.
|
|
16
|
+
* - Uses path-style URL: {endpoint}/{bucket}/{key}
|
|
17
|
+
*/
|
|
18
|
+
export const uploadToS3: CuboS3UploadFunction = async (buffer, opts) => {
|
|
19
|
+
const { bucket, region, endpoint, access_key, secret } = opts.s3
|
|
20
|
+
const mimeType = opts.mime_type || 'application/octet-stream'
|
|
21
|
+
|
|
22
|
+
const baseName = opts.file_name || defaultFileName(mimeType)
|
|
23
|
+
const fileName = buildS3Key(baseName, opts.s3.folder)
|
|
24
|
+
|
|
25
|
+
const ep = new URL(endpoint)
|
|
26
|
+
const host = ep.host
|
|
27
|
+
|
|
28
|
+
const canonicalUri = `/${encodeRfc3986(bucket)}/${encodeS3Key(fileName)}`
|
|
29
|
+
const url = new URL(`${ep.origin}${canonicalUri}`)
|
|
30
|
+
|
|
31
|
+
const method = 'PUT'
|
|
32
|
+
const service = 's3'
|
|
33
|
+
|
|
34
|
+
const amzDate = toAmzDate(new Date()) // YYYYMMDDTHHMMSSZ
|
|
35
|
+
const dateStamp = amzDate.slice(0, 8) // YYYYMMDD
|
|
36
|
+
|
|
37
|
+
const payloadHash = sha256Hex(buffer)
|
|
38
|
+
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
host,
|
|
41
|
+
'content-type': mimeType,
|
|
42
|
+
'x-amz-content-sha256': payloadHash,
|
|
43
|
+
'x-amz-date': amzDate
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const signedHeaders = Object.keys(headers)
|
|
47
|
+
.map((h) => h.toLowerCase())
|
|
48
|
+
.sort()
|
|
49
|
+
.join(';')
|
|
50
|
+
|
|
51
|
+
const canonicalHeaders = Object.keys(headers)
|
|
52
|
+
.map((k) => k.toLowerCase())
|
|
53
|
+
.sort()
|
|
54
|
+
.map((k) => `${k}:${normalizeHeaderValue(headers[k])}\n`)
|
|
55
|
+
.join('')
|
|
56
|
+
|
|
57
|
+
const canonicalQueryString = ''
|
|
58
|
+
|
|
59
|
+
const canonicalRequest =
|
|
60
|
+
`${method}\n` + `${canonicalUri}\n` + `${canonicalQueryString}\n` + `${canonicalHeaders}\n` + `${signedHeaders}\n` + `${payloadHash}`
|
|
61
|
+
|
|
62
|
+
const algorithm = 'AWS4-HMAC-SHA256'
|
|
63
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`
|
|
64
|
+
const stringToSign = `${algorithm}\n` + `${amzDate}\n` + `${credentialScope}\n` + `${sha256Hex(Buffer.from(canonicalRequest, 'utf8'))}`
|
|
65
|
+
|
|
66
|
+
const signingKey = getSignatureKey(secret, dateStamp, region, service)
|
|
67
|
+
const signature = hmacHex(signingKey, stringToSign)
|
|
68
|
+
|
|
69
|
+
const authorization =
|
|
70
|
+
`${algorithm} ` + `Credential=${access_key}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(url.toString(), {
|
|
74
|
+
method,
|
|
75
|
+
headers: {
|
|
76
|
+
...headers,
|
|
77
|
+
Authorization: authorization
|
|
78
|
+
},
|
|
79
|
+
body: buffer as any
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const text = await res.text()
|
|
84
|
+
throw {
|
|
85
|
+
code: res.status,
|
|
86
|
+
error: res.statusText,
|
|
87
|
+
text
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resUrl = res.url || url.toString()
|
|
92
|
+
|
|
93
|
+
return { fileName: baseName, url: resUrl }
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.log('error')
|
|
96
|
+
throw {
|
|
97
|
+
code: 500,
|
|
98
|
+
text: error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export * from './types'
|
|
104
|
+
export * from './utils'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type CuboS3UploadFunctionOptions = {
|
|
2
|
+
file_name?: string
|
|
3
|
+
mime_type: string
|
|
4
|
+
s3: {
|
|
5
|
+
bucket: string
|
|
6
|
+
region: string
|
|
7
|
+
endpoint: string
|
|
8
|
+
access_key: string
|
|
9
|
+
secret: string
|
|
10
|
+
folder?: string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CuboS3UploadFunctionResult = {
|
|
15
|
+
fileName: string
|
|
16
|
+
url: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CuboS3UploadFunction = (buffer: Buffer, opts: CuboS3UploadFunctionOptions) => Promise<CuboS3UploadFunctionResult>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { uuid } from '@cuboapp/utils'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import { extension } from 'mime-types'
|
|
4
|
+
|
|
5
|
+
export const defaultFileName = (mimeType: string) => {
|
|
6
|
+
const ext = extension(mimeType)
|
|
7
|
+
const id = uuid()
|
|
8
|
+
return `${id}.${ext}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const normalizeFileName = (name: string) => {
|
|
12
|
+
return name.replace(/^\/+/, '')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const toAmzDate = (d: Date) => {
|
|
16
|
+
// YYYYMMDDTHHMMSSZ
|
|
17
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
18
|
+
return (
|
|
19
|
+
d.getUTCFullYear() +
|
|
20
|
+
pad(d.getUTCMonth() + 1) +
|
|
21
|
+
pad(d.getUTCDate()) +
|
|
22
|
+
'T' +
|
|
23
|
+
pad(d.getUTCHours()) +
|
|
24
|
+
pad(d.getUTCMinutes()) +
|
|
25
|
+
pad(d.getUTCSeconds()) +
|
|
26
|
+
'Z'
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const sha256Hex = (data: Buffer) => {
|
|
31
|
+
return crypto.createHash('sha256').update(data).digest('hex')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const hmac = (key: Buffer | string, data: string) => {
|
|
35
|
+
return crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const hmacHex = (key: Buffer | string, data: string) => {
|
|
39
|
+
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const getSignatureKey = (secretKey: string, dateStamp: string, region: string, service: string) => {
|
|
43
|
+
const kDate = hmac('AWS4' + secretKey, dateStamp)
|
|
44
|
+
const kRegion = hmac(kDate, region)
|
|
45
|
+
const kService = hmac(kRegion, service)
|
|
46
|
+
const kSigning = hmac(kService, 'aws4_request')
|
|
47
|
+
return kSigning
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const normalizeHeaderValue = (v: string) => {
|
|
51
|
+
return v.trim().replace(/\s+/g, ' ')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const encodeRfc3986 = (str: string) => {
|
|
55
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const encodeS3Key = (key: string) => {
|
|
59
|
+
return key
|
|
60
|
+
.split('/')
|
|
61
|
+
.map((seg) => encodeRfc3986(seg))
|
|
62
|
+
.join('/')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const buildS3Key = (fileName: string, folder?: string) => {
|
|
66
|
+
const cleanFile = normalizeFileName(fileName)
|
|
67
|
+
|
|
68
|
+
if (!folder) {
|
|
69
|
+
return cleanFile
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cleanFolder = folder.replace(/^\/+|\/+$/g, '')
|
|
73
|
+
return `${cleanFolder}/${cleanFile}`
|
|
74
|
+
}
|