betterstart-cli 0.0.63 → 0.0.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/dist/assets/adapters/next/integrations/r2/integration.ts +1 -1
- package/dist/assets/adapters/next/integrations/railway-bucket/actions/railway-bucket.ts +93 -0
- package/dist/assets/adapters/next/integrations/railway-bucket/integration.ts +40 -0
- package/dist/assets/adapters/next/integrations/vercel-blob/integration.ts +1 -1
- package/dist/assets/adapters/next/templates/init/api/storage-read-route.ts +43 -0
- package/dist/assets/adapters/next/templates/init/lib/actions/storage/types.ts +2 -1
- package/dist/cli.js +2499 -941
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import type { AdminStorageProvider, AdminStorageUploadInput } from '@admin/actions/storage/types'
|
|
3
|
+
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
|
4
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
|
5
|
+
|
|
6
|
+
let railwayBucketClient: S3Client | null = null
|
|
7
|
+
|
|
8
|
+
function getConfig() {
|
|
9
|
+
return {
|
|
10
|
+
endpoint: process.env.BETTERSTART_RAILWAY_BUCKET_ENDPOINT?.trim() ?? '',
|
|
11
|
+
accessKeyId: process.env.BETTERSTART_RAILWAY_BUCKET_ACCESS_KEY_ID?.trim() ?? '',
|
|
12
|
+
secretAccessKey: process.env.BETTERSTART_RAILWAY_BUCKET_SECRET_ACCESS_KEY?.trim() ?? '',
|
|
13
|
+
bucketName: process.env.BETTERSTART_RAILWAY_BUCKET_NAME?.trim() ?? '',
|
|
14
|
+
region: process.env.BETTERSTART_RAILWAY_BUCKET_REGION?.trim() ?? '',
|
|
15
|
+
urlStyle: process.env.BETTERSTART_RAILWAY_BUCKET_URL_STYLE?.trim() ?? ''
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sanitizeFilename(filename: string): string {
|
|
20
|
+
return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildKey(filename: string, prefix = 'uploads'): string {
|
|
24
|
+
return `${prefix}/${Date.now()}-${randomUUID()}-${sanitizeFilename(filename)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stableReadUrl(key: string): string {
|
|
28
|
+
return `/api/admin/storage/${Buffer.from(key, 'utf-8').toString('base64url')}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getClient(): S3Client {
|
|
32
|
+
if (railwayBucketClient) return railwayBucketClient
|
|
33
|
+
|
|
34
|
+
const config = getConfig()
|
|
35
|
+
railwayBucketClient = new S3Client({
|
|
36
|
+
region: config.region,
|
|
37
|
+
endpoint: config.endpoint,
|
|
38
|
+
forcePathStyle: config.urlStyle.toLowerCase().includes('path'),
|
|
39
|
+
credentials: {
|
|
40
|
+
accessKeyId: config.accessKeyId,
|
|
41
|
+
secretAccessKey: config.secretAccessKey
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
return railwayBucketClient
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const railwayBucketStorageProvider = {
|
|
48
|
+
id: 'railway-bucket',
|
|
49
|
+
isConfigured() {
|
|
50
|
+
const config = getConfig()
|
|
51
|
+
return Boolean(
|
|
52
|
+
config.endpoint &&
|
|
53
|
+
config.accessKeyId &&
|
|
54
|
+
config.secretAccessKey &&
|
|
55
|
+
config.bucketName &&
|
|
56
|
+
config.region &&
|
|
57
|
+
config.urlStyle
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
async saveFile(input: AdminStorageUploadInput) {
|
|
61
|
+
const config = getConfig()
|
|
62
|
+
if (!this.isConfigured()) {
|
|
63
|
+
throw new Error('Railway Bucket is not fully configured.')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const key = buildKey(input.filename, input.prefix)
|
|
67
|
+
await getClient().send(
|
|
68
|
+
new PutObjectCommand({
|
|
69
|
+
Bucket: config.bucketName,
|
|
70
|
+
Key: key,
|
|
71
|
+
Body: input.buffer,
|
|
72
|
+
ContentType: input.contentType,
|
|
73
|
+
ContentLength: input.contentLength
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return { key, url: stableReadUrl(key) }
|
|
78
|
+
},
|
|
79
|
+
async createReadUrl(key: string) {
|
|
80
|
+
const config = getConfig()
|
|
81
|
+
if (!this.isConfigured()) {
|
|
82
|
+
throw new Error('Railway Bucket is not fully configured.')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return getSignedUrl(
|
|
86
|
+
getClient(),
|
|
87
|
+
new GetObjectCommand({ Bucket: config.bucketName, Key: key }),
|
|
88
|
+
{ expiresIn: 300 }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
} satisfies AdminStorageProvider & {
|
|
92
|
+
createReadUrl(key: string): Promise<string>
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BetterstartIntegrationDefinition } from '@integration-engine/types.js'
|
|
2
|
+
|
|
3
|
+
export const railwayBucketIntegration: BetterstartIntegrationDefinition = {
|
|
4
|
+
id: 'railway-bucket',
|
|
5
|
+
title: 'Railway Bucket',
|
|
6
|
+
description: 'Private S3-compatible media storage on Railway with signed reads',
|
|
7
|
+
kind: 'storage',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
dependencies: [],
|
|
10
|
+
conflicts: ['r2', 'vercel-blob'],
|
|
11
|
+
files: [
|
|
12
|
+
{
|
|
13
|
+
outputPath: 'lib/actions/railway-bucket/provider.ts',
|
|
14
|
+
templatePath: 'actions/railway-bucket.ts'
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
packageDependencies: ['@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'],
|
|
18
|
+
devDependencies: [],
|
|
19
|
+
capabilities: {
|
|
20
|
+
storageProvider: {
|
|
21
|
+
provider: 'railway-bucket',
|
|
22
|
+
importPath: '@admin/actions/railway-bucket/provider',
|
|
23
|
+
exportName: 'railwayBucketStorageProvider'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
envSections: [
|
|
27
|
+
{
|
|
28
|
+
header: 'Storage (Railway Bucket)',
|
|
29
|
+
vars: [
|
|
30
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_ENDPOINT', value: '' },
|
|
31
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_ACCESS_KEY_ID', value: '' },
|
|
32
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_SECRET_ACCESS_KEY', value: '' },
|
|
33
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_NAME', value: '' },
|
|
34
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_REGION', value: '' },
|
|
35
|
+
{ key: 'BETTERSTART_RAILWAY_BUCKET_URL_STYLE', value: '' }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
configure: 'railway-bucket'
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getStorageProvider } from '@admin/actions/storage/provider'
|
|
2
|
+
import { type NextRequest, NextResponse } from 'next/server'
|
|
3
|
+
|
|
4
|
+
type SignedReadStorageProvider = ReturnType<typeof getStorageProvider> & {
|
|
5
|
+
createReadUrl?: (storageKey: string) => Promise<string>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function decodeStorageKey(encodedKey: string): string | undefined {
|
|
9
|
+
try {
|
|
10
|
+
const key = Buffer.from(encodedKey, 'base64url').toString('utf-8')
|
|
11
|
+
const canonicalKey = Buffer.from(key, 'utf-8').toString('base64url')
|
|
12
|
+
return key && canonicalKey === encodedKey ? key : undefined
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function storageError(error: string, status: number) {
|
|
19
|
+
return NextResponse.json({ error }, { status })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function GET(request: NextRequest) {
|
|
23
|
+
const encodedKey = request.nextUrl.pathname.split('/').filter(Boolean).at(-1)
|
|
24
|
+
const key = encodedKey ? decodeStorageKey(encodedKey) : undefined
|
|
25
|
+
const provider: SignedReadStorageProvider = getStorageProvider()
|
|
26
|
+
|
|
27
|
+
if (!key || !provider.createReadUrl) {
|
|
28
|
+
return storageError('Media object not found', 404)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const signedUrl = await provider.createReadUrl(key)
|
|
33
|
+
return new NextResponse(null, {
|
|
34
|
+
status: 307,
|
|
35
|
+
headers: {
|
|
36
|
+
Location: signedUrl,
|
|
37
|
+
'Cache-Control': 'no-store'
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
} catch {
|
|
41
|
+
return storageError('Could not create a media read URL', 502)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -14,5 +14,6 @@ export interface AdminStorageUploadResult {
|
|
|
14
14
|
export interface AdminStorageProvider {
|
|
15
15
|
id: string
|
|
16
16
|
isConfigured: () => boolean
|
|
17
|
-
saveFile
|
|
17
|
+
saveFile(input: AdminStorageUploadInput): Promise<AdminStorageUploadResult>
|
|
18
|
+
createReadUrl?: (key: string) => Promise<string>
|
|
18
19
|
}
|