Monorepo for Tangled tangled.org

appview/cloudflare: add kv, r2 and dns client wrappers

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

anirudh.fi 7a94ba0c 652324f6

verified
+421 -65
+65
appview/cloudflare/client.go
··· 1 + package cloudflare 2 + 3 + import ( 4 + "fmt" 5 + 6 + cf "github.com/cloudflare/cloudflare-go/v6" 7 + "github.com/cloudflare/cloudflare-go/v6/option" 8 + 9 + "github.com/aws/aws-sdk-go-v2/aws" 10 + "github.com/aws/aws-sdk-go-v2/credentials" 11 + "github.com/aws/aws-sdk-go-v2/service/s3" 12 + 13 + "tangled.org/core/appview/config" 14 + ) 15 + 16 + // Client holds all cloudflare service clients used by the appview. 17 + type Client struct { 18 + api *cf.Client // scoped to DNS / zone operations 19 + kvAPI *cf.Client // scoped to Workers KV (separate token) 20 + s3 *s3.Client 21 + zone string 22 + kvNS string 23 + bucket string 24 + cfAcct string 25 + } 26 + 27 + func New(c *config.Config) (*Client, error) { 28 + api := cf.NewClient(option.WithAPIToken(c.Cloudflare.ApiToken)) 29 + 30 + kvAPI := cf.NewClient(option.WithAPIToken(c.Cloudflare.KV.ApiToken)) 31 + 32 + // R2 endpoint is S3-compatible, keyed by account id. 33 + r2Endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", c.Cloudflare.AccountId) 34 + 35 + s3Client := s3.New(s3.Options{ 36 + BaseEndpoint: aws.String(r2Endpoint), 37 + Region: "auto", 38 + Credentials: credentials.NewStaticCredentialsProvider( 39 + c.Cloudflare.R2.AccessKeyID, 40 + c.Cloudflare.R2.SecretAccessKey, 41 + "", 42 + ), 43 + UsePathStyle: true, 44 + }) 45 + 46 + return &Client{ 47 + api: api, 48 + kvAPI: kvAPI, 49 + s3: s3Client, 50 + zone: c.Cloudflare.ZoneId, 51 + kvNS: c.Cloudflare.KV.NamespaceId, 52 + bucket: c.Cloudflare.R2.Bucket, 53 + cfAcct: c.Cloudflare.AccountId, 54 + }, nil 55 + } 56 + 57 + // Enabled returns true when the client has enough config to perform site 58 + // operations. Callers should check this before attempting any CF operations 59 + // so the appview degrades gracefully in dev environments without credentials. 60 + func (cl *Client) Enabled() bool { 61 + return cl != nil && 62 + cl.cfAcct != "" && 63 + cl.kvNS != "" && 64 + cl.bucket != "" 65 + }
+61
appview/cloudflare/dns.go
··· 1 + package cloudflare 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + cf "github.com/cloudflare/cloudflare-go/v6" 8 + "github.com/cloudflare/cloudflare-go/v6/dns" 9 + ) 10 + 11 + type DNSRecord struct { 12 + Type string 13 + Name string 14 + Content string 15 + TTL int 16 + Proxied bool 17 + } 18 + 19 + func (cl *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (string, error) { 20 + var body dns.RecordNewParamsBodyUnion 21 + 22 + switch record.Type { 23 + case "A": 24 + body = dns.ARecordParam{ 25 + Name: cf.F(record.Name), 26 + TTL: cf.F(dns.TTL(record.TTL)), 27 + Type: cf.F(dns.ARecordTypeA), 28 + Content: cf.F(record.Content), 29 + Proxied: cf.F(record.Proxied), 30 + } 31 + case "CNAME": 32 + body = dns.CNAMERecordParam{ 33 + Name: cf.F(record.Name), 34 + TTL: cf.F(dns.TTL(record.TTL)), 35 + Type: cf.F(dns.CNAMERecordTypeCNAME), 36 + Content: cf.F(record.Content), 37 + Proxied: cf.F(record.Proxied), 38 + } 39 + default: 40 + return "", fmt.Errorf("unsupported DNS record type: %s", record.Type) 41 + } 42 + 43 + result, err := cl.api.DNS.Records.New(ctx, dns.RecordNewParams{ 44 + ZoneID: cf.F(cl.zone), 45 + Body: body, 46 + }) 47 + if err != nil { 48 + return "", fmt.Errorf("failed to create DNS record: %w", err) 49 + } 50 + return result.ID, nil 51 + } 52 + 53 + func (cl *Client) DeleteDNSRecord(ctx context.Context, recordID string) error { 54 + _, err := cl.api.DNS.Records.Delete(ctx, recordID, dns.RecordDeleteParams{ 55 + ZoneID: cf.F(cl.zone), 56 + }) 57 + if err != nil { 58 + return fmt.Errorf("failed to delete DNS record: %w", err) 59 + } 60 + return nil 61 + }
+80
appview/cloudflare/kv.go
··· 1 + package cloudflare 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + "strings" 8 + 9 + cf "github.com/cloudflare/cloudflare-go/v6" 10 + "github.com/cloudflare/cloudflare-go/v6/kv" 11 + "github.com/cloudflare/cloudflare-go/v6/shared" 12 + ) 13 + 14 + // KVPut writes or overwrites a single Workers KV entry. 15 + func (cl *Client) KVPut(ctx context.Context, key string, value []byte) error { 16 + _, err := cl.kvAPI.KV.Namespaces.Values.Update(ctx, cl.kvNS, key, kv.NamespaceValueUpdateParams{ 17 + AccountID: cf.F(cl.cfAcct), 18 + Value: cf.F[kv.NamespaceValueUpdateParamsValueUnion](shared.UnionString(value)), 19 + }) 20 + if err != nil { 21 + return fmt.Errorf("writing KV entry %q: %w", key, err) 22 + } 23 + return nil 24 + } 25 + 26 + // KVGet reads a single Workers KV entry. Returns nil, nil if the key does not exist. 27 + func (cl *Client) KVGet(ctx context.Context, key string) ([]byte, error) { 28 + res, err := cl.kvAPI.KV.Namespaces.Values.Get(ctx, cl.kvNS, key, kv.NamespaceValueGetParams{ 29 + AccountID: cf.F(cl.cfAcct), 30 + }) 31 + if err != nil { 32 + // The CF SDK returns a 404 when the key doesn't exist. The error type 33 + // lives in an internal package so we match on the error string instead. 34 + if strings.Contains(err.Error(), "404") && strings.Contains(err.Error(), "key not found") { 35 + return nil, nil 36 + } 37 + return nil, fmt.Errorf("reading KV entry %q: %w", key, err) 38 + } 39 + defer res.Body.Close() 40 + val, err := io.ReadAll(res.Body) 41 + if err != nil { 42 + return nil, fmt.Errorf("reading KV entry body %q: %w", key, err) 43 + } 44 + return val, nil 45 + } 46 + 47 + // KVDelete removes a single Workers KV entry. 48 + // Safe to call even if the entry does not exist. 49 + func (cl *Client) KVDelete(ctx context.Context, key string) error { 50 + _, err := cl.kvAPI.KV.Namespaces.Values.Delete(ctx, cl.kvNS, key, kv.NamespaceValueDeleteParams{ 51 + AccountID: cf.F(cl.cfAcct), 52 + }) 53 + if err != nil { 54 + return fmt.Errorf("deleting KV entry %q: %w", key, err) 55 + } 56 + return nil 57 + } 58 + 59 + // KVDeleteByPrefix removes every Workers KV entry whose key starts with prefix, 60 + // handling pagination automatically. 61 + func (cl *Client) KVDeleteByPrefix(ctx context.Context, prefix string) error { 62 + iter := cl.kvAPI.KV.Namespaces.Keys.ListAutoPaging(ctx, cl.kvNS, kv.NamespaceKeyListParams{ 63 + AccountID: cf.F(cl.cfAcct), 64 + Prefix: cf.F(prefix), 65 + }) 66 + 67 + for iter.Next() { 68 + entry := iter.Current() 69 + if _, err := cl.kvAPI.KV.Namespaces.Values.Delete(ctx, cl.kvNS, entry.Name, kv.NamespaceValueDeleteParams{ 70 + AccountID: cf.F(cl.cfAcct), 71 + }); err != nil { 72 + return fmt.Errorf("deleting KV entry %q: %w", entry.Name, err) 73 + } 74 + } 75 + if err := iter.Err(); err != nil { 76 + return fmt.Errorf("listing KV entries with prefix %q: %w", prefix, err) 77 + } 78 + 79 + return nil 80 + }
+139
appview/cloudflare/r2.go
··· 1 + package cloudflare 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "mime" 8 + "net/http" 9 + "path/filepath" 10 + "strings" 11 + 12 + "github.com/aws/aws-sdk-go-v2/aws" 13 + "github.com/aws/aws-sdk-go-v2/service/s3" 14 + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 15 + ) 16 + 17 + // SyncFiles uploads the given files (keyed by relative path) to R2 under 18 + // prefix and deletes any objects from a previous deploy that are no longer 19 + // present. It is a pure R2 operation — callers are responsible for 20 + // constructing the prefix and fetching the source files before calling this. 21 + func (cl *Client) SyncFiles(ctx context.Context, prefix string, files map[string][]byte) error { 22 + existingKeys, err := cl.listR2Objects(ctx, prefix) 23 + if err != nil { 24 + return fmt.Errorf("listing existing R2 objects: %w", err) 25 + } 26 + 27 + for relPath, content := range files { 28 + key := prefix + relPath 29 + _, err := cl.s3.PutObject(ctx, &s3.PutObjectInput{ 30 + Bucket: aws.String(cl.bucket), 31 + Key: aws.String(key), 32 + Body: bytes.NewReader(content), 33 + ContentType: aws.String(DetectContentType(relPath, content)), 34 + }) 35 + if err != nil { 36 + return fmt.Errorf("uploading %q: %w", key, err) 37 + } 38 + } 39 + 40 + for existingKey := range existingKeys { 41 + relPath := strings.TrimPrefix(existingKey, prefix) 42 + if _, kept := files[relPath]; !kept { 43 + if err := cl.deleteR2Object(ctx, existingKey); err != nil { 44 + return fmt.Errorf("deleting orphan %q: %w", existingKey, err) 45 + } 46 + } 47 + } 48 + 49 + return nil 50 + } 51 + 52 + // DeleteFiles removes all R2 objects under the given prefix. 53 + func (cl *Client) DeleteFiles(ctx context.Context, prefix string) error { 54 + keys, err := cl.listR2Objects(ctx, prefix) 55 + if err != nil { 56 + return fmt.Errorf("listing R2 objects for deletion: %w", err) 57 + } 58 + for key := range keys { 59 + if err := cl.deleteR2Object(ctx, key); err != nil { 60 + return fmt.Errorf("deleting %q: %w", key, err) 61 + } 62 + } 63 + return nil 64 + } 65 + 66 + // listR2Objects returns all object keys in the bucket under the given prefix, 67 + // handling pagination automatically. 68 + func (cl *Client) listR2Objects(ctx context.Context, prefix string) (map[string]struct{}, error) { 69 + keys := make(map[string]struct{}) 70 + var continuationToken *string 71 + 72 + for { 73 + out, err := cl.s3.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ 74 + Bucket: aws.String(cl.bucket), 75 + Prefix: aws.String(prefix), 76 + ContinuationToken: continuationToken, 77 + }) 78 + if err != nil { 79 + return nil, err 80 + } 81 + 82 + for _, obj := range out.Contents { 83 + if obj.Key != nil { 84 + keys[*obj.Key] = struct{}{} 85 + } 86 + } 87 + 88 + if !aws.ToBool(out.IsTruncated) { 89 + break 90 + } 91 + continuationToken = out.NextContinuationToken 92 + } 93 + 94 + return keys, nil 95 + } 96 + 97 + func (cl *Client) deleteR2Object(ctx context.Context, key string) error { 98 + _, err := cl.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ 99 + Bucket: aws.String(cl.bucket), 100 + Key: aws.String(key), 101 + }) 102 + return err 103 + } 104 + 105 + // deleteBatch deletes up to 1000 objects in a single call. 106 + // Unused for now, kept for future bulk-delete optimisation. 107 + func (cl *Client) deleteBatch(ctx context.Context, keys []string) error { 108 + if len(keys) == 0 { 109 + return nil 110 + } 111 + 112 + var objects []s3types.ObjectIdentifier 113 + for _, k := range keys { 114 + k := k 115 + objects = append(objects, s3types.ObjectIdentifier{Key: &k}) 116 + } 117 + 118 + _, err := cl.s3.DeleteObjects(ctx, &s3.DeleteObjectsInput{ 119 + Bucket: aws.String(cl.bucket), 120 + Delete: &s3types.Delete{Objects: objects}, 121 + }) 122 + return err 123 + } 124 + 125 + // DetectContentType guesses the MIME type from the file extension, falling 126 + // back to sniffing the first 512 bytes of content. 127 + func DetectContentType(relPath string, content []byte) string { 128 + if ext := filepath.Ext(relPath); ext != "" { 129 + if mt := mime.TypeByExtension(ext); mt != "" { 130 + return mt 131 + } 132 + } 133 + 134 + sniff := content 135 + if len(sniff) > 512 { 136 + sniff = sniff[:512] 137 + } 138 + return http.DetectContentType(sniff) 139 + }
+22 -9
appview/config/config.go
··· 91 91 type R2Config struct { 92 92 AccessKeyID string `env:"ACCESS_KEY_ID"` 93 93 SecretAccessKey string `env:"SECRET_ACCESS_KEY"` 94 - Bucket string `env:"BUCKET, default=tangled-pages"` 94 + Bucket string `env:"BUCKET, default=tangled-sites"` 95 + } 96 + 97 + type TurnstileConfig struct { 98 + SiteKey string `env:"SITE_KEY"` 99 + SecretKey string `env:"SECRET_KEY"` 100 + } 101 + 102 + type KVConfig struct { 103 + NamespaceId string `env:"NAMESPACE_ID"` 104 + ApiToken string `env:"API_TOKEN"` 95 105 } 96 106 97 107 type Cloudflare struct { 98 - ApiToken string `env:"API_TOKEN"` 99 - ZoneId string `env:"ZONE_ID"` 100 - AccountID string `env:"ACCOUNT_ID"` 101 - KVNamespaceID string `env:"KV_NAMESPACE_ID"` 102 - TurnstileSiteKey string `env:"TURNSTILE_SITE_KEY"` 103 - TurnstileSecretKey string `env:"TURNSTILE_SECRET_KEY"` 104 - R2 R2Config `env:",prefix=R2_"` 108 + // Legacy top-level API token. For services like Workers KV, we 109 + // now use a scoped Account API token configured under the relevant 110 + // sub-struct. 111 + ApiToken string `env:"API_TOKEN"` 112 + ZoneId string `env:"ZONE_ID"` 113 + AccountId string `env:"ACCOUNT_ID"` 114 + 115 + KV KVConfig `env:",prefix=KV_"` 116 + Turnstile TurnstileConfig `env:",prefix=TURNSTILE_"` 117 + R2 R2Config `env:",prefix=R2_"` 105 118 } 106 119 107 120 type SitesConfig struct { 108 - Domain string `env:"DOMAIN, default=tngl.page"` 121 + Domain string `env:"DOMAIN, default=tngl.io"` 109 122 } 110 123 111 124 type LabelConfig struct {
-53
appview/dns/cloudflare.go
··· 1 - package dns 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - 7 - "github.com/cloudflare/cloudflare-go" 8 - "tangled.org/core/appview/config" 9 - ) 10 - 11 - type Record struct { 12 - Type string 13 - Name string 14 - Content string 15 - TTL int 16 - Proxied bool 17 - } 18 - 19 - type Cloudflare struct { 20 - api *cloudflare.API 21 - zone string 22 - } 23 - 24 - func NewCloudflare(c *config.Config) (*Cloudflare, error) { 25 - apiToken := c.Cloudflare.ApiToken 26 - api, err := cloudflare.NewWithAPIToken(apiToken) 27 - if err != nil { 28 - return nil, err 29 - } 30 - return &Cloudflare{api: api, zone: c.Cloudflare.ZoneId}, nil 31 - } 32 - 33 - func (cf *Cloudflare) CreateDNSRecord(ctx context.Context, record Record) (string, error) { 34 - result, err := cf.api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), cloudflare.CreateDNSRecordParams{ 35 - Type: record.Type, 36 - Name: record.Name, 37 - Content: record.Content, 38 - TTL: record.TTL, 39 - Proxied: &record.Proxied, 40 - }) 41 - if err != nil { 42 - return "", fmt.Errorf("failed to create DNS record: %w", err) 43 - } 44 - return result.ID, nil 45 - } 46 - 47 - func (cf *Cloudflare) DeleteDNSRecord(ctx context.Context, recordID string) error { 48 - err := cf.api.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(cf.zone), recordID) 49 - if err != nil { 50 - return fmt.Errorf("failed to delete DNS record: %w", err) 51 - } 52 - return nil 53 - }
+17 -1
go.mod
··· 7 7 github.com/alecthomas/assert/v2 v2.11.0 8 8 github.com/alecthomas/chroma/v2 v2.23.1 9 9 github.com/avast/retry-go/v4 v4.6.1 10 + github.com/aws/aws-sdk-go-v2 v1.41.1 11 + github.com/aws/aws-sdk-go-v2/credentials v1.19.9 12 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 10 13 github.com/blevesearch/bleve/v2 v2.5.3 11 14 github.com/bluekeyes/go-gitdiff v0.8.1 12 15 github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e ··· 15 18 github.com/carlmjohnson/versioninfo v0.22.5 16 19 github.com/casbin/casbin/v2 v2.103.0 17 20 github.com/charmbracelet/log v0.4.2 18 - github.com/cloudflare/cloudflare-go v0.115.0 21 + github.com/cloudflare/cloudflare-go/v6 v6.7.0 19 22 github.com/cyphar/filepath-securejoin v0.4.1 20 23 github.com/dgraph-io/ristretto v0.2.0 21 24 github.com/docker/docker v28.2.2+incompatible ··· 63 66 github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect 64 67 github.com/alecthomas/repr v0.5.2 // indirect 65 68 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 69 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect 70 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect 71 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect 72 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect 73 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect 74 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect 75 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect 76 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect 77 + github.com/aws/smithy-go v1.24.0 // indirect 66 78 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 67 79 github.com/aymerick/douceur v0.2.0 // indirect 68 80 github.com/beorn7/perks v1.0.1 // indirect ··· 186 198 github.com/ryanuber/go-glob v1.0.0 // indirect 187 199 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 188 200 github.com/spaolacci/murmur3 v1.1.0 // indirect 201 + github.com/tidwall/gjson v1.18.0 // indirect 202 + github.com/tidwall/match v1.2.0 // indirect 203 + github.com/tidwall/pretty v1.2.1 // indirect 204 + github.com/tidwall/sjson v1.2.5 // indirect 189 205 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 190 206 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 191 207 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+37 -2
go.sum
··· 22 22 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 23 23 github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 24 24 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 25 + github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= 26 + github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= 27 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= 28 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= 29 + github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8= 30 + github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w= 31 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= 32 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= 33 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= 34 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= 35 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= 36 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= 37 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= 38 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= 39 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= 40 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= 41 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= 42 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= 43 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= 44 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= 45 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= 46 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= 47 + github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= 48 + github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 25 49 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 26 50 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 27 51 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= ··· 116 140 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 117 141 github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d h1:IiIprFGH6SqstblP0Y9NIo3eaUJGkI/YDOFVSL64Uq4= 118 142 github.com/cloudflare/circl v1.6.2-0.20250618153321-aa837fd1539d/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 119 - github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= 120 - github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= 143 + github.com/cloudflare/cloudflare-go/v6 v6.7.0 h1:MP6Xy5WmsyrxgTxoLeq/vraqR0nbTtXoHhW4vAYc4SY= 144 + github.com/cloudflare/cloudflare-go/v6 v6.7.0/go.mod h1:Lj3MUqjvKctXRpdRhLQxZYRrNZHuRs0XYuH8JtQGyoI= 121 145 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 122 146 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 123 147 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= ··· 496 520 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 497 521 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 498 522 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 523 + github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 524 + github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 525 + github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 526 + github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 527 + github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= 528 + github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 529 + github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 530 + github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 531 + github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 532 + github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 533 + github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 499 534 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 500 535 github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 501 536 github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=