diff --git a/README.md b/README.md index 4301125..a6b286d 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,16 @@ PODCAST_VOICE=alloy # Options: alloy, echo, fable, onyx, nova, shimmer # Feature Flags ALLOW_DELETE=true ALLOW_MULTIPLE_NOTES_OF_SAME_TYPE=true + +# S3 / Ceph Storage +S3_ENDPOINT=https://s3.example.com +S3_REGION=us-east-1 # default us-east-1 +S3_ACCESS_KEY=yourkey +S3_SECRET_KEY=yoursecret +S3_BUCKET=notex-uploads +S3_FORCE_PATH_STYLE=true # usually true for Ceph/MinIO +S3_SKIP_TLS_VERIFY=true # default false + ``` ## 🔧 Development diff --git a/README_CN.md b/README_CN.md index 6ec5e88..4a5e13d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -233,6 +233,15 @@ PODCAST_VOICE=alloy # 选项:alloy、echo、fable、onyx、nova、shimmer # 功能开关 ALLOW_DELETE=true ALLOW_MULTIPLE_NOTES_OF_SAME_TYPE=true + +# S3 / Ceph 对象存储,文件上传 +S3_ENDPOINT=https://s3.example.com +S3_REGION=us-east-1 # default `us-east-1` +S3_ACCESS_KEY=yourkey +S3_SECRET_KEY=yoursecret +S3_BUCKET=notex-uploads +S3_FORCE_PATH_STYLE=true # usually true for Ceph/MinIO +S3_SKIP_TLS_VERIFY=true # default false ``` ## 🔧 开发 diff --git a/README_zh-tw.md b/README_zh-tw.md index 17e647a..e35623e 100644 --- a/README_zh-tw.md +++ b/README_zh-tw.md @@ -233,6 +233,15 @@ PODCAST_VOICE=alloy # 選項:alloy、echo、fable、onyx、nova、shimmer # 功能標誌 ALLOW_DELETE=true ALLOW_MULTIPLE_NOTES_OF_SAME_TYPE=true + +# S3 / Ceph 對象儲存,檔案上傳 +S3_ENDPOINT=https://s3.example.com +S3_REGION=us-east-1 # default `us-east-1` +S3_ACCESS_KEY=yourkey +S3_SECRET_KEY=yoursecret +S3_BUCKET=notex-uploads +S3_FORCE_PATH_STYLE=true # usually true for Ceph/MinIO +S3_SKIP_TLS_VERIFY=true # default false ``` ## 🔧 開發 diff --git a/backend/config.go b/backend/config.go index bc2df02..87d6e5c 100644 --- a/backend/config.go +++ b/backend/config.go @@ -11,8 +11,8 @@ import ( // Config holds the application configuration type Config struct { // Server settings - ServerHost string - ServerPort string + ServerHost string + ServerPort string MaxUploadSize int64 // Maximum upload file size in bytes (default: 100MB) // LLM settings @@ -79,11 +79,20 @@ type Config struct { GoogleRedirectURL string // Test Mode - EnableTestMode bool - TestUserID string - TestUserName string - TestUserEmail string - TestUserAvatar string + EnableTestMode bool + TestUserID string + TestUserName string + TestUserEmail string + TestUserAvatar string + + // Optional S3 / Ceph storage configuration + S3Endpoint string + S3Region string // region string, may be blank for Ceph + S3AccessKey string + S3SecretKey string + S3Bucket string + S3ForcePathStyle bool + S3SkipTLSVerify bool } // loadEnv loads .env file if it exists (ignoring errors if file not found) @@ -147,11 +156,20 @@ func LoadConfig() Config { GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""), GoogleRedirectURL: getEnv("GOOGLE_REDIRECT_URL", ""), - EnableTestMode: getEnvBool("ENABLE_TEST_MODE", false), - TestUserID: getEnv("TEST_USER_ID", "test-user-123"), - TestUserName: getEnv("TEST_USER_NAME", "测试用户"), - TestUserEmail: getEnv("TEST_USER_EMAIL", "test@example.com"), - TestUserAvatar: getEnv("TEST_USER_AVATAR", ""), + EnableTestMode: getEnvBool("ENABLE_TEST_MODE", false), + TestUserID: getEnv("TEST_USER_ID", "test-user-123"), + TestUserName: getEnv("TEST_USER_NAME", "测试用户"), + TestUserEmail: getEnv("TEST_USER_EMAIL", "test@example.com"), + TestUserAvatar: getEnv("TEST_USER_AVATAR", ""), + + // S3 / Ceph storage + S3Endpoint: getEnv("S3_ENDPOINT", ""), + S3Region: getEnv("S3_REGION", "us-east-1"), + S3AccessKey: getEnv("S3_ACCESS_KEY", ""), + S3SecretKey: getEnv("S3_SECRET_KEY", ""), + S3Bucket: getEnv("S3_BUCKET", ""), + S3ForcePathStyle: getEnvBool("S3_FORCE_PATH_STYLE", true), + S3SkipTLSVerify: getEnvBool("S3_SKIP_TLS_VERIFY", false), } // Auto-detect provider from base URL or model name diff --git a/backend/server.go b/backend/server.go index 7c75f6f..f1e4dd1 100644 --- a/backend/server.go +++ b/backend/server.go @@ -2,9 +2,11 @@ package backend import ( "context" + "crypto/tls" "database/sql" "embed" "fmt" + "io" "io/fs" "net/http" "net/url" @@ -14,6 +16,14 @@ import ( "sync" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + + // "github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/kataras/golog" @@ -30,10 +40,11 @@ type Server struct { agent *Agent http *gin.Engine auth *AuthHandler + s3Client *s3.Client // Track which notebooks have been loaded into vector store loadedNotebooks map[string]bool vectorMutex sync.RWMutex - memoryManager *MemoryManager + memoryManager *MemoryManager } // NewServer creates a new server @@ -86,6 +97,12 @@ func NewServer(cfg Config) (*Server, error) { // Set max upload size for multipart forms router.MaxMultipartMemory = cfg.MaxUploadSize + // Initialize S3 client if configuration present + s3Client, err := NewS3Client(cfg) + if err != nil { + return nil, fmt.Errorf("failed to initialize s3 client: %w", err) + } + s := &Server{ cfg: cfg, vectorStore: vectorStore, @@ -93,6 +110,7 @@ func NewServer(cfg Config) (*Server, error) { agent: agent, http: router, auth: authHandler, + s3Client: s3Client, loadedNotebooks: make(map[string]bool), memoryManager: memoryManager, } @@ -105,6 +123,41 @@ func NewServer(cfg Config) (*Server, error) { return s, nil } +// NewS3Client create a new S3 client if S3 configuration is provided, otherwise returns nil +func NewS3Client(cfg Config) (*s3.Client, error) { + var s3Client *s3.Client + if cfg.S3Endpoint != "" { + // sanitize endpoint and ensure region + cfg.S3Endpoint = strings.TrimRight(cfg.S3Endpoint, "/") + ctxCfg := context.Background() + // Prepare HTTP client with optional TLS skip verify for self-signed certs + httpClient := http.DefaultClient + if cfg.S3SkipTLSVerify { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpClient = &http.Client{Transport: transport} + golog.Warnf("S3_SKIP_TLS_VERIFY is enabled: TLS certificate verification will be skipped for S3 endpoint %s", cfg.S3Endpoint) + } + + awsCfg, err := config.LoadDefaultConfig(ctxCfg, + config.WithBaseEndpoint(cfg.S3Endpoint), + config.WithRegion(cfg.S3Region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, "")), + config.WithHTTPClient(httpClient), + config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired), + ) + if err != nil { + return nil, fmt.Errorf("failed to configure s3 client: %w", err) + } + s3Client = s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = cfg.S3ForcePathStyle + }) + golog.Infof("✅ s3 client initialized (bucket=%s endpoint=%s)", cfg.S3Bucket, cfg.S3Endpoint) + } + return s3Client, nil +} + // setupRoutes configures all routes func (s *Server) setupRoutes() { // Serve static files from embedded filesystem (no audit) @@ -615,6 +668,30 @@ func (s *Server) handleDeleteSource(c *gin.Context) { return } + // try delete file (safe type assertion and non-existence handling) + if v, ok := source.Metadata["path"]; ok { + pathStr := v.(string) + if err := os.Remove(pathStr); err != nil { + golog.Errorf("failed to delete file: %s", pathStr) + } + } + + // if s3Client init delete object (safe type assertion) + if s.s3Client != nil { + if v, ok := source.Metadata["s3_key"]; ok { + s3Key := v.(string) + _, err := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(s.cfg.S3Bucket), + Key: aws.String(s3Key), + }) + if err != nil { + golog.Errorf("failed to delete S3 object: %v", err) + } + } else { + golog.Errorf("source not exist s3_key: %s", sourceID) + } + } + if err := s.store.DeleteSource(ctx, sourceID); err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to delete source"}) return @@ -678,16 +755,6 @@ func (s *Server) handleUpload(c *gin.Context) { return } - // Create source - source := &Source{ - NotebookID: notebookID, - Name: file.Filename, // Keep original filename for display - Type: "file", - FileName: uniqueFileName, // Store unique filename - FileSize: file.Size, - Metadata: map[string]interface{}{"path": tempPath, "user_id": userID}, - } - // Extract content content, err := s.vectorStore.ExtractDocument(ctx, tempPath) if err != nil { @@ -697,7 +764,33 @@ func (s *Server) handleUpload(c *gin.Context) { c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to extract document content: %v", err)}) return } - source.Content = content + + source := &Source{ + NotebookID: notebookID, + Name: file.Filename, // Keep original filename for display + Type: "file", + FileName: uniqueFileName, // Store unique filename + FileSize: file.Size, + Content: content, + Metadata: map[string]interface{}{"user_id": userID}, + } + + // If S3 is configured, upload then cleanup local copy and record key + if s.s3Client != nil { + s3Key := fmt.Sprintf("%s/%s", userID, uniqueFileName) + if err := s.uploadToS3(ctx, tempPath, s3Key); err != nil { + golog.Errorf("failed to upload to S3: %v", err) + // remove local copy + os.Remove(tempPath) + c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to upload file to storage"}) + return + } + source.Metadata["s3_key"] = s3Key + // local copy no longer needed once stored remotely + os.Remove(tempPath) + } else { + source.Metadata["path"] = tempPath + } if err := s.store.CreateSource(ctx, source); err != nil { golog.Errorf("failed to create source: %v", err) @@ -1445,6 +1538,70 @@ func (s *Server) handleServeFile(c *gin.Context) { filename, notebookID, isPublic, userID) } +// uploadToS3 uploads a local file to the configured S3 bucket using the +// provided object key. The caller is responsible for creating and closing the +// local file. This helper returns any error from the SDK directly. +func (s *Server) uploadToS3(ctx context.Context, localPath, key string) error { + if s.s3Client == nil { + return fmt.Errorf("s3 client not configured") + } + f, err := os.Open(localPath) + if err != nil { + return err + } + defer f.Close() + + uploader := manager.NewUploader(s.s3Client) + _, err = uploader.Upload(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.cfg.S3Bucket), + Key: aws.String(key), + Body: f, + }) + + // ERROR: XAmzContentSHA256Mismatch, like s3Client config at line 124 + // trans := transfermanager.New(s.s3Client) + // _, err = trans.UploadObject(ctx, + // &transfermanager.UploadObjectInput{ + // Bucket: aws.String(s.cfg.S3Bucket), + // Key: aws.String(key), + // Body: f, + // }) + + return err +} + +// serveFileFromS3 streams an object from S3 directly to the HTTP response. +// It assumes access control has already been performed by the caller. +func (s *Server) serveFileFromS3(c *gin.Context, key string) { + if s.s3Client == nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "S3 storage not configured"}) + return + } + + input := &s3.GetObjectInput{ + Bucket: aws.String(s.cfg.S3Bucket), + Key: aws.String(key), + } + output, err := s.s3Client.GetObject(c.Request.Context(), input) + if err != nil { + golog.Errorf("s3 get object error: %v", err) + c.JSON(http.StatusNotFound, ErrorResponse{Error: "File not found"}) + return + } + defer output.Body.Close() + + // determine content type either from S3 metadata or fallback to octet-stream + contentType := "application/octet-stream" + if output.ContentType != nil { + contentType = *output.ContentType + } + c.Header("Content-Type", contentType) + // caching headers are handled by caller + if _, err := io.Copy(c.Writer, output.Body); err != nil { + golog.Errorf("error streaming s3 object: %v", err) + } +} + func writeFile(path, content string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/go.mod b/go.mod index 524fafc..7966a22 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,29 @@ require ( modernc.org/sqlite v1.42.2 ) +require ( + github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect + github.com/aws/smithy-go v1.24.2 // indirect +) + require ( cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.14.0 // indirect diff --git a/go.sum b/go.sum index d1f7d00..6d4981f 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,50 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= +github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= +github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.5 h1:4nC6vsVBU6vClZxxF6XLEozLUY/PgUCXYlGGB/VaC8M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.5/go.mod h1:N5c+La/yy7H4YnF9rFgUqwgbfw+MloWoCHQ0RJH2EBE= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.7 h1:SbQ14jYdGky6HK2lPmNfT+vN1PEEMG2wl3bMvq+soEg= +github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.1.7/go.mod h1:Y+5Z0Usd6DelgC+4fHmBwYdFcBZR89i3FnubMkHR1FY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=