From 072be0caf97c4eda54660df2cfb97828e07118f5 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:33:50 -0800 Subject: [PATCH 001/381] Added Attachment to Message entity --- api/pkg/entities/message.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/api/pkg/entities/message.go b/api/pkg/entities/message.go index bf846c0b2..eb71f51b7 100644 --- a/api/pkg/entities/message.go +++ b/api/pkg/entities/message.go @@ -81,17 +81,23 @@ func (s SIM) String() string { return string(s) } +type MessageAttachment struct { + ContentType string `json:"content_type" example:"image/jpeg"` + URL string `json:"url" example:"https://example.com/image.jpg"` +} + // Message represents a message sent between 2 phone numbers type Message struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"` - Owner string `json:"owner" example:"+18005550199"` - UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` - Contact string `json:"contact" example:"+18005550100"` - Content string `json:"content" example:"This is a sample text message"` - Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"` - Type MessageType `json:"type" example:"mobile-terminated"` - Status MessageStatus `json:"status" example:"pending"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + RequestID *string `json:"request_id" example:"153554b5-ae44-44a0-8f4f-7bbac5657ad4" validate:"optional"` + Owner string `json:"owner" example:"+18005550199"` + UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` + Contact string `json:"contact" example:"+18005550100"` + Content string `json:"content" example:"This is a sample text message"` + Attachments []MessageAttachment `json:"attachments,omitempty" gorm:"type:json;serializer:json"` + Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"` + Type MessageType `json:"type" example:"mobile-terminated"` + Status MessageStatus `json:"status" example:"pending"` // SIM is the SIM card to use to send the message // * SMS1: use the SIM card in slot 1 // * SMS2: use the SIM card in slot 2 From 09cf30b4812b65141df74c07dd2ab557dbc25030 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:36:53 -0800 Subject: [PATCH 002/381] Added Attachments to APISentPayload & send request --- api/pkg/events/message_api_sent_event.go | 23 ++++++++++++----------- api/pkg/requests/message_send_request.go | 8 +++++--- api/pkg/services/message_service.go | 3 +++ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/api/pkg/events/message_api_sent_event.go b/api/pkg/events/message_api_sent_event.go index 7abea8439..9aeabd90d 100644 --- a/api/pkg/events/message_api_sent_event.go +++ b/api/pkg/events/message_api_sent_event.go @@ -13,15 +13,16 @@ const EventTypeMessageAPISent = "message.api.sent" // MessageAPISentPayload is the payload of the EventTypeMessageSent event type MessageAPISentPayload struct { - MessageID uuid.UUID `json:"message_id"` - UserID entities.UserID `json:"user_id"` - Owner string `json:"owner"` - RequestID *string `json:"request_id"` - MaxSendAttempts uint `json:"max_send_attempts"` - Contact string `json:"contact"` - ScheduledSendTime *time.Time `json:"scheduled_send_time"` - RequestReceivedAt time.Time `json:"request_received_at"` - Content string `json:"content"` - Encrypted bool `json:"encrypted"` - SIM entities.SIM `json:"sim"` + MessageID uuid.UUID `json:"message_id"` + UserID entities.UserID `json:"user_id"` + Owner string `json:"owner"` + RequestID *string `json:"request_id"` + MaxSendAttempts uint `json:"max_send_attempts"` + Contact string `json:"contact"` + ScheduledSendTime *time.Time `json:"scheduled_send_time"` + RequestReceivedAt time.Time `json:"request_received_at"` + Content string `json:"content"` + Attachments []entities.MessageAttachment `json:"attachments"` + Encrypted bool `json:"encrypted"` + SIM entities.SIM `json:"sim"` } diff --git a/api/pkg/requests/message_send_request.go b/api/pkg/requests/message_send_request.go index 3301691d9..1a1d6830b 100644 --- a/api/pkg/requests/message_send_request.go +++ b/api/pkg/requests/message_send_request.go @@ -14,9 +14,10 @@ import ( // MessageSend is the payload for sending and SMS message type MessageSend struct { request - From string `json:"from" example:"+18005550199"` - To string `json:"to" example:"+18005550100"` - Content string `json:"content" example:"This is a sample text message"` + From string `json:"from" example:"+18005550199"` + To string `json:"to" example:"+18005550100"` + Content string `json:"content" example:"This is a sample text message"` + Attachments []entities.MessageAttachment `json:"attachments" validate:"optional"` // Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app Encrypted bool `json:"encrypted" example:"false" validate:"optional"` @@ -47,5 +48,6 @@ func (input *MessageSend) ToMessageSendParams(userID entities.UserID, source str RequestReceivedAt: time.Now().UTC(), Contact: input.sanitizeAddress(input.To), Content: input.Content, + Attachments: input.Attachments, } } diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go index 5a95b265a..eca2334ce 100644 --- a/api/pkg/services/message_service.go +++ b/api/pkg/services/message_service.go @@ -430,6 +430,7 @@ type MessageSendParams struct { Contact string Encrypted bool Content string + Attachments []entities.MessageAttachment Source string SendAt *time.Time RequestID *string @@ -456,6 +457,7 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe Contact: params.Contact, RequestReceivedAt: params.RequestReceivedAt, Content: params.Content, + Attachments: params.Attachments, ScheduledSendTime: params.SendAt, SIM: sim, } @@ -968,6 +970,7 @@ func (service *MessageService) storeSentMessage(ctx context.Context, payload eve Contact: payload.Contact, UserID: payload.UserID, Content: payload.Content, + Attachments: payload.Attachments, RequestID: payload.RequestID, SIM: payload.SIM, Encrypted: payload.Encrypted, From 69ecce11ff420d331765e500ad88f83d56df1e42 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:45:08 -0800 Subject: [PATCH 003/381] Added validator for attachment urls --- .../validators/message_handler_validator.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index 33ab4dfef..fd690e266 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -106,6 +106,28 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context return result } + if len(request.Attachments) > 10 { + result.Add("attachments", "you cannot attach more than 10 files to a single message") + } + + for i, attachment := range request.Attachments { + if strings.TrimSpace(attachment.ContentType) == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d is missing content_type", i)) + } + + if strings.TrimSpace(attachment.URL) == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d is missing url", i)) + } else { + // Basic URL validation + parsedURL, err := url.ParseRequestURI(attachment.URL) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d has an invalid url format", i)) + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) + } + } + } + if request.SendAt != nil && request.SendAt.After(time.Now().Add(480*time.Hour)) { result.Add("send_at", "the scheduled time cannot be more than 20 days (480 hours) in the future") } From b5bfdb45587b6a5d595c397288f21d84d37767bb Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:45:48 -0800 Subject: [PATCH 004/381] Added attachment to bulk message struct --- api/pkg/requests/message_bulk_send_request.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/pkg/requests/message_bulk_send_request.go b/api/pkg/requests/message_bulk_send_request.go index a21570bba..47c2af7d0 100644 --- a/api/pkg/requests/message_bulk_send_request.go +++ b/api/pkg/requests/message_bulk_send_request.go @@ -16,6 +16,7 @@ type MessageBulkSend struct { From string `json:"from" example:"+18005550199"` To []string `json:"to" example:"+18005550100,+18005550100"` Content string `json:"content" example:"This is a sample text message"` + Attachments []entities.MessageAttachment `json:"attachments" validate:"optional"` // Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app Encrypted bool `json:"encrypted" example:"false"` @@ -52,6 +53,7 @@ func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source Contact: to, SendAt: &sendAt, Content: input.Content, + Attachments: input.Attachments, }) } From 53112cbf3d32149b38975e6f873c6e1dc63fa70d Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:47:15 -0800 Subject: [PATCH 005/381] Added same validation to bulk message --- .../validators/message_handler_validator.go | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index fd690e266..20a8ed9fb 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -114,11 +114,10 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context if strings.TrimSpace(attachment.ContentType) == "" { result.Add("attachments", fmt.Sprintf("attachment at index %d is missing content_type", i)) } - + if strings.TrimSpace(attachment.URL) == "" { result.Add("attachments", fmt.Sprintf("attachment at index %d is missing url", i)) } else { - // Basic URL validation parsedURL, err := url.ParseRequestURI(attachment.URL) if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { result.Add("attachments", fmt.Sprintf("attachment at index %d has an invalid url format", i)) @@ -178,6 +177,27 @@ func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Con return result } + if len(request.Attachments) > 10 { + result.Add("attachments", "you cannot attach more than 10 files to a single message") + } + + for i, attachment := range request.Attachments { + if strings.TrimSpace(attachment.ContentType) == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d is missing content_type", i)) + } + + if strings.TrimSpace(attachment.URL) == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d is missing url", i)) + } else { + parsedURL, err := url.ParseRequestURI(attachment.URL) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + result.Add("attachments", fmt.Sprintf("attachment at index %d has an invalid url format", i)) + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) + } + } + } + _, err := validator.phoneService.Load(ctx, userID, request.From) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { result.Add("from", fmt.Sprintf("no phone found with with 'from' number [%s]. Install the android app on your phone to start sending messages", request.From)) From 34b2cfb5d67ff3913731b2eabf955e520c4b088e Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:54:09 -0800 Subject: [PATCH 006/381] Added attachmenturls to the BulkMessage struct for csv support and a basic file type check --- api/pkg/requests/bulk_message_request.go | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/api/pkg/requests/bulk_message_request.go b/api/pkg/requests/bulk_message_request.go index ffb3f35c1..0ab6c0247 100644 --- a/api/pkg/requests/bulk_message_request.go +++ b/api/pkg/requests/bulk_message_request.go @@ -18,6 +18,7 @@ type BulkMessage struct { ToPhoneNumber string `csv:"ToPhoneNumber"` Content string `csv:"Content"` SendTime *time.Time `csv:"SendTime(optional)"` + AttachmentURLs string `csv:"AttachmentURLs(optional)" validate:"optional"` // Comma separated list of URLs } // Sanitize sets defaults to BulkMessage @@ -25,12 +26,43 @@ func (input *BulkMessage) Sanitize() *BulkMessage { input.ToPhoneNumber = input.sanitizeAddress(input.ToPhoneNumber) input.Content = strings.TrimSpace(input.Content) input.FromPhoneNumber = input.sanitizeAddress(input.FromPhoneNumber) + input.AttachmentURLs = strings.TrimSpace(input.AttachmentURLs) return input } // ToMessageSendParams converts BulkMessage to services.MessageSendParams func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string) services.MessageSendParams { from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) + + var attachments []entities.MessageAttachment + if input.AttachmentURLs != "" { + urls := strings.Split(input.AttachmentURLs, ",") + for _, u := range urls { + cleanURL := strings.TrimSpace(u) + if cleanURL == "" { + continue + } + + // Since there's no easy way to set a type in the CSV, defaulting to octet-stream and then just checking the file extension in the URL + contentType := "application/octet-stream" + lowerURL := strings.ToLower(cleanURL) + if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { + contentType = "image/jpeg" + } else if strings.HasSuffix(lowerURL, ".png") { + contentType = "image/png" + } else if strings.HasSuffix(lowerURL, ".gif") { + contentType = "image/gif" + } else if strings.HasSuffix(lowerURL, ".mp4") { + contentType = "video/mp4" + } + + attachments = append(attachments, entities.MessageAttachment{ + ContentType: contentType, + URL: cleanURL, + }) + } + } + return services.MessageSendParams{ Source: source, Owner: from, @@ -40,5 +72,6 @@ func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID RequestReceivedAt: time.Now().UTC(), Contact: input.sanitizeAddress(input.ToPhoneNumber), Content: input.Content, + Attachments: attachments, } } From bc5faf1c42ccf218378aaebfe63f04ae86940b97 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:56:01 -0800 Subject: [PATCH 007/381] Added attachment parsing to the xlsx parser --- api/pkg/validators/bulk_message_handler_validator.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 9881c53f8..acabcbba5 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -143,11 +143,17 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user } } + var attachmentURLs string + if len(row) > 4 && strings.TrimSpace(row[4]) != "" { + attachmentURLs = strings.TrimSpace(row[4]) + } + messages = append(messages, &requests.BulkMessage{ FromPhoneNumber: strings.TrimSpace(row[0]), ToPhoneNumber: strings.TrimSpace(row[1]), Content: row[2], SendTime: sendAt, + AttachmentURLs: attachmentURLs, }) } From 84974984f017cc03fa4af9db1d90aed19e720d39 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 00:59:10 -0800 Subject: [PATCH 008/381] Added validation to csv based bulk messages --- .../bulk_message_handler_validator.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index acabcbba5..848443926 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -218,6 +218,29 @@ func (v *BulkMessageHandlerValidator) parseCSV(ctxLogger telemetry.Logger, user func (v *BulkMessageHandlerValidator) validateMessages(messages []*requests.BulkMessage) url.Values { result := url.Values{} for index, message := range messages { + + if message.AttachmentURLs != "" { + urls := strings.Split(message.AttachmentURLs, ",") + + if len(urls) > 10 { + result.Add("document", fmt.Sprintf("Row [%d]: You cannot attach more than 10 files per message.", index+2)) + } + + for _, u := range urls { + cleanURL := strings.TrimSpace(u) + if cleanURL == "" { + continue + } + + parsedURL, err := url.ParseRequestURI(cleanURL) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] has an invalid url format.", index+2, cleanURL)) + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] must use http or https.", index+2, cleanURL)) + } + } + } + if _, err := phonenumbers.Parse(message.FromPhoneNumber, phonenumbers.UNKNOWN_REGION); err != nil { result.Add("document", fmt.Sprintf("Row [%d]: The FromPhoneNumber [%s] is not a valid E.164 phone number", index+2, message.FromPhoneNumber)) } From 569b56da2b129e712ce329c12a40cb4bfc6fbfb3 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:02:28 -0800 Subject: [PATCH 009/381] Added attachment_urls to discord slash command --- api/pkg/services/discord_service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/pkg/services/discord_service.go b/api/pkg/services/discord_service.go index 059231b95..8c608e9f0 100644 --- a/api/pkg/services/discord_service.go +++ b/api/pkg/services/discord_service.go @@ -169,6 +169,12 @@ func (service *DiscordService) createSlashCommand(ctx context.Context, serverID Type: 3, Required: true, }, + { + Name: "attachment_urls", + Description: "Comma-separated list of media URLs to attach", + Type: 3, + Required: false, + }, }, }) if err != nil { From a0fc868569bca37fd8628993b92a3b19ef866011 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:05:01 -0800 Subject: [PATCH 010/381] Added attachment file type check to CreateRequest --- api/pkg/handlers/discord_handler.go | 40 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go index 95591c2b3..d556dc459 100644 --- a/api/pkg/handlers/discord_handler.go +++ b/api/pkg/handlers/discord_handler.go @@ -8,9 +8,11 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/google/uuid" + "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" @@ -290,10 +292,42 @@ func (h *DiscordHandler) createRequest(payload map[string]any) requests.MessageS } return "" } + var attachments []entities.MessageAttachment + attachmentURLsStr := getOption("attachment_urls") + + if attachmentURLsStr != "" { + urls := strings.Split(attachmentURLsStr, ",") + for _, u := range urls { + cleanURL := strings.TrimSpace(u) + if cleanURL == "" { + continue + } + + // Same as with bulk CSV attachments, can't easily ask for the MIME type so + // just inferring based on the file extension + contentType := "application/octet-stream" + lowerURL := strings.ToLower(cleanURL) + if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { + contentType = "image/jpeg" + } else if strings.HasSuffix(lowerURL, ".png") { + contentType = "image/png" + } else if strings.HasSuffix(lowerURL, ".gif") { + contentType = "image/gif" + } else if strings.HasSuffix(lowerURL, ".mp4") { + contentType = "video/mp4" + } + + attachments = append(attachments, entities.MessageAttachment{ + ContentType: contentType, + URL: cleanURL, + }) + } + } return requests.MessageSend{ - From: getOption("from"), - To: getOption("to"), - Content: getOption("message"), + From: getOption("from"), + To: getOption("to"), + Content: getOption("message"), + Attachments: attachments, } } From 2f6c94a2406ec9c797cb3498f5b198d8e54ace4d Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:08:03 -0800 Subject: [PATCH 011/381] Added embed for discord confirmation --- api/pkg/handlers/discord_handler.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go index d556dc459..7be60f3b6 100644 --- a/api/pkg/handlers/discord_handler.go +++ b/api/pkg/handlers/discord_handler.go @@ -375,6 +375,19 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c *fiber.Ctx, payload map[ }, } + if len(request.Attachments) > 0 { + var urls []string + for _, att := range request.Attachments { + urls = append(urls, att.URL) + } + + fields := messageEmbed["fields"].([]fiber.Map) + messageEmbed["fields"] = append(fields, fiber.Map{ + "name": "Attachments:", + "value": strings.Join(urls, "\n"), + }) + } + if errors := h.messageValidator.ValidateMessageSend(ctx, discord.UserID, request.Sanitize()); len(errors) != 0 { msg := fmt.Sprintf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()) ctxLogger.Warn(stacktrace.NewError(msg)) From 64a40c75298975c10fb1881872aab2dd5e153e3e Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:11:13 -0800 Subject: [PATCH 012/381] Defined attachment --- android/app/src/main/java/com/httpsms/Models.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/android/app/src/main/java/com/httpsms/Models.kt b/android/app/src/main/java/com/httpsms/Models.kt index ccfe590b4..5b7184503 100644 --- a/android/app/src/main/java/com/httpsms/Models.kt +++ b/android/app/src/main/java/com/httpsms/Models.kt @@ -29,6 +29,13 @@ data class Phone ( val userID: String, ) +data class Attachment ( + @Json(name = "content_type") + val contentType: String, + + val url: String +) + data class Message ( val contact: String, val content: String, @@ -69,4 +76,6 @@ data class Message ( @Json(name = "updated_at") val updatedAt: String + + val attachments: List? = null ) From 173a4f1d2076ca0d868bde67a8e9b4d98f450a2d Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:14:20 -0800 Subject: [PATCH 013/381] Added a sendMultimediaMessage function --- .../main/java/com/httpsms/SmsManagerService.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/android/app/src/main/java/com/httpsms/SmsManagerService.kt b/android/app/src/main/java/com/httpsms/SmsManagerService.kt index 17987b5cc..196c38685 100644 --- a/android/app/src/main/java/com/httpsms/SmsManagerService.kt +++ b/android/app/src/main/java/com/httpsms/SmsManagerService.kt @@ -76,4 +76,20 @@ class SmsManagerService { context.getSystemService(SmsManager::class.java).createForSubscriptionId(subscriptionId) } } + + fun sendMultimediaMessage( + context: Context, + pduUri: android.net.Uri, + sim: String, + sentIntent: PendingIntent + ) { + val smsManager = getSmsManager(context, sim) + smsManager.sendMultimediaMessage( + context, + pduUri, + null, + null, + sentIntent + ) + } } From 8a19b1d9021c564a1bedd2c76285b15f29ad681e Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:25:10 -0800 Subject: [PATCH 014/381] Added a filesize validation to make sure we're under 1.5MB --- .../java/com/httpsms/SmsManagerService.kt | 15 ++------- .../bulk_message_handler_validator.go | 6 +++- .../validators/message_handler_validator.go | 4 +++ api/pkg/validators/validator.go | 31 +++++++++++++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/java/com/httpsms/SmsManagerService.kt b/android/app/src/main/java/com/httpsms/SmsManagerService.kt index 196c38685..59fbdad86 100644 --- a/android/app/src/main/java/com/httpsms/SmsManagerService.kt +++ b/android/app/src/main/java/com/httpsms/SmsManagerService.kt @@ -77,19 +77,8 @@ class SmsManagerService { } } - fun sendMultimediaMessage( - context: Context, - pduUri: android.net.Uri, - sim: String, - sentIntent: PendingIntent - ) { + fun sendMultimediaMessage(context: Context, pduUri: android.net.Uri, sim: String, sentIntent: PendingIntent) { val smsManager = getSmsManager(context, sim) - smsManager.sendMultimediaMessage( - context, - pduUri, - null, - null, - sentIntent - ) + smsManager.sendMultimediaMessage(context, pduUri, null, null, sentIntent) } } diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 848443926..b96829af7 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -221,7 +221,7 @@ func (v *BulkMessageHandlerValidator) validateMessages(messages []*requests.Bulk if message.AttachmentURLs != "" { urls := strings.Split(message.AttachmentURLs, ",") - + if len(urls) > 10 { result.Add("document", fmt.Sprintf("Row [%d]: You cannot attach more than 10 files per message.", index+2)) } @@ -237,6 +237,10 @@ func (v *BulkMessageHandlerValidator) validateMessages(messages []*requests.Bulk result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] has an invalid url format.", index+2, cleanURL)) } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] must use http or https.", index+2, cleanURL)) + } else { + if err := validateAttachmentURL(cleanURL); err != nil { + result.Add("attachments", fmt.Sprintf("Row [%d]: The attachment URL [%s] failed validation: %s", index+2, cleanURL, err.Error())) + } } } } diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index 20a8ed9fb..ffbdd7785 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -123,6 +123,10 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context result.Add("attachments", fmt.Sprintf("attachment at index %d has an invalid url format", i)) } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) + } else { + if err := validateAttachmentURL(attachment.URL); err != nil { + result.Add("attachments", fmt.Sprintf("attachment at index %d failed validation: %s", i, err.Error())) + } } } } diff --git a/api/pkg/validators/validator.go b/api/pkg/validators/validator.go index bc7111e81..bcef0a9bf 100644 --- a/api/pkg/validators/validator.go +++ b/api/pkg/validators/validator.go @@ -2,9 +2,11 @@ package validators import ( "fmt" + "net/http" "net/url" "regexp" "strings" + "time" "github.com/NdoleStudio/httpsms/pkg/events" @@ -160,3 +162,32 @@ func (validator *validator) ValidateUUID(ID string, name string) url.Values { return v.ValidateStruct() } + +func validateAttachmentURL(attachmentURL string) error { + client := &http.Client{ + Timeout: 5 * time.Second, + } + + req, err := http.NewRequest(http.MethodHead, attachmentURL, nil) + if err != nil { + return fmt.Errorf("invalid url format") + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("could not reach the url") + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return fmt.Errorf("url returned an error status code: %d", resp.StatusCode) + } + + const maxSizeBytes = 1.5 * 1024 * 1024 + + if resp.ContentLength > int64(maxSizeBytes) { + return fmt.Errorf("file size (%.2f MB) exceeds the 1.5 MB carrier limit", float64(resp.ContentLength)/(1024*1024)) + } + + return nil +} From 40f718cc5bfd83f77ebea57e64077798be18ac89 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:25:45 -0800 Subject: [PATCH 015/381] Added missing validation to bulk send validator --- api/pkg/validators/message_handler_validator.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index ffbdd7785..ec23ab783 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -198,6 +198,10 @@ func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Con result.Add("attachments", fmt.Sprintf("attachment at index %d has an invalid url format", i)) } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) + } else { + if err := validateAttachmentURL(attachment.URL); err != nil { + result.Add("attachments", fmt.Sprintf("attachment at index %d failed validation: %s", i, err.Error())) + } } } } From f2a0ab8a64831856ac92852c117012d6845f7e98 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:35:27 -0800 Subject: [PATCH 016/381] Added cache for csv uploads to avoid 1000s of duplicate http requests --- .../bulk_message_handler_validator.go | 10 ++++-- .../validators/message_handler_validator.go | 8 +++-- api/pkg/validators/validator.go | 34 ++++++++++++++++--- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index b96829af7..137442d04 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -17,6 +17,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/dustin/go-humanize" "github.com/jszwec/csvutil" "github.com/nyaruka/phonenumbers" @@ -30,6 +31,7 @@ type BulkMessageHandlerValidator struct { userService *services.UserService logger telemetry.Logger tracer telemetry.Tracer + cache cache.Cache } // NewBulkMessageHandlerValidator creates a new handlers.BulkMessageHandlerValidator validator @@ -38,12 +40,14 @@ func NewBulkMessageHandlerValidator( tracer telemetry.Tracer, phoneService *services.PhoneService, userService *services.UserService, + appCache cache.Cache, ) (v *BulkMessageHandlerValidator) { return &BulkMessageHandlerValidator{ logger: logger.WithService(fmt.Sprintf("%T", v)), tracer: tracer, userService: userService, phoneService: phoneService, + cache: appCache, } } @@ -79,7 +83,7 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID messages[index] = message.Sanitize() } - result = v.validateMessages(messages) + result = v.validateMessages(ctx, messages) if len(result) != 0 { return messages, result } @@ -215,7 +219,7 @@ func (v *BulkMessageHandlerValidator) parseCSV(ctxLogger telemetry.Logger, user return messages, url.Values{} } -func (v *BulkMessageHandlerValidator) validateMessages(messages []*requests.BulkMessage) url.Values { +func (v *BulkMessageHandlerValidator) validateMessages(ctx context.Context, messages []*requests.BulkMessage) url.Values { result := url.Values{} for index, message := range messages { @@ -238,7 +242,7 @@ func (v *BulkMessageHandlerValidator) validateMessages(messages []*requests.Bulk } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] must use http or https.", index+2, cleanURL)) } else { - if err := validateAttachmentURL(cleanURL); err != nil { + if err := validateAttachmentURL(ctx, v.cache, cleanURL); err != nil { result.Add("attachments", fmt.Sprintf("Row [%d]: The attachment URL [%s] failed validation: %s", index+2, cleanURL, err.Error())) } } diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index ec23ab783..527418919 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/palantir/stacktrace" @@ -25,6 +26,7 @@ type MessageHandlerValidator struct { tracer telemetry.Tracer phoneService *services.PhoneService tokenValidator *TurnstileTokenValidator + cache cache.Cache } // NewMessageHandlerValidator creates a new handlers.MessageHandler validator @@ -33,12 +35,14 @@ func NewMessageHandlerValidator( tracer telemetry.Tracer, phoneService *services.PhoneService, tokenValidator *TurnstileTokenValidator, + appCache cache.Cache, ) (v *MessageHandlerValidator) { return &MessageHandlerValidator{ logger: logger.WithService(fmt.Sprintf("%T", v)), tracer: tracer, phoneService: phoneService, tokenValidator: tokenValidator, + cache: appCache, } } @@ -124,7 +128,7 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) } else { - if err := validateAttachmentURL(attachment.URL); err != nil { + if err := validateAttachmentURL(ctx, validator.cache, attachment.URL); err != nil { result.Add("attachments", fmt.Sprintf("attachment at index %d failed validation: %s", i, err.Error())) } } @@ -199,7 +203,7 @@ func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Con } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("attachments", fmt.Sprintf("attachment at index %d must use http or https scheme", i)) } else { - if err := validateAttachmentURL(attachment.URL); err != nil { + if err := validateAttachmentURL(ctx, validator.cache, attachment.URL); err != nil { result.Add("attachments", fmt.Sprintf("attachment at index %d failed validation: %s", i, err.Error())) } } diff --git a/api/pkg/validators/validator.go b/api/pkg/validators/validator.go index bcef0a9bf..850d1e76b 100644 --- a/api/pkg/validators/validator.go +++ b/api/pkg/validators/validator.go @@ -1,6 +1,7 @@ package validators import ( + "context" "fmt" "net/http" "net/url" @@ -8,6 +9,7 @@ import ( "strings" "time" + "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/NdoleStudio/httpsms/pkg/events" "github.com/nyaruka/phonenumbers" @@ -163,31 +165,53 @@ func (validator *validator) ValidateUUID(ID string, name string) url.Values { return v.ValidateStruct() } -func validateAttachmentURL(attachmentURL string) error { +func validateAttachmentURL(ctx context.Context, c cache.Cache, attachmentURL string) error { + cacheKey := "mms-url-validation:" + attachmentURL + + if cachedVal, err := c.Get(ctx, cacheKey); err == nil { + if cachedVal == "valid" { + return nil + } + return fmt.Errorf(cachedVal) + } + client := &http.Client{ Timeout: 5 * time.Second, } req, err := http.NewRequest(http.MethodHead, attachmentURL, nil) if err != nil { - return fmt.Errorf("invalid url format") + errMsg := fmt.Sprintf("invalid url format") + saveToCache(ctx, c, cacheKey, errMsg) + return fmt.Errorf(errMsg) } resp, err := client.Do(req) if err != nil { - return fmt.Errorf("could not reach the url") + errMsg := fmt.Sprintf("could not reach the url") + saveToCache(ctx, c, cacheKey, errMsg) + return fmt.Errorf(errMsg) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return fmt.Errorf("url returned an error status code: %d", resp.StatusCode) + errMsg := fmt.Sprintf("url returned an error status code: %d", resp.StatusCode) + saveToCache(ctx, c, cacheKey, errMsg) + return fmt.Errorf(errMsg) } const maxSizeBytes = 1.5 * 1024 * 1024 if resp.ContentLength > int64(maxSizeBytes) { - return fmt.Errorf("file size (%.2f MB) exceeds the 1.5 MB carrier limit", float64(resp.ContentLength)/(1024*1024)) + errMsg := fmt.Sprintf("file size (%.2f MB) exceeds the 1.5 MB carrier limit", float64(resp.ContentLength)/(1024*1024)) + saveToCache(ctx, c, cacheKey, errMsg) + return fmt.Errorf(errMsg) } + saveToCache(ctx, c, cacheKey, "valid") return nil } + +func saveToCache(ctx context.Context, c cache.Cache, key string, value string) { + _ = c.Set(ctx, key, value, 24*time.Hour) +} From 3829d74b7830eae1a8ab301c711ce4719fdd7975 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:40:58 -0800 Subject: [PATCH 017/381] Added provider to manifest --- android/app/src/main/AndroidManifest.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6d704adea..409b7140f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -90,6 +90,17 @@ + + + + + From 41c7bedea4bd8a811f5cb135165fde819b029d8a Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:41:58 -0800 Subject: [PATCH 018/381] File path for mms attachments/cache --- android/app/src/main/res/xml/file_paths.xml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 android/app/src/main/res/xml/file_paths.xml diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..0df3af414 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From e8140cdf4b84e62a7156af4bf9e28cec5bd2824f Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 01:55:58 -0800 Subject: [PATCH 019/381] added missing cache arguments --- api/pkg/di/container.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 632e47060..02d377389 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -534,6 +534,7 @@ func (container *Container) MessageHandlerValidator() (validator *validators.Mes container.Tracer(), container.PhoneService(), container.TurnstileTokenValidator(), + container.Cache(), ) } @@ -556,6 +557,7 @@ func (container *Container) BulkMessageHandlerValidator() (validator *validators container.Tracer(), container.PhoneService(), container.UserService(), + container.Cache(), ) } From b35f60d7278609ae6c870db844673d124fa33433 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 02:15:11 -0800 Subject: [PATCH 020/381] Added web support for attachments --- web/components/MessageThread.vue | 11 +++++++++-- web/models/message.ts | 6 ++++++ web/pages/messages/index.vue | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/web/components/MessageThread.vue b/web/components/MessageThread.vue index 0f0f222b9..e2ed47392 100644 --- a/web/components/MessageThread.vue +++ b/web/components/MessageThread.vue @@ -95,8 +95,13 @@ {{ thread.contact | phoneNumber }} - - {{ thread.last_message_content }} + + + {{ thread.last_message_content }} + + + {{ mdiPaperclip }} Multimedia Message + @@ -150,6 +155,7 @@ import { mdiCheck, mdiAlert, mdiAccount, + mdiPaperclip, } from '@mdi/js' @Component @@ -160,6 +166,7 @@ export default class MessageThread extends Vue { mdiAlert = mdiAlert mdiCheck = mdiCheck mdiCheckAll = mdiCheckAll + mdiPaperclip = mdiPaperclip get threads(): Array { return this.$store.getters.getThreads diff --git a/web/models/message.ts b/web/models/message.ts index 353066481..c0d09f12b 100644 --- a/web/models/message.ts +++ b/web/models/message.ts @@ -1,6 +1,12 @@ +export interface MessageAttachment { + content_type: string + url: string +} + export interface Message { contact: string content: string + attachments?: MessageAttachment[] created_at: string failure_reason: string id: string diff --git a/web/pages/messages/index.vue b/web/pages/messages/index.vue index a1a2a1541..78a465332 100644 --- a/web/pages/messages/index.vue +++ b/web/pages/messages/index.vue @@ -33,6 +33,16 @@ placeholder="Enter your message here" label="Content" > + { @@ -113,6 +144,9 @@ export default { ), ) } + if (response.data.data.attachments) { + errors.set('attachments', response.data.data.attachments) + } if (response.data.data.from) { this.$store.dispatch('addNotification', { message: response.data.data.from[0], From adfa171d0fdf94431b62f6ae5de6381ec6da4d2d Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 02:52:01 -0800 Subject: [PATCH 021/381] Added PDU generation via android-smsmms and MMS sender/handler --- android/app/build.gradle | 1 + .../com/httpsms/FirebaseMessagingService.kt | 113 ++++++++++++++++++ .../java/com/httpsms/HttpSmsApiService.kt | 47 ++++++++ .../src/main/java/com/httpsms/SentReceiver.kt | 22 ++++ 4 files changed, 183 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index f49acb2bf..66eb98c18 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'com.googlecode.libphonenumber:libphonenumber:9.0.4' + implementation 'com.klinkerapps:android-smsmms:5.2.6' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' diff --git a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt index 8f1e448ca..dca73a5e3 100644 --- a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt +++ b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt @@ -9,6 +9,13 @@ import com.google.firebase.messaging.RemoteMessage import com.httpsms.SentReceiver.FailedMessageWorker import timber.log.Timber +import com.google.android.mms.pdu.CharacterSets +import com.google.android.mms.pdu.EncodedStringValue +import com.google.android.mms.pdu.PduBody +import com.google.android.mms.pdu.PduComposer +import com.google.android.mms.pdu.PduPart +import com.google.android.mms.pdu.SendReq + class MyFirebaseMessagingService : FirebaseMessagingService() { // [START receive_message] override fun onMessageReceived(remoteMessage: RemoteMessage) { @@ -158,6 +165,11 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { } Receiver.register(applicationContext) + + if (message.attachments != null && message.attachments.isNotEmpty()) { + return handleMmsMessage(message) + } + val parts = getMessageParts(applicationContext, message) if (parts.size == 1) { return handleSingleMessage(message, parts.first()) @@ -165,6 +177,107 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { return handleMultipartMessage(message, parts) } + private fun handleMmsMessage(message: Message): Result { + Timber.d("Processing MMS for message ID [${message.id}]") + val apiService = HttpSmsApiService.create(applicationContext) + + val downloadedFiles = mutableListOf() + + try { + for ((index, attachment) in message.attachments!!.withIndex()) { + val file = apiService.downloadAttachment(applicationContext, attachment.url, message.id, index) + if (file == null) { + handleFailed(applicationContext, message.id, "Failed to download attachment or file size exceeded 1.5MB.") + return Result.failure() + } + downloadedFiles.add(file) + } + + val sendReq = SendReq() + + val encodedContact = EncodedStringValue(message.contact) + sendReq.to = arrayOf(encodedContact) + + val pduBody = PduBody() + + if (message.content.isNotEmpty()) { + val textPart = PduPart() + textPart.setCharset(CharacterSets.UTF_8) + textPart.contentType = "text/plain".toByteArray() + textPart.name = "text".toByteArray() + textPart.contentId = "text".toByteArray() + textPart.contentLocation = "text".toByteArray() + + var messageBody = message.content + val encryptionKey = Settings.getEncryptionKey(applicationContext) + if (message.encrypted && !encryptionKey.isNullOrEmpty()) { + messageBody = Encrypter.decrypt(encryptionKey, messageBody) + } + textPart.data = messageBody.toByteArray(Charsets.UTF_8) + + pduBody.addPart(textPart) + } + + for ((index, file) in downloadedFiles.withIndex()) { + val attachment = message.attachments[index] + val fileBytes = file.readBytes() + + val mediaPart = PduPart() + mediaPart.contentType = attachment.contentType.toByteArray() + + val fileName = "attachment_$index".toByteArray() + mediaPart.name = fileName + mediaPart.contentId = fileName + mediaPart.contentLocation = fileName + mediaPart.data = fileBytes + + pduBody.addPart(mediaPart) + } + + sendReq.body = pduBody + + val pduComposer = PduComposer(applicationContext, sendReq) + val pduBytes = pduComposer.make() + + if (pduBytes == null) { + Timber.e("PduComposer failed to generate PDU byte array") + handleFailed(applicationContext, message.id, "Failed to compose MMS PDU.") + return Result.failure() + } + + val mmsDir = java.io.File(applicationContext.cacheDir, "mms_attachments") + if (!mmsDir.exists()) { + mmsDir.mkdirs() + } + + val pduFile = java.io.File(mmsDir, "pdu_${message.id}.dat") + java.io.FileOutputStream(pduFile).use { it.write(pduBytes) } + + val pduUri = androidx.core.content.FileProvider.getUriForFile( + applicationContext, + "${BuildConfig.APPLICATION_ID}.fileprovider", + pduFile + ) + + val sentIntent = createPendingIntent(message.id, SmsManagerService.sentAction()) + SmsManagerService().sendMultimediaMessage(applicationContext, pduUri, message.sim, sentIntent) + + Timber.d("Successfully dispatched MMS for message ID [${message.id}]") + return Result.success() + + } catch (e: Exception) { + Timber.e(e, "Failed to send MMS for message ID [${message.id}]") + handleFailed(applicationContext, message.id, e.message ?: "Internal error while building or sending MMS.") + return Result.failure() + } finally { + downloadedFiles.forEach { file -> + if (file.exists()) { + file.delete() + } + } + } + } + private fun handleMultipartMessage(message:Message, parts: ArrayList): Result { Timber.d("sending multipart SMS for message with ID [${message.id}]") return try { diff --git a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt index 3d813e13f..4f1fe5dd7 100644 --- a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt +++ b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt @@ -11,6 +11,8 @@ import java.net.URI import java.net.URL import java.util.logging.Level import java.util.logging.Logger.getLogger +import java.io.File +import java.io.FileOutputStream class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { @@ -156,6 +158,51 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { return true } + fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): File? { + val request = Request.Builder().url(urlString).build() + + try { + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + Timber.e("Failed to download attachment: ${response.code}") + response.close() + return null + } + + val maxSizeBytes = 1.5 * 1024 * 1024 // most (modern?) carriers have a 2MB limit, so targetting 1.5MB should be safe + val contentLength = response.body?.contentLength() ?: -1L + if (contentLength > maxSizeBytes) { + Timber.e("Attachment is too large ($contentLength bytes).") + response.close() + return null + } + + val mmsDir = File(context.cacheDir, "mms_attachments") + if (!mmsDir.exists()) { + mmsDir.mkdirs() + } + + val tempFile = File(mmsDir, "mms_${messageId}_$attachmentIndex") + val inputStream = response.body?.byteStream() + val outputStream = FileOutputStream(tempFile) + inputStream?.copyTo(outputStream) + + outputStream.close() + inputStream?.close() + response.close() + + if (tempFile.length() > maxSizeBytes) { + tempFile.delete() + Timber.e("Downloaded file exceeded 1.5MB limit.") + return null + } + + return tempFile + } catch (e: Exception) { + Timber.e(e, "Exception while downloading attachment") + return null + } + } private fun sendEvent(messageId: String, event: String, timestamp: String, reason: String? = null): Boolean { var reasonString = "null" diff --git a/android/app/src/main/java/com/httpsms/SentReceiver.kt b/android/app/src/main/java/com/httpsms/SentReceiver.kt index 7995c35c7..00e76c2cd 100644 --- a/android/app/src/main/java/com/httpsms/SentReceiver.kt +++ b/android/app/src/main/java/com/httpsms/SentReceiver.kt @@ -17,6 +17,8 @@ import timber.log.Timber internal class SentReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + val messageId = intent.getStringExtra(Constants.KEY_MESSAGE_ID) + cleanupPduFile(context, messageId) when (resultCode) { Activity.RESULT_OK -> handleMessageSent(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID)) SmsManager.RESULT_ERROR_GENERIC_FAILURE -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "GENERIC_FAILURE") @@ -27,6 +29,26 @@ internal class SentReceiver : BroadcastReceiver() { } } + private fun cleanupPduFile(context: Context, messageId: String?) { + if (messageId == null) return + + try { + val baseMessageId = messageId.substringBefore(".") + val mmsDir = File(context.cacheDir, "mms_attachments") + val pduFile = File(mmsDir, "pdu_$baseMessageId.dat") + + if (pduFile.exists()) { + if (pduFile.delete()) { + Timber.d("Cleaned up PDU file for message ID [$baseMessageId]") + } else { + Timber.w("Failed to delete PDU file for message ID [$baseMessageId]") + } + } + } catch (e: Exception) { + Timber.e(e, "Error cleaning up PDU file for message ID [$messageId]") + } + } + private fun handleMessageSent(context: Context, messageId: String?) { if (!Receiver.isValid(context, messageId)) { return From 5508157e936e35ee5aa32e74739164c2f5c4fdb7 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Fri, 27 Feb 2026 09:36:36 -0800 Subject: [PATCH 022/381] Added annotations for Codacy --- android/app/src/main/java/com/httpsms/HttpSmsApiService.kt | 1 + android/app/src/main/java/com/httpsms/Models.kt | 1 + android/app/src/main/java/com/httpsms/SmsManagerService.kt | 1 + 3 files changed, 3 insertions(+) diff --git a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt index 4f1fe5dd7..c3ae8e13a 100644 --- a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt +++ b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt @@ -158,6 +158,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { return true } + // Downloads the attachment URL content locally fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): File? { val request = Request.Builder().url(urlString).build() diff --git a/android/app/src/main/java/com/httpsms/Models.kt b/android/app/src/main/java/com/httpsms/Models.kt index 5b7184503..d2559fb00 100644 --- a/android/app/src/main/java/com/httpsms/Models.kt +++ b/android/app/src/main/java/com/httpsms/Models.kt @@ -29,6 +29,7 @@ data class Phone ( val userID: String, ) +// mms attachment data class Attachment ( @Json(name = "content_type") val contentType: String, diff --git a/android/app/src/main/java/com/httpsms/SmsManagerService.kt b/android/app/src/main/java/com/httpsms/SmsManagerService.kt index 59fbdad86..5f7ce6f56 100644 --- a/android/app/src/main/java/com/httpsms/SmsManagerService.kt +++ b/android/app/src/main/java/com/httpsms/SmsManagerService.kt @@ -77,6 +77,7 @@ class SmsManagerService { } } + // Wrapper for the smsManager's sendMultimediaMessage fun sendMultimediaMessage(context: Context, pduUri: android.net.Uri, sim: String, sentIntent: PendingIntent) { val smsManager = getSmsManager(context, sim) smsManager.sendMultimediaMessage(context, pduUri, null, null, sentIntent) From 301540c2eb89097963406a7cf7c07551f597a94a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:03:26 +0000 Subject: [PATCH 023/381] fix(deps): bump google.golang.org/api from 0.267.0 to 0.269.0 in /api Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.267.0 to 0.269.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.267.0...v0.269.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.269.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- api/go.mod | 2 +- api/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 3a1c6701c..49c3f34fd 100644 --- a/api/go.mod +++ b/api/go.mod @@ -50,7 +50,7 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 - google.golang.org/api v0.267.0 + google.golang.org/api v0.269.0 google.golang.org/protobuf v1.36.11 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 diff --git a/api/go.sum b/api/go.sum index 26dba3adf..a2c973963 100644 --- a/api/go.sum +++ b/api/go.sum @@ -541,8 +541,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= -google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 h1:MzLVemxGdOBt2uziz9LnuYRQQFw1FDV0s0af4GVYE1A= From 42210657c13b711fa8a7ba33da0c6f808d5b7bbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:03:31 +0000 Subject: [PATCH 024/381] fix(deps): bump github.com/uptrace/uptrace-go in /api Bumps [github.com/uptrace/uptrace-go](https://github.com/uptrace/uptrace-go) from 1.39.0 to 1.40.0. - [Release notes](https://github.com/uptrace/uptrace-go/releases) - [Commits](https://github.com/uptrace/uptrace-go/compare/v1.39.0...v1.40.0) --- updated-dependencies: - dependency-name: github.com/uptrace/uptrace-go dependency-version: 1.40.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- api/go.mod | 5 +++-- api/go.sum | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/api/go.mod b/api/go.mod index 3a1c6701c..976263647 100644 --- a/api/go.mod +++ b/api/go.mod @@ -43,7 +43,7 @@ require ( github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc - github.com/uptrace/uptrace-go v1.39.0 + github.com/uptrace/uptrace-go v1.40.0 github.com/xuri/excelize/v2 v2.10.0 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/metric v1.40.0 @@ -171,6 +171,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 // indirect + go.opentelemetry.io/contrib/processors/minsev v0.13.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect @@ -195,7 +196,7 @@ require ( golang.org/x/tools v0.42.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260217200457-a2cb2272a1e9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/grpc v1.79.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/api/go.sum b/api/go.sum index 26dba3adf..d625f903a 100644 --- a/api/go.sum +++ b/api/go.sum @@ -343,8 +343,8 @@ github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrI github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc h1:lzi/5fg2EfinRlh3v//YyIhnc4tY7BTqazQGwb1ar+0= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= -github.com/uptrace/uptrace-go v1.39.0 h1:MszuE3eX/z86xzYywN2JBtYcmsS4ofdo1VMDhRvkWrI= -github.com/uptrace/uptrace-go v1.39.0/go.mod h1:FquipEqgTMXPbhdhenjbiLHG1R5WYdxVH6zgwHeMzzA= +github.com/uptrace/uptrace-go v1.40.0 h1:fMva36FZ/eujU60hq+ke9HdYGkXP5jJXUTNeEuWDI+I= +github.com/uptrace/uptrace-go v1.40.0/go.mod h1:HJhggr8UMkJ+keR8B9o4KsF7kxT8lKH7Ra8X2DRwqdc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= @@ -385,6 +385,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0/go.mod h1:Z1pjGxUL3nJ/IbDDfL6rBD0Xbz7ZOViRqrIUg4l1CYE= +go.opentelemetry.io/contrib/processors/minsev v0.13.0 h1:pADh6ro5deXRfNmry136khTZYWVXn9NKZR5nZuEXtXw= +go.opentelemetry.io/contrib/processors/minsev v0.13.0/go.mod h1:MC0s+ldbPprTztVZQ/pecYqPSxwfjQkdnCeC3u6uGQU= go.opentelemetry.io/contrib/propagators/b3 v1.19.0 h1:ulz44cpm6V5oAeg5Aw9HyqGFMS6XM7untlMEhD7YzzA= go.opentelemetry.io/contrib/propagators/b3 v1.19.0/go.mod h1:OzCmE2IVS+asTI+odXQstRGVfXQ4bXv9nMBRK0nNyqQ= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= @@ -547,8 +549,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 h1:MzLVemxGdOBt2uziz9LnuYRQQFw1FDV0s0af4GVYE1A= google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9/go.mod h1:9mSgs6f8tLwHSr6EzFWG+naa04gb1Zpt4IumYKsRDs0= -google.golang.org/genproto/googleapis/api v0.0.0-20260217200457-a2cb2272a1e9 h1:yt1EUx2U7D7a0Myzkt7z+RR5hnl6mCdzcUfYHx7LEls= -google.golang.org/genproto/googleapis/api v0.0.0-20260217200457-a2cb2272a1e9/go.mod h1:S3ojYC5GRm/7ewOSg8Rh+iqeqxS3BLlLKFhVlbYwHuU= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= From 2802cde39710bf628c9d07854156afca99e3e204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:03:42 +0000 Subject: [PATCH 025/381] fix(deps): bump github.com/nyaruka/phonenumbers in /api Bumps [github.com/nyaruka/phonenumbers](https://github.com/nyaruka/phonenumbers) from 1.6.9 to 1.6.10. - [Release notes](https://github.com/nyaruka/phonenumbers/releases) - [Changelog](https://github.com/nyaruka/phonenumbers/blob/main/CHANGELOG.md) - [Commits](https://github.com/nyaruka/phonenumbers/compare/v1.6.9...v1.6.10) --- updated-dependencies: - dependency-name: github.com/nyaruka/phonenumbers dependency-version: 1.6.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- api/go.mod | 2 +- api/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 3a1c6701c..e075e3ac7 100644 --- a/api/go.mod +++ b/api/go.mod @@ -31,7 +31,7 @@ require ( github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jszwec/csvutil v1.10.0 github.com/lib/pq v1.11.2 - github.com/nyaruka/phonenumbers v1.6.9 + github.com/nyaruka/phonenumbers v1.6.10 github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 diff --git a/api/go.sum b/api/go.sum index 26dba3adf..c70fa5efc 100644 --- a/api/go.sum +++ b/api/go.sum @@ -271,8 +271,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/nyaruka/phonenumbers v1.6.9 h1:LUmsIr+WKyBhWTzxm/9j+kGC9JclO+hBOHc18PSo9iM= -github.com/nyaruka/phonenumbers v1.6.9/go.mod h1:IUu45lj2bSeYXQuxDyyuzOrdV10tyRa1YSsfH8EKN5c= +github.com/nyaruka/phonenumbers v1.6.10 h1:kGTxTzd320dUamRB/MPeZSIwKNLn4vHlysOt5Cp8uoU= +github.com/nyaruka/phonenumbers v1.6.10/go.mod h1:IUu45lj2bSeYXQuxDyyuzOrdV10tyRa1YSsfH8EKN5c= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= From a8c976bb5598f25ccebabab756c4713c55cd85ea Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Sat, 28 Feb 2026 20:22:34 -0800 Subject: [PATCH 026/381] Fixed imports and missing comma --- .../java/com/httpsms/FirebaseMessagingService.kt | 12 ++++++------ android/app/src/main/java/com/httpsms/Models.kt | 2 +- .../app/src/main/java/com/httpsms/SentReceiver.kt | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt index dca73a5e3..03c99bb47 100644 --- a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt +++ b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt @@ -9,12 +9,12 @@ import com.google.firebase.messaging.RemoteMessage import com.httpsms.SentReceiver.FailedMessageWorker import timber.log.Timber -import com.google.android.mms.pdu.CharacterSets -import com.google.android.mms.pdu.EncodedStringValue -import com.google.android.mms.pdu.PduBody -import com.google.android.mms.pdu.PduComposer -import com.google.android.mms.pdu.PduPart -import com.google.android.mms.pdu.SendReq +import com.google.android.mms.pdu_alt.CharacterSets +import com.google.android.mms.pdu_alt.EncodedStringValue +import com.google.android.mms.pdu_alt.PduBody +import com.google.android.mms.pdu_alt.PduComposer +import com.google.android.mms.pdu_alt.PduPart +import com.google.android.mms.pdu_alt.SendReq class MyFirebaseMessagingService : FirebaseMessagingService() { // [START receive_message] diff --git a/android/app/src/main/java/com/httpsms/Models.kt b/android/app/src/main/java/com/httpsms/Models.kt index d2559fb00..1ee2dbc8d 100644 --- a/android/app/src/main/java/com/httpsms/Models.kt +++ b/android/app/src/main/java/com/httpsms/Models.kt @@ -76,7 +76,7 @@ data class Message ( val type: String, @Json(name = "updated_at") - val updatedAt: String + val updatedAt: String, val attachments: List? = null ) diff --git a/android/app/src/main/java/com/httpsms/SentReceiver.kt b/android/app/src/main/java/com/httpsms/SentReceiver.kt index 00e76c2cd..8786ba2c9 100644 --- a/android/app/src/main/java/com/httpsms/SentReceiver.kt +++ b/android/app/src/main/java/com/httpsms/SentReceiver.kt @@ -14,6 +14,7 @@ import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.workDataOf import timber.log.Timber +import java.io.File internal class SentReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { From cafaab90086d37d9158e0306a59a351d75162b0b Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Sat, 28 Feb 2026 20:39:51 -0800 Subject: [PATCH 027/381] Updated UI to show attachments --- web/components/MessageThread.vue | 5 +--- web/pages/threads/_id/index.vue | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/web/components/MessageThread.vue b/web/components/MessageThread.vue index e2ed47392..51676b850 100644 --- a/web/components/MessageThread.vue +++ b/web/components/MessageThread.vue @@ -96,12 +96,9 @@ {{ thread.contact | phoneNumber }} - + {{ thread.last_message_content }} - - {{ mdiPaperclip }} Multimedia Message - diff --git a/web/pages/threads/_id/index.vue b/web/pages/threads/_id/index.vue index 06042a454..21ff39c7f 100644 --- a/web/pages/threads/_id/index.vue +++ b/web/pages/threads/_id/index.vue @@ -173,6 +173,46 @@ > + + Message Attachments + + + + + + + +
+ +
Unsupported file type
+
+
+
+
+

{{ new Date(message.order_timestamp).toLocaleString() }} From 47dde30802b6bfdad79585c8705f701e0d009440 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Sat, 28 Feb 2026 20:50:16 -0800 Subject: [PATCH 028/381] Updated templates --- web/static/templates/httpsms-bulk.csv | 6 +++--- web/static/templates/httpsms-bulk.xlsx | Bin 8991 -> 8246 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/static/templates/httpsms-bulk.csv b/web/static/templates/httpsms-bulk.csv index 38891f63e..715ecab4c 100644 --- a/web/static/templates/httpsms-bulk.csv +++ b/web/static/templates/httpsms-bulk.csv @@ -1,3 +1,3 @@ -FromPhoneNumber,ToPhoneNumber,Content -+18005550199,+18005550100,This is a sample text message1 -+18005550199,+18005550100,This is a sample text message2 +FromPhoneNumber,ToPhoneNumber,Content,SendTime(optional),AttachmentURLs(optional) ++18005550199,+18005550100,This is a sample text message1,,http://thisisasample.com/attachment.png ++18005550199,+18005550100,This is a sample text message2,2023-11-11T02:10:01, diff --git a/web/static/templates/httpsms-bulk.xlsx b/web/static/templates/httpsms-bulk.xlsx index bb713087050c20208ebcbf516a03b162f3514f34..20f2b543d704e8dfcb2836fbb5002e850cfda523 100644 GIT binary patch literal 8246 zcmd^E2UL_xvL;FzGKd3`ama#zT=#TzNbfgxYT*aqZBwq3+?%p9yh5o!Wzw&~Pcn1Oi7qat zn_#ZUEl z|2h*ixB3n{6?=a7Ono@C<|g5RT>8~xQRdLg7RR^;HZw)}cqQ6WS|^z80l|WiSYg6X zxn8evjw;KaN)m{k(e6!l>NIwLHtn3G>cFJeY~^{Q;N2Nk4l-#>uvYTe463!orVr}r zd@Xu7KUvPzC0xzH?NDwqRs5lnrPfqS6%QZJ!Y%|C69WSe8v{e@ubI*V;`+eN*4o)!96$D7gY-)df#e(pEDhmj(zk<+j&G$^!Djn!fcpkVFdJKCqlDje~A z7B+R|&WT60k3W`Mh8XzZ8#1O=rtwqAxty8J3jiw^P!08 zW}{oR;WVPAK6g!oJ*|Gv7y1U`?&C(0JLv5yai52sRO+pvDq~?`VO!hD-s;@shVL7{ zWh<$s6i1KNX(PT(;Y{!P)5D_r@wPVa>fekVWBBxHW282eoLvOYJ#S2t<^>2pA14A1 zY&e|mPfmK&U*CdeA3C~Jj+`|z4riqufD=5{77IJ18zlhqjlG4F$R_UqpWXaJ%TD1< zFrM49s6M6h8;R$ox%^TDvyV%5){n}!&3mttNKG1wgefw}uq@>wuLqc9W={1^eF#W_ z1T-MM{9OILT|NEIXQm$?h4m(M+&*9&oYpI>J>T#`^B8N(WDulb76GBAeKnQ4gL|v31K!infcHBt6F1Ly;9FDcjmv%Y1-*^XgH@Y@ zRpjoFL*S4R_>#7xIwy;&;kiC%Am66kkcigRqZzlzC2ebh)eNJPhIwi3_mwMAV6Q$O@u!9apSDOp_r zbsJj&g#@7QixqIHZ-LmNTQ6s(gT|4ATX$}nuJrV?9fGCAKG9s?Zn4EElV;3kaUp?^ z0}}$%0_&hj45lm&|I7-3IDr9g1 zED1R?4tyTmpnRq>YMl1qW2aN2?Z-~_&Ojiow-6#5en->kc|Ra{)+N=56>U7g{VY!ChCM1rwKnO zo%z@r-=99%G`#t5Y5%wP^e_A;8x-~)5jz5;kWjDyE3X40(wWNz|6ZkkN67Tf%z@;gkWfOw9L%u}c$A)1CP)Q^ zwIKeL%E;2y@seMuGnx%FhRJ+tF;k>;%&)EeG-2y=*3BX*l2o~DPGFpGoE==P-?Mmg+A#?&H*|td*QYh4 zocq=IR~k4y@xSlrv4tY%4AbWsb%w3w96FLHC!w%X)F^7!Z}U52`oO;1m6+q#p#soR?jKLeiW6UYc@_Gy`6)1>C!NfWw) zgH6Oy`4VGbtQGuG1;_fM7`{-!nYvm#T&&}+qwkC3q?}s$0ZN+v68ICWqrk`);px|S zW3yD_7y@H$Rm0T@C7E^hr~>- z%@N!sw+hp>TAfkE9Xf4YO$Q~Wa!S8^!4;~CXTl~$r(2W@eEcNhwFV3hPBi>7z*gmR zwTmb1%fP}kU>O3pUlZ@VKO8)kPI3B(K8)F5Owk7(v4|j;=jPN5I-rj9VtZrJ7DhoR z5czy>>1~aRaB@TKazRUvHiw{w*=GIq^TE&7Ck2{^H$ESKtS7l|Tn?sl+kJ9IhqhQW z(W|Oils$$}b?@)jyp%sI=5A~4XwCQi`+K?DtA8Id#!q#IizIzIJv@PriF22iYQuc+ zWw~7+$c3Wx3FT;x@%iahcCw`*J?wbW3Yv8zk%Vbov(@a?eGjc_Jl}u z%%wcqW2v_R5>1me8%SdRu~xkOUMNM?Hf^F#%O;ZiAb0A^!Um9H>03|F_9bgzlJ1J2 znBHyRal&po*78EZj+Lx5wWv~7izC@JJdJMReyr^BBrQOx0sSt}e5+31a!Me5h`Do& z{IDb@_lc|;$7BzCWD}vf+CrkD#I;Y9#qK1YaRufL#ze`^FFOL4c0}S?pBAbOhdZd6bPkMzW2*?MbwnV z*6;$c-V{G~z;;Pm-}+Mh`*FHpIY(i&Ub-CNA&KjUO#C%zPu};^_kN+Pm#eksoj1-0 zr^`6AcO5gLJx#AF_ZlZ^XXy>eA9)(p4^MNmHFveP(sg%5@7AF=>5{a^Ro=@2PO;9- z;p~1Rh^pXafejWSBpb`vY2ZjZ^kqW6I@j8?#0zd6EwY{XXKt>pE@=u^K5=WTvnGl6 z73nFfJ4iizH8ouS`IR1zEH5Cg>=UHZTQ^3F3KrNR=Houru1VKYrwY#tR%~>Ho%TcW@wbB2l_2RY>*xN{i@o3dwN7 zP5}KE)&1cVwg^dV`g$96z@ z6n!IvS|-*}r*rs#wiS?_p9&TkIhb|ZY`7}Xu#%)0Ga+v74omn<%UY6>A39}yxKtyG z4368qtACfaS5VS946>t{zZqgDzZac2O1n2hZ|PBZ0TnjEUMc<4QWIvmBIka`^)|Z)+p_ ztHBuKI(#b3j~LqvoHK@$e8S)>Rejq>3!SrzjE^V}?!`k9pFu!~pA*2#%6Km1!759FkROS155WZ}hn%cUY-byq;u>qQ3bHYR_c%INx_ZUxlX{{mn7G zRF;?syJ6!8tXk1w#+Hx#M^fbv2Vmo?xI`2j(FaQA-^M>edyqG-data}B&D5HPP92s zMenr_nGW=P3D`-vLqep@aL08^svowNHe0_vp&Zq%q0HwVMW0X1UwFn;bwx_DT^rdQHKrkn>Gb}O%9=gdNVem_f-{ul|G*H}Ox55Dj1$E%|D_Q_x<4x=~~GEc}A zt~_!#3H2UhXda=on7wV4CN$BQ5UcpY;YG>GGl=x)xzjXly3kCBt1!DmN<2=Hv(Bil zMq|^|WxDv#&flv5&ZZ9DShQ?zPxZ1_Dg~AB|q^kNMZ3!Q}T3N53 zM!?3fle4hGl?IoD{IX!&s!pLF`pjQf+{H3Ue|@Fw>!q*Hy%dpJ`QlMRgVHlm6jQkR z?XR3|n6iSN_Y*EZ684Z+`fx*FB9m_qEAFP@)g-;IV?OF|5IYT|GN>FbRF?`sf};oK z7oQe?@sg4J^3-W-{eI0EW5lb(a+y5p0N=+)hDK^^f~10{on&zqk>g>xZ6!-#=IbAU zH{j8Pt9e0ePw9;Op#i`eRdc z(1`<4#@~D8YTjIau^5An~ZjTvRt5^ zssPSry=W`BMZen~&Ep}FZp)+h(*E0t7WRGbjLtAE4L3No+VVlWz2wMUx;StXnbCxB zMcAg?`JvmQzN*Gt^og`kxGxj1xKFIja)QvMYO@UwZPFtFIcp-6NrnH=q(mr_{;=nF z8%FoZkp%OT;r3H{hS}`vIGaahmE8b25HmclQDBzMwJU7d*wBe4N=`W1_0K6v_Oc-T zX3oU@ywlapmU`tKE#~n4TPX}8Z9`u63J$Y2jEB{ne4<@&CwZCI%@h_PfznDI3zB&Z zV4#4j?O+b8Ob=WM@;bpj@zT-tzJrM-IxRnV(EyfjHj&xJ>NaCqos*U)%frlpc1CK` z2sTv+KcNBWj7?@LyqzbjxiCIJ(x3t5v3U3|Az4fnsDW^xhWq;-jON+*6(COT)=uuG zcfFjg-Hg#*j80rAFFpxAQ&@OFP3N6g5ZRo?+fk#lf_KG7f#oEASoRR2<^ z;(D3cJ(XE){JYc!5fgaMp^4IlDYkU_EXvhSxyd}T2O6}dt~qmR^5UgwWMn1MB~;{| z`KcQ71B3IE)D~39fITL(SUzbRCMQggwQngY6Uo-avOd-2i7lrU6HMusq}byr$xC&) z&bi$3l5g`*0*h7yYnL99MHaWv`2`w0IMlp$g+)1 zVKymmu!ESrE~R(9v#6L%2YV_0Jiz2(Jcldy+B;Y+Yr{8xzfUxi=XS@q-|2*f`oewG zyiDVu=LQpt3ghP?>culb^dah>%kLej{)%$3F8y;9RaCzylnb@!&(p+TQ7+bM=opQHT6iuPB)i}KS3%*|gE)p>`bNwm#C_FUz`jyB2if}PUqY2wj z`Gx{SBm572`?cxCq=-)9KZTCwd(;1&(tm}x7}L?&^QSESfVddJe{Fm*dZEMqPq71j z@Bep_z^^zLLjyY2{gk&T*b9;G*M`sw>-Nv5FPH8w#t|)5+)HS5BGhXV3j;%e9)0&; DfILSV literal 8991 zcmeHtg;yNe_H_p*0n#`G2=4Aqf(Hp2TpD+1+=IKjYaqA>mk`{7LvV-S?*8lK&3u{3 z%=Z_(SG`u%>a}X0Rdw&)=j?lK$w@)OU;y9%2mkC2{)ag9#{l`Q}b|onCUj>NNuU zzE2Mtjl95@&e-lEeT@uL)5nb9TcA)jrWt1?+VH#^%kiK|m6c_!Pf&N00#c}V?-wO) zJWj^0o;~tbY}^=Ib+yAR47}IOxFArtQ7RzGrmjPQO=Lw-vgCycCYFHLTKa5{GU`%y z15%NC0eA0Q+p>)EA}UvZ;W%(@6n(WEyJ_UW zGy&0TWW4q!_8FT7$eD*$W0kU44ZrVvg_$EgaMVt;a7(^|j^DY2XV91t8D$`YLt;s{ z{IMrPM3$SA#>XSSx38Y-i|oEWzPnZ4DEvB%=+z^}1?qO==@%hj-xp%C_ksJ%z1zHQ zdm|efz6Zw%+ny{BZ~(yLBQ!wnZ?ddWVj?|*a7`Mbx@Qnsf@}>f>=+q-oc|}s|6&dP z<<(1K-pY0`Aq5=1xeM&QoLq}zNhlfyFKMk)X+YwQjRQOlg>8bEP030ps^jGO5T9SaP*ODEHm%Uhu;n3f0U20)ITcB5MZb5amr3hY zAYnthVi^|iAQ;%8rR*9P@WEs3K9Fijc(s{5n5wvWCkl`~=FeM*#u6+=7gKM{ zTaB>qxnwhoF`Xf!~jt!r@PLf zR!&=%obc$)oh z;gXxH0FGbd(SYFOw9E&#?ke^1U$Fr%;9O<{I)(#14cc@fDZRIs3lb925NqQo0hU%0 zgq@Le=-JPyr}4ipbCFH0uO}D1I8d{Bj4iq2Pk4k2C;^EH6O{_mqLLlPnk;6dmS(0i zm=YduU3txEJj* zJk2*s+<6O$kdJ=@u{{V+!a0y!LXA=|u&Lfd$qw34omH-$gY@wt4&8)q1iSMoDFf*p zQCVe4O$MhTe>fHh)~_|lr6 zAl$i{?su%)dgm8pQ^=w(WLA4tH~cCO@x$^c)r5H;yR*PBLzXJ%6|N*aRH0+~dz2c% zq4z2$2n(%wGB`b^-*L0`-2y^SrV&`-as4y%m~BJJD)+!Zq|2O`742`lN>p2g2R<+; z(uvNj>QR{I)vh_mX8R9F?2k-gwO|?@hsOnlDlG7gyp*M#cvDDeek1y8X~wmu8B@M1 z5z19?$1doGO9%I&Tl&Zrmb7xIBcvgd?>yf^_ejMMF;z+6;913N0Gm~CGW*VAVTH9E6SM3sSM$9Ll~Ew>5T?I>5auACdcVT!Y3*Zts%<*8@~@Ea2-E z(?;O_N+k63EXuqKoz@!-I(tE!tHezYKdasohzW({f&cEKMB%MeClj#c*+U?cQ>r5->Wn=j$)3{g zbA-NX>NyHxR*$nKJeo!=)o)Ua(BJ@<01aRYPmNJP#W#sb*PhL{{!%+kWcSxf8m9vqPix zzMjb?$V~q;>2HT9!kk0sQV;P>IFLL3NcwgrhKBZbj6W~TKeB&X>|8`D6ENVA_M9N< zuq`b%D@sYtajvDR#sRf#QPcpi8RMHE+#bVJGrdzkv@zVC|Z+g$@k(8!9*T1`d3 zLu`HEHi^8c)Wr?Siy+ky<>?Lt@`&}B@gl_(dW{k{!p<;Ww{Y2=Z~bfw6=;kz{83?drHgX@B-*4K8;)d6*<$78EC*#~AmSsdt4@(P1}>)%n^GP~;s*{>I=M7`%oW(FV9r6AS`ba_T%k*sSya};79%gCG{kT*#gKV4 z#?ADr&+sG0IuD46sCj_cfXBz5go%sRdfLu}IC+LoOv5V-%-HLJnyt7*|l9Fmcc_B=n%rjS?0bK;e@RiqcoTm2ITgr#Wb^i*Xi`qq;_=?daZ<^eyGMRx~4swYmq;(I0VA z7pb{M`gle3umRl>5-C(yJcHFRHe62l^AUfZs;J*c%bvfblN|m^tK2K`mbOxfWrS5l zfvR76*sD_NC zq&AF)9+-2Xy zg`pZET3tV*yOWyB^sq&>>_h08hQ1cD$W9=X;GUzw>Ou&JuK!}h<-J4^*7K&FG!v@_ zpTKu935)ilLMaYu%$2r}o}Alz7Glypx58%G73OFm!{Dxo0% zErkOD;@K133})V!t8QbLRA^`(CfDTmo7i=XSfk1mA{%gO!d6+2EDh2+?y949C);gi zV?+qP>TVxF&;^=3=ezgnt+iwMPfXZ+;`3^!d3NKiqI8kvZYHyvR1TE}uog!^TAvyS ziX#1|vfg=C1P~%ME{i@Z7o--@>@ zHOyifv~NYAFCzI0w!}~m%SRy>?1M-Zy&L#eoz zJLky95zxQ^XcRj2@2HU_m9EMKydUyNLu#2=a^tmhf>rSpE}Zo``=o2{XtO3ci5N5Z z-5!p%PXk-|J9#p*mJ;OUuOH@!|)i$QsS}7bxP~0ROv@ylc zvMOB>e0BgV_YE2+7q- z34{Ee0=Eb=JHh8rgLM{L&o*(2GDb2H*AVK{8+|%IsqZ|2=Pv|XpCYZH+`iQ6tz;=(E;T7Q*#+R#Np07i!ZNE1nsfS%R zONpZlx-lFgsB8{PZXv|B_o)aqoaL*Xt{Dtw<>fgojGt}7QM3>1#&N3?uu~n;WgXaT z7*!$Bi6_{TETQckQaVr5d14q-Wd4oXHW7i=@bWm$vcec_M7VQrkJKyiQRAnHRD8AtOIu~Yg!NRryd>9BLDtp7+iNV zT%wD*i$S2gxeuRi0K!uw>;<17%m+=8^kQ|3gSgb`RF3D+7-GsT=&|eJUBkguly^@3 zFP)y)!NEpq9N)(Aq*57fighZS7Xk1&M+;GS| z`Bc68{Nh~DEV}WrNw1A(E>U z_9VLbI8ypG8;VzyEVGd#Nb16d3t!Sgq|e_uvr=T~=LR~^VrGt%-pZUhM~08f7&nwO zrW8Jdjk$KF)(jU;l+4>w=RaI5o!Ir*dgr8;2uC$Dj-xAWgeUKSBM(9m!q7JM)1FCI z9ko{-mD@?oRu{9`c*R`zr#SmENk_^&!;>gaVv}Nxy@Ff*NU$n?Ba1u`T+rgQB zH_q##*_p79-45hkM=m}Qo~WYvXmu|`Z(C;0(13zA=_1*@GVwkXx{ZlG^DE0+wPu-) zkWZiF=c-xh5RhaiIIb)m#JKp6%P3#y6{k37wBs3rARL=BA{7^q{tYUyz)pk9(op3Vi3Qkd0bf?C*%9JaAv3 zx#K|dd|4-2UK3?}_vVSF?ue+PX?f2S!@=4H{h&W;Xs9&kD|XGw&FW{(8F!mQRHF3T zn*J;`OGh(*qZ)oHm=>9wk-cv`{@SG)hsdrA6{GlkM=hktD6^ARa=V;QYi#Q@5X{I8 zJ#y3v;NvBE3+nLhv+jv#^>sF=-y3`eT$(l2KK^Ta!X?|wA{XLR6(CL(`9I~?&fdks z(C&vl%~i5ApJM_(2uyndZw~q$*XR&y2gWAkwz{(j_KNief(x|6$c%@&A8utFwefjs zM?HdNHplIT7wiv!9hvfyj^<1j1jgcQFGXcfS0Ln7TRC@L5UF?1fL``owG zDpfl7?4sWAE3uaxM~B8{3M_H$Jd4DByj7A9V+}_P&vXiMWw>s2)k72p8A=CrbQ~=o z5uL&HE25Z`&00iBdc2*hlxGJQ{8(aDn)Bc|4tshpGwin}!Hp(ndK1a|F^jh(k?S8v zm8TA#S_hYzYgZ-OZyLy$xQIcmPZ5hEBCdtb@}5fOA~IX_w77H0nwO@Dezb~*X7(-s zx2>ve9~K>DHroaDE8ary7XW1aBr+1zO?>g+z^Cu>he&dW%qz4_1>9DoNHSdb@?Jz} z?+d4`kGLI@W%TG)S_eI|(Zh6uTJBYh8fMOUI-TWvUkxT|@sYsOxvBRQ0V6Gl+pzGw zzVUNu{Fdj_Cvds8)vwrQ1}}oVWO2V^58Z0Mk6)0PkC~5GT5$9ciHH)qpE?;B>2cFK zi9M|q{*Lz9_CYaH2mi@it`xc6HvFT2@E*b(m@m9elQi4j?p+gODBL4?3IZ+8P=t+S{618UJK;T$CVWb&BD4=<`Uto*TH3i)l|S zaZynrj#~1HsCJlRq`9+MMeVT#71aP%Flpdukcr92GbGg6SBN+}eo$HJ30E%jvcZO; z=lotS(FNBHIkDCpZ{u^m>Bw` zdQ~>kvAJRe4{%TM&oQsEmlopPB%Td zMdgh@dX4{d+vs7}FC`#jV+@fzDx{ZT0M?hY1zX!O>Vs_!|JXuNPVE>ga#BWS$wOXtx<@uvC!2E z1Ba$SkUP{=z(Oqkq!?CS@b%)Vfo}qnH zq+8gih4|Wq1{NKzew+e?^N|*pu2VVRndQ(?)n-@OV__F<{r+LXmBGynr1#nv$(@cP zj*AA|*P&l2;v&6DmP72o$i_(;sbsP)uK82I zRl-gFCvH89f-8@T<@mJFknLI>P*qM2Lws7DnR$31JN$09GYt6>+|J9d71iN3xHUVb zlfEC5p4odkTdI7xvIN4VNuE*gq6byN%}s>W$i7i~)pZv}56F(OND9#Bz8o^Y6lD~L z#@_5E)`8Mp!MEA$g?=#Opcv=v9N<3gWjdDOn)3$p*bQ-NC(GVYx)C%Pv`70Zo_F{g zyT562Hb>B&auIkp_syW-8dhdk0C|ER%yLU?u#i#_cr$t0%Eb4ucGhuz;wtaKaIIy` zO1|_^_3-RZ`aXf8hm?|k{}kcR>H72fFCQkzN&P**-#1PF1pc@tLNxK0E!5wEzi%Y{ zf;L0E+i%-Szk~nY!~O*Y06d=l1pj|J+P}y7y|MW#QYF&=dx(FuIe(Axd(Y=rlzB+m zg1pM_9iZO>{O($R1<=6$8Q_ Date: Sat, 28 Feb 2026 21:33:15 -0800 Subject: [PATCH 029/381] Added a custom copyTo to cancel downloads as soon as they breach the size limit --- .../java/com/httpsms/HttpSmsApiService.kt | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt index c3ae8e13a..9581cedcb 100644 --- a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt +++ b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt @@ -13,6 +13,9 @@ import java.util.logging.Level import java.util.logging.Logger.getLogger import java.io.File import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { @@ -158,6 +161,28 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { return true } + fun InputStream.copyToWithLimit( + out: OutputStream, + limit: Long, + bufferSize: Int = DEFAULT_BUFFER_SIZE + ): Long { + var bytesCopied: Long = 0 + val buffer = ByteArray(bufferSize) + var bytes = read(buffer) + + while (bytes >= 0) { + bytesCopied += bytes + + if (bytesCopied > limit) { + throw IOException("Download aborted: File exceeded maximum allowed size of $limit bytes.") + } + + out.write(buffer, 0, bytes) + bytes = read(buffer) + } + return bytesCopied + } + // Downloads the attachment URL content locally fun downloadAttachment(context: Context, urlString: String, messageId: String, attachmentIndex: Int): File? { val request = Request.Builder().url(urlString).build() @@ -186,18 +211,13 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { val tempFile = File(mmsDir, "mms_${messageId}_$attachmentIndex") val inputStream = response.body?.byteStream() val outputStream = FileOutputStream(tempFile) - inputStream?.copyTo(outputStream) + + inputStream?.copyToWithLimit(outputStream, maxSizeBytes.toLong()) outputStream.close() inputStream?.close() response.close() - if (tempFile.length() > maxSizeBytes) { - tempFile.delete() - Timber.e("Downloaded file exceeded 1.5MB limit.") - return null - } - return tempFile } catch (e: Exception) { Timber.e(e, "Exception while downloading attachment") From 182d86006e32a24596251ad3f55a75042249268c Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Sat, 28 Feb 2026 21:34:17 -0800 Subject: [PATCH 030/381] Changed cache timeout to 15min --- api/pkg/validators/validator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pkg/validators/validator.go b/api/pkg/validators/validator.go index 850d1e76b..3cd85de2f 100644 --- a/api/pkg/validators/validator.go +++ b/api/pkg/validators/validator.go @@ -213,5 +213,5 @@ func validateAttachmentURL(ctx context.Context, c cache.Cache, attachmentURL str } func saveToCache(ctx context.Context, c cache.Cache, key string, value string) { - _ = c.Set(ctx, key, value, 24*time.Hour) + _ = c.Set(ctx, key, value, 15*time.Minute) } From c4f7e185582a39a8ef4f5283c8cf41e7ee4cb872 Mon Sep 17 00:00:00 2001 From: Jake Daynes Date: Sat, 28 Feb 2026 21:42:05 -0800 Subject: [PATCH 031/381] Centralised the content type check into message entities file --- api/pkg/entities/message.go | 17 +++++++++++++++++ api/pkg/handlers/discord_handler.go | 14 +------------- api/pkg/requests/bulk_message_request.go | 13 +------------ 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/api/pkg/entities/message.go b/api/pkg/entities/message.go index eb71f51b7..6f0f75e6a 100644 --- a/api/pkg/entities/message.go +++ b/api/pkg/entities/message.go @@ -1,6 +1,7 @@ package entities import ( + "strings" "time" "github.com/google/uuid" @@ -233,3 +234,19 @@ func (message *Message) updateOrderTimestamp(timestamp time.Time) { message.OrderTimestamp = timestamp } } + +func GetAttachmentContentType(url string) string { + // Since there's no easy way to set a type in the CSV, defaulting to octet-stream and then just checking the file extension in the URL + contentType := "application/octet-stream" + lowerURL := strings.ToLower(url) + if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { + contentType = "image/jpeg" + } else if strings.HasSuffix(lowerURL, ".png") { + contentType = "image/png" + } else if strings.HasSuffix(lowerURL, ".gif") { + contentType = "image/gif" + } else if strings.HasSuffix(lowerURL, ".mp4") { + contentType = "video/mp4" + } + return contentType +} diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go index 7be60f3b6..729c6ce6c 100644 --- a/api/pkg/handlers/discord_handler.go +++ b/api/pkg/handlers/discord_handler.go @@ -303,19 +303,7 @@ func (h *DiscordHandler) createRequest(payload map[string]any) requests.MessageS continue } - // Same as with bulk CSV attachments, can't easily ask for the MIME type so - // just inferring based on the file extension - contentType := "application/octet-stream" - lowerURL := strings.ToLower(cleanURL) - if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { - contentType = "image/jpeg" - } else if strings.HasSuffix(lowerURL, ".png") { - contentType = "image/png" - } else if strings.HasSuffix(lowerURL, ".gif") { - contentType = "image/gif" - } else if strings.HasSuffix(lowerURL, ".mp4") { - contentType = "video/mp4" - } + contentType := entities.GetAttachmentContentType(cleanURL) attachments = append(attachments, entities.MessageAttachment{ ContentType: contentType, diff --git a/api/pkg/requests/bulk_message_request.go b/api/pkg/requests/bulk_message_request.go index 0ab6c0247..a5aca7ce0 100644 --- a/api/pkg/requests/bulk_message_request.go +++ b/api/pkg/requests/bulk_message_request.go @@ -43,18 +43,7 @@ func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID continue } - // Since there's no easy way to set a type in the CSV, defaulting to octet-stream and then just checking the file extension in the URL - contentType := "application/octet-stream" - lowerURL := strings.ToLower(cleanURL) - if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") { - contentType = "image/jpeg" - } else if strings.HasSuffix(lowerURL, ".png") { - contentType = "image/png" - } else if strings.HasSuffix(lowerURL, ".gif") { - contentType = "image/gif" - } else if strings.HasSuffix(lowerURL, ".mp4") { - contentType = "video/mp4" - } + contentType := entities.GetAttachmentContentType(cleanURL) attachments = append(attachments, entities.MessageAttachment{ ContentType: contentType, From c4b4f1f5df58f1d9c57f27360cd8760085267272 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:35:56 +0000 Subject: [PATCH 032/381] fix(deps): bump github.com/xuri/excelize/v2 in /api Bumps [github.com/xuri/excelize/v2](https://github.com/xuri/excelize) from 2.10.0 to 2.10.1. - [Release notes](https://github.com/xuri/excelize/releases) - [Commits](https://github.com/xuri/excelize/compare/v2.10.0...v2.10.1) --- updated-dependencies: - dependency-name: github.com/xuri/excelize/v2 dependency-version: 2.10.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- api/go.mod | 2 +- api/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 2d1f8df20..2523c3bda 100644 --- a/api/go.mod +++ b/api/go.mod @@ -44,7 +44,7 @@ require ( github.com/thedevsaddam/govalidator v1.9.10 github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc github.com/uptrace/uptrace-go v1.40.0 - github.com/xuri/excelize/v2 v2.10.0 + github.com/xuri/excelize/v2 v2.10.1 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/metric v1.40.0 go.opentelemetry.io/otel/sdk v1.40.0 diff --git a/api/go.sum b/api/go.sum index 06c161bfc..a35facfe1 100644 --- a/api/go.sum +++ b/api/go.sum @@ -358,8 +358,8 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= -github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= From 78092ad9e8771a06db6174b77762171fccc8b40e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:13:24 +0000 Subject: [PATCH 033/381] fix(deps): bump google.golang.org/grpc from 1.79.1 to 1.79.3 in /api Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.1 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.79.1...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- api/go.mod | 2 +- api/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/go.mod b/api/go.mod index 2523c3bda..970afcbf0 100644 --- a/api/go.mod +++ b/api/go.mod @@ -198,7 +198,7 @@ require ( google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/api/go.sum b/api/go.sum index a35facfe1..a5e74ec79 100644 --- a/api/go.sum +++ b/api/go.sum @@ -553,8 +553,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1: google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= From c41a79983d4eb5095354e08cecacafbe2a5d3697 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 13:46:51 +0200 Subject: [PATCH 034/381] Fix gradle file --- android/build.gradle | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index e29386c31..8450dab4c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,8 +17,8 @@ buildscript { } plugins { - id 'com.android.application' version '8.9.2' apply false - id 'com.android.library' version '8.9.2' apply false + id 'com.android.application' version '8.13.2' apply false + id 'com.android.library' version '8.13.2' apply false id 'org.jetbrains.kotlin.android' version '1.6.21' apply false } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f40abbca7..0d14e6a1b 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 23 15:32:32 EEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From bded0b4a9b135051991c0e606fcd44be7b60192d Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 14:12:50 +0200 Subject: [PATCH 035/381] ADB upgrade --- android/app/build.gradle | 17 ++++++----------- android/build.gradle | 8 ++++---- android/gradle.properties | 10 ++++++++++ .../gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f49acb2bf..ae0e8d944 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,27 +2,22 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'com.google.gms.google-services' - id "io.sentry.android.gradle" version "4.3.1" + id "io.sentry.android.gradle" version "6.2.0" } -def getGitHash = { -> - def stdout = new ByteArrayOutputStream() - exec { - commandLine 'git', 'rev-parse', '--short', 'HEAD' - standardOutput = stdout - } - return stdout.toString().trim() -} +def gitHash = providers.exec { + commandLine 'git', 'rev-parse', '--short', 'HEAD' +}.standardOutput.asText.map { it.trim() }.getOrElse("unknown") android { - compileSdk 35 + compileSdk 36 defaultConfig { applicationId "com.httpsms" minSdk 28 targetSdk 35 versionCode 1 - versionName "${getGitHash()}" + versionName gitHash testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/build.gradle b/android/build.gradle index 8450dab4c..eeed1cc32 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { - kotlin_version = '2.1.0' + kotlin_version = '2.2.10' } repositories { // Check that you have the following line (if not, add it): @@ -17,9 +17,9 @@ buildscript { } plugins { - id 'com.android.application' version '8.13.2' apply false - id 'com.android.library' version '8.13.2' apply false - id 'org.jetbrains.kotlin.android' version '1.6.21' apply false + id 'com.android.application' version '9.1.0' apply false + id 'com.android.library' version '9.1.0' apply false + id 'org.jetbrains.kotlin.android' version '2.3.20' apply false } tasks.register('clean', Delete) { diff --git a/android/gradle.properties b/android/gradle.properties index cf0008ddc..8665be9a1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -22,3 +22,13 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.nonFinalResIds=false +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 0d14e6a1b..2721b96b6 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 23 15:32:32 EEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From c1dddc36c944d96744559046ee0c2f3b4cd8713f Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 14:35:00 +0200 Subject: [PATCH 036/381] Update api/pkg/validators/bulk_message_handler_validator.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/pkg/validators/bulk_message_handler_validator.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 137442d04..23b4d48d3 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -241,13 +241,10 @@ func (v *BulkMessageHandlerValidator) validateMessages(ctx context.Context, mess result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] has an invalid url format.", index+2, cleanURL)) } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { result.Add("document", fmt.Sprintf("Row [%d]: The attachment URL [%s] must use http or https.", index+2, cleanURL)) - } else { - if err := validateAttachmentURL(ctx, v.cache, cleanURL); err != nil { - result.Add("attachments", fmt.Sprintf("Row [%d]: The attachment URL [%s] failed validation: %s", index+2, cleanURL, err.Error())) - } } } } + } if _, err := phonenumbers.Parse(message.FromPhoneNumber, phonenumbers.UNKNOWN_REGION); err != nil { result.Add("document", fmt.Sprintf("Row [%d]: The FromPhoneNumber [%s] is not a valid E.164 phone number", index+2, message.FromPhoneNumber)) From 4df5ba425af133359394331f3af16ba45776a427 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 14:36:36 +0200 Subject: [PATCH 037/381] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/httpsms/FirebaseMessagingService.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt index 03c99bb47..294eb18e0 100644 --- a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt +++ b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt @@ -270,11 +270,27 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { handleFailed(applicationContext, message.id, e.message ?: "Internal error while building or sending MMS.") return Result.failure() } finally { + // Clean up any downloaded temporary files downloadedFiles.forEach { file -> if (file.exists()) { file.delete() } } + + // Also clean up the MMS PDU file to avoid cache buildup in cases where + // sendMultimediaMessage fails before the sent broadcast is delivered. + try { + val pduFile = java.io.File(applicationContext.cacheDir, "pdu_${message.id}.dat") + if (pduFile.exists()) { + val deleted = pduFile.delete() + if (!deleted) { + Timber.w("Failed to delete MMS PDU file for message ID [${message.id}] at [${pduFile.absolutePath}]") + } + } + } catch (cleanupException: Exception) { + // Best-effort cleanup; log but do not change the original result. + Timber.w(cleanupException, "Error while cleaning up MMS PDU file for message ID [${message.id}]") + } } } From 1d00212a801a56745b9db18a27a44145e97e6534 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 14:37:32 +0200 Subject: [PATCH 038/381] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/httpsms/HttpSmsApiService.kt | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt index 9581cedcb..b252d1fd4 100644 --- a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt +++ b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt @@ -188,37 +188,40 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { val request = Request.Builder().url(urlString).build() try { - val response = client.newCall(request).execute() - if (!response.isSuccessful) { - Timber.e("Failed to download attachment: ${response.code}") - response.close() - return null - } + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Timber.e("Failed to download attachment: ${response.code}") + return null + } - val maxSizeBytes = 1.5 * 1024 * 1024 // most (modern?) carriers have a 2MB limit, so targetting 1.5MB should be safe - val contentLength = response.body?.contentLength() ?: -1L - if (contentLength > maxSizeBytes) { - Timber.e("Attachment is too large ($contentLength bytes).") - response.close() - return null - } + val body = response.body + if (body == null) { + Timber.e("Failed to download attachment: response body is null") + return null + } - val mmsDir = File(context.cacheDir, "mms_attachments") - if (!mmsDir.exists()) { - mmsDir.mkdirs() - } + val maxSizeBytes = 1.5 * 1024 * 1024 // most (modern?) carriers have a 2MB limit, so targetting 1.5MB should be safe + val contentLength = body.contentLength() + if (contentLength > maxSizeBytes) { + Timber.e("Attachment is too large ($contentLength bytes).") + return null + } - val tempFile = File(mmsDir, "mms_${messageId}_$attachmentIndex") - val inputStream = response.body?.byteStream() - val outputStream = FileOutputStream(tempFile) - - inputStream?.copyToWithLimit(outputStream, maxSizeBytes.toLong()) - - outputStream.close() - inputStream?.close() - response.close() + val mmsDir = File(context.cacheDir, "mms_attachments") + if (!mmsDir.exists()) { + mmsDir.mkdirs() + } + + val tempFile = File(mmsDir, "mms_${messageId}_$attachmentIndex") + val inputStream = body.byteStream() + FileOutputStream(tempFile).use { outputStream -> + inputStream.use { input -> + input.copyToWithLimit(outputStream, maxSizeBytes.toLong()) + } + } - return tempFile + return tempFile + } } catch (e: Exception) { Timber.e(e, "Exception while downloading attachment") return null From 12d6d94f52cf66a2d19243196e48a7ef582cba47 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 14:44:23 +0200 Subject: [PATCH 039/381] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/pkg/validators/bulk_message_handler_validator.go | 9 ++++++++- web/pages/threads/_id/index.vue | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 23b4d48d3..d640c648c 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -226,7 +226,14 @@ func (v *BulkMessageHandlerValidator) validateMessages(ctx context.Context, mess if message.AttachmentURLs != "" { urls := strings.Split(message.AttachmentURLs, ",") - if len(urls) > 10 { + validAttachmentCount := 0 + for _, u := range urls { + if strings.TrimSpace(u) != "" { + validAttachmentCount++ + } + } + + if validAttachmentCount > 10 { result.Add("document", fmt.Sprintf("Row [%d]: You cannot attach more than 10 files per message.", index+2)) } diff --git a/web/pages/threads/_id/index.vue b/web/pages/threads/_id/index.vue index 21ff39c7f..00636a0f0 100644 --- a/web/pages/threads/_id/index.vue +++ b/web/pages/threads/_id/index.vue @@ -174,9 +174,9 @@ + v-if="message.attachments?.length" + shaped + > Message Attachments From 9827ebf2e5f6670011389487c91d76b4daa0bf34 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 22 Mar 2026 15:02:35 +0200 Subject: [PATCH 040/381] Fix ESLINT warnings --- .../bulk_message_handler_validator.go | 8 ++--- web/models/api.ts | 2 -- web/pages/messages/index.vue | 5 +-- web/pages/phone-api-keys/index.vue | 2 +- web/pages/settings/index.vue | 4 +-- web/pages/threads/_id/index.vue | 31 ++++++++++++------- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index d640c648c..3fa0c35c0 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -12,12 +12,12 @@ import ( "github.com/xuri/excelize/v2" + "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/dustin/go-humanize" "github.com/jszwec/csvutil" "github.com/nyaruka/phonenumbers" @@ -123,6 +123,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user result.Add("document", fmt.Sprintf("Cannot parse the uploaded excel file with name [%s].", header.Filename)) return nil, result } + defer excel.Close() rows, err := excel.GetRows(excel.GetSheetName(0)) if err != nil { @@ -212,14 +213,14 @@ func (v *BulkMessageHandlerValidator) parseCSV(ctxLogger telemetry.Logger, user var messages []*requests.BulkMessage if err := csvutil.Unmarshal(content, &messages); err != nil { ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot unmarshall contents [%s] into type [%T] for file [%s] and user [%s]", content, messages, header.Filename, user.ID))) - result.Add("document", fmt.Sprintf("Cannot read the conents of the uploaded file [%s].", header.Filename)) + result.Add("document", fmt.Sprintf("Cannot read the contents of the uploaded file [%s].", header.Filename)) return nil, result } return messages, url.Values{} } -func (v *BulkMessageHandlerValidator) validateMessages(ctx context.Context, messages []*requests.BulkMessage) url.Values { +func (v *BulkMessageHandlerValidator) validateMessages(_ context.Context, messages []*requests.BulkMessage) url.Values { result := url.Values{} for index, message := range messages { @@ -251,7 +252,6 @@ func (v *BulkMessageHandlerValidator) validateMessages(ctx context.Context, mess } } } - } if _, err := phonenumbers.Parse(message.FromPhoneNumber, phonenumbers.UNKNOWN_REGION); err != nil { result.Add("document", fmt.Sprintf("Row [%d]: The FromPhoneNumber [%s] is not a valid E.164 phone number", index+2, message.FromPhoneNumber)) diff --git a/web/models/api.ts b/web/models/api.ts index 029b5f01b..660e7493d 100644 --- a/web/models/api.ts +++ b/web/models/api.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ -/* tslint:disable */ // @ts-nocheck /* * --------------------------------------------------------------- diff --git a/web/pages/messages/index.vue b/web/pages/messages/index.vue index 78a465332..eec128e80 100644 --- a/web/pages/messages/index.vue +++ b/web/pages/messages/index.vue @@ -106,7 +106,8 @@ export default { let contentType = 'application/octet-stream' const lowerUrl = cleanUrl.toLowerCase() - if (lowerUrl.endsWith('.jpg') || lowerUrl.endsWith('.jpeg')) contentType = 'image/jpeg' + if (lowerUrl.endsWith('.jpg') || lowerUrl.endsWith('.jpeg')) + contentType = 'image/jpeg' else if (lowerUrl.endsWith('.png')) contentType = 'image/png' else if (lowerUrl.endsWith('.gif')) contentType = 'image/gif' else if (lowerUrl.endsWith('.mp4')) contentType = 'video/mp4' @@ -120,7 +121,7 @@ export default { to: this.formPhoneNumber, from: this.$store.getters.getOwner, content: this.formContent, - attachments: attachments, + attachments, sim: this.simSelected.code, }) .then(() => { diff --git a/web/pages/phone-api-keys/index.vue b/web/pages/phone-api-keys/index.vue index dc19e75c4..e83a2d707 100644 --- a/web/pages/phone-api-keys/index.vue +++ b/web/pages/phone-api-keys/index.vue @@ -24,9 +24,9 @@

{ + toCanvas(canvas, text, { errorCorrectionLevel: 'H' }, (err) => { if (err) { this.$store.dispatch('addNotification', { message: 'Failed to generate API key QR code', diff --git a/web/pages/threads/_id/index.vue b/web/pages/threads/_id/index.vue index 00636a0f0..4ea6504d8 100644 --- a/web/pages/threads/_id/index.vue +++ b/web/pages/threads/_id/index.vue @@ -173,11 +173,8 @@ > - - Message Attachments + + Message Attachments - + + + + diff --git a/web/store/index.ts b/web/store/index.ts index afeeaebd6..3ced97f05 100644 --- a/web/store/index.ts +++ b/web/store/index.ts @@ -377,6 +377,7 @@ export const actions = { missed_call_auto_reply: phone.missed_call_auto_reply, max_send_attempts: parseInt(phone.max_send_attempts.toString()), messages_per_minute: parseInt(phone.messages_per_minute.toString()), + schedule_id: (phone as any).schedule_id || null, }) .catch((error: AxiosError) => { context.dispatch('handleAxiosError', error) From a988dea84454620c7a60f619becb46b8b05a9b5a Mon Sep 17 00:00:00 2001 From: giresse19 Date: Mon, 30 Mar 2026 21:10:37 +0300 Subject: [PATCH 056/381] feat: Refactor send schedules based on review feedback --- api/pkg/di/container.go | 4 ++++ web/pages/settings/index.vue | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index c4e91bcce..e8c015d8f 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -362,6 +362,10 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK ( container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.User{}))) } + if err = db.AutoMigrate(&entities.SendSchedule{}); err != nil { + container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.SendSchedule{}))) + } + if err = db.AutoMigrate(&entities.Phone{}); err != nil { container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Phone{}))) } diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index b873bfe21..87294e0a5 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -949,7 +949,11 @@ export default Vue.extend({ return this.$store.getters.getUser.subscription_renews_at != null }, timezones() { - return Intl.supportedValuesOf('timeZone') + try { + return Intl.supportedValuesOf('timeZone') + } catch { + return [] + } }, phoneNumbers() { return this.$store.getters.getPhones.map((phone) => { From 6cfa208804116e219f38831426f8abde514f3175 Mon Sep 17 00:00:00 2001 From: giresse19 Date: Mon, 30 Mar 2026 21:19:01 +0300 Subject: [PATCH 057/381] feat: Refactor send schedules based on review feedback --- web/pages/settings/index.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index 87294e0a5..c6ef94747 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -853,7 +853,6 @@ import { mdiSquareEditOutline, mdiQrcode, } from '@mdi/js' -import QRCode from 'qrcode' import axios from '~/plugins/axios' import { toCanvas } from 'qrcode' import { ErrorMessages } from '~/plugins/errors' From 0d7ccf96868c3c14314ed4e29ed1ace462c6762e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:04:32 +0000 Subject: [PATCH 058/381] fix(deps): bump pnpm/action-setup from 4 to 5 Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 5. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v4...v5) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26d01123d..27bcd7377 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout 🛎 uses: actions/checkout@master - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 name: Install pnpm with: version: 9 From fd45ae13955124abf864be6513eccfc0c7860986 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Thu, 2 Apr 2026 20:04:54 +0300 Subject: [PATCH 059/381] Remove optional attachment URL in templates --- web/static/templates/httpsms-bulk.csv | 6 +++--- web/static/templates/httpsms-bulk.xlsx | Bin 11244 -> 10788 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/static/templates/httpsms-bulk.csv b/web/static/templates/httpsms-bulk.csv index c85d6091b..66411bc7c 100644 --- a/web/static/templates/httpsms-bulk.csv +++ b/web/static/templates/httpsms-bulk.csv @@ -1,3 +1,3 @@ -FromPhoneNumber,ToPhoneNumber,Content,SendTime(optional),AttachmentURLs(optional) -18005550199,18005550100,This is a sample text message1,,https://placehold.co/600x400/png -18005550199,18005550100,This is a sample text message2,2023-11-11T02:10:01, +FromPhoneNumber,ToPhoneNumber,Content,SendTime(optional) +18005550199,18005550100,This is a sample text message1, +18005550199,18005550100,This is a sample text message2,2023-11-11T02:10:01 diff --git a/web/static/templates/httpsms-bulk.xlsx b/web/static/templates/httpsms-bulk.xlsx index 268a4fafcc0d274cc00ee3487990c6d7be840a7d..ca23f441d18793d71460eaf3263bf0cb13afd1de 100644 GIT binary patch delta 3363 zcmZ{nXH?Tk*T6%Spi)9WI;$pt5Q@SYV1a@#!jZ*DvDVa`40-udoQO;$i{zS!e= zJ3+`08@V~2hxWI4Am8xv3?2#q0>>+}d*j@Tw@91X3%(cSoOCnXSD~m7oCZIdkXu!I z)Ab7L4#;Q(dZgFMT+-cnCGDG-R|sEbG4?e4diAgPPtNiEV-mSd_VFQ5`nxp2xO#yH zOo4cBb-vlG$AfT}z=J+-ii8$M&p6hdRUI*%vcSkwRL(b+gM5w_1=}1}Hi29EsTLph zG7G-_d*c<~sijuaW0ErrMNK&`CqciOLCZ+)Pt%zjk2o+vu&zKWNrzwIcgmmfC_!vN zn}KToj*WA(fiq1!@s9$NZ-!gWPcU@LY_t9P*)_;n2ss0}%r#RXqP~&nqG) zw>vlk?}AI^QbpeI9KQti`l@@KuPn(5yQ=dc5OE{>aMG7keanRlukfChF-sKBg>IKT zwTdjaIuh=X#UMCS*W1|^I_4MTXpv$$qY!S9BkUmXbmYrqzxPEEm|AHh*x_KW40-Wd3O}QL#1n)G~iwh<0L$FZL7{ z^`MULEpUHLM2GsSyk3Xg`?W`lO5-&W5P0o-^i_S+CZ(12rl+;dHxi<8P2y1jX!uP1 zl`tj8%Hb*Q#XAl*OAF@>}x5?ZfjnhaPT8&LbaW=frx!;BHPjG{S3_liTb^R7$tBs0fmtY5Zbv#9jxi_ULG2=1QpLiHNGy$o^|l{&}{zunZ5v-x&(xzZ(pNnK`*up{cdy~eIL zw&>xAmkgS`0|F&B#l63`6n8Sln|herJ&fHp`Zg&TeX+j9U5>O((?HhmbYX_cllIHE zYIj!KmLkcc)KRyYB@!lVi&?pVotfL-?I@U>^pC{XyM&FQ;@;|^m{*aU4kq@NXZQFL zT5a)GSj*^gJx!?O_1Xwa4{NX+a=O=IZ?1|&!RN}IC|4y0={i?6i*0H7s?82 zU}|SUecbdXHn9q$Hk`(Zw*>v}-^dkY1xCUWyLMl1QA&0m86fZ|HEIiGR!@JUg@%lr^CH}2F1|p22pO2jA0i`f_clii zhj|-rSS@ZmxkXCIUtfEA*)xXN*h_Eln0Xtukd=ikQ5&*Pk=dXf3SB1b8273Q8|+L{ zZ$uD6Xc22q1~1UJ=L2^ZFB)=_ohK$Mllv}DcX-Z?dlO3vve?3rV`IKudRTplMpfkI zLX0A9mO4*-ouXw$7FJ`=QzfRe)LJ-=XLm2*km!0dg8yopFV6QbvSb0j-t3A#kAZ}q z5OzbS{Zkgv{^O~{ReUSk4?kaZ`om^Qg?0XEclKcLvPXMU!a`PCj#PC1wCq9F?DVc5 zzAAqGD`h?SV-;g^SdHRVR1xOrKn_;>rvl|Oo`-LOrHy7ONppeF?gfJ z{sQ|d-4idqb(OLnFT88Da#k%yGRBUIl{$`%nvSju;3Pg3)}nJbS=kzOpA+@y1BZo#^2@`6L#7MopXr)c47qPo%TyJx%6?{?w;pa&_PUhpT=GMn%N ziA-WP;olI;RLv&*0QRT)Lwm@?XA}Mn@yu)4g#X8HF(3p@L7G%`f&Bw|q$*$bzk#0= zbe{chP<%ggAl1_9yerm+w+*5tnJM|Mx*Zx1@6<%l!xoBTM4AU+@;BoQ1yPS5ihqLf zpY!w^amFV8%~jrRluIX@$-ZsAVk1+!m`2rofF`VWru;aG=l+0zDU7 zW^Q{LOi?tRhLz^>76XM55U5Qo#oX2qOi?oCf|nNZ!qW@mAyB(m6$@JvFh$w;D7>_Y zw>Z5p69RRJ#aP%{gDEH0cV@Ta%%W-KMUVl<*k=~DPT-F!#$Isca^8%L!WzhcbL@FE1l&kB%PsAmtl zK9jbb7Cf7O(X5HKaG#6z)D1!$r$ z5u(1RY|C5x%Rhe2EbUC&&;hu_MK2ZL}+ zg5zIaG)Ug(ubJhYX@jn>q%BFhPB-ku^PS>KoK|l-h5Lx-yTz3_Pd7N-2o?V)iLf_bSQfCI)!tod1b z8-uYtq&qS*x-?zg9J9L=+T7?$BL(4r$UP66G7{ZGC>K@;w5eUzsb4C$%!@lQN>l&LV$CO0{fGX!(_Q_4s0@s+%|n$hQ6-wV~pX=!D%-%?{mf0-iX2u%glErY**U|%Z(a>gT1zbC)kdD71SI7T5jUjahMH=w*R%&p z52TQKm~I_v^PR2tv?M%JrvS4r`Jo=qy#z9SzB7fO>u%u-%7KXvi1w0EAL5qGD7U&m z&IP`^V623s{GAth_ZRL%qt1Mb98Lzco~$V}I*_uC=$f1WSu?Q*Oy=8PTwDlvyC}Jt zuC`^IJ9b+`$#?j=n@jRZAGR!^d|9E(!Qn+hrB0#WLdhk=a-R$t@XTL964pHdf9`yl4(&m&S-zt`sLC!8=2#quF)xu`g2PBp2W*uwJ(wHpW5Yp7iub?qaHrRo zdBk+IB_$NCmvZ4}2 zR^|@^Y!C!tWRN7!qSNZ~!rhKl8wK>G7!UZWz6VoFauD`?063;=>tu_iY?w_Iws#Wv#G|=oNHc zCD1=ajGCzm3f(%S*Wz5B??3Bz`9#79V4gH&yCg@JGi;S6ho>9m$;)h&5arV>(SfD; zj_xywNDG9Oxb1APxwiQ&LY29My6hwRR|z)_W1$FlBcK8=AhuLn|Ac$$i=b^sqo$BR z?<#vCBX1L?K{b2{Tqiq=iEVVux0;IcCbNXWPB<_nuNU(rnP$-L1&DtbdbkRR^U32G zt{WrW6Rl;3MTd06KQdm?kf)^+8nx3u!uzG3Ox@Uk;_*u7N_iIZeQ%eA<;@dyce%Y6 zB3f2??FIR6&1U!4VjZq7=uY~v*_LVcrO-y4;~1&d=awSP4!yPxW1W6n7Lc(7#Veo?DbFdfauwYp*fO)jxdq2XMT*F@wlTX8@lu zau&UUojq`5ax(SD6BY=BhY9Rv76kfzPZ~HM8rgts2^_A9X(%T`Tcg{9xf3EGT8hmY z6u29Eyzc$CF*VNgjdOumy~f-i4VYqwN_jb;ritnOsd<-Fo`kdW4*J=?)~r@(*LI!w zlhWS5y-jQ01@?AOnyXu0WoX9~8KhD#5!^x&sk^McduwP%V8}~zPSzSA(!6etVuR$X z`xbql#yrJj@(_s=3*D3Mf$CKMs(weA4NCX?&KqhwrbMmcLqK2%Cr>mspu`^Cv1q3} zgh!nutgNif%+61)j(5*uf!-!eAZZZQT3ul~JjZWWp+b8-nHxXAD&UJ(rrJ}DO)9UV zb2q976;)CklKJWTd&CwwR8=H3(nNnu%*?f7ws|t?Giii@ft0m?5)G;khOj~-W>37S zaKmft-ahvz_HwkS@Xd4-9wo)443Jrsg-i2@ETPY6>RtQIR$#hXy$g9 zP67^tbeddpWL@*v%#YEk>h|GEjjO1un{qk*m_WQd-33tIlQ7=sh#9J^dh(T?Ym*X$ zc6dGia&fP++&+21#!|^iLsevz>b^2+wlp=9(a<%$Q2HsYb7BTh^H>|7U0!zGqnzub z(y5rU4oND?*tyZ4t*m6)j3R|jN>2Xp<)r{}L(@l=UZ^pEHYQKnklwPj?v7T`zu(>4 z=-#F^0QhqM^0n1*pK21e(#=jL(w;Acwu~uJO+KJ5GeNw}5`C+jl(bDSy>9cf?zI1r zAIq)Oj|)4qF*s_;cK*?glEqn*(-j|-k^%gC8^2Q}WuGUu+}k|})Sjd-i=G*)vlTZd zcM>UExb}~muicX4r%FH1>{dBCR^BX#$J(X=!1dTQr&!AUokZHAm0I~#QrXF^dB5tr zE7*!HH3C0Cu^XaFvv#*#`d}U zGpkkg)#^xgyu;Y%DbcY!qe`5kFs-~E)nWAGGyD=IRp1gn7+TrX#CnWR4}aW$I{G3Y z;l|#$**;x;<>oI!;Oe+ecK^7YboGg0hetE31-kV0%?!@!woR5}wDDiKqOPJ}>MyW% z+uH=0-Adh^{mIXXPkga0B!JDh2FT9H_&jc;(CwI;U3ZnYWt&pYi|oDpY6wg2az2O` zzQgN0lKCL-`&i_tjW_w)4_0J5vJ_ZA%dI*Y(lqJ9tKBMz>85L$ca3PuZi%y~^BLQg znC!h5HaVJZFJ|evNTj~9SGXUR8@83X44q#mQTZGEs z8AzX?4-V?dNy~*h0ii-inT?%nuN|$^u8wq^=ILZM84Id zuz{Hn<(M~v?MO|BmA;W*s`G+ z8pd`+TI;?jqQH2kaoYF&+NB<_ll|1$`noTYrFEfuW9TQMIR*8pn%g!mk8VnvKM`Gi zD*or5Q=qF5pVbHt+2Bx!Z&FRo0vX^^;5@rLQwC%4NTe*dz%I@>>nH_k!H++OA7yI| z$Wp!=^4BUecu;_Y|M1LGe6ZPZY7FSaf#7=-v)mB5H8y^CE3|4=AAL6qsnNqDFc%%O z-VnJ1%5ivtd>jg(Hm9)xB?5R*dgX~IvA|U!{{7A$Ha_pRE#x(_c4=12Jc7!pEy`Ic zmek0LGJRirmbla%RO0y92k#@^aPTzRkR%^mG(%mfzn)u0|)}-5Zr3u zPo>rkBHh+!I4>`#sI_}*&$TMJJA=Px5y>yRsH9X@W=c`18kpN45FYncELe2m})Bh4jDSR1uzk?FQH|7v0YerZmiaxf04WuY?-%F6ODS+bkWQ8$_)v~ zj5{^ITZ&bk7y*^k`=%q)V{T@z?OA)q-P-lcUM~CY z`$s>ZCU>EraTSFT~wp6t-nkV>!Fh-to91&|oa=ou1e+ zf>!8-!a3oPV*;6Fk=NNuP5^#lXVM->S@)T21dXeTQMN6Pow4cU%Hh@a+`8CulTkK< zCFvRV`efm&p4Krp#dR;WmP_wS3wIT-m7i&-@jx6ZW4K*DiQfVUQ|Ijli)i+--vz{7 zr!5Sgec@ARaM^M9>_`F&jw_RD3I`=iG$cgNZlyGzuHYXF^tNphiKQ?hd>!R*f6jx@ zum=F4=&A5I2H??Y$^7S+4|Z|H#Udof-1`UWh4D>_Z)qDJJzx{Ji`~};`fn=?XKQRh zrrvcbNEV(FO(n5glqp~xkrdAto1coWkvq-as0W|(l|@SWNGP%JCl`!_K~r>}ufG`J z4HUACDwyI!OcqUlH6P^7w0(%`b#C1}>B#`9r8pf7g#p~hH6WI%?42-)&4}W~TmgnK zmuam~7Ji;9q19xDq-8qG8J2_or6ZSa`z=-`J+6~;)j8kzaEp1j5F5;k zGI^&F(7>P&iN{XXNm`TQ1bsHzYdt8@t~IKbqt$+*o5y2wDEjcAZ!Wb+56dBH-peeZ z^9k=l5@7H`v4iCDG}rZ_W)T$QKY!{@AwvKAu$+moX1E9LAuceOgPIb8C;ztv_pS7= z3k0ze;{V5If6YQ51HykA_WyI8nGwSQnf>l&f*jtj6@vVJTZnzrF7F5Xe^%Qct?c{Q r)z`($&-a!;(#6-$ZQr?IhlJDt3o&q4f|rFE!VKv=0D&Bo_+9o7{7Y30 From 9de6da112017596a2b89e18a01d78a1ca018e7ee Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 3 Apr 2026 12:12:41 +0300 Subject: [PATCH 060/381] Update deps --- api/go.mod | 153 +++++++++++----------- api/go.sum | 377 +++++++++++++++++++++++------------------------------ 2 files changed, 243 insertions(+), 287 deletions(-) diff --git a/api/go.mod b/api/go.mod index 970afcbf0..94ce01462 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,11 +3,11 @@ module github.com/NdoleStudio/httpsms go 1.25.0 require ( - cloud.google.com/go/cloudtasks v1.13.7 + cloud.google.com/go/cloudtasks v1.14.0 firebase.google.com/go v3.13.0+incompatible github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.31.0 - github.com/NdoleStudio/go-otelroundtripper v0.0.13 + github.com/NdoleStudio/go-otelroundtripper v0.0.14 github.com/NdoleStudio/lemonsqueezy-go v1.3.1 github.com/NdoleStudio/plunk-go v0.0.2 github.com/avast/retry-go/v5 v5.0.0 @@ -30,27 +30,27 @@ require ( github.com/joho/godotenv v1.5.1 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jszwec/csvutil v1.10.0 - github.com/lib/pq v1.11.2 - github.com/nyaruka/phonenumbers v1.6.10 + github.com/lib/pq v1.12.2 + github.com/nyaruka/phonenumbers v1.7.1 github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/pusher/pusher-http-go/v5 v5.1.1 github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 github.com/redis/go-redis/v9 v9.18.0 - github.com/rs/zerolog v1.34.0 + github.com/rs/zerolog v1.35.0 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc - github.com/uptrace/uptrace-go v1.40.0 + github.com/uptrace/uptrace-go v1.41.1 github.com/xuri/excelize/v2 v2.10.1 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/sdk/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 - google.golang.org/api v0.269.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + google.golang.org/api v0.274.0 google.golang.org/protobuf v1.36.11 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -64,90 +64,91 @@ require ( github.com/inbucket/html2text v1.0.0 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect - github.com/olekukonko/ll v0.1.6 // indirect + github.com/olekukonko/ll v0.1.8 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/yuin/goldmark v1.7.16 // indirect + github.com/yuin/goldmark v1.8.2 // indirect ) require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.19.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/firestore v1.21.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.8.0 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect - cloud.google.com/go/storage v1.60.0 // indirect - cloud.google.com/go/trace v1.11.7 // indirect + cloud.google.com/go/iam v1.7.0 // indirect + cloud.google.com/go/longrunning v0.9.0 // indirect + cloud.google.com/go/monitoring v1.25.0 // indirect + cloud.google.com/go/storage v1.61.3 // indirect + cloud.google.com/go/trace v1.12.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/ClickHouse/ch-go v0.71.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.43.0 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/PuerkitoBio/goquery v1.11.0 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/PuerkitoBio/goquery v1.12.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/jsonreference v0.21.4 // indirect - github.com/go-openapi/spec v0.22.3 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/pgx/v5 v5.9.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.34 // indirect + github.com/mattn/go-runewidth v0.0.22 // indirect + github.com/mattn/go-sqlite3 v1.14.39 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/olekukonko/tablewriter v1.1.3 // indirect - github.com/paulmach/orb v0.12.0 // indirect - github.com/pierrec/lz4/v4 v4.1.25 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect + github.com/paulmach/orb v0.13.0 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect @@ -162,43 +163,43 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect github.com/vanng822/css v1.0.1 // indirect - github.com/vanng822/go-premailer v1.31.0 // indirect + github.com/vanng822/go-premailer v1.33.0 // indirect github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib v1.40.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 // indirect - go.opentelemetry.io/contrib/processors/minsev v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect - go.opentelemetry.io/otel/log v0.16.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/contrib v1.42.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.67.0 // indirect + go.opentelemetry.io/contrib/processors/minsev v0.15.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/api/go.sum b/api/go.sum index a5e74ec79..f55f2c017 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,28 +4,28 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ= +cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/cloudtasks v1.13.7 h1:H2v8GEolNtMFfYzUpZBaZbydqU7drpyo99GtAgA+m4I= -cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4= +cloud.google.com/go/cloudtasks v1.14.0 h1:l+9VVqB6Bbpn1NhYBwn9TMs5Yu7jU0bSfd9mrRilt48= +cloud.google.com/go/cloudtasks v1.14.0/go.mod h1:mFzsLKuM4gzzmlbu1363510Fjm5ZJR+8mH1C2w5roJo= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM= cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= -cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4= +cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc= +cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= +cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= +cloud.google.com/go/trace v1.12.0 h1:XvWHYfr9q88cX4pZyou6qCcSagnuASyUq2ej1dB6NzQ= +cloud.google.com/go/trace v1.12.0/go.mod h1:TOYfyeoyCGsSH0ifXD6Aius24uQI9xV3RyvOdljFIyg= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= @@ -34,8 +34,8 @@ firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVA firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= -github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= -github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= @@ -54,16 +54,16 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/NdoleStudio/go-otelroundtripper v0.0.13 h1:fDgdxcNJov4LTrMhXqJnF/E3jO4HJVczj90wkxh5PSc= -github.com/NdoleStudio/go-otelroundtripper v0.0.13/go.mod h1:UIUQ22ErFoBUyLuPDrVNRRKmBHBTfzQO9GF1ztqDvqo= +github.com/NdoleStudio/go-otelroundtripper v0.0.14 h1:t/VoW2772wTDQnjdECxxWbtZtbnpJyuRSKxRC/hHfTg= +github.com/NdoleStudio/go-otelroundtripper v0.0.14/go.mod h1:ObQjHo1D/daXeESbFIi0UXJN0yJu4zQ7mMeSKvm4a1I= github.com/NdoleStudio/lemonsqueezy-go v1.3.1 h1:lMUVgdAx2onbOUJIVPR05xAANYuCMXBRaGWpAdA4LiM= github.com/NdoleStudio/lemonsqueezy-go v1.3.1/go.mod h1:xKRsRX1jSI6mLrVXyWh2sF/1isxTioZrSjWy6HpA3xQ= github.com/NdoleStudio/plunk-go v0.0.2 h1:afPW7MHK4Z3rsybpJBnmTmxKCLKF1M7sPI+BNGPf35A= github.com/NdoleStudio/plunk-go v0.0.2/go.mod h1:pqG3zKhpn/A2bL1K+WsWzvfTpOeSkYgXhNk5H65uEc8= -github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= -github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -80,8 +80,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= @@ -92,7 +92,6 @@ github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -111,10 +110,10 @@ github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNf github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -125,43 +124,44 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-hermes/hermes/v2 v2.6.2 h1:RuGQlICVtIHixfxtYwN7hAoqGyGxr+D3kE42oE6emcw= github.com/go-hermes/hermes/v2 v2.6.2/go.mod h1:RLVNk31/1KqF35vK3mAaQVuJvMH+K5//6OTGJk+j/80= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= -github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= -github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/contrib/otelfiber v1.0.10 h1:Bu28Pi4pfYmGfIc/9+sNaBbFwTHGY/zpSIK5jBxuRtM= github.com/gofiber/contrib/otelfiber v1.0.10/go.mod h1:jN6AvS1HolDHTQHFURsV+7jSX96FpXYeKH6nmkq8AIw= github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= @@ -170,15 +170,12 @@ github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -190,10 +187,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= @@ -204,8 +201,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hirosassa/zerodriver v0.1.4 h1:8bzamKUOHHq03aEk12qi/lnji2dM+IhFOe+RpKpIZFM= github.com/hirosassa/zerodriver v0.1.4/go.mod h1:hHOOAQvVGwBV1iVVYujM6vwOBBqQcBIFpJxCD9mJU7Y= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -216,8 +213,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaswdr/faker/v2 v2.9.1 h1:J0Rjqb2/FquZnoZplzkGVL5LmhNkeIpvsSMoJKzn+8E= @@ -234,33 +231,24 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI= github.com/jszwec/csvutil v1.10.0/go.mod h1:/E4ONrmGkwmWsk9ae9jpXnv9QT8pLHEPcCirMFhxG9I= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/lib/pq v1.12.2 h1:ajJNv84limnK3aPbDIhLtcjrUbqAw/5XNdkuI6KNe/Q= +github.com/lib/pq v1.12.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk= +github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.39 h1:sIwSjlJGOaRJjw44/HXaeTblZMjseqr6OOio1tz/+JI= +github.com/mattn/go-sqlite3 v1.14.39/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -270,26 +258,24 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/nyaruka/phonenumbers v1.6.10 h1:kGTxTzd320dUamRB/MPeZSIwKNLn4vHlysOt5Cp8uoU= -github.com/nyaruka/phonenumbers v1.6.10/go.mod h1:IUu45lj2bSeYXQuxDyyuzOrdV10tyRa1YSsfH8EKN5c= +github.com/nyaruka/phonenumbers v1.7.1 h1:k8FHBMLegwW2tEIhsurC5YJk5Dix++H1k6liu1LUruY= +github.com/nyaruka/phonenumbers v1.7.1/go.mod h1:fsKPJ70O9JetEA4ggnJadYTFWwtGPvu/lETTXNXq6Cs= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= -github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= -github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= -github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 h1:nRlQD0u1871kaznCnn1EvYiMbum36v7hw1DLPEjds4o= github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177/go.mod h1:ao5zGxj8Z4x60IOVYZUbDSmt3R8Ddo080vEgPosHpak= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= -github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= -github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= -github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= +github.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -311,9 +297,8 @@ github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93 github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= +github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -328,7 +313,6 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -338,24 +322,20 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOjzTeQRiMCvU= github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc h1:lzi/5fg2EfinRlh3v//YyIhnc4tY7BTqazQGwb1ar+0= github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= -github.com/uptrace/uptrace-go v1.40.0 h1:fMva36FZ/eujU60hq+ke9HdYGkXP5jJXUTNeEuWDI+I= -github.com/uptrace/uptrace-go v1.40.0/go.mod h1:HJhggr8UMkJ+keR8B9o4KsF7kxT8lKH7Ra8X2DRwqdc= +github.com/uptrace/uptrace-go v1.41.1 h1:EtWkkdOQqtuJMZyzeU0zT5VH6ppVY12yOouQK3VRccw= +github.com/uptrace/uptrace-go v1.41.1/go.mod h1:gdn1eRLG3KCtTyiw+L8tG+tb/wnpiyIfLfTH2qh/5Mw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= -github.com/vanng822/go-premailer v1.31.0 h1:r1a1WH2I5NnGMhrmjVZyYhY0ThvaamKBkS2UuM91Fuo= -github.com/vanng822/go-premailer v1.31.0/go.mod h1:hzI26/YvzUADrxqifxGLJvNvn3tWBU6VMHRvxsskpuo= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/vanng822/go-premailer v1.33.0 h1:nglIpKn/7e3kIAwYByiH5xpauFur7RwAucqyZ59hcic= +github.com/vanng822/go-premailer v1.33.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= @@ -364,63 +344,59 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib v1.40.0 h1:Vv1qG9EIHpJWl2EFxOlhv0WgGNYQD9s0U/z3xkEonl8= -go.opentelemetry.io/contrib v1.40.0/go.mod h1:8z64gUE9jZgMGFCiGyF7NZnN5N0xaVaxdnV2DXBmTkE= -go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/rxAWVlHsHIZ3fT2sA= -go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= -go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0/go.mod h1:Z1pjGxUL3nJ/IbDDfL6rBD0Xbz7ZOViRqrIUg4l1CYE= -go.opentelemetry.io/contrib/processors/minsev v0.13.0 h1:pADh6ro5deXRfNmry136khTZYWVXn9NKZR5nZuEXtXw= -go.opentelemetry.io/contrib/processors/minsev v0.13.0/go.mod h1:MC0s+ldbPprTztVZQ/pecYqPSxwfjQkdnCeC3u6uGQU= +go.opentelemetry.io/contrib v1.42.0 h1:845qj52z2T/bLInfZmG8AdbTO7delSd6eGVVHcAikzw= +go.opentelemetry.io/contrib v1.42.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/runtime v0.67.0 h1:fM78cKITJ2r08cl+nw5i+hI9zWAu3iak8o1Os/ca2Ck= +go.opentelemetry.io/contrib/instrumentation/runtime v0.67.0/go.mod h1:ybmlzIqGcQzwt5lAfi8TpSnHo/CI3yv1Czodmm+OJa8= +go.opentelemetry.io/contrib/processors/minsev v0.15.0 h1:82auGK0+tBbWa3Zy8RoLegy6OL1OULFk50W4eO2rSXE= +go.opentelemetry.io/contrib/processors/minsev v0.15.0/go.mod h1:+mJGjwRqiPNYDU1hehhHeO6On5DBqSX8JXOqBnawT20= go.opentelemetry.io/contrib/propagators/b3 v1.19.0 h1:ulz44cpm6V5oAeg5Aw9HyqGFMS6XM7untlMEhD7YzzA= go.opentelemetry.io/contrib/propagators/b3 v1.19.0/go.mod h1:OzCmE2IVS+asTI+odXQstRGVfXQ4bXv9nMBRK0nNyqQ= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= -go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= -go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/oteltest v1.0.0-RC3 h1:MjaeegZTaX0Bv9uB9CrdVjOFM/8slRjReoWoV9xDCpY= go.opentelemetry.io/otel/oteltest v1.0.0-RC3/go.mod h1:xpzajI9JBRr7gX63nO6kAmImmYIAtuQblZ36Z+LfCjE= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= -go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -432,36 +408,28 @@ go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -469,31 +437,25 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -501,8 +463,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -514,7 +476,6 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -523,45 +484,39 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= -google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA= +google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9 h1:MzLVemxGdOBt2uziz9LnuYRQQFw1FDV0s0af4GVYE1A= -google.golang.org/genproto v0.0.0-20260217200457-a2cb2272a1e9/go.mod h1:9mSgs6f8tLwHSr6EzFWG+naa04gb1Zpt4IumYKsRDs0= -google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= -google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9 h1:w8JYjr7zHemS95YA5FFwk+fUv5tdQU4I8twN9bFdxVU= +google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:YCEC8W7HTtK7iBv+pI7g7hGAi7qdGB6bQXw3BIYAusM= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= From 37dd4c00c7be622dbdf6c2bb1383f5c450f38d54 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 6 Apr 2026 20:38:20 +0300 Subject: [PATCH 061/381] Add examples for attachments in swagger doc --- api/pkg/entities/message.go | 2 +- api/pkg/requests/message_send_request.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/pkg/entities/message.go b/api/pkg/entities/message.go index b7d423fb8..52a9a2215 100644 --- a/api/pkg/entities/message.go +++ b/api/pkg/entities/message.go @@ -90,7 +90,7 @@ type Message struct { UserID UserID `json:"user_id" gorm:"index:idx_messages__user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` Contact string `json:"contact" example:"+18005550100"` Content string `json:"content" example:"This is a sample text message"` - Attachments pq.StringArray `json:"attachments" gorm:"type:text[]" swaggertype:"array,string"` + Attachments pq.StringArray `json:"attachments" gorm:"type:text[]" swaggertype:"array,string" example:"https://example.com/image.jpg,https://example.com/video.mp4"` Encrypted bool `json:"encrypted" example:"false" gorm:"default:false"` Type MessageType `json:"type" example:"mobile-terminated"` Status MessageStatus `json:"status" example:"pending"` diff --git a/api/pkg/requests/message_send_request.go b/api/pkg/requests/message_send_request.go index bebf6f487..727cc12e5 100644 --- a/api/pkg/requests/message_send_request.go +++ b/api/pkg/requests/message_send_request.go @@ -19,7 +19,7 @@ type MessageSend struct { Content string `json:"content" example:"This is a sample text message"` // Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS - Attachments []string `json:"attachments" validate:"optional"` + Attachments []string `json:"attachments" validate:"optional" example:"https://example.com/image.jpg,https://example.com/video.mp4"` // Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app Encrypted bool `json:"encrypted" example:"false" validate:"optional"` From fdcf11facb199fbed452875d5da9f1ff1207e2f2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 8 Apr 2026 11:00:20 +0300 Subject: [PATCH 062/381] Add information about turnstile --- README.md | 37 ++++++++++++++++++++++++++----------- api/.env.docker | 4 ++++ web/.env.docker | 4 ++++ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ccae74510..84b77a40b 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,12 @@ Quick Start Guide 👉 [https://docs.httpsms.com](https://docs.httpsms.com) - [Self Host Setup - Docker](#self-host-setup---docker) - [1. Setup Firebase](#1-setup-firebase) - [2. Setup SMTP Email service](#2-setup-smtp-email-service) - - [3. Download the code](#3-download-the-code) - - [4. Setup the environment variables](#4-setup-the-environment-variables) - - [5. Build and Run](#5-build-and-run) - - [6. Create the System User](#6-create-the-system-user) - - [7. Build the Android App.](#7-build-the-android-app) + - [3. Setup Cloudflare Turnstile](#3-setup-cloudflare-turnstile) + - [4. Download the code](#4-download-the-code) + - [5. Setup the environment variables](#5-setup-the-environment-variables) + - [6. Build and Run](#6-build-and-run) + - [7. Create the System User](#7-create-the-system-user) + - [8. Build the Android App.](#8-build-the-android-app) - [License](#license) @@ -164,7 +165,15 @@ const firebaseConfig = { The httpSMS application uses [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) to send emails to users e.g. when your Android phone has been offline for a long period of time. You can use a service like [mailtrap](https://mailtrap.io/) to create an SMTP server for development purposes. -### 3. Download the code +### 3. Setup Cloudflare Turnstile + +The message search route (`/v1/messages/search`) is protected by a [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/get-started/) captcha to prevent abuse. You need to set up a Turnstile widget for the search messages feature to work. + +1. Go to the [Cloudflare dashboard](https://dash.cloudflare.com/) and navigate to **Turnstile**. +2. Add a new site and configure it for your self-hosted domain (e.g., `localhost` for local development). +3. Note down the **Site Key** and **Secret Key** — you will need them for the frontend and backend environment variables respectively. + +### 4. Download the code Clone the httpSMS GitHub repository @@ -172,7 +181,7 @@ Clone the httpSMS GitHub repository git clone https://github.com/NdoleStudio/httpsms.git ``` -### 4. Setup the environment variables +### 5. Setup the environment variables - Copy the `.env.docker` file in the `web` directory into `.env` @@ -190,6 +199,9 @@ FIREBASE_STORAGE_BUCKET= FIREBASE_MESSAGING_SENDER_ID= FIREBASE_APP_ID= FIREBASE_MEASUREMENT_ID= + +# Cloudflare Turnstile site key from step 3 +CLOUDFLARE_TURNSTILE_SITE_KEY= ``` - Copy the `.env.docker` file in the `api` directory into `.env` @@ -198,7 +210,7 @@ FIREBASE_MEASUREMENT_ID= cp api/.env.docker api/.env ``` -- Update the environment variables in the `.env` file in the `api` directory with your firebase service account credentials and SMTP server details. +- Update the environment variables in the `.env` file in the `api` directory with your firebase service account credentials, SMTP server details, and Cloudflare Turnstile secret key. ```dotenv # SMTP email server settings @@ -212,11 +224,14 @@ FIREBASE_CREDENTIALS= # This is the `projectId` from your firebase web config GCP_PROJECT_ID= + +# Cloudflare Turnstile secret key from step 3 +CLOUDFLARE_TURNSTILE_SECRET_KEY= ``` - Don't bother about the `EVENTS_QUEUE_USER_API_KEY` and `EVENTS_QUEUE_USER_ID` settings. We will set that up later. -### 5. Build and Run +### 6. Build and Run - Build and run the API, the web UI, database and cache using the `docker-compose.yml` file. It takes a while for build and download all the docker images. When it's finished, you'll be able to access the web UI at http://localhost:3000 and the API at http://localhost:8000 @@ -225,7 +240,7 @@ GCP_PROJECT_ID= docker compose up --build ``` -### 6. Create the System User +### 7. Create the System User - The application uses the concept of a system user to process events async. You should manually create this user in `users` table in your database. Make sure you use the same `id` and `api_key` as the `EVENTS_QUEUE_USER_ID`, and `EVENTS_QUEUE_USER_API_KEY` in your `.env` file. @@ -236,7 +251,7 @@ docker compose up --build > [!IMPORTANT] > Restart your API docker container after modifying `EVENTS_QUEUE_USER_ID`, and `EVENTS_QUEUE_USER_API_KEY` in your `.env` file so that the httpSMS API can pick up the changes. -### 7. Build the Android App. +### 8. Build the Android App. - Before building the Android app in [Android Studio](https://developer.android.com/studio), you need to replace the `google-services.json` file in the `android/app` directory with the file which you got from step 1. You need to do this for the firebase FCM messages to work properly. diff --git a/api/.env.docker b/api/.env.docker index 9dc43fdb7..2e6be8fb8 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -58,3 +58,7 @@ PUSHER_APP_ID= PUSHER_KEY= PUSHER_SECRET= PUSHER_CLUSTER= + +# Cloudflare Turnstile secret key for validating captcha tokens on the /v1/messages/search route +# Get your secret key at https://developers.cloudflare.com/turnstile/get-started/ +CLOUDFLARE_TURNSTILE_SECRET_KEY= diff --git a/web/.env.docker b/web/.env.docker index d48c328fb..b1751dfb6 100644 --- a/web/.env.docker +++ b/web/.env.docker @@ -15,3 +15,7 @@ FIREBASE_STORAGE_BUCKET=httpsms-docker.appspot.com FIREBASE_MESSAGING_SENDER_ID=668063041624 FIREBASE_APP_ID=668063041624:web:29b9e3b7027965ba08a22d FIREBASE_MEASUREMENT_ID=G-18VRYL22PZ + +# Cloudflare Turnstile site key for captcha on the search messages page +# Get your site key at https://developers.cloudflare.com/turnstile/get-started/ +CLOUDFLARE_TURNSTILE_SITE_KEY= From 9de415629800568c7d9336c689462f567690e670 Mon Sep 17 00:00:00 2001 From: giresse19 Date: Fri, 10 Apr 2026 17:46:11 +0300 Subject: [PATCH 063/381] fix: handle review feedback --- api/pkg/handlers/send_schedule_handler.go | 84 +-- .../services/phone_notification_service.go | 88 ++- .../send_schedule_handler_validator.go | 93 ++- web/models/api.ts | 58 ++ web/pages/settings/index.vue | 43 +- web/pages/settings/send-schedules/index.vue | 645 +++++++++++++----- web/store/index.ts | 127 +++- 7 files changed, 884 insertions(+), 254 deletions(-) diff --git a/api/pkg/handlers/send_schedule_handler.go b/api/pkg/handlers/send_schedule_handler.go index 22b47debb..70918cdd2 100644 --- a/api/pkg/handlers/send_schedule_handler.go +++ b/api/pkg/handlers/send_schedule_handler.go @@ -21,21 +21,30 @@ type SendScheduleHandler struct { service *services.SendScheduleService } -func NewSendScheduleHandler(logger telemetry.Logger, tracer telemetry.Tracer, validator *validators.SendScheduleHandlerValidator, service *services.SendScheduleService) *SendScheduleHandler { - return &SendScheduleHandler{logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), tracer: tracer, validator: validator, service: service} +func NewSendScheduleHandler( + logger telemetry.Logger, + tracer telemetry.Tracer, + validator *validators.SendScheduleHandlerValidator, + service *services.SendScheduleService, +) *SendScheduleHandler { + return &SendScheduleHandler{ + logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), + tracer: tracer, + validator: validator, + service: service, + } } func (h *SendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) { router.Get("/v1/send-schedules", h.computeRoute(middlewares, h.Index)...) router.Post("/v1/send-schedules", h.computeRoute(middlewares, h.Store)...) - router.Get("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Show)...) router.Put("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Update)...) router.Delete("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Delete)...) } // Index godoc // @Summary List send schedules -// @Description Lists the send schedules owned by the authenticated user. +// @Description List all send schedules owned by the authenticated user. // @Security ApiKeyAuth // @Tags Send Schedules // @Produce json @@ -46,47 +55,19 @@ func (h *SendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares .. func (h *SendScheduleHandler) Index(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + schedules, err := h.service.Index(ctx, h.userIDFomContext(c)) if err != nil { ctxLogger.Error(stacktrace.Propagate(err, "cannot list send schedules")) return h.responseInternalServerError(c) } - return h.responseOK(c, "send schedules fetched successfully", schedules) -} -// Show godoc -// @Summary Show send schedule -// @Description Loads a single send schedule owned by the authenticated user. -// @Security ApiKeyAuth -// @Tags Send Schedules -// @Produce json -// @Param scheduleID path string true "Schedule ID" -// @Success 200 {object} responses.SendScheduleResponse -// @Failure 401 {object} responses.Unauthorized -// @Failure 404 {object} responses.NotFound -// @Failure 500 {object} responses.InternalServerError -// @Router /send-schedules/{scheduleID} [get] -func (h *SendScheduleHandler) Show(c *fiber.Ctx) error { - ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) - defer span.End() - scheduleID, err := uuid.Parse(c.Params("scheduleID")) - if err != nil { - return h.responseBadRequest(c, err) - } - schedule, err := h.service.Load(ctx, h.userIDFomContext(c), scheduleID) - if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load send schedule")) - if stacktrace.GetCode(err) == 404 { - return h.responseNotFound(c, err.Error()) - } - return h.responseInternalServerError(c) - } - return h.responseOK(c, "send schedule fetched successfully", schedule) + return h.responseOK(c, "send schedules fetched successfully", schedules) } // Store godoc // @Summary Create send schedule -// @Description Creates a send schedule for the authenticated user. +// @Description Create a new send schedule for the authenticated user. // @Security ApiKeyAuth // @Tags Send Schedules // @Accept json @@ -101,26 +82,34 @@ func (h *SendScheduleHandler) Show(c *fiber.Ctx) error { func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + var request requests.SendScheduleStore if err := c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) } + request = request.Sanitize() if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("validation errors [%s], while storing send schedule [%+#v]", spew.Sdump(errors), request))) + ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf( + "validation errors [%s], while storing send schedule [%+#v]", + spew.Sdump(errors), + request, + ))) return h.responseUnprocessableEntity(c, errors, "validation errors while saving send schedule") } + schedule, err := h.service.Store(ctx, request.ToParams(h.userFromContext(c))) if err != nil { ctxLogger.Error(stacktrace.Propagate(err, "cannot create send schedule")) return h.responseInternalServerError(c) } + return h.responseCreated(c, "send schedule created successfully", schedule) } // Update godoc // @Summary Update send schedule -// @Description Updates a send schedule owned by the authenticated user. +// @Description Update a send schedule owned by the authenticated user. // @Security ApiKeyAuth // @Tags Send Schedules // @Accept json @@ -137,19 +126,28 @@ func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + scheduleID, err := uuid.Parse(c.Params("scheduleID")) if err != nil { return h.responseBadRequest(c, err) } + var request requests.SendScheduleStore if err = c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) } + request = request.Sanitize() if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 { return h.responseUnprocessableEntity(c, errors, "validation errors while updating send schedule") } - schedule, err := h.service.Update(ctx, h.userIDFomContext(c), scheduleID, request.ToParams(h.userFromContext(c))) + + schedule, err := h.service.Update( + ctx, + h.userIDFomContext(c), + scheduleID, + request.ToParams(h.userFromContext(c)), + ) if err != nil { ctxLogger.Error(stacktrace.Propagate(err, "cannot update send schedule")) if stacktrace.GetCode(err) == 404 { @@ -157,12 +155,13 @@ func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { } return h.responseInternalServerError(c) } + return h.responseOK(c, "send schedule updated successfully", schedule) } // Delete godoc // @Summary Delete send schedule -// @Description Deletes a send schedule owned by the authenticated user. +// @Description Delete a send schedule owned by the authenticated user. // @Security ApiKeyAuth // @Tags Send Schedules // @Produce json @@ -170,18 +169,25 @@ func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { // @Success 204 {object} responses.NoContent // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized +// @Failure 404 {object} responses.NotFound // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules/{scheduleID} [delete] func (h *SendScheduleHandler) Delete(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + scheduleID, err := uuid.Parse(c.Params("scheduleID")) if err != nil { return h.responseBadRequest(c, err) } + if err = h.service.Delete(ctx, h.userIDFomContext(c), scheduleID); err != nil { ctxLogger.Error(stacktrace.Propagate(err, "cannot delete send schedule")) + if stacktrace.GetCode(err) == 404 { + return h.responseNotFound(c, err.Error()) + } return h.responseInternalServerError(c) } + return h.responseNoContent(c, "send schedule deleted successfully") } diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index e8b7e0137..38c577b54 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -40,7 +40,7 @@ func NewNotificationService( dispatcher *EventDispatcher, ) (s *PhoneNotificationService) { return &PhoneNotificationService{ - logger: logger.WithService(fmt.Sprintf("%T", s)), + logger: logger.WithService(fmt.Sprintf("%T", &PhoneNotificationService{})), tracer: tracer, messagingClient: messagingClient, phoneNotificationRepository: phoneNotificationRepository, @@ -95,7 +95,13 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p return nil } - ctxLogger.Info(fmt.Sprintf("successfully sent heartbeat FCM [%s] to phone with ID [%s] for user [%s] and monitor [%s]", result, payload.PhoneID, payload.UserID, payload.MonitorID)) + ctxLogger.Info(fmt.Sprintf( + "successfully sent heartbeat FCM [%s] to phone with ID [%s] for user [%s] and monitor [%s]", + result, + payload.PhoneID, + payload.UserID, + payload.MonitorID, + )) return nil } @@ -137,7 +143,15 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone Token: *phone.FcmToken, }) if err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, fmt.Sprintf("cannot send FCM to phone with ID [%s] for user with ID [%s] and message [%s]", phone.ID, phone.UserID, params.MessageID))) + ctxLogger.Warn(stacktrace.Propagate( + err, + fmt.Sprintf( + "cannot send FCM to phone with ID [%s] for user with ID [%s] and message [%s]", + phone.ID, + phone.UserID, + params.MessageID, + ), + )) msg := fmt.Sprintf("cannot send notification for to your phone [%s]. Reinstall the httpSMS app on your Android phone.", phone.PhoneNumber) return service.handleNotificationFailed(ctx, errors.New(msg), params) } @@ -184,12 +198,13 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P var schedule *entities.SendSchedule if phone.ScheduleID != nil { schedule, err = service.sendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) - if err != nil && stacktrace.GetCode(err) != repositories.ErrCodeNotFound { - msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.ScheduleID, phone.ID) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { schedule = nil + err = nil + } + if err != nil { + msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.ScheduleID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) } } @@ -206,11 +221,20 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P return service.tracer.WrapErrorSpan(span, err) } - ctxLogger.Info(fmt.Sprintf("message with id [%s] notification scheduled for [%s] with id [%s]", params.MessageID, notification.ScheduledAt, notification.ID)) + ctxLogger.Info(fmt.Sprintf( + "message with id [%s] notification scheduled for [%s] with id [%s]", + params.MessageID, + notification.ScheduledAt, + notification.ID, + )) return nil } -func (service *PhoneNotificationService) dispatchMessageNotificationSend(ctx context.Context, source string, notification *entities.PhoneNotification) error { +func (service *PhoneNotificationService) dispatchMessageNotificationSend( + ctx context.Context, + source string, + notification *entities.PhoneNotification, +) error { event, err := service.createMessageNotificationSendEvent(source, &events.MessageNotificationSendPayload{ MessageID: notification.MessageID, UserID: notification.UserID, @@ -228,7 +252,11 @@ func (service *PhoneNotificationService) dispatchMessageNotificationSend(ctx con return nil } -func (service *PhoneNotificationService) dispatchMessageNotificationScheduled(ctx context.Context, params *PhoneNotificationScheduleParams, notification *entities.PhoneNotification) error { +func (service *PhoneNotificationService) dispatchMessageNotificationScheduled( + ctx context.Context, + params *PhoneNotificationScheduleParams, + notification *entities.PhoneNotification, +) error { event, err := service.createMessageNotificationScheduledEvent(params.Source, &events.MessageNotificationScheduledPayload{ MessageID: notification.MessageID, Owner: params.Owner, @@ -273,7 +301,12 @@ func (service *PhoneNotificationService) handleNotificationFailed(ctx context.Co return nil } -func (service *PhoneNotificationService) handleNotificationSent(ctx context.Context, phone *entities.Phone, result string, params *PhoneNotificationSendParams) error { +func (service *PhoneNotificationService) handleNotificationSent( + ctx context.Context, + phone *entities.Phone, + result string, + params *PhoneNotificationSendParams, +) error { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -294,15 +327,26 @@ func (service *PhoneNotificationService) handleNotificationSent(ctx context.Cont return nil } -func (service *PhoneNotificationService) createMessageNotificationScheduledEvent(source string, payload *events.MessageNotificationScheduledPayload) (cloudevents.Event, error) { +func (service *PhoneNotificationService) createMessageNotificationScheduledEvent( + source string, + payload *events.MessageNotificationScheduledPayload, +) (cloudevents.Event, error) { return service.createEvent(events.EventTypeMessageNotificationScheduled, source, payload) } -func (service *PhoneNotificationService) createMessageNotificationSendEvent(source string, payload *events.MessageNotificationSendPayload) (cloudevents.Event, error) { +func (service *PhoneNotificationService) createMessageNotificationSendEvent( + source string, + payload *events.MessageNotificationSendPayload, +) (cloudevents.Event, error) { return service.createEvent(events.EventTypeMessageNotificationSend, source, payload) } -func (service *PhoneNotificationService) createMessageNotificationSentEvent(source string, phone *entities.Phone, fcmMessageID string, params *PhoneNotificationSendParams) (cloudevents.Event, error) { +func (service *PhoneNotificationService) createMessageNotificationSentEvent( + source string, + phone *entities.Phone, + fcmMessageID string, + params *PhoneNotificationSendParams, +) (cloudevents.Event, error) { event := cloudevents.NewEvent() event.SetSource(source) @@ -329,7 +373,11 @@ func (service *PhoneNotificationService) createMessageNotificationSentEvent(sour return event, nil } -func (service *PhoneNotificationService) createMessageNotificationFailedEvent(source string, errorMessage string, params *PhoneNotificationSendParams) (cloudevents.Event, error) { +func (service *PhoneNotificationService) createMessageNotificationFailedEvent( + source string, + errorMessage string, + params *PhoneNotificationSendParams, +) (cloudevents.Event, error) { event := cloudevents.NewEvent() event.SetSource(source) @@ -354,7 +402,11 @@ func (service *PhoneNotificationService) createMessageNotificationFailedEvent(so return event, nil } -func (service *PhoneNotificationService) updateStatus(ctx context.Context, notificationID uuid.UUID, status entities.PhoneNotificationStatus) { +func (service *PhoneNotificationService) updateStatus( + ctx context.Context, + notificationID uuid.UUID, + status entities.PhoneNotificationStatus, +) { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -362,9 +414,9 @@ func (service *PhoneNotificationService) updateStatus(ctx context.Context, notif err := service.phoneNotificationRepository.UpdateStatus(ctx, notificationID, status) if err != nil { - msg := fmt.Sprintf("cannot update status of notificaiton with id [%s] to [%s]", notificationID, status) + msg := fmt.Sprintf("cannot update status of notification with id [%s] to [%s]", notificationID, status) ctxLogger.Error(stacktrace.Propagate(err, msg)) } - ctxLogger.Info(fmt.Sprintf("updated status of notificaiton with id [%s] to [%s]", notificationID, status)) + ctxLogger.Info(fmt.Sprintf("updated status of notification with id [%s] to [%s]", notificationID, status)) } diff --git a/api/pkg/validators/send_schedule_handler_validator.go b/api/pkg/validators/send_schedule_handler_validator.go index a199f3987..313eba9df 100644 --- a/api/pkg/validators/send_schedule_handler_validator.go +++ b/api/pkg/validators/send_schedule_handler_validator.go @@ -11,17 +11,28 @@ import ( "github.com/thedevsaddam/govalidator" ) +const maxWindowsPerDay = 6 + type SendScheduleHandlerValidator struct { validator logger telemetry.Logger tracer telemetry.Tracer } -func NewSendScheduleHandlerValidator(logger telemetry.Logger, tracer telemetry.Tracer) *SendScheduleHandlerValidator { - return &SendScheduleHandlerValidator{logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandlerValidator{})), tracer: tracer} +func NewSendScheduleHandlerValidator( + logger telemetry.Logger, + tracer telemetry.Tracer, +) *SendScheduleHandlerValidator { + return &SendScheduleHandlerValidator{ + logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandlerValidator{})), + tracer: tracer, + } } -func (validator *SendScheduleHandlerValidator) ValidateStore(_ context.Context, request requests.SendScheduleStore) url.Values { +func (validator *SendScheduleHandlerValidator) ValidateStore( + _ context.Context, + request requests.SendScheduleStore, +) url.Values { v := govalidator.New(govalidator.Options{ Data: &request, Rules: govalidator.MapData{ @@ -29,27 +40,77 @@ func (validator *SendScheduleHandlerValidator) ValidateStore(_ context.Context, "timezone": []string{"required", "min:2", "max:100"}, }, }) + result := v.ValidateStruct() validator.validateWindows(result, request.Windows) + if _, err := time.LoadLocation(request.Timezone); err != nil { result.Add("timezone", "timezone must be a valid IANA timezone") } + return result } -func (validator *SendScheduleHandlerValidator) validateWindows(result url.Values, windows []requests.SendScheduleWindow) { +func (validator *SendScheduleHandlerValidator) validateWindows( + result url.Values, + windows []requests.SendScheduleWindow, +) { + windowsPerDay := make(map[int]int) + for index, item := range windows { - if item.DayOfWeek < 0 || item.DayOfWeek > 6 { - result.Add("windows", fmt.Sprintf("windows[%d].day_of_week must be between 0 and 6", index)) - } - if item.StartMinute < 0 || item.StartMinute > 1439 { - result.Add("windows", fmt.Sprintf("windows[%d].start_minute must be between 0 and 1439", index)) - } - if item.EndMinute < 1 || item.EndMinute > 1440 { - result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be between 1 and 1440", index)) - } - if item.EndMinute <= item.StartMinute { - result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be greater than start_minute", index)) - } + validator.validateDayOfWeek(result, index, item, windowsPerDay) + validator.validateStartMinute(result, index, item) + validator.validateEndMinute(result, index, item) + validator.validateWindowRange(result, index, item) + } +} + +func (validator *SendScheduleHandlerValidator) validateDayOfWeek( + result url.Values, + index int, + item requests.SendScheduleWindow, + windowsPerDay map[int]int, +) { + if item.DayOfWeek < 0 || item.DayOfWeek > 6 { + result.Add("windows", fmt.Sprintf("windows[%d].day_of_week must be between 0 and 6", index)) + return + } + + windowsPerDay[item.DayOfWeek]++ + if windowsPerDay[item.DayOfWeek] > maxWindowsPerDay { + result.Add( + "windows", + fmt.Sprintf("day_of_week %d cannot have more than %d windows", item.DayOfWeek, maxWindowsPerDay), + ) + } +} + +func (validator *SendScheduleHandlerValidator) validateStartMinute( + result url.Values, + index int, + item requests.SendScheduleWindow, +) { + if item.StartMinute < 0 || item.StartMinute > 1439 { + result.Add("windows", fmt.Sprintf("windows[%d].start_minute must be between 0 and 1439", index)) + } +} + +func (validator *SendScheduleHandlerValidator) validateEndMinute( + result url.Values, + index int, + item requests.SendScheduleWindow, +) { + if item.EndMinute < 1 || item.EndMinute > 1440 { + result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be between 1 and 1440", index)) + } +} + +func (validator *SendScheduleHandlerValidator) validateWindowRange( + result url.Values, + index int, + item requests.SendScheduleWindow, +) { + if item.EndMinute <= item.StartMinute { + result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be greater than start_minute", index)) } } diff --git a/web/models/api.ts b/web/models/api.ts index 660e7493d..c305c134e 100644 --- a/web/models/api.ts +++ b/web/models/api.ts @@ -172,6 +172,8 @@ export interface EntitiesPhone { missed_call_auto_reply?: string /** @example "+18005550199" */ phone_number: string + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + schedule_id?: string | null /** SIM card that received the message */ sim: string /** @example "2022-06-05T14:26:10.303278+03:00" */ @@ -255,6 +257,62 @@ export interface EntitiesWebhook { user_id: string } +export interface EntitiesSendScheduleWindow { + /** @example 1 */ + day_of_week: number + /** @example 1020 */ + end_minute: number + /** @example 540 */ + start_minute: number +} + +export interface EntitiesSendSchedule { + /** @example true */ + is_active: boolean + /** @example "2022-06-05T14:26:02.302718+03:00" */ + created_at: string + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + id: string + /** @example "Business Hours" */ + name: string + /** @example "Africa/Accra" */ + timezone: string + /** @example "2022-06-05T14:26:10.303278+03:00" */ + updated_at: string + /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */ + user_id: string + windows: EntitiesSendScheduleWindow[] +} + +export interface RequestsSendScheduleWindow { + day_of_week: number + end_minute: number + start_minute: number +} + +export interface RequestsSendScheduleStore { + is_active: boolean + name: string + timezone: string + windows: RequestsSendScheduleWindow[] +} + +export interface ResponsesSendScheduleResponse { + data: EntitiesSendSchedule + /** @example "Request handled successfully" */ + message: string + /** @example "success" */ + status: string +} + +export interface ResponsesSendSchedulesResponse { + data: EntitiesSendSchedule[] + /** @example "Request handled successfully" */ + message: string + /** @example "success" */ + status: string +} + export interface RequestsDiscordStore { incoming_channel_id: string name: string diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index c6ef94747..56178d9e5 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -50,6 +50,7 @@ @change="updateTimezone" >
+
API Key

Use your API Key in the x-api-key HTTP Header when @@ -256,6 +257,7 @@ >Documentation

+
Discord Integration
@@ -322,6 +324,7 @@ > Add Discord
+
Phones

List of mobile phones which are registered for sending and @@ -382,6 +385,7 @@ +

Email Notifications
@@ -427,7 +431,11 @@ {{ mdiContentSave }} Save Notification Settings -
+ +
Delete Account

@@ -497,6 +505,7 @@ + Edit Phone @@ -605,7 +614,12 @@ - + + Add a new  @@ -720,7 +734,12 @@ - + + Add a new  @@ -853,7 +872,7 @@ import { mdiSquareEditOutline, mdiQrcode, } from '@mdi/js' -import axios from '~/plugins/axios' +import { EntitiesSendSchedule } from '~/models/api' import { toCanvas } from 'qrcode' import { ErrorMessages } from '~/plugins/errors' import LoadingButton from '~/components/LoadingButton.vue' @@ -1263,13 +1282,15 @@ export default Vue.extend({ }) }, - async loadSendSchedules() { - try { - const response = await axios.get('/v1/send-schedules') - this.sendSchedules = response.data?.data || [] - } catch (error) { - this.sendSchedules = [] - } + loadSendSchedules() { + this.$store + .dispatch('getSendSchedules') + .then((sendSchedules) => { + this.sendSchedules = sendSchedules + }) + .catch(() => { + this.sendSchedules = [] + }) }, loadWebhooks() { diff --git a/web/pages/settings/send-schedules/index.vue b/web/pages/settings/send-schedules/index.vue index 05d098c18..d271f7a2e 100644 --- a/web/pages/settings/send-schedules/index.vue +++ b/web/pages/settings/send-schedules/index.vue @@ -1,5 +1,9 @@ +

Send Schedules
+

+ Create availability schedules and attach them to each phone. + Outgoing messages sent outside the schedule window are queued and + delivered when the schedule opens according to your + configured send rate. +

+ + {{ mdiCalendarClock }} + Manage Send Schedules + +
Email Notifications
@@ -432,10 +436,7 @@ Save Notification Settings -
+
Delete Account

@@ -615,11 +616,7 @@ - + Add a new  @@ -735,11 +732,7 @@ - + Add a new  From acd63b046fdc59eafa0a2a2d04e0c046fc5da9e1 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 15:11:51 +0300 Subject: [PATCH 098/381] feat(web): add send schedule create/edit/delete dialog to settings page Replace the separate /settings/send-schedules page with an inline dialog on the main settings page, matching the pattern used for phones, webhooks, and Discord integrations. - Add v-dialog for creating and editing send schedules with day/time windows - Add delete confirmation dialog - Fix edit button in schedules table (was incorrectly calling showEditPhone) - Remove the now-redundant /settings/send-schedules/ page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- web/pages/settings/index.vue | 559 ++++++++++++++++- web/pages/settings/send-schedules/index.vue | 633 -------------------- 2 files changed, 531 insertions(+), 661 deletions(-) delete mode 100644 web/pages/settings/send-schedules/index.vue diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index a1250731c..ed190566f 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -211,6 +211,7 @@ {{ event }} @@ -360,6 +361,7 @@ @@ -374,7 +376,9 @@ -

Send Schedules
+
+ Send Schedules +

Create availability schedules and attach them to each phone. Outgoing messages sent outside the schedule window are queued and @@ -385,9 +389,51 @@ >configured send rate.

- + + + + {{ mdiCalendarClock }} - Manage Send Schedules + Create Send Schedule
@@ -606,7 +652,12 @@ Update - + {{ mdiDelete }} @@ -845,10 +896,205 @@ + + + + + Add Send Schedule + Edit Send Schedule + + + + + + + + + + + + + + + +
+ {{ message }} +
+
+ + + +
+
+ {{ day.label }} +
+ + + + {{ mdiPlus }} + Add window + +
+ +
+ Unavailable +
+ +
+
+ +
+
+
+ +
+
+ + {{ mdiDelete }} + +
+
+
+
+
+ + + Save Schedule + + + + {{ mdiContentSave }} + + Update Schedule + + + + + {{ mdiDelete }} + + Delete + + +
+
+ + + + Delete schedule + + Are you sure you want to delete {{ activeSchedule.name }}? Phones attached to this schedule will no longer have schedule-based + restrictions. + + + + Delete + + + Cancel + + + - diff --git a/web/pages/settings/send-schedules/index.vue b/web/pages/settings/send-schedules/index.vue deleted file mode 100644 index d271f7a2e..000000000 --- a/web/pages/settings/send-schedules/index.vue +++ /dev/null @@ -1,633 +0,0 @@ - - - - - From 1e53c554958322aa7aaa765e8fcc4a214fd7f1bc Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 15:27:02 +0300 Subject: [PATCH 099/381] docs: add entitlement service design spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-05-03-entitlement-service-design.md | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-03-entitlement-service-design.md diff --git a/docs/superpowers/specs/2026-05-03-entitlement-service-design.md b/docs/superpowers/specs/2026-05-03-entitlement-service-design.md new file mode 100644 index 000000000..c22b8a9b5 --- /dev/null +++ b/docs/superpowers/specs/2026-05-03-entitlement-service-design.md @@ -0,0 +1,188 @@ +# Entitlement Service Design + +## Problem + +The send schedule feature (and future features) need usage limits based on the user's subscription plan. Free users should be limited to 1 send schedule; paid users get unlimited. The system must be: + +- **Scalable**: Easy to add new entity limits without architectural changes +- **Configurable**: Disabled by default for self-hosted deployments, enabled via env var for cloud +- **Non-invasive**: Enforced at the handler layer, before business logic executes + +## Approach + +Create a dedicated `EntitlementService` in `pkg/services/` that: + +1. Reads `ENTITLEMENT_ENABLED` from environment (defaults to `false`) +2. Defines a code-based map of entity limits per subscription plan +3. Exposes a single `Check()` method that handlers call before creating resources +4. Returns 402 Payment Required when a free user exceeds their limit + +## Configuration + +### Environment Variable + +```env +# Set to "true" on cloud deployment; self-hosted defaults to false (no limits) +ENTITLEMENT_ENABLED=false +``` + +### Entity Limits (code-based) + +```go +// entityLimits maps entity name → subscription plan → max count +// A limit of 0 means unlimited. If a plan is not listed, it defaults to unlimited. +var entityLimits = map[string]map[entities.SubscriptionName]int{ + "MessageSendSchedule": { + entities.SubscriptionNameFree: 1, + }, + // Future: add more entities here + // "Webhook": { + // entities.SubscriptionNameFree: 3, + // }, +} +``` + +## Service Interface + +```go +// EntitlementService checks whether a user can create more of a given entity. +type EntitlementService struct { + logger telemetry.Logger + tracer telemetry.Tracer + enabled bool + userRepository repositories.UserRepository +} + +// NewEntitlementService creates the service. `enabled` comes from ENTITLEMENT_ENABLED env var. +func NewEntitlementService( + logger telemetry.Logger, + tracer telemetry.Tracer, + enabled bool, + userRepository repositories.UserRepository, +) *EntitlementService + +// CheckResult holds the outcome of an entitlement check. +type CheckResult struct { + Allowed bool + Message string +} + +// Check verifies if the user can create another instance of the given entity. +// - If entitlements are disabled (self-hosted), always returns Allowed: true. +// - Loads the user's subscription plan. +// - Looks up the limit for the entity + plan combination. +// - Compares currentCount against the limit. +func (s *EntitlementService) Check( + ctx context.Context, + userID entities.UserID, + entityName string, + currentCount int, +) (*CheckResult, error) +``` + +## Handler Integration + +In `SendScheduleHandler.Store()`: + +```go +func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { + // 1. Validate request (existing logic) + // 2. Get current count (efficient COUNT query) + count, err := h.service.CountByUser(ctx, userID) + if err != nil { ... } + // 3. Check entitlement + result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", count) + if err != nil { + return h.responseInternalServerError(c) + } + if !result.Allowed { + return h.responsePaymentRequired(c, result.Message) + } + // 4. Proceed with creating schedule (existing logic) +} +``` + +## Repository Addition + +Add to `SendScheduleRepository` interface and GORM implementation: + +```go +// CountByUser returns the number of schedules owned by a user. +CountByUser(ctx context.Context, userID entities.UserID) (int, error) +``` + +```` + +## Error Response + +HTTP 402 Payment Required: + +```json +{ + "message": "Upgrade to a paid plan to create more than 1 send schedule. Visit https://httpsms.com/pricing for details.", + "status": "payment_required" +} +```` + +## Files to Create/Modify + +| Action | File | Change | +| ------ | --------------------------------------------------- | ------------------------------------------------------------ | +| Create | `pkg/services/entitlement_service.go` | New service with limits map, `Check()`, `CheckResult` | +| Modify | `pkg/handlers/handler.go` | Add `responsePaymentRequired()` helper method | +| Modify | `pkg/handlers/send_schedule_handler.go` | Inject `EntitlementService`, add check in `Store()` | +| Modify | `pkg/di/container.go` | Wire `EntitlementService`, read env var, inject into handler | +| Modify | `pkg/repositories/send_schedule_repository.go` | Add `CountByUser()` to interface | +| Modify | `pkg/repositories/gorm_send_schedule_repository.go` | Implement `CountByUser()` with SQL COUNT | +| Modify | `pkg/services/send_schedule_service.go` | Add `CountByUser()` pass-through method | +| Modify | `.env.example` or `.env` | Add `ENTITLEMENT_ENABLED=false` | + +## Concurrency & Race Conditions + +The handler-level check (`count → check → create`) is not atomic. Two concurrent requests could both see `count=0` and both proceed. Mitigations: + +1. **Repository count method**: Use `CountByUser(ctx, userID)` instead of loading all records (efficient SQL `SELECT COUNT(*)`). +2. **Acceptable race window**: For a limit of 1, the worst case is 2 schedules created. This is acceptable because: + - The window is extremely small (single user, same millisecond) + - The consequence is minor (user has 2 schedules instead of 1) + - A DB-level unique constraint is impractical here (limit is per-user count, not per-row uniqueness) +3. **Future hardening**: If stricter enforcement is needed, add an advisory lock or transaction-based count+insert. + +## Counting Semantics + +All schedules owned by the user count toward the limit, regardless of `is_active` status. A user must delete a schedule to free up their quota. + +## Error Handling When Enabled + +- **Entitlements disabled** (`ENTITLEMENT_ENABLED=false`): Always returns `Allowed: true`, zero DB calls. +- **Entitlements enabled, DB error loading user**: Return error (surfaces as 500). Do NOT fail-open — this is a monetized feature gate. +- **Entitlements enabled, entity not in limits map**: Returns `Allowed: true` (entity has no restrictions). + +## Design Decisions + +1. **Handler-layer enforcement**: The handler gets the count and calls `Check()`. This keeps the entitlement service free of domain-specific repository dependencies. +2. **Entity name as key**: Using the entity struct name (e.g., `"MessageSendSchedule"`) makes it self-documenting and matches the user's preference for entity-based naming. +3. **Fail-open when disabled**: Self-hosted users never hit limits. The `enabled` flag short-circuits all checks. +4. **Fail-closed on error when enabled**: If the user can't be loaded and entitlements are enabled, the request fails with 500. +5. **Separate from BillingService**: BillingService handles SMS message counting/billing. EntitlementService handles feature-level access gating. Different concerns. +6. **No caching**: User plan data is already fast to load. Caching can be added later if needed. + +## Swagger & Handler Updates + +- Add `@Failure 402 {object} responses.PaymentRequired` annotation to `Store` route +- Add `responsePaymentRequired` helper to base handler struct +- Update handler constructor to accept `*services.EntitlementService` + +## Testing Strategy + +- Unit test `EntitlementService.Check()` with: + - Disabled mode → always allowed + - Free user at limit → denied + - Free user under limit → allowed + - Paid user → always allowed + - Unknown entity → allowed (no restrictions defined) + - User load error when enabled → returns error +- Handler test for `Store`: + - Free user with 0 schedules → 201 Created + - Free user with 1 schedule → 402 Payment Required + - Paid user with N schedules → 201 Created From 24159f1eeacf268be67a837c55e62d165c2f2cb2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 16:24:31 +0300 Subject: [PATCH 100/381] feat: add entitlement service to limit send schedules for free users - Create EntitlementService with configurable entity limits per plan - Add ENTITLEMENT_ENABLED env var (defaults to false for self-hosted) - Free users limited to 1 send schedule, paid users unlimited - Add CountByUser to send schedule repository for efficient counting - Return 402 Payment Required when limit exceeded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/.env.docker | 4 + api/pkg/di/container.go | 12 +++ api/pkg/handlers/send_schedule_handler.go | 37 +++++-- .../gorm_send_schedule_repository.go | 22 +++++ .../repositories/send_schedule_repository.go | 3 + api/pkg/services/entitlement_service.go | 96 +++++++++++++++++++ api/pkg/services/send_schedule_service.go | 8 ++ 7 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 api/pkg/services/entitlement_service.go diff --git a/api/.env.docker b/api/.env.docker index 5441d7fea..cf8f8cda2 100644 --- a/api/.env.docker +++ b/api/.env.docker @@ -5,6 +5,10 @@ GCP_PROJECT_ID=httpsms-docker USE_HTTP_LOGGER=true +# Set to "true" to enable feature entitlement checks (limits for free users). +# Defaults to "false" for self-hosted deployments (no limits). +ENTITLEMENT_ENABLED=false + EVENTS_QUEUE_TYPE=emulator EVENTS_QUEUE_NAME=events-local EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 676cb1dc1..5727f6f7f 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -788,6 +788,7 @@ func (container *Container) SendScheduleHandler() *handlers.SendScheduleHandler container.Tracer(), container.SendScheduleHandlerValidator(), container.SendScheduleService(), + container.EntitlementService(), ) } @@ -801,6 +802,17 @@ func (container *Container) BillingUsageRepository() (repository repositories.Bi ) } +// EntitlementService creates a new instance of services.EntitlementService +func (container *Container) EntitlementService() *services.EntitlementService { + container.logger.Debug("creating services.EntitlementService") + return services.NewEntitlementService( + container.Logger(), + container.Tracer(), + os.Getenv("ENTITLEMENT_ENABLED") == "true", + container.UserRepository(), + ) +} + // DiscordRepository creates a new instance of repositories.DiscordRepository func (container *Container) DiscordRepository() (repository repositories.DiscordRepository) { container.logger.Debug("creating GORM repositories.DiscordRepository") diff --git a/api/pkg/handlers/send_schedule_handler.go b/api/pkg/handlers/send_schedule_handler.go index cb636f494..ebf475a3e 100644 --- a/api/pkg/handlers/send_schedule_handler.go +++ b/api/pkg/handlers/send_schedule_handler.go @@ -17,10 +17,11 @@ import ( // SendScheduleHandler handles HTTP requests for message send schedules. type SendScheduleHandler struct { handler - logger telemetry.Logger - tracer telemetry.Tracer - validator *validators.SendScheduleHandlerValidator - service *services.SendScheduleService + logger telemetry.Logger + tracer telemetry.Tracer + validator *validators.SendScheduleHandlerValidator + service *services.SendScheduleService + entitlementService *services.EntitlementService } // NewSendScheduleHandler creates a new SendScheduleHandler. @@ -29,12 +30,14 @@ func NewSendScheduleHandler( tracer telemetry.Tracer, validator *validators.SendScheduleHandlerValidator, service *services.SendScheduleService, + entitlementService *services.EntitlementService, ) *SendScheduleHandler { return &SendScheduleHandler{ - logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), - tracer: tracer, - validator: validator, - service: service, + logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), + tracer: tracer, + validator: validator, + service: service, + entitlementService: entitlementService, } } @@ -82,6 +85,7 @@ func (h *SendScheduleHandler) Index(c *fiber.Ctx) error { // @Success 201 {object} responses.SendScheduleResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized +// @Failure 402 {object} responses.BadRequest // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules [post] @@ -89,6 +93,23 @@ func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + userID := h.userIDFomContext(c) + + count, err := h.service.CountByUser(ctx, userID) + if err != nil { + ctxLogger.Error(stacktrace.Propagate(err, "cannot count send schedules for entitlement check")) + return h.responseInternalServerError(c) + } + + result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", count) + if err != nil { + ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for send schedules")) + return h.responseInternalServerError(c) + } + if !result.Allowed { + return h.responsePaymentRequired(c, result.Message) + } + var request requests.SendScheduleStore if err := c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) diff --git a/api/pkg/repositories/gorm_send_schedule_repository.go b/api/pkg/repositories/gorm_send_schedule_repository.go index afd094722..4ee3c4089 100644 --- a/api/pkg/repositories/gorm_send_schedule_repository.go +++ b/api/pkg/repositories/gorm_send_schedule_repository.go @@ -166,3 +166,25 @@ func (r *gormSendScheduleRepository) DeleteAllForUser( return nil } + +// CountByUser returns the number of schedules owned by a user. +func (r *gormSendScheduleRepository) CountByUser( + ctx context.Context, + userID entities.UserID, +) (int, error) { + ctx, span := r.tracer.Start(ctx) + defer span.End() + + var count int64 + if err := r.db.WithContext(ctx). + Model(&entities.MessageSendSchedule{}). + Where("user_id = ?", userID). + Count(&count).Error; err != nil { + return 0, r.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, "cannot count send schedules for user [%s]", userID), + ) + } + + return int(count), nil +} diff --git a/api/pkg/repositories/send_schedule_repository.go b/api/pkg/repositories/send_schedule_repository.go index 57e07ca6d..d57b42d76 100644 --- a/api/pkg/repositories/send_schedule_repository.go +++ b/api/pkg/repositories/send_schedule_repository.go @@ -26,4 +26,7 @@ type SendScheduleRepository interface { // DeleteAllForUser removes all message send schedules owned by a user. DeleteAllForUser(ctx context.Context, userID entities.UserID) error + + // CountByUser returns the number of schedules owned by a user. + CountByUser(ctx context.Context, userID entities.UserID) (int, error) } diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go new file mode 100644 index 000000000..62c280abc --- /dev/null +++ b/api/pkg/services/entitlement_service.go @@ -0,0 +1,96 @@ +package services + +import ( + "context" + "fmt" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// entityLimits maps entity name → subscription plan → max count. +// A limit of 0 means unlimited. If a plan is not listed, it defaults to unlimited (0). +var entityLimits = map[string]map[entities.SubscriptionName]int{ + "MessageSendSchedule": { + entities.SubscriptionNameFree: 1, + }, +} + +// EntitlementCheckResult holds the outcome of an entitlement check. +type EntitlementCheckResult struct { + Allowed bool + Message string +} + +// EntitlementService checks whether a user can create more of a given entity +// based on their subscription plan. +type EntitlementService struct { + service + logger telemetry.Logger + tracer telemetry.Tracer + enabled bool + userRepository repositories.UserRepository +} + +// NewEntitlementService creates a new EntitlementService. +// The enabled flag should come from the ENTITLEMENT_ENABLED environment variable. +func NewEntitlementService( + logger telemetry.Logger, + tracer telemetry.Tracer, + enabled bool, + userRepository repositories.UserRepository, +) *EntitlementService { + return &EntitlementService{ + logger: logger.WithService(fmt.Sprintf("%T", &EntitlementService{})), + tracer: tracer, + enabled: enabled, + userRepository: userRepository, + } +} + +// Check verifies if the user can create another instance of the given entity. +func (service *EntitlementService) Check( + ctx context.Context, + userID entities.UserID, + entityName string, + currentCount int, +) (*EntitlementCheckResult, error) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + if !service.enabled { + return &EntitlementCheckResult{Allowed: true}, nil + } + + limits, exists := entityLimits[entityName] + if !exists { + return &EntitlementCheckResult{Allowed: true}, nil + } + + user, err := service.userRepository.Load(ctx, userID) + if err != nil { + return nil, service.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s] for entitlement check", userID)), + ) + } + + limit, hasLimit := limits[user.SubscriptionName] + if !hasLimit || limit == 0 { + return &EntitlementCheckResult{Allowed: true}, nil + } + + if currentCount >= limit { + return &EntitlementCheckResult{ + Allowed: false, + Message: fmt.Sprintf( + "Upgrade to a paid plan to create more than %d send schedule. Visit https://httpsms.com/pricing for details.", + limit, + ), + }, nil + } + + return &EntitlementCheckResult{Allowed: true}, nil +} diff --git a/api/pkg/services/send_schedule_service.go b/api/pkg/services/send_schedule_service.go index 5bbddb7dc..b984b32d7 100644 --- a/api/pkg/services/send_schedule_service.go +++ b/api/pkg/services/send_schedule_service.go @@ -51,6 +51,14 @@ func (service *SendScheduleService) Index( return service.repository.Index(ctx, userID) } +// CountByUser returns the number of schedules owned by a user. +func (service *SendScheduleService) CountByUser( + ctx context.Context, + userID entities.UserID, +) (int, error) { + return service.repository.CountByUser(ctx, userID) +} + // Load returns a single message send schedule for a user. func (service *SendScheduleService) Load( ctx context.Context, From ceb3b35f1d45b0ccb4314b09e806561708d71477 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 16:30:45 +0300 Subject: [PATCH 101/381] feat: add formatEntityName utility for human-readable entitlement messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts PascalCase entity names to lowercase words with proper pluralization (e.g. MessageSendSchedule → 'message send schedules') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/services/entitlement_service.go | 42 ++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 62c280abc..2cdae129f 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -3,6 +3,8 @@ package services import ( "context" "fmt" + "strings" + "unicode" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" @@ -86,11 +88,49 @@ func (service *EntitlementService) Check( return &EntitlementCheckResult{ Allowed: false, Message: fmt.Sprintf( - "Upgrade to a paid plan to create more than %d send schedule. Visit https://httpsms.com/pricing for details.", + "Upgrade to a paid plan to create more than %d %s. Visit https://httpsms.com/pricing for details.", limit, + formatEntityName(entityName, true), ), }, nil } return &EntitlementCheckResult{Allowed: true}, nil } + +// formatEntityName converts a PascalCase entity name to lowercase words and optionally pluralizes it. +// e.g. "MessageSendSchedule" → "message send schedules" (plural) or "message send schedule" (singular) +func formatEntityName(name string, plural bool) string { + var words []string + start := 0 + for i := 1; i < len(name); i++ { + if unicode.IsUpper(rune(name[i])) { + words = append(words, strings.ToLower(name[start:i])) + start = i + } + } + words = append(words, strings.ToLower(name[start:])) + + if plural && len(words) > 0 { + last := words[len(words)-1] + switch { + case strings.HasSuffix(last, "s"), strings.HasSuffix(last, "x"), strings.HasSuffix(last, "z"), + strings.HasSuffix(last, "sh"), strings.HasSuffix(last, "ch"): + words[len(words)-1] = last + "es" + case strings.HasSuffix(last, "y") && len(last) > 1 && !isVowel(last[len(last)-2]): + words[len(words)-1] = last[:len(last)-1] + "ies" + default: + words[len(words)-1] = last + "s" + } + } + + return strings.Join(words, " ") +} + +func isVowel(c byte) bool { + switch c { + case 'a', 'e', 'i', 'o', 'u': + return true + } + return false +} From 52b55dd82328f3f3732442271b6c5eb9c182ab87 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 16:34:05 +0300 Subject: [PATCH 102/381] refactor: use go-pluralize for entity name formatting Replace custom pluralization logic with github.com/gertd/go-pluralize for more accurate English pluralization rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 1 + api/go.sum | 2 ++ api/pkg/services/entitlement_service.go | 23 ++++------------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/api/go.mod b/api/go.mod index 7d752977c..40a311d3a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -18,6 +18,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dgraph-io/ristretto/v2 v2.4.0 github.com/dustin/go-humanize v1.0.1 + github.com/gertd/go-pluralize v0.2.1 github.com/go-hermes/hermes/v2 v2.6.2 github.com/gofiber/contrib/otelfiber v1.0.10 github.com/gofiber/fiber/v2 v2.52.13 diff --git a/api/go.sum b/api/go.sum index 1e3acc565..02111796a 100644 --- a/api/go.sum +++ b/api/go.sum @@ -112,6 +112,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 2cdae129f..cd4d740f6 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -9,6 +9,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + pluralize "github.com/gertd/go-pluralize" "github.com/palantir/stacktrace" ) @@ -88,7 +89,7 @@ func (service *EntitlementService) Check( return &EntitlementCheckResult{ Allowed: false, Message: fmt.Sprintf( - "Upgrade to a paid plan to create more than %d %s. Visit https://httpsms.com/pricing for details.", + "Upgrade to a paid plan to create more than [%d] %s. Visit https://httpsms.com/pricing for details.", limit, formatEntityName(entityName, true), ), @@ -112,25 +113,9 @@ func formatEntityName(name string, plural bool) string { words = append(words, strings.ToLower(name[start:])) if plural && len(words) > 0 { - last := words[len(words)-1] - switch { - case strings.HasSuffix(last, "s"), strings.HasSuffix(last, "x"), strings.HasSuffix(last, "z"), - strings.HasSuffix(last, "sh"), strings.HasSuffix(last, "ch"): - words[len(words)-1] = last + "es" - case strings.HasSuffix(last, "y") && len(last) > 1 && !isVowel(last[len(last)-2]): - words[len(words)-1] = last[:len(last)-1] + "ies" - default: - words[len(words)-1] = last + "s" - } + client := pluralize.NewClient() + words[len(words)-1] = client.Plural(words[len(words)-1]) } return strings.Join(words, " ") } - -func isVowel(c byte) bool { - switch c { - case 'a', 'e', 'i', 'o', 'u': - return true - } - return false -} From 7ed5909a27573e405116fed72ce59f469c47914d Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 16:44:42 +0300 Subject: [PATCH 103/381] Fix the send schedule --- web/pages/bulk-messages/index.vue | 10 +- web/pages/settings/index.vue | 243 ++++++++++++++++-------------- 2 files changed, 136 insertions(+), 117 deletions(-) diff --git a/web/pages/bulk-messages/index.vue b/web/pages/bulk-messages/index.vue index 5b8f20111..3182530ae 100644 --- a/web/pages/bulk-messages/index.vue +++ b/web/pages/bulk-messages/index.vue @@ -39,7 +39,15 @@ >Excel template and upload it here to send your SMS messages to multiple - recipients at once. + recipients at once. You can also configure + send schedules + on your phone to make sure messages are sent out at specific times + of the day e.g + Mon - Fri 9am - 5pm.

{{ errorTitle }}
diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index ed190566f..c35fc0c7a 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -401,9 +401,13 @@ - {{ schedule.name }} - {{ schedule.timezone }} - + + {{ schedule.name }} + + + {{ schedule.timezone }} + +
{{ line[1] }}
- + - Add Send Schedule - Edit Send Schedule + Create Message Send Schedule + Edit Message Send Schedule @@ -914,8 +918,9 @@ v-model="activeSchedule.name" outlined dense - label="Schedule name" - placeholder="Business Hours" + persistent-placeholder + label="Schedule Name" + placeholder="e.g Business Hours" :error="errorMessages.has('name')" :error-messages="errorMessages.get('name')" /> @@ -931,107 +936,91 @@ :error-messages="errorMessages.get('timezone')" /> - - - - - -
- {{ message }} -
-
- - - -
-
- {{ day.label }} -
- - - - {{ mdiPlus }} - Add window - -
- -
- Unavailable -
- -
-
- -
-
-
- -
-
- - {{ mdiDelete }} - -
-
+ + + + + + + + + +
+ + +
+
+ +
+
+
+ +
+
+ + {{ mdiPlus }} + + + {{ mdiDelete }} + +
+
+
+ {{ scheduleWindowError(day.value) }} +
+
- + - + {{ mdiDelete }} Delete
+ + Close + @@ -1254,6 +1244,7 @@ export default Vue.extend({ }, timezones() { try { + // @ts-ignore return Intl.supportedValuesOf('timeZone') } catch { return [] @@ -1425,7 +1416,7 @@ export default Vue.extend({ this.showDiscordEdit = false this.loadDiscordIntegrations() }) - .catch((errors) => { + .catch((errors: ErrorMessages) => { this.errorMessages = errors }) .finally(() => { @@ -1462,7 +1453,7 @@ export default Vue.extend({ this.showDiscordEdit = false this.loadDiscordIntegrations() }) - .catch((errors) => { + .catch((errors: ErrorMessages) => { this.errorMessages = errors }) .finally(() => { @@ -1500,7 +1491,7 @@ export default Vue.extend({ this.showWebhookEdit = false this.loadWebhooks() }) - .catch((errors) => { + .catch((errors: ErrorMessages) => { this.errorMessages = errors }) .finally(() => { @@ -1539,7 +1530,7 @@ export default Vue.extend({ this.showWebhookEdit = false this.loadWebhooks() }) - .catch((errors) => { + .catch((errors: ErrorMessages) => { this.errorMessages = errors }) .finally(() => { @@ -1578,7 +1569,7 @@ export default Vue.extend({ this.loadingSendSchedules = true this.$store .dispatch('getSendSchedules') - .then((sendSchedules) => { + .then((sendSchedules: EntitiesSendSchedule[]) => { this.sendSchedules = sendSchedules }) .finally(() => { @@ -1590,7 +1581,7 @@ export default Vue.extend({ this.loadingWebhooks = true this.$store .dispatch('getWebhooks') - .then((webhooks) => { + .then((webhooks: EntitiesWebhook[]) => { this.webhooks = webhooks }) .finally(() => { @@ -1602,7 +1593,7 @@ export default Vue.extend({ this.loadingDiscordIntegrations = true this.$store .dispatch('getDiscordIntegrations') - .then((discords) => { + .then((discords: EntitiesDiscord[]) => { this.discords = discords }) .finally(() => { @@ -1614,7 +1605,7 @@ export default Vue.extend({ this.deletingAccount = true this.$store .dispatch('deleteUserAccount') - .then((message) => { + .then((message: string) => { this.$store.dispatch('addNotification', { message: message ?? 'Your account has been deleted successfully', type: 'success', @@ -1659,6 +1650,10 @@ export default Vue.extend({ return hours * 60 + minutes }, + getWeekday(index: number): string { + return this.weekDays.find((x) => x.value == index)?.label ?? '' + }, + scheduleSummary(schedule: EntitiesSendSchedule) { return this.weekDays .map((day) => { @@ -1758,6 +1753,22 @@ export default Vue.extend({ }) }, + scheduleWindowError(index: number): string | null { + const messages = this.errorMessages.has('windows') + ? this.errorMessages.get('windows') + : [] + if (messages.length == 0) { + return null + } + + const message = messages.find((x: string) => + x.includes(`Day of week ${index}`), + ) + return message + ? message.replace(`Day of week ${index}`, this.getWeekday(index)) + : null + }, + scheduleRemoveWindow(dayOfWeek: number, index: number) { const matches = this.activeSchedule.windows.filter( (x) => x.day_of_week === dayOfWeek, From 65f63651b308b8f8e0bf622a1fff864d8a0807a0 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 16:58:42 +0300 Subject: [PATCH 104/381] Finish send schedule --- web/pages/settings/index.vue | 173 +++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 78 deletions(-) diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index c35fc0c7a..de6c0b050 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -904,14 +904,17 @@ Create Message Send Schedule Edit Message Send Schedule - + - - - - - - - - - -
- + +
+
+ +
+
+
+
+ -
-
+
+
+ +
+
+ -
- -
-
-
- -
-
- - {{ mdiPlus }} - - - {{ mdiDelete }} - -
-
-
{{ mdiPlus }} + + - {{ scheduleWindowError(day.value) }} -
-
+ {{ mdiDelete }} +
+ + +
+ {{ scheduleWindowError(day.value) }} +
+ + From 83844a58e0ea62e8161e1961cf92d4d4180d47c8 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:17:55 +0300 Subject: [PATCH 105/381] docs: add scheduling send refactor design spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...6-05-03-scheduling-send-refactor-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md diff --git a/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md b/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md new file mode 100644 index 000000000..34d784459 --- /dev/null +++ b/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md @@ -0,0 +1,117 @@ +# Scheduling Send Refactor Design + +## Problem Statement + +The current SMS scheduling logic has two issues: + +1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt`, the system still applies rate-limiting and schedule window logic, which may shift the actual send time. + +2. **Bulk message contention.** When bulk messages (API or CSV) are sent, all events arrive at the Cloud Tasks queue near-simultaneously, causing DB serialization conflicts in `PhoneNotificationRepository.Schedule()` (which uses `SELECT ... ORDER BY scheduled_at DESC` in a transaction). The current workaround is a hardcoded 1-second spacing hack. + +## Proposed Solution + +### Core Principle + +- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. +- **No `SendTime`** = apply full scheduling logic (rate-limit + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. + +### Design + +#### 1. ExactSendTime Flag (Transient — not persisted) + +A boolean `ExactSendTime` flows through the event system: + +``` +Request → MessageSendParams → MessageAPISentPayload → PhoneNotificationScheduleParams +``` + +When `true`, the notification scheduling layer sets `ScheduledAt` to the exact time and skips rate-limit + window logic. + +#### 2. Rate-Based Dispatch Delay + +For bulk messages without an explicit `SendTime`, instead of the `index * 1s` hack, the service computes: + +```go +interval := time.Minute / time.Duration(messagesPerMinute) +delay := time.Duration(index) * interval +``` + +Where `index` is **per-phone** (not global across the batch). This spreads Cloud Task deliveries at the phone's actual send rate, eliminating DB contention naturally. Duration math avoids integer truncation issues for rates > 60/min or non-divisors of 60. + +#### 3. Per-Endpoint Behavior + +| Endpoint | `SendAt` provided | `SendAt` absent | +| --------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------- | +| Single SMS API (`/v1/messages/send`) | `ExactSendTime=true`, delay = `time.Until(SendAt)` | `ExactSendTime=false`, delay = 0 | +| Bulk SMS API (`/v1/messages/bulk-send`) | N/A (no SendAt field) | `ExactSendTime=false`, delay = `perPhoneIndex * interval` | +| CSV Upload | `ExactSendTime=true`, delay = `time.Until(SendTime)` | `ExactSendTime=false`, delay = `perPhoneIndex * interval` | + +**Index is per-phone**: In a CSV with messages to multiple phones, each phone maintains its own index counter. Messages to Phone A get indices 0, 1, 2... and messages to Phone B get separate indices 0, 1, 2... This ensures correct rate-limiting per phone without over-throttling unrelated phones. + +#### 4. Notification Scheduling Bypass + +In `PhoneNotificationService.Schedule()`: + +```go +if params.ExactSendTime && params.ScheduledSendTime != nil { + notification.ScheduledAt = *params.ScheduledSendTime + // Skip rate-limit and schedule window logic + // Insert directly +} else { + // Existing logic: rate-limit + schedule window +} +``` + +### Changes by File + +| File | Change | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pkg/events/message_api_sent_event.go` | Add `ExactSendTime bool` field to `MessageAPISentPayload` | +| `pkg/services/message_service.go` | Add `Index int` to `MessageSendParams`; update `getSendDelay()` to compute rate-based delay when `Index > 0` and `SendAt == nil`; set `ExactSendTime` on event payload when `SendAt != nil` | +| `pkg/services/phone_notification_service.go` | Add `ExactSendTime bool` + `ScheduledSendTime *time.Time` to `PhoneNotificationScheduleParams`; add bypass path in `Schedule()` when `ExactSendTime && ScheduledSendTime != nil` — insert notification directly without transaction/rate logic | +| `pkg/repositories/gorm_phone_notification_repository.go` | Add `ScheduleExact(ctx, notification)` method that inserts with a fixed `ScheduledAt` (no transaction, no rate query). Add unique constraint or dedupe check on `(message_id)` for pending notifications to ensure idempotency. | +| `pkg/repositories/phone_notification_repository.go` | Add `ScheduleExact` to the repository interface | +| `pkg/listeners/phone_notification_listener.go` | Pass `ExactSendTime` + `ScheduledSendTime` from event payload to service params | +| `pkg/requests/message_bulk_send_request.go` | Remove per-index `SendAt` computation; add `Index` to each `MessageSendParams` | +| `pkg/requests/bulk_message_request.go` | Propagate `Index` into params for CSV rows | +| `pkg/handlers/message_handler.go` | Remove `index * 1s` hack in `BulkSend` handler | +| `pkg/handlers/bulk_message_handler.go` | Compute per-phone index for CSV rows; remove any concurrent scheduling; ensure `Index` is passed to `MessageSendParams` | + +### Data Flow + +``` +User sends request + → Handler creates MessageSendParams (with Index for bulk, ExactSendTime derived from SendAt presence) + → MessageService.SendMessage() + → Computes dispatch delay: + - ExactSendTime: time.Until(SendAt) + - Bulk without SendAt: Index * (60/MessagesPerMinute)s + - Single without SendAt: 0 + → Sets ExactSendTime on MessageAPISentPayload + → DispatchWithTimeout(event, delay) → Cloud Tasks + → [delay elapses] → PhoneNotificationListener.onMessageAPISent() + → PhoneNotificationService.Schedule(params with ExactSendTime) + → If ExactSendTime: insert with exact ScheduledAt + → Else: apply rate-limit + schedule window logic +``` + +### Edge Cases + +- **SendAt in the past**: Send immediately (existing behavior preserved). +- **MessagesPerMinute = 0**: No rate limiting; bulk messages dispatch immediately (existing behavior — `Schedule()` already handles this). Rate-based delay uses 0 when rate is 0. +- **No schedule attached to phone**: Window logic returns current time unchanged (existing behavior). +- **CSV with mixed rows**: Some rows have `SendTime`, others don't. Each row is processed independently — those with `SendTime` get exact dispatch, those without get rate-based delay. +- **Cloud Task duplicate delivery**: `ScheduleExact` and `Schedule` use a dedupe check (unique active notification per `message_id`) to prevent duplicate notification creation on at-least-once delivery. +- **Retries for exact-send messages**: When an exact-send message expires and triggers a retry, the retry does NOT preserve exact-send semantics — it falls through to standard scheduling. The explicit time was a one-shot intent. + +### Terminology Note + +"Send at exactly that time" means the system will not apply additional rate-limit or schedule-window adjustments. It does NOT guarantee precise handset delivery timing (which depends on Cloud Tasks delivery, FCM push, and device state). + +### What Does NOT Change + +- The `MessageSendSchedule` entity and its `ResolveScheduledAt()` logic +- The `SendScheduleService` CRUD operations +- The phone notification entity schema (no new DB columns) +- The Android app behavior +- The web frontend (models auto-generated from Swagger) From 0d9741de888d8d3c89a9b7264704bd68e69e2240 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:30:34 +0300 Subject: [PATCH 106/381] docs: add scheduling send refactor implementation plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-05-03-scheduling-send-refactor.md | 951 ++++++++++++++++++ 1 file changed, 951 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md diff --git a/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md b/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md new file mode 100644 index 000000000..a85b46ed1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md @@ -0,0 +1,951 @@ +# Scheduling Send Refactor Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow users to send SMS at an exact time (bypassing scheduling) when `SendAt` is specified, and replace the 1-second bulk hack with rate-based dispatch delays. + +**Architecture:** Add a transient `ExactSendTime` flag flowing through the event system. When true, bypass rate-limit and schedule window logic in notification scheduling. For bulk sends without explicit time, compute dispatch delay from `MessagesPerMinute` per-phone instead of hardcoded 1s. + +**Tech Stack:** Go, Fiber, GORM, CockroachDB, Google Cloud Tasks (CloudEvents) + +**Spec:** `docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md` + +**Build/Test commands:** + +```bash +cd api && go build ./... +cd api && go test -vet=off ./... +``` + +--- + +## Task 1: Add ExactSendTime to Event Payload + +**Files:** + +- Modify: `api/pkg/events/message_api_sent_event.go` + +- [ ] **Step 1: Add `ExactSendTime` field to `MessageAPISentPayload`** + +In `api/pkg/events/message_api_sent_event.go`, add to the struct: + +```go +ExactSendTime bool `json:"exact_send_time"` +``` + +Add it after line 22 (`ScheduledSendTime *time.Time`). + +- [ ] **Step 2: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 3: Commit** + +```bash +cd api && git add -A && git commit -m "feat(events): add ExactSendTime field to MessageAPISentPayload" +``` + +--- + +## Task 2: Add Index and ExactSendTime to MessageSendParams + Update getSendDelay + +**Files:** + +- Modify: `api/pkg/services/message_service.go` + +- [ ] **Step 1: Add `Index` field to `MessageSendParams`** + +In `api/pkg/services/message_service.go` at line ~453, add `Index int` to the struct: + +```go +type MessageSendParams struct { + Owner *phonenumbers.PhoneNumber + Contact string + Encrypted bool + Content string + Attachments []string + Source string + SendAt *time.Time + RequestID *string + UserID entities.UserID + RequestReceivedAt time.Time + Index int +} +``` + +- [ ] **Step 2: Update `phoneSettings` to also return `MessagesPerMinute`** + +Change the `phoneSettings` method signature and body at line ~1014: + +```go +func (service *MessageService) phoneSettings(ctx context.Context, userID entities.UserID, owner string) (uint, entities.SIM, uint) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + ctxLogger := service.tracer.CtxLogger(service.logger, span) + + phone, err := service.phoneService.Load(ctx, userID, owner) + if err != nil { + msg := fmt.Sprintf("cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner) + ctxLogger.Error(stacktrace.Propagate(err, msg)) + return 2, entities.SIM1, 0 + } + + return phone.MaxSendAttemptsSanitized(), phone.SIM, phone.MessagesPerMinute +} +``` + +- [ ] **Step 3: Update `SendMessage` to use new `phoneSettings` return value and set `ExactSendTime`** + +Update `SendMessage` at line ~467. Key changes: get `messagesPerMinute` from `phoneSettings`, derive `ExactSendTime` from `SendAt != nil`, pass `messagesPerMinute` to `getSendDelay`: + +```go +func (service *MessageService) SendMessage(ctx context.Context, params MessageSendParams) (*entities.Message, error) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + ctxLogger := service.tracer.CtxLogger(service.logger, span) + + sendAttempts, sim, messagesPerMinute := service.phoneSettings(ctx, params.UserID, phonenumbers.Format(params.Owner, phonenumbers.E164)) + + eventPayload := events.MessageAPISentPayload{ + MessageID: uuid.New(), + UserID: params.UserID, + Encrypted: params.Encrypted, + MaxSendAttempts: sendAttempts, + RequestID: params.RequestID, + Owner: phonenumbers.Format(params.Owner, phonenumbers.E164), + Contact: params.Contact, + RequestReceivedAt: params.RequestReceivedAt, + Content: params.Content, + Attachments: params.Attachments, + ScheduledSendTime: params.SendAt, + ExactSendTime: params.SendAt != nil, + SIM: sim, + } + + event, err := service.createMessageAPISentEvent(params.Source, eventPayload) + if err != nil { + msg := fmt.Sprintf("cannot create %T from payload with message id [%s]", event, eventPayload.MessageID) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s] and user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID)) + + message, err := service.storeSentMessage(ctx, eventPayload) + if err != nil { + msg := fmt.Sprintf("cannot store message with id [%s]", eventPayload.MessageID) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + timeout := service.getSendDelay(ctxLogger, eventPayload, params, messagesPerMinute) + if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, timeout); err != nil { + msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s] and delay [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID, timeout)) + return message, err +} +``` + +- [ ] **Step 4: Rewrite `getSendDelay` to handle rate-based delay** + +Replace the existing `getSendDelay` method. New signature takes `messagesPerMinute` as a separate arg: + +```go +func (service *MessageService) getSendDelay(ctxLogger telemetry.Logger, eventPayload events.MessageAPISentPayload, params MessageSendParams, messagesPerMinute uint) time.Duration { + // Exact send time: delay until that time (clamped to 0 if in the past) + if params.SendAt != nil { + delay := params.SendAt.Sub(time.Now().UTC()) + if delay < 0 { + ctxLogger.Info(fmt.Sprintf("message [%s] has send time [%s] in the past. sending immediately", eventPayload.MessageID, params.SendAt.String())) + return time.Duration(0) + } + return delay + } + + // Rate-based delay for bulk messages (Index > 0) + if params.Index > 0 && messagesPerMinute > 0 { + interval := time.Minute / time.Duration(messagesPerMinute) + delay := time.Duration(params.Index) * interval + ctxLogger.Info(fmt.Sprintf("message [%s] bulk index [%d] rate-based delay [%s]", eventPayload.MessageID, params.Index, delay)) + return delay + } + + return time.Duration(0) +} +``` + +- [ ] **Step 5: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 6: Run tests** + +Run: `cd api && go test -vet=off ./...` +Expected: all pass + +- [ ] **Step 7: Commit** + +```bash +cd api && git add -A && git commit -m "feat(services): add rate-based dispatch delay and ExactSendTime to SendMessage" +``` + +--- + +## Task 3: Add ScheduleExact to Repository Interface and Implementation + +**Files:** + +- Modify: `api/pkg/repositories/phone_notification_repository.go` +- Modify: `api/pkg/repositories/gorm_phone_notification_repository.go` + +- [ ] **Step 1: Add `ScheduleExact` to the repository interface** + +In `api/pkg/repositories/phone_notification_repository.go`: + +```go +// PhoneNotificationRepository loads and persists an entities.PhoneNotification +type PhoneNotificationRepository interface { + // Schedule a new entities.PhoneNotification + Schedule(ctx context.Context, messagesPerMinute uint, schedule *entities.MessageSendSchedule, notification *entities.PhoneNotification) error + + // ScheduleExact stores a phone notification with a fixed ScheduledAt time, + // bypassing rate-limit and schedule window logic. + ScheduleExact(ctx context.Context, notification *entities.PhoneNotification) error + + // UpdateStatus of a notification + UpdateStatus(ctx context.Context, notificationID uuid.UUID, status entities.PhoneNotificationStatus) error + + // DeleteAllForUser deletes all entities.PhoneNotification for a user + DeleteAllForUser(ctx context.Context, userID entities.UserID) error +} +``` + +- [ ] **Step 2: Implement `ScheduleExact` on `gormPhoneNotificationRepository`** + +In `api/pkg/repositories/gorm_phone_notification_repository.go`, add after the `Schedule` method: + +```go +// ScheduleExact stores a phone notification with an exact ScheduledAt time. +// It performs a dedupe check — if a pending notification for the same message already exists, it's a no-op. +func (repository *gormPhoneNotificationRepository) ScheduleExact( + ctx context.Context, + notification *entities.PhoneNotification, +) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + // Dedupe: check if a pending notification for this message already exists + var count int64 + if err := repository.db.WithContext(ctx). + Model(&entities.PhoneNotification{}). + Where("message_id = ? AND status = ?", notification.MessageID, entities.PhoneNotificationStatusPending). + Count(&count).Error; err != nil { + return repository.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, "cannot check for existing notification for message [%s]", notification.MessageID), + ) + } + + if count > 0 { + return nil + } + + if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil { + return repository.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, "cannot create exact-time notification with id [%s]", notification.ID), + ) + } + + return nil +} +``` + +- [ ] **Step 3: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 4: Commit** + +```bash +cd api && git add -A && git commit -m "feat(repositories): add ScheduleExact method for exact-time notifications" +``` + +--- + +## Task 4: Update PhoneNotificationService to Support ExactSendTime + +**Files:** + +- Modify: `api/pkg/services/phone_notification_service.go` + +- [ ] **Step 1: Add fields to `PhoneNotificationScheduleParams`** + +Update the struct at line ~162: + +```go +// PhoneNotificationScheduleParams are parameters for sending a notification +type PhoneNotificationScheduleParams struct { + UserID entities.UserID + Owner string + Source string + Encrypted bool + Contact string + Content string + SIM entities.SIM + MessageID uuid.UUID + ExactSendTime bool + ScheduledSendTime *time.Time +} +``` + +- [ ] **Step 2: Add bypass logic at the start of `Schedule` method** + +Update `Schedule` method at line ~175. Add the bypass path after loading the phone: + +```go +// Schedule a notification to be sent to a phone +func (service *PhoneNotificationService) Schedule(ctx context.Context, params *PhoneNotificationScheduleParams) error { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + ctxLogger := service.tracer.CtxLogger(service.logger, span) + + phone, err := service.phoneRepository.Load(ctx, params.UserID, params.Owner) + if err != nil { + msg := fmt.Sprintf("cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + notification := &entities.PhoneNotification{ + ID: uuid.New(), + MessageID: params.MessageID, + UserID: params.UserID, + PhoneID: phone.ID, + Status: entities.PhoneNotificationStatusPending, + ScheduledAt: time.Now().UTC(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + // Bypass rate-limit and schedule window logic for exact send time + if params.ExactSendTime && params.ScheduledSendTime != nil { + scheduledAt := *params.ScheduledSendTime + // Clamp past times to now (send immediately) + if scheduledAt.Before(time.Now().UTC()) { + scheduledAt = time.Now().UTC() + } + notification.ScheduledAt = scheduledAt + if err = service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { + msg := fmt.Sprintf("cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { + ctxLogger.Error(err) + } + + if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { + return service.tracer.WrapErrorSpan(span, err) + } + + ctxLogger.Info(fmt.Sprintf( + "message with id [%s] exact notification scheduled for [%s] with id [%s]", + params.MessageID, + notification.ScheduledAt, + notification.ID, + )) + return nil + } + + // Standard path: apply rate-limit + schedule window logic + var schedule *entities.MessageSendSchedule + if phone.ScheduleID != nil { + schedule, err = service.sendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) + if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { + schedule = nil + err = nil + } + if err != nil { + msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.ScheduleID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + } + + if err = service.phoneNotificationRepository.Schedule(ctx, phone.MessagesPerMinute, schedule, notification); err != nil { + msg := fmt.Sprintf("cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { + ctxLogger.Error(err) + } + + if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { + return service.tracer.WrapErrorSpan(span, err) + } + + ctxLogger.Info(fmt.Sprintf( + "message with id [%s] notification scheduled for [%s] with id [%s]", + params.MessageID, + notification.ScheduledAt, + notification.ID, + )) + return nil +} +``` + +- [ ] **Step 3: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 4: Commit** + +```bash +cd api && git add -A && git commit -m "feat(services): add ExactSendTime bypass in PhoneNotificationService.Schedule" +``` + +--- + +## Task 5: Update Phone Notification Listener to Pass ExactSendTime + +**Files:** + +- Modify: `api/pkg/listeners/phone_notification_listener.go` + +- [ ] **Step 1: Pass ExactSendTime and ScheduledSendTime from event payload to service params** + +Update the `onMessageAPISent` method at line ~44: + +```go +func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.MessageAPISentPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + sendParams := &services.PhoneNotificationScheduleParams{ + UserID: payload.UserID, + Owner: payload.Owner, + Contact: payload.Contact, + Content: payload.Content, + SIM: payload.SIM, + Encrypted: payload.Encrypted, + Source: event.Source(), + MessageID: payload.MessageID, + ExactSendTime: payload.ExactSendTime, + ScheduledSendTime: payload.ScheduledSendTime, + } + + if err := listener.service.Schedule(ctx, sendParams); err != nil { + msg := fmt.Sprintf("cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} +``` + +- [ ] **Step 2: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 3: Commit** + +```bash +cd api && git add -A && git commit -m "feat(listeners): pass ExactSendTime to PhoneNotificationService from event" +``` + +--- + +## Task 6: Update Bulk Send Request + Handler + +**Files:** + +- Modify: `api/pkg/requests/message_bulk_send_request.go` +- Modify: `api/pkg/handlers/message_handler.go` + +- [ ] **Step 1: Remove per-index SendAt from `MessageBulkSend.ToMessageSendParams()`** + +In `api/pkg/requests/message_bulk_send_request.go`, update `ToMessageSendParams`: + +```go +// ToMessageSendParams converts MessageSend to services.MessageSendParams +func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source string) []services.MessageSendParams { + from, _ := phonenumbers.Parse(input.From, phonenumbers.UNKNOWN_REGION) + + var result []services.MessageSendParams + for index, to := range input.To { + result = append(result, services.MessageSendParams{ + Source: source, + Owner: from, + Encrypted: input.Encrypted, + RequestID: input.sanitizeStringPointer(input.RequestID), + UserID: userID, + RequestReceivedAt: time.Now().UTC(), + Contact: to, + Content: input.Content, + Attachments: input.Attachments, + Index: index, + }) + } + + return result +} +``` + +Key changes: removed `SendAt` assignment and added `Index: index`. + +- [ ] **Step 2: Remove the `index * 1s` hack from `BulkSend` handler** + +In `api/pkg/handlers/message_handler.go`, update the `BulkSend` handler goroutine (around line 160-175). Remove the `if message.SendAt == nil` block: + +Replace: + +```go +for index, message := range params { + wg.Add(1) + go func(message services.MessageSendParams, index int) { + count.Add(1) + if message.SendAt == nil { + sentAt := time.Now().UTC().Add(time.Duration(index) * time.Second) + message.SendAt = &sentAt + } + + response, err := h.service.SendMessage(ctx, message) +``` + +With: + +```go +for index, message := range params { + wg.Add(1) + go func(message services.MessageSendParams, index int) { + count.Add(1) + response, err := h.service.SendMessage(ctx, message) +``` + +- [ ] **Step 3: Remove unused `time` import if needed** + +Check if `time` is still used in `message_handler.go`. It likely is (used elsewhere), so skip this step if so. + +- [ ] **Step 4: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 5: Commit** + +```bash +cd api && git add -A && git commit -m "feat(handlers): replace 1s hack with rate-based delay for bulk send" +``` + +--- + +## Task 7: Update CSV Bulk Message Request + Handler + +**Files:** + +- Modify: `api/pkg/requests/bulk_message_request.go` +- Modify: `api/pkg/handlers/bulk_message_handler.go` + +- [ ] **Step 1: Add `Index` parameter to `BulkMessage.ToMessageSendParams()`** + +In `api/pkg/requests/bulk_message_request.go`, change the method signature to accept index: + +```go +// ToMessageSendParams converts BulkMessage to services.MessageSendParams +func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string, index int) services.MessageSendParams { + from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) + + return services.MessageSendParams{ + Source: source, + Owner: from, + RequestID: input.sanitizeStringPointer(fmt.Sprintf("bulk-%s", requestID.String())), + UserID: userID, + SendAt: input.SendTime, + RequestReceivedAt: time.Now().UTC(), + Contact: input.sanitizeAddress(input.ToPhoneNumber), + Content: input.Content, + Attachments: input.removeEmptyStrings(strings.Split(input.AttachmentURLs, ",")), + Index: index, + } +} +``` + +- [ ] **Step 2: Update `BulkMessageHandler.Store()` to compute per-phone index** + +In `api/pkg/handlers/bulk_message_handler.go`, update the Store method to compute per-phone indices: + +```go +func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { + ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) + defer span.End() + + file, err := c.FormFile("document") + if err != nil { + msg := fmt.Sprintf("cannot fetch file with name [%s] from request", "document") + ctxLogger.Warn(stacktrace.Propagate(err, msg)) + return h.responseBadRequest(c, err) + } + + messages, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) + if len(validationErrors) != 0 { + msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)) + ctxLogger.Warn(stacktrace.NewError(msg)) + return h.responseUnprocessableEntity(c, validationErrors, "validation errors while sending bulk SMS") + } + + if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(messages))); msg != nil { + ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages)))) + return h.responsePaymentRequired(c, *msg) + } + + requestID := uuid.New() + wg := sync.WaitGroup{} + count := atomic.Int64{} + + // Compute per-phone index for rate-based dispatch delay + phoneIndexMap := make(map[string]int) + for _, message := range messages { + if message.SendTime != nil { + continue // Exact-time messages don't need indexing + } + phone := message.FromPhoneNumber + phoneIndexMap[phone]++ // Pre-count not needed, we'll compute inline + } + + // Reset for actual iteration + phoneIndexCounter := make(map[string]int) + + for _, message := range messages { + wg.Add(1) + var perPhoneIndex int + if message.SendTime == nil { + perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] + phoneIndexCounter[message.FromPhoneNumber]++ + } + + go func(message *requests.BulkMessage, index int) { + count.Add(1) + _, err = h.messageService.SendMessage( + ctx, + message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index), + ) + if err != nil { + count.Add(-1) + msg := fmt.Sprintf("cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index) + ctxLogger.Error(stacktrace.Propagate(err, msg)) + } + wg.Done() + }(message, perPhoneIndex) + } + + wg.Wait() + return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages))) +} +``` + +- [ ] **Step 3: Clean up unused `phoneIndexMap` variable** + +The `phoneIndexMap` is computed but unused. Remove it — we only need `phoneIndexCounter`: + +```go +// Compute per-phone index for rate-based dispatch delay +phoneIndexCounter := make(map[string]int) + +for _, message := range messages { + wg.Add(1) + var perPhoneIndex int + if message.SendTime == nil { + perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] + phoneIndexCounter[message.FromPhoneNumber]++ + } + + go func(message *requests.BulkMessage, index int) { + // ... same as above + }(message, perPhoneIndex) +} +``` + +- [ ] **Step 4: Build to verify no compile errors** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 5: Run tests** + +Run: `cd api && go test -vet=off ./...` +Expected: all pass + +- [ ] **Step 6: Commit** + +```bash +cd api && git add -A && git commit -m "feat(handlers): add per-phone index for CSV bulk messages" +``` + +--- + +## Task 8: Add Unit Tests for getSendDelay + +**Files:** + +- Create: `api/pkg/services/message_service_test.go` + +- [ ] **Step 1: Write tests for the new `getSendDelay` logic** + +Create `api/pkg/services/message_service_test.go`: + +```go +package services + +import ( + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/trace" +) + +func TestGetSendDelay_WithSendAt_ReturnsTimeUntil(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + sendAt := time.Now().UTC().Add(5 * time.Minute) + params := MessageSendParams{SendAt: &sendAt} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + // Should be approximately 5 minutes (within 2 seconds tolerance) + assert.InDelta(t, float64(5*time.Minute), float64(delay), float64(2*time.Second)) +} + +func TestGetSendDelay_WithSendAtInPast_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + sendAt := time.Now().UTC().Add(-5 * time.Minute) + params := MessageSendParams{SendAt: &sendAt} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_BulkIndex_RateBasedDelay(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 3} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + // 10 messages per minute = 6 seconds interval + delay := service.getSendDelay(logger, payload, params, 10) + + expected := time.Duration(3) * (time.Minute / time.Duration(10)) + assert.Equal(t, expected, delay) +} + +func TestGetSendDelay_BulkIndex_ZeroRate_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 5} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 0) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_IndexZero_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 0} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_NoSendAtNoIndex_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +// noopLogger implements telemetry.Logger for testing +type noopLogger struct{} + +var _ telemetry.Logger = (*noopLogger)(nil) + +func (l *noopLogger) Error(_ error) {} +func (l *noopLogger) WithService(_ string) telemetry.Logger { return l } +func (l *noopLogger) WithString(_, _ string) telemetry.Logger { return l } +func (l *noopLogger) WithSpan(_ trace.SpanContext) telemetry.Logger { return l } +func (l *noopLogger) Trace(_ string) {} +func (l *noopLogger) Info(_ string) {} +func (l *noopLogger) Warn(_ error) {} +func (l *noopLogger) Debug(_ string) {} +func (l *noopLogger) Fatal(_ error) {} +func (l *noopLogger) Printf(_ string, _ ...interface{}) {} +``` + +- [ ] **Step 2: Run the tests** + +Run: `cd api && go test -vet=off ./pkg/services/ -run TestGetSendDelay -v` +Expected: all pass + +- [ ] **Step 3: Commit** + +```bash +cd api && git add -A && git commit -m "test(services): add unit tests for getSendDelay rate-based logic" +``` + +--- + +## Task 9: Add Unit Test for ResolveScheduledAt (Existing, Verify No Regression) + +**Files:** + +- Create: `api/pkg/entities/send_schedule_test.go` + +- [ ] **Step 1: Write tests to lock existing ResolveScheduledAt behavior** + +Create `api/pkg/entities/send_schedule_test.go`: + +```go +package entities + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestResolveScheduledAt_NilSchedule_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + var schedule *MessageSendSchedule + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_InactiveSchedule_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + schedule := &MessageSendSchedule{IsActive: false} + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_NoWindows_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{}, + } + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_WithinWindow_ReturnsCurrentUTC(t *testing.T) { + // Wednesday at 10:00 UTC, window is Wed 9:00-17:00 (540-1020 minutes) + now := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC) // Wednesday + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{ + {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, + }, + } + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_BeforeWindow_ReturnsWindowStart(t *testing.T) { + // Wednesday at 7:00 UTC, window is Wed 9:00-17:00 + now := time.Date(2025, 1, 1, 7, 0, 0, 0, time.UTC) // Wednesday + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{ + {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, + }, + } + result := schedule.ResolveScheduledAt(now) + expected := time.Date(2025, 1, 1, 9, 0, 0, 0, time.UTC) + assert.Equal(t, expected, result) +} +``` + +- [ ] **Step 2: Run the tests** + +Run: `cd api && go test -vet=off ./pkg/entities/ -run TestResolveScheduledAt -v` +Expected: all pass + +- [ ] **Step 3: Commit** + +```bash +cd api && git add -A && git commit -m "test(entities): add regression tests for ResolveScheduledAt" +``` + +--- + +## Task 10: Final Build + Integration Verification + +**Files:** None (verification only) + +- [ ] **Step 1: Full build** + +Run: `cd api && go build ./...` +Expected: success + +- [ ] **Step 2: Full test suite** + +Run: `cd api && go test -vet=off ./...` +Expected: all pass + +- [ ] **Step 3: Generate Swagger docs (if API annotations changed)** + +The API request structs' annotations haven't changed for swagger (no new endpoints, `SendAt` already documented). Skip swagger regen unless compile errors appear. + +- [ ] **Step 4: Verify git status is clean** + +Run: `cd api && git status` +Expected: clean working tree + +--- + +## Notes + +- The `noopLogger` in tests implements the full `telemetry.Logger` interface (Error, WithService, WithString, WithSpan, Trace, Info, Warn, Debug, Fatal, Printf). +- The `ExactSendTime` field is transient — no database migrations needed. +- **Dedupe strategy**: `ScheduleExact` uses a `SELECT COUNT` check before insert. This is not fully race-proof but acceptable given: (a) Cloud Tasks at-least-once duplicates are rare, and (b) the existing `Schedule` path also has this same theoretical gap. Adding a DB unique constraint on `(message_id, status='pending')` would require a partial index migration — this is deferred as a future improvement if duplicates become a problem in practice. +- The existing `Schedule` method already handles concurrency via CockroachDB's serializable transactions (`crdbgorm.ExecuteTx`), which retries automatically on conflicts. No additional dedupe is added there. +- All existing behavior for single messages without `SendAt` is preserved (delay = 0, standard scheduling path). +- Past `SendAt` times are handled at both layers: `getSendDelay` returns 0 (immediate dispatch), and `Schedule` clamps `ScheduledAt` to `now` (no past timestamps persisted). From 425098818220370dbde28e5861d6cb87f2667c0e Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:31:44 +0300 Subject: [PATCH 107/381] feat(events): add ExactSendTime field to MessageAPISentPayload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/events/message_api_sent_event.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/pkg/events/message_api_sent_event.go b/api/pkg/events/message_api_sent_event.go index 60a418b23..e86c911e0 100644 --- a/api/pkg/events/message_api_sent_event.go +++ b/api/pkg/events/message_api_sent_event.go @@ -20,6 +20,7 @@ type MessageAPISentPayload struct { MaxSendAttempts uint `json:"max_send_attempts"` Contact string `json:"contact"` ScheduledSendTime *time.Time `json:"scheduled_send_time"` + ExactSendTime bool `json:"exact_send_time"` RequestReceivedAt time.Time `json:"request_received_at"` Content string `json:"content"` Attachments []string `json:"attachments"` From 5351ef97901150e63e2ab2fcf02e9ef4b36c7c83 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:33:32 +0300 Subject: [PATCH 108/381] feat(services): add rate-based dispatch delay and ExactSendTime to SendMessage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/services/message_service.go | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go index 131c35203..929998bdf 100644 --- a/api/pkg/services/message_service.go +++ b/api/pkg/services/message_service.go @@ -461,6 +461,7 @@ type MessageSendParams struct { RequestID *string UserID entities.UserID RequestReceivedAt time.Time + Index int } // SendMessage a new message @@ -470,7 +471,7 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe ctxLogger := service.tracer.CtxLogger(service.logger, span) - sendAttempts, sim := service.phoneSettings(ctx, params.UserID, phonenumbers.Format(params.Owner, phonenumbers.E164)) + sendAttempts, sim, messagesPerMinute := service.phoneSettings(ctx, params.UserID, phonenumbers.Format(params.Owner, phonenumbers.E164)) eventPayload := events.MessageAPISentPayload{ MessageID: uuid.New(), @@ -484,6 +485,7 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe Content: params.Content, Attachments: params.Attachments, ScheduledSendTime: params.SendAt, + ExactSendTime: params.SendAt != nil, SIM: sim, } @@ -500,7 +502,7 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) } - timeout := service.getSendDelay(ctxLogger, eventPayload, params.SendAt) + timeout := service.getSendDelay(ctxLogger, eventPayload, params, messagesPerMinute) if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, timeout); err != nil { msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()) return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) @@ -559,18 +561,24 @@ func (service *MessageService) RegisterMissedCall(ctx context.Context, params *M return message, err } -func (service *MessageService) getSendDelay(ctxLogger telemetry.Logger, eventPayload events.MessageAPISentPayload, sendAt *time.Time) time.Duration { - if sendAt == nil { - return time.Duration(0) +func (service *MessageService) getSendDelay(ctxLogger telemetry.Logger, eventPayload events.MessageAPISentPayload, params MessageSendParams, messagesPerMinute uint) time.Duration { + if params.SendAt != nil { + delay := params.SendAt.Sub(time.Now().UTC()) + if delay < 0 { + ctxLogger.Info(fmt.Sprintf("message [%s] has send time [%s] in the past. sending immediately", eventPayload.MessageID, params.SendAt.String())) + return time.Duration(0) + } + return delay } - delay := sendAt.Sub(time.Now().UTC()) - if delay < 0 { - ctxLogger.Info(fmt.Sprintf("message [%s] has send time [%s] in the past. sending immediately", eventPayload.MessageID, sendAt.String())) - return time.Duration(0) + if params.Index > 0 && messagesPerMinute > 0 { + interval := time.Minute / time.Duration(messagesPerMinute) + delay := time.Duration(params.Index) * interval + ctxLogger.Info(fmt.Sprintf("message [%s] bulk index [%d] rate-based delay [%s]", eventPayload.MessageID, params.Index, delay)) + return delay } - return delay + return time.Duration(0) } // StoreReceivedMessage a new message @@ -1011,7 +1019,7 @@ func (service *MessageService) SearchMessages(ctx context.Context, params *Messa return messages, nil } -func (service *MessageService) phoneSettings(ctx context.Context, userID entities.UserID, owner string) (uint, entities.SIM) { +func (service *MessageService) phoneSettings(ctx context.Context, userID entities.UserID, owner string) (uint, entities.SIM, uint) { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -1021,10 +1029,10 @@ func (service *MessageService) phoneSettings(ctx context.Context, userID entitie if err != nil { msg := fmt.Sprintf("cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner) ctxLogger.Error(stacktrace.Propagate(err, msg)) - return 2, entities.SIM1 + return 2, entities.SIM1, 0 } - return phone.MaxSendAttemptsSanitized(), phone.SIM + return phone.MaxSendAttemptsSanitized(), phone.SIM, phone.MessagesPerMinute } // storeSentMessage a new message From 42c6fc4819b519bcb56e34957e90d9f9ced35c91 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:35:26 +0300 Subject: [PATCH 109/381] test(entities): add regression tests for ResolveScheduledAt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/entities/send_schedule_test.go | 62 +++++++++++++++++++ .../gorm_phone_notification_repository.go | 35 +++++++++++ .../phone_notification_repository.go | 4 ++ 3 files changed, 101 insertions(+) create mode 100644 api/pkg/entities/send_schedule_test.go diff --git a/api/pkg/entities/send_schedule_test.go b/api/pkg/entities/send_schedule_test.go new file mode 100644 index 000000000..1480aa2fe --- /dev/null +++ b/api/pkg/entities/send_schedule_test.go @@ -0,0 +1,62 @@ +package entities + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestResolveScheduledAt_NilSchedule_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + var schedule *MessageSendSchedule + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_InactiveSchedule_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + schedule := &MessageSendSchedule{IsActive: false} + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_NoWindows_ReturnsCurrentUTC(t *testing.T) { + now := time.Now() + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{}, + } + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_WithinWindow_ReturnsCurrentUTC(t *testing.T) { + // Wednesday at 10:00 UTC, window is Wed 9:00-17:00 (540-1020 minutes) + now := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC) // Wednesday + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{ + {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, + }, + } + result := schedule.ResolveScheduledAt(now) + assert.Equal(t, now.UTC(), result) +} + +func TestResolveScheduledAt_BeforeWindow_ReturnsWindowStart(t *testing.T) { + // Wednesday at 7:00 UTC, window is Wed 9:00-17:00 + now := time.Date(2025, 1, 1, 7, 0, 0, 0, time.UTC) // Wednesday + schedule := &MessageSendSchedule{ + IsActive: true, + Timezone: "UTC", + Windows: []MessageSendScheduleWindow{ + {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, + }, + } + result := schedule.ResolveScheduledAt(now) + expected := time.Date(2025, 1, 1, 9, 0, 0, 0, time.UTC) + assert.Equal(t, expected, result) +} diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go index 8cc16f1eb..a36bac073 100644 --- a/api/pkg/repositories/gorm_phone_notification_repository.go +++ b/api/pkg/repositories/gorm_phone_notification_repository.go @@ -203,3 +203,38 @@ func (repository *gormPhoneNotificationRepository) insert( return nil } + +// ScheduleExact stores a phone notification with an exact ScheduledAt time. +// It performs a dedupe check — if a pending notification for the same message already exists, it's a no-op. +func (repository *gormPhoneNotificationRepository) ScheduleExact( + ctx context.Context, + notification *entities.PhoneNotification, +) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + // Dedupe: check if a pending notification for this message already exists + var count int64 + if err := repository.db.WithContext(ctx). + Model(&entities.PhoneNotification{}). + Where("message_id = ? AND status = ?", notification.MessageID, entities.PhoneNotificationStatusPending). + Count(&count).Error; err != nil { + return repository.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, "cannot check for existing notification for message [%s]", notification.MessageID), + ) + } + + if count > 0 { + return nil + } + + if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil { + return repository.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, "cannot create exact-time notification with id [%s]", notification.ID), + ) + } + + return nil +} diff --git a/api/pkg/repositories/phone_notification_repository.go b/api/pkg/repositories/phone_notification_repository.go index 9d93f0b3c..e8bedfe4c 100644 --- a/api/pkg/repositories/phone_notification_repository.go +++ b/api/pkg/repositories/phone_notification_repository.go @@ -13,6 +13,10 @@ type PhoneNotificationRepository interface { // Schedule a new entities.PhoneNotification Schedule(ctx context.Context, messagesPerMinute uint, schedule *entities.MessageSendSchedule, notification *entities.PhoneNotification) error + // ScheduleExact stores a phone notification with a fixed ScheduledAt time, + // bypassing rate-limit and schedule window logic. + ScheduleExact(ctx context.Context, notification *entities.PhoneNotification) error + // UpdateStatus of a notification UpdateStatus(ctx context.Context, notificationID uuid.UUID, status entities.PhoneNotificationStatus) error From 8fb277ffe37a6c2e462a39408cbde6f1429ac8a5 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:40:02 +0300 Subject: [PATCH 110/381] feat(handlers): replace 1s hack with rate-based delay for bulk send Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/handlers/message_handler.go | 5 ----- api/pkg/requests/message_bulk_send_request.go | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/api/pkg/handlers/message_handler.go b/api/pkg/handlers/message_handler.go index 935e9ba43..9504d5189 100644 --- a/api/pkg/handlers/message_handler.go +++ b/api/pkg/handlers/message_handler.go @@ -161,11 +161,6 @@ func (h *MessageHandler) BulkSend(c *fiber.Ctx) error { wg.Add(1) go func(message services.MessageSendParams, index int) { count.Add(1) - if message.SendAt == nil { - sentAt := time.Now().UTC().Add(time.Duration(index) * time.Second) - message.SendAt = &sentAt - } - response, err := h.service.SendMessage(ctx, message) if err != nil { count.Add(-1) diff --git a/api/pkg/requests/message_bulk_send_request.go b/api/pkg/requests/message_bulk_send_request.go index 461ac2edc..8c7d8025b 100644 --- a/api/pkg/requests/message_bulk_send_request.go +++ b/api/pkg/requests/message_bulk_send_request.go @@ -54,7 +54,6 @@ func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source var result []services.MessageSendParams for index, to := range input.To { - sendAt := time.Now().UTC().Add(time.Duration(index) * time.Second) result = append(result, services.MessageSendParams{ Source: source, Owner: from, @@ -63,9 +62,9 @@ func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source UserID: userID, RequestReceivedAt: time.Now().UTC(), Contact: to, - SendAt: &sendAt, Content: input.Content, Attachments: input.Attachments, + Index: index, }) } From 2a8b83ac29141500b7ae6c3a5119f3ffff0c22b2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:40:36 +0300 Subject: [PATCH 111/381] test(services): add unit tests for getSendDelay rate-based logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/services/message_service_test.go | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 api/pkg/services/message_service_test.go diff --git a/api/pkg/services/message_service_test.go b/api/pkg/services/message_service_test.go new file mode 100644 index 000000000..263816f41 --- /dev/null +++ b/api/pkg/services/message_service_test.go @@ -0,0 +1,105 @@ +package services + +import ( + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/trace" +) + +func TestGetSendDelay_WithSendAt_ReturnsTimeUntil(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + sendAt := time.Now().UTC().Add(5 * time.Minute) + params := MessageSendParams{SendAt: &sendAt} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + // Should be approximately 5 minutes (within 2 seconds tolerance) + assert.InDelta(t, float64(5*time.Minute), float64(delay), float64(2*time.Second)) +} + +func TestGetSendDelay_WithSendAtInPast_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + sendAt := time.Now().UTC().Add(-5 * time.Minute) + params := MessageSendParams{SendAt: &sendAt} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_BulkIndex_RateBasedDelay(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 3} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + // 10 messages per minute = 6 seconds interval + delay := service.getSendDelay(logger, payload, params, 10) + + expected := time.Duration(3) * (time.Minute / time.Duration(10)) + assert.Equal(t, expected, delay) +} + +func TestGetSendDelay_BulkIndex_ZeroRate_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 5} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 0) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_IndexZero_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{Index: 0} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +func TestGetSendDelay_NoSendAtNoIndex_ReturnsZero(t *testing.T) { + service := &MessageService{} + logger := &noopLogger{} + + params := MessageSendParams{} + payload := events.MessageAPISentPayload{MessageID: uuid.New()} + + delay := service.getSendDelay(logger, payload, params, 10) + + assert.Equal(t, time.Duration(0), delay) +} + +// noopLogger implements telemetry.Logger for testing +type noopLogger struct{} + +var _ telemetry.Logger = (*noopLogger)(nil) + +func (l *noopLogger) Error(_ error) {} +func (l *noopLogger) WithService(_ string) telemetry.Logger { return l } +func (l *noopLogger) WithString(_, _ string) telemetry.Logger { return l } +func (l *noopLogger) WithSpan(_ trace.SpanContext) telemetry.Logger { return l } +func (l *noopLogger) Trace(_ string) {} +func (l *noopLogger) Info(_ string) {} +func (l *noopLogger) Warn(_ error) {} +func (l *noopLogger) Debug(_ string) {} +func (l *noopLogger) Fatal(_ error) {} +func (l *noopLogger) Printf(_ string, _ ...interface{}) {} From 9af04e339f98e026f6ec53525a6092ddfb481d0c Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:40:54 +0300 Subject: [PATCH 112/381] feat(services): add ExactSendTime bypass in PhoneNotificationService.Schedule Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../services/phone_notification_service.go | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index ffafa555a..24db0de5b 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -161,14 +161,16 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone // PhoneNotificationScheduleParams are parameters for sending a notification type PhoneNotificationScheduleParams struct { - UserID entities.UserID - Owner string - Source string - Encrypted bool - Contact string - Content string - SIM entities.SIM - MessageID uuid.UUID + UserID entities.UserID + Owner string + Source string + Encrypted bool + Contact string + Content string + SIM entities.SIM + MessageID uuid.UUID + ExactSendTime bool + ScheduledSendTime *time.Time } // Schedule a notification to be sent to a phone @@ -195,6 +197,35 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P UpdatedAt: time.Now().UTC(), } + // Bypass rate-limit and schedule window logic for exact send time + if params.ExactSendTime && params.ScheduledSendTime != nil { + scheduledAt := *params.ScheduledSendTime + if scheduledAt.Before(time.Now().UTC()) { + scheduledAt = time.Now().UTC() + } + notification.ScheduledAt = scheduledAt + if err = service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { + msg := fmt.Sprintf("cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { + ctxLogger.Error(err) + } + + if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { + return service.tracer.WrapErrorSpan(span, err) + } + + ctxLogger.Info(fmt.Sprintf( + "message with id [%s] exact notification scheduled for [%s] with id [%s]", + params.MessageID, + notification.ScheduledAt, + notification.ID, + )) + return nil + } + var schedule *entities.MessageSendSchedule if phone.ScheduleID != nil { schedule, err = service.sendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) From adbcd2e36b583a597bbc2508960d0f3497b48037 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:40:54 +0300 Subject: [PATCH 113/381] feat(listeners): pass ExactSendTime to PhoneNotificationService from event Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../listeners/phone_notification_listener.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go index e1b3eef7e..bcb156128 100644 --- a/api/pkg/listeners/phone_notification_listener.go +++ b/api/pkg/listeners/phone_notification_listener.go @@ -53,14 +53,16 @@ func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context, } sendParams := &services.PhoneNotificationScheduleParams{ - UserID: payload.UserID, - Owner: payload.Owner, - Contact: payload.Contact, - Content: payload.Content, - SIM: payload.SIM, - Encrypted: payload.Encrypted, - Source: event.Source(), - MessageID: payload.MessageID, + UserID: payload.UserID, + Owner: payload.Owner, + Contact: payload.Contact, + Content: payload.Content, + SIM: payload.SIM, + Encrypted: payload.Encrypted, + Source: event.Source(), + MessageID: payload.MessageID, + ExactSendTime: payload.ExactSendTime, + ScheduledSendTime: payload.ScheduledSendTime, } if err := listener.service.Schedule(ctx, sendParams); err != nil { From af0e5d9a6ebd3d1a18b2353d29fb109e3cd055ab Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:41:23 +0300 Subject: [PATCH 114/381] docs: add integration test setup design spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...026-05-03-integration-test-setup-design.md | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-03-integration-test-setup-design.md diff --git a/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md b/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md new file mode 100644 index 000000000..1b4ced8e4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md @@ -0,0 +1,248 @@ +# Integration Test Setup for httpSMS API + +## Problem + +The httpSMS API has no integration tests that verify the full SMS send/receive flow end-to-end. We need a CI-gated integration test that runs the entire stack in Docker and validates the core message lifecycle before deploying the API. + +## Approach + +Run the full application stack (API + PostgreSQL + Redis) in Docker alongside an **emulator** service that acts as a fake Android phone. The emulator implements a fake FCM server endpoint so the API's Firebase messaging client sends push notifications to it (instead of Google). The emulator then responds with SENT/DELIVERED events, completing the SMS lifecycle. A Go test runner exercises the API externally and asserts on final message state. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Docker Compose (tests/docker-compose.yml) │ +│ │ +│ ┌──────────┐ ┌───────┐ ┌──────────────────────────┐ │ +│ │PostgreSQL│ │ Redis │ │ API (existing Dockerfile)│ │ +│ └──────────┘ └───────┘ └────────────┬─────────────┘ │ +│ │ FCM push │ +│ ▼ │ +│ ┌──────────────────────────┐ │ +│ │ Emulator (fake phone) │ │ +│ │ - Fake FCM server :9090 │ │ +│ │ - Fires SENT/DELIVERED │ │ +│ │ events back to API │ │ +│ └──────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + ▲ + │ HTTP calls (send SMS, get message, etc.) + │ +┌────────┴──────────┐ +│ Test Runner (Go) │ ← runs on host / in CI +│ go test ./... │ +└───────────────────┘ +``` + +## Components + +### 1. `tests/docker-compose.yml` + +Brings up the full stack: + +- **postgres** — Same as root `docker-compose.yml`, seeded with `tests/seed.sql` +- **redis** — Standard Redis +- **api** — Built from `api/Dockerfile`, configured with `FCM_ENDPOINT=http://emulator:9090` to redirect Firebase messaging to the emulator +- **emulator** — Built from `tests/emulator/Dockerfile`, receives FCM pushes and fires events back + +### 2. `tests/emulator/` (Go project) + +A lightweight Go HTTP server that: + +- Exposes `POST /v1/projects/{project}/messages:send` — mimics the FCM v1 API. Receives push notification payloads from the API's Firebase messaging client. +- Exposes `POST /token` — returns a fake OAuth2 access token (the Firebase SDK calls this before sending FCM). Response format: `{"access_token": "fake-token", "token_type": "Bearer", "expires_in": 3600}` +- Exposes `GET /health` — health check endpoint +- On receiving a push with `KEY_MESSAGE_ID` in the data payload: + 1. Calls `GET http://api:8000/v1/messages/outstanding?message_id={messageID}` (using phone API key) to fetch the message like a real phone would + 2. Waits a brief delay (e.g., 200ms) + 3. Calls `POST http://api:8000/v1/messages/{messageID}/events` with event `SENT` (using phone API key) + 4. Waits another brief delay (e.g., 200ms) + 5. Calls `POST http://api:8000/v1/messages/{messageID}/events` with event `DELIVERED` (using phone API key) +- All API calls authenticated with the seeded phone API key (`x-api-key` header) +- Asserts it received the correct FCM payload structure (path, data.KEY_MESSAGE_ID present) + +### 3. `tests/seed.sql` + +SQL script that runs on PostgreSQL startup to create: + +- A test user: `id='test-user-id'`, `email='test@httpsms.com'`, `api_key='test-user-api-key'`, `subscription_name='pro'` +- A system user (for event queue): `id='system-user-id'`, `api_key='system-user-api-key'` +- A phone: `id=`, `user_id='test-user-id'`, `phone_number='+18005550199'`, `fcm_token='fake-fcm-token'` +- A phone API key: `id=`, `user_id='test-user-id'`, `api_key='test-phone-api-key'`, `phone_numbers=['+18005550199']` + +### 4. API Modification — FCM Transport Override + +In `api/pkg/di/container.go`, modify `FirebaseMessagingClient()`: + +- When `FCM_ENDPOINT` env var is set, create the Firebase App with a custom HTTP client whose `Transport` rewrites request URLs from `https://fcm.googleapis.com` to the value of `FCM_ENDPOINT` +- This requires no changes to business logic — the messaging client works normally but routes traffic to the emulator +- The Firebase credentials must be a syntactically valid fake service account JSON with `token_uri` pointing to `http://emulator:9090/token` + +### 4b. `tests/.env.test` — API environment for tests + +```env +ENV=production +GCP_PROJECT_ID=httpsms-test +EVENTS_QUEUE_TYPE=emulator +EVENTS_QUEUE_NAME=events-local +EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events +EVENTS_QUEUE_USER_API_KEY=system-user-api-key +EVENTS_QUEUE_USER_ID=system-user-id +FCM_ENDPOINT=http://emulator:9090 +DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms +DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms +REDIS_URL=redis://@redis:6379 +APP_PORT=8000 +ENTITLEMENT_ENABLED=false +USE_HTTP_LOGGER=true +FIREBASE_CREDENTIALS= +``` + +### 5. `tests/integration_test.go` (Go test files) + +Go tests using the standard `testing` package + `testify` for assertions: + +**Test 1: Send SMS E2E** + +1. `POST /v1/messages/send` with `from=`, `to=+18005550100`, `content="Hello"` (using user API key `x-api-key` header) +2. Extract message ID from response +3. Poll `GET /v1/messages/{id}` every 200ms with max 15s timeout (using user API key) +4. Assert message status reaches `delivered` +5. Assert message events include both `SENT` and `DELIVERED` + +**Test 2: Receive SMS** + +1. `POST /v1/messages/receive` (using phone API key auth) with `from=+18005550100`, `to=+18005550199`, `content="Hi there"`, `sim="SIM1"`, `timestamp=` +2. Extract message ID from response +3. `GET /v1/messages/{id}` (using user API key auth) +4. Assert message exists with correct content, from, to fields +5. Assert status is `received` + +### 6. `.github/workflows/integration-test.yml` + +GitHub Actions workflow: + +```yaml +name: integration-test +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + integration-test: + runs-on: ubuntu-latest + steps: + - Checkout + - Docker Compose up (tests/docker-compose.yml) + - Wait for health checks (API + emulator) + - Run: cd tests && go test -v -timeout 120s ./... + - Docker Compose down + + deploy-api: + needs: integration-test + # existing deploy logic +``` + +The `deploy-api` job depends on `integration-test` passing. + +## FCM Redirect Implementation Detail + +The Firebase Admin Go SDK's messaging client sends HTTP POST requests to: + +``` +https://fcm.googleapis.com/v1/projects/{project_id}/messages:send +``` + +We intercept this by providing a custom `http.RoundTripper`: + +```go +type fcmRedirectTransport struct { + target string // e.g., "http://emulator:9090" + base http.RoundTripper +} + +func (t *fcmRedirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Rewrite: https://fcm.googleapis.com/... → http://emulator:9090/... + req.URL.Scheme = "http" + req.URL.Host = strings.TrimPrefix(t.target, "http://") + return t.base.RoundTrip(req) +} +``` + +This is injected via `option.WithHTTPClient()` when creating the Firebase App in the DI container. + +## Fake Firebase Credentials + +For the integration test environment, we provide a minimal fake service account JSON: + +```json +{ + "type": "service_account", + "project_id": "httpsms-test", + "private_key_id": "test", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n\n-----END RSA PRIVATE KEY-----\n", + "client_email": "test@httpsms-test.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "http://emulator:9090/token", + "auth_provider_x509_cert_url": "http://emulator:9090/certs", + "client_x509_cert_url": "http://emulator:9090/certs/test" +} +``` + +The emulator implements: + +- `POST /token` — Accepts JWT assertion grant, returns `{"access_token": "fake-token", "token_type": "Bearer", "expires_in": 3600}` +- Does NOT validate the JWT signature — just returns a valid token response + +## Docker Health Checks & Orchestration + +Services start in order with health dependencies: + +1. **postgres** — healthy when `pg_isready` passes +2. **redis** — healthy when accepting connections +3. **emulator** — healthy when `GET /health` returns 200 +4. **api** — starts after postgres+redis+emulator healthy, healthy when `GET /v1/` returns (or a dedicated health endpoint) + +Test runner waits for all services healthy before executing `go test`. + +## File Structure + +``` +tests/ +├── docker-compose.yml +├── seed.sql +├── go.mod +├── go.sum +├── integration_test.go +├── helpers_test.go # shared HTTP client, polling helpers +├── .env.test # env vars for the API in test mode +└── emulator/ + ├── Dockerfile + ├── go.mod + ├── go.sum + ├── main.go # entry point, starts HTTP server + ├── fcm_handler.go # fake FCM endpoint + ├── token_handler.go # fake OAuth2 token endpoint + └── events.go # fires SENT/DELIVERED events to API +``` + +## Key Design Decisions + +1. **DB seeding over Firebase Auth emulator** — Simpler, keeps focus on SMS flow testing. Auth is not what we're validating. +2. **Real FCM code path with redirected transport** — Tests the actual Firebase SDK integration, payload construction, and error handling. More confidence than a noop mock. +3. **Emulator as separate Go project** — Clean separation, own Dockerfile, own module. Doesn't pollute the API codebase. +4. **Test runner runs on host (not in Docker)** — Simpler debugging, standard `go test` output, easier CI integration. +5. **Polling with timeout for async assertions** — The send flow is async (event-driven). Polling with backoff is the pragmatic approach. + +## Out of Scope + +- Testing the web frontend +- Testing the Android app +- Load/performance testing +- Testing auth flows (login, registration) +- Testing billing/entitlements +- MMS/attachment testing (can be added later) From 1558ce6963f271a978c7882131e4c40d149f490a Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:41:40 +0300 Subject: [PATCH 115/381] feat(handlers): add per-phone index for CSV bulk messages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/handlers/bulk_message_handler.go | 15 ++++++++++++--- api/pkg/requests/bulk_message_request.go | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index c660eeaaf..16c833fe8 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -89,13 +89,22 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { wg := sync.WaitGroup{} count := atomic.Int64{} - for index, message := range messages { + // Compute per-phone index for rate-based dispatch delay + phoneIndexCounter := make(map[string]int) + + for _, message := range messages { wg.Add(1) + var perPhoneIndex int + if message.SendTime == nil { + perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] + phoneIndexCounter[message.FromPhoneNumber]++ + } + go func(message *requests.BulkMessage, index int) { count.Add(1) _, err = h.messageService.SendMessage( ctx, - message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL()), + message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index), ) if err != nil { count.Add(-1) @@ -103,7 +112,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { ctxLogger.Error(stacktrace.Propagate(err, msg)) } wg.Done() - }(message, index) + }(message, perPhoneIndex) } wg.Wait() diff --git a/api/pkg/requests/bulk_message_request.go b/api/pkg/requests/bulk_message_request.go index 77319997c..000ff016f 100644 --- a/api/pkg/requests/bulk_message_request.go +++ b/api/pkg/requests/bulk_message_request.go @@ -38,7 +38,7 @@ func (input *BulkMessage) Sanitize() *BulkMessage { } // ToMessageSendParams converts BulkMessage to services.MessageSendParams -func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string) services.MessageSendParams { +func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string, index int) services.MessageSendParams { from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) return services.MessageSendParams{ @@ -51,5 +51,6 @@ func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID Contact: input.sanitizeAddress(input.ToPhoneNumber), Content: input.Content, Attachments: input.removeEmptyStrings(strings.Split(input.AttachmentURLs, ",")), + Index: index, } } From 704355d9804b4e50a39b285dedaccdb0a52cd5c5 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 17:50:40 +0300 Subject: [PATCH 116/381] docs: add integration test implementation plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-05-03-integration-test-setup.md | 1107 +++++++++++++++++ 1 file changed, 1107 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-03-integration-test-setup.md diff --git a/docs/superpowers/plans/2026-05-03-integration-test-setup.md b/docs/superpowers/plans/2026-05-03-integration-test-setup.md new file mode 100644 index 000000000..9a8f8cb64 --- /dev/null +++ b/docs/superpowers/plans/2026-05-03-integration-test-setup.md @@ -0,0 +1,1107 @@ +# Integration Test Setup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a CI-gated integration test that validates the full SMS send/receive flow using Docker, a phone emulator, and real FCM code paths redirected to the emulator. + +**Architecture:** Docker Compose brings up PostgreSQL + Redis + API + Emulator. The API's Firebase SDK is configured to route FCM traffic to the emulator via a custom HTTP transport. A Go test runner on the host exercises the API and asserts on message state. + +**Tech Stack:** Go, Docker Compose, PostgreSQL, Redis, Firebase Admin Go SDK, GitHub Actions + +--- + +## File Structure + +``` +tests/ +├── docker-compose.yml # orchestrates all services +├── seed.sql # seeds test user, phone, API keys +├── .env.test # API environment config for tests +├── firebase-credentials.json # fake service account JSON +├── go.mod # test runner Go module +├── go.sum +├── integration_test.go # test cases (send SMS, receive SMS) +├── helpers_test.go # HTTP client, polling, constants +└── emulator/ + ├── Dockerfile # builds emulator binary + ├── go.mod # emulator Go module + ├── go.sum + ├── main.go # entry point, HTTP server setup + ├── fcm_handler.go # fake FCM endpoint handler + ├── token_handler.go # fake OAuth2 token endpoint + └── events.go # fires SENT/DELIVERED events to API + +api/pkg/di/container.go # modified: FCM transport redirect +.github/workflows/integration-test.yml # new CI workflow +``` + +--- + +### Task 1: Create Feature Branch + +**Files:** + +- None (git operations only) + +- [ ] **Step 1: Create and switch to feature branch from main** + +```bash +cd C:\Users\Arnold\Work\NdoleStudio\httpsms.com +git checkout main +git pull origin main +git checkout -b feature/integration-tests +``` + +- [ ] **Step 2: Verify branch** + +Run: `git branch --show-current` +Expected: `feature/integration-tests` + +--- + +### Task 2: API Modification — FCM Transport Override + +**Files:** + +- Modify: `api/pkg/di/container.go:396-405` (FirebaseApp method) + +- [ ] **Step 1: Add the FCM redirect transport and modify FirebaseApp** + +In `api/pkg/di/container.go`, modify the `FirebaseApp()` method to check for `FCM_ENDPOINT` env var. When set, use ONLY a custom HTTP client (no credentials). When not set, use credentials as before. + +**Important:** `option.WithHTTPClient()` takes precedence over all other options in the Firebase SDK. Do NOT combine it with `option.WithAuthCredentialsJSON()`. Use one or the other. + +Create a new file `api/pkg/di/fcm_transport.go`: + +```go +package di + +import ( + "net/http" + "net/url" +) + +// fcmRedirectTransport rewrites Firebase SDK HTTP requests to a custom endpoint. +// Used in integration tests to redirect FCM traffic to the emulator. +type fcmRedirectTransport struct { + target *url.URL + base http.RoundTripper +} + +func (t *fcmRedirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.URL.Scheme = t.target.Scheme + req.URL.Host = t.target.Host + return t.base.RoundTrip(req) +} +``` + +Then modify `FirebaseApp()` in `container.go`: + +```go +// FirebaseApp creates a new instance of firebase.App +func (container *Container) FirebaseApp() (app *firebase.App) { + container.logger.Debug(fmt.Sprintf("creating %T", app)) + + var opts []option.ClientOption + + if fcmEndpoint := os.Getenv("FCM_ENDPOINT"); fcmEndpoint != "" { + container.logger.Info(fmt.Sprintf("using FCM endpoint override: %s", fcmEndpoint)) + targetURL, err := url.Parse(fcmEndpoint) + if err != nil { + container.logger.Fatal(stacktrace.Propagate(err, "cannot parse FCM_ENDPOINT")) + } + opts = append(opts, option.WithHTTPClient(&http.Client{ + Transport: &fcmRedirectTransport{ + target: targetURL, + base: http.DefaultTransport, + }, + })) + } else { + opts = append(opts, option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials())) + } + + app, err := firebase.NewApp(context.Background(), nil, opts...) + if err != nil { + msg := "cannot initialize firebase application" + container.logger.Fatal(stacktrace.Propagate(err, msg)) + } + return app +} +``` + +- [ ] **Step 2: Add `net/url` import if not already present** + +Ensure the `net/url` package is imported in `container.go` (or the new file). + +- [ ] **Step 3: Verify API still builds** + +Run: `cd api && go build ./...` +Expected: Build succeeds with no errors. + +- [ ] **Step 4: Commit** + +```bash +git add api/pkg/di/ +git commit -m "feat(api): add FCM_ENDPOINT transport override for integration tests" +``` + +--- + +### Task 3: Emulator — Project Scaffolding + +**Files:** + +- Create: `tests/emulator/go.mod` +- Create: `tests/emulator/emulator.go` +- Create: `tests/emulator/Dockerfile` + +Note: `main.go` references `NewEmulator()` and handlers, so we create the struct first. `main.go` is created AFTER all handlers exist (Task 6b). + +- [ ] **Step 1: Initialize emulator Go module** + +```bash +mkdir -p tests/emulator +cd tests/emulator +go mod init github.com/NdoleStudio/httpsms/tests/emulator +``` + +- [ ] **Step 2: Create `tests/emulator/emulator.go`** + +```go +package main + +import "net/http" + +// Emulator acts as a fake Android phone that receives FCM pushes +// and responds with message events. +type Emulator struct { + apiBaseURL string + phoneAPIKey string + httpClient *http.Client +} + +// NewEmulator creates a new Emulator instance. +func NewEmulator(apiBaseURL, phoneAPIKey string) *Emulator { + return &Emulator{ + apiBaseURL: apiBaseURL, + phoneAPIKey: phoneAPIKey, + httpClient: &http.Client{}, + } +} + +// HealthHandler returns 200 OK for health checks. +func (e *Emulator) HealthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} +``` + +- [ ] **Step 3: Create `tests/emulator/Dockerfile`** + +```dockerfile +FROM golang:1.22 AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/emulator . + +FROM alpine:latest +RUN apk add --no-cache ca-certificates +COPY --from=builder /bin/emulator /bin/emulator +EXPOSE 9090 +ENTRYPOINT ["/bin/emulator"] +``` + +- [ ] **Step 4: Commit** + +```bash +git add tests/emulator/ +git commit -m "feat(tests): scaffold emulator Go project" +``` + +--- + +### Task 4: Emulator — Token Handler + +**Files:** + +- Create: `tests/emulator/token_handler.go` + +- [ ] **Step 1: Create `tests/emulator/token_handler.go`** + +```go +package main + +import ( + "encoding/json" + "net/http" +) + +// TokenHandler returns a fake OAuth2 access token. +// The Firebase Admin SDK calls this endpoint to get an access token +// before making FCM API calls. +func (e *Emulator) TokenHandler(w http.ResponseWriter, r *http.Request) { + response := map[string]interface{}{ + "access_token": "fake-access-token", + "token_type": "Bearer", + "expires_in": 3600, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add tests/emulator/token_handler.go +git commit -m "feat(tests): add fake OAuth2 token handler to emulator" +``` + +--- + +### Task 5: Emulator — FCM Handler + +**Files:** + +- Create: `tests/emulator/fcm_handler.go` + +- [ ] **Step 1: Create `tests/emulator/fcm_handler.go`** + +```go +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" +) + +// fcmRequest represents the FCM v1 API request body +type fcmRequest struct { + Message struct { + Data map[string]string `json:"data"` + Token string `json:"token"` + Android struct { + Priority string `json:"priority"` + } `json:"android"` + } `json:"message"` +} + +// fcmResponse represents the FCM v1 API response +type fcmResponse struct { + Name string `json:"name"` +} + +// FCMHandler handles fake FCM send requests from the Firebase Admin SDK. +func (e *Emulator) FCMHandler(w http.ResponseWriter, r *http.Request) { + var req fcmRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + messageID := req.Message.Data["KEY_MESSAGE_ID"] + if messageID == "" { + http.Error(w, "missing KEY_MESSAGE_ID in data", http.StatusBadRequest) + return + } + + log.Printf("received FCM push for message: %s", messageID) + + // Respond with success immediately (like real FCM would) + resp := fcmResponse{ + Name: fmt.Sprintf("projects/httpsms-test/messages/fake-%s", messageID), + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + + // Process the message asynchronously (like a real phone would) + go e.processMessage(messageID) +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add tests/emulator/emulator.go tests/emulator/fcm_handler.go +git commit -m "feat(tests): add FCM handler to emulator" +``` + +--- + +### Task 6: Emulator — Event Firing + +**Files:** + +- Create: `tests/emulator/events.go` + +- [ ] **Step 1: Create `tests/emulator/events.go`** + +```go +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "time" +) + +// messageEvent is the payload for posting a message event to the API +type messageEvent struct { + Timestamp time.Time `json:"timestamp"` + EventName string `json:"event_name"` +} + +// processMessage simulates a phone receiving an FCM push and sending the SMS. +// It calls /messages/outstanding, then fires SENT and DELIVERED events. +func (e *Emulator) processMessage(messageID string) { + // Step 1: Fetch outstanding message (like real phone does) + e.fetchOutstanding(messageID) + + // Step 2: Wait briefly then fire SENT + time.Sleep(200 * time.Millisecond) + if err := e.fireEvent(messageID, "SENT"); err != nil { + log.Printf("error firing SENT event for message %s: %v", messageID, err) + return + } + + // Step 3: Wait briefly then fire DELIVERED + time.Sleep(200 * time.Millisecond) + if err := e.fireEvent(messageID, "DELIVERED"); err != nil { + log.Printf("error firing DELIVERED event for message %s: %v", messageID, err) + return + } + + log.Printf("completed processing message: %s", messageID) +} + +// fetchOutstanding calls GET /v1/messages/outstanding to mimic the real phone behavior +func (e *Emulator) fetchOutstanding(messageID string) { + url := fmt.Sprintf("%s/v1/messages/outstanding?message_id=%s", e.apiBaseURL, messageID) + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("x-api-key", e.phoneAPIKey) + + resp, err := e.httpClient.Do(req) + if err != nil { + log.Printf("error fetching outstanding message %s: %v", messageID, err) + return + } + defer resp.Body.Close() + log.Printf("fetched outstanding message %s: status %d", messageID, resp.StatusCode) +} + +// fireEvent posts a message event (SENT or DELIVERED) to the API +func (e *Emulator) fireEvent(messageID, eventName string) error { + url := fmt.Sprintf("%s/v1/messages/%s/events", e.apiBaseURL, messageID) + + event := messageEvent{ + Timestamp: time.Now().UTC(), + EventName: eventName, + } + + body, _ := json.Marshal(event) + req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", e.phoneAPIKey) + + resp, err := e.httpClient.Do(req) + if err != nil { + return fmt.Errorf("HTTP error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf("API returned status %d for %s event", resp.StatusCode, eventName) + } + + log.Printf("fired %s event for message %s: status %d", eventName, messageID, resp.StatusCode) + return nil +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add tests/emulator/events.go +git commit -m "feat(tests): add event firing to emulator" +``` + +--- + +### Task 6b: Emulator — Main Entry Point + +**Files:** + +- Create: `tests/emulator/main.go` + +- [ ] **Step 1: Create `tests/emulator/main.go`** + +Now that all handlers exist (HealthHandler, TokenHandler, FCMHandler), create the entry point: + +```go +package main + +import ( + "log" + "net/http" + "os" +) + +func main() { + apiBaseURL := os.Getenv("API_BASE_URL") + if apiBaseURL == "" { + apiBaseURL = "http://api:8000" + } + + phoneAPIKey := os.Getenv("PHONE_API_KEY") + if phoneAPIKey == "" { + phoneAPIKey = "pk_test-phone-api-key" + } + + emulator := NewEmulator(apiBaseURL, phoneAPIKey) + + mux := http.NewServeMux() + mux.HandleFunc("GET /health", emulator.HealthHandler) + mux.HandleFunc("POST /token", emulator.TokenHandler) + mux.HandleFunc("POST /v1/projects/{project}/messages:send", emulator.FCMHandler) + + port := os.Getenv("PORT") + if port == "" { + port = "9090" + } + + log.Printf("emulator listening on :%s", port) + if err := http.ListenAndServe(":"+port, mux); err != nil { + log.Fatalf("server error: %v", err) + } +} +``` + +- [ ] **Step 2: Verify emulator builds** + +```bash +cd tests/emulator +go build ./... +``` + +Expected: Build succeeds. + +- [ ] **Step 3: Commit** + +```bash +git add tests/emulator/main.go +git commit -m "feat(tests): add emulator main entry point" +``` + +--- + +### Task 7: Test Infrastructure — Seed Data & Config + +**Files:** + +- Create: `tests/seed.sql` +- Create: `tests/.env.test` +- Create: `tests/firebase-credentials.json` + +- [ ] **Step 1: Create `tests/seed.sql`** + +This script must match the exact table schema from entities. The tables are auto-migrated by GORM, so we insert after API startup. Actually — since we need the user to exist BEFORE the API processes requests, we seed via Docker's postgres init scripts. + +Note: GORM auto-migrates tables on API startup. The seed SQL runs AFTER table creation. We use a Docker healthcheck + depends_on to ensure ordering. Alternatively, we can use a startup script that waits for the API to be ready, then seeds. The simplest approach: mount `seed.sql` as a Postgres init script — but that runs before GORM migrates. + +**Better approach:** Create a `tests/seed.sh` script that waits for the API to start (which runs GORM migrations), then seeds the database via `psql`. + +```sql +-- tests/seed.sql +-- Seed test data for integration tests +-- Run AFTER GORM has migrated the schema (i.e., after API starts) + +-- Test user +INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) +VALUES ( + 'test-user-id', + 'test@httpsms.com', + 'test-user-api-key', + 'UTC', + 'pro-monthly', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + +-- System user (for event queue auth) +INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) +VALUES ( + 'system-user-id', + 'system@httpsms.com', + 'system-user-api-key', + 'UTC', + 'pro-monthly', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + +-- Test phone +INSERT INTO phones (id, user_id, fcm_token, phone_number, messages_per_minute, sim, max_send_attempts, message_expiration_seconds, created_at, updated_at) +VALUES ( + 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + 'test-user-id', + 'fake-fcm-token', + '+18005550199', + 60, + 'SIM1', + 2, + 600, + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + +-- Phone API key (for emulator to authenticate as phone) +INSERT INTO phone_api_keys (id, name, user_id, user_email, phone_numbers, phone_ids, api_key, created_at, updated_at) +VALUES ( + 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + 'Integration Test Phone Key', + 'test-user-id', + 'test@httpsms.com', + '{"+18005550199"}', + '{"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}', + 'pk_test-phone-api-key', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; +``` + +- [ ] **Step 2: Create `tests/.env.test`** + +```env +ENV=production +GCP_PROJECT_ID=httpsms-test +USE_HTTP_LOGGER=true +ENTITLEMENT_ENABLED=false +EVENTS_QUEUE_TYPE=emulator +EVENTS_QUEUE_NAME=events-local +EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events +EVENTS_QUEUE_USER_API_KEY=system-user-api-key +EVENTS_QUEUE_USER_ID=system-user-id +FCM_ENDPOINT=http://emulator:9090 +DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms +DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms +REDIS_URL=redis://@redis:6379 +APP_PORT=8000 +APP_NAME=httpSMS +APP_URL=http://localhost:8000 +SWAGGER_HOST=localhost:8000 +SMTP_FROM_NAME=httpSMS +SMTP_FROM_EMAIL=test@httpsms.com +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_HOST=localhost +SMTP_PORT=2525 +PUSHER_APP_ID= +PUSHER_KEY= +PUSHER_SECRET= +PUSHER_CLUSTER= +GCS_BUCKET_NAME= +UPTRACE_DSN= +CLOUDFLARE_TURNSTILE_SECRET_KEY= +``` + +- [ ] **Step 3: Create `tests/firebase-credentials.json`** + +Generate an RSA private key for the fake service account. This must be a valid RSA key so the Firebase SDK can sign JWT tokens (even though the emulator won't validate them). + +```bash +cd tests +openssl genrsa -out /tmp/test-key.pem 2048 +``` + +Then create the JSON file with the key embedded: + +```json +{ + "type": "service_account", + "project_id": "httpsms-test", + "private_key_id": "test-key-id", + "private_key": "", + "client_email": "test@httpsms-test.iam.gserviceaccount.com", + "client_id": "123456789", + "auth_uri": "http://emulator:9090/auth", + "token_uri": "http://emulator:9090/token", + "auth_provider_x509_cert_url": "http://emulator:9090/certs", + "client_x509_cert_url": "http://emulator:9090/certs/test" +} +``` + +Note: The `FIREBASE_CREDENTIALS` env var in `.env.test` should be set to the full contents of this JSON file (single-line). The docker-compose will handle this. + +- [ ] **Step 4: Commit** + +```bash +git add tests/seed.sql tests/.env.test tests/firebase-credentials.json +git commit -m "feat(tests): add seed data and test environment config" +``` + +--- + +### Task 8: Docker Compose for Tests + +**Files:** + +- Create: `tests/docker-compose.yml` + +- [ ] **Step 1: Create `tests/docker-compose.yml`** + +```yaml +services: + postgres: + image: postgres:alpine + environment: + POSTGRES_DB: httpsms + POSTGRES_PASSWORD: dbpassword + POSTGRES_USER: dbusername + ports: + - "5435:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dbusername -d httpsms"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s + + redis: + image: redis:latest + command: redis-server + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + emulator: + build: + context: ./emulator + ports: + - "9090:9090" + environment: + API_BASE_URL: http://api:8000 + PHONE_API_KEY: pk_test-phone-api-key + PORT: "9090" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/health"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: ../api + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + emulator: + condition: service_healthy + env_file: + - .env.test + environment: + FIREBASE_CREDENTIALS: "${FIREBASE_CREDENTIALS}" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/"] + interval: 5s + timeout: 10s + retries: 20 + start_period: 30s + + seed: + image: postgres:alpine + depends_on: + api: + condition: service_healthy + environment: + PGPASSWORD: dbpassword + volumes: + - ./seed.sql:/seed.sql:ro + entrypoint: + [ + "psql", + "-h", + "postgres", + "-U", + "dbusername", + "-d", + "httpsms", + "-f", + "/seed.sql", + ] + restart: "no" +``` + +- [ ] **Step 2: Commit** + +```bash +git add tests/docker-compose.yml +git commit -m "feat(tests): add docker-compose for integration test stack" +``` + +--- + +### Task 9: Test Runner — Go Module & Helpers + +**Files:** + +- Create: `tests/go.mod` +- Create: `tests/helpers_test.go` + +- [ ] **Step 1: Initialize test runner Go module** + +```bash +cd tests +go mod init github.com/NdoleStudio/httpsms/tests +go get github.com/stretchr/testify +``` + +- [ ] **Step 2: Create `tests/helpers_test.go`** + +```go +package tests + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + apiBaseURL = "http://localhost:8000" + userAPIKey = "test-user-api-key" + phoneAPIKey = "pk_test-phone-api-key" + testPhone = "+18005550199" + testContact = "+18005550100" +) + +// apiClient returns an HTTP client configured for API calls +func apiClient() *http.Client { + return &http.Client{Timeout: 10 * time.Second} +} + +// doRequest performs an HTTP request with the given API key +func doRequest(t *testing.T, method, url string, body io.Reader, apiKey string) *http.Response { + t.Helper() + req, err := http.NewRequest(method, url, body) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", apiKey) + + resp, err := apiClient().Do(req) + require.NoError(t, err) + return resp +} + +// pollMessageStatus polls GET /v1/messages/{id} until the message reaches the target status or times out +func pollMessageStatus(t *testing.T, messageID, targetStatus string, timeout time.Duration) map[string]interface{} { + t.Helper() + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + url := fmt.Sprintf("%s/v1/messages/%s", apiBaseURL, messageID) + resp := doRequest(t, "GET", url, nil, userAPIKey) + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + require.NoError(t, err) + + if resp.StatusCode == http.StatusOK { + var result map[string]interface{} + require.NoError(t, json.Unmarshal(body, &result)) + + data, ok := result["data"].(map[string]interface{}) + if ok && data["status"] == targetStatus { + return data + } + } + + time.Sleep(200 * time.Millisecond) + } + + t.Fatalf("message %s did not reach status %q within %v", messageID, targetStatus, timeout) + return nil +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add tests/go.mod tests/go.sum tests/helpers_test.go +git commit -m "feat(tests): add test runner module and helpers" +``` + +--- + +### Task 10: Test Runner — Integration Tests + +**Files:** + +- Create: `tests/integration_test.go` + +- [ ] **Step 1: Create `tests/integration_test.go`** + +```go +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSendSMS_E2E(t *testing.T) { + // Step 1: Send an SMS via the API + sendPayload := map[string]interface{}{ + "from": testPhone, + "to": testContact, + "content": "Hello from integration test", + } + body, _ := json.Marshal(sendPayload) + + url := fmt.Sprintf("%s/v1/messages/send", apiBaseURL) + resp := doRequest(t, "POST", url, bytes.NewReader(body), userAPIKey) + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "send response: %s", string(respBody)) + + // Step 2: Extract message ID + var sendResult map[string]interface{} + require.NoError(t, json.Unmarshal(respBody, &sendResult)) + data := sendResult["data"].(map[string]interface{}) + messageID := data["id"].(string) + require.NotEmpty(t, messageID) + + t.Logf("sent message with ID: %s", messageID) + + // Step 3: Poll until message is delivered + message := pollMessageStatus(t, messageID, "delivered", 15*time.Second) + + // Step 4: Assert final state + assert.Equal(t, "delivered", message["status"]) + assert.Equal(t, testPhone, message["owner"]) + assert.Equal(t, testContact, message["contact"]) + assert.Equal(t, "Hello from integration test", message["content"]) +} + +func TestReceiveSMS_E2E(t *testing.T) { + // Step 1: Simulate receiving an SMS (phone -> API) + receivePayload := map[string]interface{}{ + "from": testContact, + "to": testPhone, + "content": "Hi there from integration test", + "encrypted": false, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + } + body, _ := json.Marshal(receivePayload) + + url := fmt.Sprintf("%s/v1/messages/receive", apiBaseURL) + resp := doRequest(t, "POST", url, bytes.NewReader(body), phoneAPIKey) + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "receive response: %s", string(respBody)) + + // Step 2: Extract message ID + var receiveResult map[string]interface{} + require.NoError(t, json.Unmarshal(respBody, &receiveResult)) + data := receiveResult["data"].(map[string]interface{}) + messageID := data["id"].(string) + require.NotEmpty(t, messageID) + + t.Logf("received message with ID: %s", messageID) + + // Step 3: Verify message exists via GET + getURL := fmt.Sprintf("%s/v1/messages/%s", apiBaseURL, messageID) + getResp := doRequest(t, "GET", getURL, nil, userAPIKey) + defer getResp.Body.Close() + + getBody, err := io.ReadAll(getResp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, getResp.StatusCode) + + var getMessage map[string]interface{} + require.NoError(t, json.Unmarshal(getBody, &getMessage)) + messageData := getMessage["data"].(map[string]interface{}) + + // Step 4: Assert message fields + assert.Equal(t, "received", messageData["status"]) + assert.Equal(t, testPhone, messageData["owner"]) + assert.Equal(t, testContact, messageData["contact"]) + assert.Equal(t, "Hi there from integration test", messageData["content"]) +} +``` + +- [ ] **Step 2: Verify test file compiles** + +```bash +cd tests +go vet ./... +``` + +Expected: No errors (tests won't pass yet without the stack running). + +- [ ] **Step 3: Commit** + +```bash +git add tests/integration_test.go +git commit -m "feat(tests): add send and receive SMS integration tests" +``` + +--- + +### Task 11: GitHub Actions Workflow + +**Files:** + +- Create: `.github/workflows/integration-test.yml` + +- [ ] **Step 1: Create `.github/workflows/integration-test.yml`** + +```yaml +name: integration-test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + integration-test: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎 + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Load Firebase credentials + run: | + echo "FIREBASE_CREDENTIALS=$(jq -c . tests/firebase-credentials.json)" >> $GITHUB_ENV + + - name: Start services 🐳 + working-directory: ./tests + run: docker compose up -d --build --wait + + - name: Wait for seed to complete + working-directory: ./tests + run: | + echo "Waiting for seed container to finish..." + docker compose wait seed || true + sleep 2 + + - name: Run integration tests 🧪 + working-directory: ./tests + run: go test -v -timeout 120s ./... + + - name: Collect logs on failure 📋 + if: failure() + working-directory: ./tests + run: | + docker compose logs api + docker compose logs emulator + + - name: Stop services 🛑 + if: always() + working-directory: ./tests + run: docker compose down -v +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/integration-test.yml +git commit -m "ci: add integration test workflow" +``` + +--- + +### Task 12: Local End-to-End Verification + +**Files:** + +- None (verification only) + +- [ ] **Step 1: Generate the fake Firebase credentials file** + +```bash +cd tests +openssl genrsa 2048 > /tmp/test-key.pem +# Create firebase-credentials.json with the key (use a script or manually format) +``` + +- [ ] **Step 2: Build and start the stack** + +```bash +cd tests +export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) +docker compose up -d --build +``` + +- [ ] **Step 3: Wait for all services to be healthy** + +```bash +docker compose ps +# All services should show "healthy" or "exited (0)" for seed +``` + +- [ ] **Step 4: Run the tests** + +```bash +cd tests +go test -v -timeout 120s ./... +``` + +Expected: Both tests pass. + +- [ ] **Step 5: Tear down** + +```bash +docker compose down -v +``` + +- [ ] **Step 6: Push branch and create PR** + +```bash +git push -u origin feature/integration-tests +gh pr create --title "feat: add integration test setup for API" --body "Adds E2E integration tests that validate the full SMS send/receive flow using Docker and a phone emulator." +``` From fe05df41d0d108c29ebd0934a18213f0b7bd12fa Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 23:04:20 +0300 Subject: [PATCH 117/381] docs: add outgoing message queue technical documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- outgoing-message-queue.md | 173 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 outgoing-message-queue.md diff --git a/outgoing-message-queue.md b/outgoing-message-queue.md new file mode 100644 index 000000000..0dfed24f9 --- /dev/null +++ b/outgoing-message-queue.md @@ -0,0 +1,173 @@ +# Outgoing Message Queue + +Complete guide on how httpSMS queues outgoing SMS messages for reliable delivery, including rate-based dispatch, scheduled sending, and send schedule windows. + +## How the Message Queue Works + +When you send an SMS through httpSMS (via the API, bulk send, or Excel upload), messages don't go directly to your Android phone. Instead, they enter an **outgoing message queue** that intelligently schedules delivery to ensure reliability and prevent carrier throttling. + +The queue determines **when** each message is dispatched to your phone based on three factors: + +1. **Explicit send time** — If you specify a `send_at` time, the message is sent at exactly that time +2. **Rate-based dispatch delay** — Messages without a send time are spaced out based on your configured send rate +3. **Send schedule window** — Messages can be held until your configured active hours (if enabled) + +## 1. Explicit Send Time (Bypass Queue Logic) + +When you specify a `send_at` time in your API request or a `SendTime` column in your Excel upload, the message **bypasses** both rate-limiting and schedule window logic entirely. The message will be dispatched to your phone at exactly the time you specified. + +This is ideal for: + +- Time-sensitive alerts that must go out at a precise moment +- Promotional messages timed for a specific campaign window +- Appointment reminders scheduled for a specific time before the appointment + +### Sending a single message at a specific time + +```bash +curl -L \ + --request POST \ + --url 'https://api.httpsms.com/v1/messages/send' \ + --header 'Content-Type: application/json' \ + --header 'x-api-Key: YOUR_API_KEY' \ + --data '{ + "from": "+18005550199", + "to": "+18005550100", + "content": "Your appointment is in 1 hour", + "send_at": "2025-12-19T16:39:57-08:00" + }' +``` + +The `send_at` field accepts time in [RFC 3339 format](https://datatracker.ietf.org/doc/html/rfc3339) which includes the time zone (e.g., `1996-12-19T16:39:57-08:00`). You can schedule messages up to 20 days (480 hours) in the future. + +> **Note:** If you specify a `send_at` time that is in the past, the message will be sent immediately. + +### Setting send time in bulk Excel uploads + +When using the [bulk messages Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx), you can set the optional `SendTime(optional)` column to specify when each message should be sent. Use the format `YYYY-MM-DDTHH:MM:SS` in your local time zone (e.g., `2023-11-13T02:10:01`). + +Each row with a `SendTime` value will be dispatched at exactly that time, independent of other messages in the batch. + +## 2. Rate-Based Dispatch Delay + +When you send messages **without** a `send_at` time (especially in bulk), httpSMS automatically spaces out delivery based on your phone's configured **Messages Per Minute** rate. This prevents carrier throttling and ensures reliable delivery. + +### How rate-based dispatch works + +The system calculates a dispatch delay for each message based on its position in the batch: + +``` +interval = 60 seconds ÷ messages_per_minute +delay = message_index × interval +``` + +**Example:** If your phone is configured for 10 messages per minute: + +| Message | Index | Delay | Dispatched At | +| ------- | ----- | ----- | ------------- | +| 1st | 0 | 0s | Immediately | +| 2nd | 1 | 6s | +6 seconds | +| 3rd | 2 | 12s | +12 seconds | +| 4th | 3 | 18s | +18 seconds | +| 10th | 9 | 54s | +54 seconds | + +This ensures your phone sends at most 10 SMS per minute, matching the configured rate. + +### Per-phone indexing for bulk sends + +When sending bulk messages to multiple recipients from the same phone number, the index is calculated per phone. This means messages to different recipient numbers are all spaced according to the sending phone's rate, ensuring the sending phone isn't overwhelmed. + +When using Excel/CSV uploads with multiple sender phones (different `From` numbers), each phone gets its own independent index counter. Messages from Phone A don't affect the timing of messages from Phone B. + +### Configuring Messages Per Minute + +To modify the send rate for your phone number: + +1. Go to [https://httpsms.com/settings](https://httpsms.com/settings#phones) +2. Tap the **"EDIT"** button on the phone number +3. Update the **"Messages Per Minute"** value + +**Default:** 10 messages per minute for newly registered phones. + +**Maximum:** 29 messages per minute (the [maximum permitted by an unrooted Android phone](https://android.googlesource.com/platform/frameworks/opt/telephony/+/master/src/java/com/android/internal/telephony/SmsUsageMonitor.java#84)). + +> **Tip:** If you're sending large batches, a lower rate (5-10/min) is more reliable. Higher rates (20+/min) may trigger carrier spam filters depending on your region. + +## 3. Send Schedule Window + +The send schedule window allows you to restrict message delivery to specific hours of the day. When enabled, messages sent outside the configured window are held in the queue and dispatched when the next window opens. + +This is useful for: + +- Respecting recipient quiet hours (no messages at 3 AM) +- Complying with regional messaging regulations +- Concentrating delivery during business hours + +> **Important:** Messages with an explicit `send_at` time bypass the send schedule window entirely. Only messages without a specified send time are subject to window restrictions. + +### Configuring the Send Schedule + +You can configure the send schedule window for each phone number in your account settings at [https://httpsms.com/settings](https://httpsms.com/settings#phones). Click **"EDIT"** on the phone number and set: + +- **Schedule Active** — Enable or disable the schedule window +- **Start Time** — The time of day when sending begins (e.g., `08:00`) +- **End Time** — The time of day when sending stops (e.g., `21:00`) +- **Timezone** — The timezone for the schedule (e.g., `America/New_York`) + +### How the schedule window works + +| Current Time vs Window | Behavior | +| ---------------------- | ------------------------------------------------------ | +| Within window | Message dispatched immediately (subject to rate delay) | +| Before window opens | Message held until window start time | +| After window closes | Message held until next day's window start time | + +## Bulk Send via API + +When sending to multiple recipients using the bulk API endpoint, all messages are automatically queued with rate-based dispatch delays: + +```bash +curl -L \ + --request POST \ + --url 'https://api.httpsms.com/v1/messages/bulk-send' \ + --header 'Content-Type: application/json' \ + --header 'x-api-Key: YOUR_API_KEY' \ + --data '{ + "from": "+18005550199", + "to": ["+18005550100", "+18005550101", "+18005550102"], + "content": "Hello from httpSMS!" + }' +``` + +In this example, with a default rate of 10 messages/minute: + +- Message to `+18005550100` → sent immediately +- Message to `+18005550101` → sent after 6 seconds +- Message to `+18005550102` → sent after 12 seconds + +## Summary: Queue Decision Flow + +``` +Message received by httpSMS API + │ + ├── Has explicit `send_at` time? + │ │ + │ YES → Dispatch at exactly that time + │ (bypasses rate-limit AND schedule window) + │ + └── No `send_at` time + │ + ├── Calculate rate-based delay + │ (index × 60s ÷ messages_per_minute) + │ + └── Apply send schedule window + (hold until window opens if outside active hours) +``` + +## Key Points + +- **Explicit send time always wins** — Setting `send_at` bypasses all queue logic +- **Rate limiting prevents throttling** — Messages are spaced based on your configured rate +- **Schedule windows respect quiet hours** — Messages without a send time are held until the window opens +- **Per-phone independence** — Each sending phone has its own rate counter and schedule +- **Past send times are handled gracefully** — If `send_at` is in the past, the message sends immediately From 87de540b583cf522379aa7fbe07b37eac40f66a2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 23:05:58 +0300 Subject: [PATCH 118/381] docs: use mermaid diagram for queue decision flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- outgoing-message-queue.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/outgoing-message-queue.md b/outgoing-message-queue.md index 0dfed24f9..0e8ee1444 100644 --- a/outgoing-message-queue.md +++ b/outgoing-message-queue.md @@ -147,21 +147,19 @@ In this example, with a default rate of 10 messages/minute: ## Summary: Queue Decision Flow -``` -Message received by httpSMS API - │ - ├── Has explicit `send_at` time? - │ │ - │ YES → Dispatch at exactly that time - │ (bypasses rate-limit AND schedule window) - │ - └── No `send_at` time - │ - ├── Calculate rate-based delay - │ (index × 60s ÷ messages_per_minute) - │ - └── Apply send schedule window - (hold until window opens if outside active hours) +```mermaid +flowchart TD + A[Message received by httpSMS API] --> B{Has explicit send_at time?} + B -->|YES| C[Dispatch at exactly that time] + C --> D[Bypasses rate-limit AND schedule window] + B -->|NO| E[Calculate rate-based delay] + E --> F["delay = index × (60s ÷ messages_per_minute)"] + F --> G{Send schedule window enabled?} + G -->|YES| H{Within active window?} + G -->|NO| I[Dispatch with rate delay only] + H -->|YES| I + H -->|NO| J[Hold until window opens] + J --> I ``` ## Key Points From 1840217002c22d664a547d97a4d6fe008b0528b7 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 23:21:17 +0300 Subject: [PATCH 119/381] refactor: rename send_schedule to message_send_schedule for consistency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/docs/docs.go | 146 +- api/docs/swagger.json | 9743 ++++++++--------- api/docs/swagger.yaml | 2591 ++--- api/pkg/di/container.go | 62 +- ...d_schedule.go => message_send_schedule.go} | 0 ..._test.go => message_send_schedule_test.go} | 0 ...er.go => message_send_schedule_handler.go} | 44 +- ...r.go => message_send_schedule_listener.go} | 20 +- ... gorm_message_send_schedule_repository.go} | 28 +- ...go => message_send_schedule_repository.go} | 4 +- .../message_send_schedule_store_request.go | 52 + .../requests/send_schedule_store_request.go | 48 - .../message_send_schedule_responses.go | 15 + api/pkg/responses/send_schedule_responses.go | 13 - ...ce.go => message_send_schedule_service.go} | 40 +- .../services/phone_notification_service.go | 32 +- ...essage_send_schedule_handler_validator.go} | 44 +- 17 files changed, 6228 insertions(+), 6654 deletions(-) rename api/pkg/entities/{send_schedule.go => message_send_schedule.go} (100%) rename api/pkg/entities/{send_schedule_test.go => message_send_schedule_test.go} (100%) rename api/pkg/handlers/{send_schedule_handler.go => message_send_schedule_handler.go} (83%) rename api/pkg/listeners/{send_schedule_listener.go => message_send_schedule_listener.go} (68%) rename api/pkg/repositories/{gorm_send_schedule_repository.go => gorm_message_send_schedule_repository.go} (82%) rename api/pkg/repositories/{send_schedule_repository.go => message_send_schedule_repository.go} (89%) create mode 100644 api/pkg/requests/message_send_schedule_store_request.go delete mode 100644 api/pkg/requests/send_schedule_store_request.go create mode 100644 api/pkg/responses/message_send_schedule_responses.go delete mode 100644 api/pkg/responses/send_schedule_responses.go rename api/pkg/services/{send_schedule_service.go => message_send_schedule_service.go} (78%) rename api/pkg/validators/{send_schedule_handler_validator.go => message_send_schedule_handler_validator.go} (69%) diff --git a/api/docs/docs.go b/api/docs/docs.go index 934cd2824..018614faf 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -2242,7 +2242,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.SendScheduleStore" + "$ref": "#/definitions/requests.MessageSendScheduleStore" } } ], @@ -2250,7 +2250,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/responses.SendScheduleResponse" + "$ref": "#/definitions/responses.MessageSendScheduleResponse" } }, "400": { @@ -2265,6 +2265,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.Unauthorized" } }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -2312,7 +2318,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.SendScheduleStore" + "$ref": "#/definitions/requests.MessageSendScheduleStore" } } ], @@ -2320,7 +2326,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.SendScheduleResponse" + "$ref": "#/definitions/responses.MessageSendScheduleResponse" } }, "400": { @@ -4246,6 +4252,51 @@ const docTemplate = `{ } } }, + "requests.MessageSendScheduleStore": { + "type": "object", + "required": [ + "is_active", + "name", + "timezone", + "windows" + ], + "properties": { + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.MessageSendScheduleWindow" + } + } + } + }, + "requests.MessageSendScheduleWindow": { + "type": "object", + "required": [ + "day_of_week", + "end_minute", + "start_minute" + ], + "properties": { + "day_of_week": { + "type": "integer" + }, + "end_minute": { + "type": "integer" + }, + "start_minute": { + "type": "integer" + } + } + }, "requests.MessageThreadUpdate": { "type": "object", "required": [ @@ -4343,51 +4394,6 @@ const docTemplate = `{ } } }, - "requests.SendScheduleStore": { - "type": "object", - "required": [ - "is_active", - "name", - "timezone", - "windows" - ], - "properties": { - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "timezone": { - "type": "string" - }, - "windows": { - "type": "array", - "items": { - "$ref": "#/definitions/requests.SendScheduleWindow" - } - } - } - }, - "requests.SendScheduleWindow": { - "type": "object", - "required": [ - "day_of_week", - "end_minute", - "start_minute" - ], - "properties": { - "day_of_week": { - "type": "integer" - }, - "end_minute": { - "type": "integer" - }, - "start_minute": { - "type": "integer" - } - } - }, "requests.UserNotificationUpdate": { "type": "object", "required": [ @@ -4735,6 +4741,27 @@ const docTemplate = `{ } } }, + "responses.MessageSendScheduleResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageSendSchedule" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ @@ -4928,27 +4955,6 @@ const docTemplate = `{ } } }, - "responses.SendScheduleResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.MessageSendSchedule" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } - }, "responses.Unauthorized": { "type": "object", "required": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index e48dab663..3045ce7fe 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,5222 +1,4739 @@ { - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "description": "Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption.", - "title": "httpSMS API Reference", + "schemes": ["https"], + "swagger": "2.0", + "info": { + "description": "Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption.", + "title": "httpSMS API Reference", + "contact": { + "name": "support@httpsms.com", + "email": "support@httpsms.com" + }, + "license": { + "name": "AGPL-3.0", + "url": "https://raw.githubusercontent.com/NdoleStudio/http-sms-manager/main/LICENSE" + }, + "version": "1.0" + }, + "host": "api.httpsms.com", + "basePath": "/v1", + "paths": { + "/billing/usage": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the summary of sent and received messages for a user in the current month", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Billing"], + "summary": "Get Billing Usage.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BillingUsageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/billing/usage-history": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Billing"], + "summary": "Get billing usage history.", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "number of heartbeats to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BillingUsagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/bulk-messages": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx).", + "consumes": ["multipart/form-data"], + "produces": ["application/json"], + "tags": ["BulkSMS"], + "summary": "Store bulk SMS file", + "parameters": [ + { + "type": "file", + "description": "The Excel or CSV file containing the messages to be sent.", + "name": "document", + "in": "formData", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/discord-integrations": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the discord integrations of a user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["DiscordIntegration"], + "summary": "Get discord integrations of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of discord integrations to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter discord integrations containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of discord integrations to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.DiscordsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store a discord integration for the authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["DiscordIntegration"], + "summary": "Store discord integration", + "parameters": [ + { + "description": "Payload of the discord integration request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.DiscordStore" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.DiscordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/discord-integrations/{discordID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a discord integration for the currently authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["DiscordIntegration"], + "summary": "Update a discord integration", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the discord integration", + "name": "discordID", + "in": "path", + "required": true + }, + { + "description": "Payload of discord integration to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.DiscordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.DiscordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a discord integration for a user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Webhooks"], + "summary": "Delete discord integration", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the discord integration", + "name": "discordID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/discord/event": { + "post": { + "description": "Publish a discord event to the registered listeners", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Discord"], + "summary": "Consume a discord event", + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/heartbeats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Heartbeats"], + "summary": "Get heartbeats of an owner phone number", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "the owner's phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of heartbeats to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HeartbeatsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store the heartbeat to make notify that a phone number is still active", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Heartbeats"], + "summary": "Register heartbeat of an owner phone number", + "parameters": [ + { + "description": "Payload of the heartbeat request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.HeartbeatStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HeartbeatResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/integration/3cx/messages": { + "post": { + "description": "Sends an SMS message from the 3CX platform", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["3CXIntegration"], + "summary": "Sends a 3CX SMS message", + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/message-threads": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["MessageThreads"], + "summary": "Get message threads for a phone number", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "owner phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter message threads containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageThreadsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/message-threads/{messageThreadID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the details of a message thread", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["MessageThreads"], + "summary": "Update a message thread", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message thread", + "name": "messageThreadID", + "in": "path", + "required": true + }, + { + "description": "Payload of message thread details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageThreadUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a message thread from the database and also deletes all the messages in the thread.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["MessageThreads"], + "summary": "Delete a message thread from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message thread", + "name": "messageThreadID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Get messages which are sent between 2 phone numbers", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "the owner's phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "+18005550100", + "description": "the contact's phone number", + "name": "contact", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter messages containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/bulk-send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add bulk SMS messages to be sent by the android phone", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Send bulk SMS messages", + "parameters": [ + { + "description": "Bulk send message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageBulkSend" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.MessagesResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/calls/missed": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Register a missed call event on the mobile phone", + "parameters": [ + { + "description": "Payload of the missed call event.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageCallMissed" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/outstanding": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get an outstanding message to be sent by an android phone", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Get an outstanding message", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703cb", + "description": "The ID of the message", + "name": "message_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/receive": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add a new message received from a mobile phone", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Receive a new SMS message from a mobile phone", + "parameters": [ + { + "description": "Received message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageReceive" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This returns the list of all messages based on the filter criteria including missed calls", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Search all messages of a user", + "parameters": [ + { + "type": "string", + "description": "Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/", + "name": "token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "+18005550199,+18005550100", + "description": "the owner's phone numbers", + "name": "owners", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter messages containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 200, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add a new SMS message to be sent by your Android phone", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Send an SMS message", + "parameters": [ + { + "description": "Send message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSend" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/{messageID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a message from the database by the message ID.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Get a message from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a message from the database and removes the message content from the list of threads.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Delete a message from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/messages/{messageID}/events": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Messages"], + "summary": "Upsert an event for a message on the mobile phone", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + }, + { + "description": "Payload of the event emitted.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageEvent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phone-api-keys": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list phone API keys which a user has registered on the httpSMS application", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["PhoneAPIKeys"], + "summary": "Get the phone API keys of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of phone api keys to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter phone api keys with name containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "number of phone api keys to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneAPIKeysResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["PhoneAPIKeys"], + "summary": "Store phone API key", + "parameters": [ + { + "description": "Payload of new phone API key.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneAPIKeyStoreRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneAPIKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phone-api-keys/{phoneAPIKeyID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a phone API Key from the database and cannot be used for authentication anymore.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["PhoneAPIKeys"], + "summary": "Delete a phone API key from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone API key", + "name": "phoneAPIKeyID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "You will need to login again to the httpSMS app on your Android phone with a new phone API key.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["PhoneAPIKeys"], + "summary": "Remove the association of a phone from the phone API key.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone API key", + "name": "phoneAPIKeyID", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone", + "name": "phoneID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phones": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of phones which a user has registered on the http sms application", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Phones"], + "summary": "Get phones of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter phones containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of phones to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhonesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Phones"], + "summary": "Upsert Phone", + "parameters": [ + { + "description": "Payload of new phone number.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneUpsert" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phones/fcm-token": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Phones"], + "summary": "Upserts the FCM token of a phone", + "parameters": [ + { + "description": "Payload of new FCM token.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneFCMToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/phones/{phoneID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a phone that has been sored in the database", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Phones"], + "summary": "Delete Phone", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone", + "name": "phoneID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/send-schedules": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "List all send schedules owned by the authenticated user.", + "produces": ["application/json"], + "tags": ["Send Schedules"], + "summary": "List send schedules", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendSchedule" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create a new send schedule for the authenticated user.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Send Schedules"], + "summary": "Create send schedule", + "parameters": [ + { + "description": "Payload of new send schedule.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSendScheduleStore" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.MessageSendScheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/send-schedules/{scheduleID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a send schedule owned by the authenticated user.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Send Schedules"], + "summary": "Update send schedule", + "parameters": [ + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleID", + "in": "path", + "required": true + }, + { + "description": "Payload of updated send schedule.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSendScheduleStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageSendScheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a send schedule owned by the authenticated user.", + "produces": ["application/json"], + "tags": ["Send Schedules"], + "summary": "Delete send schedule", + "parameters": [ + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get details of the currently authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the details of the currently authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Update a user", + "parameters": [ + { + "description": "Payload of user details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Deletes the currently authenticated user together with all their data.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Delete a user", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/subscription": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Cancel the subscription of the authenticated user.", + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Cancel the user's subscription", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/subscription-update-url": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the subscription URL of the authenticated user.", + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Currently authenticated user subscription update URL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.OkString" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/subscription/invoices/{subscriptionInvoiceID}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Generates a new invoice PDF file for the given subscription payment with given parameters.", + "consumes": ["application/json"], + "produces": ["application/pdf"], + "tags": ["Users"], + "summary": "Generate a subscription payment invoice", + "parameters": [ + { + "description": "Generate subscription payment invoice parameters", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserPaymentInvoice" + } + }, + { + "type": "string", + "description": "ID of the subscription invoice to generate the PDF for", + "name": "subscriptionInvoiceID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/subscription/payments": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Get the last 10 subscription payments.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserSubscriptionPaymentsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/{userID}/api-keys": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Rotate the user's API key in case the current API Key is compromised", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Rotate the user's API Key", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the user to update", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/users/{userID}/notifications": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update the email notification settings for a user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Update notification settings", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the user to update", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "User notification details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserNotificationUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}": { + "get": { + "description": "Download an MMS attachment by its path components", + "produces": ["application/octet-stream"], + "tags": ["Attachments"], + "summary": "Download a message attachment", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "userID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message ID", + "name": "messageID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Attachment index", + "name": "attachmentIndex", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Filename with extension", + "name": "filename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/webhooks": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the webhooks of a user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Webhooks"], + "summary": "Get webhooks of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of webhooks to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter webhooks containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of webhooks to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhooksResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store a webhook for the authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Webhooks"], + "summary": "Store a webhook", + "parameters": [ + { + "description": "Payload of the webhook request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.WebhookStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhookResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + }, + "/webhooks/{webhookID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a webhook for the currently authenticated user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Webhooks"], + "summary": "Update a webhook", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the webhook", + "name": "webhookID", + "in": "path", + "required": true + }, + { + "description": "Payload of webhook details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.WebhookUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhookResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a webhook for a user", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Webhooks"], + "summary": "Delete webhook", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the webhook", + "name": "webhookID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } + } + }, + "definitions": { + "entities.BillingUsage": { + "type": "object", + "required": [ + "created_at", + "end_timestamp", + "id", + "received_messages", + "sent_messages", + "start_timestamp", + "total_cost", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "end_timestamp": { + "type": "string", + "example": "2022-01-31T23:59:59+00:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "received_messages": { + "type": "integer", + "example": 465 + }, + "sent_messages": { + "type": "integer", + "example": 321 + }, + "start_timestamp": { + "type": "string", + "example": "2022-01-01T00:00:00+00:00" + }, + "total_cost": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "entities.Discord": { + "type": "object", + "required": [ + "created_at", + "id", + "incoming_channel_id", + "name", + "server_id", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "incoming_channel_id": { + "type": "string", + "example": "1095780203256627291" + }, + "name": { + "type": "string", + "example": "Game Server" + }, + "server_id": { + "type": "string", + "example": "1095778291488653372" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "entities.Heartbeat": { + "type": "object", + "required": [ + "charging", + "id", + "owner", + "timestamp", + "user_id", + "version" + ], + "properties": { + "charging": { + "type": "boolean", + "example": true + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "timestamp": { + "type": "string", + "example": "2022-06-05T14:26:01.520828+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "version": { + "type": "string", + "example": "344c10f" + } + } + }, + "entities.Message": { + "type": "object", + "required": [ + "attachments", + "contact", + "content", + "created_at", + "encrypted", + "id", + "max_send_attempts", + "order_timestamp", + "owner", + "request_received_at", + "send_attempt_count", + "sim", + "status", + "type", + "updated_at", + "user_id" + ], + "properties": { + "attachments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://example.com/image.jpg", + "https://example.com/video.mp4" + ] + }, + "contact": { + "type": "string", + "example": "+18005550100" + }, + "content": { + "type": "string", + "example": "This is a sample text message" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "delivered_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "encrypted": { + "type": "boolean", + "example": false + }, + "expired_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "failed_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "failure_reason": { + "type": "string", + "example": "UNKNOWN" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "last_attempted_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "max_send_attempts": { + "type": "integer", + "example": 1 + }, + "order_timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "received_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "request_id": { + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + }, + "request_received_at": { + "type": "string", + "example": "2022-06-05T14:26:01.520828+03:00" + }, + "scheduled_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "scheduled_send_time": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "send_attempt_count": { + "type": "integer", + "example": 0 + }, + "send_time": { + "description": "SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message", + "type": "integer", + "example": 133414 + }, + "sent_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "sim": { + "description": "SIM is the SIM card to use to send the message\n* SMS1: use the SIM card in slot 1\n* SMS2: use the SIM card in slot 2\n* DEFAULT: used the default communication SIM card", + "allOf": [ + { + "$ref": "#/definitions/entities.SIM" + } + ], + "example": "DEFAULT" + }, + "status": { + "type": "string", + "example": "pending" + }, + "type": { + "type": "string", + "example": "mobile-terminated" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "entities.MessageSendSchedule": { + "type": "object", + "required": [ + "created_at", + "id", + "is_active", + "name", + "timezone", + "updated_at", + "user_id", + "windows" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "name": { + "type": "string", + "example": "Business Hours" + }, + "timezone": { + "type": "string", + "example": "Europe/Tallinn" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendScheduleWindow" + } + } + } + }, + "entities.MessageSendScheduleWindow": { + "type": "object", + "required": ["day_of_week", "end_minute", "start_minute"], + "properties": { + "day_of_week": { + "type": "integer", + "example": 1 + }, + "end_minute": { + "type": "integer", + "example": 1020 + }, + "start_minute": { + "type": "integer", + "example": 540 + } + } + }, + "entities.MessageThread": { + "type": "object", + "required": [ + "color", + "contact", + "created_at", + "id", + "is_archived", + "last_message_content", + "last_message_id", + "order_timestamp", + "owner", + "status", + "updated_at", + "user_id" + ], + "properties": { + "color": { + "type": "string", + "example": "indigo" + }, "contact": { - "name": "support@httpsms.com", - "email": "support@httpsms.com" + "type": "string", + "example": "+18005550100" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" }, - "license": { - "name": "AGPL-3.0", - "url": "https://raw.githubusercontent.com/NdoleStudio/http-sms-manager/main/LICENSE" + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703ca" }, - "version": "1.0" + "is_archived": { + "type": "boolean", + "example": false + }, + "last_message_content": { + "type": "string", + "example": "This is a sample message content" + }, + "last_message_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703ca" + }, + "order_timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "status": { + "type": "string", + "example": "PENDING" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "host": "api.httpsms.com", - "basePath": "/v1", - "paths": { - "/billing/usage": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the summary of sent and received messages for a user in the current month", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Billing" - ], - "summary": "Get Billing Usage.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.BillingUsageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "entities.Phone": { + "type": "object", + "required": [ + "created_at", + "id", + "max_send_attempts", + "message_expiration_seconds", + "messages_per_minute", + "phone_number", + "schedule_id", + "sim", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" }, - "/billing/usage-history": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Billing" - ], - "summary": "Get billing usage history.", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "number of heartbeats to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.BillingUsagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." }, - "/bulk-messages": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx).", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "BulkSMS" - ], - "summary": "Store bulk SMS file", - "parameters": [ - { - "type": "file", - "description": "The Excel or CSV file containing the messages to be sent.", - "name": "document", - "in": "formData", - "required": true - } - ], - "responses": { - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "/discord-integrations": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the discord integrations of a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "DiscordIntegration" - ], - "summary": "Get discord integrations of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of discord integrations to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter discord integrations containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of discord integrations to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.DiscordsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store a discord integration for the authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "DiscordIntegration" - ], - "summary": "Store discord integration", - "parameters": [ - { - "description": "Payload of the discord integration request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DiscordStore" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.DiscordResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "max_send_attempts": { + "description": "MaxSendAttempts determines how many times to retry sending an SMS message", + "type": "integer", + "example": 2 }, - "/discord-integrations/{discordID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a discord integration for the currently authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "DiscordIntegration" - ], - "summary": "Update a discord integration", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the discord integration", - "name": "discordID", - "in": "path", - "required": true - }, - { - "description": "Payload of discord integration to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DiscordUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.DiscordResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a discord integration for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Webhooks" - ], - "summary": "Delete discord integration", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the discord integration", - "name": "discordID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "message_expiration_seconds": { + "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", + "type": "integer" }, - "/discord/event": { - "post": { - "description": "Publish a discord event to the registered listeners", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Discord" - ], - "summary": "Consume a discord event", - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "messages_per_minute": { + "type": "integer", + "example": 1 }, - "/heartbeats": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Heartbeats" - ], - "summary": "Get heartbeats of an owner phone number", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "the owner's phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of heartbeats to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HeartbeatsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store the heartbeat to make notify that a phone number is still active", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Heartbeats" - ], - "summary": "Register heartbeat of an owner phone number", - "parameters": [ - { - "description": "Payload of the heartbeat request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.HeartbeatStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HeartbeatResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "missed_call_auto_reply": { + "type": "string", + "example": "This phone cannot receive calls. Please send an SMS instead." }, - "/integration/3cx/messages": { - "post": { - "description": "Sends an SMS message from the 3CX platform", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "3CXIntegration" - ], - "summary": "Sends a 3CX SMS message", - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "phone_number": { + "type": "string", + "example": "+18005550199" }, - "/message-threads": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "MessageThreads" - ], - "summary": "Get message threads for a phone number", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "owner phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter message threads containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageThreadsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "/message-threads/{messageThreadID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the details of a message thread", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "MessageThreads" - ], - "summary": "Update a message thread", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message thread", - "name": "messageThreadID", - "in": "path", - "required": true - }, - { - "description": "Payload of message thread details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageThreadUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a message thread from the database and also deletes all the messages in the thread.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "MessageThreads" - ], - "summary": "Delete a message thread from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message thread", - "name": "messageThreadID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "sim": { + "$ref": "#/definitions/entities.SIM" }, - "/messages": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Get messages which are sent between 2 phone numbers", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "the owner's phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "+18005550100", - "description": "the contact's phone number", - "name": "contact", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter messages containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" }, - "/messages/bulk-send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add bulk SMS messages to be sent by the android phone", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Send bulk SMS messages", - "parameters": [ - { - "description": "Bulk send message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageBulkSend" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.MessagesResponse" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "entities.PhoneAPIKey": { + "type": "object", + "required": [ + "api_key", + "created_at", + "id", + "name", + "phone_ids", + "phone_numbers", + "updated_at", + "user_email", + "user_id" + ], + "properties": { + "api_key": { + "type": "string", + "example": "pk_DGW8NwQp7mxKaSZ72Xq9v6xxxxx" }, - "/messages/calls/missed": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Register a missed call event on the mobile phone", - "parameters": [ - { - "description": "Payload of the missed call event.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageCallMissed" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" }, - "/messages/outstanding": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get an outstanding message to be sent by an android phone", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Get an outstanding message", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703cb", - "description": "The ID of the message", - "name": "message_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "/messages/receive": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add a new message received from a mobile phone", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Receive a new SMS message from a mobile phone", - "parameters": [ - { - "description": "Received message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageReceive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "name": { + "type": "string", + "example": "Business Phone Key" }, - "/messages/search": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This returns the list of all messages based on the filter criteria including missed calls", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Search all messages of a user", - "parameters": [ - { - "type": "string", - "description": "Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/", - "name": "token", - "in": "header", - "required": true - }, - { - "type": "string", - "default": "+18005550199,+18005550100", - "description": "the owner's phone numbers", - "name": "owners", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter messages containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 200, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "phone_ids": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "32343a19-da5e-4b1b-a767-3298a73703cb", + "32343a19-da5e-4b1b-a767-3298a73703cc" + ] }, - "/messages/send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add a new SMS message to be sent by your Android phone", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Send an SMS message", - "parameters": [ - { - "description": "Send message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageSend" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["+18005550199", "+18005550100"] }, - "/messages/{messageID}": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get a message from the database by the message ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Get a message from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a message from the database and removes the message content from the list of threads.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Delete a message from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" }, - "/messages/{messageID}/events": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Messages" - ], - "summary": "Upsert an event for a message on the mobile phone", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - }, - { - "description": "Payload of the event emitted.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageEvent" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } + "user_email": { + "type": "string", + "example": "user@gmail.com" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "entities.SIM": { + "type": "string", + "enum": ["SIM1", "SIM2"], + "x-enum-varnames": ["SIM1", "SIM2"] + }, + "entities.SubscriptionName": { + "type": "string", + "enum": [ + "free", + "pro-monthly", + "pro-yearly", + "ultra-monthly", + "ultra-yearly", + "pro-lifetime", + "20k-monthly", + "100k-monthly", + "50k-monthly", + "200k-monthly", + "20k-yearly" + ], + "x-enum-varnames": [ + "SubscriptionNameFree", + "SubscriptionNameProMonthly", + "SubscriptionNameProYearly", + "SubscriptionNameUltraMonthly", + "SubscriptionNameUltraYearly", + "SubscriptionNameProLifetime", + "SubscriptionName20KMonthly", + "SubscriptionName100KMonthly", + "SubscriptionName50KMonthly", + "SubscriptionName200KMonthly", + "SubscriptionName20KYearly" + ] + }, + "entities.User": { + "type": "object", + "required": [ + "api_key", + "created_at", + "email", + "id", + "notification_heartbeat_enabled", + "notification_message_status_enabled", + "notification_newsletter_enabled", + "notification_webhook_enabled", + "subscription_id", + "subscription_name", + "timezone", + "updated_at" + ], + "properties": { + "active_phone_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "api_key": { + "type": "string", + "example": "x-api-key" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "email": { + "type": "string", + "example": "name@email.com" + }, + "id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "notification_heartbeat_enabled": { + "type": "boolean", + "example": true + }, + "notification_message_status_enabled": { + "type": "boolean", + "example": true + }, + "notification_newsletter_enabled": { + "type": "boolean", + "example": true + }, + "notification_webhook_enabled": { + "type": "boolean", + "example": true + }, + "subscription_ends_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "subscription_id": { + "type": "string", + "example": "8f9c71b8-b84e-4417-8408-a62274f65a08" + }, + "subscription_name": { + "allOf": [ + { + "$ref": "#/definitions/entities.SubscriptionName" } + ], + "example": "free" + }, + "subscription_renews_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "subscription_status": { + "type": "string", + "example": "on_trial" + }, + "timezone": { + "type": "string", + "example": "Europe/Helsinki" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + } + } + }, + "entities.Webhook": { + "type": "object", + "required": [ + "created_at", + "events", + "id", + "phone_numbers", + "signing_key", + "updated_at", + "url", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["message.phone.received"] + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["+18005550199", "+18005550100"] + }, + "signing_key": { + "type": "string", + "example": "DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "url": { + "type": "string", + "example": "https://example.com" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } + }, + "requests.DiscordStore": { + "type": "object", + "required": ["incoming_channel_id", "name", "server_id"], + "properties": { + "incoming_channel_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "server_id": { + "type": "string" + } + } + }, + "requests.DiscordUpdate": { + "type": "object", + "required": ["incoming_channel_id", "name", "server_id"], + "properties": { + "incoming_channel_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "server_id": { + "type": "string" + } + } + }, + "requests.HeartbeatStore": { + "type": "object", + "required": ["charging", "phone_numbers"], + "properties": { + "charging": { + "type": "boolean" + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "requests.MessageAttachment": { + "type": "object", + "required": ["content", "content_type", "name"], + "properties": { + "content": { + "description": "Content is the base64-encoded attachment data", + "type": "string", + "example": "base64data..." + }, + "content_type": { + "description": "ContentType is the MIME type of the attachment", + "type": "string", + "example": "image/jpeg" + }, + "name": { + "description": "Name is the original filename of the attachment", + "type": "string", + "example": "photo.jpg" + } + } + }, + "requests.MessageBulkSend": { + "type": "object", + "required": ["content", "from", "to"], + "properties": { + "attachments": { + "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", + "type": "array", + "items": { + "type": "string" + } + }, + "content": { + "type": "string", + "example": "This is a sample text message" + }, + "encrypted": { + "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false + }, + "from": { + "type": "string", + "example": "+18005550199" + }, + "request_id": { + "description": "RequestID is an optional parameter used to track a request from the client's perspective", + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["+18005550100", "+18005550100"] + } + } + }, + "requests.MessageCallMissed": { + "type": "object", + "required": ["from", "sim", "timestamp", "to"], + "properties": { + "from": { + "type": "string", + "example": "+18005550199" + }, + "sim": { + "type": "string", + "example": "SIM1" + }, + "timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "to": { + "type": "string", + "example": "+18005550100" + } + } + }, + "requests.MessageEvent": { + "type": "object", + "required": ["event_name", "reason", "timestamp"], + "properties": { + "event_name": { + "description": "EventName is the type of event\n* SENT: is emitted when a message is sent by the mobile phone\n* FAILED: is event is emitted when the message could not be sent by the mobile phone\n* DELIVERED: is event is emitted when a delivery report has been received by the mobile phone", + "type": "string", + "example": "SENT" + }, + "reason": { + "description": "Reason is the exact error message in case the event is an error", + "type": "string" + }, + "timestamp": { + "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + } + } + }, + "requests.MessageReceive": { + "type": "object", + "required": ["content", "encrypted", "from", "sim", "timestamp", "to"], + "properties": { + "attachments": { + "description": "Attachments is the list of MMS attachments received with the message", + "type": "array", + "items": { + "$ref": "#/definitions/requests.MessageAttachment" + } + }, + "content": { + "type": "string", + "example": "This is a sample text message received on a phone" + }, + "encrypted": { + "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false + }, + "from": { + "type": "string", + "example": "+18005550199" }, - "/phone-api-keys": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list phone API keys which a user has registered on the httpSMS application", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "PhoneAPIKeys" - ], - "summary": "Get the phone API keys of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of phone api keys to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter phone api keys with name containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "number of phone api keys to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneAPIKeysResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "PhoneAPIKeys" - ], - "summary": "Store phone API key", - "parameters": [ - { - "description": "Payload of new phone API key.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneAPIKeyStoreRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneAPIKeyResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } + "sim": { + "description": "SIM card that received the message", + "allOf": [ + { + "$ref": "#/definitions/entities.SIM" } + ], + "example": "SIM1" }, - "/phone-api-keys/{phoneAPIKeyID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a phone API Key from the database and cannot be used for authentication anymore.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "PhoneAPIKeys" - ], - "summary": "Delete a phone API key from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone API key", - "name": "phoneAPIKeyID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "timestamp": { + "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" }, - "/phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "You will need to login again to the httpSMS app on your Android phone with a new phone API key.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "PhoneAPIKeys" - ], - "summary": "Remove the association of a phone from the phone API key.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone API key", - "name": "phoneAPIKeyID", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone", - "name": "phoneID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "to": { + "type": "string", + "example": "+18005550100" + } + } + }, + "requests.MessageSend": { + "type": "object", + "required": ["content", "from", "to"], + "properties": { + "attachments": { + "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://example.com/image.jpg", + "https://example.com/video.mp4" + ] }, - "/phones": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of phones which a user has registered on the http sms application", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Phones" - ], - "summary": "Get phones of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter phones containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of phones to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhonesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Phones" - ], - "summary": "Upsert Phone", - "parameters": [ - { - "description": "Payload of new phone number.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneUpsert" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "content": { + "type": "string", + "example": "This is a sample text message" }, - "/phones/fcm-token": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Phones" - ], - "summary": "Upserts the FCM token of a phone", - "parameters": [ - { - "description": "Payload of new FCM token.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneFCMToken" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "encrypted": { + "description": "Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false }, - "/phones/{phoneID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a phone that has been sored in the database", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Phones" - ], - "summary": "Delete Phone", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone", - "name": "phoneID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "from": { + "type": "string", + "example": "+18005550199" }, - "/send-schedules": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "List all send schedules owned by the authenticated user.", - "produces": [ - "application/json" - ], - "tags": [ - "Send Schedules" - ], - "summary": "List send schedules", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendSchedule" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Create a new send schedule for the authenticated user.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Send Schedules" - ], - "summary": "Create send schedule", - "parameters": [ - { - "description": "Payload of new send schedule.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SendScheduleStore" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.SendScheduleResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "request_id": { + "description": "RequestID is an optional parameter used to track a request from the client's perspective", + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" }, - "/send-schedules/{scheduleID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a send schedule owned by the authenticated user.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Send Schedules" - ], - "summary": "Update send schedule", - "parameters": [ - { - "type": "string", - "description": "Schedule ID", - "name": "scheduleID", - "in": "path", - "required": true - }, - { - "description": "Payload of updated send schedule.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SendScheduleStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.SendScheduleResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a send schedule owned by the authenticated user.", - "produces": [ - "application/json" - ], - "tags": [ - "Send Schedules" - ], - "summary": "Delete send schedule", - "parameters": [ - { - "type": "string", - "description": "Schedule ID", - "name": "scheduleID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "send_at": { + "description": "SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future.", + "type": "string", + "example": "2025-12-19T16:39:57-08:00" }, - "/users/me": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get details of the currently authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Get current user", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the details of the currently authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Update a user", - "parameters": [ - { - "description": "Payload of user details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Deletes the currently authenticated user together with all their data.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Delete a user", - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "to": { + "type": "string", + "example": "+18005550100" + } + } + }, + "requests.MessageSendScheduleStore": { + "type": "object", + "required": ["is_active", "name", "timezone", "windows"], + "properties": { + "is_active": { + "type": "boolean" }, - "/users/subscription": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Cancel the subscription of the authenticated user.", - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Cancel the user's subscription", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "name": { + "type": "string" }, - "/users/subscription-update-url": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Fetches the subscription URL of the authenticated user.", - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Currently authenticated user subscription update URL", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.OkString" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "timezone": { + "type": "string" }, - "/users/subscription/invoices/{subscriptionInvoiceID}": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Generates a new invoice PDF file for the given subscription payment with given parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/pdf" - ], - "tags": [ - "Users" - ], - "summary": "Generate a subscription payment invoice", - "parameters": [ - { - "description": "Generate subscription payment invoice parameters", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserPaymentInvoice" - } - }, - { - "type": "string", - "description": "ID of the subscription invoice to generate the PDF for", - "name": "subscriptionInvoiceID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "file" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.MessageSendScheduleWindow" + } + } + } + }, + "requests.MessageSendScheduleWindow": { + "type": "object", + "required": ["day_of_week", "end_minute", "start_minute"], + "properties": { + "day_of_week": { + "type": "integer" }, - "/users/subscription/payments": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Get the last 10 subscription payments.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserSubscriptionPaymentsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "end_minute": { + "type": "integer" }, - "/users/{userID}/api-keys": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Rotate the user's API key in case the current API Key is compromised", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Rotate the user's API Key", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the user to update", - "name": "userID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "start_minute": { + "type": "integer" + } + } + }, + "requests.MessageThreadUpdate": { + "type": "object", + "required": ["is_archived"], + "properties": { + "is_archived": { + "type": "boolean", + "example": true + } + } + }, + "requests.PhoneAPIKeyStoreRequest": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "example": "My Phone API Key" + } + } + }, + "requests.PhoneFCMToken": { + "type": "object", + "required": ["fcm_token", "phone_number", "sim"], + "properties": { + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." }, - "/users/{userID}/notifications": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update the email notification settings for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Update notification settings", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the user to update", - "name": "userID", - "in": "path", - "required": true - }, - { - "description": "User notification details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserNotificationUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "phone_number": { + "type": "string", + "example": "[+18005550199]" }, - "/v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}": { - "get": { - "description": "Download an MMS attachment by its path components", - "produces": [ - "application/octet-stream" - ], - "tags": [ - "Attachments" - ], - "summary": "Download a message attachment", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "userID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "messageID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Attachment index", - "name": "attachmentIndex", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Filename with extension", - "name": "filename", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "file" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "sim": { + "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", + "type": "string", + "example": "SIM1" + } + } + }, + "requests.PhoneUpsert": { + "type": "object", + "required": [ + "fcm_token", + "max_send_attempts", + "message_expiration_seconds", + "messages_per_minute", + "missed_call_auto_reply", + "phone_number", + "schedule_id", + "sim" + ], + "properties": { + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." }, - "/webhooks": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the webhooks of a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Webhooks" - ], - "summary": "Get webhooks of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of webhooks to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter webhooks containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of webhooks to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhooksResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store a webhook for the authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Webhooks" - ], - "summary": "Store a webhook", - "parameters": [ - { - "description": "Payload of the webhook request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.WebhookStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhookResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "max_send_attempts": { + "description": "MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline.", + "type": "integer", + "example": 2 }, - "/webhooks/{webhookID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a webhook for the currently authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Webhooks" - ], - "summary": "Update a webhook", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the webhook", - "name": "webhookID", - "in": "path", - "required": true - }, - { - "description": "Payload of webhook details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.WebhookUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhookResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a webhook for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Webhooks" - ], - "summary": "Delete webhook", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the webhook", - "name": "webhookID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } + "message_expiration_seconds": { + "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", + "type": "integer", + "example": 12345 + }, + "messages_per_minute": { + "type": "integer", + "example": 1 + }, + "missed_call_auto_reply": { + "type": "string", + "example": "e.g. This phone cannot receive calls. Please send an SMS instead." + }, + "phone_number": { + "type": "string", + "example": "+18005550199" + }, + "schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "sim": { + "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", + "type": "string", + "example": "SIM1" } + } }, - "definitions": { - "entities.BillingUsage": { - "type": "object", - "required": [ - "created_at", - "end_timestamp", - "id", - "received_messages", - "sent_messages", - "start_timestamp", - "total_cost", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "end_timestamp": { - "type": "string", - "example": "2022-01-31T23:59:59+00:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "received_messages": { - "type": "integer", - "example": 465 - }, - "sent_messages": { - "type": "integer", - "example": 321 - }, - "start_timestamp": { - "type": "string", - "example": "2022-01-01T00:00:00+00:00" - }, - "total_cost": { - "type": "integer", - "example": 0 - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "requests.UserNotificationUpdate": { + "type": "object", + "required": [ + "heartbeat_enabled", + "message_status_enabled", + "newsletter_enabled", + "webhook_enabled" + ], + "properties": { + "heartbeat_enabled": { + "type": "boolean", + "example": true }, - "entities.Discord": { - "type": "object", - "required": [ - "created_at", - "id", - "incoming_channel_id", - "name", - "server_id", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "incoming_channel_id": { - "type": "string", - "example": "1095780203256627291" - }, - "name": { - "type": "string", - "example": "Game Server" - }, - "server_id": { - "type": "string", - "example": "1095778291488653372" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "message_status_enabled": { + "type": "boolean", + "example": true }, - "entities.Heartbeat": { - "type": "object", - "required": [ - "charging", - "id", - "owner", - "timestamp", - "user_id", - "version" - ], - "properties": { - "charging": { - "type": "boolean", - "example": true - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "timestamp": { - "type": "string", - "example": "2022-06-05T14:26:01.520828+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "version": { - "type": "string", - "example": "344c10f" - } - } + "newsletter_enabled": { + "type": "boolean", + "example": true }, - "entities.Message": { - "type": "object", - "required": [ - "attachments", - "contact", - "content", - "created_at", - "encrypted", - "id", - "max_send_attempts", - "order_timestamp", - "owner", - "request_received_at", - "send_attempt_count", - "sim", - "status", - "type", - "updated_at", - "user_id" - ], - "properties": { - "attachments": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://example.com/image.jpg", - "https://example.com/video.mp4" - ] - }, - "contact": { - "type": "string", - "example": "+18005550100" - }, - "content": { - "type": "string", - "example": "This is a sample text message" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "delivered_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "encrypted": { - "type": "boolean", - "example": false - }, - "expired_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "failed_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "failure_reason": { - "type": "string", - "example": "UNKNOWN" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "last_attempted_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "max_send_attempts": { - "type": "integer", - "example": 1 - }, - "order_timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "received_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "request_id": { - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" - }, - "request_received_at": { - "type": "string", - "example": "2022-06-05T14:26:01.520828+03:00" - }, - "scheduled_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "scheduled_send_time": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "send_attempt_count": { - "type": "integer", - "example": 0 - }, - "send_time": { - "description": "SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message", - "type": "integer", - "example": 133414 - }, - "sent_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "sim": { - "description": "SIM is the SIM card to use to send the message\n* SMS1: use the SIM card in slot 1\n* SMS2: use the SIM card in slot 2\n* DEFAULT: used the default communication SIM card", - "allOf": [ - { - "$ref": "#/definitions/entities.SIM" - } - ], - "example": "DEFAULT" - }, - "status": { - "type": "string", - "example": "pending" - }, - "type": { - "type": "string", - "example": "mobile-terminated" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "webhook_enabled": { + "type": "boolean", + "example": true + } + } + }, + "requests.UserPaymentInvoice": { + "type": "object", + "required": [ + "address", + "city", + "country", + "name", + "notes", + "state", + "zip_code" + ], + "properties": { + "address": { + "type": "string", + "example": "221B Baker Street, London" }, - "entities.MessageSendSchedule": { - "type": "object", - "required": [ - "created_at", - "id", - "is_active", - "name", - "timezone", - "updated_at", - "user_id", - "windows" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "is_active": { - "type": "boolean", - "example": true - }, - "name": { - "type": "string", - "example": "Business Hours" - }, - "timezone": { - "type": "string", - "example": "Europe/Tallinn" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "windows": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendScheduleWindow" - } - } - } + "city": { + "type": "string", + "example": "Los Angeles" }, - "entities.MessageSendScheduleWindow": { - "type": "object", - "required": [ - "day_of_week", - "end_minute", - "start_minute" - ], - "properties": { - "day_of_week": { - "type": "integer", - "example": 1 - }, - "end_minute": { - "type": "integer", - "example": 1020 - }, - "start_minute": { - "type": "integer", - "example": 540 - } - } + "country": { + "type": "string", + "example": "US" }, - "entities.MessageThread": { - "type": "object", - "required": [ - "color", - "contact", - "created_at", - "id", - "is_archived", - "last_message_content", - "last_message_id", - "order_timestamp", - "owner", - "status", - "updated_at", - "user_id" - ], - "properties": { - "color": { - "type": "string", - "example": "indigo" - }, - "contact": { - "type": "string", - "example": "+18005550100" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703ca" - }, - "is_archived": { - "type": "boolean", - "example": false - }, - "last_message_content": { - "type": "string", - "example": "This is a sample message content" - }, - "last_message_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703ca" - }, - "order_timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "status": { - "type": "string", - "example": "PENDING" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "name": { + "type": "string", + "example": "Acme Corp" }, - "entities.Phone": { - "type": "object", - "required": [ - "created_at", - "id", - "max_send_attempts", - "message_expiration_seconds", - "messages_per_minute", - "phone_number", - "schedule_id", - "sim", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "max_send_attempts": { - "description": "MaxSendAttempts determines how many times to retry sending an SMS message", - "type": "integer", - "example": 2 - }, - "message_expiration_seconds": { - "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", - "type": "integer" - }, - "messages_per_minute": { - "type": "integer", - "example": 1 - }, - "missed_call_auto_reply": { - "type": "string", - "example": "This phone cannot receive calls. Please send an SMS instead." - }, - "phone_number": { - "type": "string", - "example": "+18005550199" - }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "sim": { - "$ref": "#/definitions/entities.SIM" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "notes": { + "type": "string", + "example": "Thank you for your business!" }, - "entities.PhoneAPIKey": { - "type": "object", - "required": [ - "api_key", - "created_at", - "id", - "name", - "phone_ids", - "phone_numbers", - "updated_at", - "user_email", - "user_id" - ], - "properties": { - "api_key": { - "type": "string", - "example": "pk_DGW8NwQp7mxKaSZ72Xq9v6xxxxx" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "name": { - "type": "string", - "example": "Business Phone Key" - }, - "phone_ids": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "32343a19-da5e-4b1b-a767-3298a73703cb", - "32343a19-da5e-4b1b-a767-3298a73703cc" - ] - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "+18005550199", - "+18005550100" - ] - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "user_email": { - "type": "string", - "example": "user@gmail.com" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "state": { + "type": "string", + "example": "CA" }, - "entities.SIM": { - "type": "string", - "enum": [ - "SIM1", - "SIM2" - ], - "x-enum-varnames": [ - "SIM1", - "SIM2" - ] - }, - "entities.SubscriptionName": { - "type": "string", - "enum": [ - "free", - "pro-monthly", - "pro-yearly", - "ultra-monthly", - "ultra-yearly", - "pro-lifetime", - "20k-monthly", - "100k-monthly", - "50k-monthly", - "200k-monthly", - "20k-yearly" - ], - "x-enum-varnames": [ - "SubscriptionNameFree", - "SubscriptionNameProMonthly", - "SubscriptionNameProYearly", - "SubscriptionNameUltraMonthly", - "SubscriptionNameUltraYearly", - "SubscriptionNameProLifetime", - "SubscriptionName20KMonthly", - "SubscriptionName100KMonthly", - "SubscriptionName50KMonthly", - "SubscriptionName200KMonthly", - "SubscriptionName20KYearly" - ] - }, - "entities.User": { - "type": "object", - "required": [ - "api_key", - "created_at", - "email", - "id", - "notification_heartbeat_enabled", - "notification_message_status_enabled", - "notification_newsletter_enabled", - "notification_webhook_enabled", - "subscription_id", - "subscription_name", - "timezone", - "updated_at" - ], - "properties": { - "active_phone_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "api_key": { - "type": "string", - "example": "x-api-key" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "email": { - "type": "string", - "example": "name@email.com" - }, - "id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "notification_heartbeat_enabled": { - "type": "boolean", - "example": true - }, - "notification_message_status_enabled": { - "type": "boolean", - "example": true - }, - "notification_newsletter_enabled": { - "type": "boolean", - "example": true - }, - "notification_webhook_enabled": { - "type": "boolean", - "example": true - }, - "subscription_ends_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "subscription_id": { - "type": "string", - "example": "8f9c71b8-b84e-4417-8408-a62274f65a08" - }, - "subscription_name": { - "allOf": [ - { - "$ref": "#/definitions/entities.SubscriptionName" - } - ], - "example": "free" - }, - "subscription_renews_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "subscription_status": { - "type": "string", - "example": "on_trial" - }, - "timezone": { - "type": "string", - "example": "Europe/Helsinki" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - } - } + "zip_code": { + "type": "string", + "example": "9800" + } + } + }, + "requests.UserUpdate": { + "type": "object", + "required": ["active_phone_id", "timezone"], + "properties": { + "active_phone_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "entities.Webhook": { - "type": "object", - "required": [ - "created_at", - "events", - "id", - "phone_numbers", - "signing_key", - "updated_at", - "url", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "events": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "message.phone.received" - ] - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "+18005550199", - "+18005550100" - ] - }, - "signing_key": { - "type": "string", - "example": "DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "url": { - "type": "string", - "example": "https://example.com" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } + "timezone": { + "type": "string", + "example": "Europe/Helsinki" + } + } + }, + "requests.WebhookStore": { + "type": "object", + "required": ["events", "phone_numbers", "signing_key", "url"], + "properties": { + "events": { + "type": "array", + "items": { + "type": "string" + } }, - "requests.DiscordStore": { - "type": "object", - "required": [ - "incoming_channel_id", - "name", - "server_id" - ], - "properties": { - "incoming_channel_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "server_id": { - "type": "string" - } - } + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["+18005550100", "+18005550100"] }, - "requests.DiscordUpdate": { - "type": "object", - "required": [ - "incoming_channel_id", - "name", - "server_id" - ], - "properties": { - "incoming_channel_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "server_id": { - "type": "string" - } - } + "signing_key": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "requests.WebhookUpdate": { + "type": "object", + "required": ["events", "phone_numbers", "signing_key", "url"], + "properties": { + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["+18005550100", "+18005550100"] + }, + "signing_key": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "responses.BadRequest": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "string", + "example": "The request body is not a valid JSON string" + }, + "message": { + "type": "string", + "example": "The request isn't properly formed" }, - "requests.HeartbeatStore": { - "type": "object", - "required": [ - "charging", - "phone_numbers" - ], - "properties": { - "charging": { - "type": "boolean" - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - } - } - } + "status": { + "type": "string", + "example": "error" + } + } + }, + "responses.BillingUsageResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.BillingUsage" }, - "requests.MessageAttachment": { - "type": "object", - "required": [ - "content", - "content_type", - "name" - ], - "properties": { - "content": { - "description": "Content is the base64-encoded attachment data", - "type": "string", - "example": "base64data..." - }, - "content_type": { - "description": "ContentType is the MIME type of the attachment", - "type": "string", - "example": "image/jpeg" - }, - "name": { - "description": "Name is the original filename of the attachment", - "type": "string", - "example": "photo.jpg" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.MessageBulkSend": { - "type": "object", - "required": [ - "content", - "from", - "to" - ], - "properties": { - "attachments": { - "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", - "type": "array", - "items": { - "type": "string" - } - }, - "content": { - "type": "string", - "example": "This is a sample text message" - }, - "encrypted": { - "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false - }, - "from": { - "type": "string", - "example": "+18005550199" - }, - "request_id": { - "description": "RequestID is an optional parameter used to track a request from the client's perspective", - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" - }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "+18005550100", - "+18005550100" - ] - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.BillingUsagesResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.BillingUsage" + } }, - "requests.MessageCallMissed": { - "type": "object", - "required": [ - "from", - "sim", - "timestamp", - "to" - ], - "properties": { - "from": { - "type": "string", - "example": "+18005550199" - }, - "sim": { - "type": "string", - "example": "SIM1" - }, - "timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "to": { - "type": "string", - "example": "+18005550100" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.MessageEvent": { - "type": "object", - "required": [ - "event_name", - "reason", - "timestamp" - ], - "properties": { - "event_name": { - "description": "EventName is the type of event\n* SENT: is emitted when a message is sent by the mobile phone\n* FAILED: is event is emitted when the message could not be sent by the mobile phone\n* DELIVERED: is event is emitted when a delivery report has been received by the mobile phone", - "type": "string", - "example": "SENT" - }, - "reason": { - "description": "Reason is the exact error message in case the event is an error", - "type": "string" - }, - "timestamp": { - "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.DiscordResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.Discord" }, - "requests.MessageReceive": { - "type": "object", - "required": [ - "content", - "encrypted", - "from", - "sim", - "timestamp", - "to" - ], - "properties": { - "attachments": { - "description": "Attachments is the list of MMS attachments received with the message", - "type": "array", - "items": { - "$ref": "#/definitions/requests.MessageAttachment" - } - }, - "content": { - "type": "string", - "example": "This is a sample text message received on a phone" - }, - "encrypted": { - "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false - }, - "from": { - "type": "string", - "example": "+18005550199" - }, - "sim": { - "description": "SIM card that received the message", - "allOf": [ - { - "$ref": "#/definitions/entities.SIM" - } - ], - "example": "SIM1" - }, - "timestamp": { - "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "to": { - "type": "string", - "example": "+18005550100" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.MessageSend": { - "type": "object", - "required": [ - "content", - "from", - "to" - ], - "properties": { - "attachments": { - "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://example.com/image.jpg", - "https://example.com/video.mp4" - ] - }, - "content": { - "type": "string", - "example": "This is a sample text message" - }, - "encrypted": { - "description": "Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false - }, - "from": { - "type": "string", - "example": "+18005550199" - }, - "request_id": { - "description": "RequestID is an optional parameter used to track a request from the client's perspective", - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" - }, - "send_at": { - "description": "SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future.", - "type": "string", - "example": "2025-12-19T16:39:57-08:00" - }, - "to": { - "type": "string", - "example": "+18005550100" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.DiscordsResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Discord" + } }, - "requests.MessageThreadUpdate": { - "type": "object", - "required": [ - "is_archived" - ], - "properties": { - "is_archived": { - "type": "boolean", - "example": true - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.PhoneAPIKeyStoreRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "example": "My Phone API Key" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.HeartbeatResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.Heartbeat" }, - "requests.PhoneFCMToken": { - "type": "object", - "required": [ - "fcm_token", - "phone_number", - "sim" - ], - "properties": { - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." - }, - "phone_number": { - "type": "string", - "example": "[+18005550199]" - }, - "sim": { - "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", - "type": "string", - "example": "SIM1" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.PhoneUpsert": { - "type": "object", - "required": [ - "fcm_token", - "max_send_attempts", - "message_expiration_seconds", - "messages_per_minute", - "missed_call_auto_reply", - "phone_number", - "schedule_id", - "sim" - ], - "properties": { - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." - }, - "max_send_attempts": { - "description": "MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline.", - "type": "integer", - "example": 2 - }, - "message_expiration_seconds": { - "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", - "type": "integer", - "example": 12345 - }, - "messages_per_minute": { - "type": "integer", - "example": 1 - }, - "missed_call_auto_reply": { - "type": "string", - "example": "e.g. This phone cannot receive calls. Please send an SMS instead." - }, - "phone_number": { - "type": "string", - "example": "+18005550199" - }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "sim": { - "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", - "type": "string", - "example": "SIM1" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.HeartbeatsResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Heartbeat" + } }, - "requests.SendScheduleStore": { - "type": "object", - "required": [ - "is_active", - "name", - "timezone", - "windows" - ], - "properties": { - "is_active": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "timezone": { - "type": "string" - }, - "windows": { - "type": "array", - "items": { - "$ref": "#/definitions/requests.SendScheduleWindow" - } - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.SendScheduleWindow": { - "type": "object", - "required": [ - "day_of_week", - "end_minute", - "start_minute" - ], - "properties": { - "day_of_week": { - "type": "integer" - }, - "end_minute": { - "type": "integer" - }, - "start_minute": { - "type": "integer" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.InternalServerError": { + "type": "object", + "required": ["message", "status"], + "properties": { + "message": { + "type": "string", + "example": "We ran into an internal error while handling the request." }, - "requests.UserNotificationUpdate": { - "type": "object", - "required": [ - "heartbeat_enabled", - "message_status_enabled", - "newsletter_enabled", - "webhook_enabled" - ], - "properties": { - "heartbeat_enabled": { - "type": "boolean", - "example": true - }, - "message_status_enabled": { - "type": "boolean", - "example": true - }, - "newsletter_enabled": { - "type": "boolean", - "example": true - }, - "webhook_enabled": { - "type": "boolean", - "example": true - } - } + "status": { + "type": "string", + "example": "error" + } + } + }, + "responses.MessageResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.Message" }, - "requests.UserPaymentInvoice": { - "type": "object", - "required": [ - "address", - "city", - "country", - "name", - "notes", - "state", - "zip_code" - ], - "properties": { - "address": { - "type": "string", - "example": "221B Baker Street, London" - }, - "city": { - "type": "string", - "example": "Los Angeles" - }, - "country": { - "type": "string", - "example": "US" - }, - "name": { - "type": "string", - "example": "Acme Corp" - }, - "notes": { - "type": "string", - "example": "Thank you for your business!" - }, - "state": { - "type": "string", - "example": "CA" - }, - "zip_code": { - "type": "string", - "example": "9800" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.UserUpdate": { - "type": "object", - "required": [ - "active_phone_id", - "timezone" - ], - "properties": { - "active_phone_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "timezone": { - "type": "string", - "example": "Europe/Helsinki" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.MessageSendScheduleResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageSendSchedule" }, - "requests.WebhookStore": { - "type": "object", - "required": [ - "events", - "phone_numbers", - "signing_key", - "url" - ], - "properties": { - "events": { - "type": "array", - "items": { - "type": "string" - } - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "+18005550100", - "+18005550100" - ] - }, - "signing_key": { - "type": "string" - }, - "url": { - "type": "string" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "requests.WebhookUpdate": { - "type": "object", - "required": [ - "events", - "phone_numbers", - "signing_key", - "url" - ], - "properties": { - "events": { - "type": "array", - "items": { - "type": "string" - } - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "+18005550100", - "+18005550100" - ] - }, - "signing_key": { - "type": "string" - }, - "url": { - "type": "string" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.MessageThreadsResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageThread" + } }, - "responses.BadRequest": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "string", - "example": "The request body is not a valid JSON string" - }, - "message": { - "type": "string", - "example": "The request isn't properly formed" - }, - "status": { - "type": "string", - "example": "error" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.BillingUsageResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.BillingUsage" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.MessagesResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Message" + } }, - "responses.BillingUsagesResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.BillingUsage" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.DiscordResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.Discord" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.NoContent": { + "type": "object", + "required": ["message", "status"], + "properties": { + "message": { + "type": "string", + "example": "action performed successfully" }, - "responses.DiscordsResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Discord" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.NotFound": { + "type": "object", + "required": ["message", "status"], + "properties": { + "message": { + "type": "string", + "example": "cannot find message with ID [32343a19-da5e-4b1b-a767-3298a73703ca]" }, - "responses.HeartbeatResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.Heartbeat" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "error" + } + } + }, + "responses.OkString": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "string" }, - "responses.HeartbeatsResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Heartbeat" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.InternalServerError": { - "type": "object", - "required": [ - "message", - "status" - ], - "properties": { - "message": { - "type": "string", - "example": "We ran into an internal error while handling the request." - }, - "status": { - "type": "string", - "example": "error" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.PhoneAPIKeyResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.PhoneAPIKey" }, - "responses.MessageResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.Message" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.MessageThreadsResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageThread" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.PhoneAPIKeysResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.PhoneAPIKey" + } }, - "responses.MessagesResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Message" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.NoContent": { - "type": "object", - "required": [ - "message", - "status" - ], - "properties": { - "message": { - "type": "string", - "example": "action performed successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.PhoneResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.Phone" }, - "responses.NotFound": { - "type": "object", - "required": [ - "message", - "status" - ], - "properties": { - "message": { - "type": "string", - "example": "cannot find message with ID [32343a19-da5e-4b1b-a767-3298a73703ca]" - }, - "status": { - "type": "string", - "example": "error" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.OkString": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "string" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.PhonesResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Phone" + } }, - "responses.PhoneAPIKeyResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.PhoneAPIKey" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.PhoneAPIKeysResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.PhoneAPIKey" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.Unauthorized": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "string", + "example": "Make sure your API key is set in the [X-API-Key] header in the request" }, - "responses.PhoneResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.Phone" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "You are not authorized to carry out this request." }, - "responses.PhonesResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Phone" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } + "status": { + "type": "string", + "example": "error" + } + } + }, + "responses.UnprocessableEntity": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" } + } }, - "responses.SendScheduleResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.MessageSendSchedule" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "validation errors while handling request" }, - "responses.Unauthorized": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "string", - "example": "Make sure your API key is set in the [X-API-Key] header in the request" - }, - "message": { - "type": "string", - "example": "You are not authorized to carry out this request." - }, - "status": { - "type": "string", - "example": "error" - } - } + "status": { + "type": "string", + "example": "error" + } + } + }, + "responses.UserResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.User" }, - "responses.UnprocessableEntity": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "message": { - "type": "string", - "example": "validation errors while handling request" - }, - "status": { - "type": "string", - "example": "error" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.UserResponse": { + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.UserSubscriptionPaymentsResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { "type": "object", - "required": [ - "data", - "message", - "status" - ], + "required": ["attributes", "id", "type"], "properties": { - "data": { - "$ref": "#/definitions/entities.User" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" + "attributes": { + "type": "object", + "required": [ + "billing_reason", + "card_brand", + "card_last_four", + "created_at", + "currency", + "currency_rate", + "discount_total", + "discount_total_formatted", + "discount_total_usd", + "refunded", + "refunded_amount", + "refunded_amount_formatted", + "refunded_amount_usd", + "refunded_at", + "status", + "status_formatted", + "subtotal", + "subtotal_formatted", + "subtotal_usd", + "tax", + "tax_formatted", + "tax_inclusive", + "tax_usd", + "total", + "total_formatted", + "total_usd", + "updated_at" + ], + "properties": { + "billing_reason": { + "type": "string" + }, + "card_brand": { + "type": "string" + }, + "card_last_four": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "currency_rate": { + "type": "string" + }, + "discount_total": { + "type": "integer" + }, + "discount_total_formatted": { + "type": "string" + }, + "discount_total_usd": { + "type": "integer" + }, + "refunded": { + "type": "boolean" + }, + "refunded_amount": { + "type": "integer" + }, + "refunded_amount_formatted": { + "type": "string" + }, + "refunded_amount_usd": { + "type": "integer" + }, + "refunded_at": {}, + "status": { + "type": "string" + }, + "status_formatted": { + "type": "string" + }, + "subtotal": { + "type": "integer" + }, + "subtotal_formatted": { + "type": "string" + }, + "subtotal_usd": { + "type": "integer" + }, + "tax": { + "type": "integer" + }, + "tax_formatted": { + "type": "string" + }, + "tax_inclusive": { + "type": "boolean" + }, + "tax_usd": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_formatted": { + "type": "string" + }, + "total_usd": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } } + }, + "id": { + "type": "string" + }, + "type": { + "type": "string" + } } + } }, - "responses.UserSubscriptionPaymentsResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "attributes", - "id", - "type" - ], - "properties": { - "attributes": { - "type": "object", - "required": [ - "billing_reason", - "card_brand", - "card_last_four", - "created_at", - "currency", - "currency_rate", - "discount_total", - "discount_total_formatted", - "discount_total_usd", - "refunded", - "refunded_amount", - "refunded_amount_formatted", - "refunded_amount_usd", - "refunded_at", - "status", - "status_formatted", - "subtotal", - "subtotal_formatted", - "subtotal_usd", - "tax", - "tax_formatted", - "tax_inclusive", - "tax_usd", - "total", - "total_formatted", - "total_usd", - "updated_at" - ], - "properties": { - "billing_reason": { - "type": "string" - }, - "card_brand": { - "type": "string" - }, - "card_last_four": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "currency_rate": { - "type": "string" - }, - "discount_total": { - "type": "integer" - }, - "discount_total_formatted": { - "type": "string" - }, - "discount_total_usd": { - "type": "integer" - }, - "refunded": { - "type": "boolean" - }, - "refunded_amount": { - "type": "integer" - }, - "refunded_amount_formatted": { - "type": "string" - }, - "refunded_amount_usd": { - "type": "integer" - }, - "refunded_at": {}, - "status": { - "type": "string" - }, - "status_formatted": { - "type": "string" - }, - "subtotal": { - "type": "integer" - }, - "subtotal_formatted": { - "type": "string" - }, - "subtotal_usd": { - "type": "integer" - }, - "tax": { - "type": "integer" - }, - "tax_formatted": { - "type": "string" - }, - "tax_inclusive": { - "type": "boolean" - }, - "tax_usd": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_formatted": { - "type": "string" - }, - "total_usd": { - "type": "integer" - }, - "updated_at": { - "type": "string" - } - } - }, - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" }, - "responses.WebhookResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "$ref": "#/definitions/entities.Webhook" - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "status": { + "type": "string", + "example": "success" + } + } + }, + "responses.WebhookResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "$ref": "#/definitions/entities.Webhook" }, - "responses.WebhooksResponse": { - "type": "object", - "required": [ - "data", - "message", - "status" - ], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Webhook" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" - } - } + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" } + } }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "x-api-Key", - "in": "header" + "responses.WebhooksResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Webhook" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-api-Key", + "in": "header" } -} \ No newline at end of file + } +} diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 118671d89..58cee295e 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -30,15 +30,15 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - end_timestamp - - id - - received_messages - - sent_messages - - start_timestamp - - total_cost - - updated_at - - user_id + - created_at + - end_timestamp + - id + - received_messages + - sent_messages + - start_timestamp + - total_cost + - updated_at + - user_id type: object entities.Discord: properties: @@ -64,13 +64,13 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - id - - incoming_channel_id - - name - - server_id - - updated_at - - user_id + - created_at + - id + - incoming_channel_id + - name + - server_id + - updated_at + - user_id type: object entities.Heartbeat: properties: @@ -93,19 +93,19 @@ definitions: example: 344c10f type: string required: - - charging - - id - - owner - - timestamp - - user_id - - version + - charging + - id + - owner + - timestamp + - user_id + - version type: object entities.Message: properties: attachments: example: - - https://example.com/image.jpg - - https://example.com/video.mp4 + - https://example.com/image.jpg + - https://example.com/video.mp4 items: type: string type: array @@ -167,7 +167,8 @@ definitions: example: 0 type: integer send_time: - description: SendDuration is the number of nanoseconds from when the request + description: + SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message example: 133414 type: integer @@ -176,7 +177,7 @@ definitions: type: string sim: allOf: - - $ref: '#/definitions/entities.SIM' + - $ref: "#/definitions/entities.SIM" description: |- SIM is the SIM card to use to send the message * SMS1: use the SIM card in slot 1 @@ -196,22 +197,22 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - attachments - - contact - - content - - created_at - - encrypted - - id - - max_send_attempts - - order_timestamp - - owner - - request_received_at - - send_attempt_count - - sim - - status - - type - - updated_at - - user_id + - attachments + - contact + - content + - created_at + - encrypted + - id + - max_send_attempts + - order_timestamp + - owner + - request_received_at + - send_attempt_count + - sim + - status + - type + - updated_at + - user_id type: object entities.MessageSendSchedule: properties: @@ -238,17 +239,17 @@ definitions: type: string windows: items: - $ref: '#/definitions/entities.MessageSendScheduleWindow' + $ref: "#/definitions/entities.MessageSendScheduleWindow" type: array required: - - created_at - - id - - is_active - - name - - timezone - - updated_at - - user_id - - windows + - created_at + - id + - is_active + - name + - timezone + - updated_at + - user_id + - windows type: object entities.MessageSendScheduleWindow: properties: @@ -262,9 +263,9 @@ definitions: example: 540 type: integer required: - - day_of_week - - end_minute - - start_minute + - day_of_week + - end_minute + - start_minute type: object entities.MessageThread: properties: @@ -305,18 +306,18 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - color - - contact - - created_at - - id - - is_archived - - last_message_content - - last_message_id - - order_timestamp - - owner - - status - - updated_at - - user_id + - color + - contact + - created_at + - id + - is_archived + - last_message_content + - last_message_id + - order_timestamp + - owner + - status + - updated_at + - user_id type: object entities.Phone: properties: @@ -330,12 +331,14 @@ definitions: example: 32343a19-da5e-4b1b-a767-3298a73703cb type: string max_send_attempts: - description: MaxSendAttempts determines how many times to retry sending an + description: + MaxSendAttempts determines how many times to retry sending an SMS message example: 2 type: integer message_expiration_seconds: - description: MessageExpirationSeconds is the duration in seconds after sending + description: + MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. type: integer messages_per_minute: @@ -351,7 +354,7 @@ definitions: example: 32343a19-da5e-4b1b-a767-3298a73703cb type: string sim: - $ref: '#/definitions/entities.SIM' + $ref: "#/definitions/entities.SIM" updated_at: example: "2022-06-05T14:26:10.303278+03:00" type: string @@ -359,16 +362,16 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - id - - max_send_attempts - - message_expiration_seconds - - messages_per_minute - - phone_number - - schedule_id - - sim - - updated_at - - user_id + - created_at + - id + - max_send_attempts + - message_expiration_seconds + - messages_per_minute + - phone_number + - schedule_id + - sim + - updated_at + - user_id type: object entities.PhoneAPIKey: properties: @@ -386,15 +389,15 @@ definitions: type: string phone_ids: example: - - 32343a19-da5e-4b1b-a767-3298a73703cb - - 32343a19-da5e-4b1b-a767-3298a73703cc + - 32343a19-da5e-4b1b-a767-3298a73703cb + - 32343a19-da5e-4b1b-a767-3298a73703cc items: type: string type: array phone_numbers: example: - - "+18005550199" - - "+18005550100" + - "+18005550199" + - "+18005550100" items: type: string type: array @@ -408,50 +411,50 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - api_key - - created_at - - id - - name - - phone_ids - - phone_numbers - - updated_at - - user_email - - user_id + - api_key + - created_at + - id + - name + - phone_ids + - phone_numbers + - updated_at + - user_email + - user_id type: object entities.SIM: enum: - - SIM1 - - SIM2 + - SIM1 + - SIM2 type: string x-enum-varnames: - - SIM1 - - SIM2 + - SIM1 + - SIM2 entities.SubscriptionName: enum: - - free - - pro-monthly - - pro-yearly - - ultra-monthly - - ultra-yearly - - pro-lifetime - - 20k-monthly - - 100k-monthly - - 50k-monthly - - 200k-monthly - - 20k-yearly + - free + - pro-monthly + - pro-yearly + - ultra-monthly + - ultra-yearly + - pro-lifetime + - 20k-monthly + - 100k-monthly + - 50k-monthly + - 200k-monthly + - 20k-yearly type: string x-enum-varnames: - - SubscriptionNameFree - - SubscriptionNameProMonthly - - SubscriptionNameProYearly - - SubscriptionNameUltraMonthly - - SubscriptionNameUltraYearly - - SubscriptionNameProLifetime - - SubscriptionName20KMonthly - - SubscriptionName100KMonthly - - SubscriptionName50KMonthly - - SubscriptionName200KMonthly - - SubscriptionName20KYearly + - SubscriptionNameFree + - SubscriptionNameProMonthly + - SubscriptionNameProYearly + - SubscriptionNameUltraMonthly + - SubscriptionNameUltraYearly + - SubscriptionNameProLifetime + - SubscriptionName20KMonthly + - SubscriptionName100KMonthly + - SubscriptionName50KMonthly + - SubscriptionName200KMonthly + - SubscriptionName20KYearly entities.User: properties: active_phone_id: @@ -489,7 +492,7 @@ definitions: type: string subscription_name: allOf: - - $ref: '#/definitions/entities.SubscriptionName' + - $ref: "#/definitions/entities.SubscriptionName" example: free subscription_renews_at: example: "2022-06-05T14:26:02.302718+03:00" @@ -504,18 +507,18 @@ definitions: example: "2022-06-05T14:26:10.303278+03:00" type: string required: - - api_key - - created_at - - email - - id - - notification_heartbeat_enabled - - notification_message_status_enabled - - notification_newsletter_enabled - - notification_webhook_enabled - - subscription_id - - subscription_name - - timezone - - updated_at + - api_key + - created_at + - email + - id + - notification_heartbeat_enabled + - notification_message_status_enabled + - notification_newsletter_enabled + - notification_webhook_enabled + - subscription_id + - subscription_name + - timezone + - updated_at type: object entities.Webhook: properties: @@ -524,7 +527,7 @@ definitions: type: string events: example: - - message.phone.received + - message.phone.received items: type: string type: array @@ -533,8 +536,8 @@ definitions: type: string phone_numbers: example: - - "+18005550199" - - "+18005550100" + - "+18005550199" + - "+18005550100" items: type: string type: array @@ -551,14 +554,14 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - events - - id - - phone_numbers - - signing_key - - updated_at - - url - - user_id + - created_at + - events + - id + - phone_numbers + - signing_key + - updated_at + - url + - user_id type: object requests.DiscordStore: properties: @@ -569,9 +572,9 @@ definitions: server_id: type: string required: - - incoming_channel_id - - name - - server_id + - incoming_channel_id + - name + - server_id type: object requests.DiscordUpdate: properties: @@ -582,9 +585,9 @@ definitions: server_id: type: string required: - - incoming_channel_id - - name - - server_id + - incoming_channel_id + - name + - server_id type: object requests.HeartbeatStore: properties: @@ -595,8 +598,8 @@ definitions: type: string type: array required: - - charging - - phone_numbers + - charging + - phone_numbers type: object requests.MessageAttachment: properties: @@ -613,14 +616,15 @@ definitions: example: photo.jpg type: string required: - - content - - content_type - - name + - content + - content_type + - name type: object requests.MessageBulkSend: properties: attachments: - description: Attachments are optional. When you provide a list of attachments, + description: + Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS items: type: string @@ -629,7 +633,8 @@ definitions: example: This is a sample text message type: string encrypted: - description: Encrypted is used to determine if the content is end-to-end encrypted. + description: + Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false type: boolean @@ -637,21 +642,22 @@ definitions: example: "+18005550199" type: string request_id: - description: RequestID is an optional parameter used to track a request from + description: + RequestID is an optional parameter used to track a request from the client's perspective example: 153554b5-ae44-44a0-8f4f-7bbac5657ad4 type: string to: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array required: - - content - - from - - to + - content + - from + - to type: object requests.MessageCallMissed: properties: @@ -668,10 +674,10 @@ definitions: example: "+18005550100" type: string required: - - from - - sim - - timestamp - - to + - from + - sim + - timestamp + - to type: object requests.MessageEvent: properties: @@ -687,28 +693,31 @@ definitions: description: Reason is the exact error message in case the event is an error type: string timestamp: - description: Timestamp is the time when the event was emitted, Please send + description: + Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible example: "2022-06-05T14:26:09.527976+03:00" type: string required: - - event_name - - reason - - timestamp + - event_name + - reason + - timestamp type: object requests.MessageReceive: properties: attachments: - description: Attachments is the list of MMS attachments received with the + description: + Attachments is the list of MMS attachments received with the message items: - $ref: '#/definitions/requests.MessageAttachment' + $ref: "#/definitions/requests.MessageAttachment" type: array content: example: This is a sample text message received on a phone type: string encrypted: - description: Encrypted is used to determine if the content is end-to-end encrypted. + description: + Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false type: boolean @@ -717,11 +726,12 @@ definitions: type: string sim: allOf: - - $ref: '#/definitions/entities.SIM' + - $ref: "#/definitions/entities.SIM" description: SIM card that received the message example: SIM1 timestamp: - description: Timestamp is the time when the event was emitted, Please send + description: + Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible example: "2022-06-05T14:26:09.527976+03:00" type: string @@ -729,21 +739,22 @@ definitions: example: "+18005550100" type: string required: - - content - - encrypted - - from - - sim - - timestamp - - to + - content + - encrypted + - from + - sim + - timestamp + - to type: object requests.MessageSend: properties: attachments: - description: Attachments are optional. When you provide a list of attachments, + description: + Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS example: - - https://example.com/image.jpg - - https://example.com/video.mp4 + - https://example.com/image.jpg + - https://example.com/video.mp4 items: type: string type: array @@ -751,7 +762,8 @@ definitions: example: This is a sample text message type: string encrypted: - description: Encrypted is an optional parameter used to determine if the content + description: + Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false @@ -760,12 +772,14 @@ definitions: example: "+18005550199" type: string request_id: - description: RequestID is an optional parameter used to track a request from + description: + RequestID is an optional parameter used to track a request from the client's perspective example: 153554b5-ae44-44a0-8f4f-7bbac5657ad4 type: string send_at: - description: SendAt is an optional parameter used to schedule a message to + description: + SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future. @@ -775,9 +789,40 @@ definitions: example: "+18005550100" type: string required: - - content - - from - - to + - content + - from + - to + type: object + requests.MessageSendScheduleStore: + properties: + is_active: + type: boolean + name: + type: string + timezone: + type: string + windows: + items: + $ref: "#/definitions/requests.MessageSendScheduleWindow" + type: array + required: + - is_active + - name + - timezone + - windows + type: object + requests.MessageSendScheduleWindow: + properties: + day_of_week: + type: integer + end_minute: + type: integer + start_minute: + type: integer + required: + - day_of_week + - end_minute + - start_minute type: object requests.MessageThreadUpdate: properties: @@ -785,7 +830,7 @@ definitions: example: true type: boolean required: - - is_archived + - is_archived type: object requests.PhoneAPIKeyStoreRequest: properties: @@ -793,7 +838,7 @@ definitions: example: My Phone API Key type: string required: - - name + - name type: object requests.PhoneFCMToken: properties: @@ -801,17 +846,18 @@ definitions: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd..... type: string phone_number: - example: '[+18005550199]' + example: "[+18005550199]" type: string sim: - description: SIM is the SIM slot of the phone in case the phone has more than + description: + SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot example: SIM1 type: string required: - - fcm_token - - phone_number - - sim + - fcm_token + - phone_number + - sim type: object requests.PhoneUpsert: properties: @@ -819,12 +865,14 @@ definitions: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd..... type: string max_send_attempts: - description: MaxSendAttempts is the number of attempts when sending an SMS + description: + MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline. example: 2 type: integer message_expiration_seconds: - description: MessageExpirationSeconds is the duration in seconds after sending + description: + MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. example: 12345 type: integer @@ -841,50 +889,20 @@ definitions: example: 32343a19-da5e-4b1b-a767-3298a73703cb type: string sim: - description: SIM is the SIM slot of the phone in case the phone has more than + description: + SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot example: SIM1 type: string required: - - fcm_token - - max_send_attempts - - message_expiration_seconds - - messages_per_minute - - missed_call_auto_reply - - phone_number - - schedule_id - - sim - type: object - requests.SendScheduleStore: - properties: - is_active: - type: boolean - name: - type: string - timezone: - type: string - windows: - items: - $ref: '#/definitions/requests.SendScheduleWindow' - type: array - required: - - is_active - - name - - timezone - - windows - type: object - requests.SendScheduleWindow: - properties: - day_of_week: - type: integer - end_minute: - type: integer - start_minute: - type: integer - required: - - day_of_week - - end_minute - - start_minute + - fcm_token + - max_send_attempts + - message_expiration_seconds + - messages_per_minute + - missed_call_auto_reply + - phone_number + - schedule_id + - sim type: object requests.UserNotificationUpdate: properties: @@ -901,10 +919,10 @@ definitions: example: true type: boolean required: - - heartbeat_enabled - - message_status_enabled - - newsletter_enabled - - webhook_enabled + - heartbeat_enabled + - message_status_enabled + - newsletter_enabled + - webhook_enabled type: object requests.UserPaymentInvoice: properties: @@ -930,13 +948,13 @@ definitions: example: "9800" type: string required: - - address - - city - - country - - name - - notes - - state - - zip_code + - address + - city + - country + - name + - notes + - state + - zip_code type: object requests.UserUpdate: properties: @@ -947,8 +965,8 @@ definitions: example: Europe/Helsinki type: string required: - - active_phone_id - - timezone + - active_phone_id + - timezone type: object requests.WebhookStore: properties: @@ -958,8 +976,8 @@ definitions: type: array phone_numbers: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array @@ -968,10 +986,10 @@ definitions: url: type: string required: - - events - - phone_numbers - - signing_key - - url + - events + - phone_numbers + - signing_key + - url type: object requests.WebhookUpdate: properties: @@ -981,8 +999,8 @@ definitions: type: array phone_numbers: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array @@ -991,10 +1009,10 @@ definitions: url: type: string required: - - events - - phone_numbers - - signing_key - - url + - events + - phone_numbers + - signing_key + - url type: object responses.BadRequest: properties: @@ -1008,14 +1026,14 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.BillingUsageResponse: properties: data: - $ref: '#/definitions/entities.BillingUsage' + $ref: "#/definitions/entities.BillingUsage" message: example: Request handled successfully type: string @@ -1023,15 +1041,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.BillingUsagesResponse: properties: data: items: - $ref: '#/definitions/entities.BillingUsage' + $ref: "#/definitions/entities.BillingUsage" type: array message: example: Request handled successfully @@ -1040,14 +1058,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.DiscordResponse: properties: data: - $ref: '#/definitions/entities.Discord' + $ref: "#/definitions/entities.Discord" message: example: Request handled successfully type: string @@ -1055,15 +1073,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.DiscordsResponse: properties: data: items: - $ref: '#/definitions/entities.Discord' + $ref: "#/definitions/entities.Discord" type: array message: example: Request handled successfully @@ -1072,14 +1090,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.HeartbeatResponse: properties: data: - $ref: '#/definitions/entities.Heartbeat' + $ref: "#/definitions/entities.Heartbeat" message: example: Request handled successfully type: string @@ -1087,15 +1105,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.HeartbeatsResponse: properties: data: items: - $ref: '#/definitions/entities.Heartbeat' + $ref: "#/definitions/entities.Heartbeat" type: array message: example: Request handled successfully @@ -1104,9 +1122,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.InternalServerError: properties: @@ -1117,13 +1135,13 @@ definitions: example: error type: string required: - - message - - status + - message + - status type: object responses.MessageResponse: properties: data: - $ref: '#/definitions/entities.Message' + $ref: "#/definitions/entities.Message" message: example: Request handled successfully type: string @@ -1131,15 +1149,30 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status + type: object + responses.MessageSendScheduleResponse: + properties: + data: + $ref: "#/definitions/entities.MessageSendSchedule" + message: + example: Request handled successfully + type: string + status: + example: success + type: string + required: + - data + - message + - status type: object responses.MessageThreadsResponse: properties: data: items: - $ref: '#/definitions/entities.MessageThread' + $ref: "#/definitions/entities.MessageThread" type: array message: example: Request handled successfully @@ -1148,15 +1181,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.MessagesResponse: properties: data: items: - $ref: '#/definitions/entities.Message' + $ref: "#/definitions/entities.Message" type: array message: example: Request handled successfully @@ -1165,9 +1198,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.NoContent: properties: @@ -1178,8 +1211,8 @@ definitions: example: success type: string required: - - message - - status + - message + - status type: object responses.NotFound: properties: @@ -1190,8 +1223,8 @@ definitions: example: error type: string required: - - message - - status + - message + - status type: object responses.OkString: properties: @@ -1204,14 +1237,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhoneAPIKeyResponse: properties: data: - $ref: '#/definitions/entities.PhoneAPIKey' + $ref: "#/definitions/entities.PhoneAPIKey" message: example: Request handled successfully type: string @@ -1219,15 +1252,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhoneAPIKeysResponse: properties: data: items: - $ref: '#/definitions/entities.PhoneAPIKey' + $ref: "#/definitions/entities.PhoneAPIKey" type: array message: example: Request handled successfully @@ -1236,14 +1269,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhoneResponse: properties: data: - $ref: '#/definitions/entities.Phone' + $ref: "#/definitions/entities.Phone" message: example: Request handled successfully type: string @@ -1251,15 +1284,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhonesResponse: properties: data: items: - $ref: '#/definitions/entities.Phone' + $ref: "#/definitions/entities.Phone" type: array message: example: Request handled successfully @@ -1268,24 +1301,9 @@ definitions: example: success type: string required: - - data - - message - - status - type: object - responses.SendScheduleResponse: - properties: - data: - $ref: '#/definitions/entities.MessageSendSchedule' - message: - example: Request handled successfully - type: string - status: - example: success - type: string - required: - - data - - message - - status + - data + - message + - status type: object responses.Unauthorized: properties: @@ -1299,9 +1317,9 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UnprocessableEntity: properties: @@ -1318,14 +1336,14 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UserResponse: properties: data: - $ref: '#/definitions/entities.User' + $ref: "#/definitions/entities.User" message: example: Request handled successfully type: string @@ -1333,9 +1351,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UserSubscriptionPaymentsResponse: properties: @@ -1398,42 +1416,42 @@ definitions: updated_at: type: string required: - - billing_reason - - card_brand - - card_last_four - - created_at - - currency - - currency_rate - - discount_total - - discount_total_formatted - - discount_total_usd - - refunded - - refunded_amount - - refunded_amount_formatted - - refunded_amount_usd - - refunded_at - - status - - status_formatted - - subtotal - - subtotal_formatted - - subtotal_usd - - tax - - tax_formatted - - tax_inclusive - - tax_usd - - total - - total_formatted - - total_usd - - updated_at + - billing_reason + - card_brand + - card_last_four + - created_at + - currency + - currency_rate + - discount_total + - discount_total_formatted + - discount_total_usd + - refunded + - refunded_amount + - refunded_amount_formatted + - refunded_amount_usd + - refunded_at + - status + - status_formatted + - subtotal + - subtotal_formatted + - subtotal_usd + - tax + - tax_formatted + - tax_inclusive + - tax_usd + - total + - total_formatted + - total_usd + - updated_at type: object id: type: string type: type: string required: - - attributes - - id - - type + - attributes + - id + - type type: object type: array message: @@ -1443,14 +1461,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.WebhookResponse: properties: data: - $ref: '#/definitions/entities.Webhook' + $ref: "#/definitions/entities.Webhook" message: example: Request handled successfully type: string @@ -1458,15 +1476,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.WebhooksResponse: properties: data: items: - $ref: '#/definitions/entities.Webhook' + $ref: "#/definitions/entities.Webhook" type: array message: example: Request handled successfully @@ -1475,16 +1493,17 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object host: api.httpsms.com info: contact: email: support@httpsms.com name: support@httpsms.com - description: Use your Android phone to send and receive SMS messages via a simple + description: + Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption. license: name: AGPL-3.0 @@ -1495,1834 +1514,1859 @@ paths: /billing/usage: get: consumes: - - application/json - description: Get the summary of sent and received messages for a user in the + - application/json + description: + Get the summary of sent and received messages for a user in the current month produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.BillingUsageResponse' + $ref: "#/definitions/responses.BillingUsageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get Billing Usage. tags: - - Billing + - Billing /billing/usage-history: get: consumes: - - application/json - description: Get billing usage records of sent and received messages for a user + - application/json + description: + Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order. parameters: - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: number of heartbeats to return - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: number of heartbeats to return + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.BillingUsagesResponse' + $ref: "#/definitions/responses.BillingUsagesResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get billing usage history. tags: - - Billing + - Billing /bulk-messages: post: consumes: - - multipart/form-data - description: Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) + - multipart/form-data + description: + Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx). parameters: - - description: The Excel or CSV file containing the messages to be sent. - in: formData - name: document - required: true - type: file + - description: The Excel or CSV file containing the messages to be sent. + in: formData + name: document + required: true + type: file produces: - - application/json + - application/json responses: "202": description: Accepted schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store bulk SMS file tags: - - BulkSMS + - BulkSMS /discord-integrations: get: consumes: - - application/json + - application/json description: Get the discord integrations of a user parameters: - - description: number of discord integrations to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter discord integrations containing query - in: query - name: query - type: string - - description: number of discord integrations to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of discord integrations to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter discord integrations containing query + in: query + name: query + type: string + - description: number of discord integrations to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.DiscordsResponse' + $ref: "#/definitions/responses.DiscordsResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get discord integrations of a user tags: - - DiscordIntegration + - DiscordIntegration post: consumes: - - application/json + - application/json description: Store a discord integration for the authenticated user parameters: - - description: Payload of the discord integration request - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.DiscordStore' + - description: Payload of the discord integration request + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.DiscordStore" produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: '#/definitions/responses.DiscordResponse' + $ref: "#/definitions/responses.DiscordResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store discord integration tags: - - DiscordIntegration + - DiscordIntegration /discord-integrations/{discordID}: delete: consumes: - - application/json + - application/json description: Delete a discord integration for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the discord integration - in: path - name: discordID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the discord integration + in: path + name: discordID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete discord integration tags: - - Webhooks + - Webhooks put: consumes: - - application/json + - application/json description: Update a discord integration for the currently authenticated user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the discord integration - in: path - name: discordID - required: true - type: string - - description: Payload of discord integration to update - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.DiscordUpdate' + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the discord integration + in: path + name: discordID + required: true + type: string + - description: Payload of discord integration to update + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.DiscordUpdate" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.DiscordResponse' + $ref: "#/definitions/responses.DiscordResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a discord integration tags: - - DiscordIntegration + - DiscordIntegration /discord/event: post: consumes: - - application/json + - application/json description: Publish a discord event to the registered listeners produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" summary: Consume a discord event tags: - - Discord + - Discord /heartbeats: get: consumes: - - application/json - description: Get the last time a phone number requested for outstanding messages. + - application/json + description: + Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: the owner's phone number - in: query - name: owner - required: true - type: string - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter containing query - in: query - name: query - type: string - - description: number of heartbeats to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: the owner's phone number + in: query + name: owner + required: true + type: string + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter containing query + in: query + name: query + type: string + - description: number of heartbeats to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.HeartbeatsResponse' + $ref: "#/definitions/responses.HeartbeatsResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get heartbeats of an owner phone number tags: - - Heartbeats + - Heartbeats post: consumes: - - application/json - description: Store the heartbeat to make notify that a phone number is still + - application/json + description: + Store the heartbeat to make notify that a phone number is still active parameters: - - description: Payload of the heartbeat request - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.HeartbeatStore' + - description: Payload of the heartbeat request + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.HeartbeatStore" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.HeartbeatResponse' + $ref: "#/definitions/responses.HeartbeatResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Register heartbeat of an owner phone number tags: - - Heartbeats + - Heartbeats /integration/3cx/messages: post: consumes: - - application/json + - application/json description: Sends an SMS message from the 3CX platform produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" summary: Sends a 3CX SMS message tags: - - 3CXIntegration + - 3CXIntegration /message-threads: get: consumes: - - application/json - description: Get list of contacts which a phone number has communicated with + - application/json + description: + Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: owner phone number - in: query - name: owner - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter message threads containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: owner phone number + in: query + name: owner + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter message threads containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageThreadsResponse' + $ref: "#/definitions/responses.MessageThreadsResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get message threads for a phone number tags: - - MessageThreads + - MessageThreads /message-threads/{messageThreadID}: delete: consumes: - - application/json - description: Delete a message thread from the database and also deletes all + - application/json + description: + Delete a message thread from the database and also deletes all the messages in the thread. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message thread - in: path - name: messageThreadID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message thread + in: path + name: messageThreadID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a message thread from the database. tags: - - MessageThreads + - MessageThreads put: consumes: - - application/json + - application/json description: Updates the details of a message thread parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message thread - in: path - name: messageThreadID - required: true - type: string - - description: Payload of message thread details to update - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageThreadUpdate' + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message thread + in: path + name: messageThreadID + required: true + type: string + - description: Payload of message thread details to update + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageThreadUpdate" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: "#/definitions/responses.PhoneResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a message thread tags: - - MessageThreads + - MessageThreads /messages: get: consumes: - - application/json - description: Get list of messages which are sent between 2 phone numbers. It + - application/json + description: + Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: the owner's phone number - in: query - name: owner - required: true - type: string - - default: "+18005550100" - description: the contact's phone number - in: query - name: contact - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter messages containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: the owner's phone number + in: query + name: owner + required: true + type: string + - default: "+18005550100" + description: the contact's phone number + in: query + name: contact + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter messages containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessagesResponse' + $ref: "#/definitions/responses.MessagesResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get messages which are sent between 2 phone numbers tags: - - Messages + - Messages /messages/{messageID}: delete: consumes: - - application/json - description: Delete a message from the database and removes the message content + - application/json + description: + Delete a message from the database and removes the message content from the list of threads. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a message from the database. tags: - - Messages + - Messages get: consumes: - - application/json + - application/json description: Get a message from the database by the message ID. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get a message from the database. tags: - - Messages + - Messages /messages/{messageID}/events: post: consumes: - - application/json - description: Use this endpoint to send events for a message when it is failed, + - application/json + description: + Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string - - description: Payload of the event emitted. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageEvent' + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string + - description: Payload of the event emitted. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageEvent" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upsert an event for a message on the mobile phone tags: - - Messages + - Messages /messages/bulk-send: post: consumes: - - application/json + - application/json description: Add bulk SMS messages to be sent by the android phone parameters: - - description: Bulk send message request payload - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageBulkSend' + - description: Bulk send message request payload + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageBulkSend" produces: - - application/json + - application/json responses: "200": description: OK schema: items: - $ref: '#/definitions/responses.MessagesResponse' + $ref: "#/definitions/responses.MessagesResponse" type: array "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Send bulk SMS messages tags: - - Messages + - Messages /messages/calls/missed: post: consumes: - - application/json - description: This endpoint is called by the httpSMS android app to register + - application/json + description: + This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone. parameters: - - description: Payload of the missed call event. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageCallMissed' + - description: Payload of the missed call event. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageCallMissed" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Register a missed call event on the mobile phone tags: - - Messages + - Messages /messages/outstanding: get: consumes: - - application/json + - application/json description: Get an outstanding message to be sent by an android phone parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703cb - description: The ID of the message - in: query - name: message_id - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703cb + description: The ID of the message + in: query + name: message_id + required: true + type: string produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get an outstanding message tags: - - Messages + - Messages /messages/receive: post: consumes: - - application/json + - application/json description: Add a new message received from a mobile phone parameters: - - description: Received message request payload - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageReceive' + - description: Received message request payload + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageReceive" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Receive a new SMS message from a mobile phone tags: - - Messages + - Messages /messages/search: get: consumes: - - application/json - description: This returns the list of all messages based on the filter criteria + - application/json + description: + This returns the list of all messages based on the filter criteria including missed calls parameters: - - description: Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/ - in: header - name: token - required: true - type: string - - default: +18005550199,+18005550100 - description: the owner's phone numbers - in: query - name: owners - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter messages containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 200 - minimum: 1 - name: limit - type: integer + - description: Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/ + in: header + name: token + required: true + type: string + - default: +18005550199,+18005550100 + description: the owner's phone numbers + in: query + name: owners + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter messages containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 200 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessagesResponse' + $ref: "#/definitions/responses.MessagesResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Search all messages of a user tags: - - Messages + - Messages /messages/send: post: consumes: - - application/json + - application/json description: Add a new SMS message to be sent by your Android phone parameters: - - description: Send message request payload - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.MessageSend' + - description: Send message request payload + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageSend" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.MessageResponse' + $ref: "#/definitions/responses.MessageResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Send an SMS message tags: - - Messages + - Messages /phone-api-keys: get: consumes: - - application/json - description: Get list phone API keys which a user has registered on the httpSMS + - application/json + description: + Get list phone API keys which a user has registered on the httpSMS application parameters: - - description: number of phone api keys to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter phone api keys with name containing query - in: query - name: query - type: string - - description: number of phone api keys to return - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer + - description: number of phone api keys to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter phone api keys with name containing query + in: query + name: query + type: string + - description: number of phone api keys to return + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneAPIKeysResponse' + $ref: "#/definitions/responses.PhoneAPIKeysResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get the phone API keys of a user tags: - - PhoneAPIKeys + - PhoneAPIKeys post: consumes: - - application/json - description: Creates a new phone API key which can be used to log in to the + - application/json + description: + Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone parameters: - - description: Payload of new phone API key. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.PhoneAPIKeyStoreRequest' + - description: Payload of new phone API key. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.PhoneAPIKeyStoreRequest" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneAPIKeyResponse' + $ref: "#/definitions/responses.PhoneAPIKeyResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store phone API key tags: - - PhoneAPIKeys + - PhoneAPIKeys /phone-api-keys/{phoneAPIKeyID}: delete: consumes: - - application/json - description: Delete a phone API Key from the database and cannot be used for + - application/json + description: + Delete a phone API Key from the database and cannot be used for authentication anymore. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone API key - in: path - name: phoneAPIKeyID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone API key + in: path + name: phoneAPIKeyID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a phone API key from the database. tags: - - PhoneAPIKeys + - PhoneAPIKeys /phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}: delete: consumes: - - application/json - description: You will need to login again to the httpSMS app on your Android + - application/json + description: + You will need to login again to the httpSMS app on your Android phone with a new phone API key. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone API key - in: path - name: phoneAPIKeyID - required: true - type: string - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone - in: path - name: phoneID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone API key + in: path + name: phoneAPIKeyID + required: true + type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone + in: path + name: phoneID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Remove the association of a phone from the phone API key. tags: - - PhoneAPIKeys + - PhoneAPIKeys /phones: get: consumes: - - application/json - description: Get list of phones which a user has registered on the http sms + - application/json + description: + Get list of phones which a user has registered on the http sms application parameters: - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter phones containing query - in: query - name: query - type: string - - description: number of phones to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter phones containing query + in: query + name: query + type: string + - description: number of phones to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhonesResponse' + $ref: "#/definitions/responses.PhonesResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get phones of a user tags: - - Phones + - Phones put: consumes: - - application/json - description: Updates properties of a user's phone. If the phone with this number + - application/json + description: + Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert' parameters: - - description: Payload of new phone number. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.PhoneUpsert' + - description: Payload of new phone number. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.PhoneUpsert" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: "#/definitions/responses.PhoneResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upsert Phone tags: - - Phones + - Phones /phones/{phoneID}: delete: consumes: - - application/json + - application/json description: Delete a phone that has been sored in the database parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone - in: path - name: phoneID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone + in: path + name: phoneID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete Phone tags: - - Phones + - Phones /phones/fcm-token: put: consumes: - - application/json - description: Updates the FCM token of a phone. If the phone with this number + - application/json + description: + Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert' parameters: - - description: Payload of new FCM token. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.PhoneFCMToken' + - description: Payload of new FCM token. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.PhoneFCMToken" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: "#/definitions/responses.PhoneResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upserts the FCM token of a phone tags: - - Phones + - Phones /send-schedules: get: description: List all send schedules owned by the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: items: - $ref: '#/definitions/entities.MessageSendSchedule' + $ref: "#/definitions/entities.MessageSendSchedule" type: array "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: List send schedules tags: - - Send Schedules + - Send Schedules post: consumes: - - application/json + - application/json description: Create a new send schedule for the authenticated user. parameters: - - description: Payload of new send schedule. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.SendScheduleStore' + - description: Payload of new send schedule. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageSendScheduleStore" produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: '#/definitions/responses.SendScheduleResponse' + $ref: "#/definitions/responses.MessageSendScheduleResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" + "402": + description: Payment Required + schema: + $ref: "#/definitions/responses.BadRequest" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Create send schedule tags: - - Send Schedules + - Send Schedules /send-schedules/{scheduleID}: delete: description: Delete a send schedule owned by the authenticated user. parameters: - - description: Schedule ID - in: path - name: scheduleID - required: true - type: string + - description: Schedule ID + in: path + name: scheduleID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete send schedule tags: - - Send Schedules + - Send Schedules put: consumes: - - application/json + - application/json description: Update a send schedule owned by the authenticated user. parameters: - - description: Schedule ID - in: path - name: scheduleID - required: true - type: string - - description: Payload of updated send schedule. - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.SendScheduleStore' + - description: Schedule ID + in: path + name: scheduleID + required: true + type: string + - description: Payload of updated send schedule. + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.MessageSendScheduleStore" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.SendScheduleResponse' + $ref: "#/definitions/responses.MessageSendScheduleResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update send schedule tags: - - Send Schedules + - Send Schedules /users/{userID}/api-keys: delete: consumes: - - application/json + - application/json description: Rotate the user's API key in case the current API Key is compromised parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the user to update - in: path - name: userID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the user to update + in: path + name: userID + required: true + type: string produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.UserResponse' + $ref: "#/definitions/responses.UserResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Rotate the user's API Key tags: - - Users + - Users /users/{userID}/notifications: put: consumes: - - application/json + - application/json description: Update the email notification settings for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the user to update - in: path - name: userID - required: true - type: string - - description: User notification details to update - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.UserNotificationUpdate' + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the user to update + in: path + name: userID + required: true + type: string + - description: User notification details to update + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.UserNotificationUpdate" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.UserResponse' + $ref: "#/definitions/responses.UserResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update notification settings tags: - - Users + - Users /users/me: delete: consumes: - - application/json - description: Deletes the currently authenticated user together with all their + - application/json + description: + Deletes the currently authenticated user together with all their data. produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a user tags: - - Users + - Users get: consumes: - - application/json + - application/json description: Get details of the currently authenticated user produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.UserResponse' + $ref: "#/definitions/responses.UserResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get current user tags: - - Users + - Users put: consumes: - - application/json + - application/json description: Updates the details of the currently authenticated user parameters: - - description: Payload of user details to update - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.UserUpdate' + - description: Payload of user details to update + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.UserUpdate" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: "#/definitions/responses.PhoneResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a user tags: - - Users + - Users /users/subscription: delete: description: Cancel the subscription of the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Cancel the user's subscription tags: - - Users + - Users /users/subscription-update-url: get: description: Fetches the subscription URL of the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.OkString' + $ref: "#/definitions/responses.OkString" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Currently authenticated user subscription update URL tags: - - Users + - Users /users/subscription/invoices/{subscriptionInvoiceID}: post: consumes: - - application/json - description: Generates a new invoice PDF file for the given subscription payment + - application/json + description: + Generates a new invoice PDF file for the given subscription payment with given parameters. parameters: - - description: Generate subscription payment invoice parameters - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.UserPaymentInvoice' - - description: ID of the subscription invoice to generate the PDF for - in: path - name: subscriptionInvoiceID - required: true - type: string + - description: Generate subscription payment invoice parameters + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.UserPaymentInvoice" + - description: ID of the subscription invoice to generate the PDF for + in: path + name: subscriptionInvoiceID + required: true + type: string produces: - - application/pdf + - application/pdf responses: "200": description: OK @@ -3331,85 +3375,86 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Generate a subscription payment invoice tags: - - Users + - Users /users/subscription/payments: get: consumes: - - application/json - description: Subscription payments are generated throughout the lifecycle of + - application/json + description: + Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.UserSubscriptionPaymentsResponse' + $ref: "#/definitions/responses.UserSubscriptionPaymentsResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get the last 10 subscription payments. tags: - - Users + - Users /v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}: get: description: Download an MMS attachment by its path components parameters: - - description: User ID - in: path - name: userID - required: true - type: string - - description: Message ID - in: path - name: messageID - required: true - type: string - - description: Attachment index - in: path - name: attachmentIndex - required: true - type: string - - description: Filename with extension - in: path - name: filename - required: true - type: string + - description: User ID + in: path + name: userID + required: true + type: string + - description: Message ID + in: path + name: messageID + required: true + type: string + - description: Attachment index + in: path + name: attachmentIndex + required: true + type: string + - description: Filename with extension + in: path + name: filename + required: true + type: string produces: - - application/octet-stream + - application/octet-stream responses: "200": description: OK @@ -3418,189 +3463,189 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/responses.NotFound' + $ref: "#/definitions/responses.NotFound" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" summary: Download a message attachment tags: - - Attachments + - Attachments /webhooks: get: consumes: - - application/json + - application/json description: Get the webhooks of a user parameters: - - description: number of webhooks to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter webhooks containing query - in: query - name: query - type: string - - description: number of webhooks to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of webhooks to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter webhooks containing query + in: query + name: query + type: string + - description: number of webhooks to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.WebhooksResponse' + $ref: "#/definitions/responses.WebhooksResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get webhooks of a user tags: - - Webhooks + - Webhooks post: consumes: - - application/json + - application/json description: Store a webhook for the authenticated user parameters: - - description: Payload of the webhook request - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.WebhookStore' + - description: Payload of the webhook request + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.WebhookStore" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.WebhookResponse' + $ref: "#/definitions/responses.WebhookResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store a webhook tags: - - Webhooks + - Webhooks /webhooks/{webhookID}: delete: consumes: - - application/json + - application/json description: Delete a webhook for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the webhook - in: path - name: webhookID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the webhook + in: path + name: webhookID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: '#/definitions/responses.NoContent' + $ref: "#/definitions/responses.NoContent" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete webhook tags: - - Webhooks + - Webhooks put: consumes: - - application/json + - application/json description: Update a webhook for the currently authenticated user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the webhook - in: path - name: webhookID - required: true - type: string - - description: Payload of webhook details to update - in: body - name: payload - required: true - schema: - $ref: '#/definitions/requests.WebhookUpdate' + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the webhook + in: path + name: webhookID + required: true + type: string + - description: Payload of webhook details to update + in: body + name: payload + required: true + schema: + $ref: "#/definitions/requests.WebhookUpdate" produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: '#/definitions/responses.WebhookResponse' + $ref: "#/definitions/responses.WebhookResponse" "400": description: Bad Request schema: - $ref: '#/definitions/responses.BadRequest' + $ref: "#/definitions/responses.BadRequest" "401": description: Unauthorized schema: - $ref: '#/definitions/responses.Unauthorized' + $ref: "#/definitions/responses.Unauthorized" "422": description: Unprocessable Entity schema: - $ref: '#/definitions/responses.UnprocessableEntity' + $ref: "#/definitions/responses.UnprocessableEntity" "500": description: Internal Server Error schema: - $ref: '#/definitions/responses.InternalServerError' + $ref: "#/definitions/responses.InternalServerError" security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a webhook tags: - - Webhooks + - Webhooks schemes: -- https + - https securityDefinitions: ApiKeyAuth: in: header diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 5727f6f7f..66b57468d 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -128,8 +128,8 @@ func NewContainer(projectID string, version string) (container *Container) { container.RegisterHeartbeatListeners() container.RegisterUserRoutes() - container.RegisterSendScheduleRoutes() - container.RegisterSendScheduleListeners() + container.RegisterMessageSendScheduleRoutes() + container.RegisterMessageSendScheduleListeners() container.RegisterUserListeners() container.RegisterPhoneRoutes() @@ -751,43 +751,43 @@ func (container *Container) PhoneRepository() (repository repositories.PhoneRepo ) } -// SendScheduleRepository creates a new instance of repositories.SendScheduleRepository -func (container *Container) SendScheduleRepository() repositories.SendScheduleRepository { - container.logger.Debug("creating GORM repositories.SendScheduleRepository") - return repositories.NewGormSendScheduleRepository( +// MessageSendScheduleRepository creates a new instance of repositories.MessageSendScheduleRepository +func (container *Container) MessageSendScheduleRepository() repositories.MessageSendScheduleRepository { + container.logger.Debug("creating GORM repositories.MessageSendScheduleRepository") + return repositories.NewGormMessageSendScheduleRepository( container.Logger(), container.Tracer(), container.DB(), ) } -// SendScheduleService creates a new instance of services.SendScheduleService -func (container *Container) SendScheduleService() *services.SendScheduleService { - container.logger.Debug("creating services.SendScheduleService") - return services.NewSendScheduleService( +// MessageSendScheduleService creates a new instance of services.MessageSendScheduleService +func (container *Container) MessageSendScheduleService() *services.MessageSendScheduleService { + container.logger.Debug("creating services.MessageSendScheduleService") + return services.NewMessageSendScheduleService( container.Logger(), container.Tracer(), - container.SendScheduleRepository(), + container.MessageSendScheduleRepository(), ) } -// SendScheduleHandlerValidator creates a new instance of validators.SendScheduleHandlerValidator -func (container *Container) SendScheduleHandlerValidator() *validators.SendScheduleHandlerValidator { - container.logger.Debug("creating validators.SendScheduleHandlerValidator") - return validators.NewSendScheduleHandlerValidator( +// MessageSendScheduleHandlerValidator creates a new instance of validators.MessageSendScheduleHandlerValidator +func (container *Container) MessageSendScheduleHandlerValidator() *validators.MessageSendScheduleHandlerValidator { + container.logger.Debug("creating validators.MessageSendScheduleHandlerValidator") + return validators.NewMessageSendScheduleHandlerValidator( container.Logger(), container.Tracer(), ) } -// SendScheduleHandler creates a new instance of handlers.SendScheduleHandler -func (container *Container) SendScheduleHandler() *handlers.SendScheduleHandler { - container.logger.Debug("creating handlers.SendScheduleHandler") - return handlers.NewSendScheduleHandler( +// MessageSendScheduleHandler creates a new instance of handlers.MessageSendScheduleHandler +func (container *Container) MessageSendScheduleHandler() *handlers.MessageSendScheduleHandler { + container.logger.Debug("creating handlers.MessageSendScheduleHandler") + return handlers.NewMessageSendScheduleHandler( container.Logger(), container.Tracer(), - container.SendScheduleHandlerValidator(), - container.SendScheduleService(), + container.MessageSendScheduleHandlerValidator(), + container.MessageSendScheduleService(), container.EntitlementService(), ) } @@ -1147,13 +1147,13 @@ func (container *Container) RegisterMessageListeners() { } } -// RegisterSendScheduleListeners registers event listeners for listeners.SendScheduleListener -func (container *Container) RegisterSendScheduleListeners() { - container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.SendScheduleListener{})) - _, routes := listeners.NewSendScheduleListener( +// RegisterMessageSendScheduleListeners registers event listeners for listeners.MessageSendScheduleListener +func (container *Container) RegisterMessageSendScheduleListeners() { + container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.MessageSendScheduleListener{})) + _, routes := listeners.NewMessageSendScheduleListener( container.Logger(), container.Tracer(), - container.SendScheduleService(), + container.MessageSendScheduleService(), ) for event, handler := range routes { @@ -1574,7 +1574,7 @@ func (container *Container) NotificationService() (service *services.PhoneNotifi container.FirebaseMessagingClient(), container.PhoneRepository(), container.PhoneNotificationRepository(), - container.SendScheduleRepository(), + container.MessageSendScheduleRepository(), container.EventDispatcher(), ) } @@ -1630,10 +1630,10 @@ func (container *Container) RegisterUserRoutes() { container.UserHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware()) } -// RegisterSendScheduleRoutes registers routes for the /send-schedules prefix -func (container *Container) RegisterSendScheduleRoutes() { - container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.SendScheduleHandler{})) - container.SendScheduleHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware()) +// RegisterMessageSendScheduleRoutes registers routes for the /send-schedules prefix +func (container *Container) RegisterMessageSendScheduleRoutes() { + container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.MessageSendScheduleHandler{})) + container.MessageSendScheduleHandler().RegisterRoutes(container.App(), container.AuthenticatedMiddleware()) } // RegisterEventRoutes registers routes for the /events prefix diff --git a/api/pkg/entities/send_schedule.go b/api/pkg/entities/message_send_schedule.go similarity index 100% rename from api/pkg/entities/send_schedule.go rename to api/pkg/entities/message_send_schedule.go diff --git a/api/pkg/entities/send_schedule_test.go b/api/pkg/entities/message_send_schedule_test.go similarity index 100% rename from api/pkg/entities/send_schedule_test.go rename to api/pkg/entities/message_send_schedule_test.go diff --git a/api/pkg/handlers/send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go similarity index 83% rename from api/pkg/handlers/send_schedule_handler.go rename to api/pkg/handlers/message_send_schedule_handler.go index ebf475a3e..d7df8cdbb 100644 --- a/api/pkg/handlers/send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -14,26 +14,26 @@ import ( "github.com/palantir/stacktrace" ) -// SendScheduleHandler handles HTTP requests for message send schedules. -type SendScheduleHandler struct { +// MessageSendScheduleHandler handles HTTP requests for message send schedules. +type MessageSendScheduleHandler struct { handler logger telemetry.Logger tracer telemetry.Tracer - validator *validators.SendScheduleHandlerValidator - service *services.SendScheduleService + validator *validators.MessageSendScheduleHandlerValidator + service *services.MessageSendScheduleService entitlementService *services.EntitlementService } -// NewSendScheduleHandler creates a new SendScheduleHandler. -func NewSendScheduleHandler( +// NewMessageSendScheduleHandler creates a new MessageSendScheduleHandler. +func NewMessageSendScheduleHandler( logger telemetry.Logger, tracer telemetry.Tracer, - validator *validators.SendScheduleHandlerValidator, - service *services.SendScheduleService, + validator *validators.MessageSendScheduleHandlerValidator, + service *services.MessageSendScheduleService, entitlementService *services.EntitlementService, -) *SendScheduleHandler { - return &SendScheduleHandler{ - logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandler{})), +) *MessageSendScheduleHandler { + return &MessageSendScheduleHandler{ + logger: logger.WithService(fmt.Sprintf("%T", &MessageSendScheduleHandler{})), tracer: tracer, validator: validator, service: service, @@ -42,7 +42,7 @@ func NewSendScheduleHandler( } // RegisterRoutes registers send schedule routes. -func (h *SendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) { +func (h *MessageSendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) { router.Get("/v1/send-schedules", h.computeRoute(middlewares, h.Index)...) router.Post("/v1/send-schedules", h.computeRoute(middlewares, h.Store)...) router.Put("/v1/send-schedules/:scheduleID", h.computeRoute(middlewares, h.Update)...) @@ -60,7 +60,7 @@ func (h *SendScheduleHandler) RegisterRoutes(router fiber.Router, middlewares .. // @Failure 401 {object} responses.Unauthorized // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules [get] -func (h *SendScheduleHandler) Index(c *fiber.Ctx) error { +func (h *MessageSendScheduleHandler) Index(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() @@ -81,15 +81,15 @@ func (h *SendScheduleHandler) Index(c *fiber.Ctx) error { // @Tags Send Schedules // @Accept json // @Produce json -// @Param payload body requests.SendScheduleStore true "Payload of new send schedule." -// @Success 201 {object} responses.SendScheduleResponse +// @Param payload body requests.MessageSendScheduleStore true "Payload of new send schedule." +// @Success 201 {object} responses.MessageSendScheduleResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 402 {object} responses.BadRequest // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules [post] -func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { +func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() @@ -110,7 +110,7 @@ func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { return h.responsePaymentRequired(c, result.Message) } - var request requests.SendScheduleStore + var request requests.MessageSendScheduleStore if err := c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) } @@ -143,15 +143,15 @@ func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param scheduleID path string true "Schedule ID" -// @Param payload body requests.SendScheduleStore true "Payload of updated send schedule." -// @Success 200 {object} responses.SendScheduleResponse +// @Param payload body requests.MessageSendScheduleStore true "Payload of updated send schedule." +// @Success 200 {object} responses.MessageSendScheduleResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 404 {object} responses.NotFound // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules/{scheduleID} [put] -func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { +func (h *MessageSendScheduleHandler) Update(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() @@ -160,7 +160,7 @@ func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - var request requests.SendScheduleStore + var request requests.MessageSendScheduleStore if err = c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) } @@ -201,7 +201,7 @@ func (h *SendScheduleHandler) Update(c *fiber.Ctx) error { // @Failure 404 {object} responses.NotFound // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules/{scheduleID} [delete] -func (h *SendScheduleHandler) Delete(c *fiber.Ctx) error { +func (h *MessageSendScheduleHandler) Delete(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() diff --git a/api/pkg/listeners/send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go similarity index 68% rename from api/pkg/listeners/send_schedule_listener.go rename to api/pkg/listeners/message_send_schedule_listener.go index ac65eddd0..61e57b242 100644 --- a/api/pkg/listeners/send_schedule_listener.go +++ b/api/pkg/listeners/message_send_schedule_listener.go @@ -11,21 +11,21 @@ import ( "github.com/palantir/stacktrace" ) -// SendScheduleListener handles cloud events related to message send schedules. -type SendScheduleListener struct { +// MessageSendScheduleListener handles cloud events related to message send schedules. +type MessageSendScheduleListener struct { logger telemetry.Logger tracer telemetry.Tracer - service *services.SendScheduleService + service *services.MessageSendScheduleService } -// NewSendScheduleListener creates a new instance of SendScheduleListener. -func NewSendScheduleListener( +// NewMessageSendScheduleListener creates a new instance of MessageSendScheduleListener. +func NewMessageSendScheduleListener( logger telemetry.Logger, tracer telemetry.Tracer, - service *services.SendScheduleService, -) (l *SendScheduleListener, routes map[string]events.EventListener) { - l = &SendScheduleListener{ - logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleListener{})), + service *services.MessageSendScheduleService, +) (l *MessageSendScheduleListener, routes map[string]events.EventListener) { + l = &MessageSendScheduleListener{ + logger: logger.WithService(fmt.Sprintf("%T", &MessageSendScheduleListener{})), tracer: tracer, service: service, } @@ -36,7 +36,7 @@ func NewSendScheduleListener( } // onUserAccountDeleted removes all message send schedules for a deleted user account. -func (listener *SendScheduleListener) onUserAccountDeleted( +func (listener *MessageSendScheduleListener) onUserAccountDeleted( ctx context.Context, event cloudevents.Event, ) error { diff --git a/api/pkg/repositories/gorm_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go similarity index 82% rename from api/pkg/repositories/gorm_send_schedule_repository.go rename to api/pkg/repositories/gorm_message_send_schedule_repository.go index 4ee3c4089..919eb6a93 100644 --- a/api/pkg/repositories/gorm_send_schedule_repository.go +++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go @@ -12,28 +12,28 @@ import ( "gorm.io/gorm" ) -// gormSendScheduleRepository persists and loads entities.MessageSendSchedule using GORM. -type gormSendScheduleRepository struct { +// gormMessageSendScheduleRepository persists and loads entities.MessageSendSchedule using GORM. +type gormMessageSendScheduleRepository struct { logger telemetry.Logger tracer telemetry.Tracer db *gorm.DB } -// NewGormSendScheduleRepository creates a new GORM-backed SendScheduleRepository. -func NewGormSendScheduleRepository( +// NewGormMessageSendScheduleRepository creates a new GORM-backed MessageSendScheduleRepository. +func NewGormMessageSendScheduleRepository( logger telemetry.Logger, tracer telemetry.Tracer, db *gorm.DB, -) SendScheduleRepository { - return &gormSendScheduleRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &gormSendScheduleRepository{})), +) MessageSendScheduleRepository { + return &gormMessageSendScheduleRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &gormMessageSendScheduleRepository{})), tracer: tracer, db: db, } } // Store saves a new message send schedule. -func (r *gormSendScheduleRepository) Store( +func (r *gormMessageSendScheduleRepository) Store( ctx context.Context, schedule *entities.MessageSendSchedule, ) error { @@ -51,7 +51,7 @@ func (r *gormSendScheduleRepository) Store( } // Update persists changes to an existing message send schedule. -func (r *gormSendScheduleRepository) Update( +func (r *gormMessageSendScheduleRepository) Update( ctx context.Context, schedule *entities.MessageSendSchedule, ) error { @@ -69,7 +69,7 @@ func (r *gormSendScheduleRepository) Update( } // Load fetches a message send schedule by user ID and schedule ID. -func (r *gormSendScheduleRepository) Load( +func (r *gormMessageSendScheduleRepository) Load( ctx context.Context, userID entities.UserID, scheduleID uuid.UUID, @@ -104,7 +104,7 @@ func (r *gormSendScheduleRepository) Load( } // Index lists all message send schedules owned by the given user. -func (r *gormSendScheduleRepository) Index( +func (r *gormMessageSendScheduleRepository) Index( ctx context.Context, userID entities.UserID, ) ([]entities.MessageSendSchedule, error) { @@ -126,7 +126,7 @@ func (r *gormSendScheduleRepository) Index( } // Delete removes a message send schedule owned by the given user. -func (r *gormSendScheduleRepository) Delete( +func (r *gormMessageSendScheduleRepository) Delete( ctx context.Context, userID entities.UserID, scheduleID uuid.UUID, @@ -148,7 +148,7 @@ func (r *gormSendScheduleRepository) Delete( } // DeleteAllForUser removes all message send schedules owned by the given user. -func (r *gormSendScheduleRepository) DeleteAllForUser( +func (r *gormMessageSendScheduleRepository) DeleteAllForUser( ctx context.Context, userID entities.UserID, ) error { @@ -168,7 +168,7 @@ func (r *gormSendScheduleRepository) DeleteAllForUser( } // CountByUser returns the number of schedules owned by a user. -func (r *gormSendScheduleRepository) CountByUser( +func (r *gormMessageSendScheduleRepository) CountByUser( ctx context.Context, userID entities.UserID, ) (int, error) { diff --git a/api/pkg/repositories/send_schedule_repository.go b/api/pkg/repositories/message_send_schedule_repository.go similarity index 89% rename from api/pkg/repositories/send_schedule_repository.go rename to api/pkg/repositories/message_send_schedule_repository.go index d57b42d76..82ef45188 100644 --- a/api/pkg/repositories/send_schedule_repository.go +++ b/api/pkg/repositories/message_send_schedule_repository.go @@ -7,8 +7,8 @@ import ( "github.com/google/uuid" ) -// SendScheduleRepository loads and persists entities.MessageSendSchedule. -type SendScheduleRepository interface { +// MessageSendScheduleRepository loads and persists entities.MessageSendSchedule. +type MessageSendScheduleRepository interface { // Store persists a new message send schedule. Store(ctx context.Context, schedule *entities.MessageSendSchedule) error diff --git a/api/pkg/requests/message_send_schedule_store_request.go b/api/pkg/requests/message_send_schedule_store_request.go new file mode 100644 index 000000000..1796e4bd6 --- /dev/null +++ b/api/pkg/requests/message_send_schedule_store_request.go @@ -0,0 +1,52 @@ +package requests + +import ( + "sort" + "strings" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/services" +) + +// MessageSendScheduleWindow represents a single request window for a message send schedule. +type MessageSendScheduleWindow struct { + DayOfWeek int `json:"day_of_week"` + StartMinute int `json:"start_minute"` + EndMinute int `json:"end_minute"` +} + +// MessageSendScheduleStore contains the payload used to create or update a message send schedule. +type MessageSendScheduleStore struct { + request + Name string `json:"name"` + Timezone string `json:"timezone"` + IsActive bool `json:"is_active"` + Windows []MessageSendScheduleWindow `json:"windows"` +} + +// Sanitize trims and sorts the message send schedule payload before validation. +func (input *MessageSendScheduleStore) Sanitize() MessageSendScheduleStore { + input.Name = strings.TrimSpace(input.Name) + input.Timezone = strings.TrimSpace(input.Timezone) + windows := make([]MessageSendScheduleWindow, 0, len(input.Windows)) + for _, item := range input.Windows { + windows = append(windows, MessageSendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) + } + sort.SliceStable(windows, func(i, j int) bool { + if windows[i].DayOfWeek == windows[j].DayOfWeek { + return windows[i].StartMinute < windows[j].StartMinute + } + return windows[i].DayOfWeek < windows[j].DayOfWeek + }) + input.Windows = windows + return *input +} + +// ToParams converts the request payload into message send schedule service params. +func (input *MessageSendScheduleStore) ToParams(user entities.AuthContext) *services.MessageSendScheduleUpsertParams { + windows := make([]entities.MessageSendScheduleWindow, 0, len(input.Windows)) + for _, item := range input.Windows { + windows = append(windows, entities.MessageSendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) + } + return &services.MessageSendScheduleUpsertParams{UserID: user.ID, Name: input.Name, Timezone: input.Timezone, IsActive: input.IsActive, Windows: windows} +} diff --git a/api/pkg/requests/send_schedule_store_request.go b/api/pkg/requests/send_schedule_store_request.go deleted file mode 100644 index 8a6a2c3d1..000000000 --- a/api/pkg/requests/send_schedule_store_request.go +++ /dev/null @@ -1,48 +0,0 @@ -package requests - -import ( - "sort" - "strings" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/services" -) - -type SendScheduleWindow struct { - DayOfWeek int `json:"day_of_week"` - StartMinute int `json:"start_minute"` - EndMinute int `json:"end_minute"` -} - -type SendScheduleStore struct { - request - Name string `json:"name"` - Timezone string `json:"timezone"` - IsActive bool `json:"is_active"` - Windows []SendScheduleWindow `json:"windows"` -} - -func (input *SendScheduleStore) Sanitize() SendScheduleStore { - input.Name = strings.TrimSpace(input.Name) - input.Timezone = strings.TrimSpace(input.Timezone) - windows := make([]SendScheduleWindow, 0, len(input.Windows)) - for _, item := range input.Windows { - windows = append(windows, SendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) - } - sort.SliceStable(windows, func(i, j int) bool { - if windows[i].DayOfWeek == windows[j].DayOfWeek { - return windows[i].StartMinute < windows[j].StartMinute - } - return windows[i].DayOfWeek < windows[j].DayOfWeek - }) - input.Windows = windows - return *input -} - -func (input *SendScheduleStore) ToParams(user entities.AuthContext) *services.SendScheduleUpsertParams { - windows := make([]entities.MessageSendScheduleWindow, 0, len(input.Windows)) - for _, item := range input.Windows { - windows = append(windows, entities.MessageSendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) - } - return &services.SendScheduleUpsertParams{UserID: user.ID, Name: input.Name, Timezone: input.Timezone, IsActive: input.IsActive, Windows: windows} -} diff --git a/api/pkg/responses/message_send_schedule_responses.go b/api/pkg/responses/message_send_schedule_responses.go new file mode 100644 index 000000000..630dba133 --- /dev/null +++ b/api/pkg/responses/message_send_schedule_responses.go @@ -0,0 +1,15 @@ +package responses + +import "github.com/NdoleStudio/httpsms/pkg/entities" + +// MessageSendSchedulesResponse represents a collection of message send schedules. +type MessageSendSchedulesResponse struct { + response + Data []entities.MessageSendSchedule `json:"data"` +} + +// MessageSendScheduleResponse represents a single message send schedule. +type MessageSendScheduleResponse struct { + response + Data entities.MessageSendSchedule `json:"data"` +} diff --git a/api/pkg/responses/send_schedule_responses.go b/api/pkg/responses/send_schedule_responses.go deleted file mode 100644 index 9834de86d..000000000 --- a/api/pkg/responses/send_schedule_responses.go +++ /dev/null @@ -1,13 +0,0 @@ -package responses - -import "github.com/NdoleStudio/httpsms/pkg/entities" - -type SendSchedulesResponse struct { - response - Data []entities.MessageSendSchedule `json:"data"` -} - -type SendScheduleResponse struct { - response - Data entities.MessageSendSchedule `json:"data"` -} diff --git a/api/pkg/services/send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go similarity index 78% rename from api/pkg/services/send_schedule_service.go rename to api/pkg/services/message_send_schedule_service.go index b984b32d7..e9140b5dd 100644 --- a/api/pkg/services/send_schedule_service.go +++ b/api/pkg/services/message_send_schedule_service.go @@ -13,29 +13,29 @@ import ( "github.com/palantir/stacktrace" ) -// SendScheduleService manages message send schedules for a user. -type SendScheduleService struct { +// MessageSendScheduleService manages message send schedules for a user. +type MessageSendScheduleService struct { service logger telemetry.Logger tracer telemetry.Tracer - repository repositories.SendScheduleRepository + repository repositories.MessageSendScheduleRepository } -// NewSendScheduleService creates a new SendScheduleService. -func NewSendScheduleService( +// NewMessageSendScheduleService creates a new MessageSendScheduleService. +func NewMessageSendScheduleService( logger telemetry.Logger, tracer telemetry.Tracer, - repository repositories.SendScheduleRepository, -) *SendScheduleService { - return &SendScheduleService{ - logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleService{})), + repository repositories.MessageSendScheduleRepository, +) *MessageSendScheduleService { + return &MessageSendScheduleService{ + logger: logger.WithService(fmt.Sprintf("%T", &MessageSendScheduleService{})), tracer: tracer, repository: repository, } } -// SendScheduleUpsertParams contains the fields required to create or update a message send schedule. -type SendScheduleUpsertParams struct { +// MessageSendScheduleUpsertParams contains the fields required to create or update a message send schedule. +type MessageSendScheduleUpsertParams struct { UserID entities.UserID Name string Timezone string @@ -44,7 +44,7 @@ type SendScheduleUpsertParams struct { } // Index returns all message send schedules for a user. -func (service *SendScheduleService) Index( +func (service *MessageSendScheduleService) Index( ctx context.Context, userID entities.UserID, ) ([]entities.MessageSendSchedule, error) { @@ -52,7 +52,7 @@ func (service *SendScheduleService) Index( } // CountByUser returns the number of schedules owned by a user. -func (service *SendScheduleService) CountByUser( +func (service *MessageSendScheduleService) CountByUser( ctx context.Context, userID entities.UserID, ) (int, error) { @@ -60,7 +60,7 @@ func (service *SendScheduleService) CountByUser( } // Load returns a single message send schedule for a user. -func (service *SendScheduleService) Load( +func (service *MessageSendScheduleService) Load( ctx context.Context, userID entities.UserID, scheduleID uuid.UUID, @@ -69,9 +69,9 @@ func (service *SendScheduleService) Load( } // Store creates a new message send schedule. -func (service *SendScheduleService) Store( +func (service *MessageSendScheduleService) Store( ctx context.Context, - params *SendScheduleUpsertParams, + params *MessageSendScheduleUpsertParams, ) (*entities.MessageSendSchedule, error) { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -101,11 +101,11 @@ func (service *SendScheduleService) Store( } // Update updates an existing message send schedule. -func (service *SendScheduleService) Update( +func (service *MessageSendScheduleService) Update( ctx context.Context, userID entities.UserID, scheduleID uuid.UUID, - params *SendScheduleUpsertParams, + params *MessageSendScheduleUpsertParams, ) (*entities.MessageSendSchedule, error) { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -135,7 +135,7 @@ func (service *SendScheduleService) Update( } // Delete removes a message send schedule for a user. -func (service *SendScheduleService) Delete( +func (service *MessageSendScheduleService) Delete( ctx context.Context, userID entities.UserID, scheduleID uuid.UUID, @@ -168,7 +168,7 @@ func sanitizeWindows( } // DeleteAllForUser removes all message send schedules owned by a user. -func (service *SendScheduleService) DeleteAllForUser( +func (service *MessageSendScheduleService) DeleteAllForUser( ctx context.Context, userID entities.UserID, ) error { diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 24db0de5b..9e6fb2963 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -20,13 +20,13 @@ import ( // PhoneNotificationService sends out notifications to mobile phones type PhoneNotificationService struct { service - logger telemetry.Logger - tracer telemetry.Tracer - phoneNotificationRepository repositories.PhoneNotificationRepository - phoneRepository repositories.PhoneRepository - sendScheduleRepository repositories.SendScheduleRepository - messagingClient *messaging.Client - eventDispatcher *EventDispatcher + logger telemetry.Logger + tracer telemetry.Tracer + phoneNotificationRepository repositories.PhoneNotificationRepository + phoneRepository repositories.PhoneRepository + messageSendScheduleRepository repositories.MessageSendScheduleRepository + messagingClient *messaging.Client + eventDispatcher *EventDispatcher } // NewNotificationService creates a new PhoneNotificationService @@ -36,17 +36,17 @@ func NewNotificationService( messagingClient *messaging.Client, phoneRepository repositories.PhoneRepository, phoneNotificationRepository repositories.PhoneNotificationRepository, - sendScheduleRepository repositories.SendScheduleRepository, + messageSendScheduleRepository repositories.MessageSendScheduleRepository, dispatcher *EventDispatcher, ) (s *PhoneNotificationService) { return &PhoneNotificationService{ - logger: logger.WithService(fmt.Sprintf("%T", &PhoneNotificationService{})), - tracer: tracer, - messagingClient: messagingClient, - phoneNotificationRepository: phoneNotificationRepository, - phoneRepository: phoneRepository, - sendScheduleRepository: sendScheduleRepository, - eventDispatcher: dispatcher, + logger: logger.WithService(fmt.Sprintf("%T", &PhoneNotificationService{})), + tracer: tracer, + messagingClient: messagingClient, + phoneNotificationRepository: phoneNotificationRepository, + phoneRepository: phoneRepository, + messageSendScheduleRepository: messageSendScheduleRepository, + eventDispatcher: dispatcher, } } @@ -228,7 +228,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P var schedule *entities.MessageSendSchedule if phone.ScheduleID != nil { - schedule, err = service.sendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) + schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { schedule = nil err = nil diff --git a/api/pkg/validators/send_schedule_handler_validator.go b/api/pkg/validators/message_send_schedule_handler_validator.go similarity index 69% rename from api/pkg/validators/send_schedule_handler_validator.go rename to api/pkg/validators/message_send_schedule_handler_validator.go index d402fa5cb..00e405ca6 100644 --- a/api/pkg/validators/send_schedule_handler_validator.go +++ b/api/pkg/validators/message_send_schedule_handler_validator.go @@ -14,28 +14,28 @@ import ( const maxWindowsPerDay = 6 -// SendScheduleHandlerValidator validates send schedule HTTP requests. -type SendScheduleHandlerValidator struct { +// MessageSendScheduleHandlerValidator validates send schedule HTTP requests. +type MessageSendScheduleHandlerValidator struct { validator logger telemetry.Logger tracer telemetry.Tracer } -// NewSendScheduleHandlerValidator creates a new SendScheduleHandlerValidator. -func NewSendScheduleHandlerValidator( +// NewMessageSendScheduleHandlerValidator creates a new MessageSendScheduleHandlerValidator. +func NewMessageSendScheduleHandlerValidator( logger telemetry.Logger, tracer telemetry.Tracer, -) *SendScheduleHandlerValidator { - return &SendScheduleHandlerValidator{ - logger: logger.WithService(fmt.Sprintf("%T", &SendScheduleHandlerValidator{})), +) *MessageSendScheduleHandlerValidator { + return &MessageSendScheduleHandlerValidator{ + logger: logger.WithService(fmt.Sprintf("%T", &MessageSendScheduleHandlerValidator{})), tracer: tracer, } } // ValidateStore validates a send schedule create or update request. -func (validator *SendScheduleHandlerValidator) ValidateStore( +func (validator *MessageSendScheduleHandlerValidator) ValidateStore( _ context.Context, - request requests.SendScheduleStore, + request requests.MessageSendScheduleStore, ) url.Values { v := govalidator.New(govalidator.Options{ Data: &request, @@ -57,9 +57,9 @@ func (validator *SendScheduleHandlerValidator) ValidateStore( return result } -func (validator *SendScheduleHandlerValidator) validateWindows( +func (validator *MessageSendScheduleHandlerValidator) validateWindows( result url.Values, - windows []requests.SendScheduleWindow, + windows []requests.MessageSendScheduleWindow, ) { windowsPerDay := make(map[int]int) @@ -73,10 +73,10 @@ func (validator *SendScheduleHandlerValidator) validateWindows( validator.validateOverlappingWindows(result, windows) } -func (validator *SendScheduleHandlerValidator) validateDayOfWeek( +func (validator *MessageSendScheduleHandlerValidator) validateDayOfWeek( result url.Values, index int, - item requests.SendScheduleWindow, + item requests.MessageSendScheduleWindow, windowsPerDay map[int]int, ) { if item.DayOfWeek < 0 || item.DayOfWeek > 6 { @@ -93,41 +93,41 @@ func (validator *SendScheduleHandlerValidator) validateDayOfWeek( } } -func (validator *SendScheduleHandlerValidator) validateStartMinute( +func (validator *MessageSendScheduleHandlerValidator) validateStartMinute( result url.Values, index int, - item requests.SendScheduleWindow, + item requests.MessageSendScheduleWindow, ) { if item.StartMinute < 0 || item.StartMinute > 1439 { result.Add("windows", fmt.Sprintf("windows[%d].start_minute must be between 0 and 1439", index)) } } -func (validator *SendScheduleHandlerValidator) validateEndMinute( +func (validator *MessageSendScheduleHandlerValidator) validateEndMinute( result url.Values, index int, - item requests.SendScheduleWindow, + item requests.MessageSendScheduleWindow, ) { if item.EndMinute < 1 || item.EndMinute > 1440 { result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be between 1 and 1440", index)) } } -func (validator *SendScheduleHandlerValidator) validateWindowRange( +func (validator *MessageSendScheduleHandlerValidator) validateWindowRange( result url.Values, index int, - item requests.SendScheduleWindow, + item requests.MessageSendScheduleWindow, ) { if item.EndMinute <= item.StartMinute { result.Add("windows", fmt.Sprintf("windows[%d].end_minute must be greater than start_minute", index)) } } -func (validator *SendScheduleHandlerValidator) validateOverlappingWindows( +func (validator *MessageSendScheduleHandlerValidator) validateOverlappingWindows( result url.Values, - windows []requests.SendScheduleWindow, + windows []requests.MessageSendScheduleWindow, ) { - grouped := make(map[int][]requests.SendScheduleWindow) + grouped := make(map[int][]requests.MessageSendScheduleWindow) for _, item := range windows { if item.DayOfWeek < 0 || item.DayOfWeek > 6 { From 93d8ac9f6dacddac8878a5077ed9a70b373dbd5a Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 23:24:01 +0300 Subject: [PATCH 120/381] docs: link to existing feature pages instead of re-explaining scheduling and send rate Link to https://docs.httpsms.com/features/scheduling-sms-messages and https://docs.httpsms.com/features/control-sms-send-rate instead of repeating their explanations. Add detailed MessageSendSchedule (send windows) section since it's the only new feature without its own docs page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-05-03-scheduling-send-refactor.md | 7 +- .../2026-05-03-entitlement-service-design.md | 2 +- ...6-05-03-scheduling-send-refactor-design.md | 79 ++++++++++++++++++- 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md b/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md index a85b46ed1..f92b60ed0 100644 --- a/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md +++ b/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md @@ -4,7 +4,12 @@ **Goal:** Allow users to send SMS at an exact time (bypassing scheduling) when `SendAt` is specified, and replace the 1-second bulk hack with rate-based dispatch delays. -**Architecture:** Add a transient `ExactSendTime` flag flowing through the event system. When true, bypass rate-limit and schedule window logic in notification scheduling. For bulk sends without explicit time, compute dispatch delay from `MessagesPerMinute` per-phone instead of hardcoded 1s. +**Related docs:** + +- [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) — the existing `SendAt`/`SendTime` feature +- [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) — the existing `MessagesPerMinute` rate-limiting feature + +**Architecture:** Add a transient `ExactSendTime` flag flowing through the event system. When true, bypass [rate-limit](https://docs.httpsms.com/features/control-sms-send-rate) and schedule window logic in notification scheduling. For bulk sends without explicit time, compute dispatch delay from `MessagesPerMinute` per-phone instead of hardcoded 1s. **Tech Stack:** Go, Fiber, GORM, CockroachDB, Google Cloud Tasks (CloudEvents) diff --git a/docs/superpowers/specs/2026-05-03-entitlement-service-design.md b/docs/superpowers/specs/2026-05-03-entitlement-service-design.md index c22b8a9b5..8ec3893cf 100644 --- a/docs/superpowers/specs/2026-05-03-entitlement-service-design.md +++ b/docs/superpowers/specs/2026-05-03-entitlement-service-design.md @@ -2,7 +2,7 @@ ## Problem -The send schedule feature (and future features) need usage limits based on the user's subscription plan. Free users should be limited to 1 send schedule; paid users get unlimited. The system must be: +The [MessageSendSchedule](./2026-05-03-scheduling-send-refactor-design.md#messagesendschedule-send-windows--new-feature) feature (and future features) need usage limits based on the user's subscription plan. Free users should be limited to 1 send schedule; paid users get unlimited. The system must be: - **Scalable**: Easy to add new entity limits without architectural changes - **Configurable**: Disabled by default for self-hosted deployments, enabled via env var for cloud diff --git a/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md b/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md index 34d784459..1a02bc082 100644 --- a/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md +++ b/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md @@ -1,10 +1,15 @@ # Scheduling Send Refactor Design +## Related Documentation + +- [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) — existing `SendAt`/`SendTime` scheduling feature +- [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) — existing `MessagesPerMinute` rate-limiting feature + ## Problem Statement The current SMS scheduling logic has two issues: -1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt`, the system still applies rate-limiting and schedule window logic, which may shift the actual send time. +1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt` (see [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)), the system still applies rate-limiting and schedule window logic, which may shift the actual send time. 2. **Bulk message contention.** When bulk messages (API or CSV) are sent, all events arrive at the Cloud Tasks queue near-simultaneously, causing DB serialization conflicts in `PhoneNotificationRepository.Schedule()` (which uses `SELECT ... ORDER BY scheduled_at DESC` in a transaction). The current workaround is a hardcoded 1-second spacing hack. @@ -12,8 +17,8 @@ The current SMS scheduling logic has two issues: ### Core Principle -- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. -- **No `SendTime`** = apply full scheduling logic (rate-limit + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. +- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. See [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) for how `SendAt` works. +- **No `SendTime`** = apply full scheduling logic ([rate-limit](https://docs.httpsms.com/features/control-sms-send-rate) + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. ### Design @@ -111,7 +116,73 @@ User sends request ### What Does NOT Change - The `MessageSendSchedule` entity and its `ResolveScheduledAt()` logic -- The `SendScheduleService` CRUD operations +- The `MessageSendScheduleService` CRUD operations - The phone notification entity schema (no new DB columns) - The Android app behavior - The web frontend (models auto-generated from Swagger) + +--- + +## MessageSendSchedule (Send Windows) — New Feature + +This is the only scheduling mechanism that does **not** have a dedicated documentation page yet. Unlike [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) (one-time `SendAt`) and [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) (`MessagesPerMinute` throttling), MessageSendSchedule defines **recurring availability windows** that control when a phone is allowed to send outgoing SMS messages. + +### Concept + +A `MessageSendSchedule` is a named set of time windows (per day of week) that define when the phone can send. Messages arriving outside those windows are delayed until the next available window opens. + +### Entity + +```go +type MessageSendSchedule struct { + ID uuid.UUID + UserID UserID + Name string // e.g. "Business Hours" + Timezone string // IANA timezone e.g. "Europe/Tallinn" + IsActive bool + Windows []MessageSendScheduleWindow // per-day availability slots + CreatedAt time.Time + UpdatedAt time.Time +} + +type MessageSendScheduleWindow struct { + DayOfWeek int // 0=Sunday, 6=Saturday + StartMinute int // minutes from midnight (e.g. 540 = 9:00) + EndMinute int // minutes from midnight (e.g. 1020 = 17:00) +} +``` + +### How It Works + +1. A user creates a schedule via `POST /v1/send-schedules` with a name, timezone, and one or more windows. +2. The schedule is linked to a phone via a `ScheduleID` field on the phone entity. +3. When a message is queued (without an explicit `SendAt`), the `PhoneNotificationRepository.Schedule()` method calls `MessageSendSchedule.ResolveScheduledAt(now)` to find the next allowed send time. +4. If the current time falls within a window, the message sends immediately. If not, it's delayed to the start of the next available window. + +### API Endpoints + +| Method | Endpoint | Description | +| ------ | --------------------------------- | --------------------------- | +| GET | `/v1/send-schedules` | List all user schedules | +| POST | `/v1/send-schedules` | Create a new schedule | +| PUT | `/v1/send-schedules/{scheduleID}` | Update an existing schedule | +| DELETE | `/v1/send-schedules/{scheduleID}` | Delete a schedule | + +### Validation Rules + +- `name`: required, 2–100 characters +- `timezone`: required, valid IANA timezone +- `windows[].day_of_week`: 0–6 +- `windows[].start_minute`: 0–1439 +- `windows[].end_minute`: 1–1440, must be greater than `start_minute` +- Max 6 windows per day +- No overlapping windows on the same day + +### Entitlement + +Free users are limited to 1 schedule. Paid users get unlimited schedules. Enforced via `EntitlementService.Check()` in the handler before creation. + +### Interaction with Other Scheduling Features + +- **[Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)** (`SendAt`): When provided, bypasses send windows entirely (exact send time). +- **[Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate)** (`MessagesPerMinute`): Applied independently — rate-limiting still applies within allowed windows. Both constraints compose: the message must be within a window AND respect the rate limit. From 2b9a45f2391e6b902b4005593784a9310d5f6897 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 3 May 2026 23:42:56 +0300 Subject: [PATCH 121/381] fix: address PR review comments for send schedule feature - Add schedule ownership check in phone handler to prevent cross-user schedule_id assignment (security fix) - Fix OpenAPI annotation for Index endpoint to use wrapper response type - Fix 402 response annotation to use proper PaymentRequired type - Add PaymentRequired response type to responses package - Mark schedule_id as optional in PhoneUpsert request struct - Fix scheduleWindowError matching to use backend error format (day_of_week) - Remove unnecessary Promise.all wrapping in store actions - Remove unused limit query param from getSendSchedules - Treat active schedule with empty windows as inactive (send immediately) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 1 + api/pkg/entities/message_send_schedule.go | 9 ++- .../handlers/message_send_schedule_handler.go | 4 +- api/pkg/handlers/phone_handler.go | 31 ++++++--- api/pkg/requests/phone_update_request.go | 2 +- api/pkg/responses/response.go | 6 ++ web/pages/settings/index.vue | 4 +- web/store/index.ts | 67 +++++++------------ 8 files changed, 68 insertions(+), 56 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 66b57468d..cf8360f43 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -1117,6 +1117,7 @@ func (container *Container) PhoneHandler() (handler *handlers.PhoneHandler) { container.Logger(), container.Tracer(), container.PhoneService(), + container.MessageSendScheduleService(), container.PhoneHandlerValidator(), ) } diff --git a/api/pkg/entities/message_send_schedule.go b/api/pkg/entities/message_send_schedule.go index 9ffbad124..17b8a7b7b 100644 --- a/api/pkg/entities/message_send_schedule.go +++ b/api/pkg/entities/message_send_schedule.go @@ -27,9 +27,14 @@ type MessageSendSchedule struct { // ResolveScheduledAt returns the next allowed send time based on the schedule. // If the schedule is inactive, has no windows, or has an invalid timezone, -// the current time is returned in UTC. +// the current time is returned in UTC. An active schedule with no windows +// is treated as inactive (messages are sent immediately). func (schedule *MessageSendSchedule) ResolveScheduledAt(current time.Time) time.Time { - if schedule == nil || !schedule.IsActive || len(schedule.Windows) == 0 { + if schedule == nil || !schedule.IsActive { + return current.UTC() + } + + if len(schedule.Windows) == 0 { return current.UTC() } diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go index d7df8cdbb..a688987c4 100644 --- a/api/pkg/handlers/message_send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -56,7 +56,7 @@ func (h *MessageSendScheduleHandler) RegisterRoutes(router fiber.Router, middlew // @Security ApiKeyAuth // @Tags Send Schedules // @Produce json -// @Success 200 {array} entities.MessageSendSchedule +// @Success 200 {object} responses.MessageSendSchedulesResponse // @Failure 401 {object} responses.Unauthorized // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules [get] @@ -85,7 +85,7 @@ func (h *MessageSendScheduleHandler) Index(c *fiber.Ctx) error { // @Success 201 {object} responses.MessageSendScheduleResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized -// @Failure 402 {object} responses.BadRequest +// @Failure 402 {object} responses.PaymentRequired // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /send-schedules [post] diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go index 9e5cbe1ce..62ec35b78 100644 --- a/api/pkg/handlers/phone_handler.go +++ b/api/pkg/handlers/phone_handler.go @@ -2,10 +2,13 @@ package handlers import ( "fmt" + "net/url" + "strings" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" + "github.com/google/uuid" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" @@ -16,10 +19,11 @@ import ( // PhoneHandler handles phone http requests. type PhoneHandler struct { handler - logger telemetry.Logger - tracer telemetry.Tracer - service *services.PhoneService - validator *validators.PhoneHandlerValidator + logger telemetry.Logger + tracer telemetry.Tracer + service *services.PhoneService + scheduleService *services.MessageSendScheduleService + validator *validators.PhoneHandlerValidator } // NewPhoneHandler creates a new PhoneHandler @@ -27,13 +31,15 @@ func NewPhoneHandler( logger telemetry.Logger, tracer telemetry.Tracer, service *services.PhoneService, + scheduleService *services.MessageSendScheduleService, validator *validators.PhoneHandlerValidator, ) (h *PhoneHandler) { return &PhoneHandler{ - logger: logger.WithService(fmt.Sprintf("%T", h)), - tracer: tracer, - validator: validator, - service: service, + logger: logger.WithService(fmt.Sprintf("%T", h)), + tracer: tracer, + validator: validator, + service: service, + scheduleService: scheduleService, } } @@ -127,6 +133,15 @@ func (h *PhoneHandler) Upsert(c *fiber.Ctx) error { return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones") } + if request.ScheduleID != nil && strings.TrimSpace(*request.ScheduleID) != "" { + scheduleID, _ := uuid.Parse(strings.TrimSpace(*request.ScheduleID)) + if _, err := h.scheduleService.Load(ctx, h.userFromContext(c).ID, scheduleID); err != nil { + validationErrors := url.Values{} + validationErrors.Add("schedule_id", "schedule_id does not belong to the authenticated user or does not exist") + return h.responseUnprocessableEntity(c, validationErrors, "validation errors while updating phones") + } + } + phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL())) if err != nil { msg := fmt.Sprintf("cannot update phones with params [%+#v]", request) diff --git a/api/pkg/requests/phone_update_request.go b/api/pkg/requests/phone_update_request.go index bb710024d..06876de87 100644 --- a/api/pkg/requests/phone_update_request.go +++ b/api/pkg/requests/phone_update_request.go @@ -31,7 +31,7 @@ type PhoneUpsert struct { // SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot SIM string `json:"sim" example:"SIM1"` - ScheduleID *string `json:"schedule_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + ScheduleID *string `json:"schedule_id,omitempty" example:"32343a19-da5e-4b1b-a767-3298a73703cb" validate:"optional"` } // Sanitize sets defaults to MessageOutstanding diff --git a/api/pkg/responses/response.go b/api/pkg/responses/response.go index c61c919eb..e23fea0f3 100644 --- a/api/pkg/responses/response.go +++ b/api/pkg/responses/response.go @@ -38,6 +38,12 @@ type Unauthorized struct { Data string `json:"data" example:"Make sure your API key is set in the [X-API-Key] header in the request"` } +// PaymentRequired is the response with status code is 402 +type PaymentRequired struct { + Status string `json:"status" example:"error"` + Message string `json:"message" example:"You have reached the maximum number of allowed resources. Please upgrade your plan."` +} + // NoContent is the response when status code is 204 type NoContent struct { Status string `json:"status" example:"success"` diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index de6c0b050..cbe4fe6e8 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -1779,10 +1779,10 @@ export default Vue.extend({ } const message = messages.find((x: string) => - x.includes(`Day of week ${index}`), + x.includes(`day_of_week ${index}`), ) return message - ? message.replace(`Day of week ${index}`, this.getWeekday(index)) + ? message.replace(`day_of_week ${index}`, this.getWeekday(index)) : null }, diff --git a/web/store/index.ts b/web/store/index.ts index 9e52a2402..bd3071e60 100644 --- a/web/store/index.ts +++ b/web/store/index.ts @@ -1111,23 +1111,17 @@ export const actions = { getSendSchedules(context: ActionContext) { return new Promise>((resolve, reject) => { axios - .get(`/v1/send-schedules`, { - params: { - limit: 100, - }, - }) + .get(`/v1/send-schedules`) .then((response: AxiosResponse) => { resolve(response.data.data) }) .catch(async (error: AxiosError) => { - await Promise.all([ - context.dispatch('addNotification', { - message: - (error.response?.data as any)?.message ?? - 'Error while fetching send schedules', - type: 'error', - }), - ]) + await context.dispatch('addNotification', { + message: + (error.response?.data as any)?.message ?? + 'Error while fetching send schedules', + type: 'error', + }) reject(getErrorMessages(error)) }) }) @@ -1144,14 +1138,12 @@ export const actions = { resolve(response.data.data) }) .catch(async (error: AxiosError) => { - await Promise.all([ - context.dispatch('addNotification', { - message: - (error.response?.data as any)?.message ?? - 'Error while creating send schedule', - type: 'error', - }), - ]) + await context.dispatch('addNotification', { + message: + (error.response?.data as any)?.message ?? + 'Error while creating send schedule', + type: 'error', + }) reject(getErrorMessages(error)) }) }) @@ -1171,23 +1163,18 @@ export const actions = { resolve(response.data.data) }) .catch(async (error: AxiosError) => { - await Promise.all([ - context.dispatch('addNotification', { - message: - (error.response?.data as any)?.message ?? - 'Error while updating send schedule', - type: 'error', - }), - ]) + await context.dispatch('addNotification', { + message: + (error.response?.data as any)?.message ?? + 'Error while updating send schedule', + type: 'error', + }) reject(getErrorMessages(error)) }) }) }, - deleteSendSchedule( - context: ActionContext, - payload: string, - ) { + deleteSendSchedule(context: ActionContext, payload: string) { return new Promise((resolve, reject) => { axios .delete(`/v1/send-schedules/${payload}`) @@ -1195,14 +1182,12 @@ export const actions = { resolve() }) .catch(async (error: AxiosError) => { - await Promise.all([ - context.dispatch('addNotification', { - message: - (error.response?.data as any)?.message ?? - 'Error while deleting send schedule', - type: 'error', - }), - ]) + await context.dispatch('addNotification', { + message: + (error.response?.data as any)?.message ?? + 'Error while deleting send schedule', + type: 'error', + }) reject(getErrorMessages(error)) }) }) From 5a64c0813d18ada48b96986f3e8e1547df276ba2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 4 May 2026 23:16:15 +0300 Subject: [PATCH 122/381] Fix code --- api/pkg/di/container.go | 2 +- api/pkg/entities/message_send_schedule.go | 33 ++------- .../entities/message_send_schedule_test.go | 5 +- api/pkg/entities/phone.go | 16 ++--- .../handlers/message_send_schedule_handler.go | 54 +++++++------- api/pkg/handlers/phone_handler.go | 35 +++------- .../message_send_schedule_listener.go | 22 +----- .../gorm_message_send_schedule_repository.go | 64 ++++++----------- .../message_send_schedule_store_request.go | 3 +- api/pkg/requests/phone_update_request.go | 28 ++++---- api/pkg/services/entitlement_service.go | 10 ++- .../services/message_send_schedule_service.go | 3 - .../services/phone_notification_service.go | 70 +++++++++++-------- api/pkg/services/phone_service.go | 48 ++++++------- ...message_send_schedule_handler_validator.go | 7 +- api/pkg/validators/phone_handler_validator.go | 37 +++++----- web/pages/settings/index.vue | 6 +- 17 files changed, 195 insertions(+), 248 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index cf8360f43..1dc808b5f 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -663,6 +663,7 @@ func (container *Container) PhoneHandlerValidator() (validator *validators.Phone return validators.NewPhoneHandlerValidator( container.Logger(), container.Tracer(), + container.MessageSendScheduleService(), ) } @@ -1117,7 +1118,6 @@ func (container *Container) PhoneHandler() (handler *handlers.PhoneHandler) { container.Logger(), container.Tracer(), container.PhoneService(), - container.MessageSendScheduleService(), container.PhoneHandlerValidator(), ) } diff --git a/api/pkg/entities/message_send_schedule.go b/api/pkg/entities/message_send_schedule.go index 17b8a7b7b..20c9b6286 100644 --- a/api/pkg/entities/message_send_schedule.go +++ b/api/pkg/entities/message_send_schedule.go @@ -19,7 +19,6 @@ type MessageSendSchedule struct { UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` Name string `json:"name" example:"Business Hours"` Timezone string `json:"timezone" example:"Europe/Tallinn"` - IsActive bool `json:"is_active" gorm:"default:true" example:"true"` Windows []MessageSendScheduleWindow `json:"windows" gorm:"type:jsonb;serializer:json"` CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"` UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"` @@ -30,11 +29,7 @@ type MessageSendSchedule struct { // the current time is returned in UTC. An active schedule with no windows // is treated as inactive (messages are sent immediately). func (schedule *MessageSendSchedule) ResolveScheduledAt(current time.Time) time.Time { - if schedule == nil || !schedule.IsActive { - return current.UTC() - } - - if len(schedule.Windows) == 0 { + if schedule == nil || len(schedule.Windows) == 0 { return current.UTC() } @@ -55,27 +50,11 @@ func (schedule *MessageSendSchedule) ResolveScheduledAt(current time.Time) time. continue } - start := time.Date( - day.Year(), - day.Month(), - day.Day(), - 0, - 0, - 0, - 0, - location, - ).Add(time.Duration(window.StartMinute) * time.Minute) - - end := time.Date( - day.Year(), - day.Month(), - day.Day(), - 0, - 0, - 0, - 0, - location, - ).Add(time.Duration(window.EndMinute) * time.Minute) + start := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, location). + Add(time.Duration(window.StartMinute) * time.Minute) + + end := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, location). + Add(time.Duration(window.EndMinute) * time.Minute) var candidate time.Time diff --git a/api/pkg/entities/message_send_schedule_test.go b/api/pkg/entities/message_send_schedule_test.go index 1480aa2fe..2554fda35 100644 --- a/api/pkg/entities/message_send_schedule_test.go +++ b/api/pkg/entities/message_send_schedule_test.go @@ -16,7 +16,7 @@ func TestResolveScheduledAt_NilSchedule_ReturnsCurrentUTC(t *testing.T) { func TestResolveScheduledAt_InactiveSchedule_ReturnsCurrentUTC(t *testing.T) { now := time.Now() - schedule := &MessageSendSchedule{IsActive: false} + schedule := &MessageSendSchedule{} result := schedule.ResolveScheduledAt(now) assert.Equal(t, now.UTC(), result) } @@ -24,7 +24,6 @@ func TestResolveScheduledAt_InactiveSchedule_ReturnsCurrentUTC(t *testing.T) { func TestResolveScheduledAt_NoWindows_ReturnsCurrentUTC(t *testing.T) { now := time.Now() schedule := &MessageSendSchedule{ - IsActive: true, Timezone: "UTC", Windows: []MessageSendScheduleWindow{}, } @@ -36,7 +35,6 @@ func TestResolveScheduledAt_WithinWindow_ReturnsCurrentUTC(t *testing.T) { // Wednesday at 10:00 UTC, window is Wed 9:00-17:00 (540-1020 minutes) now := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC) // Wednesday schedule := &MessageSendSchedule{ - IsActive: true, Timezone: "UTC", Windows: []MessageSendScheduleWindow{ {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, @@ -50,7 +48,6 @@ func TestResolveScheduledAt_BeforeWindow_ReturnsWindowStart(t *testing.T) { // Wednesday at 7:00 UTC, window is Wed 9:00-17:00 now := time.Date(2025, 1, 1, 7, 0, 0, 0, time.UTC) // Wednesday schedule := &MessageSendSchedule{ - IsActive: true, Timezone: "UTC", Windows: []MessageSendScheduleWindow{ {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, diff --git a/api/pkg/entities/phone.go b/api/pkg/entities/phone.go index f97212ce2..d52e452a9 100644 --- a/api/pkg/entities/phone.go +++ b/api/pkg/entities/phone.go @@ -8,14 +8,14 @@ import ( // Phone represents an android phone which has installed the http sms app type Phone struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` - FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." validate:"optional"` - PhoneNumber string `json:"phone_number" example:"+18005550199"` - MessagesPerMinute uint `json:"messages_per_minute" example:"1"` - SIM SIM `json:"sim" gorm:"default:SIM1"` - ScheduleID *uuid.UUID `json:"schedule_id" gorm:"type:uuid" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - Schedule *MessageSendSchedule `json:"-" gorm:"foreignKey:ScheduleID;constraint:OnDelete:SET NULL"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` + FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." validate:"optional"` + PhoneNumber string `json:"phone_number" example:"+18005550199"` + MessagesPerMinute uint `json:"messages_per_minute" example:"1"` + SIM SIM `json:"sim" gorm:"default:SIM1"` + MessageSendScheduleID *uuid.UUID `json:"message_send_schedule_id" gorm:"type:uuid" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + // MaxSendAttempts determines how many times to retry sending an SMS message MaxSendAttempts uint `json:"max_send_attempts" example:"2"` diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go index a688987c4..446f50e17 100644 --- a/api/pkg/handlers/message_send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -54,7 +54,7 @@ func (h *MessageSendScheduleHandler) RegisterRoutes(router fiber.Router, middlew // @Summary List send schedules // @Description List all send schedules owned by the authenticated user. // @Security ApiKeyAuth -// @Tags Send Schedules +// @Tags SendSchedules // @Produce json // @Success 200 {object} responses.MessageSendSchedulesResponse // @Failure 401 {object} responses.Unauthorized @@ -64,9 +64,11 @@ func (h *MessageSendScheduleHandler) Index(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() - schedules, err := h.service.Index(ctx, h.userIDFomContext(c)) + userID := h.userIDFomContext(c) + + schedules, err := h.service.Index(ctx, userID) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot list send schedules")) + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot list send schedules for user [%s]", userID))) return h.responseInternalServerError(c) } @@ -78,7 +80,7 @@ func (h *MessageSendScheduleHandler) Index(c *fiber.Ctx) error { // @Summary Create send schedule // @Description Create a new send schedule for the authenticated user. // @Security ApiKeyAuth -// @Tags Send Schedules +// @Tags SendSchedules // @Accept json // @Produce json // @Param payload body requests.MessageSendScheduleStore true "Payload of new send schedule." @@ -95,15 +97,11 @@ func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { userID := h.userIDFomContext(c) - count, err := h.service.CountByUser(ctx, userID) + result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", func() (int, error) { + return h.service.CountByUser(ctx, userID) + }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot count send schedules for entitlement check")) - return h.responseInternalServerError(c) - } - - result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", count) - if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for send schedules")) + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot check entitlement for send schedules for user [%s]", userID))) return h.responseInternalServerError(c) } if !result.Allowed { @@ -111,7 +109,7 @@ func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { } var request requests.MessageSendScheduleStore - if err := c.BodyParser(&request); err != nil { + if err = c.BodyParser(&request); err != nil { return h.responseBadRequest(c, err) } @@ -127,7 +125,7 @@ func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { schedule, err := h.service.Store(ctx, request.ToParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create send schedule")) + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot create send schedule for user [%s]", userID))) return h.responseInternalServerError(c) } @@ -139,7 +137,7 @@ func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { // @Summary Update send schedule // @Description Update a send schedule owned by the authenticated user. // @Security ApiKeyAuth -// @Tags Send Schedules +// @Tags SendSchedules // @Accept json // @Produce json // @Param scheduleID path string true "Schedule ID" @@ -170,14 +168,11 @@ func (h *MessageSendScheduleHandler) Update(c *fiber.Ctx) error { return h.responseUnprocessableEntity(c, errors, "validation errors while updating send schedule") } - schedule, err := h.service.Update( - ctx, - h.userIDFomContext(c), - scheduleID, - request.ToParams(h.userFromContext(c)), - ) + userID := h.userIDFomContext(c) + + schedule, err := h.service.Update(ctx, userID, scheduleID, request.ToParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update send schedule")) + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot update send schedule for user [%s] and schedule [%s]", userID, scheduleID))) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return h.responseNotFound(c, err.Error()) } @@ -192,7 +187,7 @@ func (h *MessageSendScheduleHandler) Update(c *fiber.Ctx) error { // @Summary Delete send schedule // @Description Delete a send schedule owned by the authenticated user. // @Security ApiKeyAuth -// @Tags Send Schedules +// @Tags SendSchedules // @Produce json // @Param scheduleID path string true "Schedule ID" // @Success 204 @@ -210,19 +205,18 @@ func (h *MessageSendScheduleHandler) Delete(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - if _, err = h.service.Load(ctx, h.userIDFomContext(c), scheduleID); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load send schedule for deletion")) + userID := h.userIDFomContext(c) + + if _, err = h.service.Load(ctx, userID, scheduleID); err != nil { + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot load send schedule for deletion for user [%s] and schedule [%s]", userID, scheduleID))) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return h.responseNotFound(c, err.Error()) } return h.responseInternalServerError(c) } - if err = h.service.Delete(ctx, h.userIDFomContext(c), scheduleID); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete send schedule")) - if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { - return h.responseNotFound(c, err.Error()) - } + if err = h.service.Delete(ctx, userID, scheduleID); err != nil { + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot delete send schedule for user [%s] and schedule [%s]", userID, scheduleID))) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go index 62ec35b78..28db85857 100644 --- a/api/pkg/handlers/phone_handler.go +++ b/api/pkg/handlers/phone_handler.go @@ -2,13 +2,10 @@ package handlers import ( "fmt" - "net/url" - "strings" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" - "github.com/google/uuid" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" @@ -19,11 +16,10 @@ import ( // PhoneHandler handles phone http requests. type PhoneHandler struct { handler - logger telemetry.Logger - tracer telemetry.Tracer - service *services.PhoneService - scheduleService *services.MessageSendScheduleService - validator *validators.PhoneHandlerValidator + logger telemetry.Logger + tracer telemetry.Tracer + service *services.PhoneService + validator *validators.PhoneHandlerValidator } // NewPhoneHandler creates a new PhoneHandler @@ -31,15 +27,13 @@ func NewPhoneHandler( logger telemetry.Logger, tracer telemetry.Tracer, service *services.PhoneService, - scheduleService *services.MessageSendScheduleService, validator *validators.PhoneHandlerValidator, ) (h *PhoneHandler) { return &PhoneHandler{ - logger: logger.WithService(fmt.Sprintf("%T", h)), - tracer: tracer, - validator: validator, - service: service, - scheduleService: scheduleService, + logger: logger.WithService(fmt.Sprintf("%T", h)), + tracer: tracer, + validator: validator, + service: service, } } @@ -127,22 +121,13 @@ func (h *PhoneHandler) Upsert(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - if errors := h.validator.ValidateUpsert(ctx, request.Sanitize()); len(errors) != 0 { + if errors := h.validator.ValidateUpsert(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { msg := fmt.Sprintf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request) ctxLogger.Warn(stacktrace.NewError(msg)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones") } - if request.ScheduleID != nil && strings.TrimSpace(*request.ScheduleID) != "" { - scheduleID, _ := uuid.Parse(strings.TrimSpace(*request.ScheduleID)) - if _, err := h.scheduleService.Load(ctx, h.userFromContext(c).ID, scheduleID); err != nil { - validationErrors := url.Values{} - validationErrors.Add("schedule_id", "schedule_id does not belong to the authenticated user or does not exist") - return h.responseUnprocessableEntity(c, validationErrors, "validation errors while updating phones") - } - } - - phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL())) + phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL(), c.Body())) if err != nil { msg := fmt.Sprintf("cannot update phones with params [%+#v]", request) ctxLogger.Error(stacktrace.Propagate(err, msg)) diff --git a/api/pkg/listeners/message_send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go index 61e57b242..20d4955dc 100644 --- a/api/pkg/listeners/message_send_schedule_listener.go +++ b/api/pkg/listeners/message_send_schedule_listener.go @@ -45,28 +45,12 @@ func (listener *MessageSendScheduleListener) onUserAccountDeleted( var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan( - span, - stacktrace.Propagate( - err, - "cannot decode [%s] into [%T]", - event.Data(), - payload, - ), - ) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload))) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan( - span, - stacktrace.Propagate( - err, - "cannot delete [entities.MessageSendSchedule] for user [%s] on [%s] event with ID [%s]", - payload.UserID, - event.Type(), - event.ID(), - ), - ) + msg := fmt.Sprintf("cannot delete [entities.MessageSendSchedule] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) } return nil diff --git a/api/pkg/repositories/gorm_message_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go index 919eb6a93..54e588431 100644 --- a/api/pkg/repositories/gorm_message_send_schedule_repository.go +++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go @@ -41,10 +41,7 @@ func (r *gormMessageSendScheduleRepository) Store( defer span.End() if err := r.db.WithContext(ctx).Create(schedule).Error; err != nil { - return r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot store send schedule [%s]", schedule.ID), - ) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store send schedule [%s]", schedule.ID)) } return nil @@ -59,10 +56,7 @@ func (r *gormMessageSendScheduleRepository) Update( defer span.End() if err := r.db.WithContext(ctx).Save(schedule).Error; err != nil { - return r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot update send schedule [%s]", schedule.ID), - ) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update send schedule [%s]", schedule.ID)) } return nil @@ -85,19 +79,11 @@ func (r *gormMessageSendScheduleRepository) Load( if errors.Is(err, gorm.ErrRecordNotFound) { return nil, r.tracer.WrapErrorSpan( span, - stacktrace.PropagateWithCode( - err, - ErrCodeNotFound, - "send schedule [%s] not found", - scheduleID, - ), + stacktrace.PropagateWithCode(err, ErrCodeNotFound, "send schedule [%s] not found for user with ID [%s]", scheduleID, userID), ) } if err != nil { - return nil, r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot load send schedule [%s]", scheduleID), - ) + return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load send schedule [%s]", scheduleID)) } return item, nil @@ -112,14 +98,12 @@ func (r *gormMessageSendScheduleRepository) Index( defer span.End() items := make([]entities.MessageSendSchedule, 0) - if err := r.db.WithContext(ctx). + err := r.db.WithContext(ctx). Where("user_id = ?", userID). - Order("created_at ASC"). - Find(&items).Error; err != nil { - return nil, r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot index send schedules for user [%s]", userID), - ) + Order("created_at DESC"). + Find(&items).Error + if err != nil { + return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot index send schedules for user [%s]", userID)) } return items, nil @@ -134,14 +118,12 @@ func (r *gormMessageSendScheduleRepository) Delete( ctx, span := r.tracer.Start(ctx) defer span.End() - if err := r.db.WithContext(ctx). + err := r.db.WithContext(ctx). Where("user_id = ?", userID). Where("id = ?", scheduleID). - Delete(&entities.MessageSendSchedule{}).Error; err != nil { - return r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot delete send schedule [%s]", scheduleID), - ) + Delete(&entities.MessageSendSchedule{}).Error + if err != nil { + return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete send schedule [%s]", scheduleID)) } return nil @@ -155,13 +137,11 @@ func (r *gormMessageSendScheduleRepository) DeleteAllForUser( ctx, span := r.tracer.Start(ctx) defer span.End() - if err := r.db.WithContext(ctx). + err := r.db.WithContext(ctx). Where("user_id = ?", userID). - Delete(&entities.MessageSendSchedule{}).Error; err != nil { - return r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot delete send schedules for user [%s]", userID), - ) + Delete(&entities.MessageSendSchedule{}).Error + if err != nil { + return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete send schedules for user [%s]", userID)) } return nil @@ -176,14 +156,12 @@ func (r *gormMessageSendScheduleRepository) CountByUser( defer span.End() var count int64 - if err := r.db.WithContext(ctx). + err := r.db.WithContext(ctx). Model(&entities.MessageSendSchedule{}). Where("user_id = ?", userID). - Count(&count).Error; err != nil { - return 0, r.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot count send schedules for user [%s]", userID), - ) + Count(&count).Error + if err != nil { + return 0, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot count send schedules for user [%s]", userID)) } return int(count), nil diff --git a/api/pkg/requests/message_send_schedule_store_request.go b/api/pkg/requests/message_send_schedule_store_request.go index 1796e4bd6..bd69fdc94 100644 --- a/api/pkg/requests/message_send_schedule_store_request.go +++ b/api/pkg/requests/message_send_schedule_store_request.go @@ -20,7 +20,6 @@ type MessageSendScheduleStore struct { request Name string `json:"name"` Timezone string `json:"timezone"` - IsActive bool `json:"is_active"` Windows []MessageSendScheduleWindow `json:"windows"` } @@ -48,5 +47,5 @@ func (input *MessageSendScheduleStore) ToParams(user entities.AuthContext) *serv for _, item := range input.Windows { windows = append(windows, entities.MessageSendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) } - return &services.MessageSendScheduleUpsertParams{UserID: user.ID, Name: input.Name, Timezone: input.Timezone, IsActive: input.IsActive, Windows: windows} + return &services.MessageSendScheduleUpsertParams{UserID: user.ID, Name: input.Name, Timezone: input.Timezone, Windows: windows} } diff --git a/api/pkg/requests/phone_update_request.go b/api/pkg/requests/phone_update_request.go index 06876de87..462d6428e 100644 --- a/api/pkg/requests/phone_update_request.go +++ b/api/pkg/requests/phone_update_request.go @@ -1,6 +1,7 @@ package requests import ( + "encoding/json" "strings" "time" @@ -31,7 +32,7 @@ type PhoneUpsert struct { // SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot SIM string `json:"sim" example:"SIM1"` - ScheduleID *string `json:"schedule_id,omitempty" example:"32343a19-da5e-4b1b-a767-3298a73703cb" validate:"optional"` + MessageSendScheduleID string `json:"message_send_schedule_id,omitempty" example:"32343a19-da5e-4b1b-a767-3298a73703cb" validate:"optional"` } // Sanitize sets defaults to MessageOutstanding @@ -45,37 +46,38 @@ func (input *PhoneUpsert) Sanitize() PhoneUpsert { return *input } -// ToUpsertParams converts PhoneUpsert to services.PhoneUpsertParams -func (input *PhoneUpsert) ToUpsertParams(user entities.AuthContext, source string) *services.PhoneUpsertParams { +// ToUpsertParams converts PhoneUpsert to services.PhoneUpsertParams. +// The body parameter is the raw JSON request body used to detect which fields were explicitly sent. +func (input *PhoneUpsert) ToUpsertParams(user entities.AuthContext, source string, body []byte) *services.PhoneUpsertParams { phone, _ := phonenumbers.Parse(input.PhoneNumber, phonenumbers.UNKNOWN_REGION) - // ignore value if it's default + fields := make(map[string]json.RawMessage) + _ = json.Unmarshal(body, &fields) + var messagesPerMinute *uint - if input.MessagesPerMinute != 0 { + if _, exists := fields["messages_per_minute"]; exists { messagesPerMinute = &input.MessagesPerMinute } - // ignore default var fcmToken *string - if input.FcmToken != "" { + if _, exists := fields["fcm_token"]; exists { fcmToken = &input.FcmToken } - // ignore default var timeout *time.Duration - if input.MessageExpirationSeconds != 0 { + if _, exists := fields["message_expiration_seconds"]; exists { duration := time.Duration(input.MessageExpirationSeconds) * time.Second timeout = &duration } var maxSendAttempts *uint - if input.MaxSendAttempts != 0 { + if _, exists := fields["max_send_attempts"]; exists { maxSendAttempts = &input.MaxSendAttempts } var scheduleID *uuid.UUID - if input.ScheduleID != nil && strings.TrimSpace(*input.ScheduleID) != "" { - if parsed, err := uuid.Parse(strings.TrimSpace(*input.ScheduleID)); err == nil { + if _, exists := fields["message_send_schedule_id"]; exists { + if parsed, err := uuid.Parse(strings.TrimSpace(input.MessageSendScheduleID)); err == nil { scheduleID = &parsed } } @@ -90,6 +92,6 @@ func (input *PhoneUpsert) ToUpsertParams(user entities.AuthContext, source strin FcmToken: fcmToken, UserID: user.ID, SIM: entities.SIM(input.SIM), - ScheduleID: scheduleID, + MessageSendScheduleID: scheduleID, } } diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index cd4d740f6..59f077a2d 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -58,7 +58,7 @@ func (service *EntitlementService) Check( ctx context.Context, userID entities.UserID, entityName string, - currentCount int, + countFunc func() (int, error), ) (*EntitlementCheckResult, error) { ctx, span := service.tracer.Start(ctx) defer span.End() @@ -85,6 +85,14 @@ func (service *EntitlementService) Check( return &EntitlementCheckResult{Allowed: true}, nil } + currentCount, err := countFunc() + if err != nil { + return nil, service.tracer.WrapErrorSpan( + span, + stacktrace.Propagate(err, fmt.Sprintf("cannot count entities [%s] for user [%s]", entityName, userID)), + ) + } + if currentCount >= limit { return &EntitlementCheckResult{ Allowed: false, diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go index e9140b5dd..1d8cb3377 100644 --- a/api/pkg/services/message_send_schedule_service.go +++ b/api/pkg/services/message_send_schedule_service.go @@ -39,7 +39,6 @@ type MessageSendScheduleUpsertParams struct { UserID entities.UserID Name string Timezone string - IsActive bool Windows []entities.MessageSendScheduleWindow } @@ -81,7 +80,6 @@ func (service *MessageSendScheduleService) Store( UserID: params.UserID, Name: params.Name, Timezone: params.Timezone, - IsActive: params.IsActive, Windows: sanitizeWindows(params.Windows), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), @@ -117,7 +115,6 @@ func (service *MessageSendScheduleService) Update( schedule.Name = params.Name schedule.Timezone = params.Timezone - schedule.IsActive = params.IsActive schedule.Windows = sanitizeWindows(params.Windows) schedule.UpdatedAt = time.Now().UTC() diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 9e6fb2963..63b62678d 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -15,6 +15,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/google/uuid" "github.com/palantir/stacktrace" + "go.opentelemetry.io/otel/trace" ) // PhoneNotificationService sends out notifications to mobile phones @@ -197,44 +198,19 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P UpdatedAt: time.Now().UTC(), } - // Bypass rate-limit and schedule window logic for exact send time if params.ExactSendTime && params.ScheduledSendTime != nil { - scheduledAt := *params.ScheduledSendTime - if scheduledAt.Before(time.Now().UTC()) { - scheduledAt = time.Now().UTC() - } - notification.ScheduledAt = scheduledAt - if err = service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { - msg := fmt.Sprintf("cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { - ctxLogger.Error(err) - } - - if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { - return service.tracer.WrapErrorSpan(span, err) - } - - ctxLogger.Info(fmt.Sprintf( - "message with id [%s] exact notification scheduled for [%s] with id [%s]", - params.MessageID, - notification.ScheduledAt, - notification.ID, - )) - return nil + return service.scheduleExact(ctx, span, ctxLogger, params, phone, notification) } var schedule *entities.MessageSendSchedule - if phone.ScheduleID != nil { - schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) + if phone.MessageSendScheduleID != nil { + schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.MessageSendScheduleID) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { schedule = nil err = nil } if err != nil { - msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.ScheduleID, phone.ID) + msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID) return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) } } @@ -261,6 +237,42 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P return nil } +func (service *PhoneNotificationService) scheduleExact( + ctx context.Context, + span trace.Span, + ctxLogger telemetry.Logger, + params *PhoneNotificationScheduleParams, + phone *entities.Phone, + notification *entities.PhoneNotification, +) error { + scheduledAt := *params.ScheduledSendTime + if scheduledAt.Before(time.Now().UTC()) { + scheduledAt = time.Now().UTC() + } + notification.ScheduledAt = scheduledAt + + if err := service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { + msg := fmt.Sprintf("cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err := service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { + ctxLogger.Error(err) + } + + if err := service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { + return service.tracer.WrapErrorSpan(span, err) + } + + ctxLogger.Info(fmt.Sprintf( + "message with id [%s] exact notification scheduled for [%s] with id [%s]", + params.MessageID, + notification.ScheduledAt, + notification.ID, + )) + return nil +} + func (service *PhoneNotificationService) dispatchMessageNotificationSend( ctx context.Context, source string, diff --git a/api/pkg/services/phone_service.go b/api/pkg/services/phone_service.go index 9fa01ab04..1ddbf2a42 100644 --- a/api/pkg/services/phone_service.go +++ b/api/pkg/services/phone_service.go @@ -91,7 +91,7 @@ type PhoneUpsertParams struct { MessageExpirationDuration *time.Duration MissedCallAutoReply *string SIM entities.SIM - ScheduleID *uuid.UUID + MessageSendScheduleID *uuid.UUID Source string UserID entities.UserID } @@ -106,13 +106,13 @@ func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertPara phone, err := service.repository.Load(ctx, params.UserID, phonenumbers.Format(params.PhoneNumber, phonenumbers.E164)) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return service.createPhone(ctx, &PhoneFCMTokenParams{ - Source: params.Source, - PhoneNumber: params.PhoneNumber, - PhoneAPIKeyID: nil, - UserID: params.UserID, - FcmToken: params.FcmToken, - SIM: params.SIM, - ScheduleID: params.ScheduleID, + Source: params.Source, + PhoneNumber: params.PhoneNumber, + PhoneAPIKeyID: nil, + UserID: params.UserID, + FcmToken: params.FcmToken, + SIM: params.SIM, + MessageSendScheduleID: params.MessageSendScheduleID, }) } @@ -128,13 +128,13 @@ func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertPara ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID)) return phone, service.dispatchPhoneUpdatedEvent(ctx, phone, &PhoneFCMTokenParams{ - Source: params.Source, - PhoneNumber: params.PhoneNumber, - PhoneAPIKeyID: nil, - UserID: params.UserID, - FcmToken: params.FcmToken, - SIM: params.SIM, - ScheduleID: params.ScheduleID, + Source: params.Source, + PhoneNumber: params.PhoneNumber, + PhoneAPIKeyID: nil, + UserID: params.UserID, + FcmToken: params.FcmToken, + SIM: params.SIM, + MessageSendScheduleID: params.MessageSendScheduleID, }) } @@ -204,13 +204,13 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e // PhoneFCMTokenParams are parameters for upserting an entities.Phone type PhoneFCMTokenParams struct { - Source string - PhoneNumber *phonenumbers.PhoneNumber - PhoneAPIKeyID *uuid.UUID - UserID entities.UserID - FcmToken *string - SIM entities.SIM - ScheduleID *uuid.UUID + Source string + PhoneNumber *phonenumbers.PhoneNumber + PhoneAPIKeyID *uuid.UUID + UserID entities.UserID + FcmToken *string + SIM entities.SIM + MessageSendScheduleID *uuid.UUID } // UpsertFCMToken the FCM token for an entities.Phone @@ -255,7 +255,7 @@ func (service *PhoneService) createPhone(ctx context.Context, params *PhoneFCMTo MaxSendAttempts: 2, SIM: params.SIM, MissedCallAutoReply: nil, - ScheduleID: params.ScheduleID, + MessageSendScheduleID: params.MessageSendScheduleID, PhoneNumber: phonenumbers.Format(params.PhoneNumber, phonenumbers.E164), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), @@ -299,7 +299,7 @@ func (service *PhoneService) update(phone *entities.Phone, params *PhoneUpsertPa } phone.SIM = params.SIM - phone.ScheduleID = params.ScheduleID + phone.MessageSendScheduleID = params.MessageSendScheduleID return phone } diff --git a/api/pkg/validators/message_send_schedule_handler_validator.go b/api/pkg/validators/message_send_schedule_handler_validator.go index 00e405ca6..fa850de1c 100644 --- a/api/pkg/validators/message_send_schedule_handler_validator.go +++ b/api/pkg/validators/message_send_schedule_handler_validator.go @@ -50,7 +50,7 @@ func (validator *MessageSendScheduleHandlerValidator) ValidateStore( if request.Timezone != "" { if _, err := time.LoadLocation(request.Timezone); err != nil { - result.Add("timezone", "timezone must be a valid IANA timezone") + result.Add("timezone", "The timezone must be a valid IANA timezone e.g Europe/London.") } } @@ -61,6 +61,11 @@ func (validator *MessageSendScheduleHandlerValidator) validateWindows( result url.Values, windows []requests.MessageSendScheduleWindow, ) { + if len(windows) == 0 { + result.Add("windows", "at least one active window is required") + return + } + windowsPerDay := make(map[int]int) for index, item := range windows { diff --git a/api/pkg/validators/phone_handler_validator.go b/api/pkg/validators/phone_handler_validator.go index f9c78255b..e9d4274e7 100644 --- a/api/pkg/validators/phone_handler_validator.go +++ b/api/pkg/validators/phone_handler_validator.go @@ -7,27 +7,31 @@ import ( "strings" "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/requests" + "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" "github.com/thedevsaddam/govalidator" ) // PhoneHandlerValidator validates models used in handlers.PhoneHandler type PhoneHandlerValidator struct { validator - logger telemetry.Logger - tracer telemetry.Tracer + logger telemetry.Logger + tracer telemetry.Tracer + scheduleService *services.MessageSendScheduleService } // NewPhoneHandlerValidator creates a new handlers.PhoneHandler validator func NewPhoneHandlerValidator( logger telemetry.Logger, tracer telemetry.Tracer, + scheduleService *services.MessageSendScheduleService, ) (v *PhoneHandlerValidator) { return &PhoneHandlerValidator{ - logger: logger.WithService(fmt.Sprintf("%T", v)), - tracer: tracer, + logger: logger.WithService(fmt.Sprintf("%T", v)), + tracer: tracer, + scheduleService: scheduleService, } } @@ -56,7 +60,7 @@ func (validator *PhoneHandlerValidator) ValidateIndex(_ context.Context, request } // ValidateUpsert validates requests.PhoneUpsert -func (validator *PhoneHandlerValidator) ValidateUpsert(_ context.Context, request requests.PhoneUpsert) url.Values { +func (validator *PhoneHandlerValidator) ValidateUpsert(ctx context.Context, userID entities.UserID, request requests.PhoneUpsert) url.Values { v := govalidator.New(govalidator.Options{ Data: &request, Rules: govalidator.MapData{ @@ -84,25 +88,26 @@ func (validator *PhoneHandlerValidator) ValidateUpsert(_ context.Context, reques "min:60", "max:3600", }, + "message_send_schedule_id": []string{ + "uuid", + }, }, }) result := v.ValidateStruct() - if request.ScheduleID != nil && strings.TrimSpace(*request.ScheduleID) != "" { - if uuidErrors := validator.ValidateUUID(strings.TrimSpace(*request.ScheduleID), "schedule_id"); len(uuidErrors) > 0 { - for key, values := range uuidErrors { - for _, value := range values { - result.Add(key, value) - } - } - } + if request.MaxSendAttempts > 0 && request.MessageExpirationSeconds == 0 { + result.Add("message_expiration_seconds", "message_expiration_seconds cannot be 0 when max_send_attempts is greater than 0") } + if len(result) > 0 { return result } - if request.MaxSendAttempts > 0 && request.MessageExpirationSeconds == 0 { - result.Add("message_expiration_seconds", "message_expiration_seconds cannot be 0 when max_send_attempts is greater than 0") + if strings.TrimSpace(request.MessageSendScheduleID) != "" { + scheduleID, _ := uuid.Parse(strings.TrimSpace(request.MessageSendScheduleID)) + if _, err := validator.scheduleService.Load(ctx, userID, scheduleID); err != nil { + result.Add("message_send_schedule_id", "The message_send_schedule_id does not belong to the authenticated user or does not exist") + } } return result diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index cbe4fe6e8..6df69cd26 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -622,15 +622,17 @@ > Date: Mon, 4 May 2026 23:31:58 +0300 Subject: [PATCH 123/381] Fix event --- api/pkg/di/container.go | 16 ++++ .../message_send_schedule_deleted_event.go | 18 +++++ api/pkg/listeners/phone_listener.go | 76 +++++++++++++++++++ api/pkg/repositories/gorm_phone_repository.go | 19 +++++ api/pkg/repositories/phone_repository.go | 3 + api/pkg/services/event_dispatcher_service.go | 2 +- .../services/message_send_schedule_service.go | 29 ++++++- api/pkg/services/phone_service.go | 14 ++++ 8 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 api/pkg/events/message_send_schedule_deleted_event.go create mode 100644 api/pkg/listeners/phone_listener.go diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 1dc808b5f..b27cc37cc 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -133,6 +133,7 @@ func NewContainer(projectID string, version string) (container *Container) { container.RegisterUserListeners() container.RegisterPhoneRoutes() + container.RegisterPhoneListeners() container.RegisterEventRoutes() @@ -769,6 +770,7 @@ func (container *Container) MessageSendScheduleService() *services.MessageSendSc container.Logger(), container.Tracer(), container.MessageSendScheduleRepository(), + container.EventDispatcher(), ) } @@ -1455,6 +1457,20 @@ func (container *Container) RegisterPhoneAPIKeyListeners() { } } +// RegisterPhoneListeners registers event listeners for listeners.PhoneListener +func (container *Container) RegisterPhoneListeners() { + container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.PhoneListener{})) + _, routes := listeners.NewPhoneListener( + container.Logger(), + container.Tracer(), + container.PhoneService(), + ) + + for event, handler := range routes { + container.EventDispatcher().Subscribe(event, handler) + } +} + // RegisterWebsocketListeners registers event listeners for listeners.WebsocketListener func (container *Container) RegisterWebsocketListeners() { container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.WebsocketListener{})) diff --git a/api/pkg/events/message_send_schedule_deleted_event.go b/api/pkg/events/message_send_schedule_deleted_event.go new file mode 100644 index 000000000..3a32361c6 --- /dev/null +++ b/api/pkg/events/message_send_schedule_deleted_event.go @@ -0,0 +1,18 @@ +package events + +import ( + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/google/uuid" +) + +// EventTypeMessageSendScheduleDeleted is emitted when a message send schedule is deleted +const EventTypeMessageSendScheduleDeleted = "message-send-schedule.deleted" + +// MessageSendScheduleDeletedPayload is the payload of the EventTypeMessageSendScheduleDeleted event +type MessageSendScheduleDeletedPayload struct { + ScheduleID uuid.UUID `json:"schedule_id"` + UserID entities.UserID `json:"user_id"` + Timestamp time.Time `json:"timestamp"` +} diff --git a/api/pkg/listeners/phone_listener.go b/api/pkg/listeners/phone_listener.go new file mode 100644 index 000000000..541936d9d --- /dev/null +++ b/api/pkg/listeners/phone_listener.go @@ -0,0 +1,76 @@ +package listeners + +import ( + "context" + "fmt" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/palantir/stacktrace" + + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/httpsms/pkg/telemetry" +) + +// PhoneListener handles cloud events that alter the state of entities.Phone +type PhoneListener struct { + logger telemetry.Logger + tracer telemetry.Tracer + service *services.PhoneService +} + +// NewPhoneListener creates a new instance of PhoneListener +func NewPhoneListener( + logger telemetry.Logger, + tracer telemetry.Tracer, + service *services.PhoneService, +) (l *PhoneListener, routes map[string]events.EventListener) { + l = &PhoneListener{ + logger: logger.WithService(fmt.Sprintf("%T", l)), + tracer: tracer, + service: service, + } + + return l, map[string]events.EventListener{ + events.EventTypeMessageSendScheduleDeleted: l.onMessageSendScheduleDeleted, + events.UserAccountDeleted: l.onUserAccountDeleted, + } +} + +// onMessageSendScheduleDeleted handles the events.EventTypeMessageSendScheduleDeleted event +func (listener *PhoneListener) onMessageSendScheduleDeleted(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.MessageSendScheduleDeletedPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err := listener.service.NullifyScheduleID(ctx, payload.UserID, payload.ScheduleID); err != nil { + msg := fmt.Sprintf("cannot nullify schedule ID [%s] for user [%s] on [%s] event with ID [%s]", payload.ScheduleID, payload.UserID, event.Type(), event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +// onUserAccountDeleted handles the events.UserAccountDeleted event +func (listener *PhoneListener) onUserAccountDeleted(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.UserAccountDeletedPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { + msg := fmt.Sprintf("cannot delete all [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} diff --git a/api/pkg/repositories/gorm_phone_repository.go b/api/pkg/repositories/gorm_phone_repository.go index 55b6fb3f2..a1f79ba87 100644 --- a/api/pkg/repositories/gorm_phone_repository.go +++ b/api/pkg/repositories/gorm_phone_repository.go @@ -50,6 +50,25 @@ func (repository *gormPhoneRepository) DeleteAllForUser(ctx context.Context, use return nil } +// NullifyScheduleID sets MessageSendScheduleID to NULL for all phones referencing the given schedule +func (repository *gormPhoneRepository) NullifyScheduleID(ctx context.Context, userID entities.UserID, scheduleID uuid.UUID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + err := repository.db.WithContext(ctx). + Model(&entities.Phone{}). + Where("user_id = ?", userID). + Where("message_send_schedule_id = ?", scheduleID). + Update("message_send_schedule_id", nil).Error + if err != nil { + msg := fmt.Sprintf("cannot nullify message_send_schedule_id [%s] for user [%s]", scheduleID, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + repository.cache.Clear() + return nil +} + // LoadByID loads a phone by ID func (repository *gormPhoneRepository) LoadByID(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) (*entities.Phone, error) { ctx, span := repository.tracer.Start(ctx) diff --git a/api/pkg/repositories/phone_repository.go b/api/pkg/repositories/phone_repository.go index c9e82b985..2c184963d 100644 --- a/api/pkg/repositories/phone_repository.go +++ b/api/pkg/repositories/phone_repository.go @@ -25,6 +25,9 @@ type PhoneRepository interface { // Delete an entities.Phone Delete(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) error + // NullifyScheduleID sets MessageSendScheduleID to NULL for all phones referencing the given schedule + NullifyScheduleID(ctx context.Context, userID entities.UserID, scheduleID uuid.UUID) error + // DeleteAllForUser deletes all entities.Phone for a user DeleteAllForUser(ctx context.Context, userID entities.UserID) error } diff --git a/api/pkg/services/event_dispatcher_service.go b/api/pkg/services/event_dispatcher_service.go index dfb6dae62..f9ec6b33f 100644 --- a/api/pkg/services/event_dispatcher_service.go +++ b/api/pkg/services/event_dispatcher_service.go @@ -148,7 +148,7 @@ func (dispatcher *EventDispatcher) Publish(ctx context.Context, event cloudevent dispatcher.meter.Record( ctx, - float64(time.Since(start).Microseconds())/1000, + float64(time.Since(start).Milliseconds()), metric.WithAttributes( semconv.CloudeventsEventType(event.Type()), semconv.CloudeventsEventSpecVersion(event.SpecVersion()), diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go index 1d8cb3377..1a4d85ed3 100644 --- a/api/pkg/services/message_send_schedule_service.go +++ b/api/pkg/services/message_send_schedule_service.go @@ -7,6 +7,7 @@ import ( "time" "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/google/uuid" @@ -19,6 +20,7 @@ type MessageSendScheduleService struct { logger telemetry.Logger tracer telemetry.Tracer repository repositories.MessageSendScheduleRepository + dispatcher *EventDispatcher } // NewMessageSendScheduleService creates a new MessageSendScheduleService. @@ -26,11 +28,13 @@ func NewMessageSendScheduleService( logger telemetry.Logger, tracer telemetry.Tracer, repository repositories.MessageSendScheduleRepository, + dispatcher *EventDispatcher, ) *MessageSendScheduleService { return &MessageSendScheduleService{ logger: logger.WithService(fmt.Sprintf("%T", &MessageSendScheduleService{})), tracer: tracer, repository: repository, + dispatcher: dispatcher, } } @@ -137,7 +141,30 @@ func (service *MessageSendScheduleService) Delete( userID entities.UserID, scheduleID uuid.UUID, ) error { - return service.repository.Delete(ctx, userID, scheduleID) + ctx, span := service.tracer.Start(ctx) + defer span.End() + + if err := service.repository.Delete(ctx, userID, scheduleID); err != nil { + msg := fmt.Sprintf("cannot delete message send schedule with ID [%s] for user [%s]", scheduleID, userID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + event, err := service.createEvent(events.EventTypeMessageSendScheduleDeleted, fmt.Sprintf("%T", service), events.MessageSendScheduleDeletedPayload{ + ScheduleID: scheduleID, + UserID: userID, + Timestamp: time.Now().UTC(), + }) + if err != nil { + msg := fmt.Sprintf("cannot create [%s] event for schedule [%s]", events.EventTypeMessageSendScheduleDeleted, scheduleID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err = service.dispatcher.Dispatch(ctx, event); err != nil { + msg := fmt.Sprintf("cannot dispatch [%s] event for schedule [%s]", event.Type(), scheduleID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil } // sanitizeWindows normalizes and sorts schedule windows by day and start minute. diff --git a/api/pkg/services/phone_service.go b/api/pkg/services/phone_service.go index 1ddbf2a42..ae863d32b 100644 --- a/api/pkg/services/phone_service.go +++ b/api/pkg/services/phone_service.go @@ -56,6 +56,20 @@ func (service *PhoneService) DeleteAllForUser(ctx context.Context, userID entiti return nil } +// NullifyScheduleID sets MessageSendScheduleID to NULL for all phones referencing the given schedule. +func (service *PhoneService) NullifyScheduleID(ctx context.Context, userID entities.UserID, scheduleID uuid.UUID) error { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + if err := service.repository.NullifyScheduleID(ctx, userID, scheduleID); err != nil { + msg := fmt.Sprintf("cannot nullify schedule ID [%s] for user [%s]", scheduleID, userID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + service.tracer.CtxLogger(service.logger, span).Info(fmt.Sprintf("nullified schedule ID [%s] on phones for user [%s]", scheduleID, userID)) + return nil +} + // Index fetches the heartbeats for a phone number func (service *PhoneService) Index(ctx context.Context, authUser entities.AuthContext, params repositories.IndexParams) (*[]entities.Phone, error) { ctx, span := service.tracer.Start(ctx) From fd2cddd6a95af74f234f85829d81ad581951eeac Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 4 May 2026 23:32:48 +0300 Subject: [PATCH 124/381] Generate docs --- api/docs/docs.go | 84 ++++++++++++++++++++++++++++--------------- api/docs/swagger.json | 78 +++++++++++++++++++++++++--------------- api/docs/swagger.yaml | 67 ++++++++++++++++++++++------------ 3 files changed, 150 insertions(+), 79 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 018614faf..5adf69c38 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -2191,17 +2191,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Send Schedules" + "SendSchedules" ], "summary": "List send schedules", "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendSchedule" - } + "$ref": "#/definitions/responses.MessageSendSchedulesResponse" } }, "401": { @@ -2232,7 +2229,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Send Schedules" + "SendSchedules" ], "summary": "Create send schedule", "parameters": [ @@ -2268,7 +2265,7 @@ const docTemplate = `{ "402": { "description": "Payment Required", "schema": { - "$ref": "#/definitions/responses.BadRequest" + "$ref": "#/definitions/responses.PaymentRequired" } }, "422": { @@ -2301,7 +2298,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Send Schedules" + "SendSchedules" ], "summary": "Update send schedule", "parameters": [ @@ -2372,7 +2369,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Send Schedules" + "SendSchedules" ], "summary": "Delete send schedule", "parameters": [ @@ -3529,7 +3526,6 @@ const docTemplate = `{ "required": [ "created_at", "id", - "is_active", "name", "timezone", "updated_at", @@ -3545,10 +3541,6 @@ const docTemplate = `{ "type": "string", "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "is_active": { - "type": "boolean", - "example": true - }, "name": { "type": "string", "example": "Business Hours" @@ -3669,9 +3661,9 @@ const docTemplate = `{ "id", "max_send_attempts", "message_expiration_seconds", + "message_send_schedule_id", "messages_per_minute", "phone_number", - "schedule_id", "sim", "updated_at", "user_id" @@ -3698,6 +3690,10 @@ const docTemplate = `{ "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", "type": "integer" }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, "messages_per_minute": { "type": "integer", "example": 1 @@ -3710,10 +3706,6 @@ const docTemplate = `{ "type": "string", "example": "+18005550199" }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, "sim": { "$ref": "#/definitions/entities.SIM" }, @@ -4255,15 +4247,11 @@ const docTemplate = `{ "requests.MessageSendScheduleStore": { "type": "object", "required": [ - "is_active", "name", "timezone", "windows" ], "properties": { - "is_active": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -4353,7 +4341,6 @@ const docTemplate = `{ "messages_per_minute", "missed_call_auto_reply", "phone_number", - "schedule_id", "sim" ], "properties": { @@ -4371,6 +4358,10 @@ const docTemplate = `{ "type": "integer", "example": 12345 }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, "messages_per_minute": { "type": "integer", "example": 1 @@ -4383,10 +4374,6 @@ const docTemplate = `{ "type": "string", "example": "+18005550199" }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, "sim": { "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", "type": "string", @@ -4762,6 +4749,30 @@ const docTemplate = `{ } } }, + "responses.MessageSendSchedulesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendSchedule" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ @@ -4865,6 +4876,23 @@ const docTemplate = `{ } } }, + "responses.PaymentRequired": { + "type": "object", + "required": [ + "message", + "status" + ], + "properties": { + "message": { + "type": "string", + "example": "You have reached the maximum number of allowed resources. Please upgrade your plan." + }, + "status": { + "type": "string", + "example": "error" + } + } + }, "responses.PhoneAPIKeyResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 3045ce7fe..4a9de5a75 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1991,16 +1991,13 @@ ], "description": "List all send schedules owned by the authenticated user.", "produces": ["application/json"], - "tags": ["Send Schedules"], + "tags": ["SendSchedules"], "summary": "List send schedules", "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendSchedule" - } + "$ref": "#/definitions/responses.MessageSendSchedulesResponse" } }, "401": { @@ -2026,7 +2023,7 @@ "description": "Create a new send schedule for the authenticated user.", "consumes": ["application/json"], "produces": ["application/json"], - "tags": ["Send Schedules"], + "tags": ["SendSchedules"], "summary": "Create send schedule", "parameters": [ { @@ -2061,7 +2058,7 @@ "402": { "description": "Payment Required", "schema": { - "$ref": "#/definitions/responses.BadRequest" + "$ref": "#/definitions/responses.PaymentRequired" } }, "422": { @@ -2089,7 +2086,7 @@ "description": "Update a send schedule owned by the authenticated user.", "consumes": ["application/json"], "produces": ["application/json"], - "tags": ["Send Schedules"], + "tags": ["SendSchedules"], "summary": "Update send schedule", "parameters": [ { @@ -2156,7 +2153,7 @@ ], "description": "Delete a send schedule owned by the authenticated user.", "produces": ["application/json"], - "tags": ["Send Schedules"], + "tags": ["SendSchedules"], "summary": "Delete send schedule", "parameters": [ { @@ -3234,7 +3231,6 @@ "required": [ "created_at", "id", - "is_active", "name", "timezone", "updated_at", @@ -3250,10 +3246,6 @@ "type": "string", "example": "32343a19-da5e-4b1b-a767-3298a73703cb" }, - "is_active": { - "type": "boolean", - "example": true - }, "name": { "type": "string", "example": "Business Hours" @@ -3370,9 +3362,9 @@ "id", "max_send_attempts", "message_expiration_seconds", + "message_send_schedule_id", "messages_per_minute", "phone_number", - "schedule_id", "sim", "updated_at", "user_id" @@ -3399,6 +3391,10 @@ "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", "type": "integer" }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, "messages_per_minute": { "type": "integer", "example": 1 @@ -3411,10 +3407,6 @@ "type": "string", "example": "+18005550199" }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, "sim": { "$ref": "#/definitions/entities.SIM" }, @@ -3899,11 +3891,8 @@ }, "requests.MessageSendScheduleStore": { "type": "object", - "required": ["is_active", "name", "timezone", "windows"], + "required": ["name", "timezone", "windows"], "properties": { - "is_active": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -3981,7 +3970,6 @@ "messages_per_minute", "missed_call_auto_reply", "phone_number", - "schedule_id", "sim" ], "properties": { @@ -3999,6 +3987,10 @@ "type": "integer", "example": 12345 }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, "messages_per_minute": { "type": "integer", "example": 1 @@ -4011,10 +4003,6 @@ "type": "string", "example": "+18005550199" }, - "schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, "sim": { "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", "type": "string", @@ -4332,6 +4320,26 @@ } } }, + "responses.MessageSendSchedulesResponse": { + "type": "object", + "required": ["data", "message", "status"], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendSchedule" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": ["data", "message", "status"], @@ -4417,6 +4425,20 @@ } } }, + "responses.PaymentRequired": { + "type": "object", + "required": ["message", "status"], + "properties": { + "message": { + "type": "string", + "example": "You have reached the maximum number of allowed resources. Please upgrade your plan." + }, + "status": { + "type": "string", + "example": "error" + } + } + }, "responses.PhoneAPIKeyResponse": { "type": "object", "required": ["data", "message", "status"], diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 58cee295e..ff50acaa8 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -222,9 +222,6 @@ definitions: id: example: 32343a19-da5e-4b1b-a767-3298a73703cb type: string - is_active: - example: true - type: boolean name: example: Business Hours type: string @@ -244,7 +241,6 @@ definitions: required: - created_at - id - - is_active - name - timezone - updated_at @@ -341,6 +337,9 @@ definitions: MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. type: integer + message_send_schedule_id: + example: 32343a19-da5e-4b1b-a767-3298a73703cb + type: string messages_per_minute: example: 1 type: integer @@ -350,9 +349,6 @@ definitions: phone_number: example: "+18005550199" type: string - schedule_id: - example: 32343a19-da5e-4b1b-a767-3298a73703cb - type: string sim: $ref: "#/definitions/entities.SIM" updated_at: @@ -366,9 +362,9 @@ definitions: - id - max_send_attempts - message_expiration_seconds + - message_send_schedule_id - messages_per_minute - phone_number - - schedule_id - sim - updated_at - user_id @@ -795,8 +791,6 @@ definitions: type: object requests.MessageSendScheduleStore: properties: - is_active: - type: boolean name: type: string timezone: @@ -806,7 +800,6 @@ definitions: $ref: "#/definitions/requests.MessageSendScheduleWindow" type: array required: - - is_active - name - timezone - windows @@ -876,6 +869,9 @@ definitions: a message when it is considered to be expired. example: 12345 type: integer + message_send_schedule_id: + example: 32343a19-da5e-4b1b-a767-3298a73703cb + type: string messages_per_minute: example: 1 type: integer @@ -885,9 +881,6 @@ definitions: phone_number: example: "+18005550199" type: string - schedule_id: - example: 32343a19-da5e-4b1b-a767-3298a73703cb - type: string sim: description: SIM is the SIM slot of the phone in case the phone has more than @@ -901,7 +894,6 @@ definitions: - messages_per_minute - missed_call_auto_reply - phone_number - - schedule_id - sim type: object requests.UserNotificationUpdate: @@ -1168,6 +1160,23 @@ definitions: - message - status type: object + responses.MessageSendSchedulesResponse: + properties: + data: + items: + $ref: "#/definitions/entities.MessageSendSchedule" + type: array + message: + example: Request handled successfully + type: string + status: + example: success + type: string + required: + - data + - message + - status + type: object responses.MessageThreadsResponse: properties: data: @@ -1241,6 +1250,20 @@ definitions: - message - status type: object + responses.PaymentRequired: + properties: + message: + example: + You have reached the maximum number of allowed resources. Please + upgrade your plan. + type: string + status: + example: error + type: string + required: + - message + - status + type: object responses.PhoneAPIKeyResponse: properties: data: @@ -2958,9 +2981,7 @@ paths: "200": description: OK schema: - items: - $ref: "#/definitions/entities.MessageSendSchedule" - type: array + $ref: "#/definitions/responses.MessageSendSchedulesResponse" "401": description: Unauthorized schema: @@ -2973,7 +2994,7 @@ paths: - ApiKeyAuth: [] summary: List send schedules tags: - - Send Schedules + - SendSchedules post: consumes: - application/json @@ -3003,7 +3024,7 @@ paths: "402": description: Payment Required schema: - $ref: "#/definitions/responses.BadRequest" + $ref: "#/definitions/responses.PaymentRequired" "422": description: Unprocessable Entity schema: @@ -3016,7 +3037,7 @@ paths: - ApiKeyAuth: [] summary: Create send schedule tags: - - Send Schedules + - SendSchedules /send-schedules/{scheduleID}: delete: description: Delete a send schedule owned by the authenticated user. @@ -3051,7 +3072,7 @@ paths: - ApiKeyAuth: [] summary: Delete send schedule tags: - - Send Schedules + - SendSchedules put: consumes: - application/json @@ -3099,7 +3120,7 @@ paths: - ApiKeyAuth: [] summary: Update send schedule tags: - - Send Schedules + - SendSchedules /users/{userID}/api-keys: delete: consumes: From 0fc6efe266459eee3dbfad8a5cbd6e0549a8a9ef Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 4 May 2026 23:43:32 +0300 Subject: [PATCH 125/381] update specs --- api/docs/docs.go | 1 - api/docs/swagger.json | 1 - api/docs/swagger.yaml | 1 - api/pkg/entities/phone.go | 2 +- web/models/api.ts | 183 +++++++++++++++++++++++++------------- 5 files changed, 120 insertions(+), 68 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 5adf69c38..ed2796059 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -3661,7 +3661,6 @@ const docTemplate = `{ "id", "max_send_attempts", "message_expiration_seconds", - "message_send_schedule_id", "messages_per_minute", "phone_number", "sim", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 4a9de5a75..656ee381e 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -3362,7 +3362,6 @@ "id", "max_send_attempts", "message_expiration_seconds", - "message_send_schedule_id", "messages_per_minute", "phone_number", "sim", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index ff50acaa8..e2f17d92a 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -362,7 +362,6 @@ definitions: - id - max_send_attempts - message_expiration_seconds - - message_send_schedule_id - messages_per_minute - phone_number - sim diff --git a/api/pkg/entities/phone.go b/api/pkg/entities/phone.go index d52e452a9..97df66317 100644 --- a/api/pkg/entities/phone.go +++ b/api/pkg/entities/phone.go @@ -14,7 +14,7 @@ type Phone struct { PhoneNumber string `json:"phone_number" example:"+18005550199"` MessagesPerMinute uint `json:"messages_per_minute" example:"1"` SIM SIM `json:"sim" gorm:"default:SIM1"` - MessageSendScheduleID *uuid.UUID `json:"message_send_schedule_id" gorm:"type:uuid" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + MessageSendScheduleID *uuid.UUID `json:"message_send_schedule_id" gorm:"type:uuid" example:"32343a19-da5e-4b1b-a767-3298a73703cb" validate:"optional"` // MaxSendAttempts determines how many times to retry sending an SMS message MaxSendAttempts uint `json:"max_send_attempts" example:"2"` diff --git a/web/models/api.ts b/web/models/api.ts index c305c134e..033825ca5 100644 --- a/web/models/api.ts +++ b/web/models/api.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +/* tslint:disable */ // @ts-nocheck /* * --------------------------------------------------------------- @@ -8,6 +10,25 @@ * --------------------------------------------------------------- */ +export enum EntitiesSubscriptionName { + SubscriptionNameFree = 'free', + SubscriptionNameProMonthly = 'pro-monthly', + SubscriptionNameProYearly = 'pro-yearly', + SubscriptionNameUltraMonthly = 'ultra-monthly', + SubscriptionNameUltraYearly = 'ultra-yearly', + SubscriptionNameProLifetime = 'pro-lifetime', + SubscriptionName20KMonthly = '20k-monthly', + SubscriptionName100KMonthly = '100k-monthly', + SubscriptionName50KMonthly = '50k-monthly', + SubscriptionName200KMonthly = '200k-monthly', + SubscriptionName20KYearly = '20k-yearly', +} + +export enum EntitiesSIM { + SIM1 = 'SIM1', + SIM2 = 'SIM2', +} + export interface EntitiesBillingUsage { /** @example "2022-06-05T14:26:02.302718+03:00" */ created_at: string @@ -62,6 +83,8 @@ export interface EntitiesHeartbeat { } export interface EntitiesMessage { + /** @example ["https://example.com/image.jpg","https://example.com/video.mp4"] */ + attachments: string[] /** @example "+18005550100" */ contact: string /** @example "This is a sample text message" */ @@ -114,7 +137,7 @@ export interface EntitiesMessage { * * DEFAULT: used the default communication SIM card * @example "DEFAULT" */ - sim: string + sim: EntitiesSIM /** @example "pending" */ status: string /** @example "mobile-terminated" */ @@ -125,6 +148,31 @@ export interface EntitiesMessage { user_id: string } +export interface EntitiesMessageSendSchedule { + /** @example "2022-06-05T14:26:02.302718+03:00" */ + created_at: string + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + id: string + /** @example "Business Hours" */ + name: string + /** @example "Europe/Tallinn" */ + timezone: string + /** @example "2022-06-05T14:26:10.303278+03:00" */ + updated_at: string + /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */ + user_id: string + windows: EntitiesMessageSendScheduleWindow[] +} + +export interface EntitiesMessageSendScheduleWindow { + /** @example 1 */ + day_of_week: number + /** @example 1020 */ + end_minute: number + /** @example 540 */ + start_minute: number +} + export interface EntitiesMessageThread { /** @example "indigo" */ color: string @@ -166,16 +214,15 @@ export interface EntitiesPhone { max_send_attempts: number /** MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. */ message_expiration_seconds: number + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + message_send_schedule_id?: string /** @example 1 */ messages_per_minute: number /** @example "This phone cannot receive calls. Please send an SMS instead." */ missed_call_auto_reply?: string /** @example "+18005550199" */ phone_number: string - /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ - schedule_id?: string | null - /** SIM card that received the message */ - sim: string + sim: EntitiesSIM /** @example "2022-06-05T14:26:10.303278+03:00" */ updated_at: string /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */ @@ -227,7 +274,7 @@ export interface EntitiesUser { /** @example "8f9c71b8-b84e-4417-8408-a62274f65a08" */ subscription_id: string /** @example "free" */ - subscription_name: string + subscription_name: EntitiesSubscriptionName /** @example "2022-06-05T14:26:02.302718+03:00" */ subscription_renews_at?: string /** @example "on_trial" */ @@ -257,62 +304,6 @@ export interface EntitiesWebhook { user_id: string } -export interface EntitiesSendScheduleWindow { - /** @example 1 */ - day_of_week: number - /** @example 1020 */ - end_minute: number - /** @example 540 */ - start_minute: number -} - -export interface EntitiesSendSchedule { - /** @example true */ - is_active: boolean - /** @example "2022-06-05T14:26:02.302718+03:00" */ - created_at: string - /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ - id: string - /** @example "Business Hours" */ - name: string - /** @example "Africa/Accra" */ - timezone: string - /** @example "2022-06-05T14:26:10.303278+03:00" */ - updated_at: string - /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */ - user_id: string - windows: EntitiesSendScheduleWindow[] -} - -export interface RequestsSendScheduleWindow { - day_of_week: number - end_minute: number - start_minute: number -} - -export interface RequestsSendScheduleStore { - is_active: boolean - name: string - timezone: string - windows: RequestsSendScheduleWindow[] -} - -export interface ResponsesSendScheduleResponse { - data: EntitiesSendSchedule - /** @example "Request handled successfully" */ - message: string - /** @example "success" */ - status: string -} - -export interface ResponsesSendSchedulesResponse { - data: EntitiesSendSchedule[] - /** @example "Request handled successfully" */ - message: string - /** @example "success" */ - status: string -} - export interface RequestsDiscordStore { incoming_channel_id: string name: string @@ -330,14 +321,34 @@ export interface RequestsHeartbeatStore { phone_numbers: string[] } +export interface RequestsMessageAttachment { + /** + * Content is the base64-encoded attachment data + * @example "base64data..." + */ + content: string + /** + * ContentType is the MIME type of the attachment + * @example "image/jpeg" + */ + content_type: string + /** + * Name is the original filename of the attachment + * @example "photo.jpg" + */ + name: string +} + export interface RequestsMessageBulkSend { + /** Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS */ + attachments?: string[] /** @example "This is a sample text message" */ content: string /** * Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app * @example false */ - encrypted: boolean + encrypted?: boolean /** @example "+18005550199" */ from: string /** @@ -379,6 +390,8 @@ export interface RequestsMessageEvent { } export interface RequestsMessageReceive { + /** Attachments is the list of MMS attachments received with the message */ + attachments?: RequestsMessageAttachment[] /** @example "This is a sample text message received on a phone" */ content: string /** @@ -392,7 +405,7 @@ export interface RequestsMessageReceive { * SIM card that received the message * @example "SIM1" */ - sim: string + sim: EntitiesSIM /** * Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible * @example "2022-06-05T14:26:09.527976+03:00" @@ -403,6 +416,11 @@ export interface RequestsMessageReceive { } export interface RequestsMessageSend { + /** + * Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS + * @example ["https://example.com/image.jpg","https://example.com/video.mp4"] + */ + attachments?: string[] /** @example "This is a sample text message" */ content: string /** @@ -426,6 +444,18 @@ export interface RequestsMessageSend { to: string } +export interface RequestsMessageSendScheduleStore { + name: string + timezone: string + windows: RequestsMessageSendScheduleWindow[] +} + +export interface RequestsMessageSendScheduleWindow { + day_of_week: number + end_minute: number + start_minute: number +} + export interface RequestsMessageThreadUpdate { /** @example true */ is_archived: boolean @@ -461,6 +491,8 @@ export interface RequestsPhoneUpsert { * @example 12345 */ message_expiration_seconds: number + /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */ + message_send_schedule_id?: string /** @example 1 */ messages_per_minute: number /** @example "e.g. This phone cannot receive calls. Please send an SMS instead." */ @@ -597,6 +629,22 @@ export interface ResponsesMessageResponse { status: string } +export interface ResponsesMessageSendScheduleResponse { + data: EntitiesMessageSendSchedule + /** @example "Request handled successfully" */ + message: string + /** @example "success" */ + status: string +} + +export interface ResponsesMessageSendSchedulesResponse { + data: EntitiesMessageSendSchedule[] + /** @example "Request handled successfully" */ + message: string + /** @example "success" */ + status: string +} + export interface ResponsesMessageThreadsResponse { data: EntitiesMessageThread[] /** @example "Request handled successfully" */ @@ -635,6 +683,13 @@ export interface ResponsesOkString { status: string } +export interface ResponsesPaymentRequired { + /** @example "You have reached the maximum number of allowed resources. Please upgrade your plan." */ + message: string + /** @example "error" */ + status: string +} + export interface ResponsesPhoneAPIKeyResponse { data: EntitiesPhoneAPIKey /** @example "Request handled successfully" */ From 90127e5599bd2532013cfa5c005d48842cb5fc34 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 00:16:00 +0300 Subject: [PATCH 126/381] Fix time --- .../gorm_phone_notification_repository.go | 4 +- .../services/phone_notification_service.go | 13 ++-- web/pages/settings/index.vue | 18 ++--- web/store/index.ts | 77 +++++++++++-------- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go index a36bac073..fc8d1be69 100644 --- a/api/pkg/repositories/gorm_phone_notification_repository.go +++ b/api/pkg/repositories/gorm_phone_notification_repository.go @@ -99,7 +99,7 @@ func (repository *gormPhoneNotificationRepository) Schedule( schedule *entities.MessageSendSchedule, notification *entities.PhoneNotification, ) error { - ctx, span := repository.tracer.Start(ctx) + ctx, span, _ := repository.tracer.StartWithLogger(ctx, repository.logger) defer span.End() now := time.Now().UTC() @@ -174,7 +174,7 @@ func (repository *gormPhoneNotificationRepository) resolveScheduledAt( return schedule.ResolveScheduledAt(current) } -// maxTime returns the later of the two times. +// maxTime returns the greater of the two time.Time. func (repository *gormPhoneNotificationRepository) maxTime(a, b time.Time) time.Time { if a.After(b) { return a diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 63b62678d..5da01f04a 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -205,16 +205,16 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P var schedule *entities.MessageSendSchedule if phone.MessageSendScheduleID != nil { schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.MessageSendScheduleID) - if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { - schedule = nil - err = nil - } - if err != nil { + if err != nil && stacktrace.GetCode(err) != repositories.ErrCodeNotFound { msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID) return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) } } + if schedule != nil { + ctxLogger.Info(fmt.Sprintf("loaded [%T] with ID [%s] for phone [%s]", schedule, schedule.ID, phone.ID)) + } + if err = service.phoneNotificationRepository.Schedule(ctx, phone.MessagesPerMinute, schedule, notification); err != nil { msg := fmt.Sprintf("cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID) return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) @@ -229,10 +229,11 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P } ctxLogger.Info(fmt.Sprintf( - "message with id [%s] notification scheduled for [%s] with id [%s]", + "message with id [%s] notification scheduled for [%s] with id [%s] with phone schedule ID [%s]", params.MessageID, notification.ScheduledAt, notification.ID, + phone.MessageSendScheduleID, )) return nil } diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index 6df69cd26..d24586fb3 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -1126,8 +1126,8 @@ import { ErrorMessages } from '~/plugins/errors' import LoadingButton from '~/components/LoadingButton.vue' import { EntitiesDiscord, + EntitiesMessageSendSchedule, EntitiesPhone, - EntitiesSendSchedule, EntitiesWebhook, } from '~/models/api' @@ -1204,7 +1204,7 @@ export default Vue.extend({ updatingPhone: false, updatingDiscord: false, loadingDiscordIntegrations: false, - sendSchedules: [] as EntitiesSendSchedule[], + sendSchedules: [] as EntitiesMessageSendSchedule[], events: [ 'message.phone.received', 'message.phone.sent', @@ -1219,13 +1219,11 @@ export default Vue.extend({ id: null, name: '', timezone: '', - is_active: true, windows: [], } as { id: string | null name: string timezone: string - is_active: boolean windows: Array<{ day_of_week: number start_time: string @@ -1588,7 +1586,7 @@ export default Vue.extend({ this.loadingSendSchedules = true this.$store .dispatch('getSendSchedules') - .then((sendSchedules: EntitiesSendSchedule[]) => { + .then((sendSchedules: EntitiesMessageSendSchedule[]) => { this.sendSchedules = sendSchedules }) .finally(() => { @@ -1673,7 +1671,7 @@ export default Vue.extend({ return this.weekDays.find((x) => x.value == index)?.label ?? '' }, - scheduleSummary(schedule: EntitiesSendSchedule) { + scheduleSummary(schedule: EntitiesMessageSendSchedule) { return this.weekDays .map((day) => { const windows = (schedule.windows || []).filter( @@ -1711,7 +1709,6 @@ export default Vue.extend({ id: null, name: '', timezone: this.defaultTimezone(), - is_active: true, windows: [ { day_of_week: 1, start_time: '09:00', end_time: '17:00' }, { day_of_week: 2, start_time: '09:00', end_time: '17:00' }, @@ -1723,16 +1720,12 @@ export default Vue.extend({ this.showScheduleEdit = true }, - openEditSchedule(schedule: EntitiesSendSchedule) { + openEditSchedule(schedule: EntitiesMessageSendSchedule) { this.resetErrors() this.activeSchedule = { id: schedule.id, name: schedule.name, timezone: schedule.timezone, - is_active: - typeof schedule.is_active !== 'undefined' - ? schedule.is_active - : Boolean((schedule as any).active), windows: (schedule.windows || []).map((x) => ({ day_of_week: x.day_of_week, start_time: this.minuteToClock(x.start_minute), @@ -1806,7 +1799,6 @@ export default Vue.extend({ const payload = { name: this.activeSchedule.name, timezone: this.activeSchedule.timezone, - is_active: this.activeSchedule.is_active, windows: (this.activeSchedule.windows || []).map((window) => ({ day_of_week: window.day_of_week, start_minute: this.clockToMinute(window.start_time), diff --git a/web/store/index.ts b/web/store/index.ts index bd3071e60..fc53c3958 100644 --- a/web/store/index.ts +++ b/web/store/index.ts @@ -9,27 +9,27 @@ import { BillingUsage } from '~/models/billing' import { EntitiesDiscord, EntitiesMessage, + EntitiesMessageSendSchedule, EntitiesPhone, EntitiesPhoneAPIKey, - EntitiesSendSchedule, EntitiesUser, EntitiesWebhook, RequestsDiscordStore, RequestsDiscordUpdate, - RequestsSendScheduleStore, + RequestsMessageSendScheduleStore, RequestsUserNotificationUpdate, RequestsUserPaymentInvoice, RequestsWebhookStore, RequestsWebhookUpdate, ResponsesDiscordResponse, ResponsesDiscordsResponse, + ResponsesMessageSendScheduleResponse, + ResponsesMessageSendSchedulesResponse, ResponsesMessagesResponse, ResponsesNoContent, ResponsesOkString, ResponsesPhoneAPIKeyResponse, ResponsesPhoneAPIKeysResponse, - ResponsesSendScheduleResponse, - ResponsesSendSchedulesResponse, ResponsesUnprocessableEntity, ResponsesUserResponse, ResponsesUserSubscriptionPaymentsResponse, @@ -381,7 +381,7 @@ export const actions = { missed_call_auto_reply: phone.missed_call_auto_reply, max_send_attempts: parseInt(phone.max_send_attempts.toString()), messages_per_minute: parseInt(phone.messages_per_minute.toString()), - schedule_id: phone.schedule_id ?? null, + message_send_schedule_id: phone.message_send_schedule_id ?? null, }) context.dispatch('addNotification', { @@ -1109,34 +1109,45 @@ export const actions = { }, getSendSchedules(context: ActionContext) { - return new Promise>((resolve, reject) => { - axios - .get(`/v1/send-schedules`) - .then((response: AxiosResponse) => { - resolve(response.data.data) - }) - .catch(async (error: AxiosError) => { - await context.dispatch('addNotification', { - message: - (error.response?.data as any)?.message ?? - 'Error while fetching send schedules', - type: 'error', + return new Promise>( + (resolve, reject) => { + axios + .get(`/v1/send-schedules`) + .then( + ( + response: AxiosResponse, + ) => { + resolve(response.data.data) + }, + ) + .catch(async (error: AxiosError) => { + await context.dispatch('addNotification', { + message: + (error.response?.data as any)?.message ?? + 'Error while fetching send schedules', + type: 'error', + }) + reject(getErrorMessages(error)) }) - reject(getErrorMessages(error)) - }) - }) + }, + ) }, createSendSchedule( context: ActionContext, - payload: RequestsSendScheduleStore, + payload: RequestsMessageSendScheduleStore, ) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { axios - .post(`/v1/send-schedules`, payload) - .then((response: AxiosResponse) => { - resolve(response.data.data) - }) + .post( + `/v1/send-schedules`, + payload, + ) + .then( + (response: AxiosResponse) => { + resolve(response.data.data) + }, + ) .catch(async (error: AxiosError) => { await context.dispatch('addNotification', { message: @@ -1151,17 +1162,19 @@ export const actions = { updateSendSchedule( context: ActionContext, - payload: RequestsSendScheduleStore & { id: string }, + payload: RequestsMessageSendScheduleStore & { id: string }, ) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { axios - .put( + .put( `/v1/send-schedules/${payload.id}`, payload, ) - .then((response: AxiosResponse) => { - resolve(response.data.data) - }) + .then( + (response: AxiosResponse) => { + resolve(response.data.data) + }, + ) .catch(async (error: AxiosError) => { await context.dispatch('addNotification', { message: From 10af367c953a67b2feef3a3f58190ac2223fd192 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 00:32:18 +0300 Subject: [PATCH 127/381] Fix the deleting of phone notifications --- .../listeners/phone_notification_listener.go | 20 ++++++++++++++++ .../gorm_phone_notification_repository.go | 24 +++++++++++++++---- .../phone_notification_repository.go | 3 +++ .../services/phone_notification_service.go | 14 +++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go index bcb156128..95333ef7d 100644 --- a/api/pkg/listeners/phone_notification_listener.go +++ b/api/pkg/listeners/phone_notification_listener.go @@ -38,6 +38,7 @@ func NewNotificationListener( events.EventTypeMessageNotificationSend: l.onMessageNotificationSend, events.PhoneHeartbeatMissed: l.onPhoneHeartbeatMissed, events.UserAccountDeleted: l.onUserAccountDeleted, + events.MessageAPIDeleted: l.onMessageAPIDeleted, } } @@ -167,3 +168,22 @@ func (listener *PhoneNotificationListener) onUserAccountDeleted(ctx context.Cont return nil } + +// onMessageAPIDeleted handles the events.MessageAPIDeleted event +func (listener *PhoneNotificationListener) onMessageAPIDeleted(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.MessageAPIDeletedPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err := listener.service.DeleteByMessageID(ctx, payload.UserID, payload.MessageID); err != nil { + msg := fmt.Sprintf("cannot delete [entities.PhoneNotification] for user [%s] and message [%s] on [%s] event with ID [%s]", payload.UserID, payload.MessageID, event.Type(), event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go index fc8d1be69..e11364157 100644 --- a/api/pkg/repositories/gorm_phone_notification_repository.go +++ b/api/pkg/repositories/gorm_phone_notification_repository.go @@ -60,12 +60,26 @@ func (repository *gormPhoneNotificationRepository) DeleteAllForUser( return nil } +// DeleteByMessageID deletes all entities.PhoneNotification for a user and message ID. +func (repository *gormPhoneNotificationRepository) DeleteByMessageID(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + err := repository.db.WithContext(ctx). + Where("user_id = ? AND message_id = ?", userID, messageID). + Delete(&entities.PhoneNotification{}).Error + if err != nil { + msg := fmt.Sprintf("cannot delete [%T] for user [%s] and message with ID [%s]", &entities.PhoneNotification{}, userID, messageID) + return repository.tracer.WrapErrorSpan(span, + stacktrace.Propagate(err, msg), + ) + } + + return nil +} + // UpdateStatus updates the status of a phone notification. -func (repository *gormPhoneNotificationRepository) UpdateStatus( - ctx context.Context, - notificationID uuid.UUID, - status entities.PhoneNotificationStatus, -) error { +func (repository *gormPhoneNotificationRepository) UpdateStatus(ctx context.Context, notificationID uuid.UUID, status entities.PhoneNotificationStatus) error { ctx, span := repository.tracer.Start(ctx) defer span.End() diff --git a/api/pkg/repositories/phone_notification_repository.go b/api/pkg/repositories/phone_notification_repository.go index e8bedfe4c..2abc67f9f 100644 --- a/api/pkg/repositories/phone_notification_repository.go +++ b/api/pkg/repositories/phone_notification_repository.go @@ -22,4 +22,7 @@ type PhoneNotificationRepository interface { // DeleteAllForUser deletes all entities.PhoneNotification for a user DeleteAllForUser(ctx context.Context, userID entities.UserID) error + + // DeleteByMessageID deletes entities.PhoneNotification for a message and user + DeleteByMessageID(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error } diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 5da01f04a..4b0bf8932 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -65,6 +65,20 @@ func (service *PhoneNotificationService) DeleteAllForUser(ctx context.Context, u return nil } +// DeleteByMessageID deletes all entities.PhoneNotification for a user and message ID. +func (service *PhoneNotificationService) DeleteByMessageID(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error { + ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger) + defer span.End() + + if err := service.phoneNotificationRepository.DeleteByMessageID(ctx, userID, messageID); err != nil { + msg := fmt.Sprintf("could not delete [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + ctxLogger.Info(fmt.Sprintf("deleted [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID)) + return nil +} + // SendHeartbeatFCM sends a heartbeat message so the phone can request a heartbeat func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, payload *events.PhoneHeartbeatMissedPayload) error { ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger) From c449088444bc5b85db4973ad8b65afe6038939ba Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 09:23:40 +0300 Subject: [PATCH 128/381] Fix the frontend --- web/pages/settings/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index d24586fb3..3203d0f15 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -1774,10 +1774,10 @@ export default Vue.extend({ } const message = messages.find((x: string) => - x.includes(`day_of_week ${index}`), + x.includes(`Day of week ${index}`), ) return message - ? message.replace(`day_of_week ${index}`, this.getWeekday(index)) + ? message.replace(`Day of week ${index}`, this.getWeekday(index)) : null }, From 4484aee0aa43058873c9b78ba08266b3022174ba Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 09:32:11 +0300 Subject: [PATCH 129/381] fix(web): use strict equality to fix eqeqeq lint errors Replace == with === in pages/settings/index.vue at lines 1671 and 1772 to resolve eslint eqeqeq rule violations causing CI failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- web/pages/settings/index.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index 3203d0f15..f5494e249 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -1668,7 +1668,7 @@ export default Vue.extend({ }, getWeekday(index: number): string { - return this.weekDays.find((x) => x.value == index)?.label ?? '' + return this.weekDays.find((x) => x.value === index)?.label ?? '' }, scheduleSummary(schedule: EntitiesMessageSendSchedule) { @@ -1769,7 +1769,7 @@ export default Vue.extend({ const messages = this.errorMessages.has('windows') ? this.errorMessages.get('windows') : [] - if (messages.length == 0) { + if (messages.length === 0) { return null } From bb3192200988ee8ea8397ac52d0cf753b014bc44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:33:36 +0300 Subject: [PATCH 130/381] fix(deps): bump core-js from 3.48.0 to 3.49.0 in /web (#878) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.48.0 to 3.49.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.49.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-version: 3.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 62 +++++++++++++++++++--------------------------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/web/package.json b/web/package.json index 9eb11649c..67c36f193 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "@nuxtjs/sitemap": "^2.4.0", "chart.js": "^4.5.1", "chartjs-adapter-moment": "^1.0.1", - "core-js": "^3.48.0", + "core-js": "^3.49.0", "date-fns": "^2.30.0", "dotenv": "^17.2.3", "firebase": "^10.14.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 17babf5df..62e097547 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^1.0.1 version: 1.0.1(chart.js@4.5.1)(moment@2.30.1) core-js: - specifier: ^3.48.0 - version: 3.48.0 + specifier: ^3.49.0 + version: 3.49.0 date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -3741,8 +3741,8 @@ packages: resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. - core-js@3.48.0: - resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -6373,6 +6373,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7359,8 +7364,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss@8.5.13: + resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: @@ -8733,6 +8738,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -11618,7 +11624,7 @@ snapshots: '@babel/preset-env': 7.24.7(@babel/core@7.24.7) '@babel/runtime': 7.24.7 '@vue/babel-preset-jsx': 1.4.0(@babel/core@7.24.7)(vue@2.7.16) - core-js: 3.48.0 + core-js: 3.49.0 core-js-compat: 3.37.1 regenerator-runtime: 0.14.1 transitivePeerDependencies: @@ -12026,11 +12032,11 @@ snapshots: cache-loader: 4.1.0(webpack@4.47.0) caniuse-lite: 1.0.30001639 consola: 3.2.3 - css-loader: 5.2.7(webpack@4.47.0) + css-loader: 5.2.7(webpack@5.104.1) cssnano: 7.0.3(postcss@8.5.6) eventsource-polyfill: 0.9.6 extract-css-chunks-webpack-plugin: 4.10.0(webpack@4.47.0) - file-loader: 6.2.0(webpack@4.47.0) + file-loader: 6.2.0(webpack@5.104.1) glob: 8.1.0 hard-source-webpack-plugin: 0.13.1(webpack@4.47.0) hash-sum: 2.0.0 @@ -12055,8 +12061,8 @@ snapshots: time-fix-plugin: 2.0.7(webpack@4.47.0) ufo: 1.6.1 upath: 2.0.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0) - vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) + vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) vue-style-loader: 4.1.3 vue-template-compiler: 2.7.16 watchpack: 2.5.0 @@ -14053,7 +14059,7 @@ snapshots: core-js@2.6.12: {} - core-js@3.48.0: {} + core-js@3.49.0: {} core-util-is@1.0.3: {} @@ -14169,20 +14175,6 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - css-loader@5.2.7(webpack@4.47.0): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - loader-utils: 2.0.4 - postcss: 8.5.6 - postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) - postcss-modules-scope: 3.0.0(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) - postcss-value-parser: 4.2.0 - schema-utils: 3.3.0 - semver: 7.7.3 - webpack: 4.47.0 - css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -15199,12 +15191,6 @@ snapshots: dependencies: flat-cache: 3.1.1 - file-loader@6.2.0(webpack@4.47.0): - dependencies: - loader-utils: 2.0.4 - schema-utils: 3.3.0 - webpack: 4.47.0 - file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 @@ -17318,6 +17304,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + nanoid@3.3.8: {} nanomatch@1.2.13: @@ -18447,9 +18435,9 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 - postcss@8.5.10: + postcss@8.5.13: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -19973,7 +19961,7 @@ snapshots: urix@0.1.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 @@ -20048,7 +20036,7 @@ snapshots: vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 - postcss: 8.5.10 + postcss: 8.5.13 rollup: 3.30.0 optionalDependencies: '@types/node': 25.1.0 @@ -20120,7 +20108,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): + vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): dependencies: '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21) css-loader: 5.2.7(webpack@5.104.1) From 2ea3cd7e4d9a398254eb19678d43fc8eb6128f95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:33:42 +0300 Subject: [PATCH 131/381] chore(deps-dev): bump @commitlint/config-conventional in /web (#876) Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.4.0 to 20.5.3. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.3/@commitlint/config-conventional) --- updated-dependencies: - dependency-name: "@commitlint/config-conventional" dependency-version: 20.5.3 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 57 +++++++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/web/package.json b/web/package.json index 67c36f193..ccee5e23d 100644 --- a/web/package.json +++ b/web/package.json @@ -56,7 +56,7 @@ "devDependencies": { "@babel/eslint-parser": "^7.28.6", "@commitlint/cli": "^20.4.0", - "@commitlint/config-conventional": "^20.4.0", + "@commitlint/config-conventional": "^20.5.3", "@nuxt/types": "^2.18.1", "@nuxt/typescript-build": "^3.0.2", "@nuxtjs/eslint-config-typescript": "^12.1.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 62e097547..be5adddab 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -106,8 +106,8 @@ importers: specifier: ^20.4.0 version: 20.4.0(@types/node@25.1.0)(typescript@4.9.5) '@commitlint/config-conventional': - specifier: ^20.4.0 - version: 20.4.0 + specifier: ^20.5.3 + version: 20.5.3 '@nuxt/types': specifier: ^2.18.1 version: 2.18.1 @@ -1017,8 +1017,8 @@ packages: engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@20.4.0': - resolution: {integrity: sha512-nolhFe2YKIix0D4+tPXAWnnIc9WB5fOCgmm4h2EcRyEShC64oH/DpM9n++85NRdItvIhKb+Szsaeuug7KcEeIA==} + '@commitlint/config-conventional@20.5.3': + resolution: {integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==} engines: {node: '>=v18'} '@commitlint/config-validator@20.4.0': @@ -1081,6 +1081,10 @@ packages: resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} engines: {node: '>=v18'} + '@commitlint/types@20.5.0': + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} + engines: {node: '>=v18'} + '@csstools/cascade-layer-name-parser@1.0.12': resolution: {integrity: sha512-iNCCOnaoycAfcIot3v/orjkTol+j8+Z5xgpqxUpZSdqeaxCADQZtldHhlvzDipmi7OoWdcJUO6DRZcnkMSBEIg==} engines: {node: ^14 || ^16 || >=18} @@ -2227,6 +2231,10 @@ packages: rollup: optional: true + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3710,8 +3718,8 @@ packages: resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@9.1.0: - resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==} + conventional-changelog-conventionalcommits@9.3.1: + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} engines: {node: '>=18'} conventional-commits-parser@6.2.1: @@ -3719,6 +3727,11 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -10343,19 +10356,19 @@ snapshots: - '@types/node' - typescript - '@commitlint/config-conventional@20.4.0': + '@commitlint/config-conventional@20.5.3': dependencies: - '@commitlint/types': 20.4.0 - conventional-changelog-conventionalcommits: 9.1.0 + '@commitlint/types': 20.5.0 + conventional-changelog-conventionalcommits: 9.3.1 '@commitlint/config-validator@20.4.0': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 ajv: 8.17.1 '@commitlint/ensure@20.4.0': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 kasi: 2.0.1 '@commitlint/execute-rule@20.0.0': {} @@ -10367,7 +10380,7 @@ snapshots: '@commitlint/is-ignored@20.4.0': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 semver: 7.7.3 '@commitlint/lint@20.4.0': @@ -10396,7 +10409,7 @@ snapshots: '@commitlint/parse@20.4.0': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 conventional-changelog-angular: 8.1.0 conventional-commits-parser: 6.2.1 @@ -10411,7 +10424,7 @@ snapshots: '@commitlint/resolve-extends@20.4.0': dependencies: '@commitlint/config-validator': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 global-directory: 4.0.1 import-meta-resolve: 4.2.0 lodash.mergewith: 4.6.2 @@ -10422,7 +10435,7 @@ snapshots: '@commitlint/ensure': 20.4.0 '@commitlint/message': 20.4.0 '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.5.0 '@commitlint/to-lines@20.0.0': {} @@ -10435,6 +10448,11 @@ snapshots: conventional-commits-parser: 6.2.1 picocolors: 1.1.1 + '@commitlint/types@20.5.0': + dependencies: + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 + '@csstools/cascade-layer-name-parser@1.0.12(@csstools/css-parser-algorithms@2.7.0(@csstools/css-tokenizer@2.3.2))(@csstools/css-tokenizer@2.3.2)': dependencies: '@csstools/css-parser-algorithms': 2.7.0(@csstools/css-tokenizer@2.3.2) @@ -12306,6 +12324,8 @@ snapshots: optionalDependencies: rollup: 3.30.0 + '@simple-libs/stream-utils@1.2.0': {} + '@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.34.41': {} @@ -14030,7 +14050,7 @@ snapshots: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@9.1.0: + conventional-changelog-conventionalcommits@9.3.1: dependencies: compare-func: 2.0.0 @@ -14038,6 +14058,11 @@ snapshots: dependencies: meow: 13.2.0 + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + convert-source-map@2.0.0: {} cookie@0.3.1: {} From 0cd5c75982317bf634ff3bb35dd3b856e05081da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:33:47 +0300 Subject: [PATCH 132/381] fix(deps): bump ufo from 1.6.1 to 1.6.4 in /web (#874) Bumps [ufo](https://github.com/unjs/ufo) from 1.6.1 to 1.6.4. - [Release notes](https://github.com/unjs/ufo/releases) - [Changelog](https://github.com/unjs/ufo/blob/main/CHANGELOG.md) - [Commits](https://github.com/unjs/ufo/compare/v1.6.1...v1.6.4) --- updated-dependencies: - dependency-name: ufo dependency-version: 1.6.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/web/package.json b/web/package.json index ccee5e23d..435c8724c 100644 --- a/web/package.json +++ b/web/package.json @@ -40,7 +40,7 @@ "nuxt-highlightjs": "^1.0.3", "pusher-js": "^8.4.0", "qrcode": "^1.5.0", - "ufo": "^1.6.1", + "ufo": "^1.6.4", "vue": "^2.7.16", "vue-chartjs": "^5.3.3", "vue-class-component": "^7.2.6", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index be5adddab..b95748a90 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^1.5.0 version: 1.5.4 ufo: - specifier: ^1.6.1 - version: 1.6.1 + specifier: ^1.6.4 + version: 1.6.4 vue: specifier: ^2.7.16 version: 2.7.16 @@ -8588,8 +8588,8 @@ packages: ua-parser-js@1.0.38: resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} @@ -11782,7 +11782,7 @@ snapshots: lodash: 4.17.21 rc9: 2.1.2 std-env: 3.7.0 - ufo: 1.6.1 + ufo: 1.6.4 '@nuxt/core@2.18.1': dependencies: @@ -11817,7 +11817,7 @@ snapshots: fs-extra: 11.2.0 html-minifier-terser: 7.2.0 node-html-parser: 6.1.13 - ufo: 1.6.1 + ufo: 1.6.4 '@nuxt/kit@3.12.2(rollup@3.30.0)': dependencies: @@ -11837,7 +11837,7 @@ snapshots: pkg-types: 1.1.2 scule: 1.3.0 semver: 7.7.3 - ufo: 1.6.1 + ufo: 1.6.4 unctx: 2.3.1 unimport: 3.7.2(rollup@3.30.0) untyped: 1.4.2 @@ -11862,7 +11862,7 @@ snapshots: pkg-types: 1.1.2 scule: 1.0.0 semver: 7.7.3 - ufo: 1.6.1 + ufo: 1.6.4 unctx: 2.3.1 unimport: 3.4.0(rollup@3.30.0) untyped: 1.4.0 @@ -11896,7 +11896,7 @@ snapshots: pkg-types: 1.1.2 scule: 1.3.0 std-env: 3.7.0 - ufo: 1.6.1 + ufo: 1.6.4 uncrypto: 0.1.3 unimport: 3.7.2(rollup@3.30.0) untyped: 1.4.2 @@ -11914,7 +11914,7 @@ snapshots: pkg-types: 1.1.2 postcss-import-resolver: 2.0.0 std-env: 3.7.0 - ufo: 1.6.1 + ufo: 1.6.4 unimport: 3.7.2(rollup@3.30.0) untyped: 1.4.2 transitivePeerDependencies: @@ -11939,7 +11939,7 @@ snapshots: serve-placeholder: 2.0.2 serve-static: 1.16.2 server-destroy: 1.0.1 - ufo: 1.6.1 + ufo: 1.6.4 transitivePeerDependencies: - supports-color @@ -12011,12 +12011,12 @@ snapshots: serialize-javascript: 6.0.2 signal-exit: 4.1.0 ua-parser-js: 1.0.38 - ufo: 1.6.1 + ufo: 1.6.4 '@nuxt/vue-app@2.18.1': dependencies: node-fetch-native: 1.6.7 - ufo: 1.6.1 + ufo: 1.6.4 unfetch: 5.0.0 vue: 2.7.16 vue-client-only: 2.1.0 @@ -12035,7 +12035,7 @@ snapshots: fs-extra: 11.2.0 lodash: 4.17.21 lru-cache: 5.1.1 - ufo: 1.6.1 + ufo: 1.6.4 vue: 2.7.16 vue-meta: 2.4.0 vue-server-renderer: 2.7.16 @@ -12077,7 +12077,7 @@ snapshots: terser-webpack-plugin: 4.2.3(webpack@4.47.0) thread-loader: 3.0.4(webpack@4.47.0) time-fix-plugin: 2.0.7(webpack@4.47.0) - ufo: 1.6.1 + ufo: 1.6.4 upath: 2.0.1 url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) @@ -17281,21 +17281,21 @@ snapshots: acorn: 8.15.0 pathe: 1.1.2 pkg-types: 1.1.2 - ufo: 1.6.1 + ufo: 1.6.4 mlly@1.7.1: dependencies: acorn: 8.15.0 pathe: 1.1.2 pkg-types: 1.1.2 - ufo: 1.6.1 + ufo: 1.6.4 mlly@1.7.3: dependencies: acorn: 8.15.0 pathe: 1.1.2 pkg-types: 1.2.1 - ufo: 1.6.1 + ufo: 1.6.4 moment@2.30.1: {} @@ -17568,7 +17568,7 @@ snapshots: execa: 8.0.1 pathe: 1.1.2 pkg-types: 1.2.1 - ufo: 1.6.1 + ufo: 1.6.4 object-assign@4.1.1: {} @@ -19800,7 +19800,7 @@ snapshots: ua-parser-js@1.0.38: {} - ufo@1.6.1: {} + ufo@1.6.4: {} uglify-js@3.19.3: optional: true From 7a649a9a6011feaf3609ac212417c0e83507c540 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:33:50 +0300 Subject: [PATCH 133/381] fix(deps): bump jest-environment-jsdom from 30.2.0 to 30.3.0 in /web (#873) Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.2.0 to 30.3.0. - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.3.0/packages/jest-environment-jsdom) --- updated-dependencies: - dependency-name: jest-environment-jsdom dependency-version: 30.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 355 +++++++++++++++++++++++++++++++++------------ 2 files changed, 261 insertions(+), 96 deletions(-) diff --git a/web/package.json b/web/package.json index 435c8724c..c251fbe22 100644 --- a/web/package.json +++ b/web/package.json @@ -33,7 +33,7 @@ "dotenv": "^17.2.3", "firebase": "^10.14.1", "firebaseui": "^6.1.0", - "jest-environment-jsdom": "^30.2.0", + "jest-environment-jsdom": "^30.3.0", "libphonenumber-js": "^1.12.36", "moment": "^2.30.1", "nuxt": "^2.18.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index b95748a90..2fbfb638b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(firebase@10.14.1) jest-environment-jsdom: - specifier: ^30.2.0 - version: 30.2.0 + specifier: ^30.3.0 + version: 30.3.0 libphonenumber-js: specifier: ^1.12.36 version: 1.12.36 @@ -104,7 +104,7 @@ importers: version: 7.28.6(@babel/core@7.28.4)(eslint@8.57.1) '@commitlint/cli': specifier: ^20.4.0 - version: 20.4.0(@types/node@25.1.0)(typescript@4.9.5) + version: 20.4.0(@types/node@25.6.0)(typescript@4.9.5) '@commitlint/config-conventional': specifier: ^20.5.3 version: 20.5.3 @@ -119,10 +119,10 @@ importers: version: 12.1.0(eslint@8.57.1)(typescript@4.9.5) '@nuxtjs/eslint-module': specifier: ^4.1.0 - version: 4.1.0(eslint@8.57.1)(rollup@3.30.0)(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1) + version: 4.1.0(eslint@8.57.1)(rollup@3.30.0)(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1) '@nuxtjs/stylelint-module': specifier: ^5.2.0 - version: 5.2.0(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1) + version: 5.2.0(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1) '@nuxtjs/vuetify': specifier: ^1.12.3 version: 1.12.3(vue@2.7.16)(webpack@5.104.1) @@ -158,7 +158,7 @@ importers: version: 11.11.1 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@25.1.0) + version: 30.2.0(@types/node@25.6.0) lint-staged: specifier: ^16.1.4 version: 16.1.4 @@ -185,7 +185,7 @@ importers: version: 34.0.0(stylelint@15.11.0(typescript@4.9.5)) ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.1.0))(typescript@4.9.5) + version: 29.4.6(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.3.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.3.0)(jest@30.2.0(@types/node@25.6.0))(typescript@4.9.5) vue-client-only: specifier: ^2.1.0 version: 2.1.0 @@ -228,6 +228,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.24.7': resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} @@ -422,6 +426,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.24.7': resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} @@ -1854,8 +1862,8 @@ packages: resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment-jsdom-abstract@30.2.0': - resolution: {integrity: sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==} + '@jest/environment-jsdom-abstract@30.3.0': + resolution: {integrity: sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -1868,6 +1876,10 @@ packages: resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@30.2.0': resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1880,6 +1892,10 @@ packages: resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/get-type@30.1.0': resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1937,6 +1953,10 @@ packages: resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2251,6 +2271,9 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@sinonjs/fake-timers@15.3.2': + resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==} + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -2355,12 +2378,12 @@ packages: '@types/node@16.18.55': resolution: {integrity: sha512-Y1zz/LIuJek01+hlPNzzXQhmq/Z2BCP96j18MSXC0S0jSu/IG4FFxmBs7W4/lI2vPJ7foVfEB0hUVtnOjnCiTg==} - '@types/node@24.6.2': - resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} - '@types/node@25.1.0': resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/normalize-package-data@2.4.2': resolution: {integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==} @@ -2436,6 +2459,9 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@typescript-eslint/eslint-plugin@6.7.3': resolution: {integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3371,6 +3397,10 @@ packages: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} @@ -5635,8 +5665,8 @@ packages: resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-environment-jsdom@30.2.0: - resolution: {integrity: sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==} + jest-environment-jsdom@30.3.0: + resolution: {integrity: sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -5664,10 +5694,18 @@ packages: resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.2.0: resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -5709,6 +5747,10 @@ packages: resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@30.2.0: resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6511,8 +6553,8 @@ packages: deprecated: Nuxt 2 has reached EOL and is no longer actively maintained. See https://nuxt.com/blog/nuxt2-eol for more details. hasBin: true - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} nypm@0.3.9: resolution: {integrity: sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==} @@ -6764,10 +6806,18 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -7414,6 +7464,10 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-time@1.1.0: resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} engines: {node: '>=4'} @@ -8605,12 +8659,12 @@ packages: unctx@2.3.1: resolution: {integrity: sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==} - undici-types@7.13.0: - resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici@6.19.7: resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==} engines: {node: '>=18.17'} @@ -9120,8 +9174,8 @@ packages: utf-8-validate: optional: true - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9249,6 +9303,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.24.7': {} '@babel/compat-data@7.28.4': {} @@ -9543,6 +9603,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.24.7': {} '@babel/helper-validator-option@7.27.1': {} @@ -10343,11 +10405,11 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@commitlint/cli@20.4.0(@types/node@25.1.0)(typescript@4.9.5)': + '@commitlint/cli@20.4.0(@types/node@25.6.0)(typescript@4.9.5)': dependencies: '@commitlint/format': 20.4.0 '@commitlint/lint': 20.4.0 - '@commitlint/load': 20.4.0(@types/node@25.1.0)(typescript@4.9.5) + '@commitlint/load': 20.4.0(@types/node@25.6.0)(typescript@4.9.5) '@commitlint/read': 20.4.0 '@commitlint/types': 20.4.0 tinyexec: 1.0.2 @@ -10390,14 +10452,14 @@ snapshots: '@commitlint/rules': 20.4.0 '@commitlint/types': 20.4.0 - '@commitlint/load@20.4.0(@types/node@25.1.0)(typescript@4.9.5)': + '@commitlint/load@20.4.0(@types/node@25.6.0)(typescript@4.9.5)': dependencies: '@commitlint/config-validator': 20.4.0 '@commitlint/execute-rule': 20.0.0 '@commitlint/resolve-extends': 20.4.0 '@commitlint/types': 20.4.0 cosmiconfig: 9.0.0(typescript@4.9.5) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.1.0)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.6.0)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5) is-plain-obj: 4.1.0 lodash.mergewith: 4.6.2 picocolors: 1.1.1 @@ -11275,13 +11337,13 @@ snapshots: '@grpc/grpc-js@1.6.12': dependencies: '@grpc/proto-loader': 0.7.10 - '@types/node': 25.1.0 + '@types/node': 25.6.0 optional: true '@grpc/grpc-js@1.9.15': dependencies: '@grpc/proto-loader': 0.7.10 - '@types/node': 25.1.0 + '@types/node': 25.6.0 '@grpc/proto-loader@0.6.13': dependencies: @@ -11377,24 +11439,31 @@ snapshots: '@jest/diff-sequences@30.0.1': {} - '@jest/environment-jsdom-abstract@30.2.0(jsdom@26.1.0)': + '@jest/environment-jsdom-abstract@30.3.0(jsdom@26.1.0)': dependencies: - '@jest/environment': 30.2.0 - '@jest/fake-timers': 30.2.0 - '@jest/types': 30.2.0 + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 '@types/jsdom': 21.1.7 - '@types/node': 25.1.0 - jest-mock: 30.2.0 - jest-util: 30.2.0 + '@types/node': 25.6.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 jsdom: 26.1.0 '@jest/environment@30.2.0': dependencies: '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 jest-mock: 30.2.0 + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + jest-mock: 30.3.0 + '@jest/expect-utils@30.2.0': dependencies: '@jest/get-type': 30.1.0 @@ -11410,11 +11479,20 @@ snapshots: dependencies: '@jest/types': 30.2.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 25.1.0 + '@types/node': 25.6.0 jest-message-util: 30.2.0 jest-mock: 30.2.0 jest-util: 30.2.0 + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.3.2 + '@types/node': 25.6.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + '@jest/get-type@30.1.0': {} '@jest/globals@30.2.0': @@ -11519,8 +11597,8 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.1.0 - '@types/yargs': 17.0.33 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 chalk: 4.1.2 '@jest/types@30.2.0': @@ -11533,6 +11611,16 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -12191,14 +12279,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@nuxtjs/eslint-module@4.1.0(eslint@8.57.1)(rollup@3.30.0)(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1)': + '@nuxtjs/eslint-module@4.1.0(eslint@8.57.1)(rollup@3.30.0)(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1)': dependencies: '@nuxt/kit': 3.7.4(rollup@3.30.0) chokidar: 3.5.3 eslint: 8.57.1 eslint-webpack-plugin: 4.0.1(eslint@8.57.1)(webpack@5.104.1) pathe: 1.1.1 - vite-plugin-eslint: 1.8.1(eslint@8.57.1)(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1)) + vite-plugin-eslint: 1.8.1(eslint@8.57.1)(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1)) transitivePeerDependencies: - rollup - supports-color @@ -12229,14 +12317,14 @@ snapshots: minimatch: 3.1.2 sitemap: 4.1.1 - '@nuxtjs/stylelint-module@5.2.0(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1)': + '@nuxtjs/stylelint-module@5.2.0(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1))(webpack@5.104.1)': dependencies: '@nuxt/kit': 3.12.2(rollup@3.30.0) chokidar: 3.6.0 pathe: 1.1.2 stylelint: 15.11.0(typescript@4.9.5) stylelint-webpack-plugin: 5.0.1(stylelint@15.11.0(typescript@4.9.5))(webpack@5.104.1) - vite-plugin-stylelint: 5.3.1(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1)) + vite-plugin-stylelint: 5.3.1(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1)) transitivePeerDependencies: - '@types/stylelint' - magicast @@ -12312,7 +12400,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 2.3.1 + picomatch: 2.3.2 optionalDependencies: rollup: 3.30.0 @@ -12340,6 +12428,10 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@15.3.2': + dependencies: + '@sinonjs/commons': 3.0.1 + '@tootallnate/once@2.0.0': optional: true @@ -12374,7 +12466,7 @@ snapshots: '@types/body-parser@1.19.3': dependencies: '@types/connect': 3.4.38 - '@types/node': 25.1.0 + '@types/node': 25.6.0 '@types/compression@1.7.5': dependencies: @@ -12407,7 +12499,7 @@ snapshots: '@types/express-serve-static-core@4.17.37': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 '@types/qs': 6.9.8 '@types/range-parser': 1.2.5 '@types/send': 0.17.2 @@ -12441,7 +12533,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -12451,7 +12543,7 @@ snapshots: '@types/jsonwebtoken@8.5.9': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 optional: true '@types/less@3.0.6': {} @@ -12467,14 +12559,14 @@ snapshots: '@types/node@16.18.55': {} - '@types/node@24.6.2': - dependencies: - undici-types: 7.13.0 - '@types/node@25.1.0': dependencies: undici-types: 7.16.0 + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/normalize-package-data@2.4.2': {} '@types/optimize-css-assets-webpack-plugin@5.0.8': @@ -12495,14 +12587,14 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 25.1.0 + '@types/node': 12.20.55 '@types/semver@7.5.3': {} '@types/send@0.17.2': dependencies: '@types/mime': 1.3.3 - '@types/node': 25.1.0 + '@types/node': 16.18.55 '@types/serve-static@1.15.7': dependencies: @@ -12542,7 +12634,7 @@ snapshots: '@types/webpack-sources@3.2.1': dependencies: - '@types/node': 25.1.0 + '@types/node': 16.18.55 '@types/source-list-map': 0.1.3 source-map: 0.7.6 @@ -12561,6 +12653,10 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@6.7.3(@typescript-eslint/parser@6.7.3(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)': dependencies: '@eslint-community/regexpp': 4.9.0 @@ -13850,6 +13946,8 @@ snapshots: ci-info@4.3.0: {} + ci-info@4.4.0: {} + cipher-base@1.0.4: dependencies: inherits: 2.0.4 @@ -14088,9 +14186,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.1.0)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.6.0)(cosmiconfig@9.0.0(typescript@4.9.5))(typescript@4.9.5): dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 cosmiconfig: 9.0.0(typescript@4.9.5) jiti: 2.6.1 typescript: 4.9.5 @@ -16304,7 +16402,7 @@ snapshots: '@jest/expect': 30.2.0 '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.0 @@ -16324,7 +16422,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.2.0(@types/node@25.1.0): + jest-cli@30.2.0(@types/node@25.6.0): dependencies: '@jest/core': 30.2.0 '@jest/test-result': 30.2.0 @@ -16332,7 +16430,7 @@ snapshots: chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@25.1.0) + jest-config: 30.2.0(@types/node@25.6.0) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -16375,6 +16473,38 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@30.2.0(@types/node@25.6.0): + dependencies: + '@babel/core': 7.28.4 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.4) + chalk: 4.1.2 + ci-info: 4.3.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.6.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@30.2.0: dependencies: '@jest/diff-sequences': 30.0.1 @@ -16394,12 +16524,10 @@ snapshots: jest-util: 30.2.0 pretty-format: 30.2.0 - jest-environment-jsdom@30.2.0: + jest-environment-jsdom@30.3.0: dependencies: - '@jest/environment': 30.2.0 - '@jest/environment-jsdom-abstract': 30.2.0(jsdom@26.1.0) - '@types/jsdom': 21.1.7 - '@types/node': 24.6.2 + '@jest/environment': 30.3.0 + '@jest/environment-jsdom-abstract': 30.3.0(jsdom@26.1.0) jsdom: 26.1.0 transitivePeerDependencies: - bufferutil @@ -16411,7 +16539,7 @@ snapshots: '@jest/environment': 30.2.0 '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 jest-mock: 30.2.0 jest-util: 30.2.0 jest-validate: 30.2.0 @@ -16419,7 +16547,7 @@ snapshots: jest-haste-map@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -16455,12 +16583,30 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 jest-util: 30.2.0 + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + jest-util: 30.3.0 + jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): optionalDependencies: jest-resolve: 30.2.0 @@ -16568,21 +16714,30 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.1.0 + '@types/node': 25.6.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 - picomatch: 2.3.1 + picomatch: 2.3.2 jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 chalk: 4.1.2 ci-info: 4.3.0 graceful-fs: 4.2.11 picomatch: 4.0.3 + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 25.6.0 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + jest-validate@30.2.0: dependencies: '@jest/get-type': 30.1.0 @@ -16605,37 +16760,37 @@ snapshots: jest-worker@26.6.2: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 merge-stream: 2.0.0 supports-color: 7.2.0 jest-worker@27.5.1: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.2.0: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 '@ungap/structured-clone': 1.3.0 jest-util: 30.2.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.2.0(@types/node@25.1.0): + jest@30.2.0(@types/node@25.6.0): dependencies: '@jest/core': 30.2.0 '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@25.1.0) + jest-cli: 30.2.0(@types/node@25.6.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -16691,7 +16846,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 + nwsapi: 2.2.23 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -16702,7 +16857,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.3 + ws: 8.20.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -17559,7 +17714,7 @@ snapshots: - webpack-command - whiskers - nwsapi@2.2.22: {} + nwsapi@2.2.23: {} nypm@0.3.9: dependencies: @@ -17818,8 +17973,12 @@ snapshots: picomatch@2.3.1: {} + picomatch@2.3.2: {} + picomatch@4.0.3: {} + picomatch@4.0.4: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -18494,6 +18653,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-time@1.1.0: {} pretty@2.0.0: @@ -18536,7 +18701,7 @@ snapshots: '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 '@types/long': 4.0.2 - '@types/node': 25.1.0 + '@types/node': 25.6.0 long: 4.0.0 optional: true @@ -18553,7 +18718,7 @@ snapshots: '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 '@types/long': 4.0.2 - '@types/node': 25.1.0 + '@types/node': 25.6.0 long: 4.0.0 optional: true @@ -18569,7 +18734,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 25.1.0 + '@types/node': 25.6.0 long: 5.2.3 protocols@2.0.1: {} @@ -19685,12 +19850,12 @@ snapshots: dependencies: typescript: 4.9.5 - ts-jest@29.4.6(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.1.0))(typescript@4.9.5): + ts-jest@29.4.6(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.3.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.3.0)(jest@30.2.0(@types/node@25.6.0))(typescript@4.9.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@25.1.0) + jest: 30.2.0(@types/node@25.6.0) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19701,9 +19866,9 @@ snapshots: optionalDependencies: '@babel/core': 7.28.4 '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 + '@jest/types': 30.3.0 babel-jest: 30.2.0(@babel/core@7.28.4) - jest-util: 30.2.0 + jest-util: 30.3.0 ts-loader@8.4.0(typescript@4.9.5)(webpack@5.104.1): dependencies: @@ -19821,10 +19986,10 @@ snapshots: magic-string: 0.30.10 unplugin: 1.11.0 - undici-types@7.13.0: {} - undici-types@7.16.0: {} + undici-types@7.19.2: {} + undici@6.19.7: {} unfetch@5.0.0: {} @@ -20037,34 +20202,34 @@ snapshots: vary@1.1.2: {} - vite-plugin-eslint@1.8.1(eslint@8.57.1)(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1)): + vite-plugin-eslint@1.8.1(eslint@8.57.1)(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1)): dependencies: '@rollup/pluginutils': 4.2.1 '@types/eslint': 8.44.3 eslint: 8.57.1 rollup: 2.79.2 - vite: 4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1) + vite: 4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1) - vite-plugin-stylelint@5.3.1(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1)): + vite-plugin-stylelint@5.3.1(postcss@8.5.6)(rollup@3.30.0)(stylelint@15.11.0(typescript@4.9.5))(vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1)): dependencies: '@rollup/pluginutils': 5.1.0(rollup@3.30.0) chokidar: 3.6.0 debug: 4.4.1 stylelint: 15.11.0(typescript@4.9.5) - vite: 4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1) + vite: 4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1) optionalDependencies: postcss: 8.5.6 rollup: 3.30.0 transitivePeerDependencies: - supports-color - vite@4.5.3(@types/node@25.1.0)(sass@1.32.13)(terser@5.44.1): + vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 postcss: 8.5.13 rollup: 3.30.0 optionalDependencies: - '@types/node': 25.1.0 + '@types/node': 25.6.0 fsevents: 2.3.3 sass: 1.32.13 terser: 5.44.1 @@ -20531,7 +20696,7 @@ snapshots: ws@7.5.10: {} - ws@8.18.3: {} + ws@8.20.0: {} xdg-basedir@4.0.0: optional: true From aa7aa102073bc1172b4484965ab00348994606ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:33:57 +0300 Subject: [PATCH 134/381] chore(deps-dev): bump axios from 0.31.0 to 0.31.1 in /web (#868) Bumps [axios](https://github.com/axios/axios) from 0.31.0 to 0.31.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.31.0...v0.31.1) --- updated-dependencies: - dependency-name: axios dependency-version: 0.31.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/web/package.json b/web/package.json index c251fbe22..16f18812f 100644 --- a/web/package.json +++ b/web/package.json @@ -65,7 +65,7 @@ "@nuxtjs/vuetify": "^1.12.3", "@types/qrcode": "^1.5.6", "@vue/test-utils": "^1.3.6", - "axios": "^0.31.0", + "axios": "^0.31.1", "babel-core": "7.0.0-bridge.0", "babel-jest": "^30.2.0", "eslint": "^8.57.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2fbfb638b..f15a174a1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -133,8 +133,8 @@ importers: specifier: ^1.3.6 version: 1.3.6(vue-template-compiler@2.7.16)(vue@2.7.16) axios: - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.31.1 + version: 0.31.1 babel-core: specifier: 7.0.0-bridge.0 version: 7.0.0-bridge.0(@babel/core@7.28.4) @@ -3043,8 +3043,8 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - axios@0.31.0: - resolution: {integrity: sha512-HGIUj/P74co3rSLBV9SHz9LMgCmrXFEtkfMcC5r6bS5j3dBHUcAje2tS4fmU6WM20kuhvUX04XE58594dpgi1g==} + axios@0.31.1: + resolution: {integrity: sha512-Ef8DUZSZQP6igY48mjGaoEjwhely97lserep0IFJifBH4YdKvwH5eMLniy3kig2HQoBNR8EkZpDjowxwTJcmbg==} babel-code-frame@6.26.0: resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} @@ -5172,8 +5172,8 @@ packages: hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} he@1.2.0: @@ -13385,7 +13385,7 @@ snapshots: available-typed-arrays@1.0.5: {} - axios@0.31.0: + axios@0.31.1: dependencies: follow-redirects: 1.16.0 form-data: 4.0.5 @@ -14834,7 +14834,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.3 es-shim-unscopables@1.0.0: dependencies: @@ -15497,7 +15497,7 @@ snapshots: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.3 mime-types: 2.1.35 fraction.js@4.3.7: {} @@ -15618,7 +15618,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.3 math-intrinsics: 1.1.0 get-package-type@0.1.0: {} @@ -15941,7 +15941,7 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 - hasown@2.0.2: + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -17748,7 +17748,7 @@ snapshots: dependencies: call-bind: 1.0.2 define-properties: 1.2.1 - has-symbols: 1.0.3 + has-symbols: 1.1.0 object-keys: 1.1.1 object.fromentries@2.0.7: @@ -19051,7 +19051,7 @@ snapshots: dependencies: call-bind: 1.0.2 get-intrinsic: 1.3.0 - has-symbols: 1.0.3 + has-symbols: 1.1.0 isarray: 2.0.5 safe-buffer@5.1.2: {} @@ -19974,7 +19974,7 @@ snapshots: dependencies: call-bind: 1.0.2 has-bigints: 1.0.2 - has-symbols: 1.0.3 + has-symbols: 1.1.0 which-boxed-primitive: 1.0.2 uncrypto@0.1.3: {} @@ -20619,7 +20619,7 @@ snapshots: available-typed-arrays: 1.0.5 call-bind: 1.0.2 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.2 which@1.3.1: From 271be0e7d9d16312925b2dd6e8a9980db23801fa Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 09:37:35 +0300 Subject: [PATCH 135/381] Update deps --- api/go.mod | 12 ++++++------ api/go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/api/go.mod b/api/go.mod index 40a311d3a..0c6ccd0b5 100644 --- a/api/go.mod +++ b/api/go.mod @@ -85,7 +85,7 @@ require ( dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/ClickHouse/ch-go v0.71.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.45.0 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.46.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect @@ -158,7 +158,7 @@ require ( github.com/swaggo/files/v2 v2.0.2 // indirect github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.70.0 // indirect + github.com/valyala/fasthttp v1.71.0 // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.33.0 // indirect github.com/xuri/efp v0.0.1 // indirect @@ -191,10 +191,10 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20260427160629-7cedc36a6bc4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/grpc v1.81.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect diff --git a/api/go.sum b/api/go.sum index 02111796a..5af5496ff 100644 --- a/api/go.sum +++ b/api/go.sum @@ -16,8 +16,8 @@ cloud.google.com/go/firestore v1.22.0 h1:avooeboIq37vKXobrbPUFhFBxS/c3FqmWoX0xs8 cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU= cloud.google.com/go/iam v1.10.0 h1:cWWt8u8jXv3MzpvBmQgNClvvbVCRukruCJAnoK3fIJY= cloud.google.com/go/iam v1.10.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= -cloud.google.com/go/logging v1.16.0 h1:MMNgYRvZ/pEwiNSkcoJTKWfAbAJDqCqAMJiarZx+/CI= -cloud.google.com/go/logging v1.16.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= +cloud.google.com/go/logging v1.17.0 h1:rUFekZYwHiKElXCyz3zYBGz4BOeIqzgCKxVLdgrZ5mY= +cloud.google.com/go/logging v1.17.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= cloud.google.com/go/longrunning v0.12.0 h1:wLv2hXvID9zHejLtcPo1B0JBjErnwZCYAPKSTa65xpY= cloud.google.com/go/longrunning v0.12.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= cloud.google.com/go/monitoring v1.28.0 h1:jOe0Wkm+a56ptZnEeyHevXo7+KPWAPPP5wUTEJdP7GY= @@ -34,8 +34,8 @@ firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVA firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= -github.com/ClickHouse/clickhouse-go/v2 v2.45.0 h1:iHt15nA4iYhfde5bDQAcLAat9BAh7B5ksPRNRa4UI7s= -github.com/ClickHouse/clickhouse-go/v2 v2.45.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= +github.com/ClickHouse/clickhouse-go/v2 v2.46.0 h1:s3eRy+hYmu5uzotB6ZhDofgHu8kDgGN/fpmjxRkqSpk= +github.com/ClickHouse/clickhouse-go/v2 v2.46.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg= @@ -324,8 +324,8 @@ github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLly github.com/uptrace/uptrace-go v1.43.0/go.mod h1:ehDTIdtBSolg4Z0CCvg1C8yR6VX1YFDqBcg2KmsXWn0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= -github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= +github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= +github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= github.com/vanng822/go-premailer v1.33.0 h1:nglIpKn/7e3kIAwYByiH5xpauFur7RwAucqyZ59hcic= @@ -495,14 +495,14 @@ google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk= google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20260427160629-7cedc36a6bc4 h1:2iMJZntwvmfgtse+s744JY7v7PgEdSBuFYXucvpOHNM= -google.golang.org/genproto v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:v14kaaboYyXQ1Gsu489Q+Hg/oN4B33mWtuOhF1HCeXA= -google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo= -google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8= +google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= From 7d37ffdd6c5b89084b132e26b65b5adee91c585d Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 09:52:07 +0300 Subject: [PATCH 136/381] Add validation logic --- web/pages/settings/index.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index f5494e249..ebe955623 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -619,6 +619,13 @@ dense placeholder="How many retries when sending an SMS" label="Max Send Attempts" + min="1" + max="5" + :rules="[ + (v) => + (v >= 1 && v <= 5) || + 'Max send attempts must be between 1 and 5', + ]" > Date: Tue, 5 May 2026 10:02:16 +0300 Subject: [PATCH 137/381] Add not found response code --- api/pkg/handlers/phone_handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go index 28db85857..4c8efa094 100644 --- a/api/pkg/handlers/phone_handler.go +++ b/api/pkg/handlers/phone_handler.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" + "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" @@ -165,6 +166,9 @@ func (h *PhoneHandler) Delete(c *fiber.Ctx) error { } err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c), request.PhoneIDUuid()) + if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { + return h.responseNotFound(c, fmt.Sprintf("cannot find phone with ID [%s]", request.PhoneID)) + } if err != nil { msg := fmt.Sprintf("cannot delete phones with params [%+#v]", request) ctxLogger.Error(stacktrace.Propagate(err, msg)) From e55cf10f0ff2e8e75668697c6bba88d4ddc1cb95 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 5 May 2026 23:58:11 +0300 Subject: [PATCH 138/381] feat: add integration test infrastructure with E2E SMS tests (#879) * Added integration tests * Fix readme * Use stable go version * Fix api --- .github/workflows/api.yml | 99 ++++++ .github/workflows/{ci.yml => web.yml} | 4 +- .gitignore | 3 + README.md | 24 +- api/Dockerfile | 2 +- api/cmd/fcm/main.go | 2 +- api/pkg/di/container.go | 37 +- api/pkg/services/emulator_fcm_client.go | 100 ++++++ api/pkg/services/fcm_client.go | 28 ++ .../services/phone_notification_service.go | 4 +- ...05-05-integration-tests-wiremock-design.md | 304 +++++++++++++++++ tests/.env.test | 30 ++ tests/README.md | 214 ++++++++++++ tests/docker-compose.yml | 88 +++++ tests/generate-firebase-credentials.sh | 31 ++ tests/go.mod | 17 + tests/go.sum | 114 +++++++ tests/helpers_test.go | 315 ++++++++++++++++++ tests/integration_test.go | 263 +++++++++++++++ tests/seed.sql | 26 ++ tests/wiremock/mappings/fcm-send.json | 15 + tests/wiremock/mappings/oauth-token.json | 17 + tests/wiremock/mappings/webhook-receiver.json | 15 + 23 files changed, 1736 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/api.yml rename .github/workflows/{ci.yml => web.yml} (97%) create mode 100644 api/pkg/services/emulator_fcm_client.go create mode 100644 api/pkg/services/fcm_client.go create mode 100644 docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md create mode 100644 tests/.env.test create mode 100644 tests/README.md create mode 100644 tests/docker-compose.yml create mode 100644 tests/generate-firebase-credentials.sh create mode 100644 tests/go.mod create mode 100644 tests/go.sum create mode 100644 tests/helpers_test.go create mode 100644 tests/integration_test.go create mode 100644 tests/seed.sql create mode 100644 tests/wiremock/mappings/fcm-send.json create mode 100644 tests/wiremock/mappings/oauth-token.json create mode 100644 tests/wiremock/mappings/webhook-receiver.json diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml new file mode 100644 index 000000000..aa7f20d7c --- /dev/null +++ b/.github/workflows/api.yml @@ -0,0 +1,99 @@ +name: api + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + id-token: write + +jobs: + Test: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎 + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: stable + + - name: Generate Firebase credentials + run: | + bash tests/generate-firebase-credentials.sh tests/firebase-credentials.json + echo "FIREBASE_CREDENTIALS=$(jq -c . tests/firebase-credentials.json)" >> $GITHUB_ENV + + - name: Start Services + working-directory: ./tests + run: docker compose up -d --build + + - name: Wait for services to be healthy + working-directory: ./tests + run: | + echo "Waiting for API to be healthy..." + for i in $(seq 1 40); do + if docker compose exec api curl -sf http://localhost:8000/health >/dev/null 2>&1; then + echo "API is healthy!" + break + fi + if [ $i -eq 40 ]; then + echo "API failed to become healthy" + docker compose logs api + exit 1 + fi + echo "Attempt $i/40 - waiting 5s..." + sleep 5 + done + + - name: Seed Database + working-directory: ./tests + run: | + echo "Waiting for seed container to finish..." + docker compose wait seed || true + sleep 2 + + - name: Run Integration Tests + working-directory: ./tests + run: go test -v -timeout 300s ./... + + - name: Collect Logs on Failure + if: failure() + working-directory: ./tests + run: | + docker compose logs --tail 200 + + - name: Stop Services + if: always() + working-directory: ./tests + run: docker compose down -v + + Deploy: + runs-on: ubuntu-latest + needs: Test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v3 + + - name: Trigger Cloud Build Deploy 🚀 + run: | + BUILD_ID=$(gcloud builds triggers run api-httpsms-com \ + --region=global \ + --project=httpsms-86c51 \ + --sha=${{ github.sha }} \ + --format="value(metadata.build.id)") + echo "Build ID: $BUILD_ID" + echo "Streaming build logs..." + gcloud builds log "$BUILD_ID" --region=global --project=httpsms-86c51 --stream diff --git a/.github/workflows/ci.yml b/.github/workflows/web.yml similarity index 97% rename from .github/workflows/ci.yml rename to .github/workflows/web.yml index 8c1190b5f..1d9133d9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/web.yml @@ -1,4 +1,4 @@ -name: ci +name: web on: push: @@ -25,7 +25,7 @@ jobs: - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 9 + version: 10 - name: Install dependencies 📦 run: pnpm install diff --git a/.gitignore b/.gitignore index b114cdd06..edc89ca76 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ android/app/debug/ *main.exe* android/app/release/ + +tests/firebase-credentials.json +tests/emulator/emulator.exe diff --git a/README.md b/README.md index 84b77a40b..14690530a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # httpSMS -[![Build](https://github.com/NdoleStudio/httpsms/actions/workflows/ci.yml/badge.svg)](https://github.com/NdoleStudio/httpsms/actions/workflows/ci.yml) +[![Web](https://github.com/NdoleStudio/httpsms/actions/workflows/web.yml/badge.svg)](https://github.com/NdoleStudio/httpsms/actions/workflows/web.yml) +[![API](https://github.com/NdoleStudio/httpsms/actions/workflows/api.yml/badge.svg)](https://github.com/NdoleStudio/httpsms/actions/workflows/api.yml) [![GitHub contributors](https://img.shields.io/github/contributors/NdoleStudio/httpsms)](https://github.com/NdoleStudio/httpsms/graphs/contributors) [![GitHub license](https://img.shields.io/github/license/NdoleStudio/httpsms?color=brightgreen)](https://github.com/NdoleStudio/httpsms/blob/master/LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) @@ -43,6 +44,7 @@ Quick Start Guide 👉 [https://docs.httpsms.com](https://docs.httpsms.com) - [6. Build and Run](#6-build-and-run) - [7. Create the System User](#7-create-the-system-user) - [8. Build the Android App.](#8-build-the-android-app) +- [Integration Testing](#integration-testing) - [License](#license) @@ -255,6 +257,26 @@ docker compose up --build - Before building the Android app in [Android Studio](https://developer.android.com/studio), you need to replace the `google-services.json` file in the `android/app` directory with the file which you got from step 1. You need to do this for the firebase FCM messages to work properly. +## Integration Testing + +The project includes end-to-end integration tests that validate the complete SMS send/receive lifecycle. Tests run the full stack (API, PostgreSQL, Redis) in Docker alongside a phone emulator that simulates an Android device. + +📖 **Full documentation:** [`tests/README.md`](tests/README.md) + +**Quick run:** + +```bash +cd tests +bash generate-firebase-credentials.sh +export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) +docker compose up -d --build --wait +docker compose wait seed && sleep 2 +go test -v -timeout 120s ./... +docker compose down -v +``` + +Integration tests also run automatically in CI on every push/PR to `main`. + ## License This project is licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 - see the [LICENSE](LICENSE) file for details diff --git a/api/Dockerfile b/api/Dockerfile index 8e0206a25..6e6423b1d 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -21,7 +21,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=$GI FROM alpine:latest -RUN addgroup -S http-sms && adduser -S http-sms -G http-sms +RUN apk add --no-cache curl && addgroup -S http-sms && adduser -S http-sms -G http-sms USER http-sms WORKDIR /home/http-sms diff --git a/api/cmd/fcm/main.go b/api/cmd/fcm/main.go index 4906142b1..470397cf0 100644 --- a/api/cmd/fcm/main.go +++ b/api/cmd/fcm/main.go @@ -18,7 +18,7 @@ func main() { } container := di.NewContainer(os.Getenv("GCP_PROJECT_ID"), "") - client := container.FirebaseMessagingClient() + client := container.FCMClient() result, err := client.Send(context.Background(), &messaging.Message{ Data: map[string]string{ diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index b27cc37cc..ae5e15ec6 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -47,7 +47,6 @@ import ( "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.10.0" - "firebase.google.com/go/messaging" "github.com/hirosassa/zerodriver" "github.com/rs/zerolog" "go.opentelemetry.io/otel/sdk/trace" @@ -176,6 +175,11 @@ func (container *Container) App() (app *fiber.App) { app = fiber.New() + // Health check endpoint registered before middleware for reliable Docker health checks + app.Get("/health", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + if os.Getenv("USE_HTTP_LOGGER") == "true" { app.Use(fiberLogger.New()) } @@ -397,7 +401,8 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK ( // FirebaseApp creates a new instance of firebase.App func (container *Container) FirebaseApp() (app *firebase.App) { container.logger.Debug(fmt.Sprintf("creating %T", app)) - app, err := firebase.NewApp(context.Background(), nil, option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials())) + + app, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsJSON(container.FirebaseCredentials())) if err != nil { msg := "cannot initialize firebase application" container.logger.Fatal(stacktrace.Propagate(err, msg)) @@ -419,8 +424,10 @@ func (container *Container) Cache() cache.Cache { if err != nil { container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot parse redis url [%s]", os.Getenv("REDIS_URL")))) } - opt.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, + if strings.HasPrefix(os.Getenv("REDIS_URL"), "rediss://") { + opt.TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } } redisClient := redis.NewClient(opt) @@ -506,15 +513,27 @@ func (container *Container) CloudTaskEventsQueue() (queue services.PushQueue) { ) } -// FirebaseMessagingClient creates a new instance of messaging.Client -func (container *Container) FirebaseMessagingClient() (client *messaging.Client) { - container.logger.Debug(fmt.Sprintf("creating %T", client)) +// FCMClient creates the appropriate FCM client based on configuration. +// When FCM_ENDPOINT is set, it returns an EmulatorFCMClient that sends +// notifications directly to the phone emulator via HTTP. +// Otherwise, it returns a FirebaseFCMClient that uses the real Firebase SDK. +func (container *Container) FCMClient() services.FCMClient { + if fcmEndpoint := os.Getenv("FCM_ENDPOINT"); fcmEndpoint != "" { + container.logger.Info(fmt.Sprintf("using emulator FCM client with endpoint: %s", fcmEndpoint)) + return services.NewEmulatorFCMClient( + container.HTTPClient("emulator_fcm"), + fcmEndpoint, + container.Logger(), + ) + } + + container.logger.Debug("creating FirebaseFCMClient") messagingClient, err := container.FirebaseApp().Messaging(context.Background()) if err != nil { msg := "cannot initialize firebase messaging client" container.logger.Fatal(stacktrace.Propagate(err, msg)) } - return messagingClient + return services.NewFirebaseFCMClient(messagingClient) } // FirebaseCredentials returns firebase credentials as bytes. @@ -1588,7 +1607,7 @@ func (container *Container) NotificationService() (service *services.PhoneNotifi return services.NewNotificationService( container.Logger(), container.Tracer(), - container.FirebaseMessagingClient(), + container.FCMClient(), container.PhoneRepository(), container.PhoneNotificationRepository(), container.MessageSendScheduleRepository(), diff --git a/api/pkg/services/emulator_fcm_client.go b/api/pkg/services/emulator_fcm_client.go new file mode 100644 index 000000000..85060bb4e --- /dev/null +++ b/api/pkg/services/emulator_fcm_client.go @@ -0,0 +1,100 @@ +package services + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "firebase.google.com/go/messaging" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// EmulatorFCMClient sends FCM messages to the phone emulator via HTTP. +type EmulatorFCMClient struct { + httpClient *http.Client + endpoint string + logger telemetry.Logger +} + +// NewEmulatorFCMClient creates a new EmulatorFCMClient. +func NewEmulatorFCMClient(httpClient *http.Client, endpoint string, logger telemetry.Logger) *EmulatorFCMClient { + return &EmulatorFCMClient{ + httpClient: httpClient, + endpoint: endpoint, + logger: logger, + } +} + +// emulatorFCMRequest is the payload sent to the emulator's FCM endpoint. +type emulatorFCMRequest struct { + Message *emulatorFCMMessage `json:"message"` +} + +type emulatorFCMMessage struct { + Token string `json:"token"` + Data map[string]string `json:"data,omitempty"` + Android *emulatorAndroid `json:"android,omitempty"` +} + +type emulatorAndroid struct { + Priority string `json:"priority,omitempty"` +} + +// emulatorFCMResponse is the response from the emulator. +type emulatorFCMResponse struct { + Name string `json:"name"` +} + +// Send sends a message to the emulator's FCM endpoint. +func (c *EmulatorFCMClient) Send(ctx context.Context, message *messaging.Message) (string, error) { + payload := &emulatorFCMRequest{ + Message: &emulatorFCMMessage{ + Token: message.Token, + Data: message.Data, + }, + } + if message.Android != nil { + payload.Message.Android = &emulatorAndroid{ + Priority: message.Android.Priority, + } + } + + body, err := json.Marshal(payload) + if err != nil { + return "", stacktrace.Propagate(err, "cannot marshal FCM request for emulator") + } + + url := fmt.Sprintf("%s/v1/projects/httpsms-test/messages:send", c.endpoint) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return "", stacktrace.Propagate(err, "cannot create HTTP request for emulator FCM") + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", stacktrace.Propagate(err, fmt.Sprintf("cannot send FCM to emulator at [%s]", url)) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", stacktrace.Propagate(err, "cannot read emulator FCM response body") + } + + if resp.StatusCode != http.StatusOK { + return "", stacktrace.NewError("emulator FCM returned status %d: %s", resp.StatusCode, string(respBody)) + } + + var result emulatorFCMResponse + if err = json.Unmarshal(respBody, &result); err != nil { + return "", stacktrace.Propagate(err, "cannot decode emulator FCM response") + } + + c.logger.Info(fmt.Sprintf("emulator FCM sent successfully: %s", result.Name)) + return result.Name, nil +} diff --git a/api/pkg/services/fcm_client.go b/api/pkg/services/fcm_client.go new file mode 100644 index 000000000..4e56f3167 --- /dev/null +++ b/api/pkg/services/fcm_client.go @@ -0,0 +1,28 @@ +package services + +import ( + "context" + + "firebase.google.com/go/messaging" +) + +// FCMClient is the interface for sending Firebase Cloud Messaging notifications. +type FCMClient interface { + // Send sends a message via FCM and returns the message name on success. + Send(ctx context.Context, message *messaging.Message) (string, error) +} + +// FirebaseFCMClient wraps the real Firebase messaging.Client. +type FirebaseFCMClient struct { + client *messaging.Client +} + +// NewFirebaseFCMClient creates a new FirebaseFCMClient. +func NewFirebaseFCMClient(client *messaging.Client) *FirebaseFCMClient { + return &FirebaseFCMClient{client: client} +} + +// Send sends a message via the real Firebase SDK. +func (c *FirebaseFCMClient) Send(ctx context.Context, message *messaging.Message) (string, error) { + return c.client.Send(ctx, message) +} diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 4b0bf8932..79b430378 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -26,7 +26,7 @@ type PhoneNotificationService struct { phoneNotificationRepository repositories.PhoneNotificationRepository phoneRepository repositories.PhoneRepository messageSendScheduleRepository repositories.MessageSendScheduleRepository - messagingClient *messaging.Client + messagingClient FCMClient eventDispatcher *EventDispatcher } @@ -34,7 +34,7 @@ type PhoneNotificationService struct { func NewNotificationService( logger telemetry.Logger, tracer telemetry.Tracer, - messagingClient *messaging.Client, + messagingClient FCMClient, phoneRepository repositories.PhoneRepository, phoneNotificationRepository repositories.PhoneNotificationRepository, messageSendScheduleRepository repositories.MessageSendScheduleRepository, diff --git a/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md b/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md new file mode 100644 index 000000000..f2383bff4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md @@ -0,0 +1,304 @@ +# Integration Tests: WireMock + httpsms-go Client Refactor + +## Problem + +The current integration tests use raw `net/http` calls and a custom emulator (120+ lines of Go) to simulate phone behavior. This makes tests harder to maintain and doesn't cover encryption, rate limiting, or webhook verification. We need to: + +1. Refactor tests to use the official `httpsms-go` client SDK +2. Replace the custom emulator with WireMock (stub server + request journal) +3. Add E2E encryption tests (outgoing + incoming) +4. Add rate-limit verification test +5. Assert webhook delivery with JWT authentication in all tests + +## Architecture + +``` +┌────────────────────────────────────────────────────────┐ +│ Docker Compose (tests/docker-compose.yml) │ +│ │ +│ ┌──────────┐ ┌───────┐ ┌─────────────────────────┐ │ +│ │PostgreSQL│ │ Redis │ │ API │ │ +│ └──────────┘ └───────┘ └────────────┬────────────┘ │ +│ │FCM push │ +│ │Webhook calls │ +│ ▼ │ +│ ┌─────────────────────────┐ │ +│ │ WireMock 3.x (:8080) │ │ +│ │ - Fake FCM endpoint │ │ +│ │ - Fake OAuth token │ │ +│ │ - Webhook receiver │ │ +│ │ - Request journal │ │ +│ └─────────────────────────┘ │ +└────────────────────────────────────────────────────────┘ + ▲ + │ httpsms-go client + go-wiremock client +┌────────┴──────────┐ +│ Test Runner (Go) │ +│ go test ./... │ +└───────────────────┘ +``` + +### Key Design Decisions + +- **WireMock replaces the custom emulator entirely**. It serves as both the fake FCM endpoint (receives push notifications from the API) and the webhook receiver (captures webhook events). +- **Tests fire SENT/DELIVERED events directly** to the API via HTTP. No WireMock callbacks needed — the test controls the flow deterministically. +- **Each test creates its own phone** with a random phone number for parallel test isolation. +- **go-wiremock** (`github.com/wiremock/go-wiremock`) is used to configure stubs and query the request journal from test code. + +## Test Flow (per test) + +``` +1. SETUP + ├─ Create phone (random number, test-specific messages_per_minute) + ├─ Create phone API key for that phone + ├─ Create webhook pointing to WireMock with a signing key + └─ Configure WireMock stubs (if not pre-loaded) + +2. ACT + ├─ Send/receive message via httpsms-go client + └─ (For send tests) Query WireMock journal → extract KEY_MESSAGE_ID from FCM push + +3. SIMULATE PHONE + ├─ Fire SENT event to API (POST /v1/messages/{id}/events) + └─ Fire DELIVERED event to API + +4. ASSERT + ├─ Verify message reached expected status via httpsms-go client + ├─ Query WireMock journal for webhook events + ├─ Validate JWT token: signature (HMAC-SHA256), issuer, subject, audience, expiry + └─ Validate webhook payload contains correct event type and message data +``` + +## Components + +### 1. Docker Compose Changes + +**Remove:** + +- `tests/emulator/` directory entirely (Dockerfile, Go source, go.mod) + +**Replace with WireMock:** + +```yaml +wiremock: + image: wiremock/wiremock:3x + ports: + - "8080:8080" + volumes: + - ./wiremock/mappings:/home/wiremock/mappings:ro + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] + interval: 5s + timeout: 5s + retries: 10 +``` + +**Pre-loaded WireMock mappings** (`tests/wiremock/mappings/`): + +- `fcm-send.json` — Stub for `POST /v1/projects/*/messages:send` → returns `{"name": "projects/httpsms-test/messages/fake-id"}` +- `oauth-token.json` — Stub for `POST /token` → returns `{"access_token": "fake-access-token", "token_type": "Bearer", "expires_in": 3600}` +- `webhook-receiver.json` — Stub for `POST /webhooks/test` → returns 200 (catches all webhook calls) + +### 2. API Configuration Updates + +**`.env.test` changes:** + +- `FCM_ENDPOINT=http://wiremock:8080` (was `http://emulator:9090`) + +**Firebase credentials** `token_uri` points to `http://wiremock:8080/token` + +### 3. Seed SQL (simplified) + +Only seeds: + +- Test user (`test-user-id`, `api_key='test-user-api-key'`) +- System user (`system-user-id`, for event queue auth) + +Phones, phone API keys, and webhooks are created per-test via the API. + +### 4. httpsms-go Client Additions + +New services to add to `github.com/NdoleStudio/httpsms-go`: + +#### `PhoneService` + +```go +type PhoneUpsertParams struct { + PhoneNumber string `json:"phone_number"` + FcmToken string `json:"fcm_token"` + MessagesPerMinute uint `json:"messages_per_minute"` + MaxSendAttempts uint `json:"max_send_attempts"` + MessageExpirationSeconds uint `json:"message_expiration_seconds"` + SIM string `json:"sim"` +} + +func (service *PhoneService) Upsert(ctx, params) → (*PhoneResponse, *Response, error) +// PUT /v1/phones — authenticated with user API key +``` + +#### `PhoneService` (FCM Token binding) + +```go +type PhoneFCMTokenParams struct { + PhoneNumber string `json:"phone_number"` + FcmToken string `json:"fcm_token"` + SIM string `json:"sim"` +} + +func (service *PhoneService) UpsertFCMToken(ctx, params) → (*PhoneResponse, *Response, error) +// PUT /v1/phones/fcm-token — authenticated with phone API key +// This binds the phone to the phone API key via the auth context +``` + +#### `PhoneAPIKeyService` + +```go +type PhoneAPIKeyStoreParams struct { + Name string `json:"name"` +} + +func (service *PhoneAPIKeyService) Store(ctx, params) → (*PhoneAPIKeyResponse, *Response, error) +// POST /v1/phone-api-keys/ — authenticated with user API key +// Returns the created phone API key including its api_key value +``` + +#### `WebhookService` + +```go +type WebhookStoreParams struct { + SigningKey string `json:"signing_key"` + URL string `json:"url"` + PhoneNumbers []string `json:"phone_numbers"` + Events []string `json:"events"` +} + +func (service *WebhookService) Store(ctx, params) → (*WebhookResponse, *Response, error) +// POST /v1/webhooks — authenticated with user API key +``` + +#### Phone Setup Flow (per test) + +The real Android phone registers via this flow, and tests must replicate it: + +1. `PUT /v1/phones` (user API key) — creates phone with phone_number + fcm_token + messages_per_minute +2. `POST /v1/phone-api-keys/` (user API key) — creates a phone API key, returns the `api_key` value +3. `PUT /v1/phones/fcm-token` (phone API key) — re-registers FCM token, which binds the phone to the API key via `PhoneAPIKeyListener.onPhoneUpdated` + +After step 3, the phone API key is authorized to act on behalf of that phone (fire events, receive messages, etc.). + +### 5. Test Cases + +#### `TestSendSMS_Encrypted` + +1. Generate random encryption key +2. Create phone + phone API key + webhook +3. Encrypt plaintext using `client.Cipher.Encrypt(key, "secret message")` +4. Send message with `Encrypted: true` and encrypted content +5. Query WireMock journal → verify FCM push arrived with `KEY_MESSAGE_ID` (FCM only carries the message ID, not content) +6. Call `GET /v1/messages/outstanding?message_id={id}` (phone API key) — verify response has `encrypted: true` and content is ciphertext (not plaintext) +7. Fire SENT + DELIVERED events +8. Fetch message via user API key → verify `encrypted: true`, content is ciphertext +9. Decrypt with `client.Cipher.Decrypt(key, content)` → assert equals original plaintext +10. Verify webhook event in WireMock with valid JWT + +#### `TestReceiveSMS_Encrypted` + +1. Generate random encryption key +2. Create phone + phone API key + webhook +3. Encrypt plaintext using `client.Cipher.Encrypt(key, "incoming secret")` +4. Simulate receiving an encrypted SMS (POST /v1/messages/receive with phone API key) +5. Fetch message via user API key → verify `encrypted: true` +6. Decrypt content → assert equals original plaintext +7. Verify webhook event (`message.phone.received`) in WireMock with valid JWT + +#### `TestSendSMS_RateLimit` + +1. Create phone with `messages_per_minute: 10` (= 6s gap) +2. Create phone API key + webhook +3. Send 2 messages simultaneously +4. Query WireMock journal for FCM pushes (correlate by message IDs from send responses) +5. Assert the timestamps of the two FCM pushes have ≥6 second gap +6. Fire SENT + DELIVERED for both messages +7. Verify both messages reach `delivered` status +8. Verify webhook events for both messages + +#### `TestSendSMS_OutstandingFlow` + +Validates the real phone flow (`/v1/messages/outstanding`): + +1. Create phone + phone API key + webhook +2. Send message via httpsms-go client +3. Query WireMock journal → extract `KEY_MESSAGE_ID` from FCM push +4. Call `GET /v1/messages/outstanding?message_id={id}` (phone API key) — assert returns the message with correct content, owner, contact +5. Fire SENT + DELIVERED events +6. Verify message reaches `delivered` status +7. Verify webhook events + +#### Webhook Verification (shared helper) + +For all tests, a helper function: + +```go +func assertWebhookEvent(t *testing.T, wiremockClient *wiremock.Client, signingKey string, expectedEventType string) { + // 1. Query WireMock journal for POST /webhooks/test requests + // 2. Find request with X-Event-Type header matching expectedEventType + // 3. Extract Authorization header → parse JWT + // 4. Validate signature with signingKey (HMAC-SHA256) + // 5. Assert claims: + // - Issuer == "api.httpsms.com" + // - Subject == "test-user-id" + // - Audience contains webhook URL + // - ExpiresAt is in the future + // - NotBefore is in the past +} +``` + +### 6. Test Helper Structure + +``` +tests/ +├── docker-compose.yml (updated: wiremock replaces emulator) +├── wiremock/ +│ └── mappings/ +│ ├── fcm-send.json +│ ├── oauth-token.json +│ └── webhook-receiver.json +├── seed.sql (simplified: user + system user only) +├── .env.test (updated: FCM_ENDPOINT → wiremock) +├── go.mod (add httpsms-go, go-wiremock, golang-jwt) +├── helpers_test.go (shared constants, setup helpers) +├── webhook_helpers_test.go (JWT verification helpers) +├── integration_test.go (all test cases) +└── README.md +``` + +### 7. Dependencies + +**Test module (`tests/go.mod`):** + +- `github.com/NdoleStudio/httpsms-go` — API client +- `github.com/wiremock/go-wiremock` — WireMock stub configuration + journal queries +- `github.com/golang-jwt/jwt/v5` — JWT parsing and validation +- `github.com/stretchr/testify` — assertions (already present) + +### 8. Parallel Test Execution & Request Correlation + +Each test creates its own phone with a unique random number (e.g. `+1800555XXXX` where XXXX is random). This ensures: + +- No message cross-contamination between tests +- Webhooks scoped to specific phone numbers don't fire for other tests + +**Correlation strategy for WireMock journal queries:** + +- **FCM pushes**: Correlate by message ID. The test gets the message ID from the send response, then searches WireMock journal for FCM push requests containing that `KEY_MESSAGE_ID` in the JSON body. +- **Webhook events**: Each test uses a **unique webhook URL path** (e.g. `/webhooks/{testUUID}`). This ensures journal queries for webhook assertions only match events for that specific test. Additionally, match on `X-Event-Type` header and message ID in payload body. +- **Unique FCM token per phone**: Each test generates a unique `fcm_token` string. Since WireMock captures the FCM push including the `token` field, this can be used as a secondary correlation key if needed. + +Tests use `t.Parallel()` where safe (encryption tests can run in parallel; rate-limit test may need serial execution due to timing assertions). + +## Migration Notes + +- The `tests/emulator/` directory is deleted entirely +- The CI workflow (`.github/workflows/integration-test.yml`) needs updating to remove emulator references +- Firebase credentials `token_uri` must point to `http://wiremock:8080/token` +- WireMock image is Java-based (~300MB) vs the old Alpine emulator (~15MB), but eliminates maintenance of custom code diff --git a/tests/.env.test b/tests/.env.test new file mode 100644 index 000000000..a95703f09 --- /dev/null +++ b/tests/.env.test @@ -0,0 +1,30 @@ +ENV=production +GCP_PROJECT_ID=httpsms-test +USE_HTTP_LOGGER=true +ENTITLEMENT_ENABLED=false +EVENTS_QUEUE_TYPE=emulator +EVENTS_QUEUE_NAME=events-local +EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events +EVENTS_QUEUE_USER_API_KEY=system-user-api-key +EVENTS_QUEUE_USER_ID=system-user-id +FCM_ENDPOINT=http://wiremock:8080 +DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms +DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms +REDIS_URL=redis://@redis:6379 +APP_PORT=8000 +APP_NAME=httpSMS +APP_URL=http://localhost:8000 +SWAGGER_HOST=localhost:8000 +SMTP_FROM_NAME=httpSMS +SMTP_FROM_EMAIL=test@httpsms.com +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_HOST=localhost +SMTP_PORT=2525 +PUSHER_APP_ID= +PUSHER_KEY= +PUSHER_SECRET= +PUSHER_CLUSTER= +GCS_BUCKET_NAME= +UPTRACE_DSN= +CLOUDFLARE_TURNSTILE_SECRET_KEY= diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..c914982d2 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,214 @@ +# Integration Tests + +End-to-end integration tests for the httpSMS API. These tests validate the complete SMS lifecycle by running the full application stack in Docker alongside a phone emulator service. + +## Architecture + +``` +┌──────────────┐ HTTP ┌──────────────┐ +│ Test Runner │─────────────▶│ API (Go) │ +│ (Go test) │ │ Port 8000 │ +└──────────────┘ └──────┬───────┘ + │ + FCM Push │ Events + (HTTP) │ (HTTP) + ▼ + ┌──────────────┐ + │ Emulator │ + │ (Fiber v3) │ + │ Port 9090 │ + └──────────────┘ + │ + ┌──────┴───────┐ + │ PostgreSQL │ │ Redis │ + │ Port 5435 │ │ Port 6379 │ + └──────────────┘ └─────────────┘ +``` + +### Components + +| Component | Description | +| --------------- | ------------------------------------------------------- | +| **API** | The httpSMS Go API server running in Docker | +| **Emulator** | A Fiber v3 Go service that simulates an Android phone | +| **PostgreSQL** | Database for the API | +| **Redis** | Cache and queue backend | +| **Seed** | One-shot container that seeds test data into PostgreSQL | +| **Test Runner** | Go test binary that runs on the host machine | + +### How It Works + +1. **Send SMS flow**: Test sends `POST /v1/messages/send` → API pushes FCM notification to emulator → Emulator calls `GET /v1/messages/outstanding` → Emulator fires `SENT` and `DELIVERED` events → Test polls `GET /v1/messages/{id}` until status is `delivered` + +2. **Receive SMS flow**: Test sends `POST /v1/messages/receive` (as the phone) → API stores message → Test verifies via `GET /v1/messages/{id}` + +### FCM Redirect + +The API's Firebase SDK is configured (via `FCM_ENDPOINT` env var) to redirect all FCM HTTP requests to the emulator instead of Google's servers. The emulator serves: + +- `/token` — Fake OAuth2 token endpoint (Firebase SDK requests tokens before sending) +- `/v1/projects/:project/messages:send` — Fake FCM push endpoint + +## Test Coverage + +- [x] **Send SMS E2E** — Full send lifecycle: API → FCM push → emulator responds with SENT/DELIVERED events → message reaches `delivered` status +- [x] **Receive SMS E2E** — Phone submits received message to API → message is stored and retrievable via GET endpoint + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) with Docker Compose +- [Go 1.22+](https://go.dev/dl/) +- [jq](https://jqlang.github.io/jq/download/) (for Firebase credentials generation) +- [OpenSSL](https://www.openssl.org/) (for RSA key generation) + +## Running Locally + +### 1. Generate Firebase Credentials + +The integration tests use a fake Firebase service account. Generate it with: + +```bash +cd tests +bash generate-firebase-credentials.sh +``` + +This creates `firebase-credentials.json` with a throwaway RSA key (the emulator doesn't validate tokens). + +### 2. Set Environment Variable + +```bash +export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) +``` + +### 3. Start the Stack + +```bash +docker compose up -d --build --wait +``` + +This starts PostgreSQL, Redis, the API, and the emulator. The `--wait` flag blocks until all health checks pass. + +### 4. Wait for Seeding + +```bash +docker compose wait seed +sleep 2 +``` + +The seed container inserts test users, phones, and API keys into PostgreSQL after the API has run its GORM migrations. + +### 5. Run Tests + +```bash +go test -v -timeout 120s ./... +``` + +### 6. Tear Down + +```bash +docker compose down -v +``` + +The `-v` flag removes volumes (database data) for a clean slate next run. + +### One-Liner + +```bash +cd tests && \ + bash generate-firebase-credentials.sh && \ + export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) && \ + docker compose up -d --build --wait && \ + docker compose wait seed && \ + sleep 2 && \ + go test -v -timeout 120s ./... ; \ + docker compose down -v +``` + +## CI/CD + +Integration tests run automatically via GitHub Actions (`.github/workflows/integration-test.yml`): + +- **Trigger**: Push to `main` or pull request targeting `main` +- **Flow**: Generates credentials → Starts Docker stack → Seeds DB → Runs tests → Collects logs on failure → Tears down +- **Gate**: Deployment should only proceed if integration tests pass + +## Test Data + +| Entity | Value | +| -------------- | -------------------------------------- | +| User API Key | `test-user-api-key` | +| Phone API Key | `pk_test-phone-api-key` | +| Phone Number | `+18005550199` | +| Contact Number | `+18005550100` | +| User ID | `test-user-id` | +| Phone ID | `a1b2c3d4-e5f6-7890-abcd-ef1234567890` | + +See [`seed.sql`](./seed.sql) for the complete seed data. + +## Project Structure + +``` +tests/ +├── docker-compose.yml # Full stack orchestration +├── seed.sql # Database seed data +├── .env.test # API environment variables +├── generate-firebase-credentials.sh # Generates fake Firebase credentials +├── go.mod # Test runner Go module +├── go.sum +├── helpers_test.go # Test utilities (HTTP client, polling) +├── integration_test.go # E2E test cases +└── emulator/ # Phone emulator service + ├── Dockerfile + ├── go.mod + ├── go.sum + ├── main.go # Fiber v3 entry point + ├── emulator.go # Emulator struct and config + ├── token_handler.go # Fake OAuth2 token endpoint + ├── fcm_handler.go # Fake FCM push receiver + └── events.go # Event firing logic (SENT/DELIVERED) +``` + +## Troubleshooting + +### API fails to start + +Check the API logs: + +```bash +docker compose logs api +``` + +Common issues: + +- `FIREBASE_CREDENTIALS` env var not set or malformed +- PostgreSQL not ready (increase `start_period` in healthcheck) + +### Tests timeout waiting for `delivered` status + +Check the emulator logs: + +```bash +docker compose logs emulator +``` + +The emulator should show: + +1. `[FCM]` — Receiving the push notification +2. `[EVENTS]` — Fetching outstanding messages and firing events + +If no `[FCM]` entries appear, the API isn't reaching the emulator (check `FCM_ENDPOINT` in `.env.test`). + +### Seed container fails + +```bash +docker compose logs seed +``` + +If you see "relation does not exist" errors, the API hasn't finished GORM migrations yet. Increase the API's `start_period` in `docker-compose.yml`. + +## Adding New Tests + +1. Add test functions to `integration_test.go` (or create new `*_test.go` files) +2. Use `doRequest()` helper for authenticated HTTP calls +3. Use `pollMessageStatus()` to wait for async state changes +4. Update the test coverage checklist in this README diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 000000000..a4d6b9dce --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,88 @@ +services: + postgres: + image: postgres:alpine + environment: + POSTGRES_DB: httpsms + POSTGRES_PASSWORD: dbpassword + POSTGRES_USER: dbusername + ports: + - "5435:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dbusername -d httpsms"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s + + redis: + image: redis:latest + command: redis-server + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + wiremock: + image: wiremock/wiremock:3x + ports: + - "8080:8080" + volumes: + - ./wiremock/mappings:/home/wiremock/mappings:ro + networks: + default: + aliases: + - wiremock.local + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: ../api + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + wiremock: + condition: service_healthy + env_file: + - .env.test + environment: + FIREBASE_CREDENTIALS: "${FIREBASE_CREDENTIALS}" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 5s + timeout: 10s + retries: 20 + start_period: 30s + + seed: + image: postgres:alpine + depends_on: + api: + condition: service_healthy + environment: + PGPASSWORD: dbpassword + volumes: + - ./seed.sql:/seed.sql:ro + entrypoint: + [ + "psql", + "-h", + "postgres", + "-U", + "dbusername", + "-d", + "httpsms", + "-f", + "/seed.sql", + ] + restart: "no" diff --git a/tests/generate-firebase-credentials.sh b/tests/generate-firebase-credentials.sh new file mode 100644 index 000000000..70f47cd8f --- /dev/null +++ b/tests/generate-firebase-credentials.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Generates a fake Firebase service account JSON for integration tests. +# The RSA key is throwaway — it only needs to be valid so the Firebase SDK can sign JWTs. +# WireMock does not validate these tokens. + +set -e + +OUTFILE="${1:-firebase-credentials.json}" + +# Generate a 2048-bit RSA key +PRIVATE_KEY=$(openssl genrsa 2048 2>/dev/null) + +# Escape newlines for JSON embedding +PRIVATE_KEY_ESCAPED=$(echo "$PRIVATE_KEY" | awk '{printf "%s\\n", $0}') + +cat > "$OUTFILE" <= expectedCount { + return requests + } + time.Sleep(500 * time.Millisecond) + } + + requests := findWebhookRequests(t, webhookPath) + require.GreaterOrEqual(t, len(requests), expectedCount, "expected at least %d webhook events on %s, got %d", expectedCount, webhookPath, len(requests)) + return requests +} + +func waitForFCMPush(t *testing.T, messageID string, timeout time.Duration) []wmJournal.GetRequestResponse { + t.Helper() + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + requests := findFCMRequests(t, messageID) + if len(requests) >= 1 { + return requests + } + time.Sleep(500 * time.Millisecond) + } + + t.Fatalf("FCM push for message %s not found within %v", messageID, timeout) + return nil +} diff --git a/tests/integration_test.go b/tests/integration_test.go new file mode 100644 index 000000000..dc8c933c3 --- /dev/null +++ b/tests/integration_test.go @@ -0,0 +1,263 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + "time" + + httpsms "github.com/NdoleStudio/httpsms-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSendSMS_Encrypted(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + encryptionKey := randomEncryptionKey() + signingKey, webhookPath := setupWebhook(ctx, t, phone.PhoneNumber, []string{ + "message.phone.sent", + "message.phone.delivered", + }) + + client := newAPIClient() + plaintext := "Hello encrypted world " + randomEncryptionKey() + ciphertext, err := client.Cipher.Encrypt(encryptionKey, plaintext) + require.NoError(t, err) + require.NotEqual(t, plaintext, ciphertext) + + contactNumber := randomPhoneNumber() + sendResp, resp, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contactNumber, + Content: ciphertext, + Encrypted: true, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode) + + messageID := sendResp.Data.ID.String() + require.NotEmpty(t, messageID) + t.Logf("sent encrypted message: %s", messageID) + + fcmRequests := waitForFCMPush(t, messageID, 30*time.Second) + require.Len(t, fcmRequests, 1) + + outstanding := fetchOutstandingMessage(ctx, t, phone.PhoneAPIKey, messageID) + assert.Equal(t, true, outstanding["encrypted"]) + assert.Equal(t, ciphertext, outstanding["content"]) + assert.NotEqual(t, plaintext, outstanding["content"]) + + fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "SENT") + time.Sleep(200 * time.Millisecond) + fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "DELIVERED") + + msg := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second) + assert.Equal(t, "delivered", msg.Status) + assert.True(t, msg.Encrypted) + assert.Equal(t, ciphertext, msg.Content) + + decrypted, err := client.Cipher.Decrypt(encryptionKey, msg.Content) + require.NoError(t, err) + assert.Equal(t, plaintext, decrypted) + + webhookReqs := waitForWebhookEvents(t, webhookPath, 2, 30*time.Second) + for _, req := range webhookReqs { + assertWebhookJWT(t, req.Request, signingKey) + } + + var eventTypes []string + for _, req := range webhookReqs { + if et, ok := req.Request.Headers["X-Event-Type"]; ok { + eventTypes = append(eventTypes, et) + } else if et, ok := req.Request.Headers["x-event-type"]; ok { + eventTypes = append(eventTypes, et) + } + } + assert.Contains(t, eventTypes, "message.phone.sent") + assert.Contains(t, eventTypes, "message.phone.delivered") +} + +func TestReceiveSMS_Encrypted(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + encryptionKey := randomEncryptionKey() + signingKey, webhookPath := setupWebhook(ctx, t, phone.PhoneNumber, []string{ + "message.phone.received", + }) + + client := newAPIClient() + plaintext := "Incoming secret message " + randomEncryptionKey() + ciphertext, err := client.Cipher.Encrypt(encryptionKey, plaintext) + require.NoError(t, err) + + contactNumber := randomPhoneNumber() + receivePayload := map[string]interface{}{ + "from": contactNumber, + "to": phone.PhoneNumber, + "content": ciphertext, + "encrypted": true, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + } + body, err := json.Marshal(receivePayload) + require.NoError(t, err) + + url := apiBaseURL + "/v1/messages/receive" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", phone.PhoneAPIKey) + + httpResp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer httpResp.Body.Close() + + respBody, err := io.ReadAll(httpResp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, httpResp.StatusCode, "receive response: %s", string(respBody)) + + var receiveResult httpsms.MessageResponse + require.NoError(t, json.Unmarshal(respBody, &receiveResult)) + messageID := receiveResult.Data.ID.String() + require.NotEmpty(t, messageID) + t.Logf("received encrypted message: %s", messageID) + + msg := pollMessageStatus(ctx, t, messageID, "received", 15*time.Second) + assert.Equal(t, "received", msg.Status) + assert.True(t, msg.Encrypted) + assert.Equal(t, ciphertext, msg.Content) + assert.NotEqual(t, plaintext, msg.Content) + + decrypted, err := client.Cipher.Decrypt(encryptionKey, msg.Content) + require.NoError(t, err) + assert.Equal(t, plaintext, decrypted) + + webhookReqs := waitForWebhookEvents(t, webhookPath, 1, 30*time.Second) + require.GreaterOrEqual(t, len(webhookReqs), 1) + assertWebhookJWT(t, webhookReqs[0].Request, signingKey) + + eventType := webhookReqs[0].Request.Headers["X-Event-Type"] + if eventType == "" { + eventType = webhookReqs[0].Request.Headers["x-event-type"] + } + assert.Equal(t, "message.phone.received", eventType) +} + +func TestSendSMS_RateLimit(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 10) + + signingKey, webhookPath := setupWebhook(ctx, t, phone.PhoneNumber, []string{ + "message.phone.sent", + "message.phone.delivered", + }) + + client := newAPIClient() + contactNumber := randomPhoneNumber() + + sendResp1, resp1, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contactNumber, + Content: "Rate limit test message 1", + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp1.HTTPResponse.StatusCode) + msgID1 := sendResp1.Data.ID.String() + + sendResp2, resp2, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contactNumber, + Content: "Rate limit test message 2", + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp2.HTTPResponse.StatusCode) + msgID2 := sendResp2.Data.ID.String() + + t.Logf("sent messages: %s, %s", msgID1, msgID2) + + fcm1 := waitForFCMPush(t, msgID1, 30*time.Second) + require.Len(t, fcm1, 1) + + fcm2 := waitForFCMPush(t, msgID2, 30*time.Second) + require.Len(t, fcm2, 1) + + time1 := fcm1[0].Request.LoggedDate + time2 := fcm2[0].Request.LoggedDate + gapMs := time2 - time1 + if gapMs < 0 { + gapMs = time1 - time2 + } + t.Logf("FCM push gap: %dms", gapMs) + assert.GreaterOrEqual(t, gapMs, int64(5500), "rate limit gap should be >= 5500ms (6s minus timing tolerance), got %dms", gapMs) + + fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "SENT") + fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "DELIVERED") + fireEvent(ctx, t, phone.PhoneAPIKey, msgID2, "SENT") + fireEvent(ctx, t, phone.PhoneAPIKey, msgID2, "DELIVERED") + + msg1 := pollMessageStatus(ctx, t, msgID1, "delivered", 15*time.Second) + msg2 := pollMessageStatus(ctx, t, msgID2, "delivered", 15*time.Second) + assert.Equal(t, "delivered", msg1.Status) + assert.Equal(t, "delivered", msg2.Status) + + webhookReqs := waitForWebhookEvents(t, webhookPath, 4, 30*time.Second) + for _, req := range webhookReqs { + assertWebhookJWT(t, req.Request, signingKey) + } +} + +func TestSendSMS_OutstandingFlow(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + signingKey, webhookPath := setupWebhook(ctx, t, phone.PhoneNumber, []string{ + "message.phone.sent", + "message.phone.delivered", + }) + + client := newAPIClient() + contactNumber := randomPhoneNumber() + content := "Outstanding flow test " + randomEncryptionKey() + + sendResp, resp, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contactNumber, + Content: content, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode) + + messageID := sendResp.Data.ID.String() + t.Logf("sent message: %s", messageID) + + fcmReqs := waitForFCMPush(t, messageID, 30*time.Second) + require.Len(t, fcmReqs, 1) + assert.Contains(t, fcmReqs[0].Request.Body, messageID) + assert.True(t, strings.Contains(fcmReqs[0].Request.URL, "/messages:send") || strings.Contains(fcmReqs[0].Request.AbsoluteURL, "/messages:send")) + + outstanding := fetchOutstandingMessage(ctx, t, phone.PhoneAPIKey, messageID) + assert.Equal(t, messageID, outstanding["id"]) + assert.Equal(t, content, outstanding["content"]) + assert.Equal(t, phone.PhoneNumber, outstanding["owner"]) + assert.Equal(t, contactNumber, outstanding["contact"]) + + fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "SENT") + time.Sleep(200 * time.Millisecond) + fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "DELIVERED") + + msg := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second) + assert.Equal(t, "delivered", msg.Status) + assert.Equal(t, content, msg.Content) + + webhookReqs := waitForWebhookEvents(t, webhookPath, 2, 30*time.Second) + for _, req := range webhookReqs { + assertWebhookJWT(t, req.Request, signingKey) + } +} diff --git a/tests/seed.sql b/tests/seed.sql new file mode 100644 index 000000000..4ae41006f --- /dev/null +++ b/tests/seed.sql @@ -0,0 +1,26 @@ +-- Seed test data for integration tests +-- Run AFTER GORM has migrated the schema (i.e., after API starts) + +-- Test user +INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) +VALUES ( + 'test-user-id', + 'test@httpsms.com', + 'test-user-api-key', + 'UTC', + 'pro-monthly', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + +-- System user (for event queue auth) +INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) +VALUES ( + 'system-user-id', + 'system@httpsms.com', + 'system-user-api-key', + 'UTC', + 'pro-monthly', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; diff --git a/tests/wiremock/mappings/fcm-send.json b/tests/wiremock/mappings/fcm-send.json new file mode 100644 index 000000000..7640fb7ca --- /dev/null +++ b/tests/wiremock/mappings/fcm-send.json @@ -0,0 +1,15 @@ +{ + "request": { + "urlPathPattern": "/v1/projects/.*/messages:send", + "method": "POST" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "name": "projects/httpsms-test/messages/fake-message-id" + } + } +} diff --git a/tests/wiremock/mappings/oauth-token.json b/tests/wiremock/mappings/oauth-token.json new file mode 100644 index 000000000..9518f4fe6 --- /dev/null +++ b/tests/wiremock/mappings/oauth-token.json @@ -0,0 +1,17 @@ +{ + "request": { + "urlPathPattern": "/token", + "method": "POST" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "access_token": "fake-access-token", + "token_type": "Bearer", + "expires_in": 3600 + } + } +} diff --git a/tests/wiremock/mappings/webhook-receiver.json b/tests/wiremock/mappings/webhook-receiver.json new file mode 100644 index 000000000..79966b64c --- /dev/null +++ b/tests/wiremock/mappings/webhook-receiver.json @@ -0,0 +1,15 @@ +{ + "request": { + "urlPathPattern": "/webhooks/.*", + "method": "POST" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "status": "received" + } + } +} From 5bcbdba0d5cae37f96bec43af564a0aeb2e85620 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:01:09 +0300 Subject: [PATCH 139/381] Add setp to deploy --- .github/workflows/api.yml | 10 ++++++---- .github/workflows/web.yml | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index aa7f20d7c..eeedd4ec0 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -13,7 +13,8 @@ permissions: id-token: write jobs: - Test: + test: + name: Integration Tests runs-on: ubuntu-latest steps: - name: Checkout 🛎 @@ -73,9 +74,10 @@ jobs: working-directory: ./tests run: docker compose down -v - Deploy: + deploy: + name: Deploy 🚀 runs-on: ubuntu-latest - needs: Test + needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Authenticate to Google Cloud @@ -87,7 +89,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v3 - - name: Trigger Cloud Build Deploy 🚀 + - name: Trigger Cloud Build Deploy run: | BUILD_ID=$(gcloud builds triggers run api-httpsms-com \ --region=global \ diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 1d9133d9c..fe0ae7c8f 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -11,6 +11,7 @@ defaults: jobs: ci: + name: Build & Deploy runs-on: ${{ matrix.os }} strategy: From 31c76be13e6e0defceca72768ce5a4a990a554d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 00:02:04 +0300 Subject: [PATCH 140/381] chore(deps): bump github.com/golang-jwt/jwt/v5 in /tests (#880) Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.2.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/go.mod | 2 +- tests/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/go.mod b/tests/go.mod index b3c9203ab..422d1e0bc 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -4,7 +4,7 @@ go 1.23 require ( github.com/NdoleStudio/httpsms-go v0.0.8 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 github.com/wiremock/go-wiremock v1.14.0 diff --git a/tests/go.sum b/tests/go.sum index b013a3195..2deb9da04 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -36,8 +36,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= From 879806cbaae668a4608b740dd9dff39691e9b187 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:16:55 +0300 Subject: [PATCH 141/381] ci: show Cloud Console link instead of streaming build logs Replace gcloud builds log --stream with a direct URL to the Cloud Console build page so the deploy step finishes quickly and provides a clickable link to view logs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/api.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index eeedd4ec0..17b479ca1 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -97,5 +97,4 @@ jobs: --sha=${{ github.sha }} \ --format="value(metadata.build.id)") echo "Build ID: $BUILD_ID" - echo "Streaming build logs..." - gcloud builds log "$BUILD_ID" --region=global --project=httpsms-86c51 --stream + echo "View build logs: https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=httpsms-86c51" From 8915db88a7a4bedc55613bc50339ea9b2675d73b Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:22:01 +0300 Subject: [PATCH 142/381] ci: poll Cloud Build status after showing console link Show the Cloud Console link immediately, then poll every 30s until the build reaches a terminal state. Fail the pipeline if the build does not succeed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/api.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 17b479ca1..281a9773b 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -98,3 +98,14 @@ jobs: --format="value(metadata.build.id)") echo "Build ID: $BUILD_ID" echo "View build logs: https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=httpsms-86c51" + echo "" + echo "Polling build status..." + while true; do + STATUS=$(gcloud builds describe "$BUILD_ID" --region=global --project=httpsms-86c51 --format="value(status)") + echo " Status: $STATUS" + case "$STATUS" in + SUCCESS) echo "Build succeeded!"; exit 0 ;; + FAILURE|TIMEOUT|CANCELLED|EXPIRED|INTERNAL_ERROR) echo "Build failed with status: $STATUS"; exit 1 ;; + esac + sleep 30 + done From a34db7020a4bcfddc8623b3cb28f68617ff96aa3 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:24:12 +0300 Subject: [PATCH 143/381] ci: add colored output for build URL and status Blue for the Cloud Console URL, green for success, red for failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/api.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 281a9773b..c4fe30906 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -96,16 +96,15 @@ jobs: --project=httpsms-86c51 \ --sha=${{ github.sha }} \ --format="value(metadata.build.id)") - echo "Build ID: $BUILD_ID" - echo "View build logs: https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=httpsms-86c51" + echo -e "Cloud Build: \033[34mhttps://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=httpsms-86c51\033[0m" echo "" - echo "Polling build status..." + echo "Polling Cloud Build Status..." while true; do STATUS=$(gcloud builds describe "$BUILD_ID" --region=global --project=httpsms-86c51 --format="value(status)") echo " Status: $STATUS" case "$STATUS" in - SUCCESS) echo "Build succeeded!"; exit 0 ;; - FAILURE|TIMEOUT|CANCELLED|EXPIRED|INTERNAL_ERROR) echo "Build failed with status: $STATUS"; exit 1 ;; + SUCCESS) echo -e "\033[32mBuild succeeded!\033[0m"; exit 0 ;; + FAILURE|TIMEOUT|CANCELLED|EXPIRED|INTERNAL_ERROR) echo -e "\033[31mBuild failed with status: $STATUS\033[0m"; exit 1 ;; esac sleep 30 done From 4ff75906a91e980c03dfb3108c3cd1397c38f6bd Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:25:58 +0300 Subject: [PATCH 144/381] Use higher version of auth --- .github/workflows/api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index c4fe30906..f9693a3ac 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -81,7 +81,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} From 8858766b5f2cf8053b0878ca46c170d88f126954 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:28:27 +0300 Subject: [PATCH 145/381] Remove emoji --- .github/workflows/api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index f9693a3ac..a440442cd 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -17,7 +17,7 @@ jobs: name: Integration Tests runs-on: ubuntu-latest steps: - - name: Checkout 🛎 + - name: Checkout uses: actions/checkout@v6 - name: Set up Go From 7c65c107c02b12cc399c3024b741e95ca5db3f77 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:53:29 +0300 Subject: [PATCH 146/381] Remove emoji on deploy --- .github/workflows/api.yml | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index a440442cd..910babc7c 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -75,7 +75,7 @@ jobs: run: docker compose down -v deploy: - name: Deploy 🚀 + name: Deploy runs-on: ubuntu-latest needs: test if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.gitignore b/.gitignore index edc89ca76..a457dc184 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ android/app/release/ tests/firebase-credentials.json tests/emulator/emulator.exe +SECURITY_AUDIT_REPORT.md From e463df4c1c2be9beaefd71041f4cd8e6e133b639 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 00:57:53 +0300 Subject: [PATCH 147/381] Fix status duisplay --- .github/workflows/api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 910babc7c..1c433c646 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -101,7 +101,7 @@ jobs: echo "Polling Cloud Build Status..." while true; do STATUS=$(gcloud builds describe "$BUILD_ID" --region=global --project=httpsms-86c51 --format="value(status)") - echo " Status: $STATUS" + echo -e " \033[90m$(date -u '+%Y-%m-%d %H:%M:%S UTC')\033[0m status=\"$STATUS\"" case "$STATUS" in SUCCESS) echo -e "\033[32mBuild succeeded!\033[0m"; exit 0 ;; FAILURE|TIMEOUT|CANCELLED|EXPIRED|INTERNAL_ERROR) echo -e "\033[31mBuild failed with status: $STATUS\033[0m"; exit 1 ;; From 48f23e4fcfcfee38d3a64f5ea8029855cfcbf77d Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 01:10:10 +0300 Subject: [PATCH 148/381] Reorder steps to clear cache --- tests/helpers_test.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/helpers_test.go b/tests/helpers_test.go index 41c20e35d..6b4a07822 100644 --- a/tests/helpers_test.go +++ b/tests/helpers_test.go @@ -78,7 +78,18 @@ func setupPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) testP fcmToken := "fcm-" + uuid.New().String() client := newAPIClient() - _, resp, err := client.Phones.Upsert(ctx, &httpsms.PhoneUpsertParams{ + // Create the phone API key first so that a few seconds pass (during phone upsert) + // before we use it, giving the cache time to clear. + apiKeyResp, resp, err := client.PhoneAPIKeys.Store(ctx, &httpsms.PhoneAPIKeyStoreParams{ + Name: "test-key-" + uuid.New().String(), + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode, "phone api key store failed") + + phoneAPIKeyValue := apiKeyResp.Data.APIKey + require.NotEmpty(t, phoneAPIKeyValue) + + _, resp, err = client.Phones.Upsert(ctx, &httpsms.PhoneUpsertParams{ PhoneNumber: phoneNumber, FcmToken: fcmToken, MessagesPerMinute: messagesPerMinute, @@ -89,15 +100,6 @@ func setupPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) testP require.NoError(t, err) require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode, "phone upsert failed") - apiKeyResp, resp, err := client.PhoneAPIKeys.Store(ctx, &httpsms.PhoneAPIKeyStoreParams{ - Name: "test-key-" + uuid.New().String(), - }) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode, "phone api key store failed") - - phoneAPIKeyValue := apiKeyResp.Data.APIKey - require.NotEmpty(t, phoneAPIKeyValue) - phoneClient := newPhoneClient(phoneAPIKeyValue) _, resp, err = phoneClient.Phones.UpsertFCMToken(ctx, &httpsms.PhoneFCMTokenParams{ PhoneNumber: phoneNumber, From aa8bb70af9869da36d65a6de48c39ab5fd9378c3 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 6 May 2026 01:22:34 +0300 Subject: [PATCH 149/381] Fix status --- .github/workflows/api.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 1c433c646..beb991823 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -101,7 +101,8 @@ jobs: echo "Polling Cloud Build Status..." while true; do STATUS=$(gcloud builds describe "$BUILD_ID" --region=global --project=httpsms-86c51 --format="value(status)") - echo -e " \033[90m$(date -u '+%Y-%m-%d %H:%M:%S UTC')\033[0m status=\"$STATUS\"" + LOCAL_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + echo -e " \033[90m${LOCAL_TIME}\033[0m status=\033[36m${STATUS}\033[0m" case "$STATUS" in SUCCESS) echo -e "\033[32mBuild succeeded!\033[0m"; exit 0 ;; FAILURE|TIMEOUT|CANCELLED|EXPIRED|INTERNAL_ERROR) echo -e "\033[31mBuild failed with status: $STATUS\033[0m"; exit 1 ;; From 888ddb391ac3ec329dae3bbe015a5e134d471c61 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 13 May 2026 23:20:04 +0300 Subject: [PATCH 150/381] fix: invalidate auth cache when rotating user API key (#883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: invalidate auth cache when rotating user API key (#881) RotateAPIKey now loads the old API key before updating and calls cache.Del(oldKey) after the DB transaction succeeds. This ensures the old key immediately stops authenticating instead of remaining valid for up to 2 hours (the ristretto cache TTL). Follows the surgical approach (Option B) matching how gorm_phone_api_key_repository already handles cache invalidation. Adds an integration test that rotates the key and verifies the old key returns 401 while the new key returns 200. Closes #881 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use dedicated test user for rotation test to avoid mutating shared credential The Greptile review correctly identified that the rotation test was mutating the shared userAPIKey, which would break all subsequent tests in the suite. This fix adds a dedicated 'rotate-test-user' in seed.sql and updates the test to use it instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: flush ristretto buffer before cache.Del to prevent async Set race Ristretto's Set operations are buffered asynchronously. If a prior request's SetWithTTL is still in the buffer when Del runs, Del finds nothing to remove, and the buffered Set then re-adds the entry — causing the old key to remain valid. Adding cache.Wait() before cache.Del() flushes all pending buffered operations first, ensuring the subsequent Del actually removes the cached entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make UserRistrettoCache a singleton so all callers share one cache The root cause of the test failure: UserRistrettoCache() created a new ristretto cache on every call. The auth middleware and the RotateAPIKey handler received separate cache instances, so cache.Del() in the handler had no effect on the middleware's cache — the old key kept authenticating. Fix: store the cache as a field on the Container struct and return it on subsequent calls (lazy singleton pattern), matching how db, app, and eventDispatcher are already handled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make PhoneRistrettoCache a singleton to prevent stale cache entries Same bug pattern as UserRistrettoCache — PhoneRistrettoCache() created a new ristretto cache on every call. PhoneRepository is used by PhoneService, PhoneAPIKeyService, and NotificationService, each getting a separate cache. Cache invalidations (Clear/Del) in one service had no effect on the others, leading to stale phone data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * revert: undo PhoneRistrettoCache singleton — it causes test interference The PhoneRistrettoCache stores phone metadata (not auth contexts). Making it a singleton caused cache.Clear() in one service to affect all other services, breaking TestReceiveSMS_Encrypted timing. Unlike the UserRistrettoCache (security-critical for auth), the phone cache only holds data with a 30-min TTL and doesn't have cross-service invalidation requirements. The non-singleton behavior is acceptable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 9 ++- api/pkg/repositories/gorm_user_repository.go | 12 +++ tests/integration_test.go | 85 ++++++++++++++++++++ tests/seed.sql | 12 +++ 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index ae5e15ec6..de180a0a0 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -86,6 +86,7 @@ type Container struct { eventDispatcher *services.EventDispatcher logger telemetry.Logger attachmentRepository repositories.AttachmentRepository + userRistrettoCache *ristretto.Cache[string, entities.AuthContext] } // NewLiteContainer creates a Container without any routes or listeners @@ -1730,8 +1731,11 @@ func (container *Container) PhoneRistrettoCache() (cache *ristretto.Cache[string } // UserRistrettoCache creates an in-memory *ristretto.Cache[string, entities.AuthContext] -func (container *Container) UserRistrettoCache() (cache *ristretto.Cache[string, entities.AuthContext]) { - container.logger.Debug(fmt.Sprintf("creating %T", cache)) +func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entities.AuthContext] { + if container.userRistrettoCache != nil { + return container.userRistrettoCache + } + container.logger.Debug(fmt.Sprintf("creating %T", container.userRistrettoCache)) ristrettoCache, err := ristretto.NewCache[string, entities.AuthContext](&ristretto.Config[string, entities.AuthContext]{ MaxCost: 5000, NumCounters: 5000 * 10, @@ -1740,6 +1744,7 @@ func (container *Container) UserRistrettoCache() (cache *ristretto.Cache[string, if err != nil { container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache")) } + container.userRistrettoCache = ristrettoCache return ristrettoCache } diff --git a/api/pkg/repositories/gorm_user_repository.go b/api/pkg/repositories/gorm_user_repository.go index e31b2848f..a64e8ae08 100644 --- a/api/pkg/repositories/gorm_user_repository.go +++ b/api/pkg/repositories/gorm_user_repository.go @@ -65,8 +65,13 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e } user := new(entities.User) + var oldAPIKey string err = crdbgorm.ExecuteTx(ctx, repository.db, nil, func(tx *gorm.DB) error { + if err := tx.WithContext(ctx).Where("id = ?", userID).First(user).Error; err != nil { + return err + } + oldAPIKey = user.APIKey return tx.WithContext(ctx).Model(user). Clauses(clause.Returning{}). Where("id = ?", userID). @@ -78,6 +83,13 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) } + if err == nil && oldAPIKey != "" { + // Flush pending ristretto Set operations before Del to avoid a + // buffered Set re-adding the entry after removal. + repository.cache.Wait() + repository.cache.Del(oldAPIKey) + } + return user, nil } diff --git a/tests/integration_test.go b/tests/integration_test.go index dc8c933c3..5aae58fff 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "strings" @@ -213,6 +214,90 @@ func TestSendSMS_RateLimit(t *testing.T) { } } +func TestRotateAPIKey_InvalidatesCache(t *testing.T) { + ctx := context.Background() + + // Use a dedicated test user so we don't mutate the shared userAPIKey + rotateUserAPIKey := "rotate-test-api-key" + rotateUserID := "rotate-test-user-id" + + // 1) Confirm the dedicated user's API key works and warm the cache + meURL := apiBaseURL + "/v1/users/me" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", rotateUserAPIKey) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "initial auth failed: %s", string(body)) + + // Parse the current API key from the response + var meResp struct { + Data struct { + ID string `json:"id"` + APIKey string `json:"api_key"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(body, &meResp)) + require.Equal(t, rotateUserID, meResp.Data.ID) + oldAPIKey := meResp.Data.APIKey + require.NotEmpty(t, oldAPIKey) + t.Logf("user ID: %s, old API key prefix: %s...", rotateUserID, oldAPIKey[:10]) + + // 2) Rotate the API key + rotateURL := fmt.Sprintf("%s/v1/users/%s/api-keys", apiBaseURL, rotateUserID) + req, err = http.NewRequestWithContext(ctx, http.MethodDelete, rotateURL, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", rotateUserAPIKey) + + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "rotate failed: %s", string(body)) + + // Parse new API key from rotate response + var rotateResp struct { + Data struct { + APIKey string `json:"api_key"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(body, &rotateResp)) + newAPIKey := rotateResp.Data.APIKey + require.NotEmpty(t, newAPIKey) + require.NotEqual(t, oldAPIKey, newAPIKey, "API key should have changed after rotation") + t.Logf("new API key prefix: %s...", newAPIKey[:10]) + + // 3) Old API key should immediately fail (401) — this is the bug regression check + req, err = http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", oldAPIKey) + + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "old API key should return 401 after rotation") + + // 4) New API key should work + req, err = http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", newAPIKey) + + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, "new API key should work: %s", string(body)) +} + func TestSendSMS_OutstandingFlow(t *testing.T) { ctx := context.Background() phone := setupPhone(ctx, t, 60) diff --git a/tests/seed.sql b/tests/seed.sql index 4ae41006f..36d714d99 100644 --- a/tests/seed.sql +++ b/tests/seed.sql @@ -13,6 +13,18 @@ VALUES ( NOW() ) ON CONFLICT (id) DO NOTHING; +-- Test user for API key rotation tests (isolated to avoid mutating the shared test user) +INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) +VALUES ( + 'rotate-test-user-id', + 'rotate-test@httpsms.com', + 'rotate-test-api-key', + 'UTC', + 'pro-monthly', + NOW(), + NOW() +) ON CONFLICT (id) DO NOTHING; + -- System user (for event queue auth) INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) VALUES ( From f73a0e4a6eda5afc34607db2d23686ffe3eb2c8b Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 13 May 2026 23:41:00 +0300 Subject: [PATCH 151/381] fix: use singleton pattern for PhoneRistrettoCache and InMemoryCache (#884) * fix: use singleton pattern for PhoneRistrettoCache and InMemoryCache Apply the same singleton pattern already used by UserRistrettoCache to both PhoneRistrettoCache and InMemoryCache in the DI container. Previously these methods created a new cache instance on every call, meaning each consumer got an isolated cache that could not be shared. Changes: - Add phoneRistrettoCache and inMemoryCache fields to Container struct - Return cached instance on subsequent calls instead of creating new ones - Fix error message in PhoneRistrettoCache (was 'user ristretto cache') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update api/pkg/di/container.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- api/pkg/di/container.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index de180a0a0..39fba5fc4 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -87,6 +87,8 @@ type Container struct { logger telemetry.Logger attachmentRepository repositories.AttachmentRepository userRistrettoCache *ristretto.Cache[string, entities.AuthContext] + phoneRistrettoCache *ristretto.Cache[string, *entities.Phone] + inMemoryCache cache.Cache } // NewLiteContainer creates a Container without any routes or listeners @@ -411,11 +413,15 @@ func (container *Container) FirebaseApp() (app *firebase.App) { return app } -// InMemoryCache creates a new instance of the in memory cache.Cache +// InMemoryCache returns the shared in-memory cache.Cache, creating it on the first call. func (container *Container) InMemoryCache() cache.Cache { + if container.inMemoryCache != nil { + return container.inMemoryCache + } container.logger.Debug("creating an in memory cache") c := ttlCache.New(time.Hour, time.Hour*2) - return cache.NewMemoryCache(container.Tracer(), c) + container.inMemoryCache = cache.NewMemoryCache(container.Tracer(), c) + return container.inMemoryCache } // Cache creates a new instance of cache.Cache @@ -1717,17 +1723,21 @@ func (container *Container) UserRepository() repositories.UserRepository { } // PhoneRistrettoCache creates an in-memory *ristretto.Cache[string, *entities.Phone] -func (container *Container) PhoneRistrettoCache() (cache *ristretto.Cache[string, *entities.Phone]) { - container.logger.Debug(fmt.Sprintf("creating %T", cache)) +func (container *Container) PhoneRistrettoCache() *ristretto.Cache[string, *entities.Phone] { + if container.phoneRistrettoCache != nil { + return container.phoneRistrettoCache + } + container.logger.Debug(fmt.Sprintf("creating %T", container.phoneRistrettoCache)) ristrettoCache, err := ristretto.NewCache[string, *entities.Phone](&ristretto.Config[string, *entities.Phone]{ MaxCost: 5000, NumCounters: 5000 * 10, BufferItems: 64, }) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache")) + container.logger.Fatal(stacktrace.Propagate(err, "cannot create phone ristretto cache")) } - return ristrettoCache + container.phoneRistrettoCache = ristrettoCache + return container.phoneRistrettoCache } // UserRistrettoCache creates an in-memory *ristretto.Cache[string, entities.AuthContext] From ec59191f4c6e5c280f3582336df82ee62e62f877 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Thu, 14 May 2026 10:09:00 +0300 Subject: [PATCH 152/381] Add codeowners for workflows changes --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..6893dc38e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Require review/approval for any changes to GitHub Actions workflows +/.github/workflows/ @AchoArnold From 45bb281d888474f26c648df079ba6b9d128f1891 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 15 May 2026 22:40:13 +0300 Subject: [PATCH 153/381] feat(api): add response compression middleware Enable Fiber compress middleware with LevelBestCompression to minimize JSON response payload sizes. Supports gzip, deflate, and brotli based on client Accept-Encoding header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 39fba5fc4..2ecfa04d7 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -56,6 +56,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/middlewares" "google.golang.org/api/option" + "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/NdoleStudio/httpsms/pkg/entities" @@ -183,6 +184,10 @@ func (container *Container) App() (app *fiber.App) { return c.SendStatus(fiber.StatusOK) }) + app.Use(compress.New(compress.Config{ + Level: compress.LevelBestCompression, + })) + if os.Getenv("USE_HTTP_LOGGER") == "true" { app.Use(fiberLogger.New()) } From 17da946349dbe461c9406b47a937887f8d677a16 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sat, 16 May 2026 11:59:47 +0300 Subject: [PATCH 154/381] feat(api): add Turso/libSQL backend for heartbeat repositories (#886) * docs: add design spec for Turso/libSQL heartbeat backend Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(api): add Turso/libSQL backend for heartbeat repositories Add alternative HeartbeatRepository and HeartbeatMonitorRepository implementations using libSQL (Turso) via database/sql. Switchable via HEARTBEAT_DB_BACKEND=turso env var. Requires TURSO_DATABASE_URL and TURSO_AUTH_TOKEN when enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(api): add context timeout to DeleteAllForUser in libSQL repos Also update design spec to reference correct package (libsql-client-go, not go-libsql). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add design spec for hedging repository pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(api): add hedging repositories for heartbeat dual-write Add composite repositories that write to GORM (primary) and Turso (secondary) with fail-open semantics. Secondary failures are logged and counted via OTel metric. Activated via HEARTBEAT_DB_BACKEND=hedging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(api): add hedging heartbeat integration test with Turso Add sqld (libSQL server) to test docker-compose. Integration test stores a heartbeat via the hedging repository and reads it back from both PostgreSQL (primary) and Turso/libSQL (secondary) to verify dual-write. Gated by TEST_DATABASE_URL and TEST_TURSO_DATABASE_URL environment variables. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: run hedging integration test in GitHub Actions Wait for sqld health before running tests. Set TEST_DATABASE_URL and TEST_TURSO_DATABASE_URL env vars pointing to docker compose services. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add black-box heartbeat integration test with hedging Store a heartbeat via POST /v1/heartbeats and read it back via the Index endpoint. The API is configured with HEARTBEAT_DB_BACKEND=hedging so it dual-writes to both PostgreSQL and Turso/sqld. The test only interacts with the HTTP API, no implementation details exposed. - Add sqld dependency to API service in docker-compose - Add HEARTBEAT_DB_BACKEND, TURSO_DATABASE_URL to .env.test - Remove repo-level integration test in favor of black-box test - Keep sqld health wait in CI workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): remove curl-based healthcheck from sqld container The ghcr.io/tursodatabase/libsql-server:latest image is based on debian:bullseye-slim and does not include curl. The health check was always failing, causing the container to be reported as unhealthy and blocking the api service from starting. Instead, use service_started condition since sqld starts nearly instantly and the workflow already has an explicit health polling step that checks sqld readiness from the host before running tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(api): combine Turso URL and auth token into single DSN env var Replace TURSO_DATABASE_URL and TURSO_AUTH_TOKEN with a single TURSO_DATABASE_DSN that contains the full connection string including the authToken query parameter. This simplifies configuration and aligns with standard DSN conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): retry heartbeat store to wait for async phone-API-key association The phone API key gets its phone numbers associated asynchronously via the PhoneUpdated event. In the emulator queue mode used by CI, this event is processed in a background goroutine. The heartbeat test was calling the store endpoint immediately, before the async event had associated the phone number with the API key, resulting in a 401. Add a retry loop (up to 15s) consistent with other integration tests that use polling patterns (waitForFCMPush, waitForWebhookEvents). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(api): handle rows.Err() and uuid.Parse errors in libsql repositories - Add rows.Err() check after iteration loop in Index to catch network errors or timeouts that silently end iteration - Propagate uuid.Parse errors in scanHeartbeat and scanHeartbeatRow instead of discarding them with _ - Propagate uuid.Parse errors in scanHeartbeatMonitorRow for both monitor ID and phone ID fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(api): make scan functions methods on libsql repository structs Convert package-level functions scanHeartbeat, scanHeartbeatRow, and scanHeartbeatMonitorRow into methods on their respective repository structs for consistency with the repository pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove uneeded log --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/api.yml | 15 + api/go.mod | 4 + api/go.sum | 8 + api/pkg/di/container.go | 114 ++++++-- .../hedging_heartbeat_monitor_repository.go | 128 +++++++++ .../hedging_heartbeat_repository.go | 79 ++++++ api/pkg/repositories/libsql.go | 67 +++++ .../libsql_heartbeat_monitor_repository.go | 199 +++++++++++++ .../libsql_heartbeat_repository.go | 186 ++++++++++++ .../plans/2026-05-15-hedging-repository.md | 265 ++++++++++++++++++ .../2026-05-15-hedging-repository-design.md | 164 +++++++++++ ...26-05-15-turso-heartbeat-backend-design.md | 162 +++++++++++ tests/.env.test | 2 + tests/docker-compose.yml | 7 + tests/integration_test.go | 56 ++++ 15 files changed, 1434 insertions(+), 22 deletions(-) create mode 100644 api/pkg/repositories/hedging_heartbeat_monitor_repository.go create mode 100644 api/pkg/repositories/hedging_heartbeat_repository.go create mode 100644 api/pkg/repositories/libsql.go create mode 100644 api/pkg/repositories/libsql_heartbeat_monitor_repository.go create mode 100644 api/pkg/repositories/libsql_heartbeat_repository.go create mode 100644 docs/superpowers/plans/2026-05-15-hedging-repository.md create mode 100644 docs/superpowers/specs/2026-05-15-hedging-repository-design.md create mode 100644 docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index beb991823..e8044d5cb 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -37,6 +37,21 @@ jobs: - name: Wait for services to be healthy working-directory: ./tests run: | + echo "Waiting for sqld to be healthy..." + for i in $(seq 1 20); do + if curl -sf http://localhost:8090/health >/dev/null 2>&1; then + echo "sqld is healthy!" + break + fi + if [ $i -eq 20 ]; then + echo "sqld failed to become healthy" + docker compose logs sqld + exit 1 + fi + echo "sqld attempt $i/20 - waiting 3s..." + sleep 3 + done + echo "Waiting for API to be healthy..." for i in $(seq 1 40); do if docker compose exec api curl -sf http://localhost:8000/health >/dev/null 2>&1; then diff --git a/api/go.mod b/api/go.mod index 0c6ccd0b5..d77c0bf69 100644 --- a/api/go.mod +++ b/api/go.mod @@ -44,6 +44,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 + github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885 github.com/uptrace/uptrace-go v1.43.0 github.com/xuri/excelize/v2 v2.10.1 go.opentelemetry.io/otel v1.43.0 @@ -93,11 +94,13 @@ require ( github.com/PuerkitoBio/goquery v1.12.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/fatih/color v1.19.0 // indirect @@ -183,6 +186,7 @@ require ( go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect diff --git a/api/go.sum b/api/go.sum index 5af5496ff..0710b094a 100644 --- a/api/go.sum +++ b/api/go.sum @@ -66,6 +66,8 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -88,6 +90,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -320,6 +324,8 @@ github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOj github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885 h1:YssVXwM/9nUAjGNmUWdgvb05JVcsaBrDn5yr+MaJTn0= +github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLlyQO7U= github.com/uptrace/uptrace-go v1.43.0/go.mod h1:ehDTIdtBSolg4Z0CCvg1C8yR6VX1YFDqBcg2KmsXWn0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -410,6 +416,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 2ecfa04d7..ce6a82e2f 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -3,6 +3,7 @@ package di import ( "context" "crypto/tls" + "database/sql" "fmt" "net/http" "os" @@ -82,6 +83,7 @@ type Container struct { projectID string db *gorm.DB dedicatedDB *gorm.DB + tursoDB *sql.DB version string app *fiber.App eventDispatcher *services.EventDispatcher @@ -269,19 +271,16 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { container.logger.Fatal(err) } - sqlDB, err := db.DB() - if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot get sql.DB from GORM")) - } - - sqlDB.SetMaxOpenConns(1) - sqlDB.SetMaxIdleConns(0) - sqlDB.SetConnMaxLifetime(10 * time.Second) - if err = db.Use(tracing.NewPlugin()); err != nil { container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) } + container.dedicatedDB = db + if os.Getenv("DATABASE_MIGRATION_SKIP") != "" { + container.logger.Debug(fmt.Sprintf("skipping migrations for [%T]", db)) + return container.dedicatedDB + } + container.logger.Debug(fmt.Sprintf("Running migrations for dedicated [%T]", db)) if err = db.AutoMigrate(&entities.Heartbeat{}); err != nil { container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Heartbeat{}))) @@ -291,10 +290,43 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.HeartbeatMonitor{}))) } - container.dedicatedDB = db return container.dedicatedDB } +// TursoDB creates a *sql.DB connection to a Turso/libSQL database +func (container *Container) TursoDB() *sql.DB { + if container.tursoDB != nil { + return container.tursoDB + } + + container.logger.Debug("creating Turso *sql.DB connection") + + db, err := repositories.NewTursoDB(os.Getenv("TURSO_DATABASE_DSN")) + if err != nil { + container.logger.Fatal(err) + } + + container.tursoDB = db + return container.tursoDB +} + +// HedgingFailureCounter creates an OTel counter for hedging secondary write failures +func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { + meter := otel.GetMeterProvider().Meter( + container.projectID, + otelMetric.WithInstrumentationVersion(otel.Version()), + ) + counter, err := meter.Int64Counter( + "hedging.secondary.write.failures", + otelMetric.WithUnit("1"), + otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), + ) + if err != nil { + container.logger.Fatal(stacktrace.Propagate(err, "cannot create hedging failure counter")) + } + return counter +} + // DBWithoutMigration creates an instance of gorm.DB if it has not been created already func (container *Container) DBWithoutMigration() (db *gorm.DB) { if container.db != nil { @@ -889,12 +921,31 @@ func (container *Container) MessageThreadRepository() (repository repositories.M // HeartbeatMonitorRepository creates a new instance of repositories.HeartbeatMonitorRepository func (container *Container) HeartbeatMonitorRepository() (repository repositories.HeartbeatMonitorRepository) { - container.logger.Debug("creating GORM repositories.HeartbeatMonitorRepository") - return repositories.NewGormHeartbeatMonitorRepository( - container.Logger(), - container.Tracer(), - container.DedicatedDB(), - ) + switch os.Getenv("HEARTBEAT_DB_BACKEND") { + case "turso": + container.logger.Debug("creating libSQL repositories.HeartbeatMonitorRepository") + return repositories.NewLibsqlHeartbeatMonitorRepository( + container.Logger(), + container.Tracer(), + container.TursoDB(), + ) + case "hedging": + container.logger.Debug("creating hedging repositories.HeartbeatMonitorRepository") + return repositories.NewHedgingHeartbeatMonitorRepository( + container.Logger(), + container.Tracer(), + repositories.NewGormHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), + repositories.NewLibsqlHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.TursoDB()), + container.HedgingFailureCounter(), + ) + default: + container.logger.Debug("creating GORM repositories.HeartbeatMonitorRepository") + return repositories.NewGormHeartbeatMonitorRepository( + container.Logger(), + container.Tracer(), + container.DedicatedDB(), + ) + } } // HeartbeatService creates a new instance of services.HeartbeatService @@ -1708,12 +1759,31 @@ func (container *Container) RegisterSwaggerRoutes() { // HeartbeatRepository registers a new instance of repositories.HeartbeatRepository func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { - container.logger.Debug("creating GORM repositories.HeartbeatRepository") - return repositories.NewGormHeartbeatRepository( - container.Logger(), - container.Tracer(), - container.DedicatedDB(), - ) + switch os.Getenv("HEARTBEAT_DB_BACKEND") { + case "turso": + container.logger.Debug("creating libSQL repositories.HeartbeatRepository") + return repositories.NewLibsqlHeartbeatRepository( + container.Logger(), + container.Tracer(), + container.TursoDB(), + ) + case "hedging": + container.logger.Debug("creating hedging repositories.HeartbeatRepository") + return repositories.NewHedgingHeartbeatRepository( + container.Logger(), + container.Tracer(), + repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), + repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), + container.HedgingFailureCounter(), + ) + default: + container.logger.Debug("creating GORM repositories.HeartbeatRepository") + return repositories.NewGormHeartbeatRepository( + container.Logger(), + container.Tracer(), + container.DedicatedDB(), + ) + } } // UserRepository registers a new instance of repositories.UserRepository diff --git a/api/pkg/repositories/hedging_heartbeat_monitor_repository.go b/api/pkg/repositories/hedging_heartbeat_monitor_repository.go new file mode 100644 index 000000000..3304230c6 --- /dev/null +++ b/api/pkg/repositories/hedging_heartbeat_monitor_repository.go @@ -0,0 +1,128 @@ +package repositories + +import ( + "context" + "fmt" + + "github.com/google/uuid" + otelMetric "go.opentelemetry.io/otel/metric" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// hedgingHeartbeatMonitorRepository writes to both primary and secondary repositories. +// Reads only hit primary. Secondary writes are fail-open. +type hedgingHeartbeatMonitorRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + primary HeartbeatMonitorRepository + secondary HeartbeatMonitorRepository + failureCounter otelMetric.Int64Counter +} + +// NewHedgingHeartbeatMonitorRepository creates a hedging HeartbeatMonitorRepository +func NewHedgingHeartbeatMonitorRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + primary HeartbeatMonitorRepository, + secondary HeartbeatMonitorRepository, + failureCounter otelMetric.Int64Counter, +) HeartbeatMonitorRepository { + return &hedgingHeartbeatMonitorRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatMonitorRepository{})), + tracer: tracer, + primary: primary, + secondary: secondary, + failureCounter: failureCounter, + } +} + +func (repository *hedgingHeartbeatMonitorRepository) Store(ctx context.Context, monitor *entities.HeartbeatMonitor) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.Store(ctx, monitor); err != nil { + return err + } + + if err := repository.secondary.Store(ctx, monitor); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for monitor [%s]", monitor.ID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} + +func (repository *hedgingHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error) { + return repository.primary.Load(ctx, userID, phoneNumber) +} + +func (repository *hedgingHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, monitorID uuid.UUID) (bool, error) { + return repository.primary.Exists(ctx, userID, monitorID) +} + +func (repository *hedgingHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.UpdateQueueID(ctx, monitorID, queueID); err != nil { + return err + } + + if err := repository.secondary.UpdateQueueID(ctx, monitorID, queueID); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary UpdateQueueID failed for monitor [%s]", monitorID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} + +func (repository *hedgingHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.Delete(ctx, userID, phoneNumber); err != nil { + return err + } + + if err := repository.secondary.Delete(ctx, userID, phoneNumber); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete failed for monitor with owner [%s]", phoneNumber))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} + +func (repository *hedgingHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.UpdatePhoneOnline(ctx, userID, monitorID, online); err != nil { + return err + } + + if err := repository.secondary.UpdatePhoneOnline(ctx, userID, monitorID, online); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary UpdatePhoneOnline failed for monitor [%s]", monitorID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} + +func (repository *hedgingHeartbeatMonitorRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.DeleteAllForUser(ctx, userID); err != nil { + return err + } + + if err := repository.secondary.DeleteAllForUser(ctx, userID); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete all failed for user [%s]", userID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} diff --git a/api/pkg/repositories/hedging_heartbeat_repository.go b/api/pkg/repositories/hedging_heartbeat_repository.go new file mode 100644 index 000000000..073462644 --- /dev/null +++ b/api/pkg/repositories/hedging_heartbeat_repository.go @@ -0,0 +1,79 @@ +package repositories + +import ( + "context" + "fmt" + + otelMetric "go.opentelemetry.io/otel/metric" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// hedgingHeartbeatRepository writes to both primary and secondary repositories. +// Reads only hit primary. Secondary writes are fail-open. +type hedgingHeartbeatRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + primary HeartbeatRepository + secondary HeartbeatRepository + failureCounter otelMetric.Int64Counter +} + +// NewHedgingHeartbeatRepository creates a hedging HeartbeatRepository +func NewHedgingHeartbeatRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + primary HeartbeatRepository, + secondary HeartbeatRepository, + failureCounter otelMetric.Int64Counter, +) HeartbeatRepository { + return &hedgingHeartbeatRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatRepository{})), + tracer: tracer, + primary: primary, + secondary: secondary, + failureCounter: failureCounter, + } +} + +func (repository *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.Store(ctx, heartbeat); err != nil { + return err + } + + if err := repository.secondary.Store(ctx, heartbeat); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for heartbeat [%s]", heartbeat.ID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} + +func (repository *hedgingHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { + return repository.primary.Index(ctx, userID, owner, params) +} + +func (repository *hedgingHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) { + return repository.primary.Last(ctx, userID, owner) +} + +func (repository *hedgingHeartbeatRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + if err := repository.primary.DeleteAllForUser(ctx, userID); err != nil { + return err + } + + if err := repository.secondary.DeleteAllForUser(ctx, userID); err != nil { + repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete failed for user [%s]", userID))) + repository.failureCounter.Add(ctx, 1) + } + + return nil +} diff --git a/api/pkg/repositories/libsql.go b/api/pkg/repositories/libsql.go new file mode 100644 index 000000000..66ee8c0eb --- /dev/null +++ b/api/pkg/repositories/libsql.go @@ -0,0 +1,67 @@ +package repositories + +import ( + "database/sql" + "fmt" + + _ "github.com/tursodatabase/libsql-client-go/libsql" // libSQL database driver + + "github.com/palantir/stacktrace" +) + +const ( + tableHeartbeats = "heartbeats" + tableHeartbeatMonitors = "heartbeat_monitors" +) + +// NewTursoDB creates a new *sql.DB connection to a Turso database and auto-creates tables +func NewTursoDB(dsn string) (*sql.DB, error) { + db, err := sql.Open("libsql", dsn) + if err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot open turso database with DSN [%s]", dsn)) + } + + if err = db.Ping(); err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot ping turso database with DSN [%s]", dsn)) + } + + if err = createTursoTables(db); err != nil { + return nil, stacktrace.Propagate(err, "cannot create turso tables") + } + + return db, nil +} + +func createTursoTables(db *sql.DB) error { + statements := []string{ + `CREATE TABLE IF NOT EXISTS ` + tableHeartbeats + ` ( + id TEXT PRIMARY KEY, + owner TEXT NOT NULL, + version TEXT NOT NULL, + charging INTEGER NOT NULL DEFAULT 0, + user_id TEXT NOT NULL, + timestamp DATETIME NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_heartbeats_owner_timestamp ON ` + tableHeartbeats + `(owner, timestamp)`, + `CREATE INDEX IF NOT EXISTS idx_heartbeats_user_id ON ` + tableHeartbeats + `(user_id)`, + `CREATE TABLE IF NOT EXISTS ` + tableHeartbeatMonitors + ` ( + id TEXT PRIMARY KEY, + phone_id TEXT NOT NULL, + user_id TEXT NOT NULL, + queue_id TEXT NOT NULL DEFAULT '', + owner TEXT NOT NULL, + phone_online INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_heartbeat_monitors_user_owner ON ` + tableHeartbeatMonitors + `(user_id, owner)`, + } + + for _, stmt := range statements { + if _, err := db.Exec(stmt); err != nil { + return stacktrace.Propagate(err, fmt.Sprintf("cannot execute statement: %s", stmt)) + } + } + + return nil +} diff --git a/api/pkg/repositories/libsql_heartbeat_monitor_repository.go b/api/pkg/repositories/libsql_heartbeat_monitor_repository.go new file mode 100644 index 000000000..0cb594af1 --- /dev/null +++ b/api/pkg/repositories/libsql_heartbeat_monitor_repository.go @@ -0,0 +1,199 @@ +package repositories + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// libsqlHeartbeatMonitorRepository is responsible for persisting entities.HeartbeatMonitor in Turso/libSQL +type libsqlHeartbeatMonitorRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + db *sql.DB +} + +// NewLibsqlHeartbeatMonitorRepository creates the libSQL version of the HeartbeatMonitorRepository +func NewLibsqlHeartbeatMonitorRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + db *sql.DB, +) HeartbeatMonitorRepository { + return &libsqlHeartbeatMonitorRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &libsqlHeartbeatMonitorRepository{})), + tracer: tracer, + db: db, + } +} + +func (repository *libsqlHeartbeatMonitorRepository) Store(ctx context.Context, monitor *entities.HeartbeatMonitor) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, + "INSERT INTO "+tableHeartbeatMonitors+" (id, phone_id, user_id, queue_id, owner, phone_online, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + monitor.ID.String(), + monitor.PhoneID.String(), + string(monitor.UserID), + monitor.QueueID, + monitor.Owner, + boolToInt(monitor.PhoneOnline), + monitor.CreatedAt.UTC(), + monitor.UpdatedAt.UTC(), + ) + if err != nil { + msg := fmt.Sprintf("cannot save heartbeat monitor with ID [%s]", monitor.ID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + row := repository.db.QueryRowContext(ctx, + "SELECT id, phone_id, user_id, queue_id, owner, phone_online, created_at, updated_at FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND owner = ? LIMIT 1", + string(userID), phoneNumber, + ) + + monitor, err := repository.scanHeartbeatMonitorRow(row) + if err == sql.ErrNoRows { + msg := fmt.Sprintf("heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) + } + if err != nil { + msg := fmt.Sprintf("cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return monitor, nil +} + +func (repository *libsqlHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, monitorID uuid.UUID) (bool, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + var count int + err := repository.db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND id = ?", + string(userID), monitorID.String(), + ).Scan(&count) + if err != nil { + msg := fmt.Sprintf("cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID) + return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return count > 0, nil +} + +func (repository *libsqlHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, + "UPDATE "+tableHeartbeatMonitors+" SET queue_id = ?, updated_at = ? WHERE id = ?", + queueID, time.Now().UTC(), monitorID.String(), + ) + if err != nil { + msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s]", monitorID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, + "DELETE FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND owner = ?", + string(userID), phoneNumber, + ) + if err != nil { + msg := fmt.Sprintf("cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, + "UPDATE "+tableHeartbeatMonitors+" SET phone_online = ?, updated_at = ? WHERE id = ? AND user_id = ?", + boolToInt(online), time.Now().UTC(), monitorID.String(), string(userID), + ) + if err != nil { + msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatMonitorRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, "DELETE FROM "+tableHeartbeatMonitors+" WHERE user_id = ?", string(userID)) + if err != nil { + msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatMonitorRepository) scanHeartbeatMonitorRow(row *sql.Row) (*entities.HeartbeatMonitor, error) { + monitor := new(entities.HeartbeatMonitor) + var id, phoneID, userID string + var phoneOnline int + err := row.Scan(&id, &phoneID, &userID, &monitor.QueueID, &monitor.Owner, &phoneOnline, &monitor.CreatedAt, &monitor.UpdatedAt) + if err != nil { + return nil, err + } + monitor.ID, err = uuid.Parse(id) + if err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat monitor ID [%s]", id)) + } + monitor.PhoneID, err = uuid.Parse(phoneID) + if err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat monitor phone ID [%s]", phoneID)) + } + monitor.UserID = entities.UserID(userID) + monitor.PhoneOnline = phoneOnline != 0 + return monitor, nil +} diff --git a/api/pkg/repositories/libsql_heartbeat_repository.go b/api/pkg/repositories/libsql_heartbeat_repository.go new file mode 100644 index 000000000..42fdf911c --- /dev/null +++ b/api/pkg/repositories/libsql_heartbeat_repository.go @@ -0,0 +1,186 @@ +package repositories + +import ( + "context" + "database/sql" + "fmt" + + "github.com/google/uuid" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// libsqlHeartbeatRepository is responsible for persisting entities.Heartbeat in Turso/libSQL +type libsqlHeartbeatRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + db *sql.DB +} + +// NewLibsqlHeartbeatRepository creates the libSQL version of the HeartbeatRepository +func NewLibsqlHeartbeatRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + db *sql.DB, +) HeartbeatRepository { + return &libsqlHeartbeatRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &libsqlHeartbeatRepository{})), + tracer: tracer, + db: db, + } +} + +func (repository *libsqlHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { + ctx, span, _ := repository.tracer.StartWithLogger(ctx, repository.logger) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, + "INSERT INTO "+tableHeartbeats+" (id, owner, version, charging, user_id, timestamp) VALUES (?, ?, ?, ?, ?, ?)", + heartbeat.ID.String(), + heartbeat.Owner, + heartbeat.Version, + boolToInt(heartbeat.Charging), + string(heartbeat.UserID), + heartbeat.Timestamp.UTC(), + ) + if err != nil { + msg := fmt.Sprintf("cannot save heartbeat with ID [%s]", heartbeat.ID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + var rows *sql.Rows + var err error + + if len(params.Query) > 0 { + queryPattern := "%" + params.Query + "%" + rows, err = repository.db.QueryContext(ctx, + "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? AND version LIKE ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", + string(userID), owner, queryPattern, params.Limit, params.Skip, + ) + } else { + rows, err = repository.db.QueryContext(ctx, + "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", + string(userID), owner, params.Limit, params.Skip, + ) + } + if err != nil { + msg := fmt.Sprintf("cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + defer rows.Close() + + heartbeats := make([]entities.Heartbeat, 0) + for rows.Next() { + heartbeat, scanErr := repository.scanHeartbeat(rows) + if scanErr != nil { + msg := fmt.Sprintf("cannot scan heartbeat row for owner [%s]", owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(scanErr, msg)) + } + heartbeats = append(heartbeats, *heartbeat) + } + if rowsErr := rows.Err(); rowsErr != nil { + msg := fmt.Sprintf("error iterating heartbeat rows for owner [%s]", owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(rowsErr, msg)) + } + + return &heartbeats, nil +} + +func (repository *libsqlHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + row := repository.db.QueryRowContext(ctx, + "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT 1", + string(userID), owner, + ) + + heartbeat, err := repository.scanHeartbeatRow(row) + if err == sql.ErrNoRows { + msg := fmt.Sprintf("heartbeat with userID [%s] and owner [%s] does not exist", userID, owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) + } + if err != nil { + msg := fmt.Sprintf("cannot load heartbeat with userID [%s] and owner [%s]", userID, owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return heartbeat, nil +} + +func (repository *libsqlHeartbeatRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.db.ExecContext(ctx, "DELETE FROM "+tableHeartbeats+" WHERE user_id = ?", string(userID)) + if err != nil { + msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *libsqlHeartbeatRepository) scanHeartbeat(rows *sql.Rows) (*entities.Heartbeat, error) { + heartbeat := new(entities.Heartbeat) + var id string + var charging int + var userID string + err := rows.Scan(&id, &heartbeat.Owner, &heartbeat.Version, &charging, &userID, &heartbeat.Timestamp) + if err != nil { + return nil, err + } + heartbeat.ID, err = uuid.Parse(id) + if err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat ID [%s]", id)) + } + heartbeat.Charging = charging != 0 + heartbeat.UserID = entities.UserID(userID) + return heartbeat, nil +} + +func (repository *libsqlHeartbeatRepository) scanHeartbeatRow(row *sql.Row) (*entities.Heartbeat, error) { + heartbeat := new(entities.Heartbeat) + var id string + var charging int + var userID string + err := row.Scan(&id, &heartbeat.Owner, &heartbeat.Version, &charging, &userID, &heartbeat.Timestamp) + if err != nil { + return nil, err + } + heartbeat.ID, err = uuid.Parse(id) + if err != nil { + return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat ID [%s]", id)) + } + heartbeat.Charging = charging != 0 + heartbeat.UserID = entities.UserID(userID) + return heartbeat, nil +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/docs/superpowers/plans/2026-05-15-hedging-repository.md b/docs/superpowers/plans/2026-05-15-hedging-repository.md new file mode 100644 index 000000000..e3b838fa9 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-hedging-repository.md @@ -0,0 +1,265 @@ +# Hedging Repository Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +> **Status:** All tasks implemented. This plan was written retroactively to document and verify the implementation. + +**Goal:** Create composite hedging repositories that dual-write to GORM (primary) and Turso (secondary) with fail-open semantics on secondary failures. + +**Architecture:** Two new repository files implement the existing `HeartbeatRepository` and `HeartbeatMonitorRepository` interfaces by delegating reads to primary only and writes to both. Secondary write failures are logged and counted via an OTel metric but never propagated. Activated via `HEARTBEAT_DB_BACKEND=hedging`. + +**Tech Stack:** Go, OpenTelemetry metrics (`go.opentelemetry.io/otel/metric`), existing repository interfaces + +**Spec:** `docs/superpowers/specs/2026-05-15-hedging-repository-design.md` + +--- + +### Task 1: Create hedging heartbeat repository ✅ + +**Files:** + +- Created: `api/pkg/repositories/hedging_heartbeat_repository.go` + +- [ ] **Step 1: Create the hedging heartbeat repository file** + +```go +package repositories + +import ( + "context" + "fmt" + + otelMetric "go.opentelemetry.io/otel/metric" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +type hedgingHeartbeatRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + primary HeartbeatRepository + secondary HeartbeatRepository + failureCounter otelMetric.Int64Counter +} + +func NewHedgingHeartbeatRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + primary HeartbeatRepository, + secondary HeartbeatRepository, + failureCounter otelMetric.Int64Counter, +) HeartbeatRepository { + return &hedgingHeartbeatRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatRepository{})), + tracer: tracer, + primary: primary, + secondary: secondary, + failureCounter: failureCounter, + } +} +``` + +Implement 4 methods: + +- `Store` — write to primary, then secondary (fail-open with log + metric) +- `Index` — delegate to primary only +- `Last` — delegate to primary only +- `DeleteAllForUser` — write to primary, then secondary (fail-open with log + metric) + +Write methods follow this pattern: + +```go +func (r *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { + ctx, span := r.tracer.Start(ctx) + defer span.End() + + if err := r.primary.Store(ctx, heartbeat); err != nil { + return err + } + + if err := r.secondary.Store(ctx, heartbeat); err != nil { + r.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for heartbeat [%s]", heartbeat.ID))) + r.failureCounter.Add(ctx, 1) + } + + return nil +} +``` + +Read methods simply delegate: + +```go +func (r *hedgingHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { + return r.primary.Index(ctx, userID, owner, params) +} +``` + +- [ ] **Step 2: Verify build** + +Run: `cd api && go build ./...` +Expected: exit code 0 + +- [ ] **Step 3: Commit** + +```bash +git add api/pkg/repositories/hedging_heartbeat_repository.go +git commit -m "feat(api): add hedging heartbeat repository" +``` + +--- + +### Task 2: Create hedging heartbeat monitor repository ✅ + +**Files:** + +- Created: `api/pkg/repositories/hedging_heartbeat_monitor_repository.go` + +- [ ] **Step 1: Create the hedging heartbeat monitor repository file** + +Same struct pattern as Task 1 but wrapping `HeartbeatMonitorRepository` interface. + +Implement 7 methods: + +| Method | Behavior | +| ------------------- | ---------------------- | +| `Store` | Write both (fail-open) | +| `Load` | Primary only | +| `Exists` | Primary only | +| `UpdateQueueID` | Write both (fail-open) | +| `Delete` | Write both (fail-open) | +| `UpdatePhoneOnline` | Write both (fail-open) | +| `DeleteAllForUser` | Write both (fail-open) | + +All write methods follow the same fail-open pattern: primary must succeed, secondary logs + increments counter on failure. + +- [ ] **Step 2: Verify build** + +Run: `cd api && go build ./...` +Expected: exit code 0 + +- [ ] **Step 3: Commit** + +```bash +git add api/pkg/repositories/hedging_heartbeat_monitor_repository.go +git commit -m "feat(api): add hedging heartbeat monitor repository" +``` + +--- + +### Task 3: Add HedgingFailureCounter to DI container ✅ + +**Files:** + +- Modified: `api/pkg/di/container.go` (added `HedgingFailureCounter()` method after `TursoDB()`, ~line 320) + +- [ ] **Step 1: Add the HedgingFailureCounter method** + +```go +func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { + meter := otel.GetMeterProvider().Meter( + container.projectID, + otelMetric.WithInstrumentationVersion(otel.Version()), + ) + counter, err := meter.Int64Counter( + "hedging.secondary.write.failures", + otelMetric.WithUnit("1"), + otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), + ) + if err != nil { + container.logger.Fatal(stacktrace.Propagate(err, "cannot create hedging failure counter")) + } + return counter +} +``` + +- [ ] **Step 2: Verify build** + +Run: `cd api && go build ./...` +Expected: exit code 0 + +--- + +### Task 4: Wire hedging mode in DI container ✅ + +**Files:** + +- Modified: `api/pkg/di/container.go` + + - `HeartbeatRepository()` (~line 1768) + - `HeartbeatMonitorRepository()` (~line 930) + +- [ ] **Step 1: Change both methods from if/else to switch** + +Replace the existing `if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso"` with a switch: + +```go +func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { + switch os.Getenv("HEARTBEAT_DB_BACKEND") { + case "turso": + // existing libSQL path + case "hedging": + return repositories.NewHedgingHeartbeatRepository( + container.Logger(), + container.Tracer(), + repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), + repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), + container.HedgingFailureCounter(), + ) + default: + // existing GORM path + } +} +``` + +Same pattern for `HeartbeatMonitorRepository()`. + +- [ ] **Step 2: Verify build** + +Run: `cd api && go build ./...` +Expected: exit code 0 + +- [ ] **Step 3: Run pre-commit hooks** + +Run: `cd api && gofumpt -w pkg/repositories/hedging_heartbeat_repository.go pkg/repositories/hedging_heartbeat_monitor_repository.go pkg/di/container.go` +Expected: exit code 0 + +- [ ] **Step 4: Final commit** + +```bash +git add api/pkg/di/container.go +git commit -m "feat(api): wire hedging mode in DI container (HEARTBEAT_DB_BACKEND=hedging)" +``` + +--- + +### Task 5: Final verification + +- [ ] **Step 1: Full build** + +Run: `cd api && go build ./...` +Expected: exit code 0 + +- [ ] **Step 2: Run tests** + +Run: `cd api && go test ./...` +Expected: all tests pass + +- [ ] **Step 3: go vet (no new warnings)** + +Run: `cd api && go vet ./... 2>&1 | Select-String "hedging"` +Expected: only pre-existing `non-constant format string` warnings (same category as all other repos) + +- [ ] **Step 4: Pre-commit hooks pass** + +Run: `git add -A && git commit --dry-run` +Expected: all hooks pass (go-fumpt, go-lint, go-imports, go-mod-tidy) + +- [ ] **Step 5: Verify three modes work (code review)** + +Check that the DI container correctly handles all three values: + +- Default (unset) → GORM only +- `turso` → libSQL only +- `hedging` → GORM primary + libSQL secondary diff --git a/docs/superpowers/specs/2026-05-15-hedging-repository-design.md b/docs/superpowers/specs/2026-05-15-hedging-repository-design.md new file mode 100644 index 000000000..2afc6acec --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-hedging-repository-design.md @@ -0,0 +1,164 @@ +# Hedging Repository for Heartbeat & Monitor + +**Date:** 2026-05-15 +**Status:** Approved + +## Overview + +Create composite "hedging" repositories for `HeartbeatRepository` and `HeartbeatMonitorRepository` that write to both GORM (primary) and Turso (secondary). Reads only hit the primary. Secondary writes are fail-open — errors are logged and a metric is emitted, but the operation succeeds from the caller's perspective. + +## Motivation + +Gradually migrate heartbeat data to Turso by dual-writing. The GORM/PostgreSQL backend remains the source of truth while Turso builds up a complete dataset. If Turso has issues, the system is unaffected. + +## Configuration + +Activated via `HEARTBEAT_DB_BACKEND=hedging`. The three modes are now: + +| Value | Behavior | +| ----------------- | ---------------------------------------------------------------------- | +| _(unset/default)_ | GORM/PostgreSQL only | +| `turso` | Turso/libSQL only | +| `hedging` | GORM primary (reads+writes) + Turso secondary (writes only, fail-open) | + +## Architecture + +### New Files + +| File | Purpose | +| -------------------------------------------------------------- | -------------------------------------- | +| `api/pkg/repositories/hedging_heartbeat_repository.go` | Composite `HeartbeatRepository` | +| `api/pkg/repositories/hedging_heartbeat_monitor_repository.go` | Composite `HeartbeatMonitorRepository` | + +### Modified Files + +| File | Change | +| ------------------------- | ------------------------------------------------------------------ | +| `api/pkg/di/container.go` | Add `hedging` case to switch, add `HedgingFailureCounter()` method | + +## Method Delegation + +### HeartbeatRepository + +| Method | Primary (GORM) | Secondary (Turso) | +| ------------------ | -------------- | -------------------- | +| `Store` | ✅ write | ✅ write (fail-open) | +| `Index` | ✅ read | ❌ skip | +| `Last` | ✅ read | ❌ skip | +| `DeleteAllForUser` | ✅ write | ✅ write (fail-open) | + +### HeartbeatMonitorRepository + +| Method | Primary (GORM) | Secondary (Turso) | +| ------------------- | -------------- | -------------------- | +| `Store` | ✅ write | ✅ write (fail-open) | +| `Load` | ✅ read | ❌ skip | +| `Exists` | ✅ read | ❌ skip | +| `UpdateQueueID` | ✅ write | ✅ write (fail-open) | +| `Delete` | ✅ write | ✅ write (fail-open) | +| `UpdatePhoneOnline` | ✅ write | ✅ write (fail-open) | +| `DeleteAllForUser` | ✅ write | ✅ write (fail-open) | + +## Struct Design + +```go +type hedgingHeartbeatRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + primary HeartbeatRepository + secondary HeartbeatRepository + failureCounter otelMetric.Int64Counter +} + +type hedgingHeartbeatMonitorRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + primary HeartbeatMonitorRepository + secondary HeartbeatMonitorRepository + failureCounter otelMetric.Int64Counter +} +``` + +## Error Handling (Fail-Open Pattern) + +```go +func (r *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { + ctx, span := r.tracer.Start(ctx) + defer span.End() + + // Primary: must succeed + if err := r.primary.Store(ctx, heartbeat); err != nil { + return err + } + + // Secondary: fail-open (log + metric) + if err := r.secondary.Store(ctx, heartbeat); err != nil { + r.logger.Error(stacktrace.Propagate(err, fmt.Sprintf( + "hedging: secondary write failed for heartbeat [%s]", heartbeat.ID, + ))) + r.failureCounter.Add(ctx, 1) + } + + return nil +} +``` + +**Rules:** + +- Primary error → return immediately, no secondary attempt +- Secondary error → log at ERROR level, increment `failureCounter`, return nil +- Read methods → delegate directly to primary, no secondary involvement +- Each method has its own tracing span via `tracer.Start(ctx)` + +## Observability + +**Metric:** + +- Name: `hedging.secondary.write.failures` +- Unit: `1` (count) +- Description: `Number of failed secondary writes in hedging repositories` +- Created once in DI container, shared by both hedging repos + +**Logging:** Each secondary failure logs at ERROR with the method context (entity ID, user ID, etc.) + +## DI Container Changes + +```go +func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { + switch os.Getenv("HEARTBEAT_DB_BACKEND") { + case "turso": + return repositories.NewLibsqlHeartbeatRepository(...) + case "hedging": + return repositories.NewHedgingHeartbeatRepository( + container.Logger(), + container.Tracer(), + repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), + repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), + container.HedgingFailureCounter(), + ) + default: + return repositories.NewGormHeartbeatRepository(...) + } +} + +func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { + meter := otel.GetMeterProvider().Meter(container.projectID) + counter, err := meter.Int64Counter("hedging.secondary.write.failures", + otelMetric.WithUnit("1"), + otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), + ) + if err != nil { + container.logger.Fatal(...) + } + return counter +} +``` + +Same switch pattern for `HeartbeatMonitorRepository()`. + +## Safety + +- Default behavior (no env var) is unchanged — pure GORM/PostgreSQL +- `turso` mode remains available for pure Turso usage +- `hedging` mode never fails due to Turso issues — secondary is fully fail-open +- Service layer requires no changes — same interfaces diff --git a/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md b/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md new file mode 100644 index 000000000..b501650f2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md @@ -0,0 +1,162 @@ +# Turso/libSQL Backend for Heartbeat & Monitor Repositories + +**Date:** 2026-05-15 +**Status:** Approved + +## Overview + +Add a libSQL/Turso alternative implementation for `HeartbeatRepository` and `HeartbeatMonitorRepository`, switchable via the `HEARTBEAT_DB_BACKEND` environment variable. When set to `turso`, the API connects to a cloud-hosted Turso database instead of the dedicated PostgreSQL instance. + +## Motivation + +Move heartbeat storage to a dedicated Turso database for cost efficiency and edge performance, while keeping the existing PostgreSQL path as the default fallback. + +## Configuration + +| Env Var | Purpose | Example | +| ---------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `HEARTBEAT_DB_BACKEND` | Selects backend (`turso` = libSQL, anything else = PostgreSQL) | `turso` | +| `TURSO_DATABASE_DSN` | Turso database DSN (URL with authToken query param) | `libsql://httpsms-ndolestudio.aws-us-east-1.turso.io?authToken=eyJ...` | + +When `HEARTBEAT_DB_BACKEND` is not set or is any value other than `turso`, the existing GORM/PostgreSQL path is used unchanged. + +## Architecture + +### Approach + +Direct `database/sql` with the Turso Go remote driver (`github.com/tursodatabase/libsql-client-go`). No ORM — raw SQL queries for a simple 2-table schema. This is the pure-Go HTTP driver for remote Turso Cloud access (no CGo required). + +### New Files + +| File | Purpose | +| ------------------------------------------------------------- | -------------------------------------------------------- | +| `api/pkg/repositories/libsql.go` | Connection factory, table auto-creation, shared helpers | +| `api/pkg/repositories/libsql_heartbeat_repository.go` | `HeartbeatRepository` implementation using libSQL | +| `api/pkg/repositories/libsql_heartbeat_monitor_repository.go` | `HeartbeatMonitorRepository` implementation using libSQL | + +### Modified Files + +| File | Change | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `api/pkg/di/container.go` | Add `tursoDB *sql.DB` field, `TursoDB()` method, conditional wiring in `HeartbeatRepository()` and `HeartbeatMonitorRepository()` | +| `api/go.mod` | Add `github.com/tursodatabase/libsql-client-go` dependency | + +## Database Schema + +```sql +CREATE TABLE IF NOT EXISTS heartbeats ( + id TEXT PRIMARY KEY, + owner TEXT NOT NULL, + version TEXT NOT NULL, + charging INTEGER NOT NULL DEFAULT 0, + user_id TEXT NOT NULL, + timestamp DATETIME NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_heartbeats_owner_timestamp ON heartbeats(owner, timestamp); +CREATE INDEX IF NOT EXISTS idx_heartbeats_user_id ON heartbeats(user_id); + +CREATE TABLE IF NOT EXISTS heartbeat_monitors ( + id TEXT PRIMARY KEY, + phone_id TEXT NOT NULL, + user_id TEXT NOT NULL, + queue_id TEXT NOT NULL DEFAULT '', + owner TEXT NOT NULL, + phone_online INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_heartbeat_monitors_user_owner ON heartbeat_monitors(user_id, owner); +``` + +**Type mappings from PostgreSQL:** + +- UUID → TEXT +- BOOLEAN → INTEGER (0/1) +- TIMESTAMP → DATETIME (ISO 8601 text) + +## Repository Implementations + +### `libsql.go` (shared) + +- `NewTursoDB(url, authToken string) (*sql.DB, error)` — opens connection with libSQL driver, executes CREATE TABLE/INDEX statements +- Returns `*sql.DB` for use by both repository implementations + +### `libsql_heartbeat_repository.go` + +Implements `HeartbeatRepository`: + +| Method | SQL | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| `Store` | `INSERT INTO heartbeats (id, owner, version, charging, user_id, timestamp) VALUES (?, ?, ?, ?, ?, ?)` | +| `Index` | `SELECT ... WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?` (optional `version LIKE ?` filter) | +| `Last` | `SELECT ... WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT 1` | +| `DeleteAllForUser` | `DELETE FROM heartbeats WHERE user_id = ?` | + +### `libsql_heartbeat_monitor_repository.go` + +Implements `HeartbeatMonitorRepository`: + +| Method | SQL | +| ------------------- | --------------------------------------------------------------------------- | +| `Store` | INSERT all fields | +| `Load` | SELECT by user_id + owner | +| `Exists` | `SELECT COUNT(*) FROM ... WHERE user_id = ? AND id = ?` (returns count > 0) | +| `UpdateQueueID` | UPDATE queue_id + updated_at WHERE id = ? | +| `Delete` | DELETE WHERE user_id = ? AND owner = ? | +| `UpdatePhoneOnline` | UPDATE phone_online + updated_at WHERE id = ? AND user_id = ? | +| `DeleteAllForUser` | DELETE WHERE user_id = ? | + +### Error Handling + +- `sql.ErrNoRows` → wrap with `stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)` to match GORM behavior expected by the service layer +- All other errors → wrap with `stacktrace.Propagate(err, msg)` + +### Observability + +Every method follows the existing tracing pattern: + +```go +ctx, span := repository.tracer.Start(ctx) +defer span.End() +``` + +## DI Container Changes + +```go +// New field on Container struct +tursoDB *sql.DB + +// New method +func (container *Container) TursoDB() *sql.DB { + if container.tursoDB != nil { + return container.tursoDB + } + db, err := repositories.NewTursoDB(os.Getenv("TURSO_DATABASE_DSN")) + if err != nil { + container.logger.Fatal(err) + } + container.tursoDB = db + return container.tursoDB +} + +// Modified methods +func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { + if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso" { + return repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()) + } + return repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()) +} + +func (container *Container) HeartbeatMonitorRepository() repositories.HeartbeatMonitorRepository { + if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso" { + return repositories.NewLibsqlHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.TursoDB()) + } + return repositories.NewGormHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.DedicatedDB()) +} +``` + +## Safety + +- When `HEARTBEAT_DB_BACKEND != "turso"`, `TursoDB()` is never called — no Turso connection is opened +- The existing PostgreSQL path remains the default and is completely unaffected +- Both implementations satisfy the same interface — the service layer requires no changes diff --git a/tests/.env.test b/tests/.env.test index a95703f09..909902aec 100644 --- a/tests/.env.test +++ b/tests/.env.test @@ -28,3 +28,5 @@ PUSHER_CLUSTER= GCS_BUCKET_NAME= UPTRACE_DSN= CLOUDFLARE_TURNSTILE_SECRET_KEY= +HEARTBEAT_DB_BACKEND=hedging +TURSO_DATABASE_DSN=http://sqld:8080 diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index a4d6b9dce..1e111b785 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -25,6 +25,11 @@ services: timeout: 5s retries: 10 + sqld: + image: ghcr.io/tursodatabase/libsql-server:latest + ports: + - "8090:8080" + wiremock: image: wiremock/wiremock:3x ports: @@ -53,6 +58,8 @@ services: condition: service_healthy wiremock: condition: service_healthy + sqld: + condition: service_started env_file: - .env.test environment: diff --git a/tests/integration_test.go b/tests/integration_test.go index 5aae58fff..12776aac2 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -346,3 +346,59 @@ func TestSendSMS_OutstandingFlow(t *testing.T) { assertWebhookJWT(t, req.Request, signingKey) } } + +func TestHeartbeat_StoreAndIndex(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + // Store a heartbeat via phone API key (retry to allow async phone-API-key association) + storePayload := map[string]interface{}{ + "phone_numbers": []string{phone.PhoneNumber}, + "charging": true, + } + + url := apiBaseURL + "/v1/heartbeats" + var respBody []byte + var statusCode int + deadline := time.Now().Add(15 * time.Second) + for time.Now().Before(deadline) { + body, err := json.Marshal(storePayload) + require.NoError(t, err) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", phone.PhoneAPIKey) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + respBody, err = io.ReadAll(resp.Body) + resp.Body.Close() + require.NoError(t, err) + + statusCode = resp.StatusCode + if statusCode == http.StatusCreated { + break + } + time.Sleep(500 * time.Millisecond) + } + require.Equal(t, http.StatusCreated, statusCode, "store heartbeat failed: %s", string(respBody)) + + // Read heartbeats back via user API key + client := newAPIClient() + heartbeats, indexResp, err := client.Heartbeats.Index(ctx, &httpsms.HeartbeatIndexParams{ + Owner: phone.PhoneNumber, + Limit: 1, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, indexResp.HTTPResponse.StatusCode) + + require.NotNil(t, heartbeats) + require.GreaterOrEqual(t, len(heartbeats.Data), 1, "expected at least 1 heartbeat") + + hb := heartbeats.Data[0] + assert.Equal(t, phone.PhoneNumber, hb.Owner) + assert.True(t, hb.Charging) + assert.False(t, hb.Timestamp.IsZero(), "timestamp should not be zero") +} From 5ffdcb0b7ce6092020f40fe395d2e9df5d848809 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sat, 16 May 2026 12:00:43 +0300 Subject: [PATCH 155/381] Remove docs for superpowers --- .../plans/2026-04-11-mms-attachments.md | 1139 ----------------- .../2026-05-03-integration-test-setup.md | 1107 ---------------- .../2026-05-03-scheduling-send-refactor.md | 956 -------------- .../plans/2026-05-15-hedging-repository.md | 265 ---- .../2026-04-11-mms-attachments-design.md | 220 ---- .../2026-05-03-entitlement-service-design.md | 188 --- ...026-05-03-integration-test-setup-design.md | 248 ---- ...6-05-03-scheduling-send-refactor-design.md | 188 --- ...05-05-integration-tests-wiremock-design.md | 304 ----- .../2026-05-15-hedging-repository-design.md | 164 --- ...26-05-15-turso-heartbeat-backend-design.md | 162 --- 11 files changed, 4941 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-11-mms-attachments.md delete mode 100644 docs/superpowers/plans/2026-05-03-integration-test-setup.md delete mode 100644 docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md delete mode 100644 docs/superpowers/plans/2026-05-15-hedging-repository.md delete mode 100644 docs/superpowers/specs/2026-04-11-mms-attachments-design.md delete mode 100644 docs/superpowers/specs/2026-05-03-entitlement-service-design.md delete mode 100644 docs/superpowers/specs/2026-05-03-integration-test-setup-design.md delete mode 100644 docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md delete mode 100644 docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md delete mode 100644 docs/superpowers/specs/2026-05-15-hedging-repository-design.md delete mode 100644 docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md diff --git a/docs/superpowers/plans/2026-04-11-mms-attachments.md b/docs/superpowers/plans/2026-04-11-mms-attachments.md deleted file mode 100644 index a759e1c48..000000000 --- a/docs/superpowers/plans/2026-04-11-mms-attachments.md +++ /dev/null @@ -1,1139 +0,0 @@ -# MMS Attachment Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add MMS attachment upload/download support to the httpSMS API so received MMS attachments are stored in cloud storage and downloadable via a public URL. - -**Architecture:** Android sends base64-encoded attachments in the receive request. The API decodes and uploads them to GCS (or in-memory storage) via a storage interface, stores download URLs in the existing `Message.Attachments` field, and exposes an unauthenticated download endpoint. The webhook event payload includes attachment URLs. - -**Tech Stack:** Go, Fiber v2, GORM, `cloud.google.com/go/storage`, `errgroup`, `stacktrace` - ---- - -## File Structure - -**New files:** - -| File | Responsibility | -| --------------------------------------------------- | -------------------------------------------------------------------------------- | -| `api/pkg/repositories/attachment_storage.go` | `AttachmentStorage` interface + content-type-to-extension mapping + sanitization | -| `api/pkg/repositories/gcs_attachment_storage.go` | GCS implementation of `AttachmentStorage` | -| `api/pkg/repositories/memory_attachment_storage.go` | In-memory implementation of `AttachmentStorage` | -| `api/pkg/repositories/attachment_storage_test.go` | Unit tests for content-type mapping and filename sanitization | -| `api/pkg/handlers/attachment_handler.go` | Download endpoint handler (`GET /v1/attachments/...`) | - -**Modified files:** - -| File | Change | -| ------------------------------------------------- | ------------------------------------------------------------------------------- | -| `api/pkg/requests/message_receive_request.go` | Add `Attachments` field + `MessageAttachment` struct | -| `api/pkg/services/message_service.go` | Add `Attachments` to params, upload logic in `ReceiveMessage()`, set on message | -| `api/pkg/validators/message_handler_validator.go` | Add attachment count/size/content-type validation | -| `api/pkg/events/message_phone_received_event.go` | Add `Attachments []string` to payload | -| `api/pkg/di/container.go` | Wire `AttachmentStorage`, `AttachmentHandler`, `RegisterAttachmentRoutes()` | -| `api/.env.docker` | Add `GCS_BUCKET_NAME` | -| `api/go.mod` / `api/go.sum` | Add `cloud.google.com/go/storage` and `golang.org/x/sync` (errgroup) | - ---- - -### Task 1: Add GCS SDK and errgroup dependencies - -**Files:** - -- Modify: `api/go.mod` - -- [ ] **Step 1: Add dependencies** - -```bash -cd api && go get cloud.google.com/go/storage && go get golang.org/x/sync -``` - -- [ ] **Step 2: Verify build still works** - -Run: `cd api && go build ./...` -Expected: Build succeeds - -- [ ] **Step 3: Commit** - -```bash -cd api && git add go.mod go.sum && git commit -m "chore: add cloud.google.com/go/storage and golang.org/x/sync deps" -``` - ---- - -### Task 2: Storage interface, content-type mapping, and filename sanitization - -**Files:** - -- Create: `api/pkg/repositories/attachment_storage.go` -- Create: `api/pkg/repositories/attachment_storage_test.go` - -- [ ] **Step 1: Write the test file** - -Create `api/pkg/repositories/attachment_storage_test.go`: - -```go -package repositories - -import "testing" - -func TestExtensionFromContentType(t *testing.T) { - tests := []struct { - contentType string - expected string - }{ - {"image/jpeg", ".jpg"}, - {"image/png", ".png"}, - {"image/gif", ".gif"}, - {"image/webp", ".webp"}, - {"image/bmp", ".bmp"}, - {"video/mp4", ".mp4"}, - {"video/3gpp", ".3gp"}, - {"audio/mpeg", ".mp3"}, - {"audio/ogg", ".ogg"}, - {"audio/amr", ".amr"}, - {"application/pdf", ".pdf"}, - {"text/vcard", ".vcf"}, - {"text/x-vcard", ".vcf"}, - {"application/octet-stream", ".bin"}, - {"unknown/type", ".bin"}, - {"", ".bin"}, - } - for _, tt := range tests { - t.Run(tt.contentType, func(t *testing.T) { - got := ExtensionFromContentType(tt.contentType) - if got != tt.expected { - t.Errorf("ExtensionFromContentType(%q) = %q, want %q", tt.contentType, got, tt.expected) - } - }) - } -} - -func TestSanitizeFilename(t *testing.T) { - tests := []struct { - name string - index int - expected string - }{ - {"photo.jpg", 0, "photo"}, - {"../../etc/passwd", 0, "etcpasswd"}, - {"hello/world\\test", 0, "helloworldtest"}, - {"normal_file", 0, "normal_file"}, - {"", 0, "attachment-0"}, - {" ", 0, "attachment-0"}, - {"...", 1, "attachment-1"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := SanitizeFilename(tt.name, tt.index) - if got != tt.expected { - t.Errorf("SanitizeFilename(%q, %d) = %q, want %q", tt.name, tt.index, got, tt.expected) - } - }) - } -} -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `cd api && go test ./pkg/repositories/ -run "TestExtensionFromContentType|TestSanitizeFilename" -v` -Expected: FAIL — functions not defined - -- [ ] **Step 3: Write the storage interface and utility functions** - -Create `api/pkg/repositories/attachment_storage.go`: - -```go -package repositories - -import ( - "context" - "fmt" - "path/filepath" - "strings" -) - -// AttachmentStorage is the interface for storing and retrieving message attachments -type AttachmentStorage interface { - // Upload stores attachment data at the given path - Upload(ctx context.Context, path string, data []byte) error - // Download retrieves attachment data from the given path - Download(ctx context.Context, path string) ([]byte, error) - // Delete removes an attachment at the given path - Delete(ctx context.Context, path string) error -} - -// contentTypeExtensions maps MIME types to file extensions -var contentTypeExtensions = map[string]string{ - "image/jpeg": ".jpg", - "image/png": ".png", - "image/gif": ".gif", - "image/webp": ".webp", - "image/bmp": ".bmp", - "video/mp4": ".mp4", - "video/3gpp": ".3gp", - "audio/mpeg": ".mp3", - "audio/ogg": ".ogg", - "audio/amr": ".amr", - "application/pdf": ".pdf", - "text/vcard": ".vcf", - "text/x-vcard": ".vcf", -} - -// AllowedContentTypes returns the set of allowed MIME types for attachments -func AllowedContentTypes() map[string]bool { - allowed := make(map[string]bool, len(contentTypeExtensions)) - for ct := range contentTypeExtensions { - allowed[ct] = true - } - return allowed -} - -// ExtensionFromContentType returns the file extension for a MIME content type. -// Returns ".bin" if the content type is not recognized. -func ExtensionFromContentType(contentType string) string { - if ext, ok := contentTypeExtensions[contentType]; ok { - return ext - } - return ".bin" -} - -// ContentTypeFromExtension returns the MIME content type for a file extension. -// Returns "application/octet-stream" if the extension is not recognized. -func ContentTypeFromExtension(ext string) string { - for ct, e := range contentTypeExtensions { - if e == ext { - return ct - } - } - return "application/octet-stream" -} - -// SanitizeFilename removes path separators and traversal sequences from a filename. -// Returns "attachment-{index}" if the sanitized name is empty. -func SanitizeFilename(name string, index int) string { - name = strings.TrimSuffix(name, filepath.Ext(name)) - name = strings.ReplaceAll(name, "/", "") - name = strings.ReplaceAll(name, "\\", "") - name = strings.ReplaceAll(name, "..", "") - name = strings.TrimSpace(name) - - if name == "" { - return fmt.Sprintf("attachment-%d", index) - } - return name -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `cd api && go test ./pkg/repositories/ -run "TestExtensionFromContentType|TestSanitizeFilename" -v` -Expected: PASS - -- [ ] **Step 5: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add AttachmentStorage interface and content-type utilities" -``` - ---- - -### Task 3: Memory storage implementation - -**Files:** - -- Create: `api/pkg/repositories/memory_attachment_storage.go` - -- [ ] **Step 1: Write the implementation** - -Create `api/pkg/repositories/memory_attachment_storage.go`: - -```go -package repositories - -import ( - "context" - "fmt" - "sync" - - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// MemoryAttachmentStorage stores attachments in memory -type MemoryAttachmentStorage struct { - logger telemetry.Logger - tracer telemetry.Tracer - data sync.Map -} - -// NewMemoryAttachmentStorage creates a new MemoryAttachmentStorage -func NewMemoryAttachmentStorage( - logger telemetry.Logger, - tracer telemetry.Tracer, -) *MemoryAttachmentStorage { - return &MemoryAttachmentStorage{ - logger: logger.WithService(fmt.Sprintf("%T", &MemoryAttachmentStorage{})), - tracer: tracer, - } -} - -// Upload stores attachment data at the given path -func (s *MemoryAttachmentStorage) Upload(ctx context.Context, path string, data []byte) error { - _, span := s.tracer.Start(ctx) - defer span.End() - - s.data.Store(path, data) - s.logger.Info(fmt.Sprintf("stored attachment at path [%s] with size [%d]", path, len(data))) - return nil -} - -// Download retrieves attachment data from the given path -func (s *MemoryAttachmentStorage) Download(ctx context.Context, path string) ([]byte, error) { - _, span := s.tracer.Start(ctx) - defer span.End() - - value, ok := s.data.Load(path) - if !ok { - return nil, stacktrace.NewError(fmt.Sprintf("attachment not found at path [%s]", path)) - } - return value.([]byte), nil -} - -// Delete removes an attachment at the given path -func (s *MemoryAttachmentStorage) Delete(ctx context.Context, path string) error { - _, span := s.tracer.Start(ctx) - defer span.End() - - s.data.Delete(path) - s.logger.Info(fmt.Sprintf("deleted attachment at path [%s]", path)) - return nil -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: Build succeeds - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add MemoryAttachmentStorage implementation" -``` - ---- - -### Task 4: GCS storage implementation - -**Files:** - -- Create: `api/pkg/repositories/gcs_attachment_storage.go` - -- [ ] **Step 1: Write the implementation** - -Create `api/pkg/repositories/gcs_attachment_storage.go`: - -```go -package repositories - -import ( - "context" - "fmt" - "io" - - "cloud.google.com/go/storage" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// GCSAttachmentStorage stores attachments in Google Cloud Storage -type GCSAttachmentStorage struct { - logger telemetry.Logger - tracer telemetry.Tracer - client *storage.Client - bucket string -} - -// NewGCSAttachmentStorage creates a new GCSAttachmentStorage -func NewGCSAttachmentStorage( - logger telemetry.Logger, - tracer telemetry.Tracer, - client *storage.Client, - bucket string, -) *GCSAttachmentStorage { - return &GCSAttachmentStorage{ - logger: logger.WithService(fmt.Sprintf("%T", &GCSAttachmentStorage{})), - tracer: tracer, - client: client, - bucket: bucket, - } -} - -// Upload stores attachment data at the given path in GCS -func (s *GCSAttachmentStorage) Upload(ctx context.Context, path string, data []byte) error { - ctx, span := s.tracer.Start(ctx) - defer span.End() - - writer := s.client.Bucket(s.bucket).Object(path).NewWriter(ctx) - if _, err := writer.Write(data); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot write attachment to GCS path [%s]", path))) - } - - if err := writer.Close(); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot close GCS writer for path [%s]", path))) - } - - s.logger.Info(fmt.Sprintf("uploaded attachment to GCS path [%s/%s] with size [%d]", s.bucket, path, len(data))) - return nil -} - -// Download retrieves attachment data from the given path in GCS -func (s *GCSAttachmentStorage) Download(ctx context.Context, path string) ([]byte, error) { - ctx, span := s.tracer.Start(ctx) - defer span.End() - - reader, err := s.client.Bucket(s.bucket).Object(path).NewReader(ctx) - if err != nil { - return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot open GCS reader for path [%s]", path))) - } - defer reader.Close() - - data, err := io.ReadAll(reader) - if err != nil { - return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot read attachment from GCS path [%s]", path))) - } - - return data, nil -} - -// Delete removes an attachment at the given path in GCS -func (s *GCSAttachmentStorage) Delete(ctx context.Context, path string) error { - ctx, span := s.tracer.Start(ctx) - defer span.End() - - if err := s.client.Bucket(s.bucket).Object(path).Delete(ctx); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot delete GCS object at path [%s]", path))) - } - - s.logger.Info(fmt.Sprintf("deleted attachment from GCS path [%s/%s]", s.bucket, path)) - return nil -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: Build succeeds - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add GCSAttachmentStorage implementation" -``` - ---- - -### Task 5: Update request and event structs - -**Files:** - -- Modify: `api/pkg/requests/message_receive_request.go` (full file) -- Modify: `api/pkg/services/message_service.go:290-300` (MessageReceiveParams) -- Modify: `api/pkg/events/message_phone_received_event.go:14-23` (payload struct) - -**Important:** The `requests` package already imports `services` (for `ToMessageReceiveParams`), so we **cannot** import `requests` from `services`. Define a `ServiceAttachment` struct in the services package to avoid a circular import. - -- [ ] **Step 1: Add ServiceAttachment to services package** - -In `api/pkg/services/message_service.go`, add after the imports (before `MessageService` struct at line 22): - -```go -// ServiceAttachment represents attachment data passed to the service layer -type ServiceAttachment struct { - Name string - ContentType string - Content string // base64-encoded -} -``` - -Update `MessageReceiveParams` (lines 290-300) to add `Attachments`: - -```go -type MessageReceiveParams struct { - Contact string - UserID entities.UserID - Owner phonenumbers.PhoneNumber - Content string - SIM entities.SIM - Timestamp time.Time - Encrypted bool - Source string - Attachments []ServiceAttachment -} -``` - -- [ ] **Step 2: Add MessageAttachment struct and update MessageReceive request** - -In `api/pkg/requests/message_receive_request.go`, add the `MessageAttachment` struct before the `MessageReceive` struct, and add the `Attachments` field: - -```go -// MessageAttachment represents a single MMS attachment in a receive request -type MessageAttachment struct { - // Name is the original filename of the attachment - Name string `json:"name" example:"photo.jpg"` - // ContentType is the MIME type of the attachment - ContentType string `json:"content_type" example:"image/jpeg"` - // Content is the base64-encoded attachment data - Content string `json:"content" example:"base64data..."` -} - -// MessageReceive is the payload for receiving an SMS/MMS message -type MessageReceive struct { - request - From string `json:"from" example:"+18005550199"` - To string `json:"to" example:"+18005550100"` - Content string `json:"content" example:"This is a sample text message received on a phone"` - // Encrypted is used to determine if the content is end-to-end encrypted - Encrypted bool `json:"encrypted" example:"false"` - // SIM card that received the message - SIM entities.SIM `json:"sim" example:"SIM1"` - // Timestamp is the time when the event was emitted - Timestamp time.Time `json:"timestamp" example:"2022-06-05T14:26:09.527976+03:00"` - // Attachments is the list of MMS attachments received with the message - Attachments []MessageAttachment `json:"attachments"` -} -``` - -Update `ToMessageReceiveParams` to convert attachments: - -```go -func (input *MessageReceive) ToMessageReceiveParams(userID entities.UserID, source string) *services.MessageReceiveParams { - phone, _ := phonenumbers.Parse(input.To, phonenumbers.UNKNOWN_REGION) - - attachments := make([]services.ServiceAttachment, len(input.Attachments)) - for i, a := range input.Attachments { - attachments[i] = services.ServiceAttachment{ - Name: a.Name, - ContentType: a.ContentType, - Content: a.Content, - } - } - - return &services.MessageReceiveParams{ - Source: source, - Contact: input.From, - UserID: userID, - Timestamp: input.Timestamp, - Encrypted: input.Encrypted, - Owner: *phone, - Content: input.Content, - SIM: input.SIM, - Attachments: attachments, - } -} -``` - -- [ ] **Step 3: Update MessagePhoneReceivedPayload** - -In `api/pkg/events/message_phone_received_event.go`, add `Attachments` field to the payload struct (after the `SIM` field): - -```go -type MessagePhoneReceivedPayload struct { - MessageID uuid.UUID `json:"message_id"` - UserID entities.UserID `json:"user_id"` - Owner string `json:"owner"` - Encrypted bool `json:"encrypted"` - Contact string `json:"contact"` - Timestamp time.Time `json:"timestamp"` - Content string `json:"content"` - SIM entities.SIM `json:"sim"` - Attachments []string `json:"attachments"` -} -``` - -- [ ] **Step 4: Verify build compiles (will fail until service constructor is updated)** - -Run: `cd api && go vet ./pkg/requests/... ./pkg/events/...` -Expected: No errors in these packages - -- [ ] **Step 5: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add attachment fields to request, params, and event structs" -``` - ---- - -### Task 6: Add attachment validation - -**Files:** - -- Modify: `api/pkg/validators/message_handler_validator.go:49-77` - -- [ ] **Step 1: Update ValidateMessageReceive to validate attachments** - -In `api/pkg/validators/message_handler_validator.go`, replace the `ValidateMessageReceive` method (lines 49-77) with: - -```go -const ( - maxAttachmentCount = 10 - maxAttachmentSize = (3 * 1024 * 1024) / 2 // 1.5 MB -) - -// ValidateMessageReceive validates the requests.MessageReceive request -func (validator MessageHandlerValidator) ValidateMessageReceive(_ context.Context, request requests.MessageReceive) url.Values { - v := govalidator.New(govalidator.Options{ - Data: &request, - Rules: govalidator.MapData{ - "to": []string{ - "required", - phoneNumberRule, - }, - "from": []string{ - "required", - }, - "content": []string{ - "required", - "min:1", - "max:2048", - }, - "sim": []string{ - "required", - "in:" + strings.Join([]string{ - string(entities.SIM1), - string(entities.SIM2), - }, ","), - }, - }, - }) - - errors := v.ValidateStruct() - - if len(request.Attachments) > 0 { - attachmentErrors := validator.validateAttachments(request.Attachments) - for key, values := range attachmentErrors { - for _, value := range values { - errors.Add(key, value) - } - } - } - - return errors -} - -func (validator MessageHandlerValidator) validateAttachments(attachments []requests.MessageAttachment) url.Values { - errors := url.Values{} - allowedTypes := repositories.AllowedContentTypes() - - if len(attachments) > maxAttachmentCount { - errors.Add("attachments", fmt.Sprintf("attachment count [%d] exceeds maximum of [%d]", len(attachments), maxAttachmentCount)) - return errors - } - - for i, attachment := range attachments { - if !allowedTypes[attachment.ContentType] { - errors.Add("attachments", fmt.Sprintf("attachment [%d] has unsupported content type [%s]", i, attachment.ContentType)) - continue - } - - decoded, err := base64.StdEncoding.DecodeString(attachment.Content) - if err != nil { - errors.Add("attachments", fmt.Sprintf("attachment [%d] has invalid base64 content", i)) - continue - } - - if len(decoded) > maxAttachmentSize { - errors.Add("attachments", fmt.Sprintf("attachment [%d] size [%d] exceeds maximum of [%d] bytes", i, len(decoded), maxAttachmentSize)) - } - } - - return errors -} -``` - -Add these imports to the file: `"encoding/base64"`, `"github.com/NdoleStudio/httpsms/pkg/repositories"`. - -- [ ] **Step 2: Verify build** - -Run: `cd api && go vet ./pkg/validators/...` -Expected: No errors - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add attachment count, size, and content-type validation" -``` - ---- - -### Task 7: Upload logic in MessageService.ReceiveMessage() - -**Files:** - -- Modify: `api/pkg/services/message_service.go:22-47` (struct + constructor) -- Modify: `api/pkg/services/message_service.go:302-337` (ReceiveMessage) -- Modify: `api/pkg/services/message_service.go:550-581` (storeReceivedMessage) - -- [ ] **Step 1: Add AttachmentStorage and apiBaseURL to MessageService** - -Update the `MessageService` struct (lines 22-30): - -```go -type MessageService struct { - service - logger telemetry.Logger - tracer telemetry.Tracer - eventDispatcher *EventDispatcher - phoneService *PhoneService - repository repositories.MessageRepository - attachmentStorage repositories.AttachmentStorage - apiBaseURL string -} -``` - -Update `NewMessageService` (lines 33-47) to accept the new parameters: - -```go -func NewMessageService( - logger telemetry.Logger, - tracer telemetry.Tracer, - repository repositories.MessageRepository, - eventDispatcher *EventDispatcher, - phoneService *PhoneService, - attachmentStorage repositories.AttachmentStorage, - apiBaseURL string, -) (s *MessageService) { - return &MessageService{ - logger: logger.WithService(fmt.Sprintf("%T", s)), - tracer: tracer, - repository: repository, - phoneService: phoneService, - eventDispatcher: eventDispatcher, - attachmentStorage: attachmentStorage, - apiBaseURL: apiBaseURL, - } -} -``` - -- [ ] **Step 2: Add the uploadAttachments helper method** - -Add this after `storeReceivedMessage`. Add imports: `"encoding/base64"`, `"golang.org/x/sync/errgroup"`: - -```go -func (service *MessageService) uploadAttachments(ctx context.Context, userID entities.UserID, messageID uuid.UUID, attachments []ServiceAttachment) ([]string, error) { - ctx, span := service.tracer.Start(ctx) - defer span.End() - - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - g, gCtx := errgroup.WithContext(ctx) - urls := make([]string, len(attachments)) - paths := make([]string, len(attachments)) - - for i, attachment := range attachments { - i, attachment := i, attachment - g.Go(func() error { - decoded, err := base64.StdEncoding.DecodeString(attachment.Content) - if err != nil { - return stacktrace.Propagate(err, fmt.Sprintf("cannot decode base64 content for attachment [%d]", i)) - } - - sanitizedName := repositories.SanitizeFilename(attachment.Name, i) - ext := repositories.ExtensionFromContentType(attachment.ContentType) - filename := sanitizedName + ext - - path := fmt.Sprintf("attachments/%s/%s/%d/%s", userID, messageID, i, filename) - paths[i] = path - - if err = service.attachmentStorage.Upload(gCtx, path, decoded); err != nil { - return stacktrace.Propagate(err, fmt.Sprintf("cannot upload attachment [%d] to path [%s]", i, path)) - } - - urls[i] = fmt.Sprintf("%s/v1/attachments/%s/%s/%d/%s", service.apiBaseURL, userID, messageID, i, filename) - ctxLogger.Info(fmt.Sprintf("uploaded attachment [%d] to [%s]", i, path)) - return nil - }) - } - - if err := g.Wait(); err != nil { - for _, path := range paths { - if path != "" { - _ = service.attachmentStorage.Delete(ctx, path) - } - } - return nil, stacktrace.Propagate(err, "cannot upload attachments") - } - - return urls, nil -} -``` - -- [ ] **Step 3: Update ReceiveMessage to upload attachments before event dispatch** - -Replace the `ReceiveMessage` method (lines 302-337): - -```go -func (service *MessageService) ReceiveMessage(ctx context.Context, params *MessageReceiveParams) (*entities.Message, error) { - ctx, span := service.tracer.Start(ctx) - defer span.End() - - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - messageID := uuid.New() - var attachmentURLs []string - - if len(params.Attachments) > 0 { - ctxLogger.Info(fmt.Sprintf("uploading [%d] attachments for message [%s]", len(params.Attachments), messageID)) - var err error - attachmentURLs, err = service.uploadAttachments(ctx, params.UserID, messageID, params.Attachments) - if err != nil { - msg := fmt.Sprintf("cannot upload attachments for message [%s]", messageID) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - } - - eventPayload := events.MessagePhoneReceivedPayload{ - MessageID: messageID, - UserID: params.UserID, - Encrypted: params.Encrypted, - Owner: phonenumbers.Format(¶ms.Owner, phonenumbers.E164), - Contact: params.Contact, - Timestamp: params.Timestamp, - Content: params.Content, - SIM: params.SIM, - Attachments: attachmentURLs, - } - - ctxLogger.Info(fmt.Sprintf("creating cloud event for received with ID [%s]", eventPayload.MessageID)) - - event, err := service.createMessagePhoneReceivedEvent(params.Source, eventPayload) - if err != nil { - msg := fmt.Sprintf("cannot create %T from payload with message id [%s]", event, eventPayload.MessageID) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s]", event.Type(), event.ID(), eventPayload.MessageID)) - - if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - ctxLogger.Info(fmt.Sprintf("event [%s] dispatched successfully", event.ID())) - - return service.storeReceivedMessage(ctx, eventPayload) -} -``` - -- [ ] **Step 4: Update storeReceivedMessage to set Attachments on message** - -In the `storeReceivedMessage` method (lines 550-581), add `Attachments` to the message construction: - -```go - message := &entities.Message{ - ID: params.MessageID, - Owner: params.Owner, - UserID: params.UserID, - Contact: params.Contact, - Content: params.Content, - Attachments: params.Attachments, - SIM: params.SIM, - Encrypted: params.Encrypted, - Type: entities.MessageTypeMobileOriginated, - Status: entities.MessageStatusReceived, - RequestReceivedAt: params.Timestamp, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - OrderTimestamp: params.Timestamp, - ReceivedAt: ¶ms.Timestamp, - } -``` - -- [ ] **Step 5: Verify the services package compiles** - -Run: `cd api && go vet ./pkg/services/...` -Expected: No errors (the full build may still fail until DI container is updated) - -- [ ] **Step 6: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add attachment upload logic to MessageService.ReceiveMessage()" -``` - ---- - -### Task 8: Attachment download handler - -**Files:** - -- Create: `api/pkg/handlers/attachment_handler.go` - -- [ ] **Step 1: Write the handler** - -Create `api/pkg/handlers/attachment_handler.go`: - -```go -package handlers - -import ( - "fmt" - "path/filepath" - - "github.com/NdoleStudio/httpsms/pkg/repositories" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/gofiber/fiber/v2" - "github.com/palantir/stacktrace" -) - -// AttachmentHandler handles attachment download requests -type AttachmentHandler struct { - handler - logger telemetry.Logger - tracer telemetry.Tracer - storage repositories.AttachmentStorage -} - -// NewAttachmentHandler creates a new AttachmentHandler -func NewAttachmentHandler( - logger telemetry.Logger, - tracer telemetry.Tracer, - storage repositories.AttachmentStorage, -) (h *AttachmentHandler) { - return &AttachmentHandler{ - logger: logger.WithService(fmt.Sprintf("%T", h)), - tracer: tracer, - storage: storage, - } -} - -// RegisterRoutes registers the routes for the AttachmentHandler (no auth middleware — public endpoint) -func (h *AttachmentHandler) RegisterRoutes(router fiber.Router) { - router.Get("/v1/attachments/:userID/:messageID/:attachmentIndex/:filename", h.GetAttachment) -} - -// GetAttachment downloads an attachment -// @Summary Download a message attachment -// @Description Download an MMS attachment by its path components -// @Tags Attachments -// @Produce octet-stream -// @Param userID path string true "User ID" -// @Param messageID path string true "Message ID" -// @Param attachmentIndex path string true "Attachment index" -// @Param filename path string true "Filename with extension" -// @Success 200 {file} binary -// @Failure 404 {object} responses.NotFoundResponse -// @Failure 500 {object} responses.InternalServerError -// @Router /attachments/{userID}/{messageID}/{attachmentIndex}/{filename} [get] -func (h *AttachmentHandler) GetAttachment(c *fiber.Ctx) error { - ctx, span := h.tracer.StartFromFiberCtx(c) - defer span.End() - - ctxLogger := h.tracer.CtxLogger(h.logger, span) - - userID := c.Params("userID") - messageID := c.Params("messageID") - attachmentIndex := c.Params("attachmentIndex") - filename := c.Params("filename") - - path := fmt.Sprintf("attachments/%s/%s/%s/%s", userID, messageID, attachmentIndex, filename) - - ctxLogger.Info(fmt.Sprintf("downloading attachment from path [%s]", path)) - - data, err := h.storage.Download(ctx, path) - if err != nil { - msg := fmt.Sprintf("cannot download attachment from path [%s]", path) - ctxLogger.Warn(stacktrace.Propagate(err, msg)) - return h.responseNotFound(c, "attachment not found") - } - - ext := filepath.Ext(filename) - contentType := repositories.ContentTypeFromExtension(ext) - - c.Set("Content-Type", contentType) - c.Set("Content-Disposition", "attachment") - c.Set("X-Content-Type-Options", "nosniff") - - return c.Send(data) -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd api && go vet ./pkg/handlers/...` -Expected: No errors - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat: add AttachmentHandler for downloading attachments" -``` - ---- - -### Task 9: Wire everything in the DI container and env config - -**Files:** - -- Modify: `api/pkg/di/container.go:104-163` (NewContainer) -- Modify: `api/pkg/di/container.go:1424-1434` (MessageService creation) -- Modify: `api/.env.docker` - -- [ ] **Step 1: Add GCS_BUCKET_NAME to .env.docker** - -In `api/.env.docker`, add after the `REDIS_URL=redis://@redis:6379` line (line 49): - -```env - -# Google Cloud Storage bucket for MMS attachments. Leave empty to use in-memory storage. -GCS_BUCKET_NAME= -``` - -- [ ] **Step 2: Add `attachmentStorage` field to Container struct** - -In `api/pkg/di/container.go`, add `attachmentStorage` to the `Container` struct (around line 82-90): - -```go -type Container struct { - projectID string - db *gorm.DB - dedicatedDB *gorm.DB - version string - app *fiber.App - eventDispatcher *services.EventDispatcher - logger telemetry.Logger - attachmentStorage repositories.AttachmentStorage -} -``` - -- [ ] **Step 3: Add AttachmentStorage, APIBaseURL, and AttachmentHandler getters to container.go** - -Add these methods to `api/pkg/di/container.go`. Also add required imports: `"cloud.google.com/go/storage"` and `"context"`: - -```go -// AttachmentStorage creates a cached AttachmentStorage based on configuration -func (container *Container) AttachmentStorage() repositories.AttachmentStorage { - if container.attachmentStorage != nil { - return container.attachmentStorage - } - - bucket := os.Getenv("GCS_BUCKET_NAME") - if bucket != "" { - container.logger.Debug("creating GCSAttachmentStorage") - client, err := storage.NewClient(context.Background()) - if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create GCS client")) - } - container.attachmentStorage = repositories.NewGCSAttachmentStorage( - container.Logger(), - container.Tracer(), - client, - bucket, - ) - } else { - container.logger.Debug("creating MemoryAttachmentStorage (GCS_BUCKET_NAME not set)") - container.attachmentStorage = repositories.NewMemoryAttachmentStorage( - container.Logger(), - container.Tracer(), - ) - } - - return container.attachmentStorage -} - -// APIBaseURL returns the API base URL derived from EVENTS_QUEUE_ENDPOINT -func (container *Container) APIBaseURL() string { - endpoint := os.Getenv("EVENTS_QUEUE_ENDPOINT") - return strings.TrimSuffix(endpoint, "/v1/events") -} - -// AttachmentHandler creates a new AttachmentHandler -func (container *Container) AttachmentHandler() (handler *handlers.AttachmentHandler) { - container.logger.Debug(fmt.Sprintf("creating %T", handler)) - return handlers.NewAttachmentHandler( - container.Logger(), - container.Tracer(), - container.AttachmentStorage(), - ) -} - -// RegisterAttachmentRoutes registers routes for the /attachments prefix -func (container *Container) RegisterAttachmentRoutes() { - container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.AttachmentHandler{})) - container.AttachmentHandler().RegisterRoutes(container.App()) -} -``` - -- [ ] **Step 3: Update MessageService creation to pass new parameters** - -Update the `MessageService()` getter (around line 1424-1434): - -```go -func (container *Container) MessageService() (service *services.MessageService) { - container.logger.Debug(fmt.Sprintf("creating %T", service)) - return services.NewMessageService( - container.Logger(), - container.Tracer(), - container.MessageRepository(), - container.EventDispatcher(), - container.PhoneService(), - container.AttachmentStorage(), - container.APIBaseURL(), - ) -} -``` - -- [ ] **Step 4: Register attachment routes in NewContainer** - -In the `NewContainer` function (lines 104-163), add `container.RegisterAttachmentRoutes()` after `container.RegisterMessageRoutes()` (after line 120): - -```go - container.RegisterMessageRoutes() - container.RegisterAttachmentRoutes() - container.RegisterBulkMessageRoutes() -``` - -- [ ] **Step 5: Verify full build** - -Run: `cd api && go build ./...` -Expected: Build succeeds — all components are now wired - -- [ ] **Step 6: Run all tests** - -Run: `cd api && go test ./...` -Expected: All tests pass - -- [ ] **Step 7: Commit** - -```bash -cd api && git add -A && git commit -m "feat: wire attachment storage and handler in DI container - -- Add AttachmentStorage selection (GCS vs memory) based on GCS_BUCKET_NAME env var -- Wire AttachmentHandler for public download endpoint -- Pass storage and API base URL to MessageService -- Add GCS_BUCKET_NAME to .env.docker" -``` - ---- - -### Task 10: Final verification - -- [ ] **Step 1: Run full build** - -Run: `cd api && go build -o ./tmp/main.exe .` -Expected: Build succeeds - -- [ ] **Step 2: Run all tests** - -Run: `cd api && go test ./... -v` -Expected: All tests pass including `TestExtensionFromContentType` and `TestSanitizeFilename` - -- [ ] **Step 3: Verify go vet** - -Run: `cd api && go vet ./...` -Expected: No issues - -- [ ] **Step 4: Final commit if any remaining changes** - -```bash -cd api && git add -A && git diff --cached --stat -``` diff --git a/docs/superpowers/plans/2026-05-03-integration-test-setup.md b/docs/superpowers/plans/2026-05-03-integration-test-setup.md deleted file mode 100644 index 9a8f8cb64..000000000 --- a/docs/superpowers/plans/2026-05-03-integration-test-setup.md +++ /dev/null @@ -1,1107 +0,0 @@ -# Integration Test Setup Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Create a CI-gated integration test that validates the full SMS send/receive flow using Docker, a phone emulator, and real FCM code paths redirected to the emulator. - -**Architecture:** Docker Compose brings up PostgreSQL + Redis + API + Emulator. The API's Firebase SDK is configured to route FCM traffic to the emulator via a custom HTTP transport. A Go test runner on the host exercises the API and asserts on message state. - -**Tech Stack:** Go, Docker Compose, PostgreSQL, Redis, Firebase Admin Go SDK, GitHub Actions - ---- - -## File Structure - -``` -tests/ -├── docker-compose.yml # orchestrates all services -├── seed.sql # seeds test user, phone, API keys -├── .env.test # API environment config for tests -├── firebase-credentials.json # fake service account JSON -├── go.mod # test runner Go module -├── go.sum -├── integration_test.go # test cases (send SMS, receive SMS) -├── helpers_test.go # HTTP client, polling, constants -└── emulator/ - ├── Dockerfile # builds emulator binary - ├── go.mod # emulator Go module - ├── go.sum - ├── main.go # entry point, HTTP server setup - ├── fcm_handler.go # fake FCM endpoint handler - ├── token_handler.go # fake OAuth2 token endpoint - └── events.go # fires SENT/DELIVERED events to API - -api/pkg/di/container.go # modified: FCM transport redirect -.github/workflows/integration-test.yml # new CI workflow -``` - ---- - -### Task 1: Create Feature Branch - -**Files:** - -- None (git operations only) - -- [ ] **Step 1: Create and switch to feature branch from main** - -```bash -cd C:\Users\Arnold\Work\NdoleStudio\httpsms.com -git checkout main -git pull origin main -git checkout -b feature/integration-tests -``` - -- [ ] **Step 2: Verify branch** - -Run: `git branch --show-current` -Expected: `feature/integration-tests` - ---- - -### Task 2: API Modification — FCM Transport Override - -**Files:** - -- Modify: `api/pkg/di/container.go:396-405` (FirebaseApp method) - -- [ ] **Step 1: Add the FCM redirect transport and modify FirebaseApp** - -In `api/pkg/di/container.go`, modify the `FirebaseApp()` method to check for `FCM_ENDPOINT` env var. When set, use ONLY a custom HTTP client (no credentials). When not set, use credentials as before. - -**Important:** `option.WithHTTPClient()` takes precedence over all other options in the Firebase SDK. Do NOT combine it with `option.WithAuthCredentialsJSON()`. Use one or the other. - -Create a new file `api/pkg/di/fcm_transport.go`: - -```go -package di - -import ( - "net/http" - "net/url" -) - -// fcmRedirectTransport rewrites Firebase SDK HTTP requests to a custom endpoint. -// Used in integration tests to redirect FCM traffic to the emulator. -type fcmRedirectTransport struct { - target *url.URL - base http.RoundTripper -} - -func (t *fcmRedirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.URL.Scheme = t.target.Scheme - req.URL.Host = t.target.Host - return t.base.RoundTrip(req) -} -``` - -Then modify `FirebaseApp()` in `container.go`: - -```go -// FirebaseApp creates a new instance of firebase.App -func (container *Container) FirebaseApp() (app *firebase.App) { - container.logger.Debug(fmt.Sprintf("creating %T", app)) - - var opts []option.ClientOption - - if fcmEndpoint := os.Getenv("FCM_ENDPOINT"); fcmEndpoint != "" { - container.logger.Info(fmt.Sprintf("using FCM endpoint override: %s", fcmEndpoint)) - targetURL, err := url.Parse(fcmEndpoint) - if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot parse FCM_ENDPOINT")) - } - opts = append(opts, option.WithHTTPClient(&http.Client{ - Transport: &fcmRedirectTransport{ - target: targetURL, - base: http.DefaultTransport, - }, - })) - } else { - opts = append(opts, option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials())) - } - - app, err := firebase.NewApp(context.Background(), nil, opts...) - if err != nil { - msg := "cannot initialize firebase application" - container.logger.Fatal(stacktrace.Propagate(err, msg)) - } - return app -} -``` - -- [ ] **Step 2: Add `net/url` import if not already present** - -Ensure the `net/url` package is imported in `container.go` (or the new file). - -- [ ] **Step 3: Verify API still builds** - -Run: `cd api && go build ./...` -Expected: Build succeeds with no errors. - -- [ ] **Step 4: Commit** - -```bash -git add api/pkg/di/ -git commit -m "feat(api): add FCM_ENDPOINT transport override for integration tests" -``` - ---- - -### Task 3: Emulator — Project Scaffolding - -**Files:** - -- Create: `tests/emulator/go.mod` -- Create: `tests/emulator/emulator.go` -- Create: `tests/emulator/Dockerfile` - -Note: `main.go` references `NewEmulator()` and handlers, so we create the struct first. `main.go` is created AFTER all handlers exist (Task 6b). - -- [ ] **Step 1: Initialize emulator Go module** - -```bash -mkdir -p tests/emulator -cd tests/emulator -go mod init github.com/NdoleStudio/httpsms/tests/emulator -``` - -- [ ] **Step 2: Create `tests/emulator/emulator.go`** - -```go -package main - -import "net/http" - -// Emulator acts as a fake Android phone that receives FCM pushes -// and responds with message events. -type Emulator struct { - apiBaseURL string - phoneAPIKey string - httpClient *http.Client -} - -// NewEmulator creates a new Emulator instance. -func NewEmulator(apiBaseURL, phoneAPIKey string) *Emulator { - return &Emulator{ - apiBaseURL: apiBaseURL, - phoneAPIKey: phoneAPIKey, - httpClient: &http.Client{}, - } -} - -// HealthHandler returns 200 OK for health checks. -func (e *Emulator) HealthHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) -} -``` - -- [ ] **Step 3: Create `tests/emulator/Dockerfile`** - -```dockerfile -FROM golang:1.22 AS builder - -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/emulator . - -FROM alpine:latest -RUN apk add --no-cache ca-certificates -COPY --from=builder /bin/emulator /bin/emulator -EXPOSE 9090 -ENTRYPOINT ["/bin/emulator"] -``` - -- [ ] **Step 4: Commit** - -```bash -git add tests/emulator/ -git commit -m "feat(tests): scaffold emulator Go project" -``` - ---- - -### Task 4: Emulator — Token Handler - -**Files:** - -- Create: `tests/emulator/token_handler.go` - -- [ ] **Step 1: Create `tests/emulator/token_handler.go`** - -```go -package main - -import ( - "encoding/json" - "net/http" -) - -// TokenHandler returns a fake OAuth2 access token. -// The Firebase Admin SDK calls this endpoint to get an access token -// before making FCM API calls. -func (e *Emulator) TokenHandler(w http.ResponseWriter, r *http.Request) { - response := map[string]interface{}{ - "access_token": "fake-access-token", - "token_type": "Bearer", - "expires_in": 3600, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add tests/emulator/token_handler.go -git commit -m "feat(tests): add fake OAuth2 token handler to emulator" -``` - ---- - -### Task 5: Emulator — FCM Handler - -**Files:** - -- Create: `tests/emulator/fcm_handler.go` - -- [ ] **Step 1: Create `tests/emulator/fcm_handler.go`** - -```go -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" -) - -// fcmRequest represents the FCM v1 API request body -type fcmRequest struct { - Message struct { - Data map[string]string `json:"data"` - Token string `json:"token"` - Android struct { - Priority string `json:"priority"` - } `json:"android"` - } `json:"message"` -} - -// fcmResponse represents the FCM v1 API response -type fcmResponse struct { - Name string `json:"name"` -} - -// FCMHandler handles fake FCM send requests from the Firebase Admin SDK. -func (e *Emulator) FCMHandler(w http.ResponseWriter, r *http.Request) { - var req fcmRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - - messageID := req.Message.Data["KEY_MESSAGE_ID"] - if messageID == "" { - http.Error(w, "missing KEY_MESSAGE_ID in data", http.StatusBadRequest) - return - } - - log.Printf("received FCM push for message: %s", messageID) - - // Respond with success immediately (like real FCM would) - resp := fcmResponse{ - Name: fmt.Sprintf("projects/httpsms-test/messages/fake-%s", messageID), - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - - // Process the message asynchronously (like a real phone would) - go e.processMessage(messageID) -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add tests/emulator/emulator.go tests/emulator/fcm_handler.go -git commit -m "feat(tests): add FCM handler to emulator" -``` - ---- - -### Task 6: Emulator — Event Firing - -**Files:** - -- Create: `tests/emulator/events.go` - -- [ ] **Step 1: Create `tests/emulator/events.go`** - -```go -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "time" -) - -// messageEvent is the payload for posting a message event to the API -type messageEvent struct { - Timestamp time.Time `json:"timestamp"` - EventName string `json:"event_name"` -} - -// processMessage simulates a phone receiving an FCM push and sending the SMS. -// It calls /messages/outstanding, then fires SENT and DELIVERED events. -func (e *Emulator) processMessage(messageID string) { - // Step 1: Fetch outstanding message (like real phone does) - e.fetchOutstanding(messageID) - - // Step 2: Wait briefly then fire SENT - time.Sleep(200 * time.Millisecond) - if err := e.fireEvent(messageID, "SENT"); err != nil { - log.Printf("error firing SENT event for message %s: %v", messageID, err) - return - } - - // Step 3: Wait briefly then fire DELIVERED - time.Sleep(200 * time.Millisecond) - if err := e.fireEvent(messageID, "DELIVERED"); err != nil { - log.Printf("error firing DELIVERED event for message %s: %v", messageID, err) - return - } - - log.Printf("completed processing message: %s", messageID) -} - -// fetchOutstanding calls GET /v1/messages/outstanding to mimic the real phone behavior -func (e *Emulator) fetchOutstanding(messageID string) { - url := fmt.Sprintf("%s/v1/messages/outstanding?message_id=%s", e.apiBaseURL, messageID) - - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("x-api-key", e.phoneAPIKey) - - resp, err := e.httpClient.Do(req) - if err != nil { - log.Printf("error fetching outstanding message %s: %v", messageID, err) - return - } - defer resp.Body.Close() - log.Printf("fetched outstanding message %s: status %d", messageID, resp.StatusCode) -} - -// fireEvent posts a message event (SENT or DELIVERED) to the API -func (e *Emulator) fireEvent(messageID, eventName string) error { - url := fmt.Sprintf("%s/v1/messages/%s/events", e.apiBaseURL, messageID) - - event := messageEvent{ - Timestamp: time.Now().UTC(), - EventName: eventName, - } - - body, _ := json.Marshal(event) - req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("x-api-key", e.phoneAPIKey) - - resp, err := e.httpClient.Do(req) - if err != nil { - return fmt.Errorf("HTTP error: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return fmt.Errorf("API returned status %d for %s event", resp.StatusCode, eventName) - } - - log.Printf("fired %s event for message %s: status %d", eventName, messageID, resp.StatusCode) - return nil -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add tests/emulator/events.go -git commit -m "feat(tests): add event firing to emulator" -``` - ---- - -### Task 6b: Emulator — Main Entry Point - -**Files:** - -- Create: `tests/emulator/main.go` - -- [ ] **Step 1: Create `tests/emulator/main.go`** - -Now that all handlers exist (HealthHandler, TokenHandler, FCMHandler), create the entry point: - -```go -package main - -import ( - "log" - "net/http" - "os" -) - -func main() { - apiBaseURL := os.Getenv("API_BASE_URL") - if apiBaseURL == "" { - apiBaseURL = "http://api:8000" - } - - phoneAPIKey := os.Getenv("PHONE_API_KEY") - if phoneAPIKey == "" { - phoneAPIKey = "pk_test-phone-api-key" - } - - emulator := NewEmulator(apiBaseURL, phoneAPIKey) - - mux := http.NewServeMux() - mux.HandleFunc("GET /health", emulator.HealthHandler) - mux.HandleFunc("POST /token", emulator.TokenHandler) - mux.HandleFunc("POST /v1/projects/{project}/messages:send", emulator.FCMHandler) - - port := os.Getenv("PORT") - if port == "" { - port = "9090" - } - - log.Printf("emulator listening on :%s", port) - if err := http.ListenAndServe(":"+port, mux); err != nil { - log.Fatalf("server error: %v", err) - } -} -``` - -- [ ] **Step 2: Verify emulator builds** - -```bash -cd tests/emulator -go build ./... -``` - -Expected: Build succeeds. - -- [ ] **Step 3: Commit** - -```bash -git add tests/emulator/main.go -git commit -m "feat(tests): add emulator main entry point" -``` - ---- - -### Task 7: Test Infrastructure — Seed Data & Config - -**Files:** - -- Create: `tests/seed.sql` -- Create: `tests/.env.test` -- Create: `tests/firebase-credentials.json` - -- [ ] **Step 1: Create `tests/seed.sql`** - -This script must match the exact table schema from entities. The tables are auto-migrated by GORM, so we insert after API startup. Actually — since we need the user to exist BEFORE the API processes requests, we seed via Docker's postgres init scripts. - -Note: GORM auto-migrates tables on API startup. The seed SQL runs AFTER table creation. We use a Docker healthcheck + depends_on to ensure ordering. Alternatively, we can use a startup script that waits for the API to be ready, then seeds. The simplest approach: mount `seed.sql` as a Postgres init script — but that runs before GORM migrates. - -**Better approach:** Create a `tests/seed.sh` script that waits for the API to start (which runs GORM migrations), then seeds the database via `psql`. - -```sql --- tests/seed.sql --- Seed test data for integration tests --- Run AFTER GORM has migrated the schema (i.e., after API starts) - --- Test user -INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) -VALUES ( - 'test-user-id', - 'test@httpsms.com', - 'test-user-api-key', - 'UTC', - 'pro-monthly', - NOW(), - NOW() -) ON CONFLICT (id) DO NOTHING; - --- System user (for event queue auth) -INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) -VALUES ( - 'system-user-id', - 'system@httpsms.com', - 'system-user-api-key', - 'UTC', - 'pro-monthly', - NOW(), - NOW() -) ON CONFLICT (id) DO NOTHING; - --- Test phone -INSERT INTO phones (id, user_id, fcm_token, phone_number, messages_per_minute, sim, max_send_attempts, message_expiration_seconds, created_at, updated_at) -VALUES ( - 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - 'test-user-id', - 'fake-fcm-token', - '+18005550199', - 60, - 'SIM1', - 2, - 600, - NOW(), - NOW() -) ON CONFLICT (id) DO NOTHING; - --- Phone API key (for emulator to authenticate as phone) -INSERT INTO phone_api_keys (id, name, user_id, user_email, phone_numbers, phone_ids, api_key, created_at, updated_at) -VALUES ( - 'b2c3d4e5-f6a7-8901-bcde-f12345678901', - 'Integration Test Phone Key', - 'test-user-id', - 'test@httpsms.com', - '{"+18005550199"}', - '{"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}', - 'pk_test-phone-api-key', - NOW(), - NOW() -) ON CONFLICT (id) DO NOTHING; -``` - -- [ ] **Step 2: Create `tests/.env.test`** - -```env -ENV=production -GCP_PROJECT_ID=httpsms-test -USE_HTTP_LOGGER=true -ENTITLEMENT_ENABLED=false -EVENTS_QUEUE_TYPE=emulator -EVENTS_QUEUE_NAME=events-local -EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events -EVENTS_QUEUE_USER_API_KEY=system-user-api-key -EVENTS_QUEUE_USER_ID=system-user-id -FCM_ENDPOINT=http://emulator:9090 -DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms -DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms -REDIS_URL=redis://@redis:6379 -APP_PORT=8000 -APP_NAME=httpSMS -APP_URL=http://localhost:8000 -SWAGGER_HOST=localhost:8000 -SMTP_FROM_NAME=httpSMS -SMTP_FROM_EMAIL=test@httpsms.com -SMTP_USERNAME= -SMTP_PASSWORD= -SMTP_HOST=localhost -SMTP_PORT=2525 -PUSHER_APP_ID= -PUSHER_KEY= -PUSHER_SECRET= -PUSHER_CLUSTER= -GCS_BUCKET_NAME= -UPTRACE_DSN= -CLOUDFLARE_TURNSTILE_SECRET_KEY= -``` - -- [ ] **Step 3: Create `tests/firebase-credentials.json`** - -Generate an RSA private key for the fake service account. This must be a valid RSA key so the Firebase SDK can sign JWT tokens (even though the emulator won't validate them). - -```bash -cd tests -openssl genrsa -out /tmp/test-key.pem 2048 -``` - -Then create the JSON file with the key embedded: - -```json -{ - "type": "service_account", - "project_id": "httpsms-test", - "private_key_id": "test-key-id", - "private_key": "", - "client_email": "test@httpsms-test.iam.gserviceaccount.com", - "client_id": "123456789", - "auth_uri": "http://emulator:9090/auth", - "token_uri": "http://emulator:9090/token", - "auth_provider_x509_cert_url": "http://emulator:9090/certs", - "client_x509_cert_url": "http://emulator:9090/certs/test" -} -``` - -Note: The `FIREBASE_CREDENTIALS` env var in `.env.test` should be set to the full contents of this JSON file (single-line). The docker-compose will handle this. - -- [ ] **Step 4: Commit** - -```bash -git add tests/seed.sql tests/.env.test tests/firebase-credentials.json -git commit -m "feat(tests): add seed data and test environment config" -``` - ---- - -### Task 8: Docker Compose for Tests - -**Files:** - -- Create: `tests/docker-compose.yml` - -- [ ] **Step 1: Create `tests/docker-compose.yml`** - -```yaml -services: - postgres: - image: postgres:alpine - environment: - POSTGRES_DB: httpsms - POSTGRES_PASSWORD: dbpassword - POSTGRES_USER: dbusername - ports: - - "5435:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U dbusername -d httpsms"] - interval: 5s - timeout: 5s - retries: 10 - start_period: 5s - - redis: - image: redis:latest - command: redis-server - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 10 - - emulator: - build: - context: ./emulator - ports: - - "9090:9090" - environment: - API_BASE_URL: http://api:8000 - PHONE_API_KEY: pk_test-phone-api-key - PORT: "9090" - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/health"] - interval: 5s - timeout: 5s - retries: 10 - - api: - build: - context: ../api - ports: - - "8000:8000" - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - emulator: - condition: service_healthy - env_file: - - .env.test - environment: - FIREBASE_CREDENTIALS: "${FIREBASE_CREDENTIALS}" - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/"] - interval: 5s - timeout: 10s - retries: 20 - start_period: 30s - - seed: - image: postgres:alpine - depends_on: - api: - condition: service_healthy - environment: - PGPASSWORD: dbpassword - volumes: - - ./seed.sql:/seed.sql:ro - entrypoint: - [ - "psql", - "-h", - "postgres", - "-U", - "dbusername", - "-d", - "httpsms", - "-f", - "/seed.sql", - ] - restart: "no" -``` - -- [ ] **Step 2: Commit** - -```bash -git add tests/docker-compose.yml -git commit -m "feat(tests): add docker-compose for integration test stack" -``` - ---- - -### Task 9: Test Runner — Go Module & Helpers - -**Files:** - -- Create: `tests/go.mod` -- Create: `tests/helpers_test.go` - -- [ ] **Step 1: Initialize test runner Go module** - -```bash -cd tests -go mod init github.com/NdoleStudio/httpsms/tests -go get github.com/stretchr/testify -``` - -- [ ] **Step 2: Create `tests/helpers_test.go`** - -```go -package tests - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -const ( - apiBaseURL = "http://localhost:8000" - userAPIKey = "test-user-api-key" - phoneAPIKey = "pk_test-phone-api-key" - testPhone = "+18005550199" - testContact = "+18005550100" -) - -// apiClient returns an HTTP client configured for API calls -func apiClient() *http.Client { - return &http.Client{Timeout: 10 * time.Second} -} - -// doRequest performs an HTTP request with the given API key -func doRequest(t *testing.T, method, url string, body io.Reader, apiKey string) *http.Response { - t.Helper() - req, err := http.NewRequest(method, url, body) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("x-api-key", apiKey) - - resp, err := apiClient().Do(req) - require.NoError(t, err) - return resp -} - -// pollMessageStatus polls GET /v1/messages/{id} until the message reaches the target status or times out -func pollMessageStatus(t *testing.T, messageID, targetStatus string, timeout time.Duration) map[string]interface{} { - t.Helper() - deadline := time.Now().Add(timeout) - - for time.Now().Before(deadline) { - url := fmt.Sprintf("%s/v1/messages/%s", apiBaseURL, messageID) - resp := doRequest(t, "GET", url, nil, userAPIKey) - - body, err := io.ReadAll(resp.Body) - resp.Body.Close() - require.NoError(t, err) - - if resp.StatusCode == http.StatusOK { - var result map[string]interface{} - require.NoError(t, json.Unmarshal(body, &result)) - - data, ok := result["data"].(map[string]interface{}) - if ok && data["status"] == targetStatus { - return data - } - } - - time.Sleep(200 * time.Millisecond) - } - - t.Fatalf("message %s did not reach status %q within %v", messageID, targetStatus, timeout) - return nil -} -``` - -- [ ] **Step 3: Commit** - -```bash -git add tests/go.mod tests/go.sum tests/helpers_test.go -git commit -m "feat(tests): add test runner module and helpers" -``` - ---- - -### Task 10: Test Runner — Integration Tests - -**Files:** - -- Create: `tests/integration_test.go` - -- [ ] **Step 1: Create `tests/integration_test.go`** - -```go -package tests - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSendSMS_E2E(t *testing.T) { - // Step 1: Send an SMS via the API - sendPayload := map[string]interface{}{ - "from": testPhone, - "to": testContact, - "content": "Hello from integration test", - } - body, _ := json.Marshal(sendPayload) - - url := fmt.Sprintf("%s/v1/messages/send", apiBaseURL) - resp := doRequest(t, "POST", url, bytes.NewReader(body), userAPIKey) - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode, "send response: %s", string(respBody)) - - // Step 2: Extract message ID - var sendResult map[string]interface{} - require.NoError(t, json.Unmarshal(respBody, &sendResult)) - data := sendResult["data"].(map[string]interface{}) - messageID := data["id"].(string) - require.NotEmpty(t, messageID) - - t.Logf("sent message with ID: %s", messageID) - - // Step 3: Poll until message is delivered - message := pollMessageStatus(t, messageID, "delivered", 15*time.Second) - - // Step 4: Assert final state - assert.Equal(t, "delivered", message["status"]) - assert.Equal(t, testPhone, message["owner"]) - assert.Equal(t, testContact, message["contact"]) - assert.Equal(t, "Hello from integration test", message["content"]) -} - -func TestReceiveSMS_E2E(t *testing.T) { - // Step 1: Simulate receiving an SMS (phone -> API) - receivePayload := map[string]interface{}{ - "from": testContact, - "to": testPhone, - "content": "Hi there from integration test", - "encrypted": false, - "sim": "SIM1", - "timestamp": time.Now().UTC().Format(time.RFC3339), - } - body, _ := json.Marshal(receivePayload) - - url := fmt.Sprintf("%s/v1/messages/receive", apiBaseURL) - resp := doRequest(t, "POST", url, bytes.NewReader(body), phoneAPIKey) - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode, "receive response: %s", string(respBody)) - - // Step 2: Extract message ID - var receiveResult map[string]interface{} - require.NoError(t, json.Unmarshal(respBody, &receiveResult)) - data := receiveResult["data"].(map[string]interface{}) - messageID := data["id"].(string) - require.NotEmpty(t, messageID) - - t.Logf("received message with ID: %s", messageID) - - // Step 3: Verify message exists via GET - getURL := fmt.Sprintf("%s/v1/messages/%s", apiBaseURL, messageID) - getResp := doRequest(t, "GET", getURL, nil, userAPIKey) - defer getResp.Body.Close() - - getBody, err := io.ReadAll(getResp.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, getResp.StatusCode) - - var getMessage map[string]interface{} - require.NoError(t, json.Unmarshal(getBody, &getMessage)) - messageData := getMessage["data"].(map[string]interface{}) - - // Step 4: Assert message fields - assert.Equal(t, "received", messageData["status"]) - assert.Equal(t, testPhone, messageData["owner"]) - assert.Equal(t, testContact, messageData["contact"]) - assert.Equal(t, "Hi there from integration test", messageData["content"]) -} -``` - -- [ ] **Step 2: Verify test file compiles** - -```bash -cd tests -go vet ./... -``` - -Expected: No errors (tests won't pass yet without the stack running). - -- [ ] **Step 3: Commit** - -```bash -git add tests/integration_test.go -git commit -m "feat(tests): add send and receive SMS integration tests" -``` - ---- - -### Task 11: GitHub Actions Workflow - -**Files:** - -- Create: `.github/workflows/integration-test.yml` - -- [ ] **Step 1: Create `.github/workflows/integration-test.yml`** - -```yaml -name: integration-test - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - integration-test: - runs-on: ubuntu-latest - steps: - - name: Checkout 🛎 - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" - - - name: Load Firebase credentials - run: | - echo "FIREBASE_CREDENTIALS=$(jq -c . tests/firebase-credentials.json)" >> $GITHUB_ENV - - - name: Start services 🐳 - working-directory: ./tests - run: docker compose up -d --build --wait - - - name: Wait for seed to complete - working-directory: ./tests - run: | - echo "Waiting for seed container to finish..." - docker compose wait seed || true - sleep 2 - - - name: Run integration tests 🧪 - working-directory: ./tests - run: go test -v -timeout 120s ./... - - - name: Collect logs on failure 📋 - if: failure() - working-directory: ./tests - run: | - docker compose logs api - docker compose logs emulator - - - name: Stop services 🛑 - if: always() - working-directory: ./tests - run: docker compose down -v -``` - -- [ ] **Step 2: Commit** - -```bash -git add .github/workflows/integration-test.yml -git commit -m "ci: add integration test workflow" -``` - ---- - -### Task 12: Local End-to-End Verification - -**Files:** - -- None (verification only) - -- [ ] **Step 1: Generate the fake Firebase credentials file** - -```bash -cd tests -openssl genrsa 2048 > /tmp/test-key.pem -# Create firebase-credentials.json with the key (use a script or manually format) -``` - -- [ ] **Step 2: Build and start the stack** - -```bash -cd tests -export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) -docker compose up -d --build -``` - -- [ ] **Step 3: Wait for all services to be healthy** - -```bash -docker compose ps -# All services should show "healthy" or "exited (0)" for seed -``` - -- [ ] **Step 4: Run the tests** - -```bash -cd tests -go test -v -timeout 120s ./... -``` - -Expected: Both tests pass. - -- [ ] **Step 5: Tear down** - -```bash -docker compose down -v -``` - -- [ ] **Step 6: Push branch and create PR** - -```bash -git push -u origin feature/integration-tests -gh pr create --title "feat: add integration test setup for API" --body "Adds E2E integration tests that validate the full SMS send/receive flow using Docker and a phone emulator." -``` diff --git a/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md b/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md deleted file mode 100644 index f92b60ed0..000000000 --- a/docs/superpowers/plans/2026-05-03-scheduling-send-refactor.md +++ /dev/null @@ -1,956 +0,0 @@ -# Scheduling Send Refactor Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow users to send SMS at an exact time (bypassing scheduling) when `SendAt` is specified, and replace the 1-second bulk hack with rate-based dispatch delays. - -**Related docs:** - -- [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) — the existing `SendAt`/`SendTime` feature -- [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) — the existing `MessagesPerMinute` rate-limiting feature - -**Architecture:** Add a transient `ExactSendTime` flag flowing through the event system. When true, bypass [rate-limit](https://docs.httpsms.com/features/control-sms-send-rate) and schedule window logic in notification scheduling. For bulk sends without explicit time, compute dispatch delay from `MessagesPerMinute` per-phone instead of hardcoded 1s. - -**Tech Stack:** Go, Fiber, GORM, CockroachDB, Google Cloud Tasks (CloudEvents) - -**Spec:** `docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md` - -**Build/Test commands:** - -```bash -cd api && go build ./... -cd api && go test -vet=off ./... -``` - ---- - -## Task 1: Add ExactSendTime to Event Payload - -**Files:** - -- Modify: `api/pkg/events/message_api_sent_event.go` - -- [ ] **Step 1: Add `ExactSendTime` field to `MessageAPISentPayload`** - -In `api/pkg/events/message_api_sent_event.go`, add to the struct: - -```go -ExactSendTime bool `json:"exact_send_time"` -``` - -Add it after line 22 (`ScheduledSendTime *time.Time`). - -- [ ] **Step 2: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat(events): add ExactSendTime field to MessageAPISentPayload" -``` - ---- - -## Task 2: Add Index and ExactSendTime to MessageSendParams + Update getSendDelay - -**Files:** - -- Modify: `api/pkg/services/message_service.go` - -- [ ] **Step 1: Add `Index` field to `MessageSendParams`** - -In `api/pkg/services/message_service.go` at line ~453, add `Index int` to the struct: - -```go -type MessageSendParams struct { - Owner *phonenumbers.PhoneNumber - Contact string - Encrypted bool - Content string - Attachments []string - Source string - SendAt *time.Time - RequestID *string - UserID entities.UserID - RequestReceivedAt time.Time - Index int -} -``` - -- [ ] **Step 2: Update `phoneSettings` to also return `MessagesPerMinute`** - -Change the `phoneSettings` method signature and body at line ~1014: - -```go -func (service *MessageService) phoneSettings(ctx context.Context, userID entities.UserID, owner string) (uint, entities.SIM, uint) { - ctx, span := service.tracer.Start(ctx) - defer span.End() - - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - phone, err := service.phoneService.Load(ctx, userID, owner) - if err != nil { - msg := fmt.Sprintf("cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner) - ctxLogger.Error(stacktrace.Propagate(err, msg)) - return 2, entities.SIM1, 0 - } - - return phone.MaxSendAttemptsSanitized(), phone.SIM, phone.MessagesPerMinute -} -``` - -- [ ] **Step 3: Update `SendMessage` to use new `phoneSettings` return value and set `ExactSendTime`** - -Update `SendMessage` at line ~467. Key changes: get `messagesPerMinute` from `phoneSettings`, derive `ExactSendTime` from `SendAt != nil`, pass `messagesPerMinute` to `getSendDelay`: - -```go -func (service *MessageService) SendMessage(ctx context.Context, params MessageSendParams) (*entities.Message, error) { - ctx, span := service.tracer.Start(ctx) - defer span.End() - - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - sendAttempts, sim, messagesPerMinute := service.phoneSettings(ctx, params.UserID, phonenumbers.Format(params.Owner, phonenumbers.E164)) - - eventPayload := events.MessageAPISentPayload{ - MessageID: uuid.New(), - UserID: params.UserID, - Encrypted: params.Encrypted, - MaxSendAttempts: sendAttempts, - RequestID: params.RequestID, - Owner: phonenumbers.Format(params.Owner, phonenumbers.E164), - Contact: params.Contact, - RequestReceivedAt: params.RequestReceivedAt, - Content: params.Content, - Attachments: params.Attachments, - ScheduledSendTime: params.SendAt, - ExactSendTime: params.SendAt != nil, - SIM: sim, - } - - event, err := service.createMessageAPISentEvent(params.Source, eventPayload) - if err != nil { - msg := fmt.Sprintf("cannot create %T from payload with message id [%s]", event, eventPayload.MessageID) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s] and user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID)) - - message, err := service.storeSentMessage(ctx, eventPayload) - if err != nil { - msg := fmt.Sprintf("cannot store message with id [%s]", eventPayload.MessageID) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - timeout := service.getSendDelay(ctxLogger, eventPayload, params, messagesPerMinute) - if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, timeout); err != nil { - msg := fmt.Sprintf("cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()) - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s] and delay [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID, timeout)) - return message, err -} -``` - -- [ ] **Step 4: Rewrite `getSendDelay` to handle rate-based delay** - -Replace the existing `getSendDelay` method. New signature takes `messagesPerMinute` as a separate arg: - -```go -func (service *MessageService) getSendDelay(ctxLogger telemetry.Logger, eventPayload events.MessageAPISentPayload, params MessageSendParams, messagesPerMinute uint) time.Duration { - // Exact send time: delay until that time (clamped to 0 if in the past) - if params.SendAt != nil { - delay := params.SendAt.Sub(time.Now().UTC()) - if delay < 0 { - ctxLogger.Info(fmt.Sprintf("message [%s] has send time [%s] in the past. sending immediately", eventPayload.MessageID, params.SendAt.String())) - return time.Duration(0) - } - return delay - } - - // Rate-based delay for bulk messages (Index > 0) - if params.Index > 0 && messagesPerMinute > 0 { - interval := time.Minute / time.Duration(messagesPerMinute) - delay := time.Duration(params.Index) * interval - ctxLogger.Info(fmt.Sprintf("message [%s] bulk index [%d] rate-based delay [%s]", eventPayload.MessageID, params.Index, delay)) - return delay - } - - return time.Duration(0) -} -``` - -- [ ] **Step 5: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 6: Run tests** - -Run: `cd api && go test -vet=off ./...` -Expected: all pass - -- [ ] **Step 7: Commit** - -```bash -cd api && git add -A && git commit -m "feat(services): add rate-based dispatch delay and ExactSendTime to SendMessage" -``` - ---- - -## Task 3: Add ScheduleExact to Repository Interface and Implementation - -**Files:** - -- Modify: `api/pkg/repositories/phone_notification_repository.go` -- Modify: `api/pkg/repositories/gorm_phone_notification_repository.go` - -- [ ] **Step 1: Add `ScheduleExact` to the repository interface** - -In `api/pkg/repositories/phone_notification_repository.go`: - -```go -// PhoneNotificationRepository loads and persists an entities.PhoneNotification -type PhoneNotificationRepository interface { - // Schedule a new entities.PhoneNotification - Schedule(ctx context.Context, messagesPerMinute uint, schedule *entities.MessageSendSchedule, notification *entities.PhoneNotification) error - - // ScheduleExact stores a phone notification with a fixed ScheduledAt time, - // bypassing rate-limit and schedule window logic. - ScheduleExact(ctx context.Context, notification *entities.PhoneNotification) error - - // UpdateStatus of a notification - UpdateStatus(ctx context.Context, notificationID uuid.UUID, status entities.PhoneNotificationStatus) error - - // DeleteAllForUser deletes all entities.PhoneNotification for a user - DeleteAllForUser(ctx context.Context, userID entities.UserID) error -} -``` - -- [ ] **Step 2: Implement `ScheduleExact` on `gormPhoneNotificationRepository`** - -In `api/pkg/repositories/gorm_phone_notification_repository.go`, add after the `Schedule` method: - -```go -// ScheduleExact stores a phone notification with an exact ScheduledAt time. -// It performs a dedupe check — if a pending notification for the same message already exists, it's a no-op. -func (repository *gormPhoneNotificationRepository) ScheduleExact( - ctx context.Context, - notification *entities.PhoneNotification, -) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - // Dedupe: check if a pending notification for this message already exists - var count int64 - if err := repository.db.WithContext(ctx). - Model(&entities.PhoneNotification{}). - Where("message_id = ? AND status = ?", notification.MessageID, entities.PhoneNotificationStatusPending). - Count(&count).Error; err != nil { - return repository.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot check for existing notification for message [%s]", notification.MessageID), - ) - } - - if count > 0 { - return nil - } - - if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil { - return repository.tracer.WrapErrorSpan( - span, - stacktrace.Propagate(err, "cannot create exact-time notification with id [%s]", notification.ID), - ) - } - - return nil -} -``` - -- [ ] **Step 3: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 4: Commit** - -```bash -cd api && git add -A && git commit -m "feat(repositories): add ScheduleExact method for exact-time notifications" -``` - ---- - -## Task 4: Update PhoneNotificationService to Support ExactSendTime - -**Files:** - -- Modify: `api/pkg/services/phone_notification_service.go` - -- [ ] **Step 1: Add fields to `PhoneNotificationScheduleParams`** - -Update the struct at line ~162: - -```go -// PhoneNotificationScheduleParams are parameters for sending a notification -type PhoneNotificationScheduleParams struct { - UserID entities.UserID - Owner string - Source string - Encrypted bool - Contact string - Content string - SIM entities.SIM - MessageID uuid.UUID - ExactSendTime bool - ScheduledSendTime *time.Time -} -``` - -- [ ] **Step 2: Add bypass logic at the start of `Schedule` method** - -Update `Schedule` method at line ~175. Add the bypass path after loading the phone: - -```go -// Schedule a notification to be sent to a phone -func (service *PhoneNotificationService) Schedule(ctx context.Context, params *PhoneNotificationScheduleParams) error { - ctx, span := service.tracer.Start(ctx) - defer span.End() - - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - phone, err := service.phoneRepository.Load(ctx, params.UserID, params.Owner) - if err != nil { - msg := fmt.Sprintf("cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - notification := &entities.PhoneNotification{ - ID: uuid.New(), - MessageID: params.MessageID, - UserID: params.UserID, - PhoneID: phone.ID, - Status: entities.PhoneNotificationStatusPending, - ScheduledAt: time.Now().UTC(), - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), - } - - // Bypass rate-limit and schedule window logic for exact send time - if params.ExactSendTime && params.ScheduledSendTime != nil { - scheduledAt := *params.ScheduledSendTime - // Clamp past times to now (send immediately) - if scheduledAt.Before(time.Now().UTC()) { - scheduledAt = time.Now().UTC() - } - notification.ScheduledAt = scheduledAt - if err = service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { - msg := fmt.Sprintf("cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { - ctxLogger.Error(err) - } - - if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { - return service.tracer.WrapErrorSpan(span, err) - } - - ctxLogger.Info(fmt.Sprintf( - "message with id [%s] exact notification scheduled for [%s] with id [%s]", - params.MessageID, - notification.ScheduledAt, - notification.ID, - )) - return nil - } - - // Standard path: apply rate-limit + schedule window logic - var schedule *entities.MessageSendSchedule - if phone.ScheduleID != nil { - schedule, err = service.sendScheduleRepository.Load(ctx, params.UserID, *phone.ScheduleID) - if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { - schedule = nil - err = nil - } - if err != nil { - msg := fmt.Sprintf("cannot load send schedule [%s] for phone [%s]", *phone.ScheduleID, phone.ID) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - } - - if err = service.phoneNotificationRepository.Schedule(ctx, phone.MessagesPerMinute, schedule, notification); err != nil { - msg := fmt.Sprintf("cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID) - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { - ctxLogger.Error(err) - } - - if err = service.dispatchMessageNotificationSend(ctx, params.Source, notification); err != nil { - return service.tracer.WrapErrorSpan(span, err) - } - - ctxLogger.Info(fmt.Sprintf( - "message with id [%s] notification scheduled for [%s] with id [%s]", - params.MessageID, - notification.ScheduledAt, - notification.ID, - )) - return nil -} -``` - -- [ ] **Step 3: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 4: Commit** - -```bash -cd api && git add -A && git commit -m "feat(services): add ExactSendTime bypass in PhoneNotificationService.Schedule" -``` - ---- - -## Task 5: Update Phone Notification Listener to Pass ExactSendTime - -**Files:** - -- Modify: `api/pkg/listeners/phone_notification_listener.go` - -- [ ] **Step 1: Pass ExactSendTime and ScheduledSendTime from event payload to service params** - -Update the `onMessageAPISent` method at line ~44: - -```go -func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context, event cloudevents.Event) error { - ctx, span := listener.tracer.Start(ctx) - defer span.End() - - var payload events.MessageAPISentPayload - if err := event.DataAs(&payload); err != nil { - msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - sendParams := &services.PhoneNotificationScheduleParams{ - UserID: payload.UserID, - Owner: payload.Owner, - Contact: payload.Contact, - Content: payload.Content, - SIM: payload.SIM, - Encrypted: payload.Encrypted, - Source: event.Source(), - MessageID: payload.MessageID, - ExactSendTime: payload.ExactSendTime, - ScheduledSendTime: payload.ScheduledSendTime, - } - - if err := listener.service.Schedule(ctx, sendParams); err != nil { - msg := fmt.Sprintf("cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()) - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} -``` - -- [ ] **Step 2: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "feat(listeners): pass ExactSendTime to PhoneNotificationService from event" -``` - ---- - -## Task 6: Update Bulk Send Request + Handler - -**Files:** - -- Modify: `api/pkg/requests/message_bulk_send_request.go` -- Modify: `api/pkg/handlers/message_handler.go` - -- [ ] **Step 1: Remove per-index SendAt from `MessageBulkSend.ToMessageSendParams()`** - -In `api/pkg/requests/message_bulk_send_request.go`, update `ToMessageSendParams`: - -```go -// ToMessageSendParams converts MessageSend to services.MessageSendParams -func (input *MessageBulkSend) ToMessageSendParams(userID entities.UserID, source string) []services.MessageSendParams { - from, _ := phonenumbers.Parse(input.From, phonenumbers.UNKNOWN_REGION) - - var result []services.MessageSendParams - for index, to := range input.To { - result = append(result, services.MessageSendParams{ - Source: source, - Owner: from, - Encrypted: input.Encrypted, - RequestID: input.sanitizeStringPointer(input.RequestID), - UserID: userID, - RequestReceivedAt: time.Now().UTC(), - Contact: to, - Content: input.Content, - Attachments: input.Attachments, - Index: index, - }) - } - - return result -} -``` - -Key changes: removed `SendAt` assignment and added `Index: index`. - -- [ ] **Step 2: Remove the `index * 1s` hack from `BulkSend` handler** - -In `api/pkg/handlers/message_handler.go`, update the `BulkSend` handler goroutine (around line 160-175). Remove the `if message.SendAt == nil` block: - -Replace: - -```go -for index, message := range params { - wg.Add(1) - go func(message services.MessageSendParams, index int) { - count.Add(1) - if message.SendAt == nil { - sentAt := time.Now().UTC().Add(time.Duration(index) * time.Second) - message.SendAt = &sentAt - } - - response, err := h.service.SendMessage(ctx, message) -``` - -With: - -```go -for index, message := range params { - wg.Add(1) - go func(message services.MessageSendParams, index int) { - count.Add(1) - response, err := h.service.SendMessage(ctx, message) -``` - -- [ ] **Step 3: Remove unused `time` import if needed** - -Check if `time` is still used in `message_handler.go`. It likely is (used elsewhere), so skip this step if so. - -- [ ] **Step 4: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 5: Commit** - -```bash -cd api && git add -A && git commit -m "feat(handlers): replace 1s hack with rate-based delay for bulk send" -``` - ---- - -## Task 7: Update CSV Bulk Message Request + Handler - -**Files:** - -- Modify: `api/pkg/requests/bulk_message_request.go` -- Modify: `api/pkg/handlers/bulk_message_handler.go` - -- [ ] **Step 1: Add `Index` parameter to `BulkMessage.ToMessageSendParams()`** - -In `api/pkg/requests/bulk_message_request.go`, change the method signature to accept index: - -```go -// ToMessageSendParams converts BulkMessage to services.MessageSendParams -func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string, index int) services.MessageSendParams { - from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) - - return services.MessageSendParams{ - Source: source, - Owner: from, - RequestID: input.sanitizeStringPointer(fmt.Sprintf("bulk-%s", requestID.String())), - UserID: userID, - SendAt: input.SendTime, - RequestReceivedAt: time.Now().UTC(), - Contact: input.sanitizeAddress(input.ToPhoneNumber), - Content: input.Content, - Attachments: input.removeEmptyStrings(strings.Split(input.AttachmentURLs, ",")), - Index: index, - } -} -``` - -- [ ] **Step 2: Update `BulkMessageHandler.Store()` to compute per-phone index** - -In `api/pkg/handlers/bulk_message_handler.go`, update the Store method to compute per-phone indices: - -```go -func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { - ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) - defer span.End() - - file, err := c.FormFile("document") - if err != nil { - msg := fmt.Sprintf("cannot fetch file with name [%s] from request", "document") - ctxLogger.Warn(stacktrace.Propagate(err, msg)) - return h.responseBadRequest(c, err) - } - - messages, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) - if len(validationErrors) != 0 { - msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)) - ctxLogger.Warn(stacktrace.NewError(msg)) - return h.responseUnprocessableEntity(c, validationErrors, "validation errors while sending bulk SMS") - } - - if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(messages))); msg != nil { - ctxLogger.Warn(stacktrace.NewError(fmt.Sprintf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages)))) - return h.responsePaymentRequired(c, *msg) - } - - requestID := uuid.New() - wg := sync.WaitGroup{} - count := atomic.Int64{} - - // Compute per-phone index for rate-based dispatch delay - phoneIndexMap := make(map[string]int) - for _, message := range messages { - if message.SendTime != nil { - continue // Exact-time messages don't need indexing - } - phone := message.FromPhoneNumber - phoneIndexMap[phone]++ // Pre-count not needed, we'll compute inline - } - - // Reset for actual iteration - phoneIndexCounter := make(map[string]int) - - for _, message := range messages { - wg.Add(1) - var perPhoneIndex int - if message.SendTime == nil { - perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] - phoneIndexCounter[message.FromPhoneNumber]++ - } - - go func(message *requests.BulkMessage, index int) { - count.Add(1) - _, err = h.messageService.SendMessage( - ctx, - message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index), - ) - if err != nil { - count.Add(-1) - msg := fmt.Sprintf("cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index) - ctxLogger.Error(stacktrace.Propagate(err, msg)) - } - wg.Done() - }(message, perPhoneIndex) - } - - wg.Wait() - return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages))) -} -``` - -- [ ] **Step 3: Clean up unused `phoneIndexMap` variable** - -The `phoneIndexMap` is computed but unused. Remove it — we only need `phoneIndexCounter`: - -```go -// Compute per-phone index for rate-based dispatch delay -phoneIndexCounter := make(map[string]int) - -for _, message := range messages { - wg.Add(1) - var perPhoneIndex int - if message.SendTime == nil { - perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] - phoneIndexCounter[message.FromPhoneNumber]++ - } - - go func(message *requests.BulkMessage, index int) { - // ... same as above - }(message, perPhoneIndex) -} -``` - -- [ ] **Step 4: Build to verify no compile errors** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 5: Run tests** - -Run: `cd api && go test -vet=off ./...` -Expected: all pass - -- [ ] **Step 6: Commit** - -```bash -cd api && git add -A && git commit -m "feat(handlers): add per-phone index for CSV bulk messages" -``` - ---- - -## Task 8: Add Unit Tests for getSendDelay - -**Files:** - -- Create: `api/pkg/services/message_service_test.go` - -- [ ] **Step 1: Write tests for the new `getSendDelay` logic** - -Create `api/pkg/services/message_service_test.go`: - -```go -package services - -import ( - "testing" - "time" - - "github.com/NdoleStudio/httpsms/pkg/events" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/trace" -) - -func TestGetSendDelay_WithSendAt_ReturnsTimeUntil(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - sendAt := time.Now().UTC().Add(5 * time.Minute) - params := MessageSendParams{SendAt: &sendAt} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - delay := service.getSendDelay(logger, payload, params, 10) - - // Should be approximately 5 minutes (within 2 seconds tolerance) - assert.InDelta(t, float64(5*time.Minute), float64(delay), float64(2*time.Second)) -} - -func TestGetSendDelay_WithSendAtInPast_ReturnsZero(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - sendAt := time.Now().UTC().Add(-5 * time.Minute) - params := MessageSendParams{SendAt: &sendAt} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - delay := service.getSendDelay(logger, payload, params, 10) - - assert.Equal(t, time.Duration(0), delay) -} - -func TestGetSendDelay_BulkIndex_RateBasedDelay(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - params := MessageSendParams{Index: 3} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - // 10 messages per minute = 6 seconds interval - delay := service.getSendDelay(logger, payload, params, 10) - - expected := time.Duration(3) * (time.Minute / time.Duration(10)) - assert.Equal(t, expected, delay) -} - -func TestGetSendDelay_BulkIndex_ZeroRate_ReturnsZero(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - params := MessageSendParams{Index: 5} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - delay := service.getSendDelay(logger, payload, params, 0) - - assert.Equal(t, time.Duration(0), delay) -} - -func TestGetSendDelay_IndexZero_ReturnsZero(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - params := MessageSendParams{Index: 0} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - delay := service.getSendDelay(logger, payload, params, 10) - - assert.Equal(t, time.Duration(0), delay) -} - -func TestGetSendDelay_NoSendAtNoIndex_ReturnsZero(t *testing.T) { - service := &MessageService{} - logger := &noopLogger{} - - params := MessageSendParams{} - payload := events.MessageAPISentPayload{MessageID: uuid.New()} - - delay := service.getSendDelay(logger, payload, params, 10) - - assert.Equal(t, time.Duration(0), delay) -} - -// noopLogger implements telemetry.Logger for testing -type noopLogger struct{} - -var _ telemetry.Logger = (*noopLogger)(nil) - -func (l *noopLogger) Error(_ error) {} -func (l *noopLogger) WithService(_ string) telemetry.Logger { return l } -func (l *noopLogger) WithString(_, _ string) telemetry.Logger { return l } -func (l *noopLogger) WithSpan(_ trace.SpanContext) telemetry.Logger { return l } -func (l *noopLogger) Trace(_ string) {} -func (l *noopLogger) Info(_ string) {} -func (l *noopLogger) Warn(_ error) {} -func (l *noopLogger) Debug(_ string) {} -func (l *noopLogger) Fatal(_ error) {} -func (l *noopLogger) Printf(_ string, _ ...interface{}) {} -``` - -- [ ] **Step 2: Run the tests** - -Run: `cd api && go test -vet=off ./pkg/services/ -run TestGetSendDelay -v` -Expected: all pass - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "test(services): add unit tests for getSendDelay rate-based logic" -``` - ---- - -## Task 9: Add Unit Test for ResolveScheduledAt (Existing, Verify No Regression) - -**Files:** - -- Create: `api/pkg/entities/send_schedule_test.go` - -- [ ] **Step 1: Write tests to lock existing ResolveScheduledAt behavior** - -Create `api/pkg/entities/send_schedule_test.go`: - -```go -package entities - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestResolveScheduledAt_NilSchedule_ReturnsCurrentUTC(t *testing.T) { - now := time.Now() - var schedule *MessageSendSchedule - result := schedule.ResolveScheduledAt(now) - assert.Equal(t, now.UTC(), result) -} - -func TestResolveScheduledAt_InactiveSchedule_ReturnsCurrentUTC(t *testing.T) { - now := time.Now() - schedule := &MessageSendSchedule{IsActive: false} - result := schedule.ResolveScheduledAt(now) - assert.Equal(t, now.UTC(), result) -} - -func TestResolveScheduledAt_NoWindows_ReturnsCurrentUTC(t *testing.T) { - now := time.Now() - schedule := &MessageSendSchedule{ - IsActive: true, - Timezone: "UTC", - Windows: []MessageSendScheduleWindow{}, - } - result := schedule.ResolveScheduledAt(now) - assert.Equal(t, now.UTC(), result) -} - -func TestResolveScheduledAt_WithinWindow_ReturnsCurrentUTC(t *testing.T) { - // Wednesday at 10:00 UTC, window is Wed 9:00-17:00 (540-1020 minutes) - now := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC) // Wednesday - schedule := &MessageSendSchedule{ - IsActive: true, - Timezone: "UTC", - Windows: []MessageSendScheduleWindow{ - {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, - }, - } - result := schedule.ResolveScheduledAt(now) - assert.Equal(t, now.UTC(), result) -} - -func TestResolveScheduledAt_BeforeWindow_ReturnsWindowStart(t *testing.T) { - // Wednesday at 7:00 UTC, window is Wed 9:00-17:00 - now := time.Date(2025, 1, 1, 7, 0, 0, 0, time.UTC) // Wednesday - schedule := &MessageSendSchedule{ - IsActive: true, - Timezone: "UTC", - Windows: []MessageSendScheduleWindow{ - {DayOfWeek: int(now.Weekday()), StartMinute: 540, EndMinute: 1020}, - }, - } - result := schedule.ResolveScheduledAt(now) - expected := time.Date(2025, 1, 1, 9, 0, 0, 0, time.UTC) - assert.Equal(t, expected, result) -} -``` - -- [ ] **Step 2: Run the tests** - -Run: `cd api && go test -vet=off ./pkg/entities/ -run TestResolveScheduledAt -v` -Expected: all pass - -- [ ] **Step 3: Commit** - -```bash -cd api && git add -A && git commit -m "test(entities): add regression tests for ResolveScheduledAt" -``` - ---- - -## Task 10: Final Build + Integration Verification - -**Files:** None (verification only) - -- [ ] **Step 1: Full build** - -Run: `cd api && go build ./...` -Expected: success - -- [ ] **Step 2: Full test suite** - -Run: `cd api && go test -vet=off ./...` -Expected: all pass - -- [ ] **Step 3: Generate Swagger docs (if API annotations changed)** - -The API request structs' annotations haven't changed for swagger (no new endpoints, `SendAt` already documented). Skip swagger regen unless compile errors appear. - -- [ ] **Step 4: Verify git status is clean** - -Run: `cd api && git status` -Expected: clean working tree - ---- - -## Notes - -- The `noopLogger` in tests implements the full `telemetry.Logger` interface (Error, WithService, WithString, WithSpan, Trace, Info, Warn, Debug, Fatal, Printf). -- The `ExactSendTime` field is transient — no database migrations needed. -- **Dedupe strategy**: `ScheduleExact` uses a `SELECT COUNT` check before insert. This is not fully race-proof but acceptable given: (a) Cloud Tasks at-least-once duplicates are rare, and (b) the existing `Schedule` path also has this same theoretical gap. Adding a DB unique constraint on `(message_id, status='pending')` would require a partial index migration — this is deferred as a future improvement if duplicates become a problem in practice. -- The existing `Schedule` method already handles concurrency via CockroachDB's serializable transactions (`crdbgorm.ExecuteTx`), which retries automatically on conflicts. No additional dedupe is added there. -- All existing behavior for single messages without `SendAt` is preserved (delay = 0, standard scheduling path). -- Past `SendAt` times are handled at both layers: `getSendDelay` returns 0 (immediate dispatch), and `Schedule` clamps `ScheduledAt` to `now` (no past timestamps persisted). diff --git a/docs/superpowers/plans/2026-05-15-hedging-repository.md b/docs/superpowers/plans/2026-05-15-hedging-repository.md deleted file mode 100644 index e3b838fa9..000000000 --- a/docs/superpowers/plans/2026-05-15-hedging-repository.md +++ /dev/null @@ -1,265 +0,0 @@ -# Hedging Repository Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -> **Status:** All tasks implemented. This plan was written retroactively to document and verify the implementation. - -**Goal:** Create composite hedging repositories that dual-write to GORM (primary) and Turso (secondary) with fail-open semantics on secondary failures. - -**Architecture:** Two new repository files implement the existing `HeartbeatRepository` and `HeartbeatMonitorRepository` interfaces by delegating reads to primary only and writes to both. Secondary write failures are logged and counted via an OTel metric but never propagated. Activated via `HEARTBEAT_DB_BACKEND=hedging`. - -**Tech Stack:** Go, OpenTelemetry metrics (`go.opentelemetry.io/otel/metric`), existing repository interfaces - -**Spec:** `docs/superpowers/specs/2026-05-15-hedging-repository-design.md` - ---- - -### Task 1: Create hedging heartbeat repository ✅ - -**Files:** - -- Created: `api/pkg/repositories/hedging_heartbeat_repository.go` - -- [ ] **Step 1: Create the hedging heartbeat repository file** - -```go -package repositories - -import ( - "context" - "fmt" - - otelMetric "go.opentelemetry.io/otel/metric" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -type hedgingHeartbeatRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - primary HeartbeatRepository - secondary HeartbeatRepository - failureCounter otelMetric.Int64Counter -} - -func NewHedgingHeartbeatRepository( - logger telemetry.Logger, - tracer telemetry.Tracer, - primary HeartbeatRepository, - secondary HeartbeatRepository, - failureCounter otelMetric.Int64Counter, -) HeartbeatRepository { - return &hedgingHeartbeatRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatRepository{})), - tracer: tracer, - primary: primary, - secondary: secondary, - failureCounter: failureCounter, - } -} -``` - -Implement 4 methods: - -- `Store` — write to primary, then secondary (fail-open with log + metric) -- `Index` — delegate to primary only -- `Last` — delegate to primary only -- `DeleteAllForUser` — write to primary, then secondary (fail-open with log + metric) - -Write methods follow this pattern: - -```go -func (r *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { - ctx, span := r.tracer.Start(ctx) - defer span.End() - - if err := r.primary.Store(ctx, heartbeat); err != nil { - return err - } - - if err := r.secondary.Store(ctx, heartbeat); err != nil { - r.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for heartbeat [%s]", heartbeat.ID))) - r.failureCounter.Add(ctx, 1) - } - - return nil -} -``` - -Read methods simply delegate: - -```go -func (r *hedgingHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { - return r.primary.Index(ctx, userID, owner, params) -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: exit code 0 - -- [ ] **Step 3: Commit** - -```bash -git add api/pkg/repositories/hedging_heartbeat_repository.go -git commit -m "feat(api): add hedging heartbeat repository" -``` - ---- - -### Task 2: Create hedging heartbeat monitor repository ✅ - -**Files:** - -- Created: `api/pkg/repositories/hedging_heartbeat_monitor_repository.go` - -- [ ] **Step 1: Create the hedging heartbeat monitor repository file** - -Same struct pattern as Task 1 but wrapping `HeartbeatMonitorRepository` interface. - -Implement 7 methods: - -| Method | Behavior | -| ------------------- | ---------------------- | -| `Store` | Write both (fail-open) | -| `Load` | Primary only | -| `Exists` | Primary only | -| `UpdateQueueID` | Write both (fail-open) | -| `Delete` | Write both (fail-open) | -| `UpdatePhoneOnline` | Write both (fail-open) | -| `DeleteAllForUser` | Write both (fail-open) | - -All write methods follow the same fail-open pattern: primary must succeed, secondary logs + increments counter on failure. - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: exit code 0 - -- [ ] **Step 3: Commit** - -```bash -git add api/pkg/repositories/hedging_heartbeat_monitor_repository.go -git commit -m "feat(api): add hedging heartbeat monitor repository" -``` - ---- - -### Task 3: Add HedgingFailureCounter to DI container ✅ - -**Files:** - -- Modified: `api/pkg/di/container.go` (added `HedgingFailureCounter()` method after `TursoDB()`, ~line 320) - -- [ ] **Step 1: Add the HedgingFailureCounter method** - -```go -func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { - meter := otel.GetMeterProvider().Meter( - container.projectID, - otelMetric.WithInstrumentationVersion(otel.Version()), - ) - counter, err := meter.Int64Counter( - "hedging.secondary.write.failures", - otelMetric.WithUnit("1"), - otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), - ) - if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create hedging failure counter")) - } - return counter -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: exit code 0 - ---- - -### Task 4: Wire hedging mode in DI container ✅ - -**Files:** - -- Modified: `api/pkg/di/container.go` - - - `HeartbeatRepository()` (~line 1768) - - `HeartbeatMonitorRepository()` (~line 930) - -- [ ] **Step 1: Change both methods from if/else to switch** - -Replace the existing `if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso"` with a switch: - -```go -func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { - switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "turso": - // existing libSQL path - case "hedging": - return repositories.NewHedgingHeartbeatRepository( - container.Logger(), - container.Tracer(), - repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), - container.HedgingFailureCounter(), - ) - default: - // existing GORM path - } -} -``` - -Same pattern for `HeartbeatMonitorRepository()`. - -- [ ] **Step 2: Verify build** - -Run: `cd api && go build ./...` -Expected: exit code 0 - -- [ ] **Step 3: Run pre-commit hooks** - -Run: `cd api && gofumpt -w pkg/repositories/hedging_heartbeat_repository.go pkg/repositories/hedging_heartbeat_monitor_repository.go pkg/di/container.go` -Expected: exit code 0 - -- [ ] **Step 4: Final commit** - -```bash -git add api/pkg/di/container.go -git commit -m "feat(api): wire hedging mode in DI container (HEARTBEAT_DB_BACKEND=hedging)" -``` - ---- - -### Task 5: Final verification - -- [ ] **Step 1: Full build** - -Run: `cd api && go build ./...` -Expected: exit code 0 - -- [ ] **Step 2: Run tests** - -Run: `cd api && go test ./...` -Expected: all tests pass - -- [ ] **Step 3: go vet (no new warnings)** - -Run: `cd api && go vet ./... 2>&1 | Select-String "hedging"` -Expected: only pre-existing `non-constant format string` warnings (same category as all other repos) - -- [ ] **Step 4: Pre-commit hooks pass** - -Run: `git add -A && git commit --dry-run` -Expected: all hooks pass (go-fumpt, go-lint, go-imports, go-mod-tidy) - -- [ ] **Step 5: Verify three modes work (code review)** - -Check that the DI container correctly handles all three values: - -- Default (unset) → GORM only -- `turso` → libSQL only -- `hedging` → GORM primary + libSQL secondary diff --git a/docs/superpowers/specs/2026-04-11-mms-attachments-design.md b/docs/superpowers/specs/2026-04-11-mms-attachments-design.md deleted file mode 100644 index 7beea85f7..000000000 --- a/docs/superpowers/specs/2026-04-11-mms-attachments-design.md +++ /dev/null @@ -1,220 +0,0 @@ -# MMS Attachment Support — Design Spec - -## Problem - -The Android app now forwards MMS attachments (as base64-encoded data) when receiving MMS messages via `HttpSmsApiService.receive()`. The API server needs to: - -1. Accept attachment data in the receive endpoint -2. Upload attachments to cloud storage (GCS or in-memory) -3. Store download URLs in the Message entity -4. Serve a download endpoint for retrieving attachments -5. Include attachment URLs in webhook event payloads - -## Approach - -**Approach A: Storage Interface + Minimal New Code** — Add an `AttachmentStorage` interface with GCS and memory implementations. Upload logic lives in the existing `MessageService.ReceiveMessage()` flow (synchronous). A new `AttachmentHandler` serves downloads. No new database tables — content type is encoded in the URL file extension. - -## Design - -### 1. Storage Interface - -**New file: `pkg/repositories/attachment_storage.go`** - -```go -type AttachmentStorage interface { - Upload(ctx context.Context, path string, data []byte) error - Download(ctx context.Context, path string) ([]byte, error) - Delete(ctx context.Context, path string) error -} -``` - -**GCS Implementation** (`pkg/repositories/gcs_attachment_storage.go`): - -- Uses `cloud.google.com/go/storage` SDK -- Configured with bucket name from `GCS_BUCKET_NAME` env var -- Stores objects at: `attachments/{userID}/{messageID}/{index}/{name}.{ext}` -- Extension derived from content type (e.g., `image/jpeg` → `.jpg`); falls back to `.bin` only when no mapping exists - -**Memory Implementation** (`pkg/repositories/memory_attachment_storage.go`): - -- `sync.Map`-backed in-memory store -- Used when `GCS_BUCKET_NAME` is empty/unset (local dev, testing) - -**DI selection** (in `container.go`): - -```go -if os.Getenv("GCS_BUCKET_NAME") != "" { - return NewGCSAttachmentStorage(bucket, tracer, logger) -} -return NewMemoryAttachmentStorage(tracer, logger) -``` - -### 2. Environment Variables - -| Variable | Description | Default | -| ----------------- | ------------------------------------------------------- | ---------------------------------- | -| `GCS_BUCKET_NAME` | GCS bucket for attachments. Empty = use memory storage. | `httpsms-86c51.appspot.com` (prod) | - -The API base URL for constructing download links is derived from `EVENTS_QUEUE_ENDPOINT` by stripping the `/v1/events` suffix. - -### 3. Request & Validation Changes - -**Updated `MessageReceive` request** (`pkg/requests/`): - -```go -type MessageReceive struct { - From string `json:"from"` - To string `json:"to"` - Content string `json:"content"` - Encrypted bool `json:"encrypted"` - SIM entities.SIM `json:"sim"` - Timestamp time.Time `json:"timestamp"` - Attachments []MessageAttachment `json:"attachments"` // NEW -} - -type MessageAttachment struct { - Name string `json:"name"` - ContentType string `json:"content_type"` - Content string `json:"content"` // base64-encoded -} -``` - -**Updated `MessageReceiveParams`** (`pkg/services/`): -The `ToMessageReceiveParams()` method must propagate attachments to the service layer: - -```go -type MessageReceiveParams struct { - // ... existing fields ... - Attachments []requests.MessageAttachment // NEW — raw attachment data for upload -} -``` - -**Filename sanitization:** -The `Name` field from the Android client must be sanitized to prevent path traversal attacks. Strip all path separators (`/`, `\`), directory traversal sequences (`..`), and non-printable characters. If the sanitized name is empty, use a fallback like `attachment-{index}`. - -**Content type allowlist:** -Only allow known-safe MIME types from the extension mapping table (Section 5). Reject attachments with unrecognized content types with a 400 error. - -**Validation rules** (in `pkg/validators/`): - -- Attachment count must be ≤ 10 -- Each decoded attachment must be ≤ 1.5 MB (1,572,864 bytes) -- Content type must be in the allowlist -- If any limit is exceeded → **reject entire request with 400 Bad Request** -- Validation happens before any upload or storage - -### 4. Upload Flow (Synchronous in Receive) - -In `MessageService.ReceiveMessage()`: - -1. Validate attachment count, sizes, and content types -2. Upload attachments **in parallel** using `errgroup`: - a. Decode base64 content - b. Sanitize `name` (strip path separators, `..`, non-printable chars; fallback to `attachment-{index}`) - c. Map `content_type` → file extension (e.g., `image/jpeg` → `.jpg`, unknown → `.bin`) - d. Upload to storage at path: `attachments/{userID}/{messageID}/{index}/{sanitizedName}.{ext}` - e. Build download URL: `{apiBaseURL}/v1/attachments/{userID}/{messageID}/{index}/{sanitizedName}.{ext}` -3. If any upload fails → best-effort delete of already-uploaded files, then return 500 -4. Collect download URLs into `message.Attachments` (existing `pq.StringArray` field) -5. Set `Attachments` on `MessagePhoneReceivedPayload` before dispatching event -6. `storeReceivedMessage()` copies `payload.Attachments` → `message.Attachments` -7. Store message in database -8. Fire `message.phone.received` event (includes attachment URLs) - -### 5. Content Type → Extension Mapping - -A utility function maps MIME types to file extensions: - -| Content Type | Extension | -| ----------------- | --------- | -| `image/jpeg` | `.jpg` | -| `image/png` | `.png` | -| `image/gif` | `.gif` | -| `image/webp` | `.webp` | -| `image/bmp` | `.bmp` | -| `video/mp4` | `.mp4` | -| `video/3gpp` | `.3gp` | -| `audio/mpeg` | `.mp3` | -| `audio/ogg` | `.ogg` | -| `audio/amr` | `.amr` | -| `application/pdf` | `.pdf` | -| `text/vcard` | `.vcf` | -| `text/x-vcard` | `.vcf` | -| _(default)_ | `.bin` | - -This covers common MMS content types. New mappings can be added as needed. - -### 6. Download Handler - -**New file: `pkg/handlers/attachment_handler.go`** - -**Route:** `GET /v1/attachments/:userID/:messageID/:attachmentIndex/:filename` - -- Registered **without authentication middleware** — publicly accessible, consistent with outgoing attachment URLs -- The `{userID}/{messageID}/{attachmentIndex}` path components provide sufficient obscurity (UUIDs are unguessable) - -**Download flow:** - -1. Parse URL params (userID, messageID, attachmentIndex, filename) -2. Construct storage path: `attachments/{userID}/{messageID}/{attachmentIndex}/{filename}` -3. Fetch bytes from `AttachmentStorage.Download(ctx, path)` -4. Derive `Content-Type` from filename extension -5. Set security headers: `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff` -6. Respond with binary data + correct `Content-Type` header -7. Return 404 if attachment not found in storage - -### 7. Webhook Event Changes - -**Updated `MessagePhoneReceivedPayload`** (`pkg/events/message_phone_received_event.go`): - -```go -type MessagePhoneReceivedPayload struct { - MessageID uuid.UUID `json:"message_id"` - UserID entities.UserID `json:"user_id"` - Owner string `json:"owner"` - Encrypted bool `json:"encrypted"` - Contact string `json:"contact"` - Timestamp time.Time `json:"timestamp"` - Content string `json:"content"` - SIM entities.SIM `json:"sim"` - Attachments []string `json:"attachments"` // NEW — download URLs -} -``` - -Webhook subscribers will receive the array of download URLs. They can `GET` each URL directly — no authentication required. - -### 8. Files Changed / Created - -**New files:** - -- `pkg/repositories/attachment_storage.go` — Interface definition -- `pkg/repositories/gcs_attachment_storage.go` — GCS implementation -- `pkg/repositories/memory_attachment_storage.go` — Memory implementation -- `pkg/handlers/attachment_handler.go` — Download endpoint handler -- `pkg/validators/attachment_handler_validator.go` — Download param validation - -**Modified files:** - -- `pkg/requests/message_receive.go` (or wherever `MessageReceive` is defined) — Add `Attachments` field -- `pkg/validators/message_handler_validator.go` — Add attachment count/size validation -- `pkg/services/message_service.go` — Add upload logic to `ReceiveMessage()` -- `pkg/events/message_phone_received_event.go` — Add `Attachments` field to payload -- `pkg/di/container.go` — Wire storage, new handler, pass storage to message service -- `api/.env.docker` — Add `GCS_BUCKET_NAME` variable -- `go.mod` / `go.sum` — Add `cloud.google.com/go/storage` dependency - -### 9. Validation Constraints - -| Constraint | Value | Behavior | -| ------------------------------- | ------------------------ | ---------------------------------------------------- | -| Max attachment count | 10 | 400 Bad Request | -| Max attachment size (decoded) | 1.5 MB (1,572,864 bytes) | 400 Bad Request | -| Content type not in allowlist | — | 400 Bad Request | -| Missing/empty attachments array | — | Message stored without attachments (normal SMS flow) | - -### 10. Error Handling - -- Storage upload failure → Best-effort delete of already-uploaded attachments, then return 500; message is NOT stored -- Storage download failure → Return 404 or 500 depending on error type -- Invalid base64 content → Return 400 Bad Request -- All errors wrapped with `stacktrace.Propagate()` per project convention diff --git a/docs/superpowers/specs/2026-05-03-entitlement-service-design.md b/docs/superpowers/specs/2026-05-03-entitlement-service-design.md deleted file mode 100644 index 8ec3893cf..000000000 --- a/docs/superpowers/specs/2026-05-03-entitlement-service-design.md +++ /dev/null @@ -1,188 +0,0 @@ -# Entitlement Service Design - -## Problem - -The [MessageSendSchedule](./2026-05-03-scheduling-send-refactor-design.md#messagesendschedule-send-windows--new-feature) feature (and future features) need usage limits based on the user's subscription plan. Free users should be limited to 1 send schedule; paid users get unlimited. The system must be: - -- **Scalable**: Easy to add new entity limits without architectural changes -- **Configurable**: Disabled by default for self-hosted deployments, enabled via env var for cloud -- **Non-invasive**: Enforced at the handler layer, before business logic executes - -## Approach - -Create a dedicated `EntitlementService` in `pkg/services/` that: - -1. Reads `ENTITLEMENT_ENABLED` from environment (defaults to `false`) -2. Defines a code-based map of entity limits per subscription plan -3. Exposes a single `Check()` method that handlers call before creating resources -4. Returns 402 Payment Required when a free user exceeds their limit - -## Configuration - -### Environment Variable - -```env -# Set to "true" on cloud deployment; self-hosted defaults to false (no limits) -ENTITLEMENT_ENABLED=false -``` - -### Entity Limits (code-based) - -```go -// entityLimits maps entity name → subscription plan → max count -// A limit of 0 means unlimited. If a plan is not listed, it defaults to unlimited. -var entityLimits = map[string]map[entities.SubscriptionName]int{ - "MessageSendSchedule": { - entities.SubscriptionNameFree: 1, - }, - // Future: add more entities here - // "Webhook": { - // entities.SubscriptionNameFree: 3, - // }, -} -``` - -## Service Interface - -```go -// EntitlementService checks whether a user can create more of a given entity. -type EntitlementService struct { - logger telemetry.Logger - tracer telemetry.Tracer - enabled bool - userRepository repositories.UserRepository -} - -// NewEntitlementService creates the service. `enabled` comes from ENTITLEMENT_ENABLED env var. -func NewEntitlementService( - logger telemetry.Logger, - tracer telemetry.Tracer, - enabled bool, - userRepository repositories.UserRepository, -) *EntitlementService - -// CheckResult holds the outcome of an entitlement check. -type CheckResult struct { - Allowed bool - Message string -} - -// Check verifies if the user can create another instance of the given entity. -// - If entitlements are disabled (self-hosted), always returns Allowed: true. -// - Loads the user's subscription plan. -// - Looks up the limit for the entity + plan combination. -// - Compares currentCount against the limit. -func (s *EntitlementService) Check( - ctx context.Context, - userID entities.UserID, - entityName string, - currentCount int, -) (*CheckResult, error) -``` - -## Handler Integration - -In `SendScheduleHandler.Store()`: - -```go -func (h *SendScheduleHandler) Store(c *fiber.Ctx) error { - // 1. Validate request (existing logic) - // 2. Get current count (efficient COUNT query) - count, err := h.service.CountByUser(ctx, userID) - if err != nil { ... } - // 3. Check entitlement - result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", count) - if err != nil { - return h.responseInternalServerError(c) - } - if !result.Allowed { - return h.responsePaymentRequired(c, result.Message) - } - // 4. Proceed with creating schedule (existing logic) -} -``` - -## Repository Addition - -Add to `SendScheduleRepository` interface and GORM implementation: - -```go -// CountByUser returns the number of schedules owned by a user. -CountByUser(ctx context.Context, userID entities.UserID) (int, error) -``` - -```` - -## Error Response - -HTTP 402 Payment Required: - -```json -{ - "message": "Upgrade to a paid plan to create more than 1 send schedule. Visit https://httpsms.com/pricing for details.", - "status": "payment_required" -} -```` - -## Files to Create/Modify - -| Action | File | Change | -| ------ | --------------------------------------------------- | ------------------------------------------------------------ | -| Create | `pkg/services/entitlement_service.go` | New service with limits map, `Check()`, `CheckResult` | -| Modify | `pkg/handlers/handler.go` | Add `responsePaymentRequired()` helper method | -| Modify | `pkg/handlers/send_schedule_handler.go` | Inject `EntitlementService`, add check in `Store()` | -| Modify | `pkg/di/container.go` | Wire `EntitlementService`, read env var, inject into handler | -| Modify | `pkg/repositories/send_schedule_repository.go` | Add `CountByUser()` to interface | -| Modify | `pkg/repositories/gorm_send_schedule_repository.go` | Implement `CountByUser()` with SQL COUNT | -| Modify | `pkg/services/send_schedule_service.go` | Add `CountByUser()` pass-through method | -| Modify | `.env.example` or `.env` | Add `ENTITLEMENT_ENABLED=false` | - -## Concurrency & Race Conditions - -The handler-level check (`count → check → create`) is not atomic. Two concurrent requests could both see `count=0` and both proceed. Mitigations: - -1. **Repository count method**: Use `CountByUser(ctx, userID)` instead of loading all records (efficient SQL `SELECT COUNT(*)`). -2. **Acceptable race window**: For a limit of 1, the worst case is 2 schedules created. This is acceptable because: - - The window is extremely small (single user, same millisecond) - - The consequence is minor (user has 2 schedules instead of 1) - - A DB-level unique constraint is impractical here (limit is per-user count, not per-row uniqueness) -3. **Future hardening**: If stricter enforcement is needed, add an advisory lock or transaction-based count+insert. - -## Counting Semantics - -All schedules owned by the user count toward the limit, regardless of `is_active` status. A user must delete a schedule to free up their quota. - -## Error Handling When Enabled - -- **Entitlements disabled** (`ENTITLEMENT_ENABLED=false`): Always returns `Allowed: true`, zero DB calls. -- **Entitlements enabled, DB error loading user**: Return error (surfaces as 500). Do NOT fail-open — this is a monetized feature gate. -- **Entitlements enabled, entity not in limits map**: Returns `Allowed: true` (entity has no restrictions). - -## Design Decisions - -1. **Handler-layer enforcement**: The handler gets the count and calls `Check()`. This keeps the entitlement service free of domain-specific repository dependencies. -2. **Entity name as key**: Using the entity struct name (e.g., `"MessageSendSchedule"`) makes it self-documenting and matches the user's preference for entity-based naming. -3. **Fail-open when disabled**: Self-hosted users never hit limits. The `enabled` flag short-circuits all checks. -4. **Fail-closed on error when enabled**: If the user can't be loaded and entitlements are enabled, the request fails with 500. -5. **Separate from BillingService**: BillingService handles SMS message counting/billing. EntitlementService handles feature-level access gating. Different concerns. -6. **No caching**: User plan data is already fast to load. Caching can be added later if needed. - -## Swagger & Handler Updates - -- Add `@Failure 402 {object} responses.PaymentRequired` annotation to `Store` route -- Add `responsePaymentRequired` helper to base handler struct -- Update handler constructor to accept `*services.EntitlementService` - -## Testing Strategy - -- Unit test `EntitlementService.Check()` with: - - Disabled mode → always allowed - - Free user at limit → denied - - Free user under limit → allowed - - Paid user → always allowed - - Unknown entity → allowed (no restrictions defined) - - User load error when enabled → returns error -- Handler test for `Store`: - - Free user with 0 schedules → 201 Created - - Free user with 1 schedule → 402 Payment Required - - Paid user with N schedules → 201 Created diff --git a/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md b/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md deleted file mode 100644 index 1b4ced8e4..000000000 --- a/docs/superpowers/specs/2026-05-03-integration-test-setup-design.md +++ /dev/null @@ -1,248 +0,0 @@ -# Integration Test Setup for httpSMS API - -## Problem - -The httpSMS API has no integration tests that verify the full SMS send/receive flow end-to-end. We need a CI-gated integration test that runs the entire stack in Docker and validates the core message lifecycle before deploying the API. - -## Approach - -Run the full application stack (API + PostgreSQL + Redis) in Docker alongside an **emulator** service that acts as a fake Android phone. The emulator implements a fake FCM server endpoint so the API's Firebase messaging client sends push notifications to it (instead of Google). The emulator then responds with SENT/DELIVERED events, completing the SMS lifecycle. A Go test runner exercises the API externally and asserts on final message state. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Docker Compose (tests/docker-compose.yml) │ -│ │ -│ ┌──────────┐ ┌───────┐ ┌──────────────────────────┐ │ -│ │PostgreSQL│ │ Redis │ │ API (existing Dockerfile)│ │ -│ └──────────┘ └───────┘ └────────────┬─────────────┘ │ -│ │ FCM push │ -│ ▼ │ -│ ┌──────────────────────────┐ │ -│ │ Emulator (fake phone) │ │ -│ │ - Fake FCM server :9090 │ │ -│ │ - Fires SENT/DELIVERED │ │ -│ │ events back to API │ │ -│ └──────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - ▲ - │ HTTP calls (send SMS, get message, etc.) - │ -┌────────┴──────────┐ -│ Test Runner (Go) │ ← runs on host / in CI -│ go test ./... │ -└───────────────────┘ -``` - -## Components - -### 1. `tests/docker-compose.yml` - -Brings up the full stack: - -- **postgres** — Same as root `docker-compose.yml`, seeded with `tests/seed.sql` -- **redis** — Standard Redis -- **api** — Built from `api/Dockerfile`, configured with `FCM_ENDPOINT=http://emulator:9090` to redirect Firebase messaging to the emulator -- **emulator** — Built from `tests/emulator/Dockerfile`, receives FCM pushes and fires events back - -### 2. `tests/emulator/` (Go project) - -A lightweight Go HTTP server that: - -- Exposes `POST /v1/projects/{project}/messages:send` — mimics the FCM v1 API. Receives push notification payloads from the API's Firebase messaging client. -- Exposes `POST /token` — returns a fake OAuth2 access token (the Firebase SDK calls this before sending FCM). Response format: `{"access_token": "fake-token", "token_type": "Bearer", "expires_in": 3600}` -- Exposes `GET /health` — health check endpoint -- On receiving a push with `KEY_MESSAGE_ID` in the data payload: - 1. Calls `GET http://api:8000/v1/messages/outstanding?message_id={messageID}` (using phone API key) to fetch the message like a real phone would - 2. Waits a brief delay (e.g., 200ms) - 3. Calls `POST http://api:8000/v1/messages/{messageID}/events` with event `SENT` (using phone API key) - 4. Waits another brief delay (e.g., 200ms) - 5. Calls `POST http://api:8000/v1/messages/{messageID}/events` with event `DELIVERED` (using phone API key) -- All API calls authenticated with the seeded phone API key (`x-api-key` header) -- Asserts it received the correct FCM payload structure (path, data.KEY_MESSAGE_ID present) - -### 3. `tests/seed.sql` - -SQL script that runs on PostgreSQL startup to create: - -- A test user: `id='test-user-id'`, `email='test@httpsms.com'`, `api_key='test-user-api-key'`, `subscription_name='pro'` -- A system user (for event queue): `id='system-user-id'`, `api_key='system-user-api-key'` -- A phone: `id=`, `user_id='test-user-id'`, `phone_number='+18005550199'`, `fcm_token='fake-fcm-token'` -- A phone API key: `id=`, `user_id='test-user-id'`, `api_key='test-phone-api-key'`, `phone_numbers=['+18005550199']` - -### 4. API Modification — FCM Transport Override - -In `api/pkg/di/container.go`, modify `FirebaseMessagingClient()`: - -- When `FCM_ENDPOINT` env var is set, create the Firebase App with a custom HTTP client whose `Transport` rewrites request URLs from `https://fcm.googleapis.com` to the value of `FCM_ENDPOINT` -- This requires no changes to business logic — the messaging client works normally but routes traffic to the emulator -- The Firebase credentials must be a syntactically valid fake service account JSON with `token_uri` pointing to `http://emulator:9090/token` - -### 4b. `tests/.env.test` — API environment for tests - -```env -ENV=production -GCP_PROJECT_ID=httpsms-test -EVENTS_QUEUE_TYPE=emulator -EVENTS_QUEUE_NAME=events-local -EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events -EVENTS_QUEUE_USER_API_KEY=system-user-api-key -EVENTS_QUEUE_USER_ID=system-user-id -FCM_ENDPOINT=http://emulator:9090 -DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms -DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms -REDIS_URL=redis://@redis:6379 -APP_PORT=8000 -ENTITLEMENT_ENABLED=false -USE_HTTP_LOGGER=true -FIREBASE_CREDENTIALS= -``` - -### 5. `tests/integration_test.go` (Go test files) - -Go tests using the standard `testing` package + `testify` for assertions: - -**Test 1: Send SMS E2E** - -1. `POST /v1/messages/send` with `from=`, `to=+18005550100`, `content="Hello"` (using user API key `x-api-key` header) -2. Extract message ID from response -3. Poll `GET /v1/messages/{id}` every 200ms with max 15s timeout (using user API key) -4. Assert message status reaches `delivered` -5. Assert message events include both `SENT` and `DELIVERED` - -**Test 2: Receive SMS** - -1. `POST /v1/messages/receive` (using phone API key auth) with `from=+18005550100`, `to=+18005550199`, `content="Hi there"`, `sim="SIM1"`, `timestamp=` -2. Extract message ID from response -3. `GET /v1/messages/{id}` (using user API key auth) -4. Assert message exists with correct content, from, to fields -5. Assert status is `received` - -### 6. `.github/workflows/integration-test.yml` - -GitHub Actions workflow: - -```yaml -name: integration-test -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - integration-test: - runs-on: ubuntu-latest - steps: - - Checkout - - Docker Compose up (tests/docker-compose.yml) - - Wait for health checks (API + emulator) - - Run: cd tests && go test -v -timeout 120s ./... - - Docker Compose down - - deploy-api: - needs: integration-test - # existing deploy logic -``` - -The `deploy-api` job depends on `integration-test` passing. - -## FCM Redirect Implementation Detail - -The Firebase Admin Go SDK's messaging client sends HTTP POST requests to: - -``` -https://fcm.googleapis.com/v1/projects/{project_id}/messages:send -``` - -We intercept this by providing a custom `http.RoundTripper`: - -```go -type fcmRedirectTransport struct { - target string // e.g., "http://emulator:9090" - base http.RoundTripper -} - -func (t *fcmRedirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Rewrite: https://fcm.googleapis.com/... → http://emulator:9090/... - req.URL.Scheme = "http" - req.URL.Host = strings.TrimPrefix(t.target, "http://") - return t.base.RoundTrip(req) -} -``` - -This is injected via `option.WithHTTPClient()` when creating the Firebase App in the DI container. - -## Fake Firebase Credentials - -For the integration test environment, we provide a minimal fake service account JSON: - -```json -{ - "type": "service_account", - "project_id": "httpsms-test", - "private_key_id": "test", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\n\n-----END RSA PRIVATE KEY-----\n", - "client_email": "test@httpsms-test.iam.gserviceaccount.com", - "client_id": "123456789", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "http://emulator:9090/token", - "auth_provider_x509_cert_url": "http://emulator:9090/certs", - "client_x509_cert_url": "http://emulator:9090/certs/test" -} -``` - -The emulator implements: - -- `POST /token` — Accepts JWT assertion grant, returns `{"access_token": "fake-token", "token_type": "Bearer", "expires_in": 3600}` -- Does NOT validate the JWT signature — just returns a valid token response - -## Docker Health Checks & Orchestration - -Services start in order with health dependencies: - -1. **postgres** — healthy when `pg_isready` passes -2. **redis** — healthy when accepting connections -3. **emulator** — healthy when `GET /health` returns 200 -4. **api** — starts after postgres+redis+emulator healthy, healthy when `GET /v1/` returns (or a dedicated health endpoint) - -Test runner waits for all services healthy before executing `go test`. - -## File Structure - -``` -tests/ -├── docker-compose.yml -├── seed.sql -├── go.mod -├── go.sum -├── integration_test.go -├── helpers_test.go # shared HTTP client, polling helpers -├── .env.test # env vars for the API in test mode -└── emulator/ - ├── Dockerfile - ├── go.mod - ├── go.sum - ├── main.go # entry point, starts HTTP server - ├── fcm_handler.go # fake FCM endpoint - ├── token_handler.go # fake OAuth2 token endpoint - └── events.go # fires SENT/DELIVERED events to API -``` - -## Key Design Decisions - -1. **DB seeding over Firebase Auth emulator** — Simpler, keeps focus on SMS flow testing. Auth is not what we're validating. -2. **Real FCM code path with redirected transport** — Tests the actual Firebase SDK integration, payload construction, and error handling. More confidence than a noop mock. -3. **Emulator as separate Go project** — Clean separation, own Dockerfile, own module. Doesn't pollute the API codebase. -4. **Test runner runs on host (not in Docker)** — Simpler debugging, standard `go test` output, easier CI integration. -5. **Polling with timeout for async assertions** — The send flow is async (event-driven). Polling with backoff is the pragmatic approach. - -## Out of Scope - -- Testing the web frontend -- Testing the Android app -- Load/performance testing -- Testing auth flows (login, registration) -- Testing billing/entitlements -- MMS/attachment testing (can be added later) diff --git a/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md b/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md deleted file mode 100644 index 1a02bc082..000000000 --- a/docs/superpowers/specs/2026-05-03-scheduling-send-refactor-design.md +++ /dev/null @@ -1,188 +0,0 @@ -# Scheduling Send Refactor Design - -## Related Documentation - -- [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) — existing `SendAt`/`SendTime` scheduling feature -- [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) — existing `MessagesPerMinute` rate-limiting feature - -## Problem Statement - -The current SMS scheduling logic has two issues: - -1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt` (see [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)), the system still applies rate-limiting and schedule window logic, which may shift the actual send time. - -2. **Bulk message contention.** When bulk messages (API or CSV) are sent, all events arrive at the Cloud Tasks queue near-simultaneously, causing DB serialization conflicts in `PhoneNotificationRepository.Schedule()` (which uses `SELECT ... ORDER BY scheduled_at DESC` in a transaction). The current workaround is a hardcoded 1-second spacing hack. - -## Proposed Solution - -### Core Principle - -- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. See [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) for how `SendAt` works. -- **No `SendTime`** = apply full scheduling logic ([rate-limit](https://docs.httpsms.com/features/control-sms-send-rate) + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. - -### Design - -#### 1. ExactSendTime Flag (Transient — not persisted) - -A boolean `ExactSendTime` flows through the event system: - -``` -Request → MessageSendParams → MessageAPISentPayload → PhoneNotificationScheduleParams -``` - -When `true`, the notification scheduling layer sets `ScheduledAt` to the exact time and skips rate-limit + window logic. - -#### 2. Rate-Based Dispatch Delay - -For bulk messages without an explicit `SendTime`, instead of the `index * 1s` hack, the service computes: - -```go -interval := time.Minute / time.Duration(messagesPerMinute) -delay := time.Duration(index) * interval -``` - -Where `index` is **per-phone** (not global across the batch). This spreads Cloud Task deliveries at the phone's actual send rate, eliminating DB contention naturally. Duration math avoids integer truncation issues for rates > 60/min or non-divisors of 60. - -#### 3. Per-Endpoint Behavior - -| Endpoint | `SendAt` provided | `SendAt` absent | -| --------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------- | -| Single SMS API (`/v1/messages/send`) | `ExactSendTime=true`, delay = `time.Until(SendAt)` | `ExactSendTime=false`, delay = 0 | -| Bulk SMS API (`/v1/messages/bulk-send`) | N/A (no SendAt field) | `ExactSendTime=false`, delay = `perPhoneIndex * interval` | -| CSV Upload | `ExactSendTime=true`, delay = `time.Until(SendTime)` | `ExactSendTime=false`, delay = `perPhoneIndex * interval` | - -**Index is per-phone**: In a CSV with messages to multiple phones, each phone maintains its own index counter. Messages to Phone A get indices 0, 1, 2... and messages to Phone B get separate indices 0, 1, 2... This ensures correct rate-limiting per phone without over-throttling unrelated phones. - -#### 4. Notification Scheduling Bypass - -In `PhoneNotificationService.Schedule()`: - -```go -if params.ExactSendTime && params.ScheduledSendTime != nil { - notification.ScheduledAt = *params.ScheduledSendTime - // Skip rate-limit and schedule window logic - // Insert directly -} else { - // Existing logic: rate-limit + schedule window -} -``` - -### Changes by File - -| File | Change | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `pkg/events/message_api_sent_event.go` | Add `ExactSendTime bool` field to `MessageAPISentPayload` | -| `pkg/services/message_service.go` | Add `Index int` to `MessageSendParams`; update `getSendDelay()` to compute rate-based delay when `Index > 0` and `SendAt == nil`; set `ExactSendTime` on event payload when `SendAt != nil` | -| `pkg/services/phone_notification_service.go` | Add `ExactSendTime bool` + `ScheduledSendTime *time.Time` to `PhoneNotificationScheduleParams`; add bypass path in `Schedule()` when `ExactSendTime && ScheduledSendTime != nil` — insert notification directly without transaction/rate logic | -| `pkg/repositories/gorm_phone_notification_repository.go` | Add `ScheduleExact(ctx, notification)` method that inserts with a fixed `ScheduledAt` (no transaction, no rate query). Add unique constraint or dedupe check on `(message_id)` for pending notifications to ensure idempotency. | -| `pkg/repositories/phone_notification_repository.go` | Add `ScheduleExact` to the repository interface | -| `pkg/listeners/phone_notification_listener.go` | Pass `ExactSendTime` + `ScheduledSendTime` from event payload to service params | -| `pkg/requests/message_bulk_send_request.go` | Remove per-index `SendAt` computation; add `Index` to each `MessageSendParams` | -| `pkg/requests/bulk_message_request.go` | Propagate `Index` into params for CSV rows | -| `pkg/handlers/message_handler.go` | Remove `index * 1s` hack in `BulkSend` handler | -| `pkg/handlers/bulk_message_handler.go` | Compute per-phone index for CSV rows; remove any concurrent scheduling; ensure `Index` is passed to `MessageSendParams` | - -### Data Flow - -``` -User sends request - → Handler creates MessageSendParams (with Index for bulk, ExactSendTime derived from SendAt presence) - → MessageService.SendMessage() - → Computes dispatch delay: - - ExactSendTime: time.Until(SendAt) - - Bulk without SendAt: Index * (60/MessagesPerMinute)s - - Single without SendAt: 0 - → Sets ExactSendTime on MessageAPISentPayload - → DispatchWithTimeout(event, delay) → Cloud Tasks - → [delay elapses] → PhoneNotificationListener.onMessageAPISent() - → PhoneNotificationService.Schedule(params with ExactSendTime) - → If ExactSendTime: insert with exact ScheduledAt - → Else: apply rate-limit + schedule window logic -``` - -### Edge Cases - -- **SendAt in the past**: Send immediately (existing behavior preserved). -- **MessagesPerMinute = 0**: No rate limiting; bulk messages dispatch immediately (existing behavior — `Schedule()` already handles this). Rate-based delay uses 0 when rate is 0. -- **No schedule attached to phone**: Window logic returns current time unchanged (existing behavior). -- **CSV with mixed rows**: Some rows have `SendTime`, others don't. Each row is processed independently — those with `SendTime` get exact dispatch, those without get rate-based delay. -- **Cloud Task duplicate delivery**: `ScheduleExact` and `Schedule` use a dedupe check (unique active notification per `message_id`) to prevent duplicate notification creation on at-least-once delivery. -- **Retries for exact-send messages**: When an exact-send message expires and triggers a retry, the retry does NOT preserve exact-send semantics — it falls through to standard scheduling. The explicit time was a one-shot intent. - -### Terminology Note - -"Send at exactly that time" means the system will not apply additional rate-limit or schedule-window adjustments. It does NOT guarantee precise handset delivery timing (which depends on Cloud Tasks delivery, FCM push, and device state). - -### What Does NOT Change - -- The `MessageSendSchedule` entity and its `ResolveScheduledAt()` logic -- The `MessageSendScheduleService` CRUD operations -- The phone notification entity schema (no new DB columns) -- The Android app behavior -- The web frontend (models auto-generated from Swagger) - ---- - -## MessageSendSchedule (Send Windows) — New Feature - -This is the only scheduling mechanism that does **not** have a dedicated documentation page yet. Unlike [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) (one-time `SendAt`) and [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) (`MessagesPerMinute` throttling), MessageSendSchedule defines **recurring availability windows** that control when a phone is allowed to send outgoing SMS messages. - -### Concept - -A `MessageSendSchedule` is a named set of time windows (per day of week) that define when the phone can send. Messages arriving outside those windows are delayed until the next available window opens. - -### Entity - -```go -type MessageSendSchedule struct { - ID uuid.UUID - UserID UserID - Name string // e.g. "Business Hours" - Timezone string // IANA timezone e.g. "Europe/Tallinn" - IsActive bool - Windows []MessageSendScheduleWindow // per-day availability slots - CreatedAt time.Time - UpdatedAt time.Time -} - -type MessageSendScheduleWindow struct { - DayOfWeek int // 0=Sunday, 6=Saturday - StartMinute int // minutes from midnight (e.g. 540 = 9:00) - EndMinute int // minutes from midnight (e.g. 1020 = 17:00) -} -``` - -### How It Works - -1. A user creates a schedule via `POST /v1/send-schedules` with a name, timezone, and one or more windows. -2. The schedule is linked to a phone via a `ScheduleID` field on the phone entity. -3. When a message is queued (without an explicit `SendAt`), the `PhoneNotificationRepository.Schedule()` method calls `MessageSendSchedule.ResolveScheduledAt(now)` to find the next allowed send time. -4. If the current time falls within a window, the message sends immediately. If not, it's delayed to the start of the next available window. - -### API Endpoints - -| Method | Endpoint | Description | -| ------ | --------------------------------- | --------------------------- | -| GET | `/v1/send-schedules` | List all user schedules | -| POST | `/v1/send-schedules` | Create a new schedule | -| PUT | `/v1/send-schedules/{scheduleID}` | Update an existing schedule | -| DELETE | `/v1/send-schedules/{scheduleID}` | Delete a schedule | - -### Validation Rules - -- `name`: required, 2–100 characters -- `timezone`: required, valid IANA timezone -- `windows[].day_of_week`: 0–6 -- `windows[].start_minute`: 0–1439 -- `windows[].end_minute`: 1–1440, must be greater than `start_minute` -- Max 6 windows per day -- No overlapping windows on the same day - -### Entitlement - -Free users are limited to 1 schedule. Paid users get unlimited schedules. Enforced via `EntitlementService.Check()` in the handler before creation. - -### Interaction with Other Scheduling Features - -- **[Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)** (`SendAt`): When provided, bypasses send windows entirely (exact send time). -- **[Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate)** (`MessagesPerMinute`): Applied independently — rate-limiting still applies within allowed windows. Both constraints compose: the message must be within a window AND respect the rate limit. diff --git a/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md b/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md deleted file mode 100644 index f2383bff4..000000000 --- a/docs/superpowers/specs/2026-05-05-integration-tests-wiremock-design.md +++ /dev/null @@ -1,304 +0,0 @@ -# Integration Tests: WireMock + httpsms-go Client Refactor - -## Problem - -The current integration tests use raw `net/http` calls and a custom emulator (120+ lines of Go) to simulate phone behavior. This makes tests harder to maintain and doesn't cover encryption, rate limiting, or webhook verification. We need to: - -1. Refactor tests to use the official `httpsms-go` client SDK -2. Replace the custom emulator with WireMock (stub server + request journal) -3. Add E2E encryption tests (outgoing + incoming) -4. Add rate-limit verification test -5. Assert webhook delivery with JWT authentication in all tests - -## Architecture - -``` -┌────────────────────────────────────────────────────────┐ -│ Docker Compose (tests/docker-compose.yml) │ -│ │ -│ ┌──────────┐ ┌───────┐ ┌─────────────────────────┐ │ -│ │PostgreSQL│ │ Redis │ │ API │ │ -│ └──────────┘ └───────┘ └────────────┬────────────┘ │ -│ │FCM push │ -│ │Webhook calls │ -│ ▼ │ -│ ┌─────────────────────────┐ │ -│ │ WireMock 3.x (:8080) │ │ -│ │ - Fake FCM endpoint │ │ -│ │ - Fake OAuth token │ │ -│ │ - Webhook receiver │ │ -│ │ - Request journal │ │ -│ └─────────────────────────┘ │ -└────────────────────────────────────────────────────────┘ - ▲ - │ httpsms-go client + go-wiremock client -┌────────┴──────────┐ -│ Test Runner (Go) │ -│ go test ./... │ -└───────────────────┘ -``` - -### Key Design Decisions - -- **WireMock replaces the custom emulator entirely**. It serves as both the fake FCM endpoint (receives push notifications from the API) and the webhook receiver (captures webhook events). -- **Tests fire SENT/DELIVERED events directly** to the API via HTTP. No WireMock callbacks needed — the test controls the flow deterministically. -- **Each test creates its own phone** with a random phone number for parallel test isolation. -- **go-wiremock** (`github.com/wiremock/go-wiremock`) is used to configure stubs and query the request journal from test code. - -## Test Flow (per test) - -``` -1. SETUP - ├─ Create phone (random number, test-specific messages_per_minute) - ├─ Create phone API key for that phone - ├─ Create webhook pointing to WireMock with a signing key - └─ Configure WireMock stubs (if not pre-loaded) - -2. ACT - ├─ Send/receive message via httpsms-go client - └─ (For send tests) Query WireMock journal → extract KEY_MESSAGE_ID from FCM push - -3. SIMULATE PHONE - ├─ Fire SENT event to API (POST /v1/messages/{id}/events) - └─ Fire DELIVERED event to API - -4. ASSERT - ├─ Verify message reached expected status via httpsms-go client - ├─ Query WireMock journal for webhook events - ├─ Validate JWT token: signature (HMAC-SHA256), issuer, subject, audience, expiry - └─ Validate webhook payload contains correct event type and message data -``` - -## Components - -### 1. Docker Compose Changes - -**Remove:** - -- `tests/emulator/` directory entirely (Dockerfile, Go source, go.mod) - -**Replace with WireMock:** - -```yaml -wiremock: - image: wiremock/wiremock:3x - ports: - - "8080:8080" - volumes: - - ./wiremock/mappings:/home/wiremock/mappings:ro - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] - interval: 5s - timeout: 5s - retries: 10 -``` - -**Pre-loaded WireMock mappings** (`tests/wiremock/mappings/`): - -- `fcm-send.json` — Stub for `POST /v1/projects/*/messages:send` → returns `{"name": "projects/httpsms-test/messages/fake-id"}` -- `oauth-token.json` — Stub for `POST /token` → returns `{"access_token": "fake-access-token", "token_type": "Bearer", "expires_in": 3600}` -- `webhook-receiver.json` — Stub for `POST /webhooks/test` → returns 200 (catches all webhook calls) - -### 2. API Configuration Updates - -**`.env.test` changes:** - -- `FCM_ENDPOINT=http://wiremock:8080` (was `http://emulator:9090`) - -**Firebase credentials** `token_uri` points to `http://wiremock:8080/token` - -### 3. Seed SQL (simplified) - -Only seeds: - -- Test user (`test-user-id`, `api_key='test-user-api-key'`) -- System user (`system-user-id`, for event queue auth) - -Phones, phone API keys, and webhooks are created per-test via the API. - -### 4. httpsms-go Client Additions - -New services to add to `github.com/NdoleStudio/httpsms-go`: - -#### `PhoneService` - -```go -type PhoneUpsertParams struct { - PhoneNumber string `json:"phone_number"` - FcmToken string `json:"fcm_token"` - MessagesPerMinute uint `json:"messages_per_minute"` - MaxSendAttempts uint `json:"max_send_attempts"` - MessageExpirationSeconds uint `json:"message_expiration_seconds"` - SIM string `json:"sim"` -} - -func (service *PhoneService) Upsert(ctx, params) → (*PhoneResponse, *Response, error) -// PUT /v1/phones — authenticated with user API key -``` - -#### `PhoneService` (FCM Token binding) - -```go -type PhoneFCMTokenParams struct { - PhoneNumber string `json:"phone_number"` - FcmToken string `json:"fcm_token"` - SIM string `json:"sim"` -} - -func (service *PhoneService) UpsertFCMToken(ctx, params) → (*PhoneResponse, *Response, error) -// PUT /v1/phones/fcm-token — authenticated with phone API key -// This binds the phone to the phone API key via the auth context -``` - -#### `PhoneAPIKeyService` - -```go -type PhoneAPIKeyStoreParams struct { - Name string `json:"name"` -} - -func (service *PhoneAPIKeyService) Store(ctx, params) → (*PhoneAPIKeyResponse, *Response, error) -// POST /v1/phone-api-keys/ — authenticated with user API key -// Returns the created phone API key including its api_key value -``` - -#### `WebhookService` - -```go -type WebhookStoreParams struct { - SigningKey string `json:"signing_key"` - URL string `json:"url"` - PhoneNumbers []string `json:"phone_numbers"` - Events []string `json:"events"` -} - -func (service *WebhookService) Store(ctx, params) → (*WebhookResponse, *Response, error) -// POST /v1/webhooks — authenticated with user API key -``` - -#### Phone Setup Flow (per test) - -The real Android phone registers via this flow, and tests must replicate it: - -1. `PUT /v1/phones` (user API key) — creates phone with phone_number + fcm_token + messages_per_minute -2. `POST /v1/phone-api-keys/` (user API key) — creates a phone API key, returns the `api_key` value -3. `PUT /v1/phones/fcm-token` (phone API key) — re-registers FCM token, which binds the phone to the API key via `PhoneAPIKeyListener.onPhoneUpdated` - -After step 3, the phone API key is authorized to act on behalf of that phone (fire events, receive messages, etc.). - -### 5. Test Cases - -#### `TestSendSMS_Encrypted` - -1. Generate random encryption key -2. Create phone + phone API key + webhook -3. Encrypt plaintext using `client.Cipher.Encrypt(key, "secret message")` -4. Send message with `Encrypted: true` and encrypted content -5. Query WireMock journal → verify FCM push arrived with `KEY_MESSAGE_ID` (FCM only carries the message ID, not content) -6. Call `GET /v1/messages/outstanding?message_id={id}` (phone API key) — verify response has `encrypted: true` and content is ciphertext (not plaintext) -7. Fire SENT + DELIVERED events -8. Fetch message via user API key → verify `encrypted: true`, content is ciphertext -9. Decrypt with `client.Cipher.Decrypt(key, content)` → assert equals original plaintext -10. Verify webhook event in WireMock with valid JWT - -#### `TestReceiveSMS_Encrypted` - -1. Generate random encryption key -2. Create phone + phone API key + webhook -3. Encrypt plaintext using `client.Cipher.Encrypt(key, "incoming secret")` -4. Simulate receiving an encrypted SMS (POST /v1/messages/receive with phone API key) -5. Fetch message via user API key → verify `encrypted: true` -6. Decrypt content → assert equals original plaintext -7. Verify webhook event (`message.phone.received`) in WireMock with valid JWT - -#### `TestSendSMS_RateLimit` - -1. Create phone with `messages_per_minute: 10` (= 6s gap) -2. Create phone API key + webhook -3. Send 2 messages simultaneously -4. Query WireMock journal for FCM pushes (correlate by message IDs from send responses) -5. Assert the timestamps of the two FCM pushes have ≥6 second gap -6. Fire SENT + DELIVERED for both messages -7. Verify both messages reach `delivered` status -8. Verify webhook events for both messages - -#### `TestSendSMS_OutstandingFlow` - -Validates the real phone flow (`/v1/messages/outstanding`): - -1. Create phone + phone API key + webhook -2. Send message via httpsms-go client -3. Query WireMock journal → extract `KEY_MESSAGE_ID` from FCM push -4. Call `GET /v1/messages/outstanding?message_id={id}` (phone API key) — assert returns the message with correct content, owner, contact -5. Fire SENT + DELIVERED events -6. Verify message reaches `delivered` status -7. Verify webhook events - -#### Webhook Verification (shared helper) - -For all tests, a helper function: - -```go -func assertWebhookEvent(t *testing.T, wiremockClient *wiremock.Client, signingKey string, expectedEventType string) { - // 1. Query WireMock journal for POST /webhooks/test requests - // 2. Find request with X-Event-Type header matching expectedEventType - // 3. Extract Authorization header → parse JWT - // 4. Validate signature with signingKey (HMAC-SHA256) - // 5. Assert claims: - // - Issuer == "api.httpsms.com" - // - Subject == "test-user-id" - // - Audience contains webhook URL - // - ExpiresAt is in the future - // - NotBefore is in the past -} -``` - -### 6. Test Helper Structure - -``` -tests/ -├── docker-compose.yml (updated: wiremock replaces emulator) -├── wiremock/ -│ └── mappings/ -│ ├── fcm-send.json -│ ├── oauth-token.json -│ └── webhook-receiver.json -├── seed.sql (simplified: user + system user only) -├── .env.test (updated: FCM_ENDPOINT → wiremock) -├── go.mod (add httpsms-go, go-wiremock, golang-jwt) -├── helpers_test.go (shared constants, setup helpers) -├── webhook_helpers_test.go (JWT verification helpers) -├── integration_test.go (all test cases) -└── README.md -``` - -### 7. Dependencies - -**Test module (`tests/go.mod`):** - -- `github.com/NdoleStudio/httpsms-go` — API client -- `github.com/wiremock/go-wiremock` — WireMock stub configuration + journal queries -- `github.com/golang-jwt/jwt/v5` — JWT parsing and validation -- `github.com/stretchr/testify` — assertions (already present) - -### 8. Parallel Test Execution & Request Correlation - -Each test creates its own phone with a unique random number (e.g. `+1800555XXXX` where XXXX is random). This ensures: - -- No message cross-contamination between tests -- Webhooks scoped to specific phone numbers don't fire for other tests - -**Correlation strategy for WireMock journal queries:** - -- **FCM pushes**: Correlate by message ID. The test gets the message ID from the send response, then searches WireMock journal for FCM push requests containing that `KEY_MESSAGE_ID` in the JSON body. -- **Webhook events**: Each test uses a **unique webhook URL path** (e.g. `/webhooks/{testUUID}`). This ensures journal queries for webhook assertions only match events for that specific test. Additionally, match on `X-Event-Type` header and message ID in payload body. -- **Unique FCM token per phone**: Each test generates a unique `fcm_token` string. Since WireMock captures the FCM push including the `token` field, this can be used as a secondary correlation key if needed. - -Tests use `t.Parallel()` where safe (encryption tests can run in parallel; rate-limit test may need serial execution due to timing assertions). - -## Migration Notes - -- The `tests/emulator/` directory is deleted entirely -- The CI workflow (`.github/workflows/integration-test.yml`) needs updating to remove emulator references -- Firebase credentials `token_uri` must point to `http://wiremock:8080/token` -- WireMock image is Java-based (~300MB) vs the old Alpine emulator (~15MB), but eliminates maintenance of custom code diff --git a/docs/superpowers/specs/2026-05-15-hedging-repository-design.md b/docs/superpowers/specs/2026-05-15-hedging-repository-design.md deleted file mode 100644 index 2afc6acec..000000000 --- a/docs/superpowers/specs/2026-05-15-hedging-repository-design.md +++ /dev/null @@ -1,164 +0,0 @@ -# Hedging Repository for Heartbeat & Monitor - -**Date:** 2026-05-15 -**Status:** Approved - -## Overview - -Create composite "hedging" repositories for `HeartbeatRepository` and `HeartbeatMonitorRepository` that write to both GORM (primary) and Turso (secondary). Reads only hit the primary. Secondary writes are fail-open — errors are logged and a metric is emitted, but the operation succeeds from the caller's perspective. - -## Motivation - -Gradually migrate heartbeat data to Turso by dual-writing. The GORM/PostgreSQL backend remains the source of truth while Turso builds up a complete dataset. If Turso has issues, the system is unaffected. - -## Configuration - -Activated via `HEARTBEAT_DB_BACKEND=hedging`. The three modes are now: - -| Value | Behavior | -| ----------------- | ---------------------------------------------------------------------- | -| _(unset/default)_ | GORM/PostgreSQL only | -| `turso` | Turso/libSQL only | -| `hedging` | GORM primary (reads+writes) + Turso secondary (writes only, fail-open) | - -## Architecture - -### New Files - -| File | Purpose | -| -------------------------------------------------------------- | -------------------------------------- | -| `api/pkg/repositories/hedging_heartbeat_repository.go` | Composite `HeartbeatRepository` | -| `api/pkg/repositories/hedging_heartbeat_monitor_repository.go` | Composite `HeartbeatMonitorRepository` | - -### Modified Files - -| File | Change | -| ------------------------- | ------------------------------------------------------------------ | -| `api/pkg/di/container.go` | Add `hedging` case to switch, add `HedgingFailureCounter()` method | - -## Method Delegation - -### HeartbeatRepository - -| Method | Primary (GORM) | Secondary (Turso) | -| ------------------ | -------------- | -------------------- | -| `Store` | ✅ write | ✅ write (fail-open) | -| `Index` | ✅ read | ❌ skip | -| `Last` | ✅ read | ❌ skip | -| `DeleteAllForUser` | ✅ write | ✅ write (fail-open) | - -### HeartbeatMonitorRepository - -| Method | Primary (GORM) | Secondary (Turso) | -| ------------------- | -------------- | -------------------- | -| `Store` | ✅ write | ✅ write (fail-open) | -| `Load` | ✅ read | ❌ skip | -| `Exists` | ✅ read | ❌ skip | -| `UpdateQueueID` | ✅ write | ✅ write (fail-open) | -| `Delete` | ✅ write | ✅ write (fail-open) | -| `UpdatePhoneOnline` | ✅ write | ✅ write (fail-open) | -| `DeleteAllForUser` | ✅ write | ✅ write (fail-open) | - -## Struct Design - -```go -type hedgingHeartbeatRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - primary HeartbeatRepository - secondary HeartbeatRepository - failureCounter otelMetric.Int64Counter -} - -type hedgingHeartbeatMonitorRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - primary HeartbeatMonitorRepository - secondary HeartbeatMonitorRepository - failureCounter otelMetric.Int64Counter -} -``` - -## Error Handling (Fail-Open Pattern) - -```go -func (r *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { - ctx, span := r.tracer.Start(ctx) - defer span.End() - - // Primary: must succeed - if err := r.primary.Store(ctx, heartbeat); err != nil { - return err - } - - // Secondary: fail-open (log + metric) - if err := r.secondary.Store(ctx, heartbeat); err != nil { - r.logger.Error(stacktrace.Propagate(err, fmt.Sprintf( - "hedging: secondary write failed for heartbeat [%s]", heartbeat.ID, - ))) - r.failureCounter.Add(ctx, 1) - } - - return nil -} -``` - -**Rules:** - -- Primary error → return immediately, no secondary attempt -- Secondary error → log at ERROR level, increment `failureCounter`, return nil -- Read methods → delegate directly to primary, no secondary involvement -- Each method has its own tracing span via `tracer.Start(ctx)` - -## Observability - -**Metric:** - -- Name: `hedging.secondary.write.failures` -- Unit: `1` (count) -- Description: `Number of failed secondary writes in hedging repositories` -- Created once in DI container, shared by both hedging repos - -**Logging:** Each secondary failure logs at ERROR with the method context (entity ID, user ID, etc.) - -## DI Container Changes - -```go -func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { - switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "turso": - return repositories.NewLibsqlHeartbeatRepository(...) - case "hedging": - return repositories.NewHedgingHeartbeatRepository( - container.Logger(), - container.Tracer(), - repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), - container.HedgingFailureCounter(), - ) - default: - return repositories.NewGormHeartbeatRepository(...) - } -} - -func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { - meter := otel.GetMeterProvider().Meter(container.projectID) - counter, err := meter.Int64Counter("hedging.secondary.write.failures", - otelMetric.WithUnit("1"), - otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), - ) - if err != nil { - container.logger.Fatal(...) - } - return counter -} -``` - -Same switch pattern for `HeartbeatMonitorRepository()`. - -## Safety - -- Default behavior (no env var) is unchanged — pure GORM/PostgreSQL -- `turso` mode remains available for pure Turso usage -- `hedging` mode never fails due to Turso issues — secondary is fully fail-open -- Service layer requires no changes — same interfaces diff --git a/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md b/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md deleted file mode 100644 index b501650f2..000000000 --- a/docs/superpowers/specs/2026-05-15-turso-heartbeat-backend-design.md +++ /dev/null @@ -1,162 +0,0 @@ -# Turso/libSQL Backend for Heartbeat & Monitor Repositories - -**Date:** 2026-05-15 -**Status:** Approved - -## Overview - -Add a libSQL/Turso alternative implementation for `HeartbeatRepository` and `HeartbeatMonitorRepository`, switchable via the `HEARTBEAT_DB_BACKEND` environment variable. When set to `turso`, the API connects to a cloud-hosted Turso database instead of the dedicated PostgreSQL instance. - -## Motivation - -Move heartbeat storage to a dedicated Turso database for cost efficiency and edge performance, while keeping the existing PostgreSQL path as the default fallback. - -## Configuration - -| Env Var | Purpose | Example | -| ---------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- | -| `HEARTBEAT_DB_BACKEND` | Selects backend (`turso` = libSQL, anything else = PostgreSQL) | `turso` | -| `TURSO_DATABASE_DSN` | Turso database DSN (URL with authToken query param) | `libsql://httpsms-ndolestudio.aws-us-east-1.turso.io?authToken=eyJ...` | - -When `HEARTBEAT_DB_BACKEND` is not set or is any value other than `turso`, the existing GORM/PostgreSQL path is used unchanged. - -## Architecture - -### Approach - -Direct `database/sql` with the Turso Go remote driver (`github.com/tursodatabase/libsql-client-go`). No ORM — raw SQL queries for a simple 2-table schema. This is the pure-Go HTTP driver for remote Turso Cloud access (no CGo required). - -### New Files - -| File | Purpose | -| ------------------------------------------------------------- | -------------------------------------------------------- | -| `api/pkg/repositories/libsql.go` | Connection factory, table auto-creation, shared helpers | -| `api/pkg/repositories/libsql_heartbeat_repository.go` | `HeartbeatRepository` implementation using libSQL | -| `api/pkg/repositories/libsql_heartbeat_monitor_repository.go` | `HeartbeatMonitorRepository` implementation using libSQL | - -### Modified Files - -| File | Change | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| `api/pkg/di/container.go` | Add `tursoDB *sql.DB` field, `TursoDB()` method, conditional wiring in `HeartbeatRepository()` and `HeartbeatMonitorRepository()` | -| `api/go.mod` | Add `github.com/tursodatabase/libsql-client-go` dependency | - -## Database Schema - -```sql -CREATE TABLE IF NOT EXISTS heartbeats ( - id TEXT PRIMARY KEY, - owner TEXT NOT NULL, - version TEXT NOT NULL, - charging INTEGER NOT NULL DEFAULT 0, - user_id TEXT NOT NULL, - timestamp DATETIME NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_heartbeats_owner_timestamp ON heartbeats(owner, timestamp); -CREATE INDEX IF NOT EXISTS idx_heartbeats_user_id ON heartbeats(user_id); - -CREATE TABLE IF NOT EXISTS heartbeat_monitors ( - id TEXT PRIMARY KEY, - phone_id TEXT NOT NULL, - user_id TEXT NOT NULL, - queue_id TEXT NOT NULL DEFAULT '', - owner TEXT NOT NULL, - phone_online INTEGER NOT NULL DEFAULT 1, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_heartbeat_monitors_user_owner ON heartbeat_monitors(user_id, owner); -``` - -**Type mappings from PostgreSQL:** - -- UUID → TEXT -- BOOLEAN → INTEGER (0/1) -- TIMESTAMP → DATETIME (ISO 8601 text) - -## Repository Implementations - -### `libsql.go` (shared) - -- `NewTursoDB(url, authToken string) (*sql.DB, error)` — opens connection with libSQL driver, executes CREATE TABLE/INDEX statements -- Returns `*sql.DB` for use by both repository implementations - -### `libsql_heartbeat_repository.go` - -Implements `HeartbeatRepository`: - -| Method | SQL | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | -| `Store` | `INSERT INTO heartbeats (id, owner, version, charging, user_id, timestamp) VALUES (?, ?, ?, ?, ?, ?)` | -| `Index` | `SELECT ... WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?` (optional `version LIKE ?` filter) | -| `Last` | `SELECT ... WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT 1` | -| `DeleteAllForUser` | `DELETE FROM heartbeats WHERE user_id = ?` | - -### `libsql_heartbeat_monitor_repository.go` - -Implements `HeartbeatMonitorRepository`: - -| Method | SQL | -| ------------------- | --------------------------------------------------------------------------- | -| `Store` | INSERT all fields | -| `Load` | SELECT by user_id + owner | -| `Exists` | `SELECT COUNT(*) FROM ... WHERE user_id = ? AND id = ?` (returns count > 0) | -| `UpdateQueueID` | UPDATE queue_id + updated_at WHERE id = ? | -| `Delete` | DELETE WHERE user_id = ? AND owner = ? | -| `UpdatePhoneOnline` | UPDATE phone_online + updated_at WHERE id = ? AND user_id = ? | -| `DeleteAllForUser` | DELETE WHERE user_id = ? | - -### Error Handling - -- `sql.ErrNoRows` → wrap with `stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)` to match GORM behavior expected by the service layer -- All other errors → wrap with `stacktrace.Propagate(err, msg)` - -### Observability - -Every method follows the existing tracing pattern: - -```go -ctx, span := repository.tracer.Start(ctx) -defer span.End() -``` - -## DI Container Changes - -```go -// New field on Container struct -tursoDB *sql.DB - -// New method -func (container *Container) TursoDB() *sql.DB { - if container.tursoDB != nil { - return container.tursoDB - } - db, err := repositories.NewTursoDB(os.Getenv("TURSO_DATABASE_DSN")) - if err != nil { - container.logger.Fatal(err) - } - container.tursoDB = db - return container.tursoDB -} - -// Modified methods -func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { - if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso" { - return repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()) - } - return repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()) -} - -func (container *Container) HeartbeatMonitorRepository() repositories.HeartbeatMonitorRepository { - if os.Getenv("HEARTBEAT_DB_BACKEND") == "turso" { - return repositories.NewLibsqlHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.TursoDB()) - } - return repositories.NewGormHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.DedicatedDB()) -} -``` - -## Safety - -- When `HEARTBEAT_DB_BACKEND != "turso"`, `TursoDB()` is never called — no Turso connection is opened -- The existing PostgreSQL path remains the default and is completely unaffected -- Both implementations satisfy the same interface — the service layer requires no changes From f6be7f959ef746900bcd5f71e468a2de3ea2d294 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 09:04:18 +0000 Subject: [PATCH 156/381] fix(deps): bump @babel/plugin-transform-modules-systemjs in /web Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.24.7 to 7.29.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs) --- updated-dependencies: - dependency-name: "@babel/plugin-transform-modules-systemjs" dependency-version: 7.29.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/pnpm-lock.yaml | 154 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 22 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f15a174a1..97a0d3bca 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: specifier: ^8.4.0 version: 8.4.0 qrcode: - specifier: ^1.5.0 + specifier: ^1.5.4 version: 1.5.4 ufo: specifier: ^1.6.4 @@ -263,6 +263,10 @@ packages: resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.22.5': resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} @@ -352,6 +356,10 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.24.7': resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} engines: {node: '>=6.9.0'} @@ -364,6 +372,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.22.5': resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -376,6 +390,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.24.7': resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} engines: {node: '>=6.9.0'} @@ -473,6 +491,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} engines: {node: '>=6.9.0'} @@ -807,8 +830,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.24.7': - resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} + '@babel/plugin-transform-modules-systemjs@7.29.4': + resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1001,6 +1024,10 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.24.7': resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} @@ -1009,6 +1036,10 @@ packages: resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} @@ -1017,6 +1048,10 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2522,9 +2557,11 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -7427,8 +7464,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: @@ -9376,6 +9413,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.22.5': dependencies: '@babel/types': 7.28.4 @@ -9386,7 +9431,7 @@ snapshots: '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.29.0 '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -9488,7 +9533,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.24.7': dependencies: - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.29.0 '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -9507,6 +9552,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9536,6 +9588,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.6(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.22.5': dependencies: '@babel/types': 7.28.4 @@ -9546,6 +9607,8 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -9613,7 +9676,7 @@ snapshots: dependencies: '@babel/helper-function-name': 7.24.7 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.29.0 '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -9653,6 +9716,10 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 @@ -10072,13 +10139,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -10299,7 +10366,7 @@ snapshots: '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7) '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.7) '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.24.7) '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.7) '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.7) @@ -10366,6 +10433,12 @@ snapshots: '@babel/parser': 7.28.4 '@babel/types': 7.28.4 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@babel/traverse@7.24.7': dependencies: '@babel/code-frame': 7.27.1 @@ -10393,6 +10466,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -10403,6 +10488,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@commitlint/cli@20.4.0(@types/node@25.6.0)(typescript@4.9.5)': @@ -12138,11 +12228,11 @@ snapshots: cache-loader: 4.1.0(webpack@4.47.0) caniuse-lite: 1.0.30001639 consola: 3.2.3 - css-loader: 5.2.7(webpack@5.104.1) + css-loader: 5.2.7(webpack@4.47.0) cssnano: 7.0.3(postcss@8.5.6) eventsource-polyfill: 0.9.6 extract-css-chunks-webpack-plugin: 4.10.0(webpack@4.47.0) - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@4.47.0) glob: 8.1.0 hard-source-webpack-plugin: 0.13.1(webpack@4.47.0) hash-sum: 2.0.0 @@ -12167,8 +12257,8 @@ snapshots: time-fix-plugin: 2.0.7(webpack@4.47.0) ufo: 1.6.4 upath: 2.0.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) - vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0) + vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) vue-style-loader: 4.1.3 vue-template-compiler: 2.7.16 watchpack: 2.5.0 @@ -14298,6 +14388,20 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 + css-loader@5.2.7(webpack@4.47.0): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + loader-utils: 2.0.4 + postcss: 8.5.6 + postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) + postcss-modules-scope: 3.0.0(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.7.3 + webpack: 4.47.0 + css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -15314,6 +15418,12 @@ snapshots: dependencies: flat-cache: 3.1.1 + file-loader@6.2.0(webpack@4.47.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 4.47.0 + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 @@ -18619,7 +18729,7 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 - postcss@8.5.13: + postcss@8.5.14: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -20151,7 +20261,7 @@ snapshots: urix@0.1.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 @@ -20226,7 +20336,7 @@ snapshots: vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 - postcss: 8.5.13 + postcss: 8.5.14 rollup: 3.30.0 optionalDependencies: '@types/node': 25.6.0 @@ -20298,7 +20408,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): + vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): dependencies: '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21) css-loader: 5.2.7(webpack@5.104.1) From 55a360898e95f03c25b64e90ee75ba618c402db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 09:04:22 +0000 Subject: [PATCH 157/381] fix(deps): bump fast-uri from 3.1.0 to 3.1.2 in /web Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/pnpm-lock.yaml | 52 +++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f15a174a1..bf0961770 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: specifier: ^8.4.0 version: 8.4.0 qrcode: - specifier: ^1.5.0 + specifier: ^1.5.4 version: 1.5.4 ufo: specifier: ^1.6.4 @@ -2522,9 +2522,11 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -4680,8 +4682,8 @@ packages: fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -7427,8 +7429,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: @@ -12138,11 +12140,11 @@ snapshots: cache-loader: 4.1.0(webpack@4.47.0) caniuse-lite: 1.0.30001639 consola: 3.2.3 - css-loader: 5.2.7(webpack@5.104.1) + css-loader: 5.2.7(webpack@4.47.0) cssnano: 7.0.3(postcss@8.5.6) eventsource-polyfill: 0.9.6 extract-css-chunks-webpack-plugin: 4.10.0(webpack@4.47.0) - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@4.47.0) glob: 8.1.0 hard-source-webpack-plugin: 0.13.1(webpack@4.47.0) hash-sum: 2.0.0 @@ -12167,8 +12169,8 @@ snapshots: time-fix-plugin: 2.0.7(webpack@4.47.0) ufo: 1.6.4 upath: 2.0.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) - vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0) + vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) vue-style-loader: 4.1.3 vue-template-compiler: 2.7.16 watchpack: 2.5.0 @@ -13203,7 +13205,7 @@ snapshots: ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -14298,6 +14300,20 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 + css-loader@5.2.7(webpack@4.47.0): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + loader-utils: 2.0.4 + postcss: 8.5.6 + postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) + postcss-modules-scope: 3.0.0(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.7.3 + webpack: 4.47.0 + css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -15284,7 +15300,7 @@ snapshots: fast-text-encoding@1.0.6: optional: true - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fastest-levenshtein@1.0.16: {} @@ -15314,6 +15330,12 @@ snapshots: dependencies: flat-cache: 3.1.1 + file-loader@6.2.0(webpack@4.47.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 4.47.0 + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 @@ -18619,7 +18641,7 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 - postcss@8.5.13: + postcss@8.5.14: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -20151,7 +20173,7 @@ snapshots: urix@0.1.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 @@ -20226,7 +20248,7 @@ snapshots: vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 - postcss: 8.5.13 + postcss: 8.5.14 rollup: 3.30.0 optionalDependencies: '@types/node': 25.6.0 @@ -20298,7 +20320,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): + vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): dependencies: '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21) css-loader: 5.2.7(webpack@5.104.1) From a4c94f7fae8dd2543fc4780bd547212708339994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 09:04:26 +0000 Subject: [PATCH 158/381] fix(deps): bump @protobufjs/utf8 from 1.1.0 to 1.1.1 in /web Bumps [@protobufjs/utf8](https://github.com/dcodeIO/protobuf.js) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/dcodeIO/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/dcodeIO/protobuf.js/compare/protobufjs-cli-v1.1.0...protobufjs-cli-v1.1.1) --- updated-dependencies: - dependency-name: "@protobufjs/utf8" dependency-version: 1.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/pnpm-lock.yaml | 56 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f15a174a1..6aaac1e75 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: specifier: ^8.4.0 version: 8.4.0 qrcode: - specifier: ^1.5.0 + specifier: ^1.5.4 version: 1.5.4 ufo: specifier: ^1.6.4 @@ -2226,8 +2226,8 @@ packages: '@protobufjs/pool@1.1.0': resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} '@rollup/pluginutils@4.2.1': resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} @@ -2522,9 +2522,11 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -7427,8 +7429,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: @@ -12138,11 +12140,11 @@ snapshots: cache-loader: 4.1.0(webpack@4.47.0) caniuse-lite: 1.0.30001639 consola: 3.2.3 - css-loader: 5.2.7(webpack@5.104.1) + css-loader: 5.2.7(webpack@4.47.0) cssnano: 7.0.3(postcss@8.5.6) eventsource-polyfill: 0.9.6 extract-css-chunks-webpack-plugin: 4.10.0(webpack@4.47.0) - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@4.47.0) glob: 8.1.0 hard-source-webpack-plugin: 0.13.1(webpack@4.47.0) hash-sum: 2.0.0 @@ -12167,8 +12169,8 @@ snapshots: time-fix-plugin: 2.0.7(webpack@4.47.0) ufo: 1.6.4 upath: 2.0.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) - vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0) + vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) vue-style-loader: 4.1.3 vue-template-compiler: 2.7.16 watchpack: 2.5.0 @@ -12389,7 +12391,7 @@ snapshots: '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.0': {} + '@protobufjs/utf8@1.1.1': {} '@rollup/pluginutils@4.2.1': dependencies: @@ -14298,6 +14300,20 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 + css-loader@5.2.7(webpack@4.47.0): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + loader-utils: 2.0.4 + postcss: 8.5.6 + postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) + postcss-modules-scope: 3.0.0(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.7.3 + webpack: 4.47.0 + css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -15314,6 +15330,12 @@ snapshots: dependencies: flat-cache: 3.1.1 + file-loader@6.2.0(webpack@4.47.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 4.47.0 + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 @@ -18619,7 +18641,7 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 - postcss@8.5.13: + postcss@8.5.14: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -18699,7 +18721,7 @@ snapshots: '@protobufjs/inquire': 1.1.0 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/long': 4.0.2 '@types/node': 25.6.0 long: 4.0.0 @@ -18716,7 +18738,7 @@ snapshots: '@protobufjs/inquire': 1.1.0 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/long': 4.0.2 '@types/node': 25.6.0 long: 4.0.0 @@ -18733,7 +18755,7 @@ snapshots: '@protobufjs/inquire': 1.1.0 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/node': 25.6.0 long: 5.2.3 @@ -20151,7 +20173,7 @@ snapshots: urix@0.1.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 @@ -20226,7 +20248,7 @@ snapshots: vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 - postcss: 8.5.13 + postcss: 8.5.14 rollup: 3.30.0 optionalDependencies: '@types/node': 25.6.0 @@ -20298,7 +20320,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): + vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): dependencies: '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21) css-loader: 5.2.7(webpack@5.104.1) From 3734230b64f58714c316dc812d6a02fd67a53be0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 09:04:29 +0000 Subject: [PATCH 159/381] fix(deps): bump protobufjs from 6.11.3 to 7.5.8 in /web Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 6.11.3 to 7.5.8. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.5.8/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/v6.11.3...protobufjs-v7.5.8) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 7.5.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/pnpm-lock.yaml | 91 +++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index f15a174a1..02320f5e0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: specifier: ^8.4.0 version: 8.4.0 qrcode: - specifier: ^1.5.0 + specifier: ^1.5.4 version: 1.5.4 ufo: specifier: ^1.6.4 @@ -2208,6 +2208,9 @@ packages: '@protobufjs/codegen@2.0.4': resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} @@ -2220,6 +2223,9 @@ packages: '@protobufjs/inquire@1.1.0': resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -2229,6 +2235,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@rollup/pluginutils@4.2.1': resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -2522,9 +2531,11 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -7427,8 +7438,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: @@ -7504,12 +7515,12 @@ packages: resolution: {integrity: sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==} hasBin: true - protobufjs@6.11.4: - resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} + protobufjs@6.11.6: + resolution: {integrity: sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==} hasBin: true - protobufjs@7.2.5: - resolution: {integrity: sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==} + protobufjs@7.5.8: + resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} engines: {node: '>=12.0.0'} protocols@2.0.1: @@ -11286,7 +11297,7 @@ snapshots: fast-deep-equal: 3.1.3 functional-red-black-tree: 1.0.1 google-gax: 2.30.5 - protobufjs: 6.11.4 + protobufjs: 6.11.6 transitivePeerDependencies: - encoding - supports-color @@ -11350,7 +11361,7 @@ snapshots: '@types/long': 4.0.2 lodash.camelcase: 4.3.0 long: 4.0.0 - protobufjs: 6.11.4 + protobufjs: 6.11.6 yargs: 16.2.0 optional: true @@ -11358,7 +11369,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.2.3 - protobufjs: 7.2.5 + protobufjs: 7.5.8 yargs: 17.7.2 '@humanwhocodes/config-array@0.13.0': @@ -12138,11 +12149,11 @@ snapshots: cache-loader: 4.1.0(webpack@4.47.0) caniuse-lite: 1.0.30001639 consola: 3.2.3 - css-loader: 5.2.7(webpack@5.104.1) + css-loader: 5.2.7(webpack@4.47.0) cssnano: 7.0.3(postcss@8.5.6) eventsource-polyfill: 0.9.6 extract-css-chunks-webpack-plugin: 4.10.0(webpack@4.47.0) - file-loader: 6.2.0(webpack@5.104.1) + file-loader: 6.2.0(webpack@4.47.0) glob: 8.1.0 hard-source-webpack-plugin: 0.13.1(webpack@4.47.0) hash-sum: 2.0.0 @@ -12167,8 +12178,8 @@ snapshots: time-fix-plugin: 2.0.7(webpack@4.47.0) ufo: 1.6.4 upath: 2.0.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0) - vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0) + vue-loader: 15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0) vue-style-loader: 4.1.3 vue-template-compiler: 2.7.16 watchpack: 2.5.0 @@ -12372,7 +12383,10 @@ snapshots: '@protobufjs/base64@1.1.2': {} - '@protobufjs/codegen@2.0.4': {} + '@protobufjs/codegen@2.0.4': + optional: true + + '@protobufjs/codegen@2.0.5': {} '@protobufjs/eventemitter@1.1.0': {} @@ -12385,11 +12399,16 @@ snapshots: '@protobufjs/inquire@1.1.0': {} + '@protobufjs/inquire@1.1.1': {} + '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.0': {} + '@protobufjs/utf8@1.1.0': + optional: true + + '@protobufjs/utf8@1.1.1': {} '@rollup/pluginutils@4.2.1': dependencies: @@ -14298,6 +14317,20 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 + css-loader@5.2.7(webpack@4.47.0): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + loader-utils: 2.0.4 + postcss: 8.5.6 + postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) + postcss-modules-scope: 3.0.0(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.7.3 + webpack: 4.47.0 + css-loader@5.2.7(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -15314,6 +15347,12 @@ snapshots: dependencies: flat-cache: 3.1.1 + file-loader@6.2.0(webpack@4.47.0): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 4.47.0 + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 @@ -18619,7 +18658,7 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 - postcss@8.5.13: + postcss@8.5.14: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -18685,7 +18724,7 @@ snapshots: proto3-json-serializer@0.1.9: dependencies: - protobufjs: 6.11.4 + protobufjs: 6.11.6 optional: true protobufjs@6.11.3: @@ -18705,7 +18744,7 @@ snapshots: long: 4.0.0 optional: true - protobufjs@6.11.4: + protobufjs@6.11.6: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -18722,18 +18761,18 @@ snapshots: long: 4.0.0 optional: true - protobufjs@7.2.5: + protobufjs@7.5.8: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 + '@protobufjs/codegen': 2.0.5 '@protobufjs/eventemitter': 1.1.0 '@protobufjs/fetch': 1.1.0 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/node': 25.6.0 long: 5.2.3 @@ -20151,7 +20190,7 @@ snapshots: urix@0.1.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@4.47.0))(webpack@4.47.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@4.47.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 @@ -20226,7 +20265,7 @@ snapshots: vite@4.5.3(@types/node@25.6.0)(sass@1.32.13)(terser@5.44.1): dependencies: esbuild: 0.18.20 - postcss: 8.5.13 + postcss: 8.5.14 rollup: 3.30.0 optionalDependencies: '@types/node': 25.6.0 @@ -20298,7 +20337,7 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@4.47.0))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): + vue-loader@15.11.1(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(cache-loader@4.1.0(webpack@4.47.0))(css-loader@5.2.7(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(prettier@3.8.1)(vue-template-compiler@2.7.16)(webpack@4.47.0): dependencies: '@vue/component-compiler-utils': 3.3.0(babel-core@7.0.0-bridge.0(@babel/core@7.28.4))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21) css-loader: 5.2.7(webpack@5.104.1) From a472abf2ba505a2637b194f8fb0185a956c9a476 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sat, 16 May 2026 13:43:03 +0300 Subject: [PATCH 160/381] feat: replace libSQL/Turso with MongoDB Atlas for heartbeat storage (#891) * feat: replace libSQL/Turso with MongoDB Atlas for heartbeat storage - Add MongoDB Go driver v2 repository implementations for HeartbeatRepository and HeartbeatMonitorRepository interfaces - Create mongodb.go connection helper with Atlas support and index creation - Update DI container to wire MongoDB as the hedging secondary backend - Replace 'turso' case with 'mongodb' case for standalone MongoDB usage - Update integration test docker-compose to use mongo:7 instead of sqld - Update .env.test with MongoDB connection string HEARTBEAT_DB_BACKEND options: - 'hedging': PostgreSQL primary, MongoDB secondary (fail-open writes) - 'mongodb': MongoDB only - default: PostgreSQL only (GORM) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove unused libSQL/Turso files and dependencies - Delete libsql.go, libsql_heartbeat_repository.go, libsql_heartbeat_monitor_repository.go - Remove TursoDB() method and tursoDB field from DI container - Remove unused database/sql import from container - Run go mod tidy to remove libsql-client-go dependency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use entity structs directly with bson tags instead of document types - Add bson struct tags to entities.Heartbeat and entities.HeartbeatMonitor - Register custom UUID codec so uuid.UUID is stored as string _id in MongoDB - Remove intermediate heartbeatDocument/heartbeatMonitorDocument structs - MongoDB repositories now marshal/unmarshal entities directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: derive MongoDB database name from URI appName parameter - Remove hardcoded mongoDBName constant - Parse appName query parameter from MONGODB_URI as the database name - NewMongoDB now returns (client, dbName, error) - Repository constructors accept dbName parameter - DI container caches and passes the DB name to repositories - Update test .env to include appName=httpsms in URI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: pass *mongo.Database directly to repository constructors - NewMongoDB now returns *mongo.Database instead of (*mongo.Client, dbName) - Repository constructors accept *mongo.Database instead of client + dbName - DI container caches the *mongo.Database singleton directly - Removes MongoDBName() helper - no longer needed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct outgoing message queue docs URL in settings Fix typo in URL: outgiong -> outgoing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add OpenTelemetry tracing to MongoDB client Use otelmongo.NewMonitor() as the command monitor on the MongoDB client to automatically trace all database operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review comments - Remove MongoDB URI from Ping error message to prevent credential exposure in logs and error aggregators - Use separate contexts for Ping (10s) and index creation (30s) so they don't share a timeout budget Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update send schedule docs link to correct URL Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update CI workflow to check MongoDB instead of sqld Replace the sqld health check with a MongoDB mongosh ping check since the test infrastructure now uses mongo:7 instead of sqld. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/api.yml | 12 +- api/go.mod | 16 +- api/go.sum | 32 +-- api/pkg/di/container.go | 40 ++-- api/pkg/entities/heartbeat.go | 12 +- api/pkg/entities/heartbeat_monitor.go | 16 +- api/pkg/repositories/libsql.go | 67 ------ .../libsql_heartbeat_monitor_repository.go | 199 ------------------ .../libsql_heartbeat_repository.go | 186 ---------------- .../mongo_heartbeat_monitor_repository.go | 182 ++++++++++++++++ .../mongo_heartbeat_repository.go | 135 ++++++++++++ api/pkg/repositories/mongodb.go | 127 +++++++++++ tests/.env.test | 2 +- tests/docker-compose.yml | 19 +- web/pages/settings/index.vue | 18 +- 15 files changed, 539 insertions(+), 524 deletions(-) delete mode 100644 api/pkg/repositories/libsql.go delete mode 100644 api/pkg/repositories/libsql_heartbeat_monitor_repository.go delete mode 100644 api/pkg/repositories/libsql_heartbeat_repository.go create mode 100644 api/pkg/repositories/mongo_heartbeat_monitor_repository.go create mode 100644 api/pkg/repositories/mongo_heartbeat_repository.go create mode 100644 api/pkg/repositories/mongodb.go diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index e8044d5cb..849f5ec84 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -37,18 +37,18 @@ jobs: - name: Wait for services to be healthy working-directory: ./tests run: | - echo "Waiting for sqld to be healthy..." + echo "Waiting for MongoDB to be healthy..." for i in $(seq 1 20); do - if curl -sf http://localhost:8090/health >/dev/null 2>&1; then - echo "sqld is healthy!" + if docker compose exec mongodb mongosh --eval "db.runCommand('ping').ok" --quiet >/dev/null 2>&1; then + echo "MongoDB is healthy!" break fi if [ $i -eq 20 ]; then - echo "sqld failed to become healthy" - docker compose logs sqld + echo "MongoDB failed to become healthy" + docker compose logs mongodb exit 1 fi - echo "sqld attempt $i/20 - waiting 3s..." + echo "MongoDB attempt $i/20 - waiting 3s..." sleep 3 done diff --git a/api/go.mod b/api/go.mod index d77c0bf69..1fe7dca1f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -44,9 +44,10 @@ require ( github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 - github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885 github.com/uptrace/uptrace-go v1.43.0 github.com/xuri/excelize/v2 v2.10.1 + go.mongodb.org/mongo-driver/v2 v2.6.0 + go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 @@ -94,13 +95,11 @@ require ( github.com/PuerkitoBio/goquery v1.12.0 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect - github.com/coder/websocket v1.8.12 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/fatih/color v1.19.0 // indirect @@ -164,8 +163,12 @@ require ( github.com/valyala/fasthttp v1.71.0 // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.33.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib v1.43.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect @@ -185,13 +188,12 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/api/go.sum b/api/go.sum index 0710b094a..fa1163a2f 100644 --- a/api/go.sum +++ b/api/go.sum @@ -66,8 +66,6 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -90,8 +88,6 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -324,8 +320,6 @@ github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOj github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= -github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885 h1:YssVXwM/9nUAjGNmUWdgvb05JVcsaBrDn5yr+MaJTn0= -github.com/tursodatabase/libsql-client-go v0.0.0-20260514053736-a9a8fadfe885/go.mod h1:08inkKyguB6CGGssc/JzhmQWwBgFQBgjlYFjxjRh7nU= github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLlyQO7U= github.com/uptrace/uptrace-go v1.43.0/go.mod h1:ehDTIdtBSolg4Z0CCvg1C8yR6VX1YFDqBcg2KmsXWn0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -336,6 +330,12 @@ github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= github.com/vanng822/go-premailer v1.33.0 h1:nglIpKn/7e3kIAwYByiH5xpauFur7RwAucqyZ59hcic= github.com/vanng822/go-premailer v1.33.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= @@ -344,17 +344,23 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8= +go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib v1.43.0 h1:rv+pngknCr4qpZDxSpEvEoRioutgfbkk82x6MChJQ3U= go.opentelemetry.io/contrib v1.43.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e h1:OX282aWfZNOrSVUPF59HlRhyA+MDcyi4kI8WWXt6A8I= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e/go.mod h1:lw7VQzmNsmkZBRQqOQiREGxO3GtzG/pOVEmKufablmA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= @@ -414,10 +420,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -462,8 +466,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -483,8 +487,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index ce6a82e2f..e9ca3718d 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -3,7 +3,6 @@ package di import ( "context" "crypto/tls" - "database/sql" "fmt" "net/http" "os" @@ -74,6 +73,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/handlers" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + mongoDriver "go.mongodb.org/mongo-driver/v2/mongo" "gorm.io/driver/postgres" gormLogger "gorm.io/gorm/logger" ) @@ -83,7 +83,7 @@ type Container struct { projectID string db *gorm.DB dedicatedDB *gorm.DB - tursoDB *sql.DB + mongoDB *mongoDriver.Database version string app *fiber.App eventDispatcher *services.EventDispatcher @@ -293,21 +293,21 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { return container.dedicatedDB } -// TursoDB creates a *sql.DB connection to a Turso/libSQL database -func (container *Container) TursoDB() *sql.DB { - if container.tursoDB != nil { - return container.tursoDB +// MongoDB creates a *mongo.Database connection to MongoDB Atlas +func (container *Container) MongoDB() *mongoDriver.Database { + if container.mongoDB != nil { + return container.mongoDB } - container.logger.Debug("creating Turso *sql.DB connection") + container.logger.Debug("creating MongoDB *mongo.Database connection") - db, err := repositories.NewTursoDB(os.Getenv("TURSO_DATABASE_DSN")) + db, err := repositories.NewMongoDB(os.Getenv("MONGODB_URI")) if err != nil { container.logger.Fatal(err) } - container.tursoDB = db - return container.tursoDB + container.mongoDB = db + return container.mongoDB } // HedgingFailureCounter creates an OTel counter for hedging secondary write failures @@ -922,12 +922,12 @@ func (container *Container) MessageThreadRepository() (repository repositories.M // HeartbeatMonitorRepository creates a new instance of repositories.HeartbeatMonitorRepository func (container *Container) HeartbeatMonitorRepository() (repository repositories.HeartbeatMonitorRepository) { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "turso": - container.logger.Debug("creating libSQL repositories.HeartbeatMonitorRepository") - return repositories.NewLibsqlHeartbeatMonitorRepository( + case "mongodb": + container.logger.Debug("creating MongoDB repositories.HeartbeatMonitorRepository") + return repositories.NewMongoHeartbeatMonitorRepository( container.Logger(), container.Tracer(), - container.TursoDB(), + container.MongoDB(), ) case "hedging": container.logger.Debug("creating hedging repositories.HeartbeatMonitorRepository") @@ -935,7 +935,7 @@ func (container *Container) HeartbeatMonitorRepository() (repository repositorie container.Logger(), container.Tracer(), repositories.NewGormHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewLibsqlHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.TursoDB()), + repositories.NewMongoHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.MongoDB()), container.HedgingFailureCounter(), ) default: @@ -1760,12 +1760,12 @@ func (container *Container) RegisterSwaggerRoutes() { // HeartbeatRepository registers a new instance of repositories.HeartbeatRepository func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "turso": - container.logger.Debug("creating libSQL repositories.HeartbeatRepository") - return repositories.NewLibsqlHeartbeatRepository( + case "mongodb": + container.logger.Debug("creating MongoDB repositories.HeartbeatRepository") + return repositories.NewMongoHeartbeatRepository( container.Logger(), container.Tracer(), - container.TursoDB(), + container.MongoDB(), ) case "hedging": container.logger.Debug("creating hedging repositories.HeartbeatRepository") @@ -1773,7 +1773,7 @@ func (container *Container) HeartbeatRepository() repositories.HeartbeatReposito container.Logger(), container.Tracer(), repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewLibsqlHeartbeatRepository(container.Logger(), container.Tracer(), container.TursoDB()), + repositories.NewMongoHeartbeatRepository(container.Logger(), container.Tracer(), container.MongoDB()), container.HedgingFailureCounter(), ) default: diff --git a/api/pkg/entities/heartbeat.go b/api/pkg/entities/heartbeat.go index 629efd297..abc070e90 100644 --- a/api/pkg/entities/heartbeat.go +++ b/api/pkg/entities/heartbeat.go @@ -8,10 +8,10 @@ import ( // Heartbeat represents is a pulse from an active phone type Heartbeat struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" example:"+18005550199"` - Version string `json:"version" example:"344c10f"` - Charging bool `json:"charging" example:"true"` - UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` - Timestamp time.Time `json:"timestamp" gorm:"index:idx_heartbeats_owner_timestamp" example:"2022-06-05T14:26:01.520828+03:00"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" bson:"_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" bson:"owner" example:"+18005550199"` + Version string `json:"version" bson:"version" example:"344c10f"` + Charging bool `json:"charging" bson:"charging" example:"true"` + UserID UserID `json:"user_id" bson:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` + Timestamp time.Time `json:"timestamp" gorm:"index:idx_heartbeats_owner_timestamp" bson:"timestamp" example:"2022-06-05T14:26:01.520828+03:00"` } diff --git a/api/pkg/entities/heartbeat_monitor.go b/api/pkg/entities/heartbeat_monitor.go index 6b41a31ad..7151f1952 100644 --- a/api/pkg/entities/heartbeat_monitor.go +++ b/api/pkg/entities/heartbeat_monitor.go @@ -8,14 +8,14 @@ import ( // HeartbeatMonitor is used to monitor heartbeats of a phone type HeartbeatMonitor struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - PhoneID uuid.UUID `json:"phone_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` - UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` - QueueID string `json:"queue_id" example:"0360259236613675274"` - Owner string `json:"owner" example:"+18005550199"` - PhoneOnline bool `json:"phone_online" example:"true" default:"true"` - CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"` - UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"` + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" bson:"_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + PhoneID uuid.UUID `json:"phone_id" bson:"phone_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` + UserID UserID `json:"user_id" bson:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` + QueueID string `json:"queue_id" bson:"queue_id" example:"0360259236613675274"` + Owner string `json:"owner" bson:"owner" example:"+18005550199"` + PhoneOnline bool `json:"phone_online" bson:"phone_online" example:"true" default:"true"` + CreatedAt time.Time `json:"created_at" bson:"created_at" example:"2022-06-05T14:26:02.302718+03:00"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"` } // RequiresCheck returns true if the heartbeat monitor requires a check diff --git a/api/pkg/repositories/libsql.go b/api/pkg/repositories/libsql.go deleted file mode 100644 index 66ee8c0eb..000000000 --- a/api/pkg/repositories/libsql.go +++ /dev/null @@ -1,67 +0,0 @@ -package repositories - -import ( - "database/sql" - "fmt" - - _ "github.com/tursodatabase/libsql-client-go/libsql" // libSQL database driver - - "github.com/palantir/stacktrace" -) - -const ( - tableHeartbeats = "heartbeats" - tableHeartbeatMonitors = "heartbeat_monitors" -) - -// NewTursoDB creates a new *sql.DB connection to a Turso database and auto-creates tables -func NewTursoDB(dsn string) (*sql.DB, error) { - db, err := sql.Open("libsql", dsn) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot open turso database with DSN [%s]", dsn)) - } - - if err = db.Ping(); err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot ping turso database with DSN [%s]", dsn)) - } - - if err = createTursoTables(db); err != nil { - return nil, stacktrace.Propagate(err, "cannot create turso tables") - } - - return db, nil -} - -func createTursoTables(db *sql.DB) error { - statements := []string{ - `CREATE TABLE IF NOT EXISTS ` + tableHeartbeats + ` ( - id TEXT PRIMARY KEY, - owner TEXT NOT NULL, - version TEXT NOT NULL, - charging INTEGER NOT NULL DEFAULT 0, - user_id TEXT NOT NULL, - timestamp DATETIME NOT NULL - )`, - `CREATE INDEX IF NOT EXISTS idx_heartbeats_owner_timestamp ON ` + tableHeartbeats + `(owner, timestamp)`, - `CREATE INDEX IF NOT EXISTS idx_heartbeats_user_id ON ` + tableHeartbeats + `(user_id)`, - `CREATE TABLE IF NOT EXISTS ` + tableHeartbeatMonitors + ` ( - id TEXT PRIMARY KEY, - phone_id TEXT NOT NULL, - user_id TEXT NOT NULL, - queue_id TEXT NOT NULL DEFAULT '', - owner TEXT NOT NULL, - phone_online INTEGER NOT NULL DEFAULT 1, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL - )`, - `CREATE INDEX IF NOT EXISTS idx_heartbeat_monitors_user_owner ON ` + tableHeartbeatMonitors + `(user_id, owner)`, - } - - for _, stmt := range statements { - if _, err := db.Exec(stmt); err != nil { - return stacktrace.Propagate(err, fmt.Sprintf("cannot execute statement: %s", stmt)) - } - } - - return nil -} diff --git a/api/pkg/repositories/libsql_heartbeat_monitor_repository.go b/api/pkg/repositories/libsql_heartbeat_monitor_repository.go deleted file mode 100644 index 0cb594af1..000000000 --- a/api/pkg/repositories/libsql_heartbeat_monitor_repository.go +++ /dev/null @@ -1,199 +0,0 @@ -package repositories - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/google/uuid" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// libsqlHeartbeatMonitorRepository is responsible for persisting entities.HeartbeatMonitor in Turso/libSQL -type libsqlHeartbeatMonitorRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - db *sql.DB -} - -// NewLibsqlHeartbeatMonitorRepository creates the libSQL version of the HeartbeatMonitorRepository -func NewLibsqlHeartbeatMonitorRepository( - logger telemetry.Logger, - tracer telemetry.Tracer, - db *sql.DB, -) HeartbeatMonitorRepository { - return &libsqlHeartbeatMonitorRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &libsqlHeartbeatMonitorRepository{})), - tracer: tracer, - db: db, - } -} - -func (repository *libsqlHeartbeatMonitorRepository) Store(ctx context.Context, monitor *entities.HeartbeatMonitor) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, - "INSERT INTO "+tableHeartbeatMonitors+" (id, phone_id, user_id, queue_id, owner, phone_online, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - monitor.ID.String(), - monitor.PhoneID.String(), - string(monitor.UserID), - monitor.QueueID, - monitor.Owner, - boolToInt(monitor.PhoneOnline), - monitor.CreatedAt.UTC(), - monitor.UpdatedAt.UTC(), - ) - if err != nil { - msg := fmt.Sprintf("cannot save heartbeat monitor with ID [%s]", monitor.ID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error) { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - row := repository.db.QueryRowContext(ctx, - "SELECT id, phone_id, user_id, queue_id, owner, phone_online, created_at, updated_at FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND owner = ? LIMIT 1", - string(userID), phoneNumber, - ) - - monitor, err := repository.scanHeartbeatMonitorRow(row) - if err == sql.ErrNoRows { - msg := fmt.Sprintf("heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) - } - if err != nil { - msg := fmt.Sprintf("cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return monitor, nil -} - -func (repository *libsqlHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, monitorID uuid.UUID) (bool, error) { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - var count int - err := repository.db.QueryRowContext(ctx, - "SELECT COUNT(*) FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND id = ?", - string(userID), monitorID.String(), - ).Scan(&count) - if err != nil { - msg := fmt.Sprintf("cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID) - return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return count > 0, nil -} - -func (repository *libsqlHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, - "UPDATE "+tableHeartbeatMonitors+" SET queue_id = ?, updated_at = ? WHERE id = ?", - queueID, time.Now().UTC(), monitorID.String(), - ) - if err != nil { - msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s]", monitorID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, - "DELETE FROM "+tableHeartbeatMonitors+" WHERE user_id = ? AND owner = ?", - string(userID), phoneNumber, - ) - if err != nil { - msg := fmt.Sprintf("cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, - "UPDATE "+tableHeartbeatMonitors+" SET phone_online = ?, updated_at = ? WHERE id = ? AND user_id = ?", - boolToInt(online), time.Now().UTC(), monitorID.String(), string(userID), - ) - if err != nil { - msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatMonitorRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, "DELETE FROM "+tableHeartbeatMonitors+" WHERE user_id = ?", string(userID)) - if err != nil { - msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatMonitorRepository) scanHeartbeatMonitorRow(row *sql.Row) (*entities.HeartbeatMonitor, error) { - monitor := new(entities.HeartbeatMonitor) - var id, phoneID, userID string - var phoneOnline int - err := row.Scan(&id, &phoneID, &userID, &monitor.QueueID, &monitor.Owner, &phoneOnline, &monitor.CreatedAt, &monitor.UpdatedAt) - if err != nil { - return nil, err - } - monitor.ID, err = uuid.Parse(id) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat monitor ID [%s]", id)) - } - monitor.PhoneID, err = uuid.Parse(phoneID) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat monitor phone ID [%s]", phoneID)) - } - monitor.UserID = entities.UserID(userID) - monitor.PhoneOnline = phoneOnline != 0 - return monitor, nil -} diff --git a/api/pkg/repositories/libsql_heartbeat_repository.go b/api/pkg/repositories/libsql_heartbeat_repository.go deleted file mode 100644 index 42fdf911c..000000000 --- a/api/pkg/repositories/libsql_heartbeat_repository.go +++ /dev/null @@ -1,186 +0,0 @@ -package repositories - -import ( - "context" - "database/sql" - "fmt" - - "github.com/google/uuid" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// libsqlHeartbeatRepository is responsible for persisting entities.Heartbeat in Turso/libSQL -type libsqlHeartbeatRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - db *sql.DB -} - -// NewLibsqlHeartbeatRepository creates the libSQL version of the HeartbeatRepository -func NewLibsqlHeartbeatRepository( - logger telemetry.Logger, - tracer telemetry.Tracer, - db *sql.DB, -) HeartbeatRepository { - return &libsqlHeartbeatRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &libsqlHeartbeatRepository{})), - tracer: tracer, - db: db, - } -} - -func (repository *libsqlHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { - ctx, span, _ := repository.tracer.StartWithLogger(ctx, repository.logger) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, - "INSERT INTO "+tableHeartbeats+" (id, owner, version, charging, user_id, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - heartbeat.ID.String(), - heartbeat.Owner, - heartbeat.Version, - boolToInt(heartbeat.Charging), - string(heartbeat.UserID), - heartbeat.Timestamp.UTC(), - ) - if err != nil { - msg := fmt.Sprintf("cannot save heartbeat with ID [%s]", heartbeat.ID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - var rows *sql.Rows - var err error - - if len(params.Query) > 0 { - queryPattern := "%" + params.Query + "%" - rows, err = repository.db.QueryContext(ctx, - "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? AND version LIKE ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", - string(userID), owner, queryPattern, params.Limit, params.Skip, - ) - } else { - rows, err = repository.db.QueryContext(ctx, - "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", - string(userID), owner, params.Limit, params.Skip, - ) - } - if err != nil { - msg := fmt.Sprintf("cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - defer rows.Close() - - heartbeats := make([]entities.Heartbeat, 0) - for rows.Next() { - heartbeat, scanErr := repository.scanHeartbeat(rows) - if scanErr != nil { - msg := fmt.Sprintf("cannot scan heartbeat row for owner [%s]", owner) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(scanErr, msg)) - } - heartbeats = append(heartbeats, *heartbeat) - } - if rowsErr := rows.Err(); rowsErr != nil { - msg := fmt.Sprintf("error iterating heartbeat rows for owner [%s]", owner) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(rowsErr, msg)) - } - - return &heartbeats, nil -} - -func (repository *libsqlHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - row := repository.db.QueryRowContext(ctx, - "SELECT id, owner, version, charging, user_id, timestamp FROM "+tableHeartbeats+" WHERE user_id = ? AND owner = ? ORDER BY timestamp DESC LIMIT 1", - string(userID), owner, - ) - - heartbeat, err := repository.scanHeartbeatRow(row) - if err == sql.ErrNoRows { - msg := fmt.Sprintf("heartbeat with userID [%s] and owner [%s] does not exist", userID, owner) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) - } - if err != nil { - msg := fmt.Sprintf("cannot load heartbeat with userID [%s] and owner [%s]", userID, owner) - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return heartbeat, nil -} - -func (repository *libsqlHeartbeatRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) - defer cancel() - - _, err := repository.db.ExecContext(ctx, "DELETE FROM "+tableHeartbeats+" WHERE user_id = ?", string(userID)) - if err != nil { - msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID) - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) - } - - return nil -} - -func (repository *libsqlHeartbeatRepository) scanHeartbeat(rows *sql.Rows) (*entities.Heartbeat, error) { - heartbeat := new(entities.Heartbeat) - var id string - var charging int - var userID string - err := rows.Scan(&id, &heartbeat.Owner, &heartbeat.Version, &charging, &userID, &heartbeat.Timestamp) - if err != nil { - return nil, err - } - heartbeat.ID, err = uuid.Parse(id) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat ID [%s]", id)) - } - heartbeat.Charging = charging != 0 - heartbeat.UserID = entities.UserID(userID) - return heartbeat, nil -} - -func (repository *libsqlHeartbeatRepository) scanHeartbeatRow(row *sql.Row) (*entities.Heartbeat, error) { - heartbeat := new(entities.Heartbeat) - var id string - var charging int - var userID string - err := row.Scan(&id, &heartbeat.Owner, &heartbeat.Version, &charging, &userID, &heartbeat.Timestamp) - if err != nil { - return nil, err - } - heartbeat.ID, err = uuid.Parse(id) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse heartbeat ID [%s]", id)) - } - heartbeat.Charging = charging != 0 - heartbeat.UserID = entities.UserID(userID) - return heartbeat, nil -} - -func boolToInt(b bool) int { - if b { - return 1 - } - return 0 -} diff --git a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go new file mode 100644 index 000000000..13200c073 --- /dev/null +++ b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go @@ -0,0 +1,182 @@ +package repositories + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// mongoHeartbeatMonitorRepository is responsible for persisting entities.HeartbeatMonitor in MongoDB +type mongoHeartbeatMonitorRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + collection *mongo.Collection +} + +// NewMongoHeartbeatMonitorRepository creates the MongoDB version of the HeartbeatMonitorRepository +func NewMongoHeartbeatMonitorRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + db *mongo.Database, +) HeartbeatMonitorRepository { + return &mongoHeartbeatMonitorRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &mongoHeartbeatMonitorRepository{})), + tracer: tracer, + collection: db.Collection(collectionHeartbeatMonitors), + } +} + +func (repository *mongoHeartbeatMonitorRepository) Store(ctx context.Context, monitor *entities.HeartbeatMonitor) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.collection.InsertOne(ctx, monitor) + if err != nil { + msg := fmt.Sprintf("cannot save heartbeat monitor with ID [%s]", monitor.ID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *mongoHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"user_id", string(userID)}, + {"owner", phoneNumber}, + } + + var monitor entities.HeartbeatMonitor + err := repository.collection.FindOne(ctx, filter).Decode(&monitor) + if err == mongo.ErrNoDocuments { + msg := fmt.Sprintf("heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) + } + if err != nil { + msg := fmt.Sprintf("cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return &monitor, nil +} + +func (repository *mongoHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, monitorID uuid.UUID) (bool, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"user_id", string(userID)}, + {"_id", monitorID.String()}, + } + + count, err := repository.collection.CountDocuments(ctx, filter) + if err != nil { + msg := fmt.Sprintf("cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID) + return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return count > 0, nil +} + +func (repository *mongoHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{{"_id", monitorID.String()}} + update := bson.D{{"$set", bson.D{ + {"queue_id", queueID}, + {"updated_at", time.Now().UTC()}, + }}} + + _, err := repository.collection.UpdateOne(ctx, filter, update) + if err != nil { + msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s]", monitorID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *mongoHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"user_id", string(userID)}, + {"owner", phoneNumber}, + } + + _, err := repository.collection.DeleteMany(ctx, filter) + if err != nil { + msg := fmt.Sprintf("cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *mongoHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"_id", monitorID.String()}, + {"user_id", string(userID)}, + } + update := bson.D{{"$set", bson.D{ + {"phone_online", online}, + {"updated_at", time.Now().UTC()}, + }}} + + _, err := repository.collection.UpdateOne(ctx, filter, update) + if err != nil { + msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *mongoHeartbeatMonitorRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.collection.DeleteMany(ctx, bson.D{{"user_id", string(userID)}}) + if err != nil { + msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} diff --git a/api/pkg/repositories/mongo_heartbeat_repository.go b/api/pkg/repositories/mongo_heartbeat_repository.go new file mode 100644 index 000000000..d8a7839c8 --- /dev/null +++ b/api/pkg/repositories/mongo_heartbeat_repository.go @@ -0,0 +1,135 @@ +package repositories + +import ( + "context" + "fmt" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/palantir/stacktrace" +) + +// mongoHeartbeatRepository is responsible for persisting entities.Heartbeat in MongoDB +type mongoHeartbeatRepository struct { + logger telemetry.Logger + tracer telemetry.Tracer + collection *mongo.Collection +} + +// NewMongoHeartbeatRepository creates the MongoDB version of the HeartbeatRepository +func NewMongoHeartbeatRepository( + logger telemetry.Logger, + tracer telemetry.Tracer, + db *mongo.Database, +) HeartbeatRepository { + return &mongoHeartbeatRepository{ + logger: logger.WithService(fmt.Sprintf("%T", &mongoHeartbeatRepository{})), + tracer: tracer, + collection: db.Collection(collectionHeartbeats), + } +} + +func (repository *mongoHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { + ctx, span, _ := repository.tracer.StartWithLogger(ctx, repository.logger) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.collection.InsertOne(ctx, heartbeat) + if err != nil { + msg := fmt.Sprintf("cannot save heartbeat with ID [%s]", heartbeat.ID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} + +func (repository *mongoHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"user_id", string(userID)}, + {"owner", owner}, + } + + if len(params.Query) > 0 { + filter = append(filter, bson.E{"version", bson.D{{"$regex", params.Query}, {"$options", "i"}}}) + } + + opts := options.Find(). + SetSort(bson.D{{"timestamp", -1}}). + SetSkip(int64(params.Skip)). + SetLimit(int64(params.Limit)) + + cursor, err := repository.collection.Find(ctx, filter, opts) + if err != nil { + msg := fmt.Sprintf("cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + defer cursor.Close(ctx) + + var heartbeats []entities.Heartbeat + if err = cursor.All(ctx, &heartbeats); err != nil { + msg := fmt.Sprintf("cannot decode heartbeats for owner [%s]", owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if heartbeats == nil { + heartbeats = make([]entities.Heartbeat, 0) + } + + return &heartbeats, nil +} + +func (repository *mongoHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + filter := bson.D{ + {"user_id", string(userID)}, + {"owner", owner}, + } + + opts := options.FindOne().SetSort(bson.D{{"timestamp", -1}}) + + var heartbeat entities.Heartbeat + err := repository.collection.FindOne(ctx, filter, opts).Decode(&heartbeat) + if err == mongo.ErrNoDocuments { + msg := fmt.Sprintf("heartbeat with userID [%s] and owner [%s] does not exist", userID, owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) + } + if err != nil { + msg := fmt.Sprintf("cannot load heartbeat with userID [%s] and owner [%s]", userID, owner) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return &heartbeat, nil +} + +func (repository *mongoHeartbeatRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + ctx, cancel := context.WithTimeout(ctx, dbOperationDuration) + defer cancel() + + _, err := repository.collection.DeleteMany(ctx, bson.D{{"user_id", string(userID)}}) + if err != nil { + msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return nil +} diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go new file mode 100644 index 000000000..f2f65ea91 --- /dev/null +++ b/api/pkg/repositories/mongodb.go @@ -0,0 +1,127 @@ +package repositories + +import ( + "context" + "fmt" + "net/url" + "reflect" + "time" + + "github.com/google/uuid" + "github.com/palantir/stacktrace" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo" +) + +const ( + collectionHeartbeats = "heartbeats" + collectionHeartbeatMonitors = "heartbeat_monitors" +) + +// uuidEncodeValue encodes uuid.UUID as a BSON string +func uuidEncodeValue(_ bson.EncodeContext, vw bson.ValueWriter, val reflect.Value) error { + u := val.Interface().(uuid.UUID) + return vw.WriteString(u.String()) +} + +// uuidDecodeValue decodes a BSON string into uuid.UUID +func uuidDecodeValue(_ bson.DecodeContext, vr bson.ValueReader, val reflect.Value) error { + str, err := vr.ReadString() + if err != nil { + return err + } + parsed, err := uuid.Parse(str) + if err != nil { + return err + } + val.Set(reflect.ValueOf(parsed)) + return nil +} + +// newMongoRegistry creates a BSON registry that encodes uuid.UUID as strings +func newMongoRegistry() *bson.Registry { + rb := bson.NewRegistry() + rb.RegisterTypeEncoder(reflect.TypeOf(uuid.UUID{}), bson.ValueEncoderFunc(uuidEncodeValue)) + rb.RegisterTypeDecoder(reflect.TypeOf(uuid.UUID{}), bson.ValueDecoderFunc(uuidDecodeValue)) + return rb +} + +// NewMongoDB creates a new *mongo.Database connection to MongoDB Atlas and ensures indexes. +// The database name is derived from the appName query parameter in the URI. +func NewMongoDB(uri string) (*mongo.Database, error) { + dbName, err := parseMongoDBName(uri) + if err != nil { + return nil, stacktrace.Propagate(err, "cannot parse database name from MongoDB URI") + } + + pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer pingCancel() + + serverAPI := options.ServerAPI(options.ServerAPIVersion1) + registry := newMongoRegistry() + opts := options.Client(). + ApplyURI(uri). + SetServerAPIOptions(serverAPI). + SetRegistry(registry). + SetMonitor(otelmongo.NewMonitor()) + + client, err := mongo.Connect(opts) + if err != nil { + return nil, stacktrace.Propagate(err, "cannot connect to MongoDB Atlas") + } + + if err = client.Ping(pingCtx, nil); err != nil { + return nil, stacktrace.Propagate(err, "cannot ping MongoDB Atlas") + } + + db := client.Database(dbName) + + indexCtx, indexCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer indexCancel() + + if err = createMongoIndexes(indexCtx, db); err != nil { + return nil, stacktrace.Propagate(err, "cannot create MongoDB indexes") + } + + return db, nil +} + +// parseMongoDBName extracts the appName query parameter from the MongoDB URI to use as the database name +func parseMongoDBName(uri string) (string, error) { + parsed, err := url.Parse(uri) + if err != nil { + return "", stacktrace.Propagate(err, fmt.Sprintf("cannot parse MongoDB URI [%s]", uri)) + } + + appName := parsed.Query().Get("appName") + if appName == "" { + return "", stacktrace.NewError("MongoDB URI is missing the 'appName' query parameter which is used as the database name") + } + + return appName, nil +} + +func createMongoIndexes(ctx context.Context, db *mongo.Database) error { + // Heartbeats indexes + heartbeatsCol := db.Collection(collectionHeartbeats) + _, err := heartbeatsCol.Indexes().CreateMany(ctx, []mongo.IndexModel{ + {Keys: bson.D{{"owner", 1}, {"timestamp", -1}}}, + {Keys: bson.D{{"user_id", 1}}}, + }) + if err != nil { + return stacktrace.Propagate(err, "cannot create indexes on heartbeats collection") + } + + // Heartbeat monitors indexes + monitorsCol := db.Collection(collectionHeartbeatMonitors) + _, err = monitorsCol.Indexes().CreateMany(ctx, []mongo.IndexModel{ + {Keys: bson.D{{"user_id", 1}, {"owner", 1}}}, + }) + if err != nil { + return stacktrace.Propagate(err, "cannot create indexes on heartbeat_monitors collection") + } + + return nil +} diff --git a/tests/.env.test b/tests/.env.test index 909902aec..ac18af920 100644 --- a/tests/.env.test +++ b/tests/.env.test @@ -29,4 +29,4 @@ GCS_BUCKET_NAME= UPTRACE_DSN= CLOUDFLARE_TURNSTILE_SECRET_KEY= HEARTBEAT_DB_BACKEND=hedging -TURSO_DATABASE_DSN=http://sqld:8080 +MONGODB_URI=mongodb://httpsms:testpassword@mongodb:27017/?authSource=admin&appName=httpsms diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 1e111b785..515b62983 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -25,10 +25,19 @@ services: timeout: 5s retries: 10 - sqld: - image: ghcr.io/tursodatabase/libsql-server:latest + mongodb: + image: mongo:7 ports: - - "8090:8080" + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: httpsms + MONGO_INITDB_ROOT_PASSWORD: testpassword + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s wiremock: image: wiremock/wiremock:3x @@ -58,8 +67,8 @@ services: condition: service_healthy wiremock: condition: service_healthy - sqld: - condition: service_started + mongodb: + condition: service_healthy env_file: - .env.test environment: diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index ebe955623..96aa83f33 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -385,7 +385,7 @@ delivered when the schedule opens according to your configured send rate.

@@ -435,10 +435,18 @@ - - {{ mdiCalendarClock }} - Create Send Schedule - +
+ + {{ mdiCalendarClock }} + Create Send Schedule + + Documentation +
Email Notifications From 2e84e46bdc6613ce42fc9298bca838f72b7aab4f Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sat, 16 May 2026 13:44:09 +0300 Subject: [PATCH 161/381] Remove unuxed docs --- outgoing-message-queue.md | 171 -------------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 outgoing-message-queue.md diff --git a/outgoing-message-queue.md b/outgoing-message-queue.md deleted file mode 100644 index 0e8ee1444..000000000 --- a/outgoing-message-queue.md +++ /dev/null @@ -1,171 +0,0 @@ -# Outgoing Message Queue - -Complete guide on how httpSMS queues outgoing SMS messages for reliable delivery, including rate-based dispatch, scheduled sending, and send schedule windows. - -## How the Message Queue Works - -When you send an SMS through httpSMS (via the API, bulk send, or Excel upload), messages don't go directly to your Android phone. Instead, they enter an **outgoing message queue** that intelligently schedules delivery to ensure reliability and prevent carrier throttling. - -The queue determines **when** each message is dispatched to your phone based on three factors: - -1. **Explicit send time** — If you specify a `send_at` time, the message is sent at exactly that time -2. **Rate-based dispatch delay** — Messages without a send time are spaced out based on your configured send rate -3. **Send schedule window** — Messages can be held until your configured active hours (if enabled) - -## 1. Explicit Send Time (Bypass Queue Logic) - -When you specify a `send_at` time in your API request or a `SendTime` column in your Excel upload, the message **bypasses** both rate-limiting and schedule window logic entirely. The message will be dispatched to your phone at exactly the time you specified. - -This is ideal for: - -- Time-sensitive alerts that must go out at a precise moment -- Promotional messages timed for a specific campaign window -- Appointment reminders scheduled for a specific time before the appointment - -### Sending a single message at a specific time - -```bash -curl -L \ - --request POST \ - --url 'https://api.httpsms.com/v1/messages/send' \ - --header 'Content-Type: application/json' \ - --header 'x-api-Key: YOUR_API_KEY' \ - --data '{ - "from": "+18005550199", - "to": "+18005550100", - "content": "Your appointment is in 1 hour", - "send_at": "2025-12-19T16:39:57-08:00" - }' -``` - -The `send_at` field accepts time in [RFC 3339 format](https://datatracker.ietf.org/doc/html/rfc3339) which includes the time zone (e.g., `1996-12-19T16:39:57-08:00`). You can schedule messages up to 20 days (480 hours) in the future. - -> **Note:** If you specify a `send_at` time that is in the past, the message will be sent immediately. - -### Setting send time in bulk Excel uploads - -When using the [bulk messages Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx), you can set the optional `SendTime(optional)` column to specify when each message should be sent. Use the format `YYYY-MM-DDTHH:MM:SS` in your local time zone (e.g., `2023-11-13T02:10:01`). - -Each row with a `SendTime` value will be dispatched at exactly that time, independent of other messages in the batch. - -## 2. Rate-Based Dispatch Delay - -When you send messages **without** a `send_at` time (especially in bulk), httpSMS automatically spaces out delivery based on your phone's configured **Messages Per Minute** rate. This prevents carrier throttling and ensures reliable delivery. - -### How rate-based dispatch works - -The system calculates a dispatch delay for each message based on its position in the batch: - -``` -interval = 60 seconds ÷ messages_per_minute -delay = message_index × interval -``` - -**Example:** If your phone is configured for 10 messages per minute: - -| Message | Index | Delay | Dispatched At | -| ------- | ----- | ----- | ------------- | -| 1st | 0 | 0s | Immediately | -| 2nd | 1 | 6s | +6 seconds | -| 3rd | 2 | 12s | +12 seconds | -| 4th | 3 | 18s | +18 seconds | -| 10th | 9 | 54s | +54 seconds | - -This ensures your phone sends at most 10 SMS per minute, matching the configured rate. - -### Per-phone indexing for bulk sends - -When sending bulk messages to multiple recipients from the same phone number, the index is calculated per phone. This means messages to different recipient numbers are all spaced according to the sending phone's rate, ensuring the sending phone isn't overwhelmed. - -When using Excel/CSV uploads with multiple sender phones (different `From` numbers), each phone gets its own independent index counter. Messages from Phone A don't affect the timing of messages from Phone B. - -### Configuring Messages Per Minute - -To modify the send rate for your phone number: - -1. Go to [https://httpsms.com/settings](https://httpsms.com/settings#phones) -2. Tap the **"EDIT"** button on the phone number -3. Update the **"Messages Per Minute"** value - -**Default:** 10 messages per minute for newly registered phones. - -**Maximum:** 29 messages per minute (the [maximum permitted by an unrooted Android phone](https://android.googlesource.com/platform/frameworks/opt/telephony/+/master/src/java/com/android/internal/telephony/SmsUsageMonitor.java#84)). - -> **Tip:** If you're sending large batches, a lower rate (5-10/min) is more reliable. Higher rates (20+/min) may trigger carrier spam filters depending on your region. - -## 3. Send Schedule Window - -The send schedule window allows you to restrict message delivery to specific hours of the day. When enabled, messages sent outside the configured window are held in the queue and dispatched when the next window opens. - -This is useful for: - -- Respecting recipient quiet hours (no messages at 3 AM) -- Complying with regional messaging regulations -- Concentrating delivery during business hours - -> **Important:** Messages with an explicit `send_at` time bypass the send schedule window entirely. Only messages without a specified send time are subject to window restrictions. - -### Configuring the Send Schedule - -You can configure the send schedule window for each phone number in your account settings at [https://httpsms.com/settings](https://httpsms.com/settings#phones). Click **"EDIT"** on the phone number and set: - -- **Schedule Active** — Enable or disable the schedule window -- **Start Time** — The time of day when sending begins (e.g., `08:00`) -- **End Time** — The time of day when sending stops (e.g., `21:00`) -- **Timezone** — The timezone for the schedule (e.g., `America/New_York`) - -### How the schedule window works - -| Current Time vs Window | Behavior | -| ---------------------- | ------------------------------------------------------ | -| Within window | Message dispatched immediately (subject to rate delay) | -| Before window opens | Message held until window start time | -| After window closes | Message held until next day's window start time | - -## Bulk Send via API - -When sending to multiple recipients using the bulk API endpoint, all messages are automatically queued with rate-based dispatch delays: - -```bash -curl -L \ - --request POST \ - --url 'https://api.httpsms.com/v1/messages/bulk-send' \ - --header 'Content-Type: application/json' \ - --header 'x-api-Key: YOUR_API_KEY' \ - --data '{ - "from": "+18005550199", - "to": ["+18005550100", "+18005550101", "+18005550102"], - "content": "Hello from httpSMS!" - }' -``` - -In this example, with a default rate of 10 messages/minute: - -- Message to `+18005550100` → sent immediately -- Message to `+18005550101` → sent after 6 seconds -- Message to `+18005550102` → sent after 12 seconds - -## Summary: Queue Decision Flow - -```mermaid -flowchart TD - A[Message received by httpSMS API] --> B{Has explicit send_at time?} - B -->|YES| C[Dispatch at exactly that time] - C --> D[Bypasses rate-limit AND schedule window] - B -->|NO| E[Calculate rate-based delay] - E --> F["delay = index × (60s ÷ messages_per_minute)"] - F --> G{Send schedule window enabled?} - G -->|YES| H{Within active window?} - G -->|NO| I[Dispatch with rate delay only] - H -->|YES| I - H -->|NO| J[Hold until window opens] - J --> I -``` - -## Key Points - -- **Explicit send time always wins** — Setting `send_at` bypasses all queue logic -- **Rate limiting prevents throttling** — Messages are spaced based on your configured rate -- **Schedule windows respect quiet hours** — Messages without a send time are held until the window opens -- **Per-phone independence** — Each sending phone has its own rate counter and schedule -- **Past send times are handled gracefully** — If `send_at` is in the past, the message sends immediately From 49901d0685d388fb3f0e29116d29447d3b56e2e5 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 13:26:54 +0300 Subject: [PATCH 162/381] feat: switch heartbeat repositories to use MongoDB directly (#892) Replace the hedging dual-write pattern with direct MongoDB usage for heartbeat monitors and heartbeats. When HEARTBEAT_DB_BACKEND is set to 'hedging', it now routes to MongoDB instead of the dual-write hedging repository. This is the first migration step before switching to MongoDB completely. - Remove hedging_heartbeat_monitor_repository.go - Remove hedging_heartbeat_repository.go - Remove HedgingFailureCounter from DI container - Route 'hedging' env value to MongoDB in both repository factories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 4 + api/go.sum | 10 ++ api/pkg/di/container.go | 39 +----- .../hedging_heartbeat_monitor_repository.go | 128 ------------------ .../hedging_heartbeat_repository.go | 79 ----------- 5 files changed, 16 insertions(+), 244 deletions(-) delete mode 100644 api/pkg/repositories/hedging_heartbeat_monitor_repository.go delete mode 100644 api/pkg/repositories/hedging_heartbeat_repository.go diff --git a/api/go.mod b/api/go.mod index 1fe7dca1f..08f6a5c01 100644 --- a/api/go.mod +++ b/api/go.mod @@ -41,6 +41,7 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.19.0 github.com/redis/go-redis/v9 v9.19.0 github.com/rs/zerolog v1.35.1 + github.com/schollz/progressbar/v3 v3.19.0 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 @@ -141,6 +142,7 @@ require ( github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-sqlite3 v1.14.44 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -153,6 +155,7 @@ require ( github.com/redis/go-redis/extra/rediscmd/v9 v9.19.0 // indirect github.com/richardlehane/mscfb v1.0.6 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -193,6 +196,7 @@ require ( golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/api/go.sum b/api/go.sum index fa1163a2f..0f6dcce52 100644 --- a/api/go.sum +++ b/api/go.sum @@ -78,6 +78,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -245,6 +247,8 @@ github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3Ry github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -291,10 +295,14 @@ github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= +github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -477,6 +485,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index e9ca3718d..6da12033c 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -310,23 +310,6 @@ func (container *Container) MongoDB() *mongoDriver.Database { return container.mongoDB } -// HedgingFailureCounter creates an OTel counter for hedging secondary write failures -func (container *Container) HedgingFailureCounter() otelMetric.Int64Counter { - meter := otel.GetMeterProvider().Meter( - container.projectID, - otelMetric.WithInstrumentationVersion(otel.Version()), - ) - counter, err := meter.Int64Counter( - "hedging.secondary.write.failures", - otelMetric.WithUnit("1"), - otelMetric.WithDescription("Number of failed secondary writes in hedging repositories"), - ) - if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create hedging failure counter")) - } - return counter -} - // DBWithoutMigration creates an instance of gorm.DB if it has not been created already func (container *Container) DBWithoutMigration() (db *gorm.DB) { if container.db != nil { @@ -922,22 +905,13 @@ func (container *Container) MessageThreadRepository() (repository repositories.M // HeartbeatMonitorRepository creates a new instance of repositories.HeartbeatMonitorRepository func (container *Container) HeartbeatMonitorRepository() (repository repositories.HeartbeatMonitorRepository) { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "mongodb": + case "mongodb", "hedging": container.logger.Debug("creating MongoDB repositories.HeartbeatMonitorRepository") return repositories.NewMongoHeartbeatMonitorRepository( container.Logger(), container.Tracer(), container.MongoDB(), ) - case "hedging": - container.logger.Debug("creating hedging repositories.HeartbeatMonitorRepository") - return repositories.NewHedgingHeartbeatMonitorRepository( - container.Logger(), - container.Tracer(), - repositories.NewGormHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewMongoHeartbeatMonitorRepository(container.Logger(), container.Tracer(), container.MongoDB()), - container.HedgingFailureCounter(), - ) default: container.logger.Debug("creating GORM repositories.HeartbeatMonitorRepository") return repositories.NewGormHeartbeatMonitorRepository( @@ -1760,22 +1734,13 @@ func (container *Container) RegisterSwaggerRoutes() { // HeartbeatRepository registers a new instance of repositories.HeartbeatRepository func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "mongodb": + case "mongodb", "hedging": container.logger.Debug("creating MongoDB repositories.HeartbeatRepository") return repositories.NewMongoHeartbeatRepository( container.Logger(), container.Tracer(), container.MongoDB(), ) - case "hedging": - container.logger.Debug("creating hedging repositories.HeartbeatRepository") - return repositories.NewHedgingHeartbeatRepository( - container.Logger(), - container.Tracer(), - repositories.NewGormHeartbeatRepository(container.Logger(), container.Tracer(), container.DedicatedDB()), - repositories.NewMongoHeartbeatRepository(container.Logger(), container.Tracer(), container.MongoDB()), - container.HedgingFailureCounter(), - ) default: container.logger.Debug("creating GORM repositories.HeartbeatRepository") return repositories.NewGormHeartbeatRepository( diff --git a/api/pkg/repositories/hedging_heartbeat_monitor_repository.go b/api/pkg/repositories/hedging_heartbeat_monitor_repository.go deleted file mode 100644 index 3304230c6..000000000 --- a/api/pkg/repositories/hedging_heartbeat_monitor_repository.go +++ /dev/null @@ -1,128 +0,0 @@ -package repositories - -import ( - "context" - "fmt" - - "github.com/google/uuid" - otelMetric "go.opentelemetry.io/otel/metric" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// hedgingHeartbeatMonitorRepository writes to both primary and secondary repositories. -// Reads only hit primary. Secondary writes are fail-open. -type hedgingHeartbeatMonitorRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - primary HeartbeatMonitorRepository - secondary HeartbeatMonitorRepository - failureCounter otelMetric.Int64Counter -} - -// NewHedgingHeartbeatMonitorRepository creates a hedging HeartbeatMonitorRepository -func NewHedgingHeartbeatMonitorRepository( - logger telemetry.Logger, - tracer telemetry.Tracer, - primary HeartbeatMonitorRepository, - secondary HeartbeatMonitorRepository, - failureCounter otelMetric.Int64Counter, -) HeartbeatMonitorRepository { - return &hedgingHeartbeatMonitorRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatMonitorRepository{})), - tracer: tracer, - primary: primary, - secondary: secondary, - failureCounter: failureCounter, - } -} - -func (repository *hedgingHeartbeatMonitorRepository) Store(ctx context.Context, monitor *entities.HeartbeatMonitor) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.Store(ctx, monitor); err != nil { - return err - } - - if err := repository.secondary.Store(ctx, monitor); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for monitor [%s]", monitor.ID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} - -func (repository *hedgingHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error) { - return repository.primary.Load(ctx, userID, phoneNumber) -} - -func (repository *hedgingHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, monitorID uuid.UUID) (bool, error) { - return repository.primary.Exists(ctx, userID, monitorID) -} - -func (repository *hedgingHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.UpdateQueueID(ctx, monitorID, queueID); err != nil { - return err - } - - if err := repository.secondary.UpdateQueueID(ctx, monitorID, queueID); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary UpdateQueueID failed for monitor [%s]", monitorID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} - -func (repository *hedgingHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.Delete(ctx, userID, phoneNumber); err != nil { - return err - } - - if err := repository.secondary.Delete(ctx, userID, phoneNumber); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete failed for monitor with owner [%s]", phoneNumber))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} - -func (repository *hedgingHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.UpdatePhoneOnline(ctx, userID, monitorID, online); err != nil { - return err - } - - if err := repository.secondary.UpdatePhoneOnline(ctx, userID, monitorID, online); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary UpdatePhoneOnline failed for monitor [%s]", monitorID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} - -func (repository *hedgingHeartbeatMonitorRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.DeleteAllForUser(ctx, userID); err != nil { - return err - } - - if err := repository.secondary.DeleteAllForUser(ctx, userID); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete all failed for user [%s]", userID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} diff --git a/api/pkg/repositories/hedging_heartbeat_repository.go b/api/pkg/repositories/hedging_heartbeat_repository.go deleted file mode 100644 index 073462644..000000000 --- a/api/pkg/repositories/hedging_heartbeat_repository.go +++ /dev/null @@ -1,79 +0,0 @@ -package repositories - -import ( - "context" - "fmt" - - otelMetric "go.opentelemetry.io/otel/metric" - - "github.com/NdoleStudio/httpsms/pkg/entities" - "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" -) - -// hedgingHeartbeatRepository writes to both primary and secondary repositories. -// Reads only hit primary. Secondary writes are fail-open. -type hedgingHeartbeatRepository struct { - logger telemetry.Logger - tracer telemetry.Tracer - primary HeartbeatRepository - secondary HeartbeatRepository - failureCounter otelMetric.Int64Counter -} - -// NewHedgingHeartbeatRepository creates a hedging HeartbeatRepository -func NewHedgingHeartbeatRepository( - logger telemetry.Logger, - tracer telemetry.Tracer, - primary HeartbeatRepository, - secondary HeartbeatRepository, - failureCounter otelMetric.Int64Counter, -) HeartbeatRepository { - return &hedgingHeartbeatRepository{ - logger: logger.WithService(fmt.Sprintf("%T", &hedgingHeartbeatRepository{})), - tracer: tracer, - primary: primary, - secondary: secondary, - failureCounter: failureCounter, - } -} - -func (repository *hedgingHeartbeatRepository) Store(ctx context.Context, heartbeat *entities.Heartbeat) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.Store(ctx, heartbeat); err != nil { - return err - } - - if err := repository.secondary.Store(ctx, heartbeat); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary write failed for heartbeat [%s]", heartbeat.ID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} - -func (repository *hedgingHeartbeatRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) { - return repository.primary.Index(ctx, userID, owner, params) -} - -func (repository *hedgingHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) { - return repository.primary.Last(ctx, userID, owner) -} - -func (repository *hedgingHeartbeatRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { - ctx, span := repository.tracer.Start(ctx) - defer span.End() - - if err := repository.primary.DeleteAllForUser(ctx, userID); err != nil { - return err - } - - if err := repository.secondary.DeleteAllForUser(ctx, userID); err != nil { - repository.logger.Error(stacktrace.Propagate(err, fmt.Sprintf("hedging: secondary delete failed for user [%s]", userID))) - repository.failureCounter.Add(ctx, 1) - } - - return nil -} From d0482a4c4c2a9885d73592e6a49a525f96c2fad2 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 14:20:06 +0300 Subject: [PATCH 163/381] refactor: remove hedging tag, use mongodb directly for heartbeats (#893) Now that the migration is complete, remove the 'hedging' case from the HEARTBEAT_DB_BACKEND switch. Only 'mongodb' is supported going forward. - Update DI container to only recognize 'mongodb' case - Update integration test .env.test to use HEARTBEAT_DB_BACKEND=mongodb Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/di/container.go | 4 ++-- tests/.env.test | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 6da12033c..76304d3eb 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -905,7 +905,7 @@ func (container *Container) MessageThreadRepository() (repository repositories.M // HeartbeatMonitorRepository creates a new instance of repositories.HeartbeatMonitorRepository func (container *Container) HeartbeatMonitorRepository() (repository repositories.HeartbeatMonitorRepository) { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "mongodb", "hedging": + case "mongodb": container.logger.Debug("creating MongoDB repositories.HeartbeatMonitorRepository") return repositories.NewMongoHeartbeatMonitorRepository( container.Logger(), @@ -1734,7 +1734,7 @@ func (container *Container) RegisterSwaggerRoutes() { // HeartbeatRepository registers a new instance of repositories.HeartbeatRepository func (container *Container) HeartbeatRepository() repositories.HeartbeatRepository { switch os.Getenv("HEARTBEAT_DB_BACKEND") { - case "mongodb", "hedging": + case "mongodb": container.logger.Debug("creating MongoDB repositories.HeartbeatRepository") return repositories.NewMongoHeartbeatRepository( container.Logger(), diff --git a/tests/.env.test b/tests/.env.test index ac18af920..0e2b1a5cd 100644 --- a/tests/.env.test +++ b/tests/.env.test @@ -28,5 +28,5 @@ PUSHER_CLUSTER= GCS_BUCKET_NAME= UPTRACE_DSN= CLOUDFLARE_TURNSTILE_SECRET_KEY= -HEARTBEAT_DB_BACKEND=hedging +HEARTBEAT_DB_BACKEND=mongodb MONGODB_URI=mongodb://httpsms:testpassword@mongodb:27017/?authSource=admin&appName=httpsms From 8fe0a3fc81010b4e8818c690fb701525fec0a247 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 14:54:30 +0300 Subject: [PATCH 164/381] feat: Add custom error code when MMS attachment is too large --- .../sessions/kotlin-compiler-17434733840375792193.salive | 0 android/app/src/main/java/com/httpsms/SentReceiver.kt | 5 +++-- android/app/src/main/java/com/httpsms/SmsManagerService.kt | 2 +- android/build.gradle.kts | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive b/android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive new file mode 100644 index 000000000..e69de29bb diff --git a/android/app/src/main/java/com/httpsms/SentReceiver.kt b/android/app/src/main/java/com/httpsms/SentReceiver.kt index 8786ba2c9..2b5bfd129 100644 --- a/android/app/src/main/java/com/httpsms/SentReceiver.kt +++ b/android/app/src/main/java/com/httpsms/SentReceiver.kt @@ -26,13 +26,14 @@ internal class SentReceiver : BroadcastReceiver() { SmsManager.RESULT_ERROR_NO_SERVICE -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "NO_SERVICE") SmsManager.RESULT_ERROR_NULL_PDU -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "NULL_PDU") SmsManager.RESULT_ERROR_RADIO_OFF -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "RADIO_OFF") - else -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "UNKNOWN") + SmsManager.RESULT_ERROR_LIMIT_EXCEEDED -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "LIMIT_EXCEEDED") + else -> handleMessageFailed(context, intent.getStringExtra(Constants.KEY_MESSAGE_ID), "UNKNOWN:${resultCode}") } } private fun cleanupPduFile(context: Context, messageId: String?) { if (messageId == null) return - + try { val baseMessageId = messageId.substringBefore(".") val mmsDir = File(context.cacheDir, "mms_attachments") diff --git a/android/app/src/main/java/com/httpsms/SmsManagerService.kt b/android/app/src/main/java/com/httpsms/SmsManagerService.kt index 5f7ce6f56..c96a90a03 100644 --- a/android/app/src/main/java/com/httpsms/SmsManagerService.kt +++ b/android/app/src/main/java/com/httpsms/SmsManagerService.kt @@ -62,7 +62,7 @@ class SmsManagerService { } Timber.d("active subscription info size: [${localSubscriptionManager.activeSubscriptionInfoList!!.size}]") - val subscriptionId = if (sim == Constants.SIM1 && localSubscriptionManager.activeSubscriptionInfoList!!.size > 0) { + val subscriptionId = if (sim == Constants.SIM1 && localSubscriptionManager.activeSubscriptionInfoList!!.isNotEmpty()) { localSubscriptionManager.activeSubscriptionInfoList!![0].subscriptionId } else if (sim == Constants.SIM2 && localSubscriptionManager.activeSubscriptionInfoList!!.size > 1) { localSubscriptionManager.activeSubscriptionInfoList!![1].subscriptionId diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 32077a010..ee8d1b8d2 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -10,8 +10,8 @@ buildscript { } plugins { - id("com.android.application") version "9.1.1" apply false - id("com.android.library") version "9.1.1" apply false + id("com.android.application") version "9.2.1" apply false + id("com.android.library") version "9.2.1" apply false } tasks.register("clean") { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 2721b96b6..ff340ba9e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 23 15:32:32 EEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From c0b3adf657bcad19654333ead6c54cd693781ade Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 15:52:53 +0300 Subject: [PATCH 165/381] Delete messages after 1 year --- .gitignore | 2 ++ android/.gitignore | 1 + ...otlin-compiler-17434733840375792193.salive | 0 web/pages/index.vue | 4 ++-- web/pages/login.vue | 4 ++-- web/pages/settings/index.vue | 21 ++++++++++++++++++- 6 files changed, 27 insertions(+), 5 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive diff --git a/.gitignore b/.gitignore index a457dc184..b7c1baedd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ android/app/release/ tests/firebase-credentials.json tests/emulator/emulator.exe SECURITY_AUDIT_REPORT.md + +*.exe diff --git a/android/.gitignore b/android/.gitignore index aa724b770..6c13541ca 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -12,4 +12,5 @@ /captures .externalNativeBuild .cxx +.kotlin/sessions/ local.properties diff --git a/android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive b/android/.kotlin/sessions/kotlin-compiler-17434733840375792193.salive deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/pages/index.vue b/web/pages/index.vue index f40028cbf..731aaecb0 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -51,8 +51,8 @@

- ⚡Trusted by 16,212+ happy users who have sent or received - more than 5,921,545+ messages. + ⚡Trusted by 23,273+ users who send/receive more than + 500,000 messages per month.

Welcome

- Join 16,212+ happy users who have sent or + Join 23,273+ users who send/receive more than
- received more than 5,921,545+ SMS messages + 500,000 messages per month

diff --git a/web/pages/settings/index.vue b/web/pages/settings/index.vue index 96aa83f33..503fd3a4f 100644 --- a/web/pages/settings/index.vue +++ b/web/pages/settings/index.vue @@ -494,7 +494,26 @@ Save Notification Settings -
+
Message Data Retention
+

+ Your messages are permanently deleted once they exceed the max + retention period below, counted from when the message was sent or + received. You can always delete your messages manually on the + message search page. +

+ + +
Delete Account

From 73395a4609e58fa2a9f6ff0df53b40a38d4bab5e Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 16:01:20 +0300 Subject: [PATCH 166/381] Remove .air --- api/.air.toml | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 api/.air.toml diff --git a/api/.air.toml b/api/.air.toml deleted file mode 100644 index 15d45a057..000000000 --- a/api/.air.toml +++ /dev/null @@ -1,36 +0,0 @@ -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] - bin = "tmp\\main.exe" - cmd = "go build -o ./tmp/main.exe ." - delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] - exclude_file = [] - exclude_regex = ["_test.go"] - exclude_unchanged = false - follow_symlink = false - full_bin = "" - include_dir = [] - include_ext = ["go", "tpl", "tmpl", "html"] - kill_delay = "0s" - log = "build-errors.log" - send_interrupt = false - stop_on_error = true - -[color] - app = "" - build = "yellow" - main = "magenta" - runner = "green" - watcher = "cyan" - -[log] - time = false - -[misc] - clean_on_exit = false - -[screen] - clear_on_rebuild = false From 16d7d46808d13160c323368a4b64a54de72c81ee Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 18:58:07 +0300 Subject: [PATCH 167/381] feat: limit free users to 1 phone API key (#894) Add entitlement check to the phone API key creation endpoint so free users can only create 1 phone API key. Paid users remain unlimited. Self-hosted setups are unaffected (controlled by ENTITLEMENT_ENABLED). Reuses the existing EntitlementService pattern from send schedules: - Add PhoneAPIKey to entityLimits map (free: 1) - Add CountByUser to PhoneAPIKeyRepository and service - Inject EntitlementService into PhoneAPIKeyHandler - Check entitlement before creating, return 402 if exceeded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 4 --- api/go.sum | 10 ------ api/pkg/di/container.go | 1 + api/pkg/handlers/phone_api_key_handler.go | 33 ++++++++++++++----- .../gorm_phone_api_key_repository.go | 17 ++++++++++ .../repositories/phone_api_key_repository.go | 3 ++ api/pkg/services/entitlement_service.go | 3 ++ api/pkg/services/phone_api_key_service.go | 8 +++++ 8 files changed, 57 insertions(+), 22 deletions(-) diff --git a/api/go.mod b/api/go.mod index 08f6a5c01..1fe7dca1f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -41,7 +41,6 @@ require ( github.com/redis/go-redis/extra/redisotel/v9 v9.19.0 github.com/redis/go-redis/v9 v9.19.0 github.com/rs/zerolog v1.35.1 - github.com/schollz/progressbar/v3 v3.19.0 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 github.com/thedevsaddam/govalidator v1.9.10 @@ -142,7 +141,6 @@ require ( github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-sqlite3 v1.14.44 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -155,7 +153,6 @@ require ( github.com/redis/go-redis/extra/rediscmd/v9 v9.19.0 // indirect github.com/richardlehane/mscfb v1.0.6 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect @@ -196,7 +193,6 @@ require ( golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect diff --git a/api/go.sum b/api/go.sum index 0f6dcce52..fa1163a2f 100644 --- a/api/go.sum +++ b/api/go.sum @@ -78,8 +78,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -247,8 +245,6 @@ github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3Ry github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -295,14 +291,10 @@ github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= -github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= -github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -485,8 +477,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 76304d3eb..df2e5d6f8 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -1264,6 +1264,7 @@ func (container *Container) PhoneAPIKeyHandler() (handler *handlers.PhoneAPIKeyH container.Tracer(), container.PhoneAPIKeyHandlerValidator(), container.PhoneAPIKeyService(), + container.EntitlementService(), ) } diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go index c10df5130..53df0ef34 100644 --- a/api/pkg/handlers/phone_api_key_handler.go +++ b/api/pkg/handlers/phone_api_key_handler.go @@ -17,10 +17,11 @@ import ( // PhoneAPIKeyHandler handles phone API key http requests type PhoneAPIKeyHandler struct { handler - logger telemetry.Logger - tracer telemetry.Tracer - validator *validators.PhoneAPIKeyHandlerValidator - service *services.PhoneAPIKeyService + logger telemetry.Logger + tracer telemetry.Tracer + validator *validators.PhoneAPIKeyHandlerValidator + service *services.PhoneAPIKeyService + entitlementService *services.EntitlementService } // NewPhoneAPIKeyHandler creates a new PhoneAPIKeyHandler @@ -29,12 +30,14 @@ func NewPhoneAPIKeyHandler( tracer telemetry.Tracer, validator *validators.PhoneAPIKeyHandlerValidator, service *services.PhoneAPIKeyService, + entitlementService *services.EntitlementService, ) *PhoneAPIKeyHandler { return &PhoneAPIKeyHandler{ - logger: logger.WithService(fmt.Sprintf("%T", &PhoneAPIKeyHandler{})), - tracer: tracer, - validator: validator, - service: service, + logger: logger.WithService(fmt.Sprintf("%T", &PhoneAPIKeyHandler{})), + tracer: tracer, + validator: validator, + service: service, + entitlementService: entitlementService, } } @@ -99,6 +102,7 @@ func (h *PhoneAPIKeyHandler) index(c *fiber.Ctx) error { // @Success 200 {object} responses.PhoneAPIKeyResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized +// @Failure 402 {object} responses.PaymentRequired // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /phone-api-keys [post] @@ -106,6 +110,19 @@ func (h *PhoneAPIKeyHandler) store(c *fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() + userID := h.userIDFomContext(c) + + result, err := h.entitlementService.Check(ctx, userID, "PhoneAPIKey", func() (int, error) { + return h.service.CountByUser(ctx, userID) + }) + if err != nil { + ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot check entitlement for phone API keys for user [%s]", userID))) + return h.responseInternalServerError(c) + } + if !result.Allowed { + return h.responsePaymentRequired(c, result.Message) + } + var request requests.PhoneAPIKeyStoreRequest if err := c.BodyParser(&request); err != nil { msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request) diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go index 3c306f5ae..68692a048 100644 --- a/api/pkg/repositories/gorm_phone_api_key_repository.go +++ b/api/pkg/repositories/gorm_phone_api_key_repository.go @@ -61,6 +61,23 @@ WHERE user_id = ? AND array_position(phone_ids, ?) IS NOT NULL; return nil } +// CountByUser returns the number of phone API keys owned by a user. +func (repository *gormPhoneAPIKeyRepository) CountByUser(ctx context.Context, userID entities.UserID) (int, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + var count int64 + err := repository.db.WithContext(ctx). + Model(&entities.PhoneAPIKey{}). + Where("user_id = ?", userID). + Count(&count).Error + if err != nil { + return 0, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot count phone API keys for user [%s]", userID)) + } + + return int(count), nil +} + // Load an entities.PhoneAPIKey based on the entities.UserID func (repository *gormPhoneAPIKeyRepository) Load(ctx context.Context, userID entities.UserID, phoneAPIKeyID uuid.UUID) (*entities.PhoneAPIKey, error) { ctx, span := repository.tracer.Start(ctx) diff --git a/api/pkg/repositories/phone_api_key_repository.go b/api/pkg/repositories/phone_api_key_repository.go index 8894e4ac1..ceecda852 100644 --- a/api/pkg/repositories/phone_api_key_repository.go +++ b/api/pkg/repositories/phone_api_key_repository.go @@ -31,6 +31,9 @@ type PhoneAPIKeyRepository interface { // RemovePhone removes an entities.Phone to an entities.PhoneAPIKey RemovePhone(ctx context.Context, phoneAPIKey *entities.PhoneAPIKey, phone *entities.Phone) error + // CountByUser returns the number of phone API keys owned by a user + CountByUser(ctx context.Context, userID entities.UserID) (int, error) + // DeleteAllForUser deletes all entities.PhoneAPIKey for a user DeleteAllForUser(ctx context.Context, userID entities.UserID) error diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 59f077a2d..c67d4803e 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -19,6 +19,9 @@ var entityLimits = map[string]map[entities.SubscriptionName]int{ "MessageSendSchedule": { entities.SubscriptionNameFree: 1, }, + "PhoneAPIKey": { + entities.SubscriptionNameFree: 1, + }, } // EntitlementCheckResult holds the outcome of an entitlement check. diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go index 5a1485834..c9283fddc 100644 --- a/api/pkg/services/phone_api_key_service.go +++ b/api/pkg/services/phone_api_key_service.go @@ -40,6 +40,14 @@ func NewPhoneAPIKeyService( } } +// CountByUser returns the number of phone API keys owned by a user. +func (service *PhoneAPIKeyService) CountByUser(ctx context.Context, userID entities.UserID) (int, error) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + return service.repository.CountByUser(ctx, userID) +} + // Index fetches the entities.Webhook for an entities.UserID func (service *PhoneAPIKeyService) Index(ctx context.Context, userID entities.UserID, params repositories.IndexParams) ([]*entities.PhoneAPIKey, error) { ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger) From e114f5a138d10467bed103b53bde35b7d812fd34 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 19:06:33 +0300 Subject: [PATCH 168/381] refactor: move entitlement entity name constants to entities package (#895) Move EntityNameMessageSendSchedule and EntityNamePhoneAPIKey constants from the services package to their respective entity files in the entities package, keeping them co-located with the types they describe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/entities/message_send_schedule.go | 3 +++ api/pkg/entities/phone_api_key.go | 3 +++ api/pkg/handlers/message_send_schedule_handler.go | 3 ++- api/pkg/handlers/phone_api_key_handler.go | 3 ++- api/pkg/services/entitlement_service.go | 4 ++-- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/api/pkg/entities/message_send_schedule.go b/api/pkg/entities/message_send_schedule.go index 20c9b6286..7be7dc2be 100644 --- a/api/pkg/entities/message_send_schedule.go +++ b/api/pkg/entities/message_send_schedule.go @@ -6,6 +6,9 @@ import ( "github.com/google/uuid" ) +// EntityNameMessageSendSchedule is the entitlement entity name for message send schedules. +const EntityNameMessageSendSchedule = "MessageSendSchedule" + // MessageSendScheduleWindow represents a single availability window for a day of the week. type MessageSendScheduleWindow struct { DayOfWeek int `json:"day_of_week" example:"1"` diff --git a/api/pkg/entities/phone_api_key.go b/api/pkg/entities/phone_api_key.go index 5a32c2348..d8dda3280 100644 --- a/api/pkg/entities/phone_api_key.go +++ b/api/pkg/entities/phone_api_key.go @@ -7,6 +7,9 @@ import ( "github.com/lib/pq" ) +// EntityNamePhoneAPIKey is the entitlement entity name for phone API keys. +const EntityNamePhoneAPIKey = "PhoneAPIKey" + // PhoneAPIKey represents the API key for a phone type PhoneAPIKey struct { ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"` diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go index 446f50e17..3218dcebe 100644 --- a/api/pkg/handlers/message_send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" + "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" @@ -97,7 +98,7 @@ func (h *MessageSendScheduleHandler) Store(c *fiber.Ctx) error { userID := h.userIDFomContext(c) - result, err := h.entitlementService.Check(ctx, userID, "MessageSendSchedule", func() (int, error) { + result, err := h.entitlementService.Check(ctx, userID, entities.EntityNameMessageSendSchedule, func() (int, error) { return h.service.CountByUser(ctx, userID) }) if err != nil { diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go index 53df0ef34..4cd7e1ab3 100644 --- a/api/pkg/handlers/phone_api_key_handler.go +++ b/api/pkg/handlers/phone_api_key_handler.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" + "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" @@ -112,7 +113,7 @@ func (h *PhoneAPIKeyHandler) store(c *fiber.Ctx) error { userID := h.userIDFomContext(c) - result, err := h.entitlementService.Check(ctx, userID, "PhoneAPIKey", func() (int, error) { + result, err := h.entitlementService.Check(ctx, userID, entities.EntityNamePhoneAPIKey, func() (int, error) { return h.service.CountByUser(ctx, userID) }) if err != nil { diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index c67d4803e..3581c211c 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -16,10 +16,10 @@ import ( // entityLimits maps entity name → subscription plan → max count. // A limit of 0 means unlimited. If a plan is not listed, it defaults to unlimited (0). var entityLimits = map[string]map[entities.SubscriptionName]int{ - "MessageSendSchedule": { + entities.EntityNameMessageSendSchedule: { entities.SubscriptionNameFree: 1, }, - "PhoneAPIKey": { + entities.EntityNamePhoneAPIKey: { entities.SubscriptionNameFree: 1, }, } From 33d5a2c16b3ba5ebe8d27a89f17702cade0af1aa Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 17 May 2026 19:28:58 +0300 Subject: [PATCH 169/381] Fix the rendering of 'phone API key' --- api/pkg/services/entitlement_service.go | 29 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 3581c211c..1010c89c6 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -111,17 +111,34 @@ func (service *EntitlementService) Check( } // formatEntityName converts a PascalCase entity name to lowercase words and optionally pluralizes it. -// e.g. "MessageSendSchedule" → "message send schedules" (plural) or "message send schedule" (singular) +// Consecutive uppercase letters (acronyms like API) are kept together as a single word. +// e.g. "MessageSendSchedule" → "message send schedules", "PhoneAPIKey" → "phone API keys" func formatEntityName(name string, plural bool) string { var words []string + runes := []rune(name) start := 0 - for i := 1; i < len(name); i++ { - if unicode.IsUpper(rune(name[i])) { - words = append(words, strings.ToLower(name[start:i])) - start = i + for i := 1; i < len(runes); i++ { + if unicode.IsUpper(runes[i]) { + if !unicode.IsUpper(runes[i-1]) { + // transition from lowercase to uppercase: split before i + words = append(words, string(runes[start:i])) + start = i + } else if i+1 < len(runes) && unicode.IsLower(runes[i+1]) { + // transition from uppercase run to a new word (e.g., "API" followed by "Key") + words = append(words, string(runes[start:i])) + start = i + } } } - words = append(words, strings.ToLower(name[start:])) + words = append(words, string(runes[start:])) + + for i, word := range words { + if word == strings.ToUpper(word) && len(word) > 1 { + // keep acronyms uppercase (e.g., "API") + continue + } + words[i] = strings.ToLower(word) + } if plural && len(words) > 0 { client := pluralize.NewClient() From 2bc05d05985decb48bbff31269009e69d2e4b009 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 18 May 2026 11:01:42 +0300 Subject: [PATCH 170/381] test: replace PostgreSQL with CockroachDB in integration tests (#896) * docs: add design spec for CockroachDB integration tests Replace PostgreSQL with CockroachDB in the integration test Docker Compose to match the production database environment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add implementation plan for cockroachdb integration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: replace postgres with cockroachdb in integration tests Use cockroachdb/cockroach:latest in single-node insecure mode with in-memory storage. Add cockroachdb-init service to create the database and update the seed service to use cockroach sql CLI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: update env vars for cockroachdb connection Point DATABASE_URL at cockroachdb:26257 with root user (insecure mode). Enable DATABASE_MIGRATION_CONSTRAINT_FIX for CockroachDB compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update integration test README for cockroachdb Replace all PostgreSQL references with CockroachDB in the architecture diagram, components table, and troubleshooting section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: add docs/ to .gitignore Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use cockroach sql for healthcheck instead of curl The CockroachDB Docker image does not include curl. Use 'cockroach sql --execute=SELECT 1' for the healthcheck instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: increase cockroachdb memory store to minimum 640MiB CockroachDB requires at least 640 MiB for in-memory storage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 ++ tests/.env.test | 5 ++-- tests/README.md | 26 ++++++++--------- tests/docker-compose.yml | 62 +++++++++++++++++++++++++--------------- 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index b7c1baedd..0aa9dfff8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ tests/emulator/emulator.exe SECURITY_AUDIT_REPORT.md *.exe + +docs/ diff --git a/tests/.env.test b/tests/.env.test index 0e2b1a5cd..5692e7fec 100644 --- a/tests/.env.test +++ b/tests/.env.test @@ -8,8 +8,9 @@ EVENTS_QUEUE_ENDPOINT=http://localhost:8000/v1/events EVENTS_QUEUE_USER_API_KEY=system-user-api-key EVENTS_QUEUE_USER_ID=system-user-id FCM_ENDPOINT=http://wiremock:8080 -DATABASE_URL=postgresql://dbusername:dbpassword@postgres:5432/httpsms -DATABASE_URL_DEDICATED=postgresql://dbusername:dbpassword@postgres:5432/httpsms +DATABASE_URL=postgresql://root@cockroachdb:26257/httpsms?sslmode=disable +DATABASE_URL_DEDICATED=postgresql://root@cockroachdb:26257/httpsms?sslmode=disable +DATABASE_MIGRATION_CONSTRAINT_FIX=1 REDIS_URL=redis://@redis:6379 APP_PORT=8000 APP_NAME=httpSMS diff --git a/tests/README.md b/tests/README.md index c914982d2..a37653e53 100644 --- a/tests/README.md +++ b/tests/README.md @@ -20,21 +20,21 @@ End-to-end integration tests for the httpSMS API. These tests validate the compl └──────────────┘ │ ┌──────┴───────┐ - │ PostgreSQL │ │ Redis │ - │ Port 5435 │ │ Port 6379 │ + │ CockroachDB │ │ Redis │ + │ Port 26257 │ │ Port 6379 │ └──────────────┘ └─────────────┘ ``` ### Components -| Component | Description | -| --------------- | ------------------------------------------------------- | -| **API** | The httpSMS Go API server running in Docker | -| **Emulator** | A Fiber v3 Go service that simulates an Android phone | -| **PostgreSQL** | Database for the API | -| **Redis** | Cache and queue backend | -| **Seed** | One-shot container that seeds test data into PostgreSQL | -| **Test Runner** | Go test binary that runs on the host machine | +| Component | Description | +| --------------- | -------------------------------------------------------- | +| **API** | The httpSMS Go API server running in Docker | +| **Emulator** | A Fiber v3 Go service that simulates an Android phone | +| **CockroachDB** | Database for the API (single-node, insecure mode) | +| **Redis** | Cache and queue backend | +| **Seed** | One-shot container that seeds test data into CockroachDB | +| **Test Runner** | Go test binary that runs on the host machine | ### How It Works @@ -86,7 +86,7 @@ export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json) docker compose up -d --build --wait ``` -This starts PostgreSQL, Redis, the API, and the emulator. The `--wait` flag blocks until all health checks pass. +This starts CockroachDB, Redis, the API, and the emulator. The `--wait` flag blocks until all health checks pass. ### 4. Wait for Seeding @@ -95,7 +95,7 @@ docker compose wait seed sleep 2 ``` -The seed container inserts test users, phones, and API keys into PostgreSQL after the API has run its GORM migrations. +The seed container inserts test users, phones, and API keys into CockroachDB after the API has run its GORM migrations. ### 5. Run Tests @@ -181,7 +181,7 @@ docker compose logs api Common issues: - `FIREBASE_CREDENTIALS` env var not set or malformed -- PostgreSQL not ready (increase `start_period` in healthcheck) +- CockroachDB not ready (increase `start_period` in healthcheck) ### Tests timeout waiting for `delivered` status diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 515b62983..3d82d47c1 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,18 +1,39 @@ services: - postgres: - image: postgres:alpine - environment: - POSTGRES_DB: httpsms - POSTGRES_PASSWORD: dbpassword - POSTGRES_USER: dbusername + cockroachdb: + image: cockroachdb/cockroach:latest + command: start-single-node --insecure --store=type=mem,size=640MiB ports: - - "5435:5432" + - "26257:26257" + - "8081:8080" healthcheck: - test: ["CMD-SHELL", "pg_isready -U dbusername -d httpsms"] + test: + [ + "CMD", + "cockroach", + "sql", + "--insecure", + "--host=localhost", + "--execute=SELECT 1;", + ] interval: 5s timeout: 5s retries: 10 - start_period: 5s + start_period: 10s + + cockroachdb-init: + image: cockroachdb/cockroach:latest + depends_on: + cockroachdb: + condition: service_healthy + entrypoint: + [ + "cockroach", + "sql", + "--insecure", + "--host=cockroachdb", + "--execute=CREATE DATABASE IF NOT EXISTS httpsms;", + ] + restart: "no" redis: image: redis:latest @@ -61,8 +82,8 @@ services: ports: - "8000:8000" depends_on: - postgres: - condition: service_healthy + cockroachdb-init: + condition: service_completed_successfully redis: condition: service_healthy wiremock: @@ -81,24 +102,19 @@ services: start_period: 30s seed: - image: postgres:alpine + image: cockroachdb/cockroach:latest depends_on: api: condition: service_healthy - environment: - PGPASSWORD: dbpassword volumes: - ./seed.sql:/seed.sql:ro entrypoint: [ - "psql", - "-h", - "postgres", - "-U", - "dbusername", - "-d", - "httpsms", - "-f", - "/seed.sql", + "cockroach", + "sql", + "--insecure", + "--host=cockroachdb", + "--database=httpsms", + "--file=/seed.sql", ] restart: "no" From d85234a38cbe794cba4dd0fb3b0d0801b33728a6 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Mon, 18 May 2026 11:34:44 +0300 Subject: [PATCH 171/381] refactor: optimize heartbeats MongoDB indexes Replace two separate indexes (owner_1_timestamp_-1, user_id_1) with a single compound index {user_id: 1, owner: 1, timestamp: -1} that covers all query patterns. Old indexes are dropped automatically on startup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/repositories/mongodb.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go index f2f65ea91..bd6f12bed 100644 --- a/api/pkg/repositories/mongodb.go +++ b/api/pkg/repositories/mongodb.go @@ -106,9 +106,14 @@ func parseMongoDBName(uri string) (string, error) { func createMongoIndexes(ctx context.Context, db *mongo.Database) error { // Heartbeats indexes heartbeatsCol := db.Collection(collectionHeartbeats) + + // TODO: Remove this block after deploying once — old indexes will have been dropped in production. + for _, name := range []string{"owner_1_timestamp_-1", "user_id_1"} { + _ = heartbeatsCol.Indexes().DropOne(ctx, name) + } + _, err := heartbeatsCol.Indexes().CreateMany(ctx, []mongo.IndexModel{ - {Keys: bson.D{{"owner", 1}, {"timestamp", -1}}}, - {Keys: bson.D{{"user_id", 1}}}, + {Keys: bson.D{{"user_id", 1}, {"owner", 1}, {"timestamp", -1}}}, }) if err != nil { return stacktrace.Propagate(err, "cannot create indexes on heartbeats collection") From 86447cef389875003a52974b1d36adc04322ee27 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 09:24:57 +0300 Subject: [PATCH 172/381] feat: add bulk messages history table with status counts (#898) * feat: add bulk messages history table with status counts - Add GET /v1/bulk-messages endpoint that returns the last 10 bulk message batches for the authenticated user - Add optimized SQL query using GROUP BY and FILTER to aggregate status counts (scheduled, pending, sent, delivered, failed) from millions of messages - Add Vuetify data table on /bulk-messages page showing history - Add View action that navigates to /search-messages with the bulk message ID pre-filled as a query parameter - Update search-messages page to read ?query= URL param and auto-fill + auto-search on page load Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use StartWithLogger in GetBulkMessages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add error notification for bulk messages history fetch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Generate swagger docs and models * fix: address PR review comments - Fix double-search CAPTCHA conflict by adding initialLoadComplete guard to prevent the options watcher from firing before mounted completes - Fix nil slice serializing as null by using make() for empty slice init - Regenerate swagger docs to match actual type names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use v-simple-table for bulk message history Replace v-data-table with v-simple-table to match the pattern used in the settings page. Added a short description paragraph explaining what the table shows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: handle phone numbers without + prefix and fix CSV SendTime parsing - Add + prefix normalization in validateMessages before phonenumbers.Parse - Change SendTime from *time.Time to string (SendTimeRaw) for CSV unmarshaling - Add GetSendTime() method that parses multiple date formats gracefully - Empty SendTime fields no longer cause 'Cannot read contents' errors - Support RFC3339, YYYY-MM-DDTHH:MM:SS, YYYY-MM-DD HH:MM:SS, YYYY-MM-DD formats Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add expired count to bulk messages history table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * revert: remove + prefix normalization in phone number validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: increase search query max length from 20 to 50 characters Allows searching by bulk message IDs (e.g. bulk-8dcc3d68-57bd-4913-a5ff-52c107ffc0c9) which are 41 characters long. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add bulk SMS integration tests for CSV and Excel upload - TestBulkSMS_CSV: uploads CSV with 1 message, fires SENT event, verifies history - TestBulkSMS_Excel: uploads Excel with 2 messages, fires DELIVERED on one, verifies mixed status counts - Add shared helpers: uploadBulkFile, fetchBulkMessages, searchMessages, findBulkEntry - Add excelize/v2 dependency for Excel file creation in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use correct query params for message index endpoint in integration tests The searchMessages helper was using 'owners' (plural) param and missing 'contact', but the GET /v1/messages endpoint requires 'owner' (singular) and 'contact' fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct expected status in TestBulkSMS_Excel assertion Message 2 transitions to 'scheduled' after waitForFCMPush, not 'pending'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove old heartbeats index drop code The old indexes (owner_1_timestamp_-1, user_id_1) have already been dropped in production, so this migration code is no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/docs/docs.go | 115 + api/docs/swagger.json | 9902 +++++++++-------- api/docs/swagger.yaml | 2610 ++--- api/go.mod | 1 + api/go.sum | 2 + api/pkg/entities/bulk_message.go | 16 + api/pkg/handlers/bulk_message_handler.go | 55 +- .../repositories/gorm_message_repository.go | 31 + api/pkg/repositories/message_repository.go | 3 + api/pkg/repositories/mongodb.go | 5 - api/pkg/requests/bulk_message_request.go | 42 +- api/pkg/responses/bulk_message_responses.go | 9 + api/pkg/services/message_service.go | 15 + .../bulk_message_handler_validator.go | 44 +- .../validators/message_handler_validator.go | 2 +- tests/go.mod | 11 +- tests/go.sum | 34 +- tests/helpers_test.go | 97 + tests/integration_test.go | 156 + web/models/api.ts | 27 + web/pages/bulk-messages/index.vue | 91 + web/pages/search-messages/index.vue | 17 + web/store/index.ts | 17 + 23 files changed, 7325 insertions(+), 5977 deletions(-) create mode 100644 api/pkg/entities/bulk_message.go create mode 100644 api/pkg/responses/bulk_message_responses.go diff --git a/api/docs/docs.go b/api/docs/docs.go index ed2796059..dadff723c 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -144,6 +144,44 @@ const docTemplate = `{ } }, "/bulk-messages": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the last 10 bulk message order summaries for the authenticated user showing counts per status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "BulkSMS" + ], + "summary": "List bulk message orders", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BulkMessagesResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, "post": { "security": [ { @@ -1759,6 +1797,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.Unauthorized" } }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.PaymentRequired" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -3299,6 +3343,53 @@ const docTemplate = `{ } } }, + "entities.BulkMessage": { + "type": "object", + "required": [ + "created_at", + "delivered_count", + "failed_count", + "pending_count", + "request_id", + "scheduled_count", + "sent_count", + "total" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "delivered_count": { + "type": "integer", + "example": 25 + }, + "failed_count": { + "type": "integer", + "example": 5 + }, + "pending_count": { + "type": "integer", + "example": 30 + }, + "request_id": { + "type": "string", + "example": "bulk-32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "scheduled_count": { + "type": "integer", + "example": 50 + }, + "sent_count": { + "type": "integer", + "example": 40 + }, + "total": { + "type": "integer", + "example": 150 + } + } + }, "entities.Discord": { "type": "object", "required": [ @@ -4599,6 +4690,30 @@ const docTemplate = `{ } } }, + "responses.BulkMessagesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.BulkMessage" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.DiscordResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 656ee381e..69e9d3633 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,4760 +1,5370 @@ { - "schemes": ["https"], - "swagger": "2.0", - "info": { - "description": "Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption.", - "title": "httpSMS API Reference", - "contact": { - "name": "support@httpsms.com", - "email": "support@httpsms.com" - }, - "license": { - "name": "AGPL-3.0", - "url": "https://raw.githubusercontent.com/NdoleStudio/http-sms-manager/main/LICENSE" - }, - "version": "1.0" - }, - "host": "api.httpsms.com", - "basePath": "/v1", - "paths": { - "/billing/usage": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the summary of sent and received messages for a user in the current month", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Billing"], - "summary": "Get Billing Usage.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.BillingUsageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/billing/usage-history": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Billing"], - "summary": "Get billing usage history.", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "number of heartbeats to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.BillingUsagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/bulk-messages": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx).", - "consumes": ["multipart/form-data"], - "produces": ["application/json"], - "tags": ["BulkSMS"], - "summary": "Store bulk SMS file", - "parameters": [ - { - "type": "file", - "description": "The Excel or CSV file containing the messages to be sent.", - "name": "document", - "in": "formData", - "required": true - } - ], - "responses": { - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/discord-integrations": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the discord integrations of a user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["DiscordIntegration"], - "summary": "Get discord integrations of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of discord integrations to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter discord integrations containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of discord integrations to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.DiscordsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store a discord integration for the authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["DiscordIntegration"], - "summary": "Store discord integration", - "parameters": [ - { - "description": "Payload of the discord integration request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DiscordStore" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.DiscordResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/discord-integrations/{discordID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a discord integration for the currently authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["DiscordIntegration"], - "summary": "Update a discord integration", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the discord integration", - "name": "discordID", - "in": "path", - "required": true - }, - { - "description": "Payload of discord integration to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DiscordUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.DiscordResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a discord integration for a user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Webhooks"], - "summary": "Delete discord integration", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the discord integration", - "name": "discordID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/discord/event": { - "post": { - "description": "Publish a discord event to the registered listeners", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Discord"], - "summary": "Consume a discord event", - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/heartbeats": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Heartbeats"], - "summary": "Get heartbeats of an owner phone number", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "the owner's phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of heartbeats to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HeartbeatsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store the heartbeat to make notify that a phone number is still active", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Heartbeats"], - "summary": "Register heartbeat of an owner phone number", - "parameters": [ - { - "description": "Payload of the heartbeat request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.HeartbeatStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.HeartbeatResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/integration/3cx/messages": { - "post": { - "description": "Sends an SMS message from the 3CX platform", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["3CXIntegration"], - "summary": "Sends a 3CX SMS message", - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/message-threads": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["MessageThreads"], - "summary": "Get message threads for a phone number", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "owner phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter message threads containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageThreadsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/message-threads/{messageThreadID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the details of a message thread", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["MessageThreads"], - "summary": "Update a message thread", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message thread", - "name": "messageThreadID", - "in": "path", - "required": true - }, - { - "description": "Payload of message thread details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageThreadUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a message thread from the database and also deletes all the messages in the thread.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["MessageThreads"], - "summary": "Delete a message thread from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message thread", - "name": "messageThreadID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Get messages which are sent between 2 phone numbers", - "parameters": [ - { - "type": "string", - "default": "+18005550199", - "description": "the owner's phone number", - "name": "owner", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "+18005550100", - "description": "the contact's phone number", - "name": "contact", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter messages containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/bulk-send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add bulk SMS messages to be sent by the android phone", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Send bulk SMS messages", - "parameters": [ - { - "description": "Bulk send message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageBulkSend" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.MessagesResponse" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/calls/missed": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Register a missed call event on the mobile phone", - "parameters": [ - { - "description": "Payload of the missed call event.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageCallMissed" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/outstanding": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get an outstanding message to be sent by an android phone", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Get an outstanding message", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703cb", - "description": "The ID of the message", - "name": "message_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/receive": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add a new message received from a mobile phone", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Receive a new SMS message from a mobile phone", - "parameters": [ - { - "description": "Received message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageReceive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/search": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "This returns the list of all messages based on the filter criteria including missed calls", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Search all messages of a user", - "parameters": [ - { - "type": "string", - "description": "Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/", - "name": "token", - "in": "header", - "required": true - }, - { - "type": "string", - "default": "+18005550199,+18005550100", - "description": "the owner's phone numbers", - "name": "owners", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "description": "number of messages to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter messages containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 200, - "minimum": 1, - "type": "integer", - "description": "number of messages to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessagesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add a new SMS message to be sent by your Android phone", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Send an SMS message", - "parameters": [ - { - "description": "Send message request payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageSend" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/{messageID}": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get a message from the database by the message ID.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Get a message from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a message from the database and removes the message content from the list of threads.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Delete a message from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/messages/{messageID}/events": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Messages"], - "summary": "Upsert an event for a message on the mobile phone", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the message", - "name": "messageID", - "in": "path", - "required": true - }, - { - "description": "Payload of the event emitted.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageEvent" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phone-api-keys": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list phone API keys which a user has registered on the httpSMS application", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["PhoneAPIKeys"], - "summary": "Get the phone API keys of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of phone api keys to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter phone api keys with name containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "number of phone api keys to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneAPIKeysResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["PhoneAPIKeys"], - "summary": "Store phone API key", - "parameters": [ - { - "description": "Payload of new phone API key.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneAPIKeyStoreRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneAPIKeyResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phone-api-keys/{phoneAPIKeyID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a phone API Key from the database and cannot be used for authentication anymore.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["PhoneAPIKeys"], - "summary": "Delete a phone API key from the database.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone API key", - "name": "phoneAPIKeyID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "You will need to login again to the httpSMS app on your Android phone with a new phone API key.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["PhoneAPIKeys"], - "summary": "Remove the association of a phone from the phone API key.", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone API key", - "name": "phoneAPIKeyID", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone", - "name": "phoneID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phones": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get list of phones which a user has registered on the http sms application", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Phones"], - "summary": "Get phones of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of heartbeats to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter phones containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of phones to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhonesResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Phones"], - "summary": "Upsert Phone", - "parameters": [ - { - "description": "Payload of new phone number.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneUpsert" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phones/fcm-token": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Phones"], - "summary": "Upserts the FCM token of a phone", - "parameters": [ - { - "description": "Payload of new FCM token.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.PhoneFCMToken" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/phones/{phoneID}": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a phone that has been sored in the database", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Phones"], - "summary": "Delete Phone", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the phone", - "name": "phoneID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/send-schedules": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "List all send schedules owned by the authenticated user.", - "produces": ["application/json"], - "tags": ["SendSchedules"], - "summary": "List send schedules", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageSendSchedulesResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Create a new send schedule for the authenticated user.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["SendSchedules"], - "summary": "Create send schedule", - "parameters": [ - { - "description": "Payload of new send schedule.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageSendScheduleStore" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.MessageSendScheduleResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/responses.PaymentRequired" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/send-schedules/{scheduleID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a send schedule owned by the authenticated user.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["SendSchedules"], - "summary": "Update send schedule", - "parameters": [ - { - "type": "string", - "description": "Schedule ID", - "name": "scheduleID", - "in": "path", - "required": true - }, - { - "description": "Payload of updated send schedule.", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.MessageSendScheduleStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.MessageSendScheduleResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a send schedule owned by the authenticated user.", - "produces": ["application/json"], - "tags": ["SendSchedules"], - "summary": "Delete send schedule", - "parameters": [ - { - "type": "string", - "description": "Schedule ID", - "name": "scheduleID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/me": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get details of the currently authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Get current user", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Updates the details of the currently authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Update a user", - "parameters": [ - { - "description": "Payload of user details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.PhoneResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Deletes the currently authenticated user together with all their data.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Delete a user", - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/subscription": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Cancel the subscription of the authenticated user.", - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Cancel the user's subscription", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/subscription-update-url": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Fetches the subscription URL of the authenticated user.", - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Currently authenticated user subscription update URL", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.OkString" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/subscription/invoices/{subscriptionInvoiceID}": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Generates a new invoice PDF file for the given subscription payment with given parameters.", - "consumes": ["application/json"], - "produces": ["application/pdf"], - "tags": ["Users"], - "summary": "Generate a subscription payment invoice", - "parameters": [ - { - "description": "Generate subscription payment invoice parameters", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserPaymentInvoice" - } - }, - { - "type": "string", - "description": "ID of the subscription invoice to generate the PDF for", - "name": "subscriptionInvoiceID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "file" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/subscription/payments": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal.", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Get the last 10 subscription payments.", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserSubscriptionPaymentsResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/{userID}/api-keys": { - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Rotate the user's API key in case the current API Key is compromised", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Rotate the user's API Key", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the user to update", - "name": "userID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/users/{userID}/notifications": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update the email notification settings for a user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Users"], - "summary": "Update notification settings", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the user to update", - "name": "userID", - "in": "path", - "required": true - }, - { - "description": "User notification details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserNotificationUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.UserResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}": { - "get": { - "description": "Download an MMS attachment by its path components", - "produces": ["application/octet-stream"], - "tags": ["Attachments"], - "summary": "Download a message attachment", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "userID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Message ID", - "name": "messageID", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Attachment index", - "name": "attachmentIndex", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Filename with extension", - "name": "filename", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "file" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/responses.NotFound" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/webhooks": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get the webhooks of a user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Webhooks"], - "summary": "Get webhooks of a user", - "parameters": [ - { - "minimum": 0, - "type": "integer", - "description": "number of webhooks to skip", - "name": "skip", - "in": "query" - }, - { - "type": "string", - "description": "filter webhooks containing query", - "name": "query", - "in": "query" - }, - { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "number of webhooks to return", - "name": "limit", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhooksResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Store a webhook for the authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Webhooks"], - "summary": "Store a webhook", - "parameters": [ - { - "description": "Payload of the webhook request", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.WebhookStore" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhookResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - }, - "/webhooks/{webhookID}": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Update a webhook for the currently authenticated user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Webhooks"], - "summary": "Update a webhook", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the webhook", - "name": "webhookID", - "in": "path", - "required": true - }, - { - "description": "Payload of webhook details to update", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.WebhookUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/responses.WebhookResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - }, - "delete": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Delete a webhook for a user", - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Webhooks"], - "summary": "Delete webhook", - "parameters": [ - { - "type": "string", - "default": "32343a19-da5e-4b1b-a767-3298a73703ca", - "description": "ID of the webhook", - "name": "webhookID", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/responses.NoContent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/responses.BadRequest" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/responses.Unauthorized" - } - }, - "422": { - "description": "Unprocessable Entity", - "schema": { - "$ref": "#/definitions/responses.UnprocessableEntity" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/responses.InternalServerError" - } - } - } - } - } - }, - "definitions": { - "entities.BillingUsage": { - "type": "object", - "required": [ - "created_at", - "end_timestamp", - "id", - "received_messages", - "sent_messages", - "start_timestamp", - "total_cost", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "end_timestamp": { - "type": "string", - "example": "2022-01-31T23:59:59+00:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "received_messages": { - "type": "integer", - "example": 465 - }, - "sent_messages": { - "type": "integer", - "example": 321 - }, - "start_timestamp": { - "type": "string", - "example": "2022-01-01T00:00:00+00:00" - }, - "total_cost": { - "type": "integer", - "example": 0 - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.Discord": { - "type": "object", - "required": [ - "created_at", - "id", - "incoming_channel_id", - "name", - "server_id", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "incoming_channel_id": { - "type": "string", - "example": "1095780203256627291" - }, - "name": { - "type": "string", - "example": "Game Server" - }, - "server_id": { - "type": "string", - "example": "1095778291488653372" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.Heartbeat": { - "type": "object", - "required": [ - "charging", - "id", - "owner", - "timestamp", - "user_id", - "version" - ], - "properties": { - "charging": { - "type": "boolean", - "example": true - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "timestamp": { - "type": "string", - "example": "2022-06-05T14:26:01.520828+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "version": { - "type": "string", - "example": "344c10f" - } - } - }, - "entities.Message": { - "type": "object", - "required": [ - "attachments", - "contact", - "content", - "created_at", - "encrypted", - "id", - "max_send_attempts", - "order_timestamp", - "owner", - "request_received_at", - "send_attempt_count", - "sim", - "status", - "type", - "updated_at", - "user_id" - ], - "properties": { - "attachments": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://example.com/image.jpg", - "https://example.com/video.mp4" - ] - }, - "contact": { - "type": "string", - "example": "+18005550100" - }, - "content": { - "type": "string", - "example": "This is a sample text message" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "delivered_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "encrypted": { - "type": "boolean", - "example": false - }, - "expired_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "failed_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "failure_reason": { - "type": "string", - "example": "UNKNOWN" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "last_attempted_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "max_send_attempts": { - "type": "integer", - "example": 1 - }, - "order_timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "received_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "request_id": { - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" - }, - "request_received_at": { - "type": "string", - "example": "2022-06-05T14:26:01.520828+03:00" - }, - "scheduled_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "scheduled_send_time": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "send_attempt_count": { - "type": "integer", - "example": 0 - }, - "send_time": { - "description": "SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message", - "type": "integer", - "example": 133414 - }, - "sent_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "sim": { - "description": "SIM is the SIM card to use to send the message\n* SMS1: use the SIM card in slot 1\n* SMS2: use the SIM card in slot 2\n* DEFAULT: used the default communication SIM card", - "allOf": [ - { - "$ref": "#/definitions/entities.SIM" - } - ], - "example": "DEFAULT" - }, - "status": { - "type": "string", - "example": "pending" - }, - "type": { - "type": "string", - "example": "mobile-terminated" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.MessageSendSchedule": { - "type": "object", - "required": [ - "created_at", - "id", - "name", - "timezone", - "updated_at", - "user_id", - "windows" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "name": { - "type": "string", - "example": "Business Hours" - }, - "timezone": { - "type": "string", - "example": "Europe/Tallinn" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "windows": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendScheduleWindow" - } - } - } - }, - "entities.MessageSendScheduleWindow": { - "type": "object", - "required": ["day_of_week", "end_minute", "start_minute"], - "properties": { - "day_of_week": { - "type": "integer", - "example": 1 - }, - "end_minute": { - "type": "integer", - "example": 1020 - }, - "start_minute": { - "type": "integer", - "example": 540 - } - } - }, - "entities.MessageThread": { - "type": "object", - "required": [ - "color", - "contact", - "created_at", - "id", - "is_archived", - "last_message_content", - "last_message_id", - "order_timestamp", - "owner", - "status", - "updated_at", - "user_id" - ], - "properties": { - "color": { - "type": "string", - "example": "indigo" - }, + "schemes": [ + "https" + ], + "swagger": "2.0", + "info": { + "description": "Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption.", + "title": "httpSMS API Reference", "contact": { - "type": "string", - "example": "+18005550100" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703ca" - }, - "is_archived": { - "type": "boolean", - "example": false - }, - "last_message_content": { - "type": "string", - "example": "This is a sample message content" - }, - "last_message_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703ca" - }, - "order_timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "owner": { - "type": "string", - "example": "+18005550199" - }, - "status": { - "type": "string", - "example": "PENDING" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.Phone": { - "type": "object", - "required": [ - "created_at", - "id", - "max_send_attempts", - "message_expiration_seconds", - "messages_per_minute", - "phone_number", - "sim", - "updated_at", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "max_send_attempts": { - "description": "MaxSendAttempts determines how many times to retry sending an SMS message", - "type": "integer", - "example": 2 - }, - "message_expiration_seconds": { - "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", - "type": "integer" - }, - "message_send_schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "messages_per_minute": { - "type": "integer", - "example": 1 - }, - "missed_call_auto_reply": { - "type": "string", - "example": "This phone cannot receive calls. Please send an SMS instead." - }, - "phone_number": { - "type": "string", - "example": "+18005550199" - }, - "sim": { - "$ref": "#/definitions/entities.SIM" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.PhoneAPIKey": { - "type": "object", - "required": [ - "api_key", - "created_at", - "id", - "name", - "phone_ids", - "phone_numbers", - "updated_at", - "user_email", - "user_id" - ], - "properties": { - "api_key": { - "type": "string", - "example": "pk_DGW8NwQp7mxKaSZ72Xq9v6xxxxx" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "name": { - "type": "string", - "example": "Business Phone Key" - }, - "phone_ids": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "32343a19-da5e-4b1b-a767-3298a73703cb", - "32343a19-da5e-4b1b-a767-3298a73703cc" - ] - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["+18005550199", "+18005550100"] - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" + "name": "support@httpsms.com", + "email": "support@httpsms.com" }, - "user_email": { - "type": "string", - "example": "user@gmail.com" + "license": { + "name": "AGPL-3.0", + "url": "https://raw.githubusercontent.com/NdoleStudio/http-sms-manager/main/LICENSE" }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "entities.SIM": { - "type": "string", - "enum": ["SIM1", "SIM2"], - "x-enum-varnames": ["SIM1", "SIM2"] + "version": "1.0" }, - "entities.SubscriptionName": { - "type": "string", - "enum": [ - "free", - "pro-monthly", - "pro-yearly", - "ultra-monthly", - "ultra-yearly", - "pro-lifetime", - "20k-monthly", - "100k-monthly", - "50k-monthly", - "200k-monthly", - "20k-yearly" - ], - "x-enum-varnames": [ - "SubscriptionNameFree", - "SubscriptionNameProMonthly", - "SubscriptionNameProYearly", - "SubscriptionNameUltraMonthly", - "SubscriptionNameUltraYearly", - "SubscriptionNameProLifetime", - "SubscriptionName20KMonthly", - "SubscriptionName100KMonthly", - "SubscriptionName50KMonthly", - "SubscriptionName200KMonthly", - "SubscriptionName20KYearly" - ] - }, - "entities.User": { - "type": "object", - "required": [ - "api_key", - "created_at", - "email", - "id", - "notification_heartbeat_enabled", - "notification_message_status_enabled", - "notification_newsletter_enabled", - "notification_webhook_enabled", - "subscription_id", - "subscription_name", - "timezone", - "updated_at" - ], - "properties": { - "active_phone_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "api_key": { - "type": "string", - "example": "x-api-key" - }, - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "email": { - "type": "string", - "example": "name@email.com" - }, - "id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - }, - "notification_heartbeat_enabled": { - "type": "boolean", - "example": true - }, - "notification_message_status_enabled": { - "type": "boolean", - "example": true - }, - "notification_newsletter_enabled": { - "type": "boolean", - "example": true - }, - "notification_webhook_enabled": { - "type": "boolean", - "example": true - }, - "subscription_ends_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "subscription_id": { - "type": "string", - "example": "8f9c71b8-b84e-4417-8408-a62274f65a08" - }, - "subscription_name": { - "allOf": [ - { - "$ref": "#/definitions/entities.SubscriptionName" + "host": "api.httpsms.com", + "basePath": "/v1", + "paths": { + "/billing/usage": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the summary of sent and received messages for a user in the current month", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Billing" + ], + "summary": "Get Billing Usage.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BillingUsageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } } - ], - "example": "free" - }, - "subscription_renews_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "subscription_status": { - "type": "string", - "example": "on_trial" - }, - "timezone": { - "type": "string", - "example": "Europe/Helsinki" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - } - } - }, - "entities.Webhook": { - "type": "object", - "required": [ - "created_at", - "events", - "id", - "phone_numbers", - "signing_key", - "updated_at", - "url", - "user_id" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2022-06-05T14:26:02.302718+03:00" - }, - "events": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["message.phone.received"] - }, - "id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["+18005550199", "+18005550100"] - }, - "signing_key": { - "type": "string", - "example": "DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY" - }, - "updated_at": { - "type": "string", - "example": "2022-06-05T14:26:10.303278+03:00" - }, - "url": { - "type": "string", - "example": "https://example.com" - }, - "user_id": { - "type": "string", - "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" - } - } - }, - "requests.DiscordStore": { - "type": "object", - "required": ["incoming_channel_id", "name", "server_id"], - "properties": { - "incoming_channel_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "server_id": { - "type": "string" - } - } - }, - "requests.DiscordUpdate": { - "type": "object", - "required": ["incoming_channel_id", "name", "server_id"], - "properties": { - "incoming_channel_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "server_id": { - "type": "string" - } - } - }, - "requests.HeartbeatStore": { - "type": "object", - "required": ["charging", "phone_numbers"], - "properties": { - "charging": { - "type": "boolean" - }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "requests.MessageAttachment": { - "type": "object", - "required": ["content", "content_type", "name"], - "properties": { - "content": { - "description": "Content is the base64-encoded attachment data", - "type": "string", - "example": "base64data..." - }, - "content_type": { - "description": "ContentType is the MIME type of the attachment", - "type": "string", - "example": "image/jpeg" - }, - "name": { - "description": "Name is the original filename of the attachment", - "type": "string", - "example": "photo.jpg" - } - } - }, - "requests.MessageBulkSend": { - "type": "object", - "required": ["content", "from", "to"], - "properties": { - "attachments": { - "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", - "type": "array", - "items": { - "type": "string" - } - }, - "content": { - "type": "string", - "example": "This is a sample text message" }, - "encrypted": { - "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false - }, - "from": { - "type": "string", - "example": "+18005550199" + "/billing/usage-history": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Billing" + ], + "summary": "Get billing usage history.", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "number of heartbeats to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BillingUsagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "request_id": { - "description": "RequestID is an optional parameter used to track a request from the client's perspective", - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + "/bulk-messages": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the last 10 bulk message order summaries for the authenticated user showing counts per status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "BulkSMS" + ], + "summary": "List bulk message orders", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.BulkMessagesResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx).", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "BulkSMS" + ], + "summary": "Store bulk SMS file", + "parameters": [ + { + "type": "file", + "description": "The Excel or CSV file containing the messages to be sent.", + "name": "document", + "in": "formData", + "required": true + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "to": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["+18005550100", "+18005550100"] - } - } - }, - "requests.MessageCallMissed": { - "type": "object", - "required": ["from", "sim", "timestamp", "to"], - "properties": { - "from": { - "type": "string", - "example": "+18005550199" + "/discord-integrations": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the discord integrations of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DiscordIntegration" + ], + "summary": "Get discord integrations of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of discord integrations to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter discord integrations containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of discord integrations to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.DiscordsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store a discord integration for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DiscordIntegration" + ], + "summary": "Store discord integration", + "parameters": [ + { + "description": "Payload of the discord integration request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.DiscordStore" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.DiscordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "sim": { - "type": "string", - "example": "SIM1" + "/discord-integrations/{discordID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a discord integration for the currently authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DiscordIntegration" + ], + "summary": "Update a discord integration", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the discord integration", + "name": "discordID", + "in": "path", + "required": true + }, + { + "description": "Payload of discord integration to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.DiscordUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.DiscordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a discord integration for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhooks" + ], + "summary": "Delete discord integration", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the discord integration", + "name": "discordID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "timestamp": { - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" + "/discord/event": { + "post": { + "description": "Publish a discord event to the registered listeners", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Discord" + ], + "summary": "Consume a discord event", + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "to": { - "type": "string", - "example": "+18005550100" - } - } - }, - "requests.MessageEvent": { - "type": "object", - "required": ["event_name", "reason", "timestamp"], - "properties": { - "event_name": { - "description": "EventName is the type of event\n* SENT: is emitted when a message is sent by the mobile phone\n* FAILED: is event is emitted when the message could not be sent by the mobile phone\n* DELIVERED: is event is emitted when a delivery report has been received by the mobile phone", - "type": "string", - "example": "SENT" + "/heartbeats": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Heartbeats" + ], + "summary": "Get heartbeats of an owner phone number", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "the owner's phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of heartbeats to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HeartbeatsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store the heartbeat to make notify that a phone number is still active", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Heartbeats" + ], + "summary": "Register heartbeat of an owner phone number", + "parameters": [ + { + "description": "Payload of the heartbeat request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.HeartbeatStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.HeartbeatResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "reason": { - "description": "Reason is the exact error message in case the event is an error", - "type": "string" + "/integration/3cx/messages": { + "post": { + "description": "Sends an SMS message from the 3CX platform", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "3CXIntegration" + ], + "summary": "Sends a 3CX SMS message", + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "timestamp": { - "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" - } - } - }, - "requests.MessageReceive": { - "type": "object", - "required": ["content", "encrypted", "from", "sim", "timestamp", "to"], - "properties": { - "attachments": { - "description": "Attachments is the list of MMS attachments received with the message", - "type": "array", - "items": { - "$ref": "#/definitions/requests.MessageAttachment" - } + "/message-threads": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MessageThreads" + ], + "summary": "Get message threads for a phone number", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "owner phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter message threads containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageThreadsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "content": { - "type": "string", - "example": "This is a sample text message received on a phone" + "/message-threads/{messageThreadID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the details of a message thread", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MessageThreads" + ], + "summary": "Update a message thread", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message thread", + "name": "messageThreadID", + "in": "path", + "required": true + }, + { + "description": "Payload of message thread details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageThreadUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a message thread from the database and also deletes all the messages in the thread.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "MessageThreads" + ], + "summary": "Delete a message thread from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message thread", + "name": "messageThreadID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "encrypted": { - "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false + "/messages": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Get messages which are sent between 2 phone numbers", + "parameters": [ + { + "type": "string", + "default": "+18005550199", + "description": "the owner's phone number", + "name": "owner", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "+18005550100", + "description": "the contact's phone number", + "name": "contact", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter messages containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "from": { - "type": "string", - "example": "+18005550199" + "/messages/bulk-send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add bulk SMS messages to be sent by the android phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send bulk SMS messages", + "parameters": [ + { + "description": "Bulk send message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageBulkSend" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.MessagesResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "sim": { - "description": "SIM card that received the message", - "allOf": [ - { - "$ref": "#/definitions/entities.SIM" + "/messages/calls/missed": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Register a missed call event on the mobile phone", + "parameters": [ + { + "description": "Payload of the missed call event.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageCallMissed" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } } - ], - "example": "SIM1" }, - "timestamp": { - "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", - "type": "string", - "example": "2022-06-05T14:26:09.527976+03:00" + "/messages/outstanding": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get an outstanding message to be sent by an android phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Get an outstanding message", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703cb", + "description": "The ID of the message", + "name": "message_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "to": { - "type": "string", - "example": "+18005550100" - } - } - }, - "requests.MessageSend": { - "type": "object", - "required": ["content", "from", "to"], - "properties": { - "attachments": { - "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://example.com/image.jpg", - "https://example.com/video.mp4" - ] + "/messages/receive": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add a new message received from a mobile phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Receive a new SMS message from a mobile phone", + "parameters": [ + { + "description": "Received message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageReceive" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "content": { - "type": "string", - "example": "This is a sample text message" + "/messages/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "This returns the list of all messages based on the filter criteria including missed calls", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Search all messages of a user", + "parameters": [ + { + "type": "string", + "description": "Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/", + "name": "token", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "+18005550199,+18005550100", + "description": "the owner's phone numbers", + "name": "owners", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "description": "number of messages to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter messages containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 200, + "minimum": 1, + "type": "integer", + "description": "number of messages to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessagesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "encrypted": { - "description": "Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", - "type": "boolean", - "example": false + "/messages/send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add a new SMS message to be sent by your Android phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send an SMS message", + "parameters": [ + { + "description": "Send message request payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSend" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "from": { - "type": "string", - "example": "+18005550199" + "/messages/{messageID}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a message from the database by the message ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Get a message from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a message from the database and removes the message content from the list of threads.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Delete a message from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "request_id": { - "description": "RequestID is an optional parameter used to track a request from the client's perspective", - "type": "string", - "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + "/messages/{messageID}/events": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Upsert an event for a message on the mobile phone", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the message", + "name": "messageID", + "in": "path", + "required": true + }, + { + "description": "Payload of the event emitted.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageEvent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "send_at": { - "description": "SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future.", - "type": "string", - "example": "2025-12-19T16:39:57-08:00" + "/phone-api-keys": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list phone API keys which a user has registered on the httpSMS application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PhoneAPIKeys" + ], + "summary": "Get the phone API keys of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of phone api keys to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter phone api keys with name containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "number of phone api keys to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneAPIKeysResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PhoneAPIKeys" + ], + "summary": "Store phone API key", + "parameters": [ + { + "description": "Payload of new phone API key.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneAPIKeyStoreRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneAPIKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.PaymentRequired" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "to": { - "type": "string", - "example": "+18005550100" - } - } - }, - "requests.MessageSendScheduleStore": { - "type": "object", - "required": ["name", "timezone", "windows"], - "properties": { - "name": { - "type": "string" + "/phone-api-keys/{phoneAPIKeyID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a phone API Key from the database and cannot be used for authentication anymore.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PhoneAPIKeys" + ], + "summary": "Delete a phone API key from the database.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone API key", + "name": "phoneAPIKeyID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "timezone": { - "type": "string" + "/phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "You will need to login again to the httpSMS app on your Android phone with a new phone API key.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PhoneAPIKeys" + ], + "summary": "Remove the association of a phone from the phone API key.", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone API key", + "name": "phoneAPIKeyID", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone", + "name": "phoneID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "windows": { - "type": "array", - "items": { - "$ref": "#/definitions/requests.MessageSendScheduleWindow" - } - } - } - }, - "requests.MessageSendScheduleWindow": { - "type": "object", - "required": ["day_of_week", "end_minute", "start_minute"], - "properties": { - "day_of_week": { - "type": "integer" + "/phones": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get list of phones which a user has registered on the http sms application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Phones" + ], + "summary": "Get phones of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of heartbeats to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter phones containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of phones to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhonesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Phones" + ], + "summary": "Upsert Phone", + "parameters": [ + { + "description": "Payload of new phone number.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneUpsert" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "end_minute": { - "type": "integer" + "/phones/fcm-token": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Phones" + ], + "summary": "Upserts the FCM token of a phone", + "parameters": [ + { + "description": "Payload of new FCM token.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.PhoneFCMToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "start_minute": { - "type": "integer" - } - } - }, - "requests.MessageThreadUpdate": { - "type": "object", - "required": ["is_archived"], - "properties": { - "is_archived": { - "type": "boolean", - "example": true - } - } - }, - "requests.PhoneAPIKeyStoreRequest": { - "type": "object", - "required": ["name"], - "properties": { - "name": { - "type": "string", - "example": "My Phone API Key" - } - } - }, - "requests.PhoneFCMToken": { - "type": "object", - "required": ["fcm_token", "phone_number", "sim"], - "properties": { - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." + "/phones/{phoneID}": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a phone that has been sored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Phones" + ], + "summary": "Delete Phone", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the phone", + "name": "phoneID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "phone_number": { - "type": "string", - "example": "[+18005550199]" + "/send-schedules": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "List all send schedules owned by the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "SendSchedules" + ], + "summary": "List send schedules", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageSendSchedulesResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Create a new send schedule for the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SendSchedules" + ], + "summary": "Create send schedule", + "parameters": [ + { + "description": "Payload of new send schedule.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSendScheduleStore" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.MessageSendScheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/responses.PaymentRequired" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "sim": { - "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", - "type": "string", - "example": "SIM1" - } - } - }, - "requests.PhoneUpsert": { - "type": "object", - "required": [ - "fcm_token", - "max_send_attempts", - "message_expiration_seconds", - "messages_per_minute", - "missed_call_auto_reply", - "phone_number", - "sim" - ], - "properties": { - "fcm_token": { - "type": "string", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." + "/send-schedules/{scheduleID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a send schedule owned by the authenticated user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SendSchedules" + ], + "summary": "Update send schedule", + "parameters": [ + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleID", + "in": "path", + "required": true + }, + { + "description": "Payload of updated send schedule.", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.MessageSendScheduleStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.MessageSendScheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a send schedule owned by the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "SendSchedules" + ], + "summary": "Delete send schedule", + "parameters": [ + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "max_send_attempts": { - "description": "MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline.", - "type": "integer", - "example": 2 + "/users/me": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get details of the currently authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Updates the details of the currently authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update a user", + "parameters": [ + { + "description": "Payload of user details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.PhoneResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Deletes the currently authenticated user together with all their data.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Delete a user", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "message_expiration_seconds": { - "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", - "type": "integer", - "example": 12345 + "/users/subscription": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Cancel the subscription of the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Cancel the user's subscription", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "message_send_schedule_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + "/users/subscription-update-url": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fetches the subscription URL of the authenticated user.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Currently authenticated user subscription update URL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.OkString" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "messages_per_minute": { - "type": "integer", - "example": 1 + "/users/subscription/invoices/{subscriptionInvoiceID}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Generates a new invoice PDF file for the given subscription payment with given parameters.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/pdf" + ], + "tags": [ + "Users" + ], + "summary": "Generate a subscription payment invoice", + "parameters": [ + { + "description": "Generate subscription payment invoice parameters", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserPaymentInvoice" + } + }, + { + "type": "string", + "description": "ID of the subscription invoice to generate the PDF for", + "name": "subscriptionInvoiceID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "missed_call_auto_reply": { - "type": "string", - "example": "e.g. This phone cannot receive calls. Please send an SMS instead." + "/users/subscription/payments": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get the last 10 subscription payments.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserSubscriptionPaymentsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "phone_number": { - "type": "string", - "example": "+18005550199" + "/users/{userID}/api-keys": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Rotate the user's API key in case the current API Key is compromised", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Rotate the user's API Key", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the user to update", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "sim": { - "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", - "type": "string", - "example": "SIM1" - } - } - }, - "requests.UserNotificationUpdate": { - "type": "object", - "required": [ - "heartbeat_enabled", - "message_status_enabled", - "newsletter_enabled", - "webhook_enabled" - ], - "properties": { - "heartbeat_enabled": { - "type": "boolean", - "example": true + "/users/{userID}/notifications": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update the email notification settings for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update notification settings", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the user to update", + "name": "userID", + "in": "path", + "required": true + }, + { + "description": "User notification details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UserNotificationUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "message_status_enabled": { - "type": "boolean", - "example": true + "/v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}": { + "get": { + "description": "Download an MMS attachment by its path components", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Attachments" + ], + "summary": "Download a message attachment", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "userID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message ID", + "name": "messageID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Attachment index", + "name": "attachmentIndex", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Filename with extension", + "name": "filename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "newsletter_enabled": { - "type": "boolean", - "example": true + "/webhooks": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get the webhooks of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhooks" + ], + "summary": "Get webhooks of a user", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "description": "number of webhooks to skip", + "name": "skip", + "in": "query" + }, + { + "type": "string", + "description": "filter webhooks containing query", + "name": "query", + "in": "query" + }, + { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "number of webhooks to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhooksResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Store a webhook for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhooks" + ], + "summary": "Store a webhook", + "parameters": [ + { + "description": "Payload of the webhook request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.WebhookStore" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhookResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } }, - "webhook_enabled": { - "type": "boolean", - "example": true + "/webhooks/{webhookID}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a webhook for the currently authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhooks" + ], + "summary": "Update a webhook", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the webhook", + "name": "webhookID", + "in": "path", + "required": true + }, + { + "description": "Payload of webhook details to update", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.WebhookUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.WebhookResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Delete a webhook for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhooks" + ], + "summary": "Delete webhook", + "parameters": [ + { + "type": "string", + "default": "32343a19-da5e-4b1b-a767-3298a73703ca", + "description": "ID of the webhook", + "name": "webhookID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/responses.NoContent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/responses.BadRequest" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/responses.Unauthorized" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/responses.UnprocessableEntity" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/responses.InternalServerError" + } + } + } + } } - } }, - "requests.UserPaymentInvoice": { - "type": "object", - "required": [ - "address", - "city", - "country", - "name", - "notes", - "state", - "zip_code" - ], - "properties": { - "address": { - "type": "string", - "example": "221B Baker Street, London" - }, - "city": { - "type": "string", - "example": "Los Angeles" - }, - "country": { - "type": "string", - "example": "US" + "definitions": { + "entities.BillingUsage": { + "type": "object", + "required": [ + "created_at", + "end_timestamp", + "id", + "received_messages", + "sent_messages", + "start_timestamp", + "total_cost", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "end_timestamp": { + "type": "string", + "example": "2022-01-31T23:59:59+00:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "received_messages": { + "type": "integer", + "example": 465 + }, + "sent_messages": { + "type": "integer", + "example": 321 + }, + "start_timestamp": { + "type": "string", + "example": "2022-01-01T00:00:00+00:00" + }, + "total_cost": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "name": { - "type": "string", - "example": "Acme Corp" + "entities.BulkMessage": { + "type": "object", + "required": [ + "created_at", + "delivered_count", + "failed_count", + "pending_count", + "request_id", + "scheduled_count", + "sent_count", + "total" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "delivered_count": { + "type": "integer", + "example": 25 + }, + "failed_count": { + "type": "integer", + "example": 5 + }, + "pending_count": { + "type": "integer", + "example": 30 + }, + "request_id": { + "type": "string", + "example": "bulk-32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "scheduled_count": { + "type": "integer", + "example": 50 + }, + "sent_count": { + "type": "integer", + "example": 40 + }, + "total": { + "type": "integer", + "example": 150 + } + } }, - "notes": { - "type": "string", - "example": "Thank you for your business!" + "entities.Discord": { + "type": "object", + "required": [ + "created_at", + "id", + "incoming_channel_id", + "name", + "server_id", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "incoming_channel_id": { + "type": "string", + "example": "1095780203256627291" + }, + "name": { + "type": "string", + "example": "Game Server" + }, + "server_id": { + "type": "string", + "example": "1095778291488653372" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "state": { - "type": "string", - "example": "CA" + "entities.Heartbeat": { + "type": "object", + "required": [ + "charging", + "id", + "owner", + "timestamp", + "user_id", + "version" + ], + "properties": { + "charging": { + "type": "boolean", + "example": true + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "timestamp": { + "type": "string", + "example": "2022-06-05T14:26:01.520828+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "version": { + "type": "string", + "example": "344c10f" + } + } }, - "zip_code": { - "type": "string", - "example": "9800" - } - } - }, - "requests.UserUpdate": { - "type": "object", - "required": ["active_phone_id", "timezone"], - "properties": { - "active_phone_id": { - "type": "string", - "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + "entities.Message": { + "type": "object", + "required": [ + "attachments", + "contact", + "content", + "created_at", + "encrypted", + "id", + "max_send_attempts", + "order_timestamp", + "owner", + "request_received_at", + "send_attempt_count", + "sim", + "status", + "type", + "updated_at", + "user_id" + ], + "properties": { + "attachments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://example.com/image.jpg", + "https://example.com/video.mp4" + ] + }, + "contact": { + "type": "string", + "example": "+18005550100" + }, + "content": { + "type": "string", + "example": "This is a sample text message" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "delivered_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "encrypted": { + "type": "boolean", + "example": false + }, + "expired_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "failed_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "failure_reason": { + "type": "string", + "example": "UNKNOWN" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "last_attempted_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "max_send_attempts": { + "type": "integer", + "example": 1 + }, + "order_timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "received_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "request_id": { + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + }, + "request_received_at": { + "type": "string", + "example": "2022-06-05T14:26:01.520828+03:00" + }, + "scheduled_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "scheduled_send_time": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "send_attempt_count": { + "type": "integer", + "example": 0 + }, + "send_time": { + "description": "SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message", + "type": "integer", + "example": 133414 + }, + "sent_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "sim": { + "description": "SIM is the SIM card to use to send the message\n* SMS1: use the SIM card in slot 1\n* SMS2: use the SIM card in slot 2\n* DEFAULT: used the default communication SIM card", + "allOf": [ + { + "$ref": "#/definitions/entities.SIM" + } + ], + "example": "DEFAULT" + }, + "status": { + "type": "string", + "example": "pending" + }, + "type": { + "type": "string", + "example": "mobile-terminated" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "timezone": { - "type": "string", - "example": "Europe/Helsinki" - } - } - }, - "requests.WebhookStore": { - "type": "object", - "required": ["events", "phone_numbers", "signing_key", "url"], - "properties": { - "events": { - "type": "array", - "items": { - "type": "string" - } + "entities.MessageSendSchedule": { + "type": "object", + "required": [ + "created_at", + "id", + "name", + "timezone", + "updated_at", + "user_id", + "windows" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "name": { + "type": "string", + "example": "Business Hours" + }, + "timezone": { + "type": "string", + "example": "Europe/Tallinn" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendScheduleWindow" + } + } + } }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["+18005550100", "+18005550100"] + "entities.MessageSendScheduleWindow": { + "type": "object", + "required": [ + "day_of_week", + "end_minute", + "start_minute" + ], + "properties": { + "day_of_week": { + "type": "integer", + "example": 1 + }, + "end_minute": { + "type": "integer", + "example": 1020 + }, + "start_minute": { + "type": "integer", + "example": 540 + } + } }, - "signing_key": { - "type": "string" + "entities.MessageThread": { + "type": "object", + "required": [ + "color", + "contact", + "created_at", + "id", + "is_archived", + "last_message_content", + "last_message_id", + "order_timestamp", + "owner", + "status", + "updated_at", + "user_id" + ], + "properties": { + "color": { + "type": "string", + "example": "indigo" + }, + "contact": { + "type": "string", + "example": "+18005550100" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703ca" + }, + "is_archived": { + "type": "boolean", + "example": false + }, + "last_message_content": { + "type": "string", + "example": "This is a sample message content" + }, + "last_message_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703ca" + }, + "order_timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "owner": { + "type": "string", + "example": "+18005550199" + }, + "status": { + "type": "string", + "example": "PENDING" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "url": { - "type": "string" - } - } - }, - "requests.WebhookUpdate": { - "type": "object", - "required": ["events", "phone_numbers", "signing_key", "url"], - "properties": { - "events": { - "type": "array", - "items": { - "type": "string" - } + "entities.Phone": { + "type": "object", + "required": [ + "created_at", + "id", + "max_send_attempts", + "message_expiration_seconds", + "messages_per_minute", + "phone_number", + "sim", + "updated_at", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "max_send_attempts": { + "description": "MaxSendAttempts determines how many times to retry sending an SMS message", + "type": "integer", + "example": 2 + }, + "message_expiration_seconds": { + "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", + "type": "integer" + }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "messages_per_minute": { + "type": "integer", + "example": 1 + }, + "missed_call_auto_reply": { + "type": "string", + "example": "This phone cannot receive calls. Please send an SMS instead." + }, + "phone_number": { + "type": "string", + "example": "+18005550199" + }, + "sim": { + "$ref": "#/definitions/entities.SIM" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "phone_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["+18005550100", "+18005550100"] + "entities.PhoneAPIKey": { + "type": "object", + "required": [ + "api_key", + "created_at", + "id", + "name", + "phone_ids", + "phone_numbers", + "updated_at", + "user_email", + "user_id" + ], + "properties": { + "api_key": { + "type": "string", + "example": "pk_DGW8NwQp7mxKaSZ72Xq9v6xxxxx" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "name": { + "type": "string", + "example": "Business Phone Key" + }, + "phone_ids": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "32343a19-da5e-4b1b-a767-3298a73703cb", + "32343a19-da5e-4b1b-a767-3298a73703cc" + ] + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "+18005550199", + "+18005550100" + ] + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "user_email": { + "type": "string", + "example": "user@gmail.com" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "signing_key": { - "type": "string" + "entities.SIM": { + "type": "string", + "enum": [ + "SIM1", + "SIM2" + ], + "x-enum-varnames": [ + "SIM1", + "SIM2" + ] + }, + "entities.SubscriptionName": { + "type": "string", + "enum": [ + "free", + "pro-monthly", + "pro-yearly", + "ultra-monthly", + "ultra-yearly", + "pro-lifetime", + "20k-monthly", + "100k-monthly", + "50k-monthly", + "200k-monthly", + "20k-yearly" + ], + "x-enum-varnames": [ + "SubscriptionNameFree", + "SubscriptionNameProMonthly", + "SubscriptionNameProYearly", + "SubscriptionNameUltraMonthly", + "SubscriptionNameUltraYearly", + "SubscriptionNameProLifetime", + "SubscriptionName20KMonthly", + "SubscriptionName100KMonthly", + "SubscriptionName50KMonthly", + "SubscriptionName200KMonthly", + "SubscriptionName20KYearly" + ] + }, + "entities.User": { + "type": "object", + "required": [ + "api_key", + "created_at", + "email", + "id", + "notification_heartbeat_enabled", + "notification_message_status_enabled", + "notification_newsletter_enabled", + "notification_webhook_enabled", + "subscription_id", + "subscription_name", + "timezone", + "updated_at" + ], + "properties": { + "active_phone_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "api_key": { + "type": "string", + "example": "x-api-key" + }, + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "email": { + "type": "string", + "example": "name@email.com" + }, + "id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + }, + "notification_heartbeat_enabled": { + "type": "boolean", + "example": true + }, + "notification_message_status_enabled": { + "type": "boolean", + "example": true + }, + "notification_newsletter_enabled": { + "type": "boolean", + "example": true + }, + "notification_webhook_enabled": { + "type": "boolean", + "example": true + }, + "subscription_ends_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "subscription_id": { + "type": "string", + "example": "8f9c71b8-b84e-4417-8408-a62274f65a08" + }, + "subscription_name": { + "allOf": [ + { + "$ref": "#/definitions/entities.SubscriptionName" + } + ], + "example": "free" + }, + "subscription_renews_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "subscription_status": { + "type": "string", + "example": "on_trial" + }, + "timezone": { + "type": "string", + "example": "Europe/Helsinki" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + } + } }, - "url": { - "type": "string" - } - } - }, - "responses.BadRequest": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "string", - "example": "The request body is not a valid JSON string" + "entities.Webhook": { + "type": "object", + "required": [ + "created_at", + "events", + "id", + "phone_numbers", + "signing_key", + "updated_at", + "url", + "user_id" + ], + "properties": { + "created_at": { + "type": "string", + "example": "2022-06-05T14:26:02.302718+03:00" + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "message.phone.received" + ] + }, + "id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "+18005550199", + "+18005550100" + ] + }, + "signing_key": { + "type": "string", + "example": "DGW8NwQp7mxKaSZ72Xq9v67SLqSbWQvckzzmK8D6rvd7NywSEkdMJtuxKyEkYnCY" + }, + "updated_at": { + "type": "string", + "example": "2022-06-05T14:26:10.303278+03:00" + }, + "url": { + "type": "string", + "example": "https://example.com" + }, + "user_id": { + "type": "string", + "example": "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" + } + } }, - "message": { - "type": "string", - "example": "The request isn't properly formed" + "requests.DiscordStore": { + "type": "object", + "required": [ + "incoming_channel_id", + "name", + "server_id" + ], + "properties": { + "incoming_channel_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "server_id": { + "type": "string" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.BillingUsageResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.BillingUsage" + "requests.DiscordUpdate": { + "type": "object", + "required": [ + "incoming_channel_id", + "name", + "server_id" + ], + "properties": { + "incoming_channel_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "server_id": { + "type": "string" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.HeartbeatStore": { + "type": "object", + "required": [ + "charging", + "phone_numbers" + ], + "properties": { + "charging": { + "type": "boolean" + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + } + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.BillingUsagesResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.BillingUsage" - } + "requests.MessageAttachment": { + "type": "object", + "required": [ + "content", + "content_type", + "name" + ], + "properties": { + "content": { + "description": "Content is the base64-encoded attachment data", + "type": "string", + "example": "base64data..." + }, + "content_type": { + "description": "ContentType is the MIME type of the attachment", + "type": "string", + "example": "image/jpeg" + }, + "name": { + "description": "Name is the original filename of the attachment", + "type": "string", + "example": "photo.jpg" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.MessageBulkSend": { + "type": "object", + "required": [ + "content", + "from", + "to" + ], + "properties": { + "attachments": { + "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", + "type": "array", + "items": { + "type": "string" + } + }, + "content": { + "type": "string", + "example": "This is a sample text message" + }, + "encrypted": { + "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false + }, + "from": { + "type": "string", + "example": "+18005550199" + }, + "request_id": { + "description": "RequestID is an optional parameter used to track a request from the client's perspective", + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + }, + "to": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "+18005550100", + "+18005550100" + ] + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.DiscordResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.Discord" + "requests.MessageCallMissed": { + "type": "object", + "required": [ + "from", + "sim", + "timestamp", + "to" + ], + "properties": { + "from": { + "type": "string", + "example": "+18005550199" + }, + "sim": { + "type": "string", + "example": "SIM1" + }, + "timestamp": { + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "to": { + "type": "string", + "example": "+18005550100" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.MessageEvent": { + "type": "object", + "required": [ + "event_name", + "reason", + "timestamp" + ], + "properties": { + "event_name": { + "description": "EventName is the type of event\n* SENT: is emitted when a message is sent by the mobile phone\n* FAILED: is event is emitted when the message could not be sent by the mobile phone\n* DELIVERED: is event is emitted when a delivery report has been received by the mobile phone", + "type": "string", + "example": "SENT" + }, + "reason": { + "description": "Reason is the exact error message in case the event is an error", + "type": "string" + }, + "timestamp": { + "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.DiscordsResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Discord" - } + "requests.MessageReceive": { + "type": "object", + "required": [ + "content", + "encrypted", + "from", + "sim", + "timestamp", + "to" + ], + "properties": { + "attachments": { + "description": "Attachments is the list of MMS attachments received with the message", + "type": "array", + "items": { + "$ref": "#/definitions/requests.MessageAttachment" + } + }, + "content": { + "type": "string", + "example": "This is a sample text message received on a phone" + }, + "encrypted": { + "description": "Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false + }, + "from": { + "type": "string", + "example": "+18005550199" + }, + "sim": { + "description": "SIM card that received the message", + "allOf": [ + { + "$ref": "#/definitions/entities.SIM" + } + ], + "example": "SIM1" + }, + "timestamp": { + "description": "Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible", + "type": "string", + "example": "2022-06-05T14:26:09.527976+03:00" + }, + "to": { + "type": "string", + "example": "+18005550100" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.MessageSend": { + "type": "object", + "required": [ + "content", + "from", + "to" + ], + "properties": { + "attachments": { + "description": "Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://example.com/image.jpg", + "https://example.com/video.mp4" + ] + }, + "content": { + "type": "string", + "example": "This is a sample text message" + }, + "encrypted": { + "description": "Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app", + "type": "boolean", + "example": false + }, + "from": { + "type": "string", + "example": "+18005550199" + }, + "request_id": { + "description": "RequestID is an optional parameter used to track a request from the client's perspective", + "type": "string", + "example": "153554b5-ae44-44a0-8f4f-7bbac5657ad4" + }, + "send_at": { + "description": "SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future.", + "type": "string", + "example": "2025-12-19T16:39:57-08:00" + }, + "to": { + "type": "string", + "example": "+18005550100" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.HeartbeatResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.Heartbeat" + "requests.MessageSendScheduleStore": { + "type": "object", + "required": [ + "name", + "timezone", + "windows" + ], + "properties": { + "name": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "windows": { + "type": "array", + "items": { + "$ref": "#/definitions/requests.MessageSendScheduleWindow" + } + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.MessageSendScheduleWindow": { + "type": "object", + "required": [ + "day_of_week", + "end_minute", + "start_minute" + ], + "properties": { + "day_of_week": { + "type": "integer" + }, + "end_minute": { + "type": "integer" + }, + "start_minute": { + "type": "integer" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.HeartbeatsResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Heartbeat" - } + "requests.MessageThreadUpdate": { + "type": "object", + "required": [ + "is_archived" + ], + "properties": { + "is_archived": { + "type": "boolean", + "example": true + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.PhoneAPIKeyStoreRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "My Phone API Key" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.InternalServerError": { - "type": "object", - "required": ["message", "status"], - "properties": { - "message": { - "type": "string", - "example": "We ran into an internal error while handling the request." + "requests.PhoneFCMToken": { + "type": "object", + "required": [ + "fcm_token", + "phone_number", + "sim" + ], + "properties": { + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." + }, + "phone_number": { + "type": "string", + "example": "[+18005550199]" + }, + "sim": { + "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", + "type": "string", + "example": "SIM1" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.MessageResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.Message" + "requests.PhoneUpsert": { + "type": "object", + "required": [ + "fcm_token", + "max_send_attempts", + "message_expiration_seconds", + "messages_per_minute", + "missed_call_auto_reply", + "phone_number", + "sim" + ], + "properties": { + "fcm_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....." + }, + "max_send_attempts": { + "description": "MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline.", + "type": "integer", + "example": 2 + }, + "message_expiration_seconds": { + "description": "MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired.", + "type": "integer", + "example": 12345 + }, + "message_send_schedule_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "messages_per_minute": { + "type": "integer", + "example": 1 + }, + "missed_call_auto_reply": { + "type": "string", + "example": "e.g. This phone cannot receive calls. Please send an SMS instead." + }, + "phone_number": { + "type": "string", + "example": "+18005550199" + }, + "sim": { + "description": "SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot", + "type": "string", + "example": "SIM1" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.UserNotificationUpdate": { + "type": "object", + "required": [ + "heartbeat_enabled", + "message_status_enabled", + "newsletter_enabled", + "webhook_enabled" + ], + "properties": { + "heartbeat_enabled": { + "type": "boolean", + "example": true + }, + "message_status_enabled": { + "type": "boolean", + "example": true + }, + "newsletter_enabled": { + "type": "boolean", + "example": true + }, + "webhook_enabled": { + "type": "boolean", + "example": true + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.MessageSendScheduleResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.MessageSendSchedule" + "requests.UserPaymentInvoice": { + "type": "object", + "required": [ + "address", + "city", + "country", + "name", + "notes", + "state", + "zip_code" + ], + "properties": { + "address": { + "type": "string", + "example": "221B Baker Street, London" + }, + "city": { + "type": "string", + "example": "Los Angeles" + }, + "country": { + "type": "string", + "example": "US" + }, + "name": { + "type": "string", + "example": "Acme Corp" + }, + "notes": { + "type": "string", + "example": "Thank you for your business!" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip_code": { + "type": "string", + "example": "9800" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.UserUpdate": { + "type": "object", + "required": [ + "active_phone_id", + "timezone" + ], + "properties": { + "active_phone_id": { + "type": "string", + "example": "32343a19-da5e-4b1b-a767-3298a73703cb" + }, + "timezone": { + "type": "string", + "example": "Europe/Helsinki" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.MessageSendSchedulesResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageSendSchedule" - } + "requests.WebhookStore": { + "type": "object", + "required": [ + "events", + "phone_numbers", + "signing_key", + "url" + ], + "properties": { + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "+18005550100", + "+18005550100" + ] + }, + "signing_key": { + "type": "string" + }, + "url": { + "type": "string" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "requests.WebhookUpdate": { + "type": "object", + "required": [ + "events", + "phone_numbers", + "signing_key", + "url" + ], + "properties": { + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "phone_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "+18005550100", + "+18005550100" + ] + }, + "signing_key": { + "type": "string" + }, + "url": { + "type": "string" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.MessageThreadsResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.MessageThread" - } + "responses.BadRequest": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "string", + "example": "The request body is not a valid JSON string" + }, + "message": { + "type": "string", + "example": "The request isn't properly formed" + }, + "status": { + "type": "string", + "example": "error" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.BillingUsageResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.BillingUsage" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.MessagesResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Message" - } + "responses.BillingUsagesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.BillingUsage" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.BulkMessagesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.BulkMessage" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.NoContent": { - "type": "object", - "required": ["message", "status"], - "properties": { - "message": { - "type": "string", - "example": "action performed successfully" + "responses.DiscordResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.Discord" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.NotFound": { - "type": "object", - "required": ["message", "status"], - "properties": { - "message": { - "type": "string", - "example": "cannot find message with ID [32343a19-da5e-4b1b-a767-3298a73703ca]" + "responses.DiscordsResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Discord" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.OkString": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "string" + "responses.HeartbeatResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.Heartbeat" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.HeartbeatsResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Heartbeat" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.PaymentRequired": { - "type": "object", - "required": ["message", "status"], - "properties": { - "message": { - "type": "string", - "example": "You have reached the maximum number of allowed resources. Please upgrade your plan." + "responses.InternalServerError": { + "type": "object", + "required": [ + "message", + "status" + ], + "properties": { + "message": { + "type": "string", + "example": "We ran into an internal error while handling the request." + }, + "status": { + "type": "string", + "example": "error" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.PhoneAPIKeyResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.PhoneAPIKey" + "responses.MessageResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.Message" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.MessageSendScheduleResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageSendSchedule" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.PhoneAPIKeysResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.PhoneAPIKey" - } + "responses.MessageSendSchedulesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageSendSchedule" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.MessageThreadsResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.MessageThread" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.PhoneResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.Phone" + "responses.MessagesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Message" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.NoContent": { + "type": "object", + "required": [ + "message", + "status" + ], + "properties": { + "message": { + "type": "string", + "example": "action performed successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.PhonesResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Phone" - } + "responses.NotFound": { + "type": "object", + "required": [ + "message", + "status" + ], + "properties": { + "message": { + "type": "string", + "example": "cannot find message with ID [32343a19-da5e-4b1b-a767-3298a73703ca]" + }, + "status": { + "type": "string", + "example": "error" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.OkString": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "string" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.Unauthorized": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "string", - "example": "Make sure your API key is set in the [X-API-Key] header in the request" + "responses.PaymentRequired": { + "type": "object", + "required": [ + "message", + "status" + ], + "properties": { + "message": { + "type": "string", + "example": "You have reached the maximum number of allowed resources. Please upgrade your plan." + }, + "status": { + "type": "string", + "example": "error" + } + } }, - "message": { - "type": "string", - "example": "You are not authorized to carry out this request." + "responses.PhoneAPIKeyResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.PhoneAPIKey" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.UnprocessableEntity": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" + "responses.PhoneAPIKeysResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.PhoneAPIKey" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } } - } }, - "message": { - "type": "string", - "example": "validation errors while handling request" + "responses.PhoneResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.Phone" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "error" - } - } - }, - "responses.UserResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.User" + "responses.PhonesResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Phone" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.Unauthorized": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "string", + "example": "Make sure your API key is set in the [X-API-Key] header in the request" + }, + "message": { + "type": "string", + "example": "You are not authorized to carry out this request." + }, + "status": { + "type": "string", + "example": "error" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.UserSubscriptionPaymentsResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { + "responses.UnprocessableEntity": { "type": "object", - "required": ["attributes", "id", "type"], + "required": [ + "data", + "message", + "status" + ], "properties": { - "attributes": { - "type": "object", - "required": [ - "billing_reason", - "card_brand", - "card_last_four", - "created_at", - "currency", - "currency_rate", - "discount_total", - "discount_total_formatted", - "discount_total_usd", - "refunded", - "refunded_amount", - "refunded_amount_formatted", - "refunded_amount_usd", - "refunded_at", - "status", - "status_formatted", - "subtotal", - "subtotal_formatted", - "subtotal_usd", - "tax", - "tax_formatted", - "tax_inclusive", - "tax_usd", - "total", - "total_formatted", - "total_usd", - "updated_at" - ], - "properties": { - "billing_reason": { - "type": "string" - }, - "card_brand": { - "type": "string" - }, - "card_last_four": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "currency_rate": { - "type": "string" - }, - "discount_total": { - "type": "integer" - }, - "discount_total_formatted": { - "type": "string" - }, - "discount_total_usd": { - "type": "integer" - }, - "refunded": { - "type": "boolean" - }, - "refunded_amount": { - "type": "integer" - }, - "refunded_amount_formatted": { - "type": "string" - }, - "refunded_amount_usd": { - "type": "integer" - }, - "refunded_at": {}, - "status": { - "type": "string" - }, - "status_formatted": { - "type": "string" - }, - "subtotal": { - "type": "integer" - }, - "subtotal_formatted": { - "type": "string" - }, - "subtotal_usd": { - "type": "integer" - }, - "tax": { - "type": "integer" - }, - "tax_formatted": { - "type": "string" - }, - "tax_inclusive": { - "type": "boolean" - }, - "tax_usd": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_formatted": { - "type": "string" - }, - "total_usd": { - "type": "integer" - }, - "updated_at": { - "type": "string" - } + "data": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "message": { + "type": "string", + "example": "validation errors while handling request" + }, + "status": { + "type": "string", + "example": "error" } - }, - "id": { - "type": "string" - }, - "type": { - "type": "string" - } } - } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.UserResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.User" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" - } - } - }, - "responses.WebhookResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "$ref": "#/definitions/entities.Webhook" + "responses.UserSubscriptionPaymentsResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "attributes", + "id", + "type" + ], + "properties": { + "attributes": { + "type": "object", + "required": [ + "billing_reason", + "card_brand", + "card_last_four", + "created_at", + "currency", + "currency_rate", + "discount_total", + "discount_total_formatted", + "discount_total_usd", + "refunded", + "refunded_amount", + "refunded_amount_formatted", + "refunded_amount_usd", + "refunded_at", + "status", + "status_formatted", + "subtotal", + "subtotal_formatted", + "subtotal_usd", + "tax", + "tax_formatted", + "tax_inclusive", + "tax_usd", + "total", + "total_formatted", + "total_usd", + "updated_at" + ], + "properties": { + "billing_reason": { + "type": "string" + }, + "card_brand": { + "type": "string" + }, + "card_last_four": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "currency_rate": { + "type": "string" + }, + "discount_total": { + "type": "integer" + }, + "discount_total_formatted": { + "type": "string" + }, + "discount_total_usd": { + "type": "integer" + }, + "refunded": { + "type": "boolean" + }, + "refunded_amount": { + "type": "integer" + }, + "refunded_amount_formatted": { + "type": "string" + }, + "refunded_amount_usd": { + "type": "integer" + }, + "refunded_at": {}, + "status": { + "type": "string" + }, + "status_formatted": { + "type": "string" + }, + "subtotal": { + "type": "integer" + }, + "subtotal_formatted": { + "type": "string" + }, + "subtotal_usd": { + "type": "integer" + }, + "tax": { + "type": "integer" + }, + "tax_formatted": { + "type": "string" + }, + "tax_inclusive": { + "type": "boolean" + }, + "tax_usd": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_formatted": { + "type": "string" + }, + "total_usd": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "message": { - "type": "string", - "example": "Request handled successfully" + "responses.WebhookResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.Webhook" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } }, - "status": { - "type": "string", - "example": "success" + "responses.WebhooksResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entities.Webhook" + } + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } } - } }, - "responses.WebhooksResponse": { - "type": "object", - "required": ["data", "message", "status"], - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/entities.Webhook" - } - }, - "message": { - "type": "string", - "example": "Request handled successfully" - }, - "status": { - "type": "string", - "example": "success" + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-api-Key", + "in": "header" } - } - } - }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "x-api-Key", - "in": "header" } - } } diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index e2f17d92a..4c6d0938e 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -30,15 +30,51 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - end_timestamp - - id - - received_messages - - sent_messages - - start_timestamp - - total_cost - - updated_at - - user_id + - created_at + - end_timestamp + - id + - received_messages + - sent_messages + - start_timestamp + - total_cost + - updated_at + - user_id + type: object + entities.BulkMessage: + properties: + created_at: + example: "2022-06-05T14:26:02.302718+03:00" + type: string + delivered_count: + example: 25 + type: integer + failed_count: + example: 5 + type: integer + pending_count: + example: 30 + type: integer + request_id: + example: bulk-32343a19-da5e-4b1b-a767-3298a73703cb + type: string + scheduled_count: + example: 50 + type: integer + sent_count: + example: 40 + type: integer + total: + example: 150 + type: integer + required: + - created_at + - delivered_count + - failed_count + - pending_count + - request_id + - scheduled_count + - sent_count + - total type: object entities.Discord: properties: @@ -64,13 +100,13 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - id - - incoming_channel_id - - name - - server_id - - updated_at - - user_id + - created_at + - id + - incoming_channel_id + - name + - server_id + - updated_at + - user_id type: object entities.Heartbeat: properties: @@ -93,19 +129,19 @@ definitions: example: 344c10f type: string required: - - charging - - id - - owner - - timestamp - - user_id - - version + - charging + - id + - owner + - timestamp + - user_id + - version type: object entities.Message: properties: attachments: example: - - https://example.com/image.jpg - - https://example.com/video.mp4 + - https://example.com/image.jpg + - https://example.com/video.mp4 items: type: string type: array @@ -167,8 +203,7 @@ definitions: example: 0 type: integer send_time: - description: - SendDuration is the number of nanoseconds from when the request + description: SendDuration is the number of nanoseconds from when the request was received until when the mobile phone send the message example: 133414 type: integer @@ -177,7 +212,7 @@ definitions: type: string sim: allOf: - - $ref: "#/definitions/entities.SIM" + - $ref: '#/definitions/entities.SIM' description: |- SIM is the SIM card to use to send the message * SMS1: use the SIM card in slot 1 @@ -197,22 +232,22 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - attachments - - contact - - content - - created_at - - encrypted - - id - - max_send_attempts - - order_timestamp - - owner - - request_received_at - - send_attempt_count - - sim - - status - - type - - updated_at - - user_id + - attachments + - contact + - content + - created_at + - encrypted + - id + - max_send_attempts + - order_timestamp + - owner + - request_received_at + - send_attempt_count + - sim + - status + - type + - updated_at + - user_id type: object entities.MessageSendSchedule: properties: @@ -236,16 +271,16 @@ definitions: type: string windows: items: - $ref: "#/definitions/entities.MessageSendScheduleWindow" + $ref: '#/definitions/entities.MessageSendScheduleWindow' type: array required: - - created_at - - id - - name - - timezone - - updated_at - - user_id - - windows + - created_at + - id + - name + - timezone + - updated_at + - user_id + - windows type: object entities.MessageSendScheduleWindow: properties: @@ -259,9 +294,9 @@ definitions: example: 540 type: integer required: - - day_of_week - - end_minute - - start_minute + - day_of_week + - end_minute + - start_minute type: object entities.MessageThread: properties: @@ -302,18 +337,18 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - color - - contact - - created_at - - id - - is_archived - - last_message_content - - last_message_id - - order_timestamp - - owner - - status - - updated_at - - user_id + - color + - contact + - created_at + - id + - is_archived + - last_message_content + - last_message_id + - order_timestamp + - owner + - status + - updated_at + - user_id type: object entities.Phone: properties: @@ -327,14 +362,12 @@ definitions: example: 32343a19-da5e-4b1b-a767-3298a73703cb type: string max_send_attempts: - description: - MaxSendAttempts determines how many times to retry sending an + description: MaxSendAttempts determines how many times to retry sending an SMS message example: 2 type: integer message_expiration_seconds: - description: - MessageExpirationSeconds is the duration in seconds after sending + description: MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. type: integer message_send_schedule_id: @@ -350,7 +383,7 @@ definitions: example: "+18005550199" type: string sim: - $ref: "#/definitions/entities.SIM" + $ref: '#/definitions/entities.SIM' updated_at: example: "2022-06-05T14:26:10.303278+03:00" type: string @@ -358,15 +391,15 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - id - - max_send_attempts - - message_expiration_seconds - - messages_per_minute - - phone_number - - sim - - updated_at - - user_id + - created_at + - id + - max_send_attempts + - message_expiration_seconds + - messages_per_minute + - phone_number + - sim + - updated_at + - user_id type: object entities.PhoneAPIKey: properties: @@ -384,15 +417,15 @@ definitions: type: string phone_ids: example: - - 32343a19-da5e-4b1b-a767-3298a73703cb - - 32343a19-da5e-4b1b-a767-3298a73703cc + - 32343a19-da5e-4b1b-a767-3298a73703cb + - 32343a19-da5e-4b1b-a767-3298a73703cc items: type: string type: array phone_numbers: example: - - "+18005550199" - - "+18005550100" + - "+18005550199" + - "+18005550100" items: type: string type: array @@ -406,50 +439,50 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - api_key - - created_at - - id - - name - - phone_ids - - phone_numbers - - updated_at - - user_email - - user_id + - api_key + - created_at + - id + - name + - phone_ids + - phone_numbers + - updated_at + - user_email + - user_id type: object entities.SIM: enum: - - SIM1 - - SIM2 + - SIM1 + - SIM2 type: string x-enum-varnames: - - SIM1 - - SIM2 + - SIM1 + - SIM2 entities.SubscriptionName: enum: - - free - - pro-monthly - - pro-yearly - - ultra-monthly - - ultra-yearly - - pro-lifetime - - 20k-monthly - - 100k-monthly - - 50k-monthly - - 200k-monthly - - 20k-yearly + - free + - pro-monthly + - pro-yearly + - ultra-monthly + - ultra-yearly + - pro-lifetime + - 20k-monthly + - 100k-monthly + - 50k-monthly + - 200k-monthly + - 20k-yearly type: string x-enum-varnames: - - SubscriptionNameFree - - SubscriptionNameProMonthly - - SubscriptionNameProYearly - - SubscriptionNameUltraMonthly - - SubscriptionNameUltraYearly - - SubscriptionNameProLifetime - - SubscriptionName20KMonthly - - SubscriptionName100KMonthly - - SubscriptionName50KMonthly - - SubscriptionName200KMonthly - - SubscriptionName20KYearly + - SubscriptionNameFree + - SubscriptionNameProMonthly + - SubscriptionNameProYearly + - SubscriptionNameUltraMonthly + - SubscriptionNameUltraYearly + - SubscriptionNameProLifetime + - SubscriptionName20KMonthly + - SubscriptionName100KMonthly + - SubscriptionName50KMonthly + - SubscriptionName200KMonthly + - SubscriptionName20KYearly entities.User: properties: active_phone_id: @@ -487,7 +520,7 @@ definitions: type: string subscription_name: allOf: - - $ref: "#/definitions/entities.SubscriptionName" + - $ref: '#/definitions/entities.SubscriptionName' example: free subscription_renews_at: example: "2022-06-05T14:26:02.302718+03:00" @@ -502,18 +535,18 @@ definitions: example: "2022-06-05T14:26:10.303278+03:00" type: string required: - - api_key - - created_at - - email - - id - - notification_heartbeat_enabled - - notification_message_status_enabled - - notification_newsletter_enabled - - notification_webhook_enabled - - subscription_id - - subscription_name - - timezone - - updated_at + - api_key + - created_at + - email + - id + - notification_heartbeat_enabled + - notification_message_status_enabled + - notification_newsletter_enabled + - notification_webhook_enabled + - subscription_id + - subscription_name + - timezone + - updated_at type: object entities.Webhook: properties: @@ -522,7 +555,7 @@ definitions: type: string events: example: - - message.phone.received + - message.phone.received items: type: string type: array @@ -531,8 +564,8 @@ definitions: type: string phone_numbers: example: - - "+18005550199" - - "+18005550100" + - "+18005550199" + - "+18005550100" items: type: string type: array @@ -549,14 +582,14 @@ definitions: example: WB7DRDWrJZRGbYrv2CKGkqbzvqdC type: string required: - - created_at - - events - - id - - phone_numbers - - signing_key - - updated_at - - url - - user_id + - created_at + - events + - id + - phone_numbers + - signing_key + - updated_at + - url + - user_id type: object requests.DiscordStore: properties: @@ -567,9 +600,9 @@ definitions: server_id: type: string required: - - incoming_channel_id - - name - - server_id + - incoming_channel_id + - name + - server_id type: object requests.DiscordUpdate: properties: @@ -580,9 +613,9 @@ definitions: server_id: type: string required: - - incoming_channel_id - - name - - server_id + - incoming_channel_id + - name + - server_id type: object requests.HeartbeatStore: properties: @@ -593,8 +626,8 @@ definitions: type: string type: array required: - - charging - - phone_numbers + - charging + - phone_numbers type: object requests.MessageAttachment: properties: @@ -611,15 +644,14 @@ definitions: example: photo.jpg type: string required: - - content - - content_type - - name + - content + - content_type + - name type: object requests.MessageBulkSend: properties: attachments: - description: - Attachments are optional. When you provide a list of attachments, + description: Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS items: type: string @@ -628,8 +660,7 @@ definitions: example: This is a sample text message type: string encrypted: - description: - Encrypted is used to determine if the content is end-to-end encrypted. + description: Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false type: boolean @@ -637,22 +668,21 @@ definitions: example: "+18005550199" type: string request_id: - description: - RequestID is an optional parameter used to track a request from + description: RequestID is an optional parameter used to track a request from the client's perspective example: 153554b5-ae44-44a0-8f4f-7bbac5657ad4 type: string to: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array required: - - content - - from - - to + - content + - from + - to type: object requests.MessageCallMissed: properties: @@ -669,10 +699,10 @@ definitions: example: "+18005550100" type: string required: - - from - - sim - - timestamp - - to + - from + - sim + - timestamp + - to type: object requests.MessageEvent: properties: @@ -688,31 +718,28 @@ definitions: description: Reason is the exact error message in case the event is an error type: string timestamp: - description: - Timestamp is the time when the event was emitted, Please send + description: Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible example: "2022-06-05T14:26:09.527976+03:00" type: string required: - - event_name - - reason - - timestamp + - event_name + - reason + - timestamp type: object requests.MessageReceive: properties: attachments: - description: - Attachments is the list of MMS attachments received with the + description: Attachments is the list of MMS attachments received with the message items: - $ref: "#/definitions/requests.MessageAttachment" + $ref: '#/definitions/requests.MessageAttachment' type: array content: example: This is a sample text message received on a phone type: string encrypted: - description: - Encrypted is used to determine if the content is end-to-end encrypted. + description: Encrypted is used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false type: boolean @@ -721,12 +748,11 @@ definitions: type: string sim: allOf: - - $ref: "#/definitions/entities.SIM" + - $ref: '#/definitions/entities.SIM' description: SIM card that received the message example: SIM1 timestamp: - description: - Timestamp is the time when the event was emitted, Please send + description: Timestamp is the time when the event was emitted, Please send the timestamp in UTC with as much precision as possible example: "2022-06-05T14:26:09.527976+03:00" type: string @@ -734,22 +760,21 @@ definitions: example: "+18005550100" type: string required: - - content - - encrypted - - from - - sim - - timestamp - - to + - content + - encrypted + - from + - sim + - timestamp + - to type: object requests.MessageSend: properties: attachments: - description: - Attachments are optional. When you provide a list of attachments, + description: Attachments are optional. When you provide a list of attachments, the message will be sent out as an MMS example: - - https://example.com/image.jpg - - https://example.com/video.mp4 + - https://example.com/image.jpg + - https://example.com/video.mp4 items: type: string type: array @@ -757,8 +782,7 @@ definitions: example: This is a sample text message type: string encrypted: - description: - Encrypted is an optional parameter used to determine if the content + description: Encrypted is an optional parameter used to determine if the content is end-to-end encrypted. Make sure to set the encryption key on the httpSMS mobile app example: false @@ -767,14 +791,12 @@ definitions: example: "+18005550199" type: string request_id: - description: - RequestID is an optional parameter used to track a request from + description: RequestID is an optional parameter used to track a request from the client's perspective example: 153554b5-ae44-44a0-8f4f-7bbac5657ad4 type: string send_at: - description: - SendAt is an optional parameter used to schedule a message to + description: SendAt is an optional parameter used to schedule a message to be sent in the future. The time is considered to be in your profile's local timezone and you can queue messages for up to 20 days (480 hours) in the future. @@ -784,9 +806,9 @@ definitions: example: "+18005550100" type: string required: - - content - - from - - to + - content + - from + - to type: object requests.MessageSendScheduleStore: properties: @@ -796,12 +818,12 @@ definitions: type: string windows: items: - $ref: "#/definitions/requests.MessageSendScheduleWindow" + $ref: '#/definitions/requests.MessageSendScheduleWindow' type: array required: - - name - - timezone - - windows + - name + - timezone + - windows type: object requests.MessageSendScheduleWindow: properties: @@ -812,9 +834,9 @@ definitions: start_minute: type: integer required: - - day_of_week - - end_minute - - start_minute + - day_of_week + - end_minute + - start_minute type: object requests.MessageThreadUpdate: properties: @@ -822,7 +844,7 @@ definitions: example: true type: boolean required: - - is_archived + - is_archived type: object requests.PhoneAPIKeyStoreRequest: properties: @@ -830,7 +852,7 @@ definitions: example: My Phone API Key type: string required: - - name + - name type: object requests.PhoneFCMToken: properties: @@ -838,18 +860,17 @@ definitions: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd..... type: string phone_number: - example: "[+18005550199]" + example: '[+18005550199]' type: string sim: - description: - SIM is the SIM slot of the phone in case the phone has more than + description: SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot example: SIM1 type: string required: - - fcm_token - - phone_number - - sim + - fcm_token + - phone_number + - sim type: object requests.PhoneUpsert: properties: @@ -857,14 +878,12 @@ definitions: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd..... type: string max_send_attempts: - description: - MaxSendAttempts is the number of attempts when sending an SMS + description: MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline. example: 2 type: integer message_expiration_seconds: - description: - MessageExpirationSeconds is the duration in seconds after sending + description: MessageExpirationSeconds is the duration in seconds after sending a message when it is considered to be expired. example: 12345 type: integer @@ -881,19 +900,18 @@ definitions: example: "+18005550199" type: string sim: - description: - SIM is the SIM slot of the phone in case the phone has more than + description: SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot example: SIM1 type: string required: - - fcm_token - - max_send_attempts - - message_expiration_seconds - - messages_per_minute - - missed_call_auto_reply - - phone_number - - sim + - fcm_token + - max_send_attempts + - message_expiration_seconds + - messages_per_minute + - missed_call_auto_reply + - phone_number + - sim type: object requests.UserNotificationUpdate: properties: @@ -910,10 +928,10 @@ definitions: example: true type: boolean required: - - heartbeat_enabled - - message_status_enabled - - newsletter_enabled - - webhook_enabled + - heartbeat_enabled + - message_status_enabled + - newsletter_enabled + - webhook_enabled type: object requests.UserPaymentInvoice: properties: @@ -939,13 +957,13 @@ definitions: example: "9800" type: string required: - - address - - city - - country - - name - - notes - - state - - zip_code + - address + - city + - country + - name + - notes + - state + - zip_code type: object requests.UserUpdate: properties: @@ -956,8 +974,8 @@ definitions: example: Europe/Helsinki type: string required: - - active_phone_id - - timezone + - active_phone_id + - timezone type: object requests.WebhookStore: properties: @@ -967,8 +985,8 @@ definitions: type: array phone_numbers: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array @@ -977,10 +995,10 @@ definitions: url: type: string required: - - events - - phone_numbers - - signing_key - - url + - events + - phone_numbers + - signing_key + - url type: object requests.WebhookUpdate: properties: @@ -990,8 +1008,8 @@ definitions: type: array phone_numbers: example: - - "+18005550100" - - "+18005550100" + - "+18005550100" + - "+18005550100" items: type: string type: array @@ -1000,10 +1018,10 @@ definitions: url: type: string required: - - events - - phone_numbers - - signing_key - - url + - events + - phone_numbers + - signing_key + - url type: object responses.BadRequest: properties: @@ -1017,14 +1035,14 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.BillingUsageResponse: properties: data: - $ref: "#/definitions/entities.BillingUsage" + $ref: '#/definitions/entities.BillingUsage' message: example: Request handled successfully type: string @@ -1032,15 +1050,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.BillingUsagesResponse: properties: data: items: - $ref: "#/definitions/entities.BillingUsage" + $ref: '#/definitions/entities.BillingUsage' type: array message: example: Request handled successfully @@ -1049,14 +1067,31 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status + type: object + responses.BulkMessagesResponse: + properties: + data: + items: + $ref: '#/definitions/entities.BulkMessage' + type: array + message: + example: Request handled successfully + type: string + status: + example: success + type: string + required: + - data + - message + - status type: object responses.DiscordResponse: properties: data: - $ref: "#/definitions/entities.Discord" + $ref: '#/definitions/entities.Discord' message: example: Request handled successfully type: string @@ -1064,15 +1099,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.DiscordsResponse: properties: data: items: - $ref: "#/definitions/entities.Discord" + $ref: '#/definitions/entities.Discord' type: array message: example: Request handled successfully @@ -1081,14 +1116,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.HeartbeatResponse: properties: data: - $ref: "#/definitions/entities.Heartbeat" + $ref: '#/definitions/entities.Heartbeat' message: example: Request handled successfully type: string @@ -1096,15 +1131,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.HeartbeatsResponse: properties: data: items: - $ref: "#/definitions/entities.Heartbeat" + $ref: '#/definitions/entities.Heartbeat' type: array message: example: Request handled successfully @@ -1113,9 +1148,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.InternalServerError: properties: @@ -1126,13 +1161,13 @@ definitions: example: error type: string required: - - message - - status + - message + - status type: object responses.MessageResponse: properties: data: - $ref: "#/definitions/entities.Message" + $ref: '#/definitions/entities.Message' message: example: Request handled successfully type: string @@ -1140,14 +1175,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.MessageSendScheduleResponse: properties: data: - $ref: "#/definitions/entities.MessageSendSchedule" + $ref: '#/definitions/entities.MessageSendSchedule' message: example: Request handled successfully type: string @@ -1155,15 +1190,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.MessageSendSchedulesResponse: properties: data: items: - $ref: "#/definitions/entities.MessageSendSchedule" + $ref: '#/definitions/entities.MessageSendSchedule' type: array message: example: Request handled successfully @@ -1172,15 +1207,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.MessageThreadsResponse: properties: data: items: - $ref: "#/definitions/entities.MessageThread" + $ref: '#/definitions/entities.MessageThread' type: array message: example: Request handled successfully @@ -1189,15 +1224,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.MessagesResponse: properties: data: items: - $ref: "#/definitions/entities.Message" + $ref: '#/definitions/entities.Message' type: array message: example: Request handled successfully @@ -1206,9 +1241,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.NoContent: properties: @@ -1219,8 +1254,8 @@ definitions: example: success type: string required: - - message - - status + - message + - status type: object responses.NotFound: properties: @@ -1231,8 +1266,8 @@ definitions: example: error type: string required: - - message - - status + - message + - status type: object responses.OkString: properties: @@ -1245,28 +1280,27 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PaymentRequired: properties: message: - example: - You have reached the maximum number of allowed resources. Please + example: You have reached the maximum number of allowed resources. Please upgrade your plan. type: string status: example: error type: string required: - - message - - status + - message + - status type: object responses.PhoneAPIKeyResponse: properties: data: - $ref: "#/definitions/entities.PhoneAPIKey" + $ref: '#/definitions/entities.PhoneAPIKey' message: example: Request handled successfully type: string @@ -1274,15 +1308,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhoneAPIKeysResponse: properties: data: items: - $ref: "#/definitions/entities.PhoneAPIKey" + $ref: '#/definitions/entities.PhoneAPIKey' type: array message: example: Request handled successfully @@ -1291,14 +1325,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhoneResponse: properties: data: - $ref: "#/definitions/entities.Phone" + $ref: '#/definitions/entities.Phone' message: example: Request handled successfully type: string @@ -1306,15 +1340,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.PhonesResponse: properties: data: items: - $ref: "#/definitions/entities.Phone" + $ref: '#/definitions/entities.Phone' type: array message: example: Request handled successfully @@ -1323,9 +1357,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.Unauthorized: properties: @@ -1339,9 +1373,9 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UnprocessableEntity: properties: @@ -1358,14 +1392,14 @@ definitions: example: error type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UserResponse: properties: data: - $ref: "#/definitions/entities.User" + $ref: '#/definitions/entities.User' message: example: Request handled successfully type: string @@ -1373,9 +1407,9 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.UserSubscriptionPaymentsResponse: properties: @@ -1438,42 +1472,42 @@ definitions: updated_at: type: string required: - - billing_reason - - card_brand - - card_last_four - - created_at - - currency - - currency_rate - - discount_total - - discount_total_formatted - - discount_total_usd - - refunded - - refunded_amount - - refunded_amount_formatted - - refunded_amount_usd - - refunded_at - - status - - status_formatted - - subtotal - - subtotal_formatted - - subtotal_usd - - tax - - tax_formatted - - tax_inclusive - - tax_usd - - total - - total_formatted - - total_usd - - updated_at + - billing_reason + - card_brand + - card_last_four + - created_at + - currency + - currency_rate + - discount_total + - discount_total_formatted + - discount_total_usd + - refunded + - refunded_amount + - refunded_amount_formatted + - refunded_amount_usd + - refunded_at + - status + - status_formatted + - subtotal + - subtotal_formatted + - subtotal_usd + - tax + - tax_formatted + - tax_inclusive + - tax_usd + - total + - total_formatted + - total_usd + - updated_at type: object id: type: string type: type: string required: - - attributes - - id - - type + - attributes + - id + - type type: object type: array message: @@ -1483,14 +1517,14 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.WebhookResponse: properties: data: - $ref: "#/definitions/entities.Webhook" + $ref: '#/definitions/entities.Webhook' message: example: Request handled successfully type: string @@ -1498,15 +1532,15 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object responses.WebhooksResponse: properties: data: items: - $ref: "#/definitions/entities.Webhook" + $ref: '#/definitions/entities.Webhook' type: array message: example: Request handled successfully @@ -1515,17 +1549,16 @@ definitions: example: success type: string required: - - data - - message - - status + - data + - message + - status type: object host: api.httpsms.com info: contact: email: support@httpsms.com name: support@httpsms.com - description: - Use your Android phone to send and receive SMS messages via a simple + description: Use your Android phone to send and receive SMS messages via a simple programmable API with end-to-end encryption. license: name: AGPL-3.0 @@ -1536,1857 +1569,1865 @@ paths: /billing/usage: get: consumes: - - application/json - description: - Get the summary of sent and received messages for a user in the + - application/json + description: Get the summary of sent and received messages for a user in the current month produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.BillingUsageResponse" + $ref: '#/definitions/responses.BillingUsageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get Billing Usage. tags: - - Billing + - Billing /billing/usage-history: get: consumes: - - application/json - description: - Get billing usage records of sent and received messages for a user + - application/json + description: Get billing usage records of sent and received messages for a user in the past. It will be sorted by timestamp in descending order. parameters: - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: number of heartbeats to return - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: number of heartbeats to return + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.BillingUsagesResponse" + $ref: '#/definitions/responses.BillingUsagesResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get billing usage history. tags: - - Billing + - Billing /bulk-messages: + get: + consumes: + - application/json + description: Fetches the last 10 bulk message order summaries for the authenticated + user showing counts per status. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.BulkMessagesResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/responses.Unauthorized' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/responses.InternalServerError' + security: + - ApiKeyAuth: [] + summary: List bulk message orders + tags: + - BulkSMS post: consumes: - - multipart/form-data - description: - Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) + - multipart/form-data + description: Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx). parameters: - - description: The Excel or CSV file containing the messages to be sent. - in: formData - name: document - required: true - type: file + - description: The Excel or CSV file containing the messages to be sent. + in: formData + name: document + required: true + type: file produces: - - application/json + - application/json responses: "202": description: Accepted schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store bulk SMS file tags: - - BulkSMS + - BulkSMS /discord-integrations: get: consumes: - - application/json + - application/json description: Get the discord integrations of a user parameters: - - description: number of discord integrations to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter discord integrations containing query - in: query - name: query - type: string - - description: number of discord integrations to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of discord integrations to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter discord integrations containing query + in: query + name: query + type: string + - description: number of discord integrations to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.DiscordsResponse" + $ref: '#/definitions/responses.DiscordsResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get discord integrations of a user tags: - - DiscordIntegration + - DiscordIntegration post: consumes: - - application/json + - application/json description: Store a discord integration for the authenticated user parameters: - - description: Payload of the discord integration request - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.DiscordStore" + - description: Payload of the discord integration request + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.DiscordStore' produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: "#/definitions/responses.DiscordResponse" + $ref: '#/definitions/responses.DiscordResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store discord integration tags: - - DiscordIntegration + - DiscordIntegration /discord-integrations/{discordID}: delete: consumes: - - application/json + - application/json description: Delete a discord integration for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the discord integration - in: path - name: discordID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the discord integration + in: path + name: discordID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete discord integration tags: - - Webhooks + - Webhooks put: consumes: - - application/json + - application/json description: Update a discord integration for the currently authenticated user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the discord integration - in: path - name: discordID - required: true - type: string - - description: Payload of discord integration to update - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.DiscordUpdate" + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the discord integration + in: path + name: discordID + required: true + type: string + - description: Payload of discord integration to update + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.DiscordUpdate' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.DiscordResponse" + $ref: '#/definitions/responses.DiscordResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a discord integration tags: - - DiscordIntegration + - DiscordIntegration /discord/event: post: consumes: - - application/json + - application/json description: Publish a discord event to the registered listeners produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' summary: Consume a discord event tags: - - Discord + - Discord /heartbeats: get: consumes: - - application/json - description: - Get the last time a phone number requested for outstanding messages. + - application/json + description: Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: the owner's phone number - in: query - name: owner - required: true - type: string - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter containing query - in: query - name: query - type: string - - description: number of heartbeats to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: the owner's phone number + in: query + name: owner + required: true + type: string + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter containing query + in: query + name: query + type: string + - description: number of heartbeats to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.HeartbeatsResponse" + $ref: '#/definitions/responses.HeartbeatsResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get heartbeats of an owner phone number tags: - - Heartbeats + - Heartbeats post: consumes: - - application/json - description: - Store the heartbeat to make notify that a phone number is still + - application/json + description: Store the heartbeat to make notify that a phone number is still active parameters: - - description: Payload of the heartbeat request - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.HeartbeatStore" + - description: Payload of the heartbeat request + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.HeartbeatStore' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.HeartbeatResponse" + $ref: '#/definitions/responses.HeartbeatResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Register heartbeat of an owner phone number tags: - - Heartbeats + - Heartbeats /integration/3cx/messages: post: consumes: - - application/json + - application/json description: Sends an SMS message from the 3CX platform produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' summary: Sends a 3CX SMS message tags: - - 3CXIntegration + - 3CXIntegration /message-threads: get: consumes: - - application/json - description: - Get list of contacts which a phone number has communicated with + - application/json + description: Get list of contacts which a phone number has communicated with (threads). It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: owner phone number - in: query - name: owner - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter message threads containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: owner phone number + in: query + name: owner + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter message threads containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageThreadsResponse" + $ref: '#/definitions/responses.MessageThreadsResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get message threads for a phone number tags: - - MessageThreads + - MessageThreads /message-threads/{messageThreadID}: delete: consumes: - - application/json - description: - Delete a message thread from the database and also deletes all + - application/json + description: Delete a message thread from the database and also deletes all the messages in the thread. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message thread - in: path - name: messageThreadID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message thread + in: path + name: messageThreadID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a message thread from the database. tags: - - MessageThreads + - MessageThreads put: consumes: - - application/json + - application/json description: Updates the details of a message thread parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message thread - in: path - name: messageThreadID - required: true - type: string - - description: Payload of message thread details to update - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageThreadUpdate" + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message thread + in: path + name: messageThreadID + required: true + type: string + - description: Payload of message thread details to update + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageThreadUpdate' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneResponse" + $ref: '#/definitions/responses.PhoneResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a message thread tags: - - MessageThreads + - MessageThreads /messages: get: consumes: - - application/json - description: - Get list of messages which are sent between 2 phone numbers. It + - application/json + description: Get list of messages which are sent between 2 phone numbers. It will be sorted by timestamp in descending order. parameters: - - default: "+18005550199" - description: the owner's phone number - in: query - name: owner - required: true - type: string - - default: "+18005550100" - description: the contact's phone number - in: query - name: contact - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter messages containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - default: "+18005550199" + description: the owner's phone number + in: query + name: owner + required: true + type: string + - default: "+18005550100" + description: the contact's phone number + in: query + name: contact + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter messages containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessagesResponse" + $ref: '#/definitions/responses.MessagesResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get messages which are sent between 2 phone numbers tags: - - Messages + - Messages /messages/{messageID}: delete: consumes: - - application/json - description: - Delete a message from the database and removes the message content + - application/json + description: Delete a message from the database and removes the message content from the list of threads. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a message from the database. tags: - - Messages + - Messages get: consumes: - - application/json + - application/json description: Get a message from the database by the message ID. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get a message from the database. tags: - - Messages + - Messages /messages/{messageID}/events: post: consumes: - - application/json - description: - Use this endpoint to send events for a message when it is failed, + - application/json + description: Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the message - in: path - name: messageID - required: true - type: string - - description: Payload of the event emitted. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageEvent" + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the message + in: path + name: messageID + required: true + type: string + - description: Payload of the event emitted. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageEvent' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upsert an event for a message on the mobile phone tags: - - Messages + - Messages /messages/bulk-send: post: consumes: - - application/json + - application/json description: Add bulk SMS messages to be sent by the android phone parameters: - - description: Bulk send message request payload - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageBulkSend" + - description: Bulk send message request payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageBulkSend' produces: - - application/json + - application/json responses: "200": description: OK schema: items: - $ref: "#/definitions/responses.MessagesResponse" + $ref: '#/definitions/responses.MessagesResponse' type: array "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Send bulk SMS messages tags: - - Messages + - Messages /messages/calls/missed: post: consumes: - - application/json - description: - This endpoint is called by the httpSMS android app to register + - application/json + description: This endpoint is called by the httpSMS android app to register a missed call event on the mobile phone. parameters: - - description: Payload of the missed call event. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageCallMissed" + - description: Payload of the missed call event. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageCallMissed' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Register a missed call event on the mobile phone tags: - - Messages + - Messages /messages/outstanding: get: consumes: - - application/json + - application/json description: Get an outstanding message to be sent by an android phone parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703cb - description: The ID of the message - in: query - name: message_id - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703cb + description: The ID of the message + in: query + name: message_id + required: true + type: string produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get an outstanding message tags: - - Messages + - Messages /messages/receive: post: consumes: - - application/json + - application/json description: Add a new message received from a mobile phone parameters: - - description: Received message request payload - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageReceive" + - description: Received message request payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageReceive' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Receive a new SMS message from a mobile phone tags: - - Messages + - Messages /messages/search: get: consumes: - - application/json - description: - This returns the list of all messages based on the filter criteria + - application/json + description: This returns the list of all messages based on the filter criteria including missed calls parameters: - - description: Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/ - in: header - name: token - required: true - type: string - - default: +18005550199,+18005550100 - description: the owner's phone numbers - in: query - name: owners - required: true - type: string - - description: number of messages to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter messages containing query - in: query - name: query - type: string - - description: number of messages to return - in: query - maximum: 200 - minimum: 1 - name: limit - type: integer + - description: Cloudflare turnstile token https://www.cloudflare.com/en-gb/application-services/products/turnstile/ + in: header + name: token + required: true + type: string + - default: +18005550199,+18005550100 + description: the owner's phone numbers + in: query + name: owners + required: true + type: string + - description: number of messages to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter messages containing query + in: query + name: query + type: string + - description: number of messages to return + in: query + maximum: 200 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessagesResponse" + $ref: '#/definitions/responses.MessagesResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Search all messages of a user tags: - - Messages + - Messages /messages/send: post: consumes: - - application/json + - application/json description: Add a new SMS message to be sent by your Android phone parameters: - - description: Send message request payload - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageSend" + - description: Send message request payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageSend' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageResponse" + $ref: '#/definitions/responses.MessageResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Send an SMS message tags: - - Messages + - Messages /phone-api-keys: get: consumes: - - application/json - description: - Get list phone API keys which a user has registered on the httpSMS + - application/json + description: Get list phone API keys which a user has registered on the httpSMS application parameters: - - description: number of phone api keys to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter phone api keys with name containing query - in: query - name: query - type: string - - description: number of phone api keys to return - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer + - description: number of phone api keys to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter phone api keys with name containing query + in: query + name: query + type: string + - description: number of phone api keys to return + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneAPIKeysResponse" + $ref: '#/definitions/responses.PhoneAPIKeysResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get the phone API keys of a user tags: - - PhoneAPIKeys + - PhoneAPIKeys post: consumes: - - application/json - description: - Creates a new phone API key which can be used to log in to the + - application/json + description: Creates a new phone API key which can be used to log in to the httpSMS app on your Android phone parameters: - - description: Payload of new phone API key. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.PhoneAPIKeyStoreRequest" + - description: Payload of new phone API key. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.PhoneAPIKeyStoreRequest' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneAPIKeyResponse" + $ref: '#/definitions/responses.PhoneAPIKeyResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' + "402": + description: Payment Required + schema: + $ref: '#/definitions/responses.PaymentRequired' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store phone API key tags: - - PhoneAPIKeys + - PhoneAPIKeys /phone-api-keys/{phoneAPIKeyID}: delete: consumes: - - application/json - description: - Delete a phone API Key from the database and cannot be used for + - application/json + description: Delete a phone API Key from the database and cannot be used for authentication anymore. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone API key - in: path - name: phoneAPIKeyID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone API key + in: path + name: phoneAPIKeyID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a phone API key from the database. tags: - - PhoneAPIKeys + - PhoneAPIKeys /phone-api-keys/{phoneAPIKeyID}/phones/{phoneID}: delete: consumes: - - application/json - description: - You will need to login again to the httpSMS app on your Android + - application/json + description: You will need to login again to the httpSMS app on your Android phone with a new phone API key. parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone API key - in: path - name: phoneAPIKeyID - required: true - type: string - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone - in: path - name: phoneID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone API key + in: path + name: phoneAPIKeyID + required: true + type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone + in: path + name: phoneID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Remove the association of a phone from the phone API key. tags: - - PhoneAPIKeys + - PhoneAPIKeys /phones: get: consumes: - - application/json - description: - Get list of phones which a user has registered on the http sms + - application/json + description: Get list of phones which a user has registered on the http sms application parameters: - - description: number of heartbeats to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter phones containing query - in: query - name: query - type: string - - description: number of phones to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of heartbeats to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter phones containing query + in: query + name: query + type: string + - description: number of phones to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhonesResponse" + $ref: '#/definitions/responses.PhonesResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get phones of a user tags: - - Phones + - Phones put: consumes: - - application/json - description: - Updates properties of a user's phone. If the phone with this number + - application/json + description: Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert' parameters: - - description: Payload of new phone number. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.PhoneUpsert" + - description: Payload of new phone number. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.PhoneUpsert' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneResponse" + $ref: '#/definitions/responses.PhoneResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upsert Phone tags: - - Phones + - Phones /phones/{phoneID}: delete: consumes: - - application/json + - application/json description: Delete a phone that has been sored in the database parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the phone - in: path - name: phoneID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the phone + in: path + name: phoneID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete Phone tags: - - Phones + - Phones /phones/fcm-token: put: consumes: - - application/json - description: - Updates the FCM token of a phone. If the phone with this number + - application/json + description: Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert' parameters: - - description: Payload of new FCM token. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.PhoneFCMToken" + - description: Payload of new FCM token. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.PhoneFCMToken' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneResponse" + $ref: '#/definitions/responses.PhoneResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Upserts the FCM token of a phone tags: - - Phones + - Phones /send-schedules: get: description: List all send schedules owned by the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageSendSchedulesResponse" + $ref: '#/definitions/responses.MessageSendSchedulesResponse' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: List send schedules tags: - - SendSchedules + - SendSchedules post: consumes: - - application/json + - application/json description: Create a new send schedule for the authenticated user. parameters: - - description: Payload of new send schedule. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageSendScheduleStore" + - description: Payload of new send schedule. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageSendScheduleStore' produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: "#/definitions/responses.MessageSendScheduleResponse" + $ref: '#/definitions/responses.MessageSendScheduleResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "402": description: Payment Required schema: - $ref: "#/definitions/responses.PaymentRequired" + $ref: '#/definitions/responses.PaymentRequired' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Create send schedule tags: - - SendSchedules + - SendSchedules /send-schedules/{scheduleID}: delete: description: Delete a send schedule owned by the authenticated user. parameters: - - description: Schedule ID - in: path - name: scheduleID - required: true - type: string + - description: Schedule ID + in: path + name: scheduleID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete send schedule tags: - - SendSchedules + - SendSchedules put: consumes: - - application/json + - application/json description: Update a send schedule owned by the authenticated user. parameters: - - description: Schedule ID - in: path - name: scheduleID - required: true - type: string - - description: Payload of updated send schedule. - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.MessageSendScheduleStore" + - description: Schedule ID + in: path + name: scheduleID + required: true + type: string + - description: Payload of updated send schedule. + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.MessageSendScheduleStore' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.MessageSendScheduleResponse" + $ref: '#/definitions/responses.MessageSendScheduleResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update send schedule tags: - - SendSchedules + - SendSchedules /users/{userID}/api-keys: delete: consumes: - - application/json + - application/json description: Rotate the user's API key in case the current API Key is compromised parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the user to update - in: path - name: userID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the user to update + in: path + name: userID + required: true + type: string produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.UserResponse" + $ref: '#/definitions/responses.UserResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Rotate the user's API Key tags: - - Users + - Users /users/{userID}/notifications: put: consumes: - - application/json + - application/json description: Update the email notification settings for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the user to update - in: path - name: userID - required: true - type: string - - description: User notification details to update - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.UserNotificationUpdate" + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the user to update + in: path + name: userID + required: true + type: string + - description: User notification details to update + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.UserNotificationUpdate' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.UserResponse" + $ref: '#/definitions/responses.UserResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update notification settings tags: - - Users + - Users /users/me: delete: consumes: - - application/json - description: - Deletes the currently authenticated user together with all their + - application/json + description: Deletes the currently authenticated user together with all their data. produces: - - application/json + - application/json responses: "201": description: Created schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete a user tags: - - Users + - Users get: consumes: - - application/json + - application/json description: Get details of the currently authenticated user produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.UserResponse" + $ref: '#/definitions/responses.UserResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get current user tags: - - Users + - Users put: consumes: - - application/json + - application/json description: Updates the details of the currently authenticated user parameters: - - description: Payload of user details to update - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.UserUpdate" + - description: Payload of user details to update + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.UserUpdate' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.PhoneResponse" + $ref: '#/definitions/responses.PhoneResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a user tags: - - Users + - Users /users/subscription: delete: description: Cancel the subscription of the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Cancel the user's subscription tags: - - Users + - Users /users/subscription-update-url: get: description: Fetches the subscription URL of the authenticated user. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.OkString" + $ref: '#/definitions/responses.OkString' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Currently authenticated user subscription update URL tags: - - Users + - Users /users/subscription/invoices/{subscriptionInvoiceID}: post: consumes: - - application/json - description: - Generates a new invoice PDF file for the given subscription payment + - application/json + description: Generates a new invoice PDF file for the given subscription payment with given parameters. parameters: - - description: Generate subscription payment invoice parameters - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.UserPaymentInvoice" - - description: ID of the subscription invoice to generate the PDF for - in: path - name: subscriptionInvoiceID - required: true - type: string + - description: Generate subscription payment invoice parameters + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.UserPaymentInvoice' + - description: ID of the subscription invoice to generate the PDF for + in: path + name: subscriptionInvoiceID + required: true + type: string produces: - - application/pdf + - application/pdf responses: "200": description: OK @@ -3395,86 +3436,85 @@ paths: "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Generate a subscription payment invoice tags: - - Users + - Users /users/subscription/payments: get: consumes: - - application/json - description: - Subscription payments are generated throughout the lifecycle of + - application/json + description: Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal. produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.UserSubscriptionPaymentsResponse" + $ref: '#/definitions/responses.UserSubscriptionPaymentsResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get the last 10 subscription payments. tags: - - Users + - Users /v1/attachments/{userID}/{messageID}/{attachmentIndex}/{filename}: get: description: Download an MMS attachment by its path components parameters: - - description: User ID - in: path - name: userID - required: true - type: string - - description: Message ID - in: path - name: messageID - required: true - type: string - - description: Attachment index - in: path - name: attachmentIndex - required: true - type: string - - description: Filename with extension - in: path - name: filename - required: true - type: string + - description: User ID + in: path + name: userID + required: true + type: string + - description: Message ID + in: path + name: messageID + required: true + type: string + - description: Attachment index + in: path + name: attachmentIndex + required: true + type: string + - description: Filename with extension + in: path + name: filename + required: true + type: string produces: - - application/octet-stream + - application/octet-stream responses: "200": description: OK @@ -3483,189 +3523,189 @@ paths: "404": description: Not Found schema: - $ref: "#/definitions/responses.NotFound" + $ref: '#/definitions/responses.NotFound' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' summary: Download a message attachment tags: - - Attachments + - Attachments /webhooks: get: consumes: - - application/json + - application/json description: Get the webhooks of a user parameters: - - description: number of webhooks to skip - in: query - minimum: 0 - name: skip - type: integer - - description: filter webhooks containing query - in: query - name: query - type: string - - description: number of webhooks to return - in: query - maximum: 20 - minimum: 1 - name: limit - type: integer + - description: number of webhooks to skip + in: query + minimum: 0 + name: skip + type: integer + - description: filter webhooks containing query + in: query + name: query + type: string + - description: number of webhooks to return + in: query + maximum: 20 + minimum: 1 + name: limit + type: integer produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.WebhooksResponse" + $ref: '#/definitions/responses.WebhooksResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Get webhooks of a user tags: - - Webhooks + - Webhooks post: consumes: - - application/json + - application/json description: Store a webhook for the authenticated user parameters: - - description: Payload of the webhook request - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.WebhookStore" + - description: Payload of the webhook request + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.WebhookStore' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.WebhookResponse" + $ref: '#/definitions/responses.WebhookResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Store a webhook tags: - - Webhooks + - Webhooks /webhooks/{webhookID}: delete: consumes: - - application/json + - application/json description: Delete a webhook for a user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the webhook - in: path - name: webhookID - required: true - type: string + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the webhook + in: path + name: webhookID + required: true + type: string produces: - - application/json + - application/json responses: "204": description: No Content schema: - $ref: "#/definitions/responses.NoContent" + $ref: '#/definitions/responses.NoContent' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Delete webhook tags: - - Webhooks + - Webhooks put: consumes: - - application/json + - application/json description: Update a webhook for the currently authenticated user parameters: - - default: 32343a19-da5e-4b1b-a767-3298a73703ca - description: ID of the webhook - in: path - name: webhookID - required: true - type: string - - description: Payload of webhook details to update - in: body - name: payload - required: true - schema: - $ref: "#/definitions/requests.WebhookUpdate" + - default: 32343a19-da5e-4b1b-a767-3298a73703ca + description: ID of the webhook + in: path + name: webhookID + required: true + type: string + - description: Payload of webhook details to update + in: body + name: payload + required: true + schema: + $ref: '#/definitions/requests.WebhookUpdate' produces: - - application/json + - application/json responses: "200": description: OK schema: - $ref: "#/definitions/responses.WebhookResponse" + $ref: '#/definitions/responses.WebhookResponse' "400": description: Bad Request schema: - $ref: "#/definitions/responses.BadRequest" + $ref: '#/definitions/responses.BadRequest' "401": description: Unauthorized schema: - $ref: "#/definitions/responses.Unauthorized" + $ref: '#/definitions/responses.Unauthorized' "422": description: Unprocessable Entity schema: - $ref: "#/definitions/responses.UnprocessableEntity" + $ref: '#/definitions/responses.UnprocessableEntity' "500": description: Internal Server Error schema: - $ref: "#/definitions/responses.InternalServerError" + $ref: '#/definitions/responses.InternalServerError' security: - - ApiKeyAuth: [] + - ApiKeyAuth: [] summary: Update a webhook tags: - - Webhooks + - Webhooks schemes: - - https +- https securityDefinitions: ApiKeyAuth: in: header diff --git a/api/go.mod b/api/go.mod index 1fe7dca1f..754baa145 100644 --- a/api/go.mod +++ b/api/go.mod @@ -33,6 +33,7 @@ require ( github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jszwec/csvutil v1.10.0 github.com/lib/pq v1.12.3 + github.com/matoous/go-nanoid/v2 v2.1.0 github.com/nyaruka/phonenumbers v1.7.2 github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/api/go.sum b/api/go.sum index fa1163a2f..cb11e652b 100644 --- a/api/go.sum +++ b/api/go.sum @@ -237,6 +237,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= +github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= diff --git a/api/pkg/entities/bulk_message.go b/api/pkg/entities/bulk_message.go new file mode 100644 index 000000000..86227ffa1 --- /dev/null +++ b/api/pkg/entities/bulk_message.go @@ -0,0 +1,16 @@ +package entities + +import "time" + +// BulkMessage represents a summary of a bulk message batch +type BulkMessage struct { + RequestID string `json:"request_id" example:"bulk-csv-a1B2c3D4e5"` + Total int64 `json:"total" example:"150"` + ScheduledCount int64 `json:"scheduled_count" example:"50"` + PendingCount int64 `json:"pending_count" example:"30"` + FailedCount int64 `json:"failed_count" example:"5"` + ExpiredCount int64 `json:"expired_count" example:"3"` + SentCount int64 `json:"sent_count" example:"40"` + DeliveredCount int64 `json:"delivered_count" example:"25"` + CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"` +} diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index 16c833fe8..c388ef141 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -1,18 +1,18 @@ package handlers import ( + "crypto/rand" "fmt" "sync" "sync/atomic" "github.com/NdoleStudio/httpsms/pkg/requests" - "github.com/google/uuid" - "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v2" + gonanoid "github.com/matoous/go-nanoid/v2" "github.com/palantir/stacktrace" ) @@ -45,9 +45,35 @@ func NewBulkMessageHandler( // RegisterRoutes registers the routes for the MessageHandler func (h *BulkMessageHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) { + router.Get("/v1/bulk-messages", h.computeRoute(middlewares, h.Index)...) router.Post("/v1/bulk-messages", h.computeRoute(middlewares, h.Store)...) } +// Index fetches the bulk message order history. +// @Summary List bulk message orders +// @Description Fetches the last 10 bulk message order summaries for the authenticated user showing counts per status. +// @Security ApiKeyAuth +// @Tags BulkSMS +// @Accept json +// @Produce json +// @Success 200 {object} responses.BulkMessagesResponse +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError +// @Router /bulk-messages [get] +func (h *BulkMessageHandler) Index(c *fiber.Ctx) error { + ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) + defer span.End() + + orders, err := h.messageService.GetBulkMessages(ctx, h.userIDFomContext(c)) + if err != nil { + msg := fmt.Sprintf("cannot fetch bulk messages for user [%s]", h.userIDFomContext(c)) + ctxLogger.Error(stacktrace.Propagate(err, msg)) + return h.responseInternalServerError(c) + } + + return h.responseOK(c, fmt.Sprintf("fetched %d bulk %s", len(orders), h.pluralize("message", len(orders))), orders) +} + // Store sends bulk SMS messages from a CSV or Excel file. // @Summary Store bulk SMS file // @Description Sends bulk SMS messages to multiple users based on our [CSV template](https://httpsms.com/templates/httpsms-bulk.csv) or our [Excel template](https://httpsms.com/templates/httpsms-bulk.xlsx). @@ -73,7 +99,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - messages, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) + messages, fileType, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) if len(validationErrors) != 0 { msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)) ctxLogger.Warn(stacktrace.NewError(msg)) @@ -85,7 +111,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responsePaymentRequired(c, *msg) } - requestID := uuid.New() + requestID := h.generateRequestID(fileType, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") wg := sync.WaitGroup{} count := atomic.Int64{} @@ -95,7 +121,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { for _, message := range messages { wg.Add(1) var perPhoneIndex int - if message.SendTime == nil { + if message.GetSendTime() == nil { perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] phoneIndexCounter[message.FromPhoneNumber]++ } @@ -118,3 +144,22 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { wg.Wait() return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages))) } + +func (h *BulkMessageHandler) generateRequestID(fileType string, alphabet string) string { + id, err := gonanoid.Generate(alphabet, 10) + if err != nil { + id = h.randomAlphaNum(10, alphabet) + } + return fmt.Sprintf("bulk-%s-%s", fileType, id) +} + +func (h *BulkMessageHandler) randomAlphaNum(length int, alphabet string) string { + b := make([]byte, length) + if _, err := rand.Read(b); err != nil { + return alphabet[:length] + } + for i := range b { + b[i] = alphabet[int(b[i])%len(alphabet)] + } + return string(b) +} diff --git a/api/pkg/repositories/gorm_message_repository.go b/api/pkg/repositories/gorm_message_repository.go index 607af44e6..032375668 100644 --- a/api/pkg/repositories/gorm_message_repository.go +++ b/api/pkg/repositories/gorm_message_repository.go @@ -176,6 +176,37 @@ func (repository *gormMessageRepository) Search(ctx context.Context, userID enti return messages, nil } +// GetBulkMessages fetches the last bulk message summaries for a user +func (repository *gormMessageRepository) GetBulkMessages(ctx context.Context, userID entities.UserID, limit int) ([]*entities.BulkMessage, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + orders := make([]*entities.BulkMessage, 0) + err := repository.db.WithContext(ctx).Raw(` + SELECT + request_id, + COUNT(*) as total, + COUNT(*) FILTER (WHERE status = 'scheduled') as scheduled_count, + COUNT(*) FILTER (WHERE status = 'pending') as pending_count, + COUNT(*) FILTER (WHERE status = 'failed') as failed_count, + COUNT(*) FILTER (WHERE status = 'expired') as expired_count, + COUNT(*) FILTER (WHERE status = 'sent') as sent_count, + COUNT(*) FILTER (WHERE status = 'delivered') as delivered_count, + MIN(created_at) as created_at + FROM messages + WHERE user_id = ? AND request_id LIKE 'bulk-%' + GROUP BY request_id + ORDER BY MIN(created_at) DESC + LIMIT ? + `, userID, limit).Scan(&orders).Error + if err != nil { + msg := fmt.Sprintf("cannot fetch bulk message orders for user [%s]", userID) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + return orders, nil +} + // Store a new entities.Message func (repository *gormMessageRepository) Store(ctx context.Context, message *entities.Message) error { ctx, span := repository.tracer.Start(ctx) diff --git a/api/pkg/repositories/message_repository.go b/api/pkg/repositories/message_repository.go index 3ad700154..c8f85fbb0 100644 --- a/api/pkg/repositories/message_repository.go +++ b/api/pkg/repositories/message_repository.go @@ -27,6 +27,9 @@ type MessageRepository interface { // Search entities.Message for a user Search(ctx context.Context, userID entities.UserID, owners []string, types []entities.MessageType, statuses []entities.MessageStatus, params IndexParams) ([]*entities.Message, error) + // GetBulkMessages fetches the last bulk message summaries for a user + GetBulkMessages(ctx context.Context, userID entities.UserID, limit int) ([]*entities.BulkMessage, error) + // GetOutstanding fetches an entities.Message which is outstanding GetOutstanding(ctx context.Context, userID entities.UserID, messageID uuid.UUID, phoneNumbers []string) (*entities.Message, error) diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go index bd6f12bed..f76f97bc0 100644 --- a/api/pkg/repositories/mongodb.go +++ b/api/pkg/repositories/mongodb.go @@ -107,11 +107,6 @@ func createMongoIndexes(ctx context.Context, db *mongo.Database) error { // Heartbeats indexes heartbeatsCol := db.Collection(collectionHeartbeats) - // TODO: Remove this block after deploying once — old indexes will have been dropped in production. - for _, name := range []string{"owner_1_timestamp_-1", "user_id_1"} { - _ = heartbeatsCol.Indexes().DropOne(ctx, name) - } - _, err := heartbeatsCol.Indexes().CreateMany(ctx, []mongo.IndexModel{ {Keys: bson.D{{"user_id", 1}, {"owner", 1}, {"timestamp", -1}}}, }) diff --git a/api/pkg/requests/bulk_message_request.go b/api/pkg/requests/bulk_message_request.go index 000ff016f..b84425fc1 100644 --- a/api/pkg/requests/bulk_message_request.go +++ b/api/pkg/requests/bulk_message_request.go @@ -1,24 +1,46 @@ package requests import ( - "fmt" "strings" "time" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/services" - "github.com/google/uuid" "github.com/nyaruka/phonenumbers" ) // BulkMessage represents a single message in a bulk SMS request type BulkMessage struct { request - FromPhoneNumber string `csv:"FromPhoneNumber"` - ToPhoneNumber string `csv:"ToPhoneNumber"` - Content string `csv:"Content"` - SendTime *time.Time `csv:"SendTime(optional)"` - AttachmentURLs string `csv:"AttachmentURLs(optional)" validate:"optional"` // Comma separated list of URLs + FileType string `json:"type"` + FromPhoneNumber string `csv:"FromPhoneNumber"` + ToPhoneNumber string `csv:"ToPhoneNumber"` + Content string `csv:"Content"` + SendTime string `csv:"SendTime(optional)"` + AttachmentURLs string `csv:"AttachmentURLs(optional)" validate:"optional"` // Comma separated list of URLs +} + +// GetSendTime parses the raw SendTime string into a *time.Time +func (input *BulkMessage) GetSendTime() *time.Time { + raw := strings.TrimSpace(input.SendTime) + if raw == "" { + return nil + } + + formats := []string{ + time.RFC3339, + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + "2006-01-02", + } + + for _, format := range formats { + if t, err := time.Parse(format, raw); err == nil { + utc := t.UTC() + return &utc + } + } + return nil } // Sanitize sets defaults to BulkMessage @@ -38,15 +60,15 @@ func (input *BulkMessage) Sanitize() *BulkMessage { } // ToMessageSendParams converts BulkMessage to services.MessageSendParams -func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID uuid.UUID, source string, index int) services.MessageSendParams { +func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID string, source string, index int) services.MessageSendParams { from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) return services.MessageSendParams{ Source: source, Owner: from, - RequestID: input.sanitizeStringPointer(fmt.Sprintf("bulk-%s", requestID.String())), + RequestID: input.sanitizeStringPointer(requestID), UserID: userID, - SendAt: input.SendTime, + SendAt: input.GetSendTime(), RequestReceivedAt: time.Now().UTC(), Contact: input.sanitizeAddress(input.ToPhoneNumber), Content: input.Content, diff --git a/api/pkg/responses/bulk_message_responses.go b/api/pkg/responses/bulk_message_responses.go new file mode 100644 index 000000000..eda242a63 --- /dev/null +++ b/api/pkg/responses/bulk_message_responses.go @@ -0,0 +1,9 @@ +package responses + +import "github.com/NdoleStudio/httpsms/pkg/entities" + +// BulkMessagesResponse is the payload containing []*entities.BulkMessage +type BulkMessagesResponse struct { + response + Data []*entities.BulkMessage `json:"data"` +} diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go index 929998bdf..56766c981 100644 --- a/api/pkg/services/message_service.go +++ b/api/pkg/services/message_service.go @@ -123,6 +123,21 @@ func (service *MessageService) DeleteAllForUser(ctx context.Context, userID enti return nil } +// GetBulkMessages fetches the last bulk message summaries for a user +func (service *MessageService) GetBulkMessages(ctx context.Context, userID entities.UserID) ([]*entities.BulkMessage, error) { + ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger) + defer span.End() + + orders, err := service.repository.GetBulkMessages(ctx, userID, 10) + if err != nil { + msg := fmt.Sprintf("could not fetch bulk messages for user [%s]", userID) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + ctxLogger.Info(fmt.Sprintf("fetched [%d] bulk messages for user [%s]", len(orders), userID)) + return orders, nil +} + // DeleteMessage deletes a message from the database func (service *MessageService) DeleteMessage(ctx context.Context, source string, message *entities.Message) error { ctx, span := service.tracer.Start(ctx) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 3fa0c35c0..5976c2cc0 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -52,7 +52,7 @@ func NewBulkMessageHandlerValidator( } // ValidateStore validates the requests.BillingUsageHistory request -func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) { +func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) { ctx, span, ctxLogger := v.tracer.StartWithLogger(ctx, v.logger) defer span.End() @@ -61,22 +61,22 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID result := url.Values{} result.Add("document", "Cannot load your account. Please try again later or contact support.") ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s]", userID)))) - return nil, result + return nil, "", result } - messages, result := v.parseFile(ctxLogger, user, header) + messages, fileType, result := v.parseFile(ctxLogger, user, header) if len(result) != 0 { - return messages, result + return messages, fileType, result } if len(messages) == 0 { result.Add("document", "The uploaded file doesn't contain any valid records. Make sure you are using the official httpSMS template.") - return messages, result + return messages, fileType, result } if len(messages) > 1000 { result.Add("document", "The uploaded file must contain less than 1000 records.") - return messages, result + return messages, fileType, result } for index, message := range messages { @@ -85,30 +85,32 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID result = v.validateMessages(ctx, messages) if len(result) != 0 { - return messages, result + return messages, fileType, result } result = v.validateOwners(ctx, userID, messages) if len(result) != 0 { - return messages, result + return messages, fileType, result } - return messages, result + return messages, fileType, result } -func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) { +func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) { if header.Header.Get("Content-Type") == "text/csv" || strings.HasSuffix(header.Filename, ".csv") { - return v.parseCSV(ctxLogger, user, header) + messages, result := v.parseCSV(ctxLogger, user, header) + return messages, "csv", result } if header.Header.Get("Content-Type") == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || strings.HasSuffix(header.Filename, ".xlsx") { - return v.parseXlsx(ctxLogger, user, header) + messages, result := v.parseXlsx(ctxLogger, user, header) + return messages, "xls", result } ctxLogger.Error(stacktrace.NewError(fmt.Sprintf("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type")))) result := url.Values{} result.Add("document", fmt.Sprintf("The file [%s] is not a valid CSV or Excel file.", header.Filename)) - return nil, result + return nil, "", result } func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) { @@ -138,14 +140,15 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user continue } - var sendAt *time.Time + var sendTimeRaw string if len(row) > 3 && strings.TrimSpace(row[3]) != "" { ctxLogger.Info(fmt.Sprintf("excel time = [%s]", row[3])) - sendAt, err = v.convertExcelTime(user, row[3]) + sendAt, err := v.convertExcelTime(user, row[3]) if err != nil { result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] is not in the correct format e.g [2006-01-02T15:04:05] where 2006 is the year, 01 is January, 02 is the second day of the month and the time is 15:04:05", index+1, row[3])) return nil, result } + sendTimeRaw = sendAt.Format(time.RFC3339) } var attachmentURLs string @@ -157,7 +160,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user FromPhoneNumber: strings.TrimSpace(row[0]), ToPhoneNumber: strings.TrimSpace(row[1]), Content: row[2], - SendTime: sendAt, + SendTime: sendTimeRaw, AttachmentURLs: attachmentURLs, }) } @@ -265,8 +268,13 @@ func (v *BulkMessageHandlerValidator) validateMessages(_ context.Context, messag result.Add("document", fmt.Sprintf("Row [%d]: The message content must be less than 1024 characters.", index+2)) } - if message.SendTime != nil && message.SendTime.After(time.Now().Add(420*time.Hour)) { - result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] cannot be more than 20 days (420 hours) in the future.", index+2, message.SendTime.Format(time.RFC3339))) + if strings.TrimSpace(message.SendTime) != "" { + sendTime := message.GetSendTime() + if sendTime == nil { + result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] is not a valid date format. Use RFC3339 (e.g. 2023-11-11T02:10:01Z) or YYYY-MM-DDTHH:MM:SS.", index+2, message.SendTime)) + } else if sendTime.After(time.Now().Add(420 * time.Hour)) { + result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] cannot be more than 20 days (420 hours) in the future.", index+2, sendTime.Format(time.RFC3339))) + } } } return result diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index ee6cf9b27..da6a7a1d6 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -328,7 +328,7 @@ func (validator MessageHandlerValidator) ValidateMessageSearch(ctx context.Conte "min:0", }, "query": []string{ - "max:20", + "max:50", }, "token": []string{ "required", diff --git a/tests/go.mod b/tests/go.mod index 422d1e0bc..1cc657c08 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -1,6 +1,6 @@ module github.com/NdoleStudio/httpsms/tests -go 1.23 +go 1.24.0 require ( github.com/NdoleStudio/httpsms-go v0.0.8 @@ -8,10 +8,19 @@ require ( github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 github.com/wiremock/go-wiremock v1.14.0 + github.com/xuri/excelize/v2 v2.10.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 2deb9da04..441bc37ef 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -68,6 +68,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -78,12 +82,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8= github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/wiremock/go-wiremock v1.14.0 h1:cVAV98Odg+hySEYKDRUasVo30q7JE/ysrdx5qOmF4f4= github.com/wiremock/go-wiremock v1.14.0/go.mod h1:T5XkKnsKS2asycbUrk2cpxXTEXwa6klHfCWVN8BkhkU= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= @@ -94,14 +106,24 @@ go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPi go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= diff --git a/tests/helpers_test.go b/tests/helpers_test.go index 6b4a07822..49a7a6545 100644 --- a/tests/helpers_test.go +++ b/tests/helpers_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "math/big" + "mime/multipart" "net/http" "strings" "testing" @@ -315,3 +316,99 @@ func waitForFCMPush(t *testing.T, messageID string, timeout time.Duration) []wmJ t.Fatalf("FCM push for message %s not found within %v", messageID, timeout) return nil } + +type BulkMessageEntry struct { + RequestID string `json:"request_id"` + Total int `json:"total"` + ScheduledCount int `json:"scheduled_count"` + PendingCount int `json:"pending_count"` + FailedCount int `json:"failed_count"` + ExpiredCount int `json:"expired_count"` + SentCount int `json:"sent_count"` + DeliveredCount int `json:"delivered_count"` + CreatedAt string `json:"created_at"` +} + +func uploadBulkFile(ctx context.Context, t *testing.T, filename string, fileBytes []byte) (int, []byte) { + t.Helper() + + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + part, err := writer.CreateFormFile("document", filename) + require.NoError(t, err) + + _, err = part.Write(fileBytes) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + url := apiBaseURL + "/v1/bulk-messages" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) + require.NoError(t, err) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("x-api-key", userAPIKey) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return resp.StatusCode, body +} + +func fetchBulkMessages(ctx context.Context, t *testing.T) []BulkMessageEntry { + t.Helper() + + url := apiBaseURL + "/v1/bulk-messages" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", userAPIKey) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "fetch bulk messages failed: %s", string(body)) + + var result struct { + Data []BulkMessageEntry `json:"data"` + } + require.NoError(t, json.Unmarshal(body, &result)) + return result.Data +} + +func searchMessages(ctx context.Context, t *testing.T, contact string, owner string) []httpsms.Message { + t.Helper() + + url := fmt.Sprintf("%s/v1/messages?contact=%s&owner=%s&limit=20&skip=0", apiBaseURL, contact, owner) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + require.NoError(t, err) + req.Header.Set("x-api-key", userAPIKey) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode, "search messages failed: %s", string(body)) + + var result struct { + Data []httpsms.Message `json:"data"` + } + require.NoError(t, json.Unmarshal(body, &result)) + return result.Data +} + +func findBulkEntry(entries []BulkMessageEntry, requestID string) *BulkMessageEntry { + for i := range entries { + if entries[i].RequestID == requestID { + return &entries[i] + } + } + return nil +} diff --git a/tests/integration_test.go b/tests/integration_test.go index 12776aac2..29d4ced89 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -14,6 +14,7 @@ import ( httpsms "github.com/NdoleStudio/httpsms-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xuri/excelize/v2" ) func TestSendSMS_Encrypted(t *testing.T) { @@ -402,3 +403,158 @@ func TestHeartbeat_StoreAndIndex(t *testing.T) { assert.True(t, hb.Charging) assert.False(t, hb.Timestamp.IsZero(), "timestamp should not be zero") } + +func TestBulkSMS_CSV(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + // Build CSV content with 1 message + contact := randomPhoneNumber() + csvContent := fmt.Sprintf("FromPhoneNumber,ToPhoneNumber,Content,SendTime(optional)\n%s,%s,CSV bulk test message,\n", + phone.PhoneNumber, contact) + + // Upload CSV + statusCode, respBody := uploadBulkFile(ctx, t, "test.csv", []byte(csvContent)) + require.Equal(t, http.StatusAccepted, statusCode, "upload failed: %s", string(respBody)) + t.Logf("upload response: %s", string(respBody)) + + // Parse the response to verify message count + var uploadResp struct { + Message string `json:"message"` + } + require.NoError(t, json.Unmarshal(respBody, &uploadResp)) + assert.Contains(t, uploadResp.Message, "1 out of 1") + + // Wait a moment for messages to be persisted + time.Sleep(2 * time.Second) + + // Search for the bulk message by owner to get message IDs + messages := searchMessages(ctx, t, contact, phone.PhoneNumber) + require.GreaterOrEqual(t, len(messages), 1, "expected at least 1 message for phone %s", phone.PhoneNumber) + + // Find the message with bulk- request_id prefix + var bulkMsg *httpsms.Message + for i := range messages { + if messages[i].RequestID != nil && strings.HasPrefix(*messages[i].RequestID, "bulk-") { + bulkMsg = &messages[i] + break + } + } + require.NotNil(t, bulkMsg, "no message with bulk- request_id found") + messageID := bulkMsg.ID.String() + requestID := *bulkMsg.RequestID + t.Logf("found bulk message: id=%s, request_id=%s", messageID, requestID) + + // Wait for FCM push + waitForFCMPush(t, messageID, 30*time.Second) + + // Fire SENT event + fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "SENT") + + // Poll until message reaches "sent" status + msg := pollMessageStatus(ctx, t, messageID, "sent", 15*time.Second) + assert.Equal(t, "sent", msg.Status) + + // Verify bulk-messages history endpoint + entries := fetchBulkMessages(ctx, t) + entry := findBulkEntry(entries, requestID) + require.NotNil(t, entry, "bulk entry with request_id %s not found in history", requestID) + + assert.Equal(t, 1, entry.Total) + assert.Equal(t, 1, entry.SentCount) + assert.Equal(t, 0, entry.PendingCount) + assert.Equal(t, 0, entry.FailedCount) + assert.Equal(t, 0, entry.ExpiredCount) + assert.Equal(t, 0, entry.DeliveredCount) + assert.Equal(t, 0, entry.ScheduledCount) +} + +func TestBulkSMS_Excel(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + + contact1 := randomPhoneNumber() + contact2 := randomPhoneNumber() + + // Build Excel file with 2 messages + f := excelize.NewFile() + sheet := f.GetSheetName(0) + f.SetCellValue(sheet, "A1", "FromPhoneNumber") + f.SetCellValue(sheet, "B1", "ToPhoneNumber") + f.SetCellValue(sheet, "C1", "Content") + f.SetCellValue(sheet, "D1", "SendTime(optional)") + + f.SetCellValue(sheet, "A2", phone.PhoneNumber) + f.SetCellValue(sheet, "B2", contact1) + f.SetCellValue(sheet, "C2", "Excel bulk test message 1") + f.SetCellValue(sheet, "D2", "") + + f.SetCellValue(sheet, "A3", phone.PhoneNumber) + f.SetCellValue(sheet, "B3", contact2) + f.SetCellValue(sheet, "C3", "Excel bulk test message 2") + f.SetCellValue(sheet, "D3", "") + + var excelBuf bytes.Buffer + require.NoError(t, f.Write(&excelBuf)) + + // Upload Excel + statusCode, respBody := uploadBulkFile(ctx, t, "test.xlsx", excelBuf.Bytes()) + require.Equal(t, http.StatusAccepted, statusCode, "upload failed: %s", string(respBody)) + t.Logf("upload response: %s", string(respBody)) + + var uploadResp struct { + Message string `json:"message"` + } + require.NoError(t, json.Unmarshal(respBody, &uploadResp)) + assert.Contains(t, uploadResp.Message, "2 out of 2") + + // Wait for messages to be persisted + time.Sleep(2 * time.Second) + + // Search for bulk messages by owner and each contact + messages1 := searchMessages(ctx, t, contact1, phone.PhoneNumber) + messages2 := searchMessages(ctx, t, contact2, phone.PhoneNumber) + messages := append(messages1, messages2...) + require.GreaterOrEqual(t, len(messages), 2, "expected at least 2 messages for phone %s", phone.PhoneNumber) + + // Find messages with bulk- request_id prefix + var bulkMessages []httpsms.Message + var requestID string + for i := range messages { + if messages[i].RequestID != nil && strings.HasPrefix(*messages[i].RequestID, "bulk-") { + bulkMessages = append(bulkMessages, messages[i]) + requestID = *messages[i].RequestID + } + } + require.Len(t, bulkMessages, 2, "expected 2 messages with bulk- request_id") + require.NotEmpty(t, requestID) + t.Logf("found %d bulk messages with request_id=%s", len(bulkMessages), requestID) + + // Wait for FCM pushes for both messages + msgID1 := bulkMessages[0].ID.String() + msgID2 := bulkMessages[1].ID.String() + waitForFCMPush(t, msgID1, 30*time.Second) + waitForFCMPush(t, msgID2, 30*time.Second) + + // Fire SENT then DELIVERED on message 1, leave message 2 pending + fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "SENT") + time.Sleep(200 * time.Millisecond) + fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "DELIVERED") + + // Poll until message 1 reaches "delivered" + msg1 := pollMessageStatus(ctx, t, msgID1, "delivered", 15*time.Second) + assert.Equal(t, "delivered", msg1.Status) + + // Verify bulk-messages history endpoint + entries := fetchBulkMessages(ctx, t) + entry := findBulkEntry(entries, requestID) + require.NotNil(t, entry, "bulk entry with request_id %s not found in history", requestID) + + assert.Equal(t, 2, entry.Total) + assert.Equal(t, 1, entry.DeliveredCount) + assert.Equal(t, 0, entry.PendingCount) + assert.Equal(t, 0, entry.SentCount) + assert.Equal(t, 0, entry.FailedCount) + assert.Equal(t, 0, entry.ExpiredCount) + assert.Equal(t, 1, entry.ScheduledCount) +} diff --git a/web/models/api.ts b/web/models/api.ts index 033825ca5..c4d7b8dc9 100644 --- a/web/models/api.ts +++ b/web/models/api.ts @@ -50,6 +50,25 @@ export interface EntitiesBillingUsage { user_id: string } +export interface EntitiesBulkMessage { + /** @example "2022-06-05T14:26:02.302718+03:00" */ + created_at: string + /** @example 25 */ + delivered_count: number + /** @example 5 */ + failed_count: number + /** @example 30 */ + pending_count: number + /** @example "bulk-32343a19-da5e-4b1b-a767-3298a73703cb" */ + request_id: string + /** @example 50 */ + scheduled_count: number + /** @example 40 */ + sent_count: number + /** @example 150 */ + total: number +} + export interface EntitiesDiscord { /** @example "2022-06-05T14:26:02.302718+03:00" */ created_at: string @@ -582,6 +601,14 @@ export interface ResponsesBillingUsagesResponse { status: string } +export interface ResponsesBulkMessagesResponse { + data: EntitiesBulkMessage[] + /** @example "Request handled successfully" */ + message: string + /** @example "success" */ + status: string +} + export interface ResponsesDiscordResponse { data: EntitiesDiscord /** @example "Request handled successfully" */ diff --git a/web/pages/bulk-messages/index.vue b/web/pages/bulk-messages/index.vue index 3182530ae..b2935bcd7 100644 --- a/web/pages/bulk-messages/index.vue +++ b/web/pages/bulk-messages/index.vue @@ -96,6 +96,62 @@ + + +

Bulk Message History

+

+ Your 10 most recent bulk SMS uploads are shown below, including a + delivery status breakdown for each batch. Click on a row to see + individual messages on the search page. +

+ + + + + + @@ -145,9 +201,11 @@ export default Vue.extend({ mdiSquareEditOutline, formFile: null, loading: true, + loadingHistory: true, errorTitle: '', errorMessages: new ErrorMessages(), dialog: false, + bulkOrders: [] as any[], } }, head() { @@ -159,8 +217,32 @@ export default Vue.extend({ async mounted() { await this.$store.dispatch('loadUser') this.loading = false + this.fetchBulkOrders() }, methods: { + cleanName(requestId: string): string { + if (requestId.startsWith('bulk-csv-')) { + return requestId.replace(/^bulk-csv-/, '') + '.csv' + } + if (requestId.startsWith('bulk-xls-')) { + return requestId.replace(/^bulk-xls-/, '') + '.xlsx' + } + return requestId.replace(/^bulk-/, '') + }, + fetchBulkOrders() { + this.loadingHistory = true + this.$store + .dispatch('fetchBulkMessageOrders') + .then((orders: any[]) => { + this.bulkOrders = orders + }) + .catch(() => { + // silently fail - the table will show "no data" + }) + .finally(() => { + this.loadingHistory = false + }) + }, sendBulkMessages() { this.loading = true this.errorMessages = new ErrorMessages() @@ -186,3 +268,12 @@ export default Vue.extend({ }, }) + + diff --git a/web/pages/search-messages/index.vue b/web/pages/search-messages/index.vue index 9fd1f6676..67062625a 100644 --- a/web/pages/search-messages/index.vue +++ b/web/pages/search-messages/index.vue @@ -325,6 +325,7 @@ export default Vue.extend({ mdiCallMade, mdiProgressCheck, loading: true, + initialLoadComplete: false, errorTitle: '', showDeleteDialog: false, selectedMessages: [] as EntitiesMessage[], @@ -388,6 +389,9 @@ export default Vue.extend({ watch: { options: { handler() { + if (!this.initialLoadComplete) { + return + } this.fetchMessages() }, deep: true, @@ -396,7 +400,20 @@ export default Vue.extend({ async mounted() { await this.$store.dispatch('loadUser') await this.$store.dispatch('loadPhones') + + // Auto-fill search query from URL params + const queryParam = this.$route.query.query + if (queryParam && typeof queryParam === 'string') { + this.formQuery = queryParam + } + this.loading = false + this.initialLoadComplete = true + + // Auto-search if query param was provided + if (this.formQuery) { + this.fetchMessages(true) + } }, methods: { diff --git a/web/store/index.ts b/web/store/index.ts index fc53c3958..9077ccf6a 100644 --- a/web/store/index.ts +++ b/web/store/index.ts @@ -395,6 +395,23 @@ export const actions = { } }, + fetchBulkMessageOrders(context: ActionContext) { + return new Promise((resolve, reject) => { + axios + .get<{ data: any[] }>(`/v1/bulk-messages`) + .then((response) => { + resolve(response.data.data ?? []) + }) + .catch(async (error: AxiosError) => { + await context.dispatch('addNotification', { + message: 'Error while fetching bulk messages history', + type: 'error', + }) + reject(error) + }) + }) + }, + sendBulkMessages(context: ActionContext, document: File) { return new Promise((resolve, reject) => { const formData = new FormData() From 2e483d86220a2b8d136fc6ddf3236446e8b9b8d8 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 09:53:35 +0300 Subject: [PATCH 173/381] feat(web): add bulk resend button and mobile UI improvements to search messages (#901) * feat(web): add bulk resend button and mobile UI improvements to search messages - Add RESEND button with confirmation dialog for bulk resending messages - Resend is only enabled when all selected messages are MT with expired/failed status - Hide RESEND button on mobile - On mobile: remove icons from DELETE, EXPORT, and SEARCH buttons, show uppercase text only - On desktop: keep original icons and text for DELETE, EXPORT, and SEARCH buttons Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(web): include request_id when resending messages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(web): update resend button text and use default color Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(web): resolve duplicate notifications and partial failure handling in resend - Use direct axios.post instead of sendMessage store action to avoid per-message notifications - Switch from Promise.all to Promise.allSettled to handle partial failures gracefully - Report how many messages succeeded vs failed when partial failures occur Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- web/pages/search-messages/index.vue | 119 ++++++++++++++++++++++++++-- web/store/index.ts | 1 + 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/web/pages/search-messages/index.vue b/web/pages/search-messages/index.vue index 67062625a..f5eda862a 100644 --- a/web/pages/search-messages/index.vue +++ b/web/pages/search-messages/index.vue @@ -95,8 +95,11 @@ class="py-5" @click="fetchMessages(true)" > - {{ mdiMagnify }} - Search Messages + {{ + mdiMagnify + }} + SEARCH + Search Messages @@ -120,8 +123,11 @@ v-bind="attrs" v-on="on" > - {{ mdiDelete }} - Delete messages + {{ + mdiDelete + }} + DELETE + Delete messages @@ -139,7 +145,6 @@ :loading="loading" @click="deleteMessages" > - {{ mdiDelete }} Yes Delete Messages @@ -147,6 +152,46 @@ + + + + + Are you sure you want to resend the + {{ selectedMessages.length }} selected messages? + + + The selected messages will be queued for sending again using + the original sender, recipient, and content. + + + + Yes Resend Messages + + + Close + + + - {{ mdiExport }} - Export to CSV + {{ + mdiExport + }} + EXPORT + Export to CSV @@ -280,11 +328,13 @@ import { mdiCallReceived, mdiCallMade, mdiExport, + mdiRefresh, mdiProgressCheck, mdiAlert, } from '@mdi/js' import { AxiosError } from 'axios' import { DataOptions } from 'vuetify' +import axios from '~/plugins/axios' import { ErrorMessages, getErrorMessages } from '~/plugins/errors' import capitalize from '~/plugins/capitalize' import { @@ -317,6 +367,7 @@ export default Vue.extend({ mdiMagnify, mdiArrowLeft, mdiExport, + mdiRefresh, mdiAlert, mdiCheck, mdiCheckAll, @@ -328,6 +379,7 @@ export default Vue.extend({ initialLoadComplete: false, errorTitle: '', showDeleteDialog: false, + showResendDialog: false, selectedMessages: [] as EntitiesMessage[], errorMessages: new ErrorMessages(), formOwners: [], @@ -360,6 +412,16 @@ export default Vue.extend({ } }, computed: { + canResendSelected(): boolean { + return ( + this.selectedMessages.length > 0 && + this.selectedMessages.every( + (message: EntitiesMessage) => + message.type === 'mobile-terminated' && + (message.status === 'expired' || message.status === 'failed'), + ) + ) + }, phoneNumberSelectItems() { return this.$store.getters.getPhones.map((phone: EntitiesPhone) => { return { @@ -495,6 +557,49 @@ export default Vue.extend({ }) }, + resendMessages() { + this.loading = true + Promise.allSettled( + this.selectedMessages.map((message) => + axios.post('/v1/messages/send', { + from: message.owner, + to: message.contact, + content: message.content, + sim: message.sim, + request_id: message.request_id, + }), + ), + ) + .then((results) => { + const failed = results.filter((r) => r.status === 'rejected') + if (failed.length === 0) { + this.$store.dispatch('addNotification', { + message: 'The selected messages have been queued for resending', + type: 'success', + }) + this.selectedMessages = [] + } else if (failed.length === results.length) { + this.$store.dispatch('addNotification', { + message: 'Error while resending the selected messages', + type: 'error', + }) + } else { + this.$store.dispatch('addNotification', { + message: `${results.length - failed.length} messages resent, ${ + failed.length + } failed`, + type: 'warning', + }) + this.selectedMessages = [] + } + }) + .finally(() => { + this.loading = false + this.showResendDialog = false + this.fetchMessages() + }) + }, + fetchMessages(reset = false) { this.loading = true this.errorMessages = new ErrorMessages() diff --git a/web/store/index.ts b/web/store/index.ts index 9077ccf6a..4e9e9482c 100644 --- a/web/store/index.ts +++ b/web/store/index.ts @@ -294,6 +294,7 @@ export type SendMessageRequest = { to: string content: string sim: SIM + request_id?: string } export const actions = { From da880b6f0356ffca64b4c1463e0bb2e5a8b2a840 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 10:05:38 +0300 Subject: [PATCH 174/381] fix(deps): bump protobufjs from 6.11.3 to 7.6.0 in /web (#897) Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 6.11.3 to 7.6.0. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.0/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/v6.11.3...protobufjs-v7.6.0) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 7.6.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/pnpm-lock.yaml | 13039 +++++++++++-------------------------------- 1 file changed, 3365 insertions(+), 9674 deletions(-) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 0cec0c8d2..3dd5059aa 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false importers: + .: dependencies: '@mdi/js': @@ -199,2248 +200,1427 @@ importers: version: 1.1.1 packages: + '@aashutoshrathi/word-wrap@1.2.6': - resolution: - { - integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} '@ampproject/remapping@2.2.1': - resolution: - { - integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} '@asamuzakjp/css-color@3.2.0': - resolution: - { - integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==, - } + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} '@babel/code-frame@7.22.13': - resolution: - { - integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} '@babel/code-frame@7.23.5': - resolution: - { - integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} '@babel/code-frame@7.24.7': - resolution: - { - integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} '@babel/code-frame@7.27.1': - resolution: - { - integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} '@babel/code-frame@7.29.0': - resolution: - { - integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} '@babel/compat-data@7.24.7': - resolution: - { - integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.4': - resolution: - { - integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} '@babel/core@7.24.7': - resolution: - { - integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} '@babel/core@7.28.4': - resolution: - { - integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} '@babel/eslint-parser@7.28.6': - resolution: - { - integrity: sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==, - } - engines: { node: ^10.13.0 || ^12.13.0 || >=14.0.0 } + resolution: {integrity: sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 '@babel/generator@7.24.7': - resolution: - { - integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} '@babel/generator@7.28.3': - resolution: - { - integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.22.5': - resolution: - { - integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.24.7': - resolution: - { - integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': - resolution: - { - integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.24.7': - resolution: - { - integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': - resolution: - { - integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} '@babel/helper-create-class-features-plugin@7.24.5': - resolution: - { - integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-create-class-features-plugin@7.24.7': - resolution: - { - integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-create-regexp-features-plugin@7.22.15': - resolution: - { - integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-create-regexp-features-plugin@7.24.7': - resolution: - { - integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-define-polyfill-provider@0.6.2': - resolution: - { - integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==, - } + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 '@babel/helper-environment-visitor@7.22.20': - resolution: - { - integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} '@babel/helper-environment-visitor@7.24.7': - resolution: - { - integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} '@babel/helper-function-name@7.23.0': - resolution: - { - integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} '@babel/helper-function-name@7.24.7': - resolution: - { - integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} '@babel/helper-globals@7.28.0': - resolution: - { - integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} '@babel/helper-hoist-variables@7.24.7': - resolution: - { - integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} '@babel/helper-member-expression-to-functions@7.24.5': - resolution: - { - integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} + engines: {node: '>=6.9.0'} '@babel/helper-member-expression-to-functions@7.24.7': - resolution: - { - integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} + engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.24.7': - resolution: - { - integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.27.1': - resolution: - { - integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} '@babel/helper-module-transforms@7.24.7': - resolution: - { - integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-module-transforms@7.28.3': - resolution: - { - integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-optimise-call-expression@7.22.5': - resolution: - { - integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} '@babel/helper-optimise-call-expression@7.24.7': - resolution: - { - integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} '@babel/helper-plugin-utils@7.27.1': - resolution: - { - integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} '@babel/helper-remap-async-to-generator@7.24.7': - resolution: - { - integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-replace-supers@7.24.1': - resolution: - { - integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-replace-supers@7.24.7': - resolution: - { - integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-simple-access@7.24.7': - resolution: - { - integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} '@babel/helper-skip-transparent-expression-wrappers@7.22.5': - resolution: - { - integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - resolution: - { - integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.5': - resolution: - { - integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} + engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.7': - resolution: - { - integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.27.1': - resolution: - { - integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.24.5': - resolution: - { - integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.27.1': - resolution: - { - integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.28.5': - resolution: - { - integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.24.7': - resolution: - { - integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': - resolution: - { - integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} '@babel/helper-wrap-function@7.24.7': - resolution: - { - integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} + engines: {node: '>=6.9.0'} '@babel/helpers@7.24.7': - resolution: - { - integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} '@babel/helpers@7.28.4': - resolution: - { - integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} '@babel/highlight@7.23.4': - resolution: - { - integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} '@babel/highlight@7.24.7': - resolution: - { - integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} '@babel/parser@7.24.0': - resolution: - { - integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/parser@7.28.0': - resolution: - { - integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/parser@7.28.4': - resolution: - { - integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': - resolution: - { - integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7': - resolution: - { - integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': - resolution: - { - integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7': - resolution: - { - integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-proposal-class-properties@7.18.6': - resolution: - { - integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-decorators@7.24.7': - resolution: - { - integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: - { - integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-optional-chaining@7.21.0': - resolution: - { - integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-methods@7.18.6': - resolution: - { - integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: - { - integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-property-in-object@7.21.11': - resolution: - { - integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-async-generators@7.8.4': - resolution: - { - integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==, - } + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-bigint@7.8.3': - resolution: - { - integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==, - } + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-properties@7.12.13': - resolution: - { - integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==, - } + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: - { - integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-decorators@7.24.7': - resolution: - { - integrity: sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: - { - integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==, - } + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: - { - integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==, - } + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-assertions@7.24.7': - resolution: - { - integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-attributes@7.24.7': - resolution: - { - integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: - { - integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-meta@7.10.4': - resolution: - { - integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==, - } + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-json-strings@7.8.3': - resolution: - { - integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==, - } + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-jsx@7.27.1': - resolution: - { - integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: - { - integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==, - } + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: - { - integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==, - } + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: - { - integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==, - } + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: - { - integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==, - } + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: - { - integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==, - } + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: - { - integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==, - } + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: - { - integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: - { - integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-typescript@7.27.1': - resolution: - { - integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: - { - integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-arrow-functions@7.24.7': - resolution: - { - integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-generator-functions@7.24.7': - resolution: - { - integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-to-generator@7.24.7': - resolution: - { - integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoped-functions@7.24.7': - resolution: - { - integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoping@7.24.7': - resolution: - { - integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-properties@7.24.7': - resolution: - { - integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-static-block@7.24.7': - resolution: - { - integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 '@babel/plugin-transform-classes@7.24.7': - resolution: - { - integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-computed-properties@7.24.7': - resolution: - { - integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-destructuring@7.24.7': - resolution: - { - integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-dotall-regex@7.24.7': - resolution: - { - integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-duplicate-keys@7.24.7': - resolution: - { - integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-dynamic-import@7.24.7': - resolution: - { - integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-exponentiation-operator@7.24.7': - resolution: - { - integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-export-namespace-from@7.24.7': - resolution: - { - integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-for-of@7.24.7': - resolution: - { - integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-function-name@7.24.7': - resolution: - { - integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-json-strings@7.24.7': - resolution: - { - integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-literals@7.24.7': - resolution: - { - integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-logical-assignment-operators@7.24.7': - resolution: - { - integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-member-expression-literals@7.24.7': - resolution: - { - integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-amd@7.24.7': - resolution: - { - integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-commonjs@7.24.7': - resolution: - { - integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-systemjs@7.24.7': - resolution: - { - integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-umd@7.24.7': - resolution: - { - integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': - resolution: - { - integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/plugin-transform-new-target@7.24.7': - resolution: - { - integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': - resolution: - { - integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-numeric-separator@7.24.7': - resolution: - { - integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-rest-spread@7.24.7': - resolution: - { - integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-super@7.24.7': - resolution: - { - integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-catch-binding@7.24.7': - resolution: - { - integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-chaining@7.24.7': - resolution: - { - integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-parameters@7.24.7': - resolution: - { - integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-methods@7.24.7': - resolution: - { - integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-property-in-object@7.24.7': - resolution: - { - integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-property-literals@7.24.7': - resolution: - { - integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-regenerator@7.24.7': - resolution: - { - integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-reserved-words@7.24.7': - resolution: - { - integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-runtime@7.24.7': - resolution: - { - integrity: sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-shorthand-properties@7.24.7': - resolution: - { - integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-spread@7.24.7': - resolution: - { - integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-sticky-regex@7.24.7': - resolution: - { - integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-template-literals@7.24.7': - resolution: - { - integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typeof-symbol@7.24.7': - resolution: - { - integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-escapes@7.24.7': - resolution: - { - integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-property-regex@7.24.7': - resolution: - { - integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-regex@7.24.7': - resolution: - { - integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-sets-regex@7.24.7': - resolution: - { - integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/preset-env@7.24.7': - resolution: - { - integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: - { - integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==, - } + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 '@babel/regjsgen@0.8.0': - resolution: - { - integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==, - } + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} '@babel/runtime@7.24.5': - resolution: - { - integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} + engines: {node: '>=6.9.0'} '@babel/runtime@7.24.7': - resolution: - { - integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} + engines: {node: '>=6.9.0'} '@babel/standalone@7.23.1': - resolution: - { - integrity: sha512-a4muOYz1qUaSoybuUKwK90mRG4sf5rBeUbuzpuGLzG32ZDE/Y2YEebHDODFJN+BtyOKi19hrLfq2qbNyKMx0TA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-a4muOYz1qUaSoybuUKwK90mRG4sf5rBeUbuzpuGLzG32ZDE/Y2YEebHDODFJN+BtyOKi19hrLfq2qbNyKMx0TA==} + engines: {node: '>=6.9.0'} '@babel/standalone@7.24.7': - resolution: - { - integrity: sha512-QRIRMJ2KTeN+vt4l9OjYlxDVXEpcor1Z6V7OeYzeBOw6Q8ew9oMTHjzTx8s6ClsZO7wVf6JgTRutihatN6K0yA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-QRIRMJ2KTeN+vt4l9OjYlxDVXEpcor1Z6V7OeYzeBOw6Q8ew9oMTHjzTx8s6ClsZO7wVf6JgTRutihatN6K0yA==} + engines: {node: '>=6.9.0'} '@babel/template@7.24.7': - resolution: - { - integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} '@babel/template@7.27.2': - resolution: - { - integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} '@babel/traverse@7.24.7': - resolution: - { - integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} '@babel/traverse@7.28.4': - resolution: - { - integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} '@babel/types@7.28.2': - resolution: - { - integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} '@babel/types@7.28.4': - resolution: - { - integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': - resolution: - { - integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, - } + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} '@commitlint/cli@20.4.0': - resolution: - { - integrity: sha512-2lqrFrYNxjKxgMqeYiO3zNM14XN9v72/5xIJyvdLw7sHEGlfg6sweW01PGNWiqZa6/AuZwsb0uzkgWJy6F4N2w==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-2lqrFrYNxjKxgMqeYiO3zNM14XN9v72/5xIJyvdLw7sHEGlfg6sweW01PGNWiqZa6/AuZwsb0uzkgWJy6F4N2w==} + engines: {node: '>=v18'} hasBin: true '@commitlint/config-conventional@20.5.3': - resolution: - { - integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==} + engines: {node: '>=v18'} '@commitlint/config-validator@20.4.0': - resolution: - { - integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==} + engines: {node: '>=v18'} '@commitlint/ensure@20.4.0': - resolution: - { - integrity: sha512-F3qwnanJUisFWwh44GYYmMOxfgJL1FKV73FCB5zxo8pw1CHkxXadGfDfzNkN8B3iqgSGusDN2+oDH6upBmLszA==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-F3qwnanJUisFWwh44GYYmMOxfgJL1FKV73FCB5zxo8pw1CHkxXadGfDfzNkN8B3iqgSGusDN2+oDH6upBmLszA==} + engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': - resolution: - { - integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} '@commitlint/format@20.4.0': - resolution: - { - integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==} + engines: {node: '>=v18'} '@commitlint/is-ignored@20.4.0': - resolution: - { - integrity: sha512-E8AHpedEfuf+lZatFvFiJXA4TtZgBZ10+A7HzFudaEmTPPE5o6MGswxbxUIGAciaHAFj/oTTmyFc6A5tcvxE3Q==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-E8AHpedEfuf+lZatFvFiJXA4TtZgBZ10+A7HzFudaEmTPPE5o6MGswxbxUIGAciaHAFj/oTTmyFc6A5tcvxE3Q==} + engines: {node: '>=v18'} '@commitlint/lint@20.4.0': - resolution: - { - integrity: sha512-W90YCbm5h3Yg+btF5/X+cxsY6vd/H3tsFt6U7WBmDQSkKV8NmitYg89zeoSQyYEiQCwAsH0dcA+99aQtLZiSnw==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-W90YCbm5h3Yg+btF5/X+cxsY6vd/H3tsFt6U7WBmDQSkKV8NmitYg89zeoSQyYEiQCwAsH0dcA+99aQtLZiSnw==} + engines: {node: '>=v18'} '@commitlint/load@20.4.0': - resolution: - { - integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==} + engines: {node: '>=v18'} '@commitlint/message@20.4.0': - resolution: - { - integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==} + engines: {node: '>=v18'} '@commitlint/parse@20.4.0': - resolution: - { - integrity: sha512-NcRkqo/QUnuc1RgxRCIKTqobKzF0BKJ8h3i1jRyeZ+SEy5rO9dPNOh4BqrFsSznb5mnwETYB7ph9tUcthNkwAQ==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-NcRkqo/QUnuc1RgxRCIKTqobKzF0BKJ8h3i1jRyeZ+SEy5rO9dPNOh4BqrFsSznb5mnwETYB7ph9tUcthNkwAQ==} + engines: {node: '>=v18'} '@commitlint/read@20.4.0': - resolution: - { - integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==} + engines: {node: '>=v18'} '@commitlint/resolve-extends@20.4.0': - resolution: - { - integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==} + engines: {node: '>=v18'} '@commitlint/rules@20.4.0': - resolution: - { - integrity: sha512-E+UoAA7WA4xrre9lDyX2vL4Df26I+vqMN4D8JoW/L2xE/VRDvn533/ibhgSlGYDltB9nm2S+1lti3PagEwO0ag==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-E+UoAA7WA4xrre9lDyX2vL4Df26I+vqMN4D8JoW/L2xE/VRDvn533/ibhgSlGYDltB9nm2S+1lti3PagEwO0ag==} + engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': - resolution: - { - integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} '@commitlint/top-level@20.4.0': - resolution: - { - integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==} + engines: {node: '>=v18'} '@commitlint/types@20.4.0': - resolution: - { - integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} + engines: {node: '>=v18'} '@commitlint/types@20.5.0': - resolution: - { - integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} + engines: {node: '>=v18'} '@csstools/cascade-layer-name-parser@1.0.12': - resolution: - { - integrity: sha512-iNCCOnaoycAfcIot3v/orjkTol+j8+Z5xgpqxUpZSdqeaxCADQZtldHhlvzDipmi7OoWdcJUO6DRZcnkMSBEIg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-iNCCOnaoycAfcIot3v/orjkTol+j8+Z5xgpqxUpZSdqeaxCADQZtldHhlvzDipmi7OoWdcJUO6DRZcnkMSBEIg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.7.0 '@csstools/css-tokenizer': ^2.3.2 '@csstools/color-helpers@4.2.1': - resolution: - { - integrity: sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==} + engines: {node: ^14 || ^16 || >=18} '@csstools/color-helpers@5.1.0': - resolution: - { - integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} '@csstools/css-calc@1.2.3': - resolution: - { - integrity: sha512-rlOh81K3CvtY969Od5b1h29YT6MpCHejMCURKrRrXFeCpz67HGaBNvBmWT5S7S+CKn+V7KJ+qxSmK8jNd/aZWA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-rlOh81K3CvtY969Od5b1h29YT6MpCHejMCURKrRrXFeCpz67HGaBNvBmWT5S7S+CKn+V7KJ+qxSmK8jNd/aZWA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.7.0 '@csstools/css-tokenizer': ^2.3.2 '@csstools/css-calc@1.2.4': - resolution: - { - integrity: sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.7.1 '@csstools/css-tokenizer': ^2.4.1 '@csstools/css-calc@2.1.4': - resolution: - { - integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-color-parser@2.0.3': - resolution: - { - integrity: sha512-Qqhb5I/gEh1wI4brf6Kmy0Xn4J1IqO8OTDKWGRsBYtL4bGkHcV9i0XI2Mmo/UYFtSRoXW/RmKTcMh6sCI433Cw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-Qqhb5I/gEh1wI4brf6Kmy0Xn4J1IqO8OTDKWGRsBYtL4bGkHcV9i0XI2Mmo/UYFtSRoXW/RmKTcMh6sCI433Cw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.7.0 '@csstools/css-tokenizer': ^2.3.2 '@csstools/css-color-parser@3.1.0': - resolution: - { - integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-parser-algorithms@2.3.2': - resolution: - { - integrity: sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-tokenizer': ^2.2.1 '@csstools/css-parser-algorithms@2.7.0': - resolution: - { - integrity: sha512-qvBMcOU/uWFCH/VO0MYe0AMs0BGMWAt6FTryMbFIKYtZtVnqTZtT8ktv5o718llkaGZWomJezJZjq3vJDHeJNQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-qvBMcOU/uWFCH/VO0MYe0AMs0BGMWAt6FTryMbFIKYtZtVnqTZtT8ktv5o718llkaGZWomJezJZjq3vJDHeJNQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-tokenizer': ^2.3.2 '@csstools/css-parser-algorithms@3.0.5': - resolution: - { - integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-tokenizer@2.2.1': - resolution: - { - integrity: sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==} + engines: {node: ^14 || ^16 || >=18} '@csstools/css-tokenizer@2.3.2': - resolution: - { - integrity: sha512-0xYOf4pQpAaE6Sm2Q0x3p25oRukzWQ/O8hWVvhIt9Iv98/uu053u2CGm/g3kJ+P0vOYTAYzoU8Evq2pg9ZPXtw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-0xYOf4pQpAaE6Sm2Q0x3p25oRukzWQ/O8hWVvhIt9Iv98/uu053u2CGm/g3kJ+P0vOYTAYzoU8Evq2pg9ZPXtw==} + engines: {node: ^14 || ^16 || >=18} '@csstools/css-tokenizer@3.0.4': - resolution: - { - integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} '@csstools/media-query-list-parser@2.1.12': - resolution: - { - integrity: sha512-t1/CdyVJzOQUiGUcIBXRzTAkWTFPxiPnoKwowKW2z9Uj78c2bBWI/X94BeVfUwVq1xtCjD7dnO8kS6WONgp8Jw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-t1/CdyVJzOQUiGUcIBXRzTAkWTFPxiPnoKwowKW2z9Uj78c2bBWI/X94BeVfUwVq1xtCjD7dnO8kS6WONgp8Jw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.7.0 '@csstools/css-tokenizer': ^2.3.2 '@csstools/media-query-list-parser@2.1.5': - resolution: - { - integrity: sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-parser-algorithms': ^2.3.2 '@csstools/css-tokenizer': ^2.2.1 '@csstools/postcss-cascade-layers@4.0.6': - resolution: - { - integrity: sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-color-function@3.0.17': - resolution: - { - integrity: sha512-hi6g5KHMvxpxf01LCVu5xnNxX5h2Vkn9aKRmspn2esWjWtshuTXVOavTjwvogA+Eycm9Rn21QTYNU+qbKw6IeQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-hi6g5KHMvxpxf01LCVu5xnNxX5h2Vkn9aKRmspn2esWjWtshuTXVOavTjwvogA+Eycm9Rn21QTYNU+qbKw6IeQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-color-mix-function@2.0.17': - resolution: - { - integrity: sha512-Y65GHGCY1R+9+/5KrJjN7gAF1NZydng4AGknMggeUJIyo2ckLb4vBrlDmpIcHDdjQtV5631j1hxvalVTbpoiFw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-Y65GHGCY1R+9+/5KrJjN7gAF1NZydng4AGknMggeUJIyo2ckLb4vBrlDmpIcHDdjQtV5631j1hxvalVTbpoiFw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-exponential-functions@1.0.8': - resolution: - { - integrity: sha512-/4WHpu4MrCCsUWRaDreyBcdF+5xnudk1JJLg6aWREeMaSpr3vsD0eywmOXct3xUm28TCqKS//S86IlcDJJdzoQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-/4WHpu4MrCCsUWRaDreyBcdF+5xnudk1JJLg6aWREeMaSpr3vsD0eywmOXct3xUm28TCqKS//S86IlcDJJdzoQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-font-format-keywords@3.0.2': - resolution: - { - integrity: sha512-E0xz2sjm4AMCkXLCFvI/lyl4XO6aN1NCSMMVEOngFDJ+k2rDwfr6NDjWljk1li42jiLNChVX+YFnmfGCigZKXw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-E0xz2sjm4AMCkXLCFvI/lyl4XO6aN1NCSMMVEOngFDJ+k2rDwfr6NDjWljk1li42jiLNChVX+YFnmfGCigZKXw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-gamut-mapping@1.0.10': - resolution: - { - integrity: sha512-iPz4/cO8YiNjAYdtAiKGBdKZdFlAvDtUr2AgvAMxCa83e9MwTIKmsJZC3Frw7VYmkfknmdElEZr1FJU+PmB2PA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-iPz4/cO8YiNjAYdtAiKGBdKZdFlAvDtUr2AgvAMxCa83e9MwTIKmsJZC3Frw7VYmkfknmdElEZr1FJU+PmB2PA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-gradients-interpolation-method@4.0.18': - resolution: - { - integrity: sha512-rZH7RnNYY911I/n8+DRrcri89GffptdyuFDGGj/UbxDISFirdR1uI/wcur9KYR/uFHXqrnJjrfi1cisfB7bL+g==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-rZH7RnNYY911I/n8+DRrcri89GffptdyuFDGGj/UbxDISFirdR1uI/wcur9KYR/uFHXqrnJjrfi1cisfB7bL+g==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-hwb-function@3.0.16': - resolution: - { - integrity: sha512-nlC4D5xB7pomgR4kDZ1lqbVqrs6gxPqsM2OE5CkCn0EqCMxtqqtadtbK2dcFwzyujv3DL4wYNo+fgF4rJgLPZA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-nlC4D5xB7pomgR4kDZ1lqbVqrs6gxPqsM2OE5CkCn0EqCMxtqqtadtbK2dcFwzyujv3DL4wYNo+fgF4rJgLPZA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-ic-unit@3.0.6': - resolution: - { - integrity: sha512-fHaU9C/sZPauXMrzPitZ/xbACbvxbkPpHoUgB9Kw5evtsBWdVkVrajOyiT9qX7/c+G1yjApoQjP1fQatldsy9w==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-fHaU9C/sZPauXMrzPitZ/xbACbvxbkPpHoUgB9Kw5evtsBWdVkVrajOyiT9qX7/c+G1yjApoQjP1fQatldsy9w==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-initial@1.0.1': - resolution: - { - integrity: sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-is-pseudo-class@4.0.8': - resolution: - { - integrity: sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-light-dark-function@1.0.6': - resolution: - { - integrity: sha512-bu+cxKpcTrMDMkVCv7QURwKNPZEuXA3J0Udvz3HfmQHt4+OIvvfvDpTgejFXdOliCU4zK9/QdqebPcYneygZtg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-bu+cxKpcTrMDMkVCv7QURwKNPZEuXA3J0Udvz3HfmQHt4+OIvvfvDpTgejFXdOliCU4zK9/QdqebPcYneygZtg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-logical-float-and-clear@2.0.1': - resolution: - { - integrity: sha512-SsrWUNaXKr+e/Uo4R/uIsqJYt3DaggIh/jyZdhy/q8fECoJSKsSMr7nObSLdvoULB69Zb6Bs+sefEIoMG/YfOA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-SsrWUNaXKr+e/Uo4R/uIsqJYt3DaggIh/jyZdhy/q8fECoJSKsSMr7nObSLdvoULB69Zb6Bs+sefEIoMG/YfOA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-logical-overflow@1.0.1': - resolution: - { - integrity: sha512-Kl4lAbMg0iyztEzDhZuQw8Sj9r2uqFDcU1IPl+AAt2nue8K/f1i7ElvKtXkjhIAmKiy5h2EY8Gt/Cqg0pYFDCw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-Kl4lAbMg0iyztEzDhZuQw8Sj9r2uqFDcU1IPl+AAt2nue8K/f1i7ElvKtXkjhIAmKiy5h2EY8Gt/Cqg0pYFDCw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-logical-overscroll-behavior@1.0.1': - resolution: - { - integrity: sha512-+kHamNxAnX8ojPCtV8WPcUP3XcqMFBSDuBuvT6MHgq7oX4IQxLIXKx64t7g9LiuJzE7vd06Q9qUYR6bh4YnGpQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-+kHamNxAnX8ojPCtV8WPcUP3XcqMFBSDuBuvT6MHgq7oX4IQxLIXKx64t7g9LiuJzE7vd06Q9qUYR6bh4YnGpQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-logical-resize@2.0.1': - resolution: - { - integrity: sha512-W5Gtwz7oIuFcKa5SmBjQ2uxr8ZoL7M2bkoIf0T1WeNqljMkBrfw1DDA8/J83k57NQ1kcweJEjkJ04pUkmyee3A==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-W5Gtwz7oIuFcKa5SmBjQ2uxr8ZoL7M2bkoIf0T1WeNqljMkBrfw1DDA8/J83k57NQ1kcweJEjkJ04pUkmyee3A==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-logical-viewport-units@2.0.10': - resolution: - { - integrity: sha512-nGP0KanI/jXrUMpaIBz6mdy/vNs3d/cjbNYuoEc7lCdNkntmxZvwxC2zIKI8QzGWaYsh9jahozMVceZ0jNyjgg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-nGP0KanI/jXrUMpaIBz6mdy/vNs3d/cjbNYuoEc7lCdNkntmxZvwxC2zIKI8QzGWaYsh9jahozMVceZ0jNyjgg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-media-minmax@1.1.7': - resolution: - { - integrity: sha512-AjLG+vJvhrN2geUjYNvzncW1TJ+vC4QrVPGrLPxOSJ2QXC94krQErSW4aXMj0b13zhvVWeqf2NHIOVQknqV9cg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-AjLG+vJvhrN2geUjYNvzncW1TJ+vC4QrVPGrLPxOSJ2QXC94krQErSW4aXMj0b13zhvVWeqf2NHIOVQknqV9cg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-media-queries-aspect-ratio-number-values@2.0.10': - resolution: - { - integrity: sha512-DXae3i7OYJTejxcoUuf/AOIpy+6FWfGGKo/I3WefZI538l3k+ErU6V2xQOx/UmUXT2FDIdE1Ucl9JkZib2rEsA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-DXae3i7OYJTejxcoUuf/AOIpy+6FWfGGKo/I3WefZI538l3k+ErU6V2xQOx/UmUXT2FDIdE1Ucl9JkZib2rEsA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-nested-calc@3.0.2': - resolution: - { - integrity: sha512-ySUmPyawiHSmBW/VI44+IObcKH0v88LqFe0d09Sb3w4B1qjkaROc6d5IA3ll9kjD46IIX/dbO5bwFN/swyoyZA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-ySUmPyawiHSmBW/VI44+IObcKH0v88LqFe0d09Sb3w4B1qjkaROc6d5IA3ll9kjD46IIX/dbO5bwFN/swyoyZA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-normalize-display-values@3.0.2': - resolution: - { - integrity: sha512-fCapyyT/dUdyPtrelQSIV+d5HqtTgnNP/BEG9IuhgXHt93Wc4CfC1bQ55GzKAjWrZbgakMQ7MLfCXEf3rlZJOw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-fCapyyT/dUdyPtrelQSIV+d5HqtTgnNP/BEG9IuhgXHt93Wc4CfC1bQ55GzKAjWrZbgakMQ7MLfCXEf3rlZJOw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-oklab-function@3.0.17': - resolution: - { - integrity: sha512-kIng3Xmw6NKUvD/eEoHGwbyDFXDsuzsVGtNo3ndgZYYqy+DLiD+3drxwRKiViE5LUieLB1ERczXpLVmpSw61eg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-kIng3Xmw6NKUvD/eEoHGwbyDFXDsuzsVGtNo3ndgZYYqy+DLiD+3drxwRKiViE5LUieLB1ERczXpLVmpSw61eg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-progressive-custom-properties@3.2.0': - resolution: - { - integrity: sha512-BZlirVxCRgKlE7yVme+Xvif72eTn1MYXj8oZ4Knb+jwaH4u3AN1DjbhM7j86RP5vvuAOexJ4JwfifYYKWMN/QQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-BZlirVxCRgKlE7yVme+Xvif72eTn1MYXj8oZ4Knb+jwaH4u3AN1DjbhM7j86RP5vvuAOexJ4JwfifYYKWMN/QQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-relative-color-syntax@2.0.17': - resolution: - { - integrity: sha512-EVckAtG8bocItZflXLJ50Su+gwg/4Jhkz1BztyNsT0/svwS6QMAeLjyUA75OsgtejNWQHvBMWna4xc9LCqdjrQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-EVckAtG8bocItZflXLJ50Su+gwg/4Jhkz1BztyNsT0/svwS6QMAeLjyUA75OsgtejNWQHvBMWna4xc9LCqdjrQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-scope-pseudo-class@3.0.1': - resolution: - { - integrity: sha512-3ZFonK2gfgqg29gUJ2w7xVw2wFJ1eNWVDONjbzGkm73gJHVCYK5fnCqlLr+N+KbEfv2XbWAO0AaOJCFB6Fer6A==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-3ZFonK2gfgqg29gUJ2w7xVw2wFJ1eNWVDONjbzGkm73gJHVCYK5fnCqlLr+N+KbEfv2XbWAO0AaOJCFB6Fer6A==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-stepped-value-functions@3.0.9': - resolution: - { - integrity: sha512-uAw1J8hiZ0mM1DLaziI7CP5oagSwDnS5kufuROGIJFzESYfTqNVS3b7FgDZto9AxXdkwI+Sn48+cvG8PwzGMog==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-uAw1J8hiZ0mM1DLaziI7CP5oagSwDnS5kufuROGIJFzESYfTqNVS3b7FgDZto9AxXdkwI+Sn48+cvG8PwzGMog==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-text-decoration-shorthand@3.0.7': - resolution: - { - integrity: sha512-+cptcsM5r45jntU6VjotnkC9GteFR7BQBfZ5oW7inLCxj7AfLGAzMbZ60hKTP13AULVZBdxky0P8um0IBfLHVA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-+cptcsM5r45jntU6VjotnkC9GteFR7BQBfZ5oW7inLCxj7AfLGAzMbZ60hKTP13AULVZBdxky0P8um0IBfLHVA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-trigonometric-functions@3.0.9': - resolution: - { - integrity: sha512-rCAtKX3EsH91ZIHoxFzAAcMQeQCS+PsjzHl6fvsGXz/SV3lqzSmO7MWgFXyPktC2zjZXgOObAJ/2QkhMqVpgNg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-rCAtKX3EsH91ZIHoxFzAAcMQeQCS+PsjzHl6fvsGXz/SV3lqzSmO7MWgFXyPktC2zjZXgOObAJ/2QkhMqVpgNg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/postcss-unset-value@3.0.1': - resolution: - { - integrity: sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@csstools/selector-resolve-nested@1.1.0': - resolution: - { - integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss-selector-parser: ^6.0.13 '@csstools/selector-specificity@3.0.0': - resolution: - { - integrity: sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss-selector-parser: ^6.0.13 '@csstools/selector-specificity@3.1.1': - resolution: - { - integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss-selector-parser: ^6.0.13 '@csstools/utilities@1.0.0': - resolution: - { - integrity: sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 '@discoveryjs/json-ext@0.5.7': - resolution: - { - integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} '@emnapi/core@1.5.0': - resolution: - { - integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==, - } + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} '@emnapi/runtime@1.5.0': - resolution: - { - integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==, - } + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} '@emnapi/wasi-threads@1.1.0': - resolution: - { - integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==, - } + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} '@esbuild/android-arm64@0.18.20': - resolution: - { - integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} cpu: [arm64] os: [android] '@esbuild/android-arm@0.18.20': - resolution: - { - integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} cpu: [arm] os: [android] '@esbuild/android-x64@0.18.20': - resolution: - { - integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.18.20': - resolution: - { - integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.18.20': - resolution: - { - integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.18.20': - resolution: - { - integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.18.20': - resolution: - { - integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.18.20': - resolution: - { - integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.18.20': - resolution: - { - integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.18.20': - resolution: - { - integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.18.20': - resolution: - { - integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.18.20': - resolution: - { - integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.18.20': - resolution: - { - integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.18.20': - resolution: - { - integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.18.20': - resolution: - { - integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.18.20': - resolution: - { - integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} cpu: [x64] os: [linux] '@esbuild/netbsd-x64@0.18.20': - resolution: - { - integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} cpu: [x64] os: [netbsd] '@esbuild/openbsd-x64@0.18.20': - resolution: - { - integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} cpu: [x64] os: [openbsd] '@esbuild/sunos-x64@0.18.20': - resolution: - { - integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.18.20': - resolution: - { - integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.18.20': - resolution: - { - integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.18.20': - resolution: - { - integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} cpu: [x64] os: [win32] '@eslint-community/eslint-utils@4.4.0': - resolution: - { - integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/eslint-utils@4.7.0': - resolution: - { - integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/regexpp@4.9.0': - resolution: - { - integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==, - } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + resolution: {integrity: sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@2.1.4': - resolution: - { - integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@eslint/js@8.57.1': - resolution: - { - integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@fastify/busboy@1.2.1': - resolution: - { - integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} + engines: {node: '>=14'} '@firebase/analytics-compat@0.2.14': - resolution: - { - integrity: sha512-unRVY6SvRqfNFIAA/kwl4vK+lvQAL2HVcgu9zTrUtTyYDmtIt/lOuHJynBMYEgLnKm39YKBDhtqdapP2e++ASw==, - } + resolution: {integrity: sha512-unRVY6SvRqfNFIAA/kwl4vK+lvQAL2HVcgu9zTrUtTyYDmtIt/lOuHJynBMYEgLnKm39YKBDhtqdapP2e++ASw==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/analytics-types@0.8.2': - resolution: - { - integrity: sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==, - } + resolution: {integrity: sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==} '@firebase/analytics@0.10.8': - resolution: - { - integrity: sha512-CVnHcS4iRJPqtIDc411+UmFldk0ShSK3OB+D0bKD8Ck5Vro6dbK5+APZpkuWpbfdL359DIQUnAaMLE+zs/PVyA==, - } + resolution: {integrity: sha512-CVnHcS4iRJPqtIDc411+UmFldk0ShSK3OB+D0bKD8Ck5Vro6dbK5+APZpkuWpbfdL359DIQUnAaMLE+zs/PVyA==} peerDependencies: '@firebase/app': 0.x '@firebase/app-check-compat@0.3.15': - resolution: - { - integrity: sha512-zFIvIFFNqDXpOT2huorz9cwf56VT3oJYRFjSFYdSbGYEJYEaXjLJbfC79lx/zjx4Fh+yuN8pry3TtvwaevrGbg==, - } + resolution: {integrity: sha512-zFIvIFFNqDXpOT2huorz9cwf56VT3oJYRFjSFYdSbGYEJYEaXjLJbfC79lx/zjx4Fh+yuN8pry3TtvwaevrGbg==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/app-check-interop-types@0.3.2': - resolution: - { - integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==, - } + resolution: {integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==} '@firebase/app-check-types@0.5.2': - resolution: - { - integrity: sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==, - } + resolution: {integrity: sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==} '@firebase/app-check@0.8.8': - resolution: - { - integrity: sha512-O49RGF1xj7k6BuhxGpHmqOW5hqBIAEbt2q6POW0lIywx7emYtzPDeQI+ryQpC4zbKX646SoVZ711TN1DBLNSOQ==, - } + resolution: {integrity: sha512-O49RGF1xj7k6BuhxGpHmqOW5hqBIAEbt2q6POW0lIywx7emYtzPDeQI+ryQpC4zbKX646SoVZ711TN1DBLNSOQ==} peerDependencies: '@firebase/app': 0.x '@firebase/app-compat@0.2.43': - resolution: - { - integrity: sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg==, - } + resolution: {integrity: sha512-HM96ZyIblXjAC7TzE8wIk2QhHlSvksYkQ4Ukh1GmEenzkucSNUmUX4QvoKrqeWsLEQ8hdcojABeCV8ybVyZmeg==} '@firebase/app-types@0.8.1': - resolution: - { - integrity: sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==, - } + resolution: {integrity: sha512-p75Ow3QhB82kpMzmOntv866wH9eZ3b4+QbUY+8/DA5Zzdf1c8Nsk8B7kbFpzJt4wwHMdy5LTF5YUnoTc1JiWkw==} '@firebase/app-types@0.9.2': - resolution: - { - integrity: sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==, - } + resolution: {integrity: sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==} '@firebase/app@0.10.13': - resolution: - { - integrity: sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA==, - } + resolution: {integrity: sha512-OZiDAEK/lDB6xy/XzYAyJJkaDqmQ+BCtOEPLqFvxWKUz5JbBmej7IiiRHdtiIOD/twW7O5AxVsfaaGA/V1bNsA==} '@firebase/auth-compat@0.5.14': - resolution: - { - integrity: sha512-2eczCSqBl1KUPJacZlFpQayvpilg3dxXLy9cSMTKtQMTQSmondUtPI47P3ikH3bQAXhzKLOE+qVxJ3/IRtu9pw==, - } + resolution: {integrity: sha512-2eczCSqBl1KUPJacZlFpQayvpilg3dxXLy9cSMTKtQMTQSmondUtPI47P3ikH3bQAXhzKLOE+qVxJ3/IRtu9pw==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/auth-interop-types@0.1.7': - resolution: - { - integrity: sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==, - } + resolution: {integrity: sha512-yA/dTveGGPcc85JP8ZE/KZqfGQyQTBCV10THdI8HTlP1GDvNrhr//J5jAt58MlsCOaO3XmC4DqScPBbtIsR/EA==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x '@firebase/auth-interop-types@0.2.3': - resolution: - { - integrity: sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==, - } + resolution: {integrity: sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==} '@firebase/auth-types@0.12.2': - resolution: - { - integrity: sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==, - } + resolution: {integrity: sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x '@firebase/auth@1.7.9': - resolution: - { - integrity: sha512-yLD5095kVgDw965jepMyUrIgDklD6qH/BZNHeKOgvu7pchOKNjVM+zQoOVYJIKWMWOWBq8IRNVU6NXzBbozaJg==, - } + resolution: {integrity: sha512-yLD5095kVgDw965jepMyUrIgDklD6qH/BZNHeKOgvu7pchOKNjVM+zQoOVYJIKWMWOWBq8IRNVU6NXzBbozaJg==} peerDependencies: '@firebase/app': 0.x '@react-native-async-storage/async-storage': ^1.18.1 @@ -2449,391 +1629,229 @@ packages: optional: true '@firebase/component@0.5.21': - resolution: - { - integrity: sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==, - } + resolution: {integrity: sha512-12MMQ/ulfygKpEJpseYMR0HunJdlsLrwx2XcEs40M18jocy2+spyzHHEwegN3x/2/BLFBjR5247Etmz0G97Qpg==} '@firebase/component@0.6.9': - resolution: - { - integrity: sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==, - } + resolution: {integrity: sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==} '@firebase/data-connect@0.1.0': - resolution: - { - integrity: sha512-vSe5s8dY13ilhLnfY0eYRmQsdTbH7PUFZtBbqU6JVX/j8Qp9A6G5gG6//ulbX9/1JFOF1IWNOne9c8S/DOCJaQ==, - } + resolution: {integrity: sha512-vSe5s8dY13ilhLnfY0eYRmQsdTbH7PUFZtBbqU6JVX/j8Qp9A6G5gG6//ulbX9/1JFOF1IWNOne9c8S/DOCJaQ==} peerDependencies: '@firebase/app': 0.x '@firebase/database-compat@0.2.10': - resolution: - { - integrity: sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==, - } + resolution: {integrity: sha512-fK+IgUUqVKcWK/gltzDU+B1xauCOfY6vulO8lxoNTkcCGlSxuTtwsdqjGkFmgFRMYjXFWWJ6iFcJ/vXahzwCtA==} '@firebase/database-compat@1.0.8': - resolution: - { - integrity: sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==, - } + resolution: {integrity: sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==} '@firebase/database-types@0.9.17': - resolution: - { - integrity: sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==, - } + resolution: {integrity: sha512-YQm2tCZyxNtEnlS5qo5gd2PAYgKCy69tUKwioGhApCFThW+mIgZs7IeYeJo2M51i4LCixYUl+CvnOyAnb/c3XA==} '@firebase/database-types@1.0.5': - resolution: - { - integrity: sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==, - } + resolution: {integrity: sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==} '@firebase/database@0.13.10': - resolution: - { - integrity: sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==, - } + resolution: {integrity: sha512-KRucuzZ7ZHQsRdGEmhxId5jyM2yKsjsQWF9yv0dIhlxYg0D8rCVDZc/waoPKA5oV3/SEIoptF8F7R1Vfe7BCQA==} '@firebase/database@1.0.8': - resolution: - { - integrity: sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==, - } + resolution: {integrity: sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==} '@firebase/firestore-compat@0.3.38': - resolution: - { - integrity: sha512-GoS0bIMMkjpLni6StSwRJarpu2+S5m346Na7gr9YZ/BZ/W3/8iHGNr9PxC+f0rNZXqS4fGRn88pICjrZEgbkqQ==, - } + resolution: {integrity: sha512-GoS0bIMMkjpLni6StSwRJarpu2+S5m346Na7gr9YZ/BZ/W3/8iHGNr9PxC+f0rNZXqS4fGRn88pICjrZEgbkqQ==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/firestore-types@3.0.2': - resolution: - { - integrity: sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==, - } + resolution: {integrity: sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x '@firebase/firestore@4.7.3': - resolution: - { - integrity: sha512-NwVU+JPZ/3bhvNSJMCSzfcBZZg8SUGyzZ2T0EW3/bkUeefCyzMISSt/TTIfEHc8cdyXGlMqfGe3/62u9s74UEg==, - } - engines: { node: '>=10.10.0' } + resolution: {integrity: sha512-NwVU+JPZ/3bhvNSJMCSzfcBZZg8SUGyzZ2T0EW3/bkUeefCyzMISSt/TTIfEHc8cdyXGlMqfGe3/62u9s74UEg==} + engines: {node: '>=10.10.0'} peerDependencies: '@firebase/app': 0.x '@firebase/functions-compat@0.3.14': - resolution: - { - integrity: sha512-dZ0PKOKQFnOlMfcim39XzaXonSuPPAVuzpqA4ONTIdyaJK/OnBaIEVs/+BH4faa1a2tLeR+Jy15PKqDRQoNIJw==, - } + resolution: {integrity: sha512-dZ0PKOKQFnOlMfcim39XzaXonSuPPAVuzpqA4ONTIdyaJK/OnBaIEVs/+BH4faa1a2tLeR+Jy15PKqDRQoNIJw==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/functions-types@0.6.2': - resolution: - { - integrity: sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==, - } + resolution: {integrity: sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==} '@firebase/functions@0.11.8': - resolution: - { - integrity: sha512-Lo2rTPDn96naFIlSZKVd1yvRRqqqwiJk7cf9TZhUerwnPKgBzXy+aHE22ry+6EjCaQusUoNai6mU6p+G8QZT1g==, - } + resolution: {integrity: sha512-Lo2rTPDn96naFIlSZKVd1yvRRqqqwiJk7cf9TZhUerwnPKgBzXy+aHE22ry+6EjCaQusUoNai6mU6p+G8QZT1g==} peerDependencies: '@firebase/app': 0.x '@firebase/installations-compat@0.2.9': - resolution: - { - integrity: sha512-2lfdc6kPXR7WaL4FCQSQUhXcPbI7ol3wF+vkgtU25r77OxPf8F/VmswQ7sgIkBBWtymn5ZF20TIKtnOj9rjb6w==, - } + resolution: {integrity: sha512-2lfdc6kPXR7WaL4FCQSQUhXcPbI7ol3wF+vkgtU25r77OxPf8F/VmswQ7sgIkBBWtymn5ZF20TIKtnOj9rjb6w==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/installations-types@0.5.2': - resolution: - { - integrity: sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==, - } + resolution: {integrity: sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==} peerDependencies: '@firebase/app-types': 0.x '@firebase/installations@0.6.9': - resolution: - { - integrity: sha512-hlT7AwCiKghOX3XizLxXOsTFiFCQnp/oj86zp1UxwDGmyzsyoxtX+UIZyVyH/oBF5+XtblFG9KZzZQ/h+dpy+Q==, - } + resolution: {integrity: sha512-hlT7AwCiKghOX3XizLxXOsTFiFCQnp/oj86zp1UxwDGmyzsyoxtX+UIZyVyH/oBF5+XtblFG9KZzZQ/h+dpy+Q==} peerDependencies: '@firebase/app': 0.x '@firebase/logger@0.3.4': - resolution: - { - integrity: sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==, - } + resolution: {integrity: sha512-hlFglGRgZEwoyClZcGLx/Wd+zoLfGmbDkFx56mQt/jJ0XMbfPqwId1kiPl0zgdWZX+D8iH+gT6GuLPFsJWgiGw==} '@firebase/logger@0.4.2': - resolution: - { - integrity: sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==, - } + resolution: {integrity: sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==} '@firebase/messaging-compat@0.2.12': - resolution: - { - integrity: sha512-pKsiUVZrbmRgdImYqhBNZlkKJbqjlPkVdQRZGRbkTyX4OSGKR0F/oJeCt1a8jEg5UnBp4fdVwSWSp4DuCovvEQ==, - } + resolution: {integrity: sha512-pKsiUVZrbmRgdImYqhBNZlkKJbqjlPkVdQRZGRbkTyX4OSGKR0F/oJeCt1a8jEg5UnBp4fdVwSWSp4DuCovvEQ==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/messaging-interop-types@0.2.2': - resolution: - { - integrity: sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==, - } + resolution: {integrity: sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==} '@firebase/messaging@0.12.12': - resolution: - { - integrity: sha512-6q0pbzYBJhZEtUoQx7hnPhZvAbuMNuBXKQXOx2YlWhSrlv9N1m0ZzlNpBbu/ItTzrwNKTibdYzUyaaxdWLg+4w==, - } + resolution: {integrity: sha512-6q0pbzYBJhZEtUoQx7hnPhZvAbuMNuBXKQXOx2YlWhSrlv9N1m0ZzlNpBbu/ItTzrwNKTibdYzUyaaxdWLg+4w==} peerDependencies: '@firebase/app': 0.x '@firebase/performance-compat@0.2.9': - resolution: - { - integrity: sha512-dNl95IUnpsu3fAfYBZDCVhXNkASE0uo4HYaEPd2/PKscfTvsgqFAOxfAXzBEDOnynDWiaGUnb5M1O00JQ+3FXA==, - } + resolution: {integrity: sha512-dNl95IUnpsu3fAfYBZDCVhXNkASE0uo4HYaEPd2/PKscfTvsgqFAOxfAXzBEDOnynDWiaGUnb5M1O00JQ+3FXA==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/performance-types@0.2.2': - resolution: - { - integrity: sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==, - } + resolution: {integrity: sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==} '@firebase/performance@0.6.9': - resolution: - { - integrity: sha512-PnVaak5sqfz5ivhua+HserxTJHtCar/7zM0flCX6NkzBNzJzyzlH4Hs94h2Il0LQB99roBqoE5QT1JqWqcLJHQ==, - } + resolution: {integrity: sha512-PnVaak5sqfz5ivhua+HserxTJHtCar/7zM0flCX6NkzBNzJzyzlH4Hs94h2Il0LQB99roBqoE5QT1JqWqcLJHQ==} peerDependencies: '@firebase/app': 0.x '@firebase/remote-config-compat@0.2.9': - resolution: - { - integrity: sha512-AxzGpWfWFYejH2twxfdOJt5Cfh/ATHONegTd/a0p5flEzsD5JsxXgfkFToop+mypEL3gNwawxrxlZddmDoNxyA==, - } + resolution: {integrity: sha512-AxzGpWfWFYejH2twxfdOJt5Cfh/ATHONegTd/a0p5flEzsD5JsxXgfkFToop+mypEL3gNwawxrxlZddmDoNxyA==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/remote-config-types@0.3.2': - resolution: - { - integrity: sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==, - } + resolution: {integrity: sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==} '@firebase/remote-config@0.4.9': - resolution: - { - integrity: sha512-EO1NLCWSPMHdDSRGwZ73kxEEcTopAxX1naqLJFNApp4hO8WfKfmEpmjxmP5TrrnypjIf2tUkYaKsfbEA7+AMmA==, - } + resolution: {integrity: sha512-EO1NLCWSPMHdDSRGwZ73kxEEcTopAxX1naqLJFNApp4hO8WfKfmEpmjxmP5TrrnypjIf2tUkYaKsfbEA7+AMmA==} peerDependencies: '@firebase/app': 0.x '@firebase/storage-compat@0.3.12': - resolution: - { - integrity: sha512-hA4VWKyGU5bWOll+uwzzhEMMYGu9PlKQc1w4DWxB3aIErWYzonrZjF0icqNQZbwKNIdh8SHjZlFeB2w6OSsjfg==, - } + resolution: {integrity: sha512-hA4VWKyGU5bWOll+uwzzhEMMYGu9PlKQc1w4DWxB3aIErWYzonrZjF0icqNQZbwKNIdh8SHjZlFeB2w6OSsjfg==} peerDependencies: '@firebase/app-compat': 0.x '@firebase/storage-types@0.8.2': - resolution: - { - integrity: sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==, - } + resolution: {integrity: sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==} peerDependencies: '@firebase/app-types': 0.x '@firebase/util': 1.x '@firebase/storage@0.13.2': - resolution: - { - integrity: sha512-fxuJnHshbhVwuJ4FuISLu+/76Aby2sh+44ztjF2ppoe0TELIDxPW6/r1KGlWYt//AD0IodDYYA8ZTN89q8YqUw==, - } + resolution: {integrity: sha512-fxuJnHshbhVwuJ4FuISLu+/76Aby2sh+44ztjF2ppoe0TELIDxPW6/r1KGlWYt//AD0IodDYYA8ZTN89q8YqUw==} peerDependencies: '@firebase/app': 0.x '@firebase/util@1.10.0': - resolution: - { - integrity: sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==, - } + resolution: {integrity: sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==} '@firebase/util@1.7.3': - resolution: - { - integrity: sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==, - } + resolution: {integrity: sha512-wxNqWbqokF551WrJ9BIFouU/V5SL1oYCGx1oudcirdhadnQRFH5v1sjgGL7cUV/UsekSycygphdrF2lxBxOYKg==} '@firebase/vertexai-preview@0.0.4': - resolution: - { - integrity: sha512-EBSqyu9eg8frQlVU9/HjKtHN7odqbh9MtAcVz3WwHj4gLCLOoN9F/o+oxlq3CxvFrd3CNTZwu6d2mZtVlEInng==, - } - engines: { node: '>=18.0.0' } + resolution: {integrity: sha512-EBSqyu9eg8frQlVU9/HjKtHN7odqbh9MtAcVz3WwHj4gLCLOoN9F/o+oxlq3CxvFrd3CNTZwu6d2mZtVlEInng==} + engines: {node: '>=18.0.0'} peerDependencies: '@firebase/app': 0.x '@firebase/app-types': 0.x '@firebase/webchannel-wrapper@1.0.1': - resolution: - { - integrity: sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==, - } + resolution: {integrity: sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==} '@gar/promisify@1.1.3': - resolution: - { - integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==, - } + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} '@google-cloud/firestore@4.15.1': - resolution: - { - integrity: sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==, - } - engines: { node: '>=10.10.0' } + resolution: {integrity: sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==} + engines: {node: '>=10.10.0'} '@google-cloud/paginator@3.0.7': - resolution: - { - integrity: sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==} + engines: {node: '>=10'} '@google-cloud/projectify@2.1.1': - resolution: - { - integrity: sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==} + engines: {node: '>=10'} '@google-cloud/promisify@2.0.4': - resolution: - { - integrity: sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==} + engines: {node: '>=10'} '@google-cloud/storage@5.20.5': - resolution: - { - integrity: sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==} + engines: {node: '>=10'} '@grpc/grpc-js@1.6.12': - resolution: - { - integrity: sha512-JmvQ03OTSpVd9JTlj/K3IWHSz4Gk/JMLUTtW7Zb0KvO1LcOYGATh5cNuRYzCAeDR3O8wq+q8FZe97eO9MBrkUw==, - } - engines: { node: ^8.13.0 || >=10.10.0 } + resolution: {integrity: sha512-JmvQ03OTSpVd9JTlj/K3IWHSz4Gk/JMLUTtW7Zb0KvO1LcOYGATh5cNuRYzCAeDR3O8wq+q8FZe97eO9MBrkUw==} + engines: {node: ^8.13.0 || >=10.10.0} '@grpc/grpc-js@1.9.15': - resolution: - { - integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==, - } - engines: { node: ^8.13.0 || >=10.10.0 } + resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} + engines: {node: ^8.13.0 || >=10.10.0} '@grpc/proto-loader@0.6.13': - resolution: - { - integrity: sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==} + engines: {node: '>=6'} hasBin: true '@grpc/proto-loader@0.7.10': - resolution: - { - integrity: sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==} + engines: {node: '>=6'} hasBin: true '@humanwhocodes/config-array@0.13.0': - resolution: - { - integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==, - } - engines: { node: '>=10.10.0' } + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': - resolution: - { - integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, - } - engines: { node: '>=12.22' } + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} '@humanwhocodes/object-schema@2.0.3': - resolution: - { - integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==, - } + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead '@isaacs/cliui@8.0.2': - resolution: - { - integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} '@istanbuljs/load-nyc-config@1.1.0': - resolution: - { - integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} '@istanbuljs/schema@0.1.3': - resolution: - { - integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} '@jest/console@30.2.0': - resolution: - { - integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/core@30.2.0': - resolution: - { - integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -2841,18 +1859,12 @@ packages: optional: true '@jest/diff-sequences@30.0.1': - resolution: - { - integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/environment-jsdom-abstract@30.3.0': - resolution: - { - integrity: sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 jsdom: '*' @@ -2861,74 +1873,44 @@ packages: optional: true '@jest/environment@30.2.0': - resolution: - { - integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/environment@30.3.0': - resolution: - { - integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/expect-utils@30.2.0': - resolution: - { - integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/expect@30.2.0': - resolution: - { - integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/fake-timers@30.2.0': - resolution: - { - integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/fake-timers@30.3.0': - resolution: - { - integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/get-type@30.1.0': - resolution: - { - integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/globals@30.2.0': - resolution: - { - integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/pattern@30.0.1': - resolution: - { - integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/reporters@30.2.0': - resolution: - { - integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -2936,588 +1918,333 @@ packages: optional: true '@jest/schemas@29.6.3': - resolution: - { - integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jest/schemas@30.0.5': - resolution: - { - integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/snapshot-utils@30.2.0': - resolution: - { - integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/source-map@30.0.1': - resolution: - { - integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/test-result@30.2.0': - resolution: - { - integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/test-sequencer@30.2.0': - resolution: - { - integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/transform@30.2.0': - resolution: - { - integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@29.6.3': - resolution: - { - integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jest/types@30.2.0': - resolution: - { - integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@30.3.0': - resolution: - { - integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jridgewell/gen-mapping@0.3.13': - resolution: - { - integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, - } + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/gen-mapping@0.3.3': - resolution: - { - integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} '@jridgewell/gen-mapping@0.3.5': - resolution: - { - integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} '@jridgewell/remapping@2.3.5': - resolution: - { - integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, - } + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} '@jridgewell/set-array@1.1.2': - resolution: - { - integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} '@jridgewell/set-array@1.2.1': - resolution: - { - integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} '@jridgewell/source-map@0.3.11': - resolution: - { - integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==, - } + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.5.5': - resolution: - { - integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, - } + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.31': - resolution: - { - integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, - } + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jsonjoy.com/base64@1.1.2': - resolution: - { - integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==, - } - engines: { node: '>=10.0' } + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} peerDependencies: tslib: '2' '@jsonjoy.com/json-pack@1.0.4': - resolution: - { - integrity: sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==, - } - engines: { node: '>=10.0' } + resolution: {integrity: sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==} + engines: {node: '>=10.0'} peerDependencies: tslib: '2' '@jsonjoy.com/util@1.2.0': - resolution: - { - integrity: sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==, - } - engines: { node: '>=10.0' } + resolution: {integrity: sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==} + engines: {node: '>=10.0'} peerDependencies: tslib: '2' '@kurkle/color@0.3.4': - resolution: - { - integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==, - } + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} '@mdi/js@7.4.47': - resolution: - { - integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==, - } + resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} '@napi-rs/wasm-runtime@0.2.12': - resolution: - { - integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==, - } + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - resolution: - { - integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==, - } + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} '@nodelib/fs.scandir@2.1.5': - resolution: - { - integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} '@nodelib/fs.stat@2.0.5': - resolution: - { - integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} '@nodelib/fs.walk@1.2.8': - resolution: - { - integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} '@npmcli/fs@1.1.1': - resolution: - { - integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==, - } + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} '@npmcli/move-file@1.1.2': - resolution: - { - integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs '@nuxt/babel-preset-app@2.18.1': - resolution: - { - integrity: sha512-7AYAGVjykrvta7k+koMGbt6y6PTMwl74PX2i9Ubyc1VC9ewy9U/b6cW0gVJOR/ZJWPzaABAgVZC7N58PprUDfA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-7AYAGVjykrvta7k+koMGbt6y6PTMwl74PX2i9Ubyc1VC9ewy9U/b6cW0gVJOR/ZJWPzaABAgVZC7N58PprUDfA==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/builder@2.18.1': - resolution: - { - integrity: sha512-hc4AUP3Nvov7jL0BEP7jFXt8zOfa6gt+y1kyoVvU1WHEVNcWnrGtRKvJuCwi1IwCVlx7Weh+luvHI4nzQwEeKg==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-hc4AUP3Nvov7jL0BEP7jFXt8zOfa6gt+y1kyoVvU1WHEVNcWnrGtRKvJuCwi1IwCVlx7Weh+luvHI4nzQwEeKg==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/cli@2.18.1': - resolution: - { - integrity: sha512-ZOoDlE4Fw1Cum6oG8DVnb7B4ivovXySxdDI8vnIt49Ypx22pBGt5y2ErF7g+5TAxGMIHpyh7peJWJwYp88PqPA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-ZOoDlE4Fw1Cum6oG8DVnb7B4ivovXySxdDI8vnIt49Ypx22pBGt5y2ErF7g+5TAxGMIHpyh7peJWJwYp88PqPA==} + engines: {node: ^14.18.0 || >=16.10.0} hasBin: true '@nuxt/components@2.2.1': - resolution: - { - integrity: sha512-r1LHUzifvheTnJtYrMuA+apgsrEJbxcgFKIimeXKb+jl8TnPWdV3egmrxBCaDJchrtY/wmHyP47tunsft7AWwg==, - } + resolution: {integrity: sha512-r1LHUzifvheTnJtYrMuA+apgsrEJbxcgFKIimeXKb+jl8TnPWdV3egmrxBCaDJchrtY/wmHyP47tunsft7AWwg==} peerDependencies: consola: '*' '@nuxt/config@2.18.1': - resolution: - { - integrity: sha512-CTsUMFtNCJ6+7AkgMRz53zM9vxmsMYVJWBQOnikVzwFxm/jsWzjyXkp3pQb5/fNZuqR7qXmpUKIRtrdeUeN4JQ==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-CTsUMFtNCJ6+7AkgMRz53zM9vxmsMYVJWBQOnikVzwFxm/jsWzjyXkp3pQb5/fNZuqR7qXmpUKIRtrdeUeN4JQ==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/core@2.18.1': - resolution: - { - integrity: sha512-BFnKVH7caEdDrK04qQ2U9F4Rf4hV/BqqXBJiIeHp7vM9CLKjTL5/yhiognDw3SBefmSJkpOATx1HJl3XM8c4fg==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-BFnKVH7caEdDrK04qQ2U9F4Rf4hV/BqqXBJiIeHp7vM9CLKjTL5/yhiognDw3SBefmSJkpOATx1HJl3XM8c4fg==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/devalue@2.0.2': - resolution: - { - integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==, - } + resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==} '@nuxt/friendly-errors-webpack-plugin@2.6.0': - resolution: - { - integrity: sha512-3IZj6MXbzlvUxDncAxgBMLQwGPY/JlNhy2i+AGyOHCAReR5HcBxYjVRBvyaKM9R3s5k4OODYKeHAbrToZH/47w==, - } - engines: { node: '>=14.18.0', npm: '>=5.0.0' } + resolution: {integrity: sha512-3IZj6MXbzlvUxDncAxgBMLQwGPY/JlNhy2i+AGyOHCAReR5HcBxYjVRBvyaKM9R3s5k4OODYKeHAbrToZH/47w==} + engines: {node: '>=14.18.0', npm: '>=5.0.0'} peerDependencies: webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 '@nuxt/generator@2.18.1': - resolution: - { - integrity: sha512-kZMfB5Ymvd/5ek+xfk2svQiMJWEAjZf5XNFTG+2WiNsitHb01Bo3W2QGidy+dwfuLtHoiOJkMovRlyAKWxTohg==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-kZMfB5Ymvd/5ek+xfk2svQiMJWEAjZf5XNFTG+2WiNsitHb01Bo3W2QGidy+dwfuLtHoiOJkMovRlyAKWxTohg==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/kit@3.12.2': - resolution: - { - integrity: sha512-5kOqEzfc3FsAncjK2je7vuq4/QsR5ypViTnop52mlFLf0Ku1NMCrWCSWYowAh4P0yqTACMAZYa+HdRZHscU84g==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-5kOqEzfc3FsAncjK2je7vuq4/QsR5ypViTnop52mlFLf0Ku1NMCrWCSWYowAh4P0yqTACMAZYa+HdRZHscU84g==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/kit@3.7.4': - resolution: - { - integrity: sha512-/S5abZL62BITCvC/TY3KWA6N721U1Osln3cQdBb56XHIeafZCBVqTi92Xb0o7ovl72mMRhrKwRu7elzvz9oT/g==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-/S5abZL62BITCvC/TY3KWA6N721U1Osln3cQdBb56XHIeafZCBVqTi92Xb0o7ovl72mMRhrKwRu7elzvz9oT/g==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/loading-screen@2.0.4': - resolution: - { - integrity: sha512-xpEDAoRu75tLUYCkUJCIvJkWJSuwr8pqomvQ+fkXpSrkxZ/9OzlBFjAbVdOAWTMj4aV/LVQso4vcEdircKeFIQ==, - } + resolution: {integrity: sha512-xpEDAoRu75tLUYCkUJCIvJkWJSuwr8pqomvQ+fkXpSrkxZ/9OzlBFjAbVdOAWTMj4aV/LVQso4vcEdircKeFIQ==} '@nuxt/opencollective@0.4.0': - resolution: - { - integrity: sha512-uUsxOcO2lFeotV+BGOwNLeau+U17mhpaCRhE7v8nJLdWJ2iErQXadl28HaHe6btuT8RD0LDSpvwCiKrHznDxUA==, - } - engines: { node: '>=8.0.0', npm: '>=5.0.0' } + resolution: {integrity: sha512-uUsxOcO2lFeotV+BGOwNLeau+U17mhpaCRhE7v8nJLdWJ2iErQXadl28HaHe6btuT8RD0LDSpvwCiKrHznDxUA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true '@nuxt/schema@3.12.2': - resolution: - { - integrity: sha512-IRBuOEPOIe1CANKnO2OUiqZ1Hp/0htPkLaigK7WT6ef/SdIFZUd68Tqqejqy2AFrbgU9G80k3U7eg2XUdaiQlQ==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-IRBuOEPOIe1CANKnO2OUiqZ1Hp/0htPkLaigK7WT6ef/SdIFZUd68Tqqejqy2AFrbgU9G80k3U7eg2XUdaiQlQ==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/schema@3.7.4': - resolution: - { - integrity: sha512-q6js+97vDha4Fa2x2kDVEuokJr+CGIh1TY2wZp2PLZ7NhG3XEeib7x9Hq8XE8B6pD0GKBRy3eRPPOY69gekBCw==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-q6js+97vDha4Fa2x2kDVEuokJr+CGIh1TY2wZp2PLZ7NhG3XEeib7x9Hq8XE8B6pD0GKBRy3eRPPOY69gekBCw==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/server@2.18.1': - resolution: - { - integrity: sha512-4GHmgi1NS6uCL+3QzlxmHmEoKkejQKTDrKPtA16w8iw/8EBgCrAkvXukcIMxF7Of+IYi1I/duVmCyferxo7jyw==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-4GHmgi1NS6uCL+3QzlxmHmEoKkejQKTDrKPtA16w8iw/8EBgCrAkvXukcIMxF7Of+IYi1I/duVmCyferxo7jyw==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/telemetry@1.5.0': - resolution: - { - integrity: sha512-MhxiiYCFe0MayN2TvmpcsCV66zBePtrSVkFLJHwTFuneQ5Qma5x0NmCwdov7O4NSuTfgSZels9qPJh0zy0Kc4g==, - } + resolution: {integrity: sha512-MhxiiYCFe0MayN2TvmpcsCV66zBePtrSVkFLJHwTFuneQ5Qma5x0NmCwdov7O4NSuTfgSZels9qPJh0zy0Kc4g==} hasBin: true '@nuxt/types@2.18.1': - resolution: - { - integrity: sha512-PpReoV9oHCnSpB9WqemTUWmlH1kqFHC3Xe5LH904VvCl/3xLO2nGYcrHeZCMV5hXNWsDUyqDnd/2cQHmeqj5lA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-PpReoV9oHCnSpB9WqemTUWmlH1kqFHC3Xe5LH904VvCl/3xLO2nGYcrHeZCMV5hXNWsDUyqDnd/2cQHmeqj5lA==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/typescript-build@3.0.2': - resolution: - { - integrity: sha512-IFSznjafW5xm0XHg9Q9aHVW7i9J2pAYfyorh3ro3Pf0OnCbS0acmwBnp2juza+DqNhZa1DhNentmUsgiYp730g==, - } + resolution: {integrity: sha512-IFSznjafW5xm0XHg9Q9aHVW7i9J2pAYfyorh3ro3Pf0OnCbS0acmwBnp2juza+DqNhZa1DhNentmUsgiYp730g==} peerDependencies: '@nuxt/types': '>=2.13.1' typescript: 4.x || 5.x '@nuxt/ui-templates@1.3.1': - resolution: - { - integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==, - } + resolution: {integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==} '@nuxt/utils@2.18.1': - resolution: - { - integrity: sha512-aWeB8VMhtymo5zXUiQaohCu8IqJqENF9iCag3wyJpdhpNDVoghGUJAl0F6mQvNTJgQzseFtf4XKqTfvcgVzyGg==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-aWeB8VMhtymo5zXUiQaohCu8IqJqENF9iCag3wyJpdhpNDVoghGUJAl0F6mQvNTJgQzseFtf4XKqTfvcgVzyGg==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/vue-app@2.18.1': - resolution: - { - integrity: sha512-yxkunoTv6EVa42xM7qES0N1DNMo4UbP/s89L7HjqngQ4KzVWyyzK0qqJ9u3Gu4CabXhHFSquu11gtn+dylKyTA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-yxkunoTv6EVa42xM7qES0N1DNMo4UbP/s89L7HjqngQ4KzVWyyzK0qqJ9u3Gu4CabXhHFSquu11gtn+dylKyTA==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/vue-renderer@2.18.1': - resolution: - { - integrity: sha512-Nl8/IbV+sTEWCczHKcjLbZrFO6y5fCcFxZwd6Opatcbr2z380abwpDf3a9UjnVW3wPEM+/xoy1/MBCLY3VmWcw==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-Nl8/IbV+sTEWCczHKcjLbZrFO6y5fCcFxZwd6Opatcbr2z380abwpDf3a9UjnVW3wPEM+/xoy1/MBCLY3VmWcw==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxt/webpack@2.18.1': - resolution: - { - integrity: sha512-6EqbIoheLAJ0E7dfQB5ftOKL4d74N98dFMY3q89QTaoS9VXBFB5D1MLd27WuyfhChmzuHRwHfjaBW8QFdhjwew==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-6EqbIoheLAJ0E7dfQB5ftOKL4d74N98dFMY3q89QTaoS9VXBFB5D1MLd27WuyfhChmzuHRwHfjaBW8QFdhjwew==} + engines: {node: ^14.18.0 || >=16.10.0} '@nuxtjs/dotenv@1.4.2': - resolution: - { - integrity: sha512-/3+Cw5qLNIQD8ZvXLJG1suxvfC4ltlUuYegOwirHrLrzptHh/+rCkBXrNbrz2qAiwc+/yK91XjZGGzNM1dFmCw==, - } + resolution: {integrity: sha512-/3+Cw5qLNIQD8ZvXLJG1suxvfC4ltlUuYegOwirHrLrzptHh/+rCkBXrNbrz2qAiwc+/yK91XjZGGzNM1dFmCw==} '@nuxtjs/eslint-config-typescript@12.1.0': - resolution: - { - integrity: sha512-l2fLouDYwdAvCZEEw7wGxOBj+i8TQcHFu3zMPTLqKuv1qu6WcZIr0uztkbaa8ND1uKZ9YPqKx6UlSOjM4Le69Q==, - } + resolution: {integrity: sha512-l2fLouDYwdAvCZEEw7wGxOBj+i8TQcHFu3zMPTLqKuv1qu6WcZIr0uztkbaa8ND1uKZ9YPqKx6UlSOjM4Le69Q==} peerDependencies: eslint: ^8.48.0 '@nuxtjs/eslint-config@12.0.0': - resolution: - { - integrity: sha512-ewenelo75x0eYEUK+9EBXjc/OopQCvdkmYmlZuoHq5kub/vtiRpyZ/autppwokpHUq8tiVyl2ejMakoiHiDTrg==, - } + resolution: {integrity: sha512-ewenelo75x0eYEUK+9EBXjc/OopQCvdkmYmlZuoHq5kub/vtiRpyZ/autppwokpHUq8tiVyl2ejMakoiHiDTrg==} peerDependencies: eslint: ^8.23.0 '@nuxtjs/eslint-module@4.1.0': - resolution: - { - integrity: sha512-lW9ozEjOrnU8Uot3GOAZ/0ThNAds0d6UAp9n46TNxcTvH/MOcAggGbMNs16c0HYT2HlyPQvXORCHQ5+9p87mmw==, - } + resolution: {integrity: sha512-lW9ozEjOrnU8Uot3GOAZ/0ThNAds0d6UAp9n46TNxcTvH/MOcAggGbMNs16c0HYT2HlyPQvXORCHQ5+9p87mmw==} peerDependencies: eslint: '>=7' '@nuxtjs/firebase@8.2.2': - resolution: - { - integrity: sha512-j+kW0utwq23w71D0I4RyOc9/eYGe8WpsoI2GD9PT744rMWmj4MFHASjmgyDPk2KdZGxsknxUW6yq29aLd0E2ow==, - } + resolution: {integrity: sha512-j+kW0utwq23w71D0I4RyOc9/eYGe8WpsoI2GD9PT744rMWmj4MFHASjmgyDPk2KdZGxsknxUW6yq29aLd0E2ow==} peerDependencies: firebase: ^9.6.2 nuxt: ^2.15.6 '@nuxtjs/sitemap@2.4.0': - resolution: - { - integrity: sha512-TVgIYOtPp7KAfaUo76WRpGbO20j4D/xi/A7shFIGjARHs+FvfAWXNCtBT87dTwe/RoYzAsEKtijFFUTaSu5bUA==, - } - engines: { node: '>=8.9.0', npm: '>=5.0.0' } + resolution: {integrity: sha512-TVgIYOtPp7KAfaUo76WRpGbO20j4D/xi/A7shFIGjARHs+FvfAWXNCtBT87dTwe/RoYzAsEKtijFFUTaSu5bUA==} + engines: {node: '>=8.9.0', npm: '>=5.0.0'} '@nuxtjs/stylelint-module@5.2.0': - resolution: - { - integrity: sha512-CMGZORt5fM1pK+5Xj3p2uajkK9DZ9Sja7jewXa8LZFNMjt7GIsKaoAvH4poCUMorhIVBS0lGQZ9BlRmg3MWxvg==, - } + resolution: {integrity: sha512-CMGZORt5fM1pK+5Xj3p2uajkK9DZ9Sja7jewXa8LZFNMjt7GIsKaoAvH4poCUMorhIVBS0lGQZ9BlRmg3MWxvg==} peerDependencies: stylelint: '>=13' '@nuxtjs/vuetify@1.12.3': - resolution: - { - integrity: sha512-6uVL3cfESMB00eVjJTNkyU4jvuPTGPn1yteo7lQTH6v+fxHcPaOgvzVYHIKSHIz1DecuOiB5c9b+YjsRP5+C8A==, - } + resolution: {integrity: sha512-6uVL3cfESMB00eVjJTNkyU4jvuPTGPn1yteo7lQTH6v+fxHcPaOgvzVYHIKSHIz1DecuOiB5c9b+YjsRP5+C8A==} '@nuxtjs/youch@4.2.3': - resolution: - { - integrity: sha512-XiTWdadTwtmL/IGkNqbVe+dOlT+IMvcBu7TvKI7plWhVQeBCQ9iKhk3jgvVWFyiwL2yHJDlEwOM5v9oVES5Xmw==, - } + resolution: {integrity: sha512-XiTWdadTwtmL/IGkNqbVe+dOlT+IMvcBu7TvKI7plWhVQeBCQ9iKhk3jgvVWFyiwL2yHJDlEwOM5v9oVES5Xmw==} '@one-ini/wasm@0.1.1': - resolution: - { - integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==, - } + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} '@panva/asn1.js@1.0.0': - resolution: - { - integrity: sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==} + engines: {node: '>=10.13.0'} '@pkgjs/parseargs@0.11.0': - resolution: - { - integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} '@pkgr/core@0.2.9': - resolution: - { - integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==, - } - engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@polka/url@1.0.0-next.23': - resolution: - { - integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==, - } + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} '@protobufjs/aspromise@1.1.2': - resolution: - { - integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==, - } + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} '@protobufjs/base64@1.1.2': - resolution: - { - integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==, - } + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} '@protobufjs/codegen@2.0.4': - resolution: - { - integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==, - } + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} '@protobufjs/codegen@2.0.5': - resolution: - { - integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==, - } + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} '@protobufjs/eventemitter@1.1.0': - resolution: - { - integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==, - } + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} '@protobufjs/fetch@1.1.0': - resolution: - { - integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==, - } + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} '@protobufjs/float@1.0.2': - resolution: - { - integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==, - } + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} '@protobufjs/inquire@1.1.0': - resolution: - { - integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==, - } + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} '@protobufjs/inquire@1.1.1': - resolution: - { - integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==, - } + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} '@protobufjs/path@1.1.2': - resolution: - { - integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==, - } + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} '@protobufjs/pool@1.1.0': - resolution: - { - integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==, - } + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} '@protobufjs/utf8@1.1.0': - resolution: - { - integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==, - } + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} '@protobufjs/utf8@1.1.1': - resolution: - { - integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==, - } + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} '@rollup/pluginutils@4.2.1': - resolution: - { - integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==, - } - engines: { node: '>= 8.0.0' } + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} '@rollup/pluginutils@5.0.4': - resolution: - { - integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0 peerDependenciesMeta: @@ -3525,11 +2252,8 @@ packages: optional: true '@rollup/pluginutils@5.1.0': - resolution: - { - integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: @@ -3537,429 +2261,219 @@ packages: optional: true '@simple-libs/stream-utils@1.2.0': - resolution: - { - integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} '@sinclair/typebox@0.27.8': - resolution: - { - integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, - } + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} '@sinclair/typebox@0.34.41': - resolution: - { - integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==, - } + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} '@sindresorhus/merge-streams@2.3.0': - resolution: - { - integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} '@sinonjs/commons@3.0.1': - resolution: - { - integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==, - } + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} '@sinonjs/fake-timers@13.0.5': - resolution: - { - integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==, - } + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} '@sinonjs/fake-timers@15.3.2': - resolution: - { - integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==, - } + resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==} '@tootallnate/once@2.0.0': - resolution: - { - integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==, - } - engines: { node: '>= 10' } + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} '@trysound/sax@0.2.0': - resolution: - { - integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} '@tybys/wasm-util@0.10.1': - resolution: - { - integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, - } + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/babel__core@7.20.5': - resolution: - { - integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, - } + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} '@types/babel__generator@7.27.0': - resolution: - { - integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, - } + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} '@types/babel__template@7.4.4': - resolution: - { - integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, - } + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} '@types/babel__traverse@7.28.0': - resolution: - { - integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, - } + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/body-parser@1.19.3': - resolution: - { - integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==, - } + resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} '@types/compression@1.7.5': - resolution: - { - integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==, - } + resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==} '@types/connect@3.4.38': - resolution: - { - integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, - } + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/eslint-scope@3.7.7': - resolution: - { - integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==, - } + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} '@types/eslint@8.44.3': - resolution: - { - integrity: sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==, - } + resolution: {integrity: sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==} '@types/eslint@9.6.1': - resolution: - { - integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==, - } + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} '@types/estree@1.0.8': - resolution: - { - integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, - } + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/etag@1.8.3': - resolution: - { - integrity: sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==, - } + resolution: {integrity: sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==} '@types/express-serve-static-core@4.17.37': - resolution: - { - integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==, - } + resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} '@types/express@4.17.18': - resolution: - { - integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==, - } + resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} '@types/file-loader@5.0.4': - resolution: - { - integrity: sha512-aB4X92oi5D2nIGI8/kolnJ47btRM2MQjQS4eJgA/VnCD12x0+kP5v7b5beVQWKHLOcquwUXvv6aMt8PmMy9uug==, - } + resolution: {integrity: sha512-aB4X92oi5D2nIGI8/kolnJ47btRM2MQjQS4eJgA/VnCD12x0+kP5v7b5beVQWKHLOcquwUXvv6aMt8PmMy9uug==} '@types/html-minifier-terser@5.1.2': - resolution: - { - integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==, - } + resolution: {integrity: sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==} '@types/html-minifier-terser@7.0.2': - resolution: - { - integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==, - } + resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} '@types/http-errors@2.0.4': - resolution: - { - integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==, - } + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} '@types/istanbul-lib-coverage@2.0.6': - resolution: - { - integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==, - } + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} '@types/istanbul-lib-report@3.0.3': - resolution: - { - integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==, - } + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} '@types/istanbul-reports@3.0.4': - resolution: - { - integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==, - } + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} '@types/jsdom@21.1.7': - resolution: - { - integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==, - } + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} '@types/json-schema@7.0.15': - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': - resolution: - { - integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==, - } + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} '@types/jsonwebtoken@8.5.9': - resolution: - { - integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==, - } + resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} '@types/less@3.0.6': - resolution: - { - integrity: sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw==, - } + resolution: {integrity: sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw==} '@types/long@4.0.2': - resolution: - { - integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==, - } + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} '@types/mime@1.3.3': - resolution: - { - integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==, - } + resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} '@types/minimist@1.2.3': - resolution: - { - integrity: sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==, - } + resolution: {integrity: sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==} '@types/node@12.20.55': - resolution: - { - integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==, - } + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} '@types/node@16.18.55': - resolution: - { - integrity: sha512-Y1zz/LIuJek01+hlPNzzXQhmq/Z2BCP96j18MSXC0S0jSu/IG4FFxmBs7W4/lI2vPJ7foVfEB0hUVtnOjnCiTg==, - } + resolution: {integrity: sha512-Y1zz/LIuJek01+hlPNzzXQhmq/Z2BCP96j18MSXC0S0jSu/IG4FFxmBs7W4/lI2vPJ7foVfEB0hUVtnOjnCiTg==} '@types/node@25.1.0': - resolution: - { - integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==, - } + resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} '@types/node@25.6.0': - resolution: - { - integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==, - } + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/normalize-package-data@2.4.2': - resolution: - { - integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==, - } + resolution: {integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==} '@types/optimize-css-assets-webpack-plugin@5.0.8': - resolution: - { - integrity: sha512-n134DdmRVXTy0KKbgg3A/G02r2XJKJicYzbJYhdIO8rdYdzoMv6GNHjog2Oq1ttaCOhsYcPIA6Sn7eFxEGCM1A==, - } + resolution: {integrity: sha512-n134DdmRVXTy0KKbgg3A/G02r2XJKJicYzbJYhdIO8rdYdzoMv6GNHjog2Oq1ttaCOhsYcPIA6Sn7eFxEGCM1A==} '@types/parse-json@4.0.0': - resolution: - { - integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==, - } + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} '@types/pug@2.0.10': - resolution: - { - integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==, - } + resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} '@types/qrcode@1.5.6': - resolution: - { - integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==, - } + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} '@types/qs@6.9.8': - resolution: - { - integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==, - } + resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==} '@types/range-parser@1.2.5': - resolution: - { - integrity: sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==, - } + resolution: {integrity: sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==} '@types/sax@1.2.7': - resolution: - { - integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==, - } + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} '@types/semver@7.5.3': - resolution: - { - integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==, - } + resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} '@types/send@0.17.2': - resolution: - { - integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==, - } + resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} '@types/serve-static@1.15.7': - resolution: - { - integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==, - } + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} '@types/source-list-map@0.1.3': - resolution: - { - integrity: sha512-I9R/7fUjzUOyDy6AFkehCK711wWoAXEaBi80AfjZt1lIkbe6AcXKd3ckQc3liMvQExWvfOeh/8CtKzrfUFN5gA==, - } + resolution: {integrity: sha512-I9R/7fUjzUOyDy6AFkehCK711wWoAXEaBi80AfjZt1lIkbe6AcXKd3ckQc3liMvQExWvfOeh/8CtKzrfUFN5gA==} '@types/stack-utils@2.0.3': - resolution: - { - integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==, - } + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} '@types/strip-bom@3.0.0': - resolution: - { - integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==, - } + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} '@types/strip-json-comments@0.0.30': - resolution: - { - integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==, - } + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} '@types/tapable@1.0.9': - resolution: - { - integrity: sha512-fOHIwZua0sRltqWzODGUM6b4ffZrf/vzGUmNXdR+4DzuJP42PMbM5dLKcdzlYvv8bMJ3GALOzkk1q7cDm2zPyA==, - } + resolution: {integrity: sha512-fOHIwZua0sRltqWzODGUM6b4ffZrf/vzGUmNXdR+4DzuJP42PMbM5dLKcdzlYvv8bMJ3GALOzkk1q7cDm2zPyA==} '@types/terser-webpack-plugin@4.2.1': - resolution: - { - integrity: sha512-x688KsgQKJF8PPfv4qSvHQztdZNHLlWJdolN9/ptAGimHVy3rY+vHdfglQDFh1Z39h7eMWOd6fQ7ke3PKQcdyA==, - } + resolution: {integrity: sha512-x688KsgQKJF8PPfv4qSvHQztdZNHLlWJdolN9/ptAGimHVy3rY+vHdfglQDFh1Z39h7eMWOd6fQ7ke3PKQcdyA==} '@types/tough-cookie@4.0.5': - resolution: - { - integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==, - } + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} '@types/uglify-js@3.17.2': - resolution: - { - integrity: sha512-9SjrHO54LINgC/6Ehr81NjAxAYvwEZqjUHLjJYvC4Nmr9jbLQCIZbWSvl4vXQkkmR1UAuaKDycau3O1kWGFyXQ==, - } + resolution: {integrity: sha512-9SjrHO54LINgC/6Ehr81NjAxAYvwEZqjUHLjJYvC4Nmr9jbLQCIZbWSvl4vXQkkmR1UAuaKDycau3O1kWGFyXQ==} '@types/webpack-bundle-analyzer@3.9.5': - resolution: - { - integrity: sha512-QlyDyX7rsOIJHASzXWlih8DT9fR+XCG9cwIV/4pKrtScdHv4XFshdEf/7iiqLqG0lzWcoBdzG8ylMHQ5XLNixw==, - } + resolution: {integrity: sha512-QlyDyX7rsOIJHASzXWlih8DT9fR+XCG9cwIV/4pKrtScdHv4XFshdEf/7iiqLqG0lzWcoBdzG8ylMHQ5XLNixw==} '@types/webpack-hot-middleware@2.25.5': - resolution: - { - integrity: sha512-/eRWWMgZteNzl17qLCRdRmtKPZuWy984b11Igz9+BAU5a99Hc2AJinnMohMPVahGRSHby4XwsnjlgIt9m0Ce3g==, - } + resolution: {integrity: sha512-/eRWWMgZteNzl17qLCRdRmtKPZuWy984b11Igz9+BAU5a99Hc2AJinnMohMPVahGRSHby4XwsnjlgIt9m0Ce3g==} '@types/webpack-sources@3.2.1': - resolution: - { - integrity: sha512-iLC3Fsx62ejm3ST3PQ8vBMC54Rb3EoCprZjeJGI5q+9QjfDLGt9jeg/k245qz1G9AQnORGk0vqPicJFPT1QODQ==, - } + resolution: {integrity: sha512-iLC3Fsx62ejm3ST3PQ8vBMC54Rb3EoCprZjeJGI5q+9QjfDLGt9jeg/k245qz1G9AQnORGk0vqPicJFPT1QODQ==} '@types/webpack@4.41.38': - resolution: - { - integrity: sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==, - } + resolution: {integrity: sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==} '@types/yargs-parser@21.0.3': - resolution: - { - integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==, - } + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.33': - resolution: - { - integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==, - } + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} '@types/yargs@17.0.35': - resolution: - { - integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==, - } + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} '@typescript-eslint/eslint-plugin@6.7.3': - resolution: - { - integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha eslint: ^7.0.0 || ^8.0.0 @@ -3969,11 +2483,8 @@ packages: optional: true '@typescript-eslint/parser@6.7.3': - resolution: - { - integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: '*' @@ -3982,18 +2493,12 @@ packages: optional: true '@typescript-eslint/scope-manager@6.7.3': - resolution: - { - integrity: sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==} + engines: {node: ^16.0.0 || >=18.0.0} '@typescript-eslint/type-utils@6.7.3': - resolution: - { - integrity: sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: '*' @@ -4002,18 +2507,12 @@ packages: optional: true '@typescript-eslint/types@6.7.3': - resolution: - { - integrity: sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==} + engines: {node: ^16.0.0 || >=18.0.0} '@typescript-eslint/typescript-estree@6.7.3': - resolution: - { - integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4021,206 +2520,128 @@ packages: optional: true '@typescript-eslint/utils@6.7.3': - resolution: - { - integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 '@typescript-eslint/visitor-keys@6.7.3': - resolution: - { - integrity: sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==, - } - engines: { node: ^16.0.0 || >=18.0.0 } + resolution: {integrity: sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==} + engines: {node: ^16.0.0 || >=18.0.0} '@ungap/structured-clone@1.2.0': - resolution: - { - integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==, - } + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@ungap/structured-clone@1.3.0': - resolution: - { - integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==, - } + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: - { - integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==, - } + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] os: [android] '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: - { - integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==, - } + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} cpu: [arm64] os: [android] '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: - { - integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==, - } + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} cpu: [arm64] os: [darwin] '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: - { - integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==, - } + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} cpu: [x64] os: [darwin] '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: - { - integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==, - } + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} cpu: [x64] os: [freebsd] '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: - { - integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==, - } + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} cpu: [arm] os: [linux] '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: - { - integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==, - } + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} cpu: [arm] os: [linux] '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: - { - integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==, - } + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: - { - integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==, - } + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: - { - integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==, - } + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: - { - integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==, - } + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: - { - integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==, - } + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: - { - integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==, - } + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: - { - integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==, - } + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: - { - integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==, - } + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: - { - integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} cpu: [wasm32] '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: - { - integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==, - } + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} cpu: [arm64] os: [win32] '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: - { - integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==, - } + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} cpu: [ia32] os: [win32] '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: - { - integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==, - } + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} cpu: [x64] os: [win32] '@vue/babel-helper-vue-jsx-merge-props@1.4.0': - resolution: - { - integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==, - } + resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==} '@vue/babel-plugin-transform-vue-jsx@1.4.0': - resolution: - { - integrity: sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==, - } + resolution: {integrity: sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-preset-jsx@1.4.0': - resolution: - { - integrity: sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==, - } + resolution: {integrity: sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==} peerDependencies: '@babel/core': ^7.0.0-0 vue: '*' @@ -4229,378 +2650,207 @@ packages: optional: true '@vue/babel-sugar-composition-api-inject-h@1.4.0': - resolution: - { - integrity: sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==, - } + resolution: {integrity: sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-sugar-composition-api-render-instance@1.4.0': - resolution: - { - integrity: sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==, - } + resolution: {integrity: sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-sugar-functional-vue@1.4.0': - resolution: - { - integrity: sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==, - } + resolution: {integrity: sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-sugar-inject-h@1.4.0': - resolution: - { - integrity: sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==, - } + resolution: {integrity: sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-sugar-v-model@1.4.0': - resolution: - { - integrity: sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==, - } + resolution: {integrity: sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/babel-sugar-v-on@1.4.0': - resolution: - { - integrity: sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==, - } + resolution: {integrity: sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==} peerDependencies: '@babel/core': ^7.0.0-0 '@vue/compiler-sfc@2.7.16': - resolution: - { - integrity: sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==, - } + resolution: {integrity: sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==} '@vue/component-compiler-utils@3.3.0': - resolution: - { - integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==, - } + resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==} '@vue/test-utils@1.3.6': - resolution: - { - integrity: sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==, - } + resolution: {integrity: sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==} peerDependencies: vue: 2.x vue-template-compiler: ^2.x '@webassemblyjs/ast@1.14.1': - resolution: - { - integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==, - } + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} '@webassemblyjs/ast@1.9.0': - resolution: - { - integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==, - } + resolution: {integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==} '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: - { - integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==, - } + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} '@webassemblyjs/floating-point-hex-parser@1.9.0': - resolution: - { - integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==, - } + resolution: {integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==} '@webassemblyjs/helper-api-error@1.13.2': - resolution: - { - integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==, - } + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} '@webassemblyjs/helper-api-error@1.9.0': - resolution: - { - integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==, - } + resolution: {integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==} '@webassemblyjs/helper-buffer@1.14.1': - resolution: - { - integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==, - } + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} '@webassemblyjs/helper-buffer@1.9.0': - resolution: - { - integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==, - } + resolution: {integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==} '@webassemblyjs/helper-code-frame@1.9.0': - resolution: - { - integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==, - } + resolution: {integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==} '@webassemblyjs/helper-fsm@1.9.0': - resolution: - { - integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==, - } + resolution: {integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==} '@webassemblyjs/helper-module-context@1.9.0': - resolution: - { - integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==, - } + resolution: {integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==} '@webassemblyjs/helper-numbers@1.13.2': - resolution: - { - integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==, - } + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: - { - integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==, - } + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} '@webassemblyjs/helper-wasm-bytecode@1.9.0': - resolution: - { - integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==, - } + resolution: {integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==} '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: - { - integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==, - } + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} '@webassemblyjs/helper-wasm-section@1.9.0': - resolution: - { - integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==, - } + resolution: {integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==} '@webassemblyjs/ieee754@1.13.2': - resolution: - { - integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==, - } + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} '@webassemblyjs/ieee754@1.9.0': - resolution: - { - integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==, - } + resolution: {integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==} '@webassemblyjs/leb128@1.13.2': - resolution: - { - integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==, - } + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} '@webassemblyjs/leb128@1.9.0': - resolution: - { - integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==, - } + resolution: {integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==} '@webassemblyjs/utf8@1.13.2': - resolution: - { - integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==, - } + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} '@webassemblyjs/utf8@1.9.0': - resolution: - { - integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==, - } + resolution: {integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==} '@webassemblyjs/wasm-edit@1.14.1': - resolution: - { - integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==, - } + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} '@webassemblyjs/wasm-edit@1.9.0': - resolution: - { - integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==, - } + resolution: {integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==} '@webassemblyjs/wasm-gen@1.14.1': - resolution: - { - integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==, - } + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} '@webassemblyjs/wasm-gen@1.9.0': - resolution: - { - integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==, - } + resolution: {integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==} '@webassemblyjs/wasm-opt@1.14.1': - resolution: - { - integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==, - } + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} '@webassemblyjs/wasm-opt@1.9.0': - resolution: - { - integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==, - } + resolution: {integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==} '@webassemblyjs/wasm-parser@1.14.1': - resolution: - { - integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==, - } + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} '@webassemblyjs/wasm-parser@1.9.0': - resolution: - { - integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==, - } + resolution: {integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==} '@webassemblyjs/wast-parser@1.9.0': - resolution: - { - integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==, - } + resolution: {integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==} '@webassemblyjs/wast-printer@1.14.1': - resolution: - { - integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==, - } + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} '@webassemblyjs/wast-printer@1.9.0': - resolution: - { - integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==, - } + resolution: {integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==} '@xtuc/ieee754@1.2.0': - resolution: - { - integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==, - } + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} '@xtuc/long@4.2.2': - resolution: - { - integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==, - } + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} abbrev@1.1.1: - resolution: - { - integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==, - } + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} abort-controller@3.0.0: - resolution: - { - integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, - } - engines: { node: '>=6.5' } + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} accepts@1.3.8: - resolution: - { - integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} acorn-import-phases@1.0.4: - resolution: - { - integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} peerDependencies: acorn: ^8.14.0 acorn-jsx@5.3.2: - resolution: - { - integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, - } + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn-walk@8.2.0: - resolution: - { - integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} acorn@6.4.2: - resolution: - { - integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} + engines: {node: '>=0.4.0'} hasBin: true acorn@8.15.0: - resolution: - { - integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} hasBin: true agent-base@6.0.2: - resolution: - { - integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==, - } - engines: { node: '>= 6.0.0' } + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} agent-base@7.1.4: - resolution: - { - integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==, - } - engines: { node: '>= 14' } + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} aggregate-error@3.1.0: - resolution: - { - integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} ajv-errors@1.0.1: - resolution: - { - integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==, - } + resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} peerDependencies: ajv: '>=5.0.0' ajv-formats@2.1.1: - resolution: - { - integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==, - } + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: @@ -4608,766 +2858,424 @@ packages: optional: true ajv-keywords@3.5.2: - resolution: - { - integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==, - } + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: ajv: ^6.9.1 ajv-keywords@5.1.0: - resolution: - { - integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==, - } + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 ajv@6.12.6: - resolution: - { - integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, - } + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} ajv@8.12.0: - resolution: - { - integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==, - } + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} ajv@8.17.1: - resolution: - { - integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==, - } + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} ansi-align@3.0.1: - resolution: - { - integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==, - } + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} ansi-escapes@4.3.2: - resolution: - { - integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} ansi-escapes@7.1.1: - resolution: - { - integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + engines: {node: '>=18'} ansi-html-community@0.0.8: - resolution: - { - integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==, - } - engines: { '0': node >= 0.8.0 } + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} hasBin: true ansi-regex@2.1.1: - resolution: - { - integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} ansi-regex@5.0.1: - resolution: - { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.1.0: - resolution: - { - integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} ansi-styles@2.2.1: - resolution: - { - integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} ansi-styles@3.2.1: - resolution: - { - integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} ansi-styles@4.3.0: - resolution: - { - integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} ansi-styles@5.2.0: - resolution: - { - integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} ansi-styles@6.2.3: - resolution: - { - integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} anymatch@2.0.0: - resolution: - { - integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==, - } + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} anymatch@3.1.3: - resolution: - { - integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} aproba@1.2.0: - resolution: - { - integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==, - } + resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} arg@4.1.3: - resolution: - { - integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==, - } + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} arg@5.0.2: - resolution: - { - integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, - } + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} argparse@1.0.10: - resolution: - { - integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, - } + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: - resolution: - { - integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, - } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} arr-diff@4.0.0: - resolution: - { - integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} arr-flatten@1.1.0: - resolution: - { - integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} arr-union@3.1.0: - resolution: - { - integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} array-buffer-byte-length@1.0.0: - resolution: - { - integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==, - } + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} array-ify@1.0.0: - resolution: - { - integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==, - } + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} array-includes@3.1.7: - resolution: - { - integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} array-union@2.1.0: - resolution: - { - integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} array-unique@0.3.2: - resolution: - { - integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} array.prototype.findlastindex@1.2.3: - resolution: - { - integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} array.prototype.flat@1.3.2: - resolution: - { - integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} array.prototype.flatmap@1.3.2: - resolution: - { - integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} array.prototype.reduce@1.0.6: - resolution: - { - integrity: sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==} + engines: {node: '>= 0.4'} arraybuffer.prototype.slice@1.0.2: - resolution: - { - integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} arrify@1.0.1: - resolution: - { - integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} arrify@2.0.1: - resolution: - { - integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} asn1.js@5.4.1: - resolution: - { - integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==, - } + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} assert@1.5.1: - resolution: - { - integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==, - } + resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} assign-symbols@1.0.0: - resolution: - { - integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} astral-regex@2.0.0: - resolution: - { - integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} async-cache@1.1.0: - resolution: - { - integrity: sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==, - } + resolution: {integrity: sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==} deprecated: No longer maintained. Use [lru-cache](http://npm.im/lru-cache) version 7.6 or higher, and provide an asynchronous `fetchMethod` option. async-each@1.0.6: - resolution: - { - integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==, - } + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} async-retry@1.3.3: - resolution: - { - integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==, - } + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} async@3.2.6: - resolution: - { - integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, - } + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: - resolution: - { - integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, - } + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} at-least-node@1.0.0: - resolution: - { - integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==, - } - engines: { node: '>= 4.0.0' } + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} atob@2.1.2: - resolution: - { - integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==, - } - engines: { node: '>= 4.5.0' } + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} hasBin: true autoprefixer@10.4.19: - resolution: - { - integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 available-typed-arrays@1.0.5: - resolution: - { - integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} axios@0.31.1: - resolution: - { - integrity: sha512-Ef8DUZSZQP6igY48mjGaoEjwhely97lserep0IFJifBH4YdKvwH5eMLniy3kig2HQoBNR8EkZpDjowxwTJcmbg==, - } + resolution: {integrity: sha512-Ef8DUZSZQP6igY48mjGaoEjwhely97lserep0IFJifBH4YdKvwH5eMLniy3kig2HQoBNR8EkZpDjowxwTJcmbg==} babel-code-frame@6.26.0: - resolution: - { - integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==, - } + resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} babel-core@7.0.0-bridge.0: - resolution: - { - integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==, - } + resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 babel-jest@30.2.0: - resolution: - { - integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-0 babel-loader@8.3.0: - resolution: - { - integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==, - } - engines: { node: '>= 8.9' } + resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} + engines: {node: '>= 8.9'} peerDependencies: '@babel/core': ^7.0.0 webpack: '>=2' babel-messages@6.23.0: - resolution: - { - integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==, - } + resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} babel-plugin-istanbul@7.0.1: - resolution: - { - integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} babel-plugin-jest-hoist@30.2.0: - resolution: - { - integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-plugin-polyfill-corejs2@0.4.11: - resolution: - { - integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==, - } + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-corejs3@0.10.4: - resolution: - { - integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==, - } + resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-regenerator@0.6.2: - resolution: - { - integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==, - } + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-transform-es2015-modules-commonjs@6.26.2: - resolution: - { - integrity: sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==, - } + resolution: {integrity: sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==} babel-plugin-transform-strict-mode@6.24.1: - resolution: - { - integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==, - } + resolution: {integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==} babel-preset-current-node-syntax@1.2.0: - resolution: - { - integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==, - } + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 babel-preset-jest@30.2.0: - resolution: - { - integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@babel/core': ^7.11.0 || ^8.0.0-beta.1 babel-runtime@6.26.0: - resolution: - { - integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==, - } + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} babel-template@6.26.0: - resolution: - { - integrity: sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==, - } + resolution: {integrity: sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==} babel-traverse@6.26.0: - resolution: - { - integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==, - } + resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} babel-types@6.26.0: - resolution: - { - integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==, - } + resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} babylon@6.18.0: - resolution: - { - integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==, - } + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} hasBin: true balanced-match@1.0.2: - resolution: - { - integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, - } + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} balanced-match@2.0.0: - resolution: - { - integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==, - } + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} base64-js@1.5.1: - resolution: - { - integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, - } + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} base@0.11.2: - resolution: - { - integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} baseline-browser-mapping@2.9.11: - resolution: - { - integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==, - } + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true big.js@5.2.2: - resolution: - { - integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==, - } + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} bignumber.js@9.1.2: - resolution: - { - integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==, - } + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} binary-extensions@1.13.1: - resolution: - { - integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} binary-extensions@2.2.0: - resolution: - { - integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} bindings@1.5.0: - resolution: - { - integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==, - } + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} bluebird@3.7.2: - resolution: - { - integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==, - } + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} bn.js@4.12.0: - resolution: - { - integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==, - } + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} bn.js@5.2.1: - resolution: - { - integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==, - } + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} boolbase@1.0.0: - resolution: - { - integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, - } + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} boxen@5.1.2: - resolution: - { - integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} brace-expansion@1.1.11: - resolution: - { - integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, - } + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} brace-expansion@2.0.1: - resolution: - { - integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==, - } + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} brace-expansion@2.0.2: - resolution: - { - integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==, - } + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} brace-expansion@2.1.0: - resolution: - { - integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==, - } + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} braces@2.3.2: - resolution: - { - integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} braces@3.0.2: - resolution: - { - integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} brorand@1.1.0: - resolution: - { - integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==, - } + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} browserify-aes@1.2.0: - resolution: - { - integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==, - } + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} browserify-cipher@1.0.1: - resolution: - { - integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==, - } + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} browserify-des@1.0.2: - resolution: - { - integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==, - } + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} browserify-rsa@4.1.0: - resolution: - { - integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==, - } + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} browserify-sign@4.2.2: - resolution: - { - integrity: sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==} + engines: {node: '>= 4'} browserify-zlib@0.2.0: - resolution: - { - integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==, - } + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} browserslist@4.28.1: - resolution: - { - integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true bs-logger@0.2.6: - resolution: - { - integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} bser@2.1.1: - resolution: - { - integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==, - } + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} buffer-equal-constant-time@1.0.1: - resolution: - { - integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==, - } + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} buffer-from@1.1.2: - resolution: - { - integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, - } + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer-json@2.0.0: - resolution: - { - integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==, - } + resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} buffer-xor@1.0.3: - resolution: - { - integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==, - } + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} buffer@4.9.2: - resolution: - { - integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==, - } + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} builtin-modules@3.3.0: - resolution: - { - integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} builtin-status-codes@3.0.0: - resolution: - { - integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, - } + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} builtins@5.0.1: - resolution: - { - integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==, - } + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} bytes@3.0.0: - resolution: - { - integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} c12@1.11.1: - resolution: - { - integrity: sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==, - } + resolution: {integrity: sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==} peerDependencies: magicast: ^0.3.4 peerDependenciesMeta: @@ -5375,564 +3283,312 @@ packages: optional: true c12@1.4.2: - resolution: - { - integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==, - } + resolution: {integrity: sha512-3IP/MuamSVRVw8W8+CHWAz9gKN4gd+voF2zm/Ln6D25C2RhytEZ1ABbC8MjKr4BR9rhoV1JQ7jJA158LDiTkLg==} cacache@12.0.4: - resolution: - { - integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==, - } + resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} cacache@15.3.0: - resolution: - { - integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==, - } - engines: { node: '>= 10' } + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} cache-base@1.0.1: - resolution: - { - integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} cache-loader@4.1.0: - resolution: - { - integrity: sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==, - } - engines: { node: '>= 8.9.0' } + resolution: {integrity: sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==} + engines: {node: '>= 8.9.0'} peerDependencies: webpack: ^4.0.0 call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} call-bind@1.0.2: - resolution: - { - integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==, - } + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} callsite@1.0.0: - resolution: - { - integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==, - } + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} callsites@3.1.0: - resolution: - { - integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} camel-case@4.1.2: - resolution: - { - integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==, - } + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} camelcase-keys@7.0.2: - resolution: - { - integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} camelcase@5.3.1: - resolution: - { - integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} camelcase@6.3.0: - resolution: - { - integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} caniuse-api@3.0.0: - resolution: - { - integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==, - } + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} caniuse-lite@1.0.30001639: - resolution: - { - integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==, - } + resolution: {integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==} caniuse-lite@1.0.30001762: - resolution: - { - integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==, - } + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} chalk@1.1.3: - resolution: - { - integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} chalk@2.4.2: - resolution: - { - integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} chalk@4.1.2: - resolution: - { - integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} chalk@5.5.0: - resolution: - { - integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==, - } - engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } + resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} char-regex@1.0.2: - resolution: - { - integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} chardet@0.7.0: - resolution: - { - integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==, - } + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} chart.js@4.5.1: - resolution: - { - integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==, - } - engines: { pnpm: '>=8' } + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} chartjs-adapter-moment@1.0.1: - resolution: - { - integrity: sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==, - } + resolution: {integrity: sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==} peerDependencies: chart.js: '>=3.0.0' moment: ^2.10.2 chokidar@2.1.8: - resolution: - { - integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==, - } + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies chokidar@3.5.3: - resolution: - { - integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==, - } - engines: { node: '>= 8.10.0' } + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} chokidar@3.6.0: - resolution: - { - integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, - } - engines: { node: '>= 8.10.0' } + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} chownr@1.1.4: - resolution: - { - integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==, - } + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} chownr@2.0.0: - resolution: - { - integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} chrome-trace-event@1.0.4: - resolution: - { - integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} ci-info@3.8.0: - resolution: - { - integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} ci-info@3.9.0: - resolution: - { - integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} ci-info@4.3.0: - resolution: - { - integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + engines: {node: '>=8'} ci-info@4.4.0: - resolution: - { - integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} cipher-base@1.0.4: - resolution: - { - integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==, - } + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} citty@0.1.6: - resolution: - { - integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==, - } + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} cjs-module-lexer@2.1.0: - resolution: - { - integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==, - } + resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} class-utils@0.3.6: - resolution: - { - integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} clean-css@4.2.4: - resolution: - { - integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==, - } - engines: { node: '>= 4.0' } + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} clean-css@5.3.3: - resolution: - { - integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==, - } - engines: { node: '>= 10.0' } + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} clean-regexp@1.0.0: - resolution: - { - integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} clean-stack@2.2.0: - resolution: - { - integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} cli-boxes@2.2.1: - resolution: - { - integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} cli-cursor@3.1.0: - resolution: - { - integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} cli-cursor@5.0.0: - resolution: - { - integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} cli-truncate@4.0.0: - resolution: - { - integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} cli-width@3.0.0: - resolution: - { - integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==, - } - engines: { node: '>= 10' } + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} cliui@6.0.0: - resolution: - { - integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==, - } + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} cliui@7.0.4: - resolution: - { - integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==, - } + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} cliui@8.0.1: - resolution: - { - integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} clone@2.1.2: - resolution: - { - integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==, - } - engines: { node: '>=0.8' } + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} co@4.6.0: - resolution: - { - integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==, - } - engines: { iojs: '>= 1.0.0', node: '>= 0.12.0' } + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} collect-v8-coverage@1.0.2: - resolution: - { - integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==, - } + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} collection-visit@1.0.0: - resolution: - { - integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} color-convert@1.9.3: - resolution: - { - integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, - } + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} color-convert@2.0.1: - resolution: - { - integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, - } - engines: { node: '>=7.0.0' } + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} color-name@1.1.3: - resolution: - { - integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, - } + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} color-name@1.1.4: - resolution: - { - integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, - } + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} colord@2.9.3: - resolution: - { - integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==, - } + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} colorette@2.0.20: - resolution: - { - integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, - } + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} combined-stream@1.0.8: - resolution: - { - integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} commander@10.0.1: - resolution: - { - integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} commander@14.0.0: - resolution: - { - integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==, - } - engines: { node: '>=20' } + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} commander@2.20.3: - resolution: - { - integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, - } + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} commander@4.1.1: - resolution: - { - integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} commander@7.2.0: - resolution: - { - integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==, - } - engines: { node: '>= 10' } + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} commondir@1.0.1: - resolution: - { - integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, - } + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} compare-func@2.0.0: - resolution: - { - integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==, - } + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} compatx@0.1.8: - resolution: - { - integrity: sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==, - } + resolution: {integrity: sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==} component-emitter@1.3.0: - resolution: - { - integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==, - } + resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} compressible@2.0.18: - resolution: - { - integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} compression@1.7.4: - resolution: - { - integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} concat-map@0.0.1: - resolution: - { - integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, - } + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} concat-stream@1.6.2: - resolution: - { - integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==, - } - engines: { '0': node >= 0.8 } + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} condense-newlines@0.2.1: - resolution: - { - integrity: sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==} + engines: {node: '>=0.10.0'} confbox@0.1.7: - resolution: - { - integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==, - } + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} confbox@0.1.8: - resolution: - { - integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==, - } + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} config-chain@1.1.13: - resolution: - { - integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==, - } + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} configstore@5.0.1: - resolution: - { - integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} connect@3.7.0: - resolution: - { - integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==, - } - engines: { node: '>= 0.10.0' } + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} consola@2.15.3: - resolution: - { - integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==, - } + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} consola@3.2.3: - resolution: - { - integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} console-browserify@1.2.0: - resolution: - { - integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==, - } + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} consolidate@0.15.1: - resolution: - { - integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==, - } - engines: { node: '>= 0.10.0' } + resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} + engines: {node: '>= 0.10.0'} deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog peerDependencies: arc-templates: ^0.5.3 @@ -6097,124 +3753,73 @@ packages: optional: true constants-browserify@1.0.0: - resolution: - { - integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==, - } + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} conventional-changelog-angular@8.1.0: - resolution: - { - integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} + engines: {node: '>=18'} conventional-changelog-conventionalcommits@9.3.1: - resolution: - { - integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} + engines: {node: '>=18'} conventional-commits-parser@6.2.1: - resolution: - { - integrity: sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==} + engines: {node: '>=18'} hasBin: true conventional-commits-parser@6.4.0: - resolution: - { - integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} hasBin: true convert-source-map@2.0.0: - resolution: - { - integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, - } + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie@0.3.1: - resolution: - { - integrity: sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==} + engines: {node: '>= 0.6'} copy-concurrently@1.0.5: - resolution: - { - integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==, - } + resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} deprecated: This package is no longer supported. copy-descriptor@0.1.1: - resolution: - { - integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} core-js-compat@3.37.1: - resolution: - { - integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==, - } + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} core-js@2.6.12: - resolution: - { - integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==, - } + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. core-js@3.49.0: - resolution: - { - integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==, - } + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} core-util-is@1.0.3: - resolution: - { - integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, - } + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} cosmiconfig-typescript-loader@6.2.0: - resolution: - { - integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==, - } - engines: { node: '>=v18' } + resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} + engines: {node: '>=v18'} peerDependencies: '@types/node': '*' cosmiconfig: '>=9' typescript: '>=5' cosmiconfig@6.0.0: - resolution: - { - integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} + engines: {node: '>=8'} cosmiconfig@7.1.0: - resolution: - { - integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} cosmiconfig@8.3.6: - resolution: - { - integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' peerDependenciesMeta: @@ -6222,11 +3827,8 @@ packages: optional: true cosmiconfig@9.0.0: - resolution: - { - integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' peerDependenciesMeta: @@ -6234,11 +3836,8 @@ packages: optional: true crc@4.3.2: - resolution: - { - integrity: sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==} + engines: {node: '>=12'} peerDependencies: buffer: '>=6.0.3' peerDependenciesMeta: @@ -6246,308 +3845,182 @@ packages: optional: true create-ecdh@4.0.4: - resolution: - { - integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==, - } + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} create-hash@1.2.0: - resolution: - { - integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==, - } + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} create-hmac@1.1.7: - resolution: - { - integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==, - } + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} create-require@1.1.1: - resolution: - { - integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==, - } + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} cross-spawn@7.0.6: - resolution: - { - integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} crypto-browserify@3.12.0: - resolution: - { - integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==, - } + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} crypto-random-string@2.0.0: - resolution: - { - integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} css-blank-pseudo@6.0.2: - resolution: - { - integrity: sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 css-declaration-sorter@6.4.1: - resolution: - { - integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} + engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 css-declaration-sorter@7.2.0: - resolution: - { - integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.0.9 css-functions-list@3.2.1: - resolution: - { - integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==, - } - engines: { node: '>=12 || >=16' } + resolution: {integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==} + engines: {node: '>=12 || >=16'} css-has-pseudo@6.0.5: - resolution: - { - integrity: sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 css-loader@5.2.7: - resolution: - { - integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} + engines: {node: '>= 10.13.0'} peerDependencies: webpack: ^4.27.0 || ^5.0.0 css-prefers-color-scheme@9.0.1: - resolution: - { - integrity: sha512-iFit06ochwCKPRiWagbTa1OAWCvWWVdEnIFd8BaRrgO8YrrNh4RAWUQTFcYX5tdFZgFl1DJ3iiULchZyEbnF4g==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-iFit06ochwCKPRiWagbTa1OAWCvWWVdEnIFd8BaRrgO8YrrNh4RAWUQTFcYX5tdFZgFl1DJ3iiULchZyEbnF4g==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 css-select@4.3.0: - resolution: - { - integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==, - } + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} css-select@5.1.0: - resolution: - { - integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==, - } + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} css-tree@1.1.3: - resolution: - { - integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} css-tree@2.2.1: - resolution: - { - integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==, - } - engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0' } + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} css-tree@2.3.1: - resolution: - { - integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==, - } - engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0 } + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css-what@6.1.0: - resolution: - { - integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} css@2.2.4: - resolution: - { - integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==, - } + resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==} cssdb@8.0.2: - resolution: - { - integrity: sha512-zbOCmmbcHvr2lP+XrZSgftGMGumbosC6IM3dbxwifwPEBD70pVJaH3Ho191VBEqDg644AM7PPPVj0ZXokTjZng==, - } + resolution: {integrity: sha512-zbOCmmbcHvr2lP+XrZSgftGMGumbosC6IM3dbxwifwPEBD70pVJaH3Ho191VBEqDg644AM7PPPVj0ZXokTjZng==} cssesc@3.0.0: - resolution: - { - integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} hasBin: true cssnano-preset-default@5.2.14: - resolution: - { - integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 cssnano-preset-default@7.0.3: - resolution: - { - integrity: sha512-dQ3Ba1p/oewICp/szF1XjFFgql8OlOBrI2YNBUUwhHQnJNoMOcQTa+Bi7jSJN8r/eM1egW0Ud1se/S7qlduWKA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-dQ3Ba1p/oewICp/szF1XjFFgql8OlOBrI2YNBUUwhHQnJNoMOcQTa+Bi7jSJN8r/eM1egW0Ud1se/S7qlduWKA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 cssnano-utils@3.1.0: - resolution: - { - integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 cssnano-utils@5.0.0: - resolution: - { - integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 cssnano@5.1.15: - resolution: - { - integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 cssnano@7.0.3: - resolution: - { - integrity: sha512-lsekJctOTqdCn4cNrtrSwsuMR/fHC+oiVMHkp/OugBWtwjH8XJag1/OtGaYJGtz0un1fQcRy4ryfYTQsfh+KSQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-lsekJctOTqdCn4cNrtrSwsuMR/fHC+oiVMHkp/OugBWtwjH8XJag1/OtGaYJGtz0un1fQcRy4ryfYTQsfh+KSQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 csso@4.2.0: - resolution: - { - integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} csso@5.0.5: - resolution: - { - integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==, - } - engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0' } + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} cssstyle@4.6.0: - resolution: - { - integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} csstype@3.1.2: - resolution: - { - integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==, - } + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} cuint@0.2.2: - resolution: - { - integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==, - } + resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} cyclist@1.0.2: - resolution: - { - integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==, - } + resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} dargs@8.1.0: - resolution: - { - integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} data-urls@5.0.0: - resolution: - { - integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} date-fns@2.30.0: - resolution: - { - integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, - } - engines: { node: '>=0.11' } + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} de-indent@1.0.2: - resolution: - { - integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==, - } + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} deasync@0.1.29: - resolution: - { - integrity: sha512-EBtfUhVX23CE9GR6m+F8WPeImEE4hR/FW9RkK0PMl9V1t283s0elqsTD8EZjaKX28SY1BW2rYfCgNsAYdpamUw==, - } - engines: { node: '>=0.11.0' } + resolution: {integrity: sha512-EBtfUhVX23CE9GR6m+F8WPeImEE4hR/FW9RkK0PMl9V1t283s0elqsTD8EZjaKX28SY1BW2rYfCgNsAYdpamUw==} + engines: {node: '>=0.11.0'} debounce@1.2.1: - resolution: - { - integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==, - } + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} debug@2.6.9: - resolution: - { - integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==, - } + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6555,10 +4028,7 @@ packages: optional: true debug@3.2.7: - resolution: - { - integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, - } + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6566,11 +4036,8 @@ packages: optional: true debug@4.3.4: - resolution: - { - integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6578,11 +4045,8 @@ packages: optional: true debug@4.3.6: - resolution: - { - integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6590,11 +4054,8 @@ packages: optional: true debug@4.4.1: - resolution: - { - integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6602,11 +4063,8 @@ packages: optional: true debug@4.4.3: - resolution: - { - integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -6614,50 +4072,29 @@ packages: optional: true decache@4.6.2: - resolution: - { - integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==, - } + resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} decamelize-keys@1.1.1: - resolution: - { - integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} decamelize@1.2.0: - resolution: - { - integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} decamelize@5.0.1: - resolution: - { - integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} decimal.js@10.6.0: - resolution: - { - integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, - } + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-uri-component@0.2.2: - resolution: - { - integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} dedent@1.7.0: - resolution: - { - integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==, - } + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -6665,587 +4102,323 @@ packages: optional: true deep-is@0.1.4: - resolution: - { - integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, - } + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} deepmerge@4.3.1: - resolution: - { - integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} define-data-property@1.1.0: - resolution: - { - integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} define-properties@1.2.1: - resolution: - { - integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} define-property@0.2.5: - resolution: - { - integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} define-property@1.0.0: - resolution: - { - integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} define-property@2.0.2: - resolution: - { - integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} defu@5.0.1: - resolution: - { - integrity: sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==, - } + resolution: {integrity: sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==} defu@6.1.2: - resolution: - { - integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==, - } + resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} defu@6.1.4: - resolution: - { - integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, - } + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} delayed-stream@1.0.0: - resolution: - { - integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, - } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} depd@2.0.0: - resolution: - { - integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} des.js@1.1.0: - resolution: - { - integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==, - } + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} destr@2.0.3: - resolution: - { - integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==, - } + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} destroy@1.2.0: - resolution: - { - integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, - } - engines: { node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16 } + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} detect-indent@5.0.0: - resolution: - { - integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} + engines: {node: '>=4'} detect-newline@3.1.0: - resolution: - { - integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} devalue@2.0.1: - resolution: - { - integrity: sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q==, - } + resolution: {integrity: sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q==} dialog-polyfill@0.4.10: - resolution: - { - integrity: sha512-j5yGMkP8T00UFgyO+78OxiN5vC5dzRQF3BEio+LhNvDbyfxWBsi3sfPArDm54VloaJwy2hm3erEiDWqHRC8rzw==, - } + resolution: {integrity: sha512-j5yGMkP8T00UFgyO+78OxiN5vC5dzRQF3BEio+LhNvDbyfxWBsi3sfPArDm54VloaJwy2hm3erEiDWqHRC8rzw==} diffie-hellman@5.0.3: - resolution: - { - integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==, - } + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} dijkstrajs@1.0.3: - resolution: - { - integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==, - } + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} dir-glob@3.0.1: - resolution: - { - integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} doctrine@2.1.0: - resolution: - { - integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} doctrine@3.0.0: - resolution: - { - integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} dom-converter@0.2.0: - resolution: - { - integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==, - } + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} dom-event-types@1.1.0: - resolution: - { - integrity: sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==, - } + resolution: {integrity: sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==} dom-serializer@1.4.1: - resolution: - { - integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==, - } + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} dom-serializer@2.0.0: - resolution: - { - integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, - } + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} domain-browser@1.2.0: - resolution: - { - integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==, - } - engines: { node: '>=0.4', npm: '>=1.2' } + resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} + engines: {node: '>=0.4', npm: '>=1.2'} domelementtype@2.3.0: - resolution: - { - integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==, - } + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} domhandler@4.3.1: - resolution: - { - integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} domhandler@5.0.3: - resolution: - { - integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} domutils@2.8.0: - resolution: - { - integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==, - } + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} domutils@3.2.2: - resolution: - { - integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==, - } + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} dot-case@3.0.4: - resolution: - { - integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, - } + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dot-prop@5.3.0: - resolution: - { - integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} dotenv@16.6.1: - resolution: - { - integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} dotenv@17.2.3: - resolution: - { - integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} dotenv@8.6.0: - resolution: - { - integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} dotenv@9.0.2: - resolution: - { - integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} + engines: {node: '>=10'} dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} duplexer@0.1.2: - resolution: - { - integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==, - } + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} duplexify@3.7.1: - resolution: - { - integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==, - } + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} duplexify@4.1.2: - resolution: - { - integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==, - } + resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} eastasianwidth@0.2.0: - resolution: - { - integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, - } + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} ecdsa-sig-formatter@1.0.11: - resolution: - { - integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, - } + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} editorconfig@1.0.4: - resolution: - { - integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} hasBin: true ee-first@1.1.1: - resolution: - { - integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, - } + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} ejs@3.1.10: - resolution: - { - integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} hasBin: true electron-to-chromium@1.5.267: - resolution: - { - integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==, - } + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} elliptic@6.6.0: - resolution: - { - integrity: sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==, - } + resolution: {integrity: sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==} emittery@0.13.1: - resolution: - { - integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} emoji-regex@10.4.0: - resolution: - { - integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==, - } + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} emoji-regex@8.0.0: - resolution: - { - integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, - } + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: - resolution: - { - integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, - } + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} emojis-list@3.0.0: - resolution: - { - integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} encodeurl@1.0.2: - resolution: - { - integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} encodeurl@2.0.0: - resolution: - { - integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} end-of-stream@1.4.4: - resolution: - { - integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, - } + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} enhanced-resolve@4.5.0: - resolution: - { - integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} + engines: {node: '>=6.9.0'} enhanced-resolve@5.18.4: - resolution: - { - integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} ent@2.2.0: - resolution: - { - integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==, - } + resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} entities@2.2.0: - resolution: - { - integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, - } + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} entities@4.5.0: - resolution: - { - integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, - } - engines: { node: '>=0.12' } + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} entities@6.0.1: - resolution: - { - integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==, - } - engines: { node: '>=0.12' } + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} env-paths@2.2.1: - resolution: - { - integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} environment@1.1.0: - resolution: - { - integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} errno@0.1.8: - resolution: - { - integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==, - } + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true error-ex@1.3.2: - resolution: - { - integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==, - } + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} error-stack-parser@2.1.4: - resolution: - { - integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==, - } + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} es-abstract@1.22.2: - resolution: - { - integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} es-array-method-boxes-properly@1.0.0: - resolution: - { - integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==, - } + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-module-lexer@2.0.0: - resolution: - { - integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==, - } + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: - resolution: - { - integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: - resolution: - { - integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} es-shim-unscopables@1.0.0: - resolution: - { - integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==, - } + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} es-to-primitive@1.2.1: - resolution: - { - integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} esbuild@0.18.20: - resolution: - { - integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} hasBin: true escalade@3.2.0: - resolution: - { - integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} escape-html@1.0.3: - resolution: - { - integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, - } + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} escape-string-regexp@1.0.5: - resolution: - { - integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} escape-string-regexp@2.0.0: - resolution: - { - integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} escape-string-regexp@4.0.0: - resolution: - { - integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} escape-string-regexp@5.0.0: - resolution: - { - integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} eslint-config-prettier@10.1.8: - resolution: - { - integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==, - } + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' eslint-config-standard@17.1.0: - resolution: - { - integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} peerDependencies: eslint: ^8.0.1 eslint-plugin-import: ^2.25.2 @@ -7253,27 +4426,18 @@ packages: eslint-plugin-promise: ^6.0.0 eslint-import-resolver-node@0.3.9: - resolution: - { - integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==, - } + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} eslint-import-resolver-typescript@3.6.1: - resolution: - { - integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==, - } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' eslint-plugin-import: '*' eslint-module-utils@2.8.0: - resolution: - { - integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' eslint: '*' @@ -7293,29 +4457,20 @@ packages: optional: true eslint-plugin-es@3.0.1: - resolution: - { - integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==, - } - engines: { node: '>=8.10.0' } + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' eslint-plugin-es@4.1.0: - resolution: - { - integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==, - } - engines: { node: '>=8.10.0' } + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' eslint-plugin-import@2.28.1: - resolution: - { - integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 @@ -7324,575 +4479,332 @@ packages: optional: true eslint-plugin-n@15.7.0: - resolution: - { - integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==, - } - engines: { node: '>=12.22.0' } + resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} + engines: {node: '>=12.22.0'} peerDependencies: eslint: '>=7.0.0' eslint-plugin-node@11.1.0: - resolution: - { - integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==, - } - engines: { node: '>=8.10.0' } + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=5.16.0' eslint-plugin-nuxt@4.0.0: - resolution: - { - integrity: sha512-v3Vwdk8YKe52bAz8eSIDqQuTtfL/T1r9dSl1uhC5SyR5pgLxgKkQdxXVf/Bf6Ax7uyd9rHqiAuYVdqqDb7ILdA==, - } + resolution: {integrity: sha512-v3Vwdk8YKe52bAz8eSIDqQuTtfL/T1r9dSl1uhC5SyR5pgLxgKkQdxXVf/Bf6Ax7uyd9rHqiAuYVdqqDb7ILdA==} eslint-plugin-promise@6.1.1: - resolution: - { - integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 eslint-plugin-unicorn@44.0.2: - resolution: - { - integrity: sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w==, - } - engines: { node: '>=14.18' } + resolution: {integrity: sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w==} + engines: {node: '>=14.18'} peerDependencies: eslint: '>=8.23.1' eslint-plugin-vue@9.33.0: - resolution: - { - integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==, - } - engines: { node: ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 eslint-scope@4.0.3: - resolution: - { - integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==} + engines: {node: '>=4.0.0'} eslint-scope@5.1.1: - resolution: - { - integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} eslint-scope@7.2.2: - resolution: - { - integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-utils@2.1.0: - resolution: - { - integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} eslint-utils@3.0.0: - resolution: - { - integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==, - } - engines: { node: ^10.0.0 || ^12.0.0 || >= 14.0.0 } + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' eslint-visitor-keys@1.3.0: - resolution: - { - integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} eslint-visitor-keys@2.1.0: - resolution: - { - integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} eslint-visitor-keys@3.4.3: - resolution: - { - integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-webpack-plugin@4.0.1: - resolution: - { - integrity: sha512-fUFcXpui/FftGx3NzvWgLZXlLbu+m74sUxGEgxgoxYcUtkIQbS6SdNNZkS99m5ycb23TfoNYrDpp1k/CK5j6Hw==, - } - engines: { node: '>= 14.15.0' } + resolution: {integrity: sha512-fUFcXpui/FftGx3NzvWgLZXlLbu+m74sUxGEgxgoxYcUtkIQbS6SdNNZkS99m5ycb23TfoNYrDpp1k/CK5j6Hw==} + engines: {node: '>= 14.15.0'} peerDependencies: eslint: ^8.0.0 webpack: ^5.0.0 eslint@8.57.1: - resolution: - { - integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: - resolution: - { - integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==, - } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} esprima@4.0.1: - resolution: - { - integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} hasBin: true esquery@1.5.0: - resolution: - { - integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} esquery@1.6.0: - resolution: - { - integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} esrecurse@4.3.0: - resolution: - { - integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} estraverse@4.3.0: - resolution: - { - integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} estraverse@5.3.0: - resolution: - { - integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} estree-walker@2.0.2: - resolution: - { - integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, - } + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: - resolution: - { - integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, - } + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} esutils@2.0.3: - resolution: - { - integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} etag@1.8.1: - resolution: - { - integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} event-target-shim@5.0.1: - resolution: - { - integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} eventemitter3@5.0.1: - resolution: - { - integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, - } + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} events@3.3.0: - resolution: - { - integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, - } - engines: { node: '>=0.8.x' } + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} eventsource-polyfill@0.9.6: - resolution: - { - integrity: sha512-LyMFp2oPDGhum2lMvkjqKZEwWd2/AoXyt8aoyftTBMWwPHNgU+2tdxhTHPluDxoz+z4gNj0uHAPR9nqevATMbg==, - } + resolution: {integrity: sha512-LyMFp2oPDGhum2lMvkjqKZEwWd2/AoXyt8aoyftTBMWwPHNgU+2tdxhTHPluDxoz+z4gNj0uHAPR9nqevATMbg==} evp_bytestokey@1.0.3: - resolution: - { - integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==, - } + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} execa@5.1.1: - resolution: - { - integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} execa@8.0.1: - resolution: - { - integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==, - } - engines: { node: '>=16.17' } + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} exit-x@0.2.2: - resolution: - { - integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} exit@0.1.2: - resolution: - { - integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} expand-brackets@2.1.4: - resolution: - { - integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} expect@30.2.0: - resolution: - { - integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} extend-shallow@2.0.1: - resolution: - { - integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} extend-shallow@3.0.2: - resolution: - { - integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} extend@3.0.2: - resolution: - { - integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==, - } + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} external-editor@3.1.0: - resolution: - { - integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} extglob@2.0.4: - resolution: - { - integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} extract-css-chunks-webpack-plugin@4.10.0: - resolution: - { - integrity: sha512-D/wb/Tbexq8XMBl4uhthto25WBaHI9P8vucDdzwPtLTyVi4Rdw/aiRLSL2rHaF6jZfPAjThWXepFU9PXsdtIbA==, - } - engines: { node: '>= 6.9.0' } + resolution: {integrity: sha512-D/wb/Tbexq8XMBl4uhthto25WBaHI9P8vucDdzwPtLTyVi4Rdw/aiRLSL2rHaF6jZfPAjThWXepFU9PXsdtIbA==} + engines: {node: '>= 6.9.0'} peerDependencies: webpack: ^4.4.0 || ^5.0.0 extract-from-css@0.4.4: - resolution: - { - integrity: sha512-41qWGBdtKp9U7sgBxAQ7vonYqSXzgW/SiAYzq4tdWSVhAShvpVCH1nyvPQgjse6EdgbW7Y7ERdT3674/lKr65A==, - } - engines: { node: '>=0.10.0', npm: '>=2.0.0' } + resolution: {integrity: sha512-41qWGBdtKp9U7sgBxAQ7vonYqSXzgW/SiAYzq4tdWSVhAShvpVCH1nyvPQgjse6EdgbW7Y7ERdT3674/lKr65A==} + engines: {node: '>=0.10.0', npm: '>=2.0.0'} fast-deep-equal@3.1.3: - resolution: - { - integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, - } + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-glob@3.3.1: - resolution: - { - integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==, - } - engines: { node: '>=8.6.0' } + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} fast-glob@3.3.2: - resolution: - { - integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, - } - engines: { node: '>=8.6.0' } + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: - resolution: - { - integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, - } + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: - resolution: - { - integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, - } + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} fast-text-encoding@1.0.6: - resolution: - { - integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==, - } + resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} fast-uri@3.1.0: - resolution: - { - integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==, - } + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} fastest-levenshtein@1.0.16: - resolution: - { - integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==, - } - engines: { node: '>= 4.9.1' } + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} fastq@1.15.0: - resolution: - { - integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==, - } + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} faye-websocket@0.11.4: - resolution: - { - integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} fb-watchman@2.0.2: - resolution: - { - integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==, - } + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} figgy-pudding@3.5.2: - resolution: - { - integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==, - } + resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} deprecated: This module is no longer supported. figures@3.2.0: - resolution: - { - integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} file-entry-cache@6.0.1: - resolution: - { - integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==, - } - engines: { node: ^10.12.0 || >=12.0.0 } + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} file-entry-cache@7.0.1: - resolution: - { - integrity: sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==} + engines: {node: '>=12.0.0'} file-loader@6.2.0: - resolution: - { - integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} peerDependencies: webpack: ^4.0.0 || ^5.0.0 file-uri-to-path@1.0.0: - resolution: - { - integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==, - } + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} filelist@1.0.6: - resolution: - { - integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==, - } + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} fill-range@4.0.0: - resolution: - { - integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} fill-range@7.0.1: - resolution: - { - integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} finalhandler@1.1.2: - resolution: - { - integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} find-babel-config@1.2.0: - resolution: - { - integrity: sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==} + engines: {node: '>=4.0.0'} find-cache-dir@2.1.0: - resolution: - { - integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} find-cache-dir@3.3.2: - resolution: - { - integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} find-up@3.0.0: - resolution: - { - integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} find-up@4.1.0: - resolution: - { - integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} find-up@5.0.0: - resolution: - { - integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} firebase-admin@10.3.0: - resolution: - { - integrity: sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==, - } - engines: { node: '>=12.7.0' } + resolution: {integrity: sha512-A0wgMLEjyVyUE+heyMJYqHRkPVjpebhOYsa47RHdrTM4ltApcx8Tn86sUmjqxlfh09gNnILAm7a8q5+FmgBYpg==} + engines: {node: '>=12.7.0'} firebase@10.14.1: - resolution: - { - integrity: sha512-0KZxU+Ela9rUCULqFsUUOYYkjh7OM1EWdIfG6///MtXd0t2/uUIf0iNV5i0KariMhRQ5jve/OY985nrAXFaZeQ==, - } + resolution: {integrity: sha512-0KZxU+Ela9rUCULqFsUUOYYkjh7OM1EWdIfG6///MtXd0t2/uUIf0iNV5i0KariMhRQ5jve/OY985nrAXFaZeQ==} firebaseui@6.1.0: - resolution: - { - integrity: sha512-5WiVYVxPGMANuZKxg6KLyU1tyqIsbqf/59Zm4HrdFYwPtM5lxxB0THvgaIk4ix+hCgF0qmY89sKiktcifKzGIA==, - } + resolution: {integrity: sha512-5WiVYVxPGMANuZKxg6KLyU1tyqIsbqf/59Zm4HrdFYwPtM5lxxB0THvgaIk4ix+hCgF0qmY89sKiktcifKzGIA==} peerDependencies: firebase: ^9.1.3 || ^10.0.0 flat-cache@3.1.1: - resolution: - { - integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} + engines: {node: '>=12.0.0'} flat@5.0.2: - resolution: - { - integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==, - } + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true flatted@3.2.9: - resolution: - { - integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==, - } + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} flush-write-stream@1.1.1: - resolution: - { - integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==, - } + resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} follow-redirects@1.16.0: - resolution: - { - integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==, - } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} peerDependencies: debug: '*' peerDependenciesMeta: @@ -7900,31 +4812,19 @@ packages: optional: true for-each@0.3.3: - resolution: - { - integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==, - } + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} for-in@1.0.2: - resolution: - { - integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} foreground-child@3.3.1: - resolution: - { - integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} fork-ts-checker-webpack-plugin@6.5.3: - resolution: - { - integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==, - } - engines: { node: '>=10', yarn: '>=1.0.0' } + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: eslint: '>= 6' typescript: '>= 2.7' @@ -7937,1444 +4837,811 @@ packages: optional: true form-data@4.0.5: - resolution: - { - integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} fraction.js@4.3.7: - resolution: - { - integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==, - } + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} fragment-cache@0.2.1: - resolution: - { - integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} fresh@0.5.2: - resolution: - { - integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} from2@2.3.0: - resolution: - { - integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==, - } + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} fs-extra@11.2.0: - resolution: - { - integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==, - } - engines: { node: '>=14.14' } + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} fs-extra@8.1.0: - resolution: - { - integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==, - } - engines: { node: '>=6 <7 || >=8' } + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} fs-extra@9.1.0: - resolution: - { - integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} fs-memo@1.2.0: - resolution: - { - integrity: sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==, - } + resolution: {integrity: sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==} fs-minipass@2.1.0: - resolution: - { - integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} fs-monkey@1.0.5: - resolution: - { - integrity: sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==, - } + resolution: {integrity: sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==} fs-write-stream-atomic@1.0.10: - resolution: - { - integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==, - } + resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} deprecated: This package is no longer supported. fs.realpath@1.0.0: - resolution: - { - integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, - } + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} fsevents@1.2.13: - resolution: - { - integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==, - } - engines: { node: '>= 4.0' } + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} os: [darwin] deprecated: Upgrade to fsevents v2 to mitigate potential security issues fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] function-bind@1.1.1: - resolution: - { - integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==, - } + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} function.prototype.name@1.1.6: - resolution: - { - integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} functional-red-black-tree@1.0.1: - resolution: - { - integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==, - } + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} functions-have-names@1.2.3: - resolution: - { - integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, - } + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} gaxios@4.3.3: - resolution: - { - integrity: sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==} + engines: {node: '>=10'} gcp-metadata@4.3.1: - resolution: - { - integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==} + engines: {node: '>=10'} gensync@1.0.0-beta.2: - resolution: - { - integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, - } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} get-caller-file@2.0.5: - resolution: - { - integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, - } - engines: { node: 6.* || 8.* || >= 10.* } + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} get-east-asian-width@1.3.0: - resolution: - { - integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} get-intrinsic@1.2.1: - resolution: - { - integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==, - } + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} get-package-type@0.1.0: - resolution: - { - integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} get-port-please@2.6.1: - resolution: - { - integrity: sha512-4PDSrL6+cuMM1xs6w36ZIkaKzzE0xzfVBCfebHIJ3FE8iB9oic/ECwPw3iNiD4h1AoJ5XLLBhEviFAVrZsDC5A==, - } + resolution: {integrity: sha512-4PDSrL6+cuMM1xs6w36ZIkaKzzE0xzfVBCfebHIJ3FE8iB9oic/ECwPw3iNiD4h1AoJ5XLLBhEviFAVrZsDC5A==} get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} get-stream@6.0.1: - resolution: - { - integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} get-stream@8.0.1: - resolution: - { - integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} get-symbol-description@1.0.0: - resolution: - { - integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} get-tsconfig@4.7.2: - resolution: - { - integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==, - } + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} get-value@2.0.6: - resolution: - { - integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} giget@1.1.2: - resolution: - { - integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==, - } + resolution: {integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==} hasBin: true giget@1.2.3: - resolution: - { - integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==, - } + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} hasBin: true git-config-path@2.0.0: - resolution: - { - integrity: sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==} + engines: {node: '>=4'} git-raw-commits@4.0.0: - resolution: - { - integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true git-up@7.0.0: - resolution: - { - integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==, - } + resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} git-url-parse@13.1.1: - resolution: - { - integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==, - } + resolution: {integrity: sha512-PCFJyeSSdtnbfhSNRw9Wk96dDCNx+sogTe4YNXeXSJxt7xz5hvXekuRn9JX7m+Mf4OscCu8h+mtAl3+h5Fo8lQ==} glob-parent@3.1.0: - resolution: - { - integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==, - } + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-parent@6.0.2: - resolution: - { - integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} glob-to-regexp@0.4.1: - resolution: - { - integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==, - } + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} glob@10.4.5: - resolution: - { - integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, - } + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: - resolution: - { - integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, - } + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: - resolution: - { - integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: - resolution: - { - integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} global-modules@2.0.0: - resolution: - { - integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} global-prefix@3.0.0: - resolution: - { - integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} globals@11.12.0: - resolution: - { - integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} globals@13.24.0: - resolution: - { - integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} globals@9.18.0: - resolution: - { - integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} + engines: {node: '>=0.10.0'} globalthis@1.0.3: - resolution: - { - integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} globby@11.1.0: - resolution: - { - integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} globby@13.2.2: - resolution: - { - integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} globby@14.0.2: - resolution: - { - integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} globjoin@0.1.4: - resolution: - { - integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==, - } + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} google-auth-library@7.14.1: - resolution: - { - integrity: sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==} + engines: {node: '>=10'} google-gax@2.30.5: - resolution: - { - integrity: sha512-Jey13YrAN2hfpozHzbtrwEfEHdStJh1GwaQ2+Akh1k0Tv/EuNVSuBtHZoKSBm5wBMvNsxTsEIZ/152NrYyZgxQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Jey13YrAN2hfpozHzbtrwEfEHdStJh1GwaQ2+Akh1k0Tv/EuNVSuBtHZoKSBm5wBMvNsxTsEIZ/152NrYyZgxQ==} + engines: {node: '>=10'} hasBin: true google-p12-pem@3.1.4: - resolution: - { - integrity: sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==} + engines: {node: '>=10'} deprecated: Package is no longer maintained hasBin: true gopd@1.0.1: - resolution: - { - integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==, - } + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} graceful-fs@4.2.11: - resolution: - { - integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, - } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: - resolution: - { - integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, - } + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} gtoken@5.3.2: - resolution: - { - integrity: sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==} + engines: {node: '>=10'} gzip-size@6.0.0: - resolution: - { - integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} handlebars@4.7.8: - resolution: - { - integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==, - } - engines: { node: '>=0.4.7' } + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} hasBin: true hard-rejection@2.1.0: - resolution: - { - integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} hard-source-webpack-plugin@0.13.1: - resolution: - { - integrity: sha512-r9zf5Wq7IqJHdVAQsZ4OP+dcUSvoHqDMxJlIzaE2J0TZWn3UjMMrHqwDHR8Jr/pzPfG7XxSe36E7Y8QGNdtuAw==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-r9zf5Wq7IqJHdVAQsZ4OP+dcUSvoHqDMxJlIzaE2J0TZWn3UjMMrHqwDHR8Jr/pzPfG7XxSe36E7Y8QGNdtuAw==} + engines: {node: '>=8.0.0'} peerDependencies: webpack: '*' has-ansi@2.0.0: - resolution: - { - integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} has-bigints@1.0.2: - resolution: - { - integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==, - } + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} has-flag@3.0.0: - resolution: - { - integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} has-flag@4.0.0: - resolution: - { - integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} has-property-descriptors@1.0.0: - resolution: - { - integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==, - } + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} has-proto@1.0.1: - resolution: - { - integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} has-symbols@1.0.3: - resolution: - { - integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} has-tostringtag@1.0.2: - resolution: - { - integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} has-value@0.3.1: - resolution: - { - integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} has-value@1.0.0: - resolution: - { - integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} has-values@0.1.4: - resolution: - { - integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} has-values@1.0.0: - resolution: - { - integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} has@1.0.3: - resolution: - { - integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==, - } - engines: { node: '>= 0.4.0' } + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} hash-base@3.1.0: - resolution: - { - integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} hash-stream-validation@0.2.4: - resolution: - { - integrity: sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==, - } + resolution: {integrity: sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==} hash-sum@1.0.2: - resolution: - { - integrity: sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==, - } + resolution: {integrity: sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==} hash-sum@2.0.0: - resolution: - { - integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==, - } + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} hash.js@1.1.7: - resolution: - { - integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==, - } + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} hasown@2.0.3: - resolution: - { - integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} he@1.2.0: - resolution: - { - integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, - } + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true highlight.js@11.11.1: - resolution: - { - integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} hmac-drbg@1.0.1: - resolution: - { - integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==, - } + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} hookable@4.4.1: - resolution: - { - integrity: sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==, - } + resolution: {integrity: sha512-KWjZM8C7IVT2qne5HTXjM6R6VnRfjfRlf/oCnHd+yFxoHO1DzOl6B9LzV/VqGQK/IrFewq+EG+ePVrE9Tpc3fg==} hookable@5.5.3: - resolution: - { - integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==, - } + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} hosted-git-info@2.8.9: - resolution: - { - integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==, - } + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} hosted-git-info@4.1.0: - resolution: - { - integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} html-encoding-sniffer@4.0.0: - resolution: - { - integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} html-entities@2.4.0: - resolution: - { - integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==, - } + resolution: {integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==} html-escaper@2.0.2: - resolution: - { - integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, - } + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} html-minifier-terser@5.1.1: - resolution: - { - integrity: sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==} + engines: {node: '>=6'} hasBin: true html-minifier-terser@7.2.0: - resolution: - { - integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==, - } - engines: { node: ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} hasBin: true html-tags@2.0.0: - resolution: - { - integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==} + engines: {node: '>=4'} html-tags@3.3.1: - resolution: - { - integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} html-webpack-plugin@4.5.2: - resolution: - { - integrity: sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==, - } - engines: { node: '>=6.9' } + resolution: {integrity: sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==} + engines: {node: '>=6.9'} peerDependencies: webpack: ^4.0.0 || ^5.0.0 htmlparser2@6.1.0: - resolution: - { - integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==, - } + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} htmlparser2@8.0.2: - resolution: - { - integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, - } + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} http-errors@2.0.0: - resolution: - { - integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} http-parser-js@0.5.8: - resolution: - { - integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==, - } + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} http-proxy-agent@5.0.0: - resolution: - { - integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} http-proxy-agent@7.0.2: - resolution: - { - integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==, - } - engines: { node: '>= 14' } + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} https-browserify@1.0.0: - resolution: - { - integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==, - } + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} https-proxy-agent@5.0.1: - resolution: - { - integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} https-proxy-agent@7.0.6: - resolution: - { - integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, - } - engines: { node: '>= 14' } + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} human-signals@2.1.0: - resolution: - { - integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, - } - engines: { node: '>=10.17.0' } + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} human-signals@5.0.0: - resolution: - { - integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==, - } - engines: { node: '>=16.17.0' } + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} hyperdyperid@1.2.0: - resolution: - { - integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==, - } - engines: { node: '>=10.18' } + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} iconv-lite@0.4.24: - resolution: - { - integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} iconv-lite@0.6.3: - resolution: - { - integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} icss-utils@5.1.0: - resolution: - { - integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==, - } - engines: { node: ^10 || ^12 || >= 14 } + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 idb@7.1.1: - resolution: - { - integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==, - } + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} ieee754@1.2.1: - resolution: - { - integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, - } + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} iferr@0.1.5: - resolution: - { - integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==, - } + resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} ignore@5.2.4: - resolution: - { - integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} ignore@5.3.1: - resolution: - { - integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} import-fresh@3.3.0: - resolution: - { - integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} import-fresh@3.3.1: - resolution: - { - integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} import-lazy@4.0.0: - resolution: - { - integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} import-local@3.2.0: - resolution: - { - integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} hasBin: true import-meta-resolve@4.2.0: - resolution: - { - integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==, - } + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} imurmurhash@0.1.4: - resolution: - { - integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, - } - engines: { node: '>=0.8.19' } + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} indent-string@4.0.0: - resolution: - { - integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} indent-string@5.0.0: - resolution: - { - integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} infer-owner@1.0.4: - resolution: - { - integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==, - } + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} inflight@1.0.6: - resolution: - { - integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, - } + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.3: - resolution: - { - integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==, - } + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: - resolution: - { - integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==, - } + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} ini@4.1.1: - resolution: - { - integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==, - } - engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} inquirer@7.3.3: - resolution: - { - integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==, - } - engines: { node: '>=8.0.0' } + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} internal-slot@1.0.5: - resolution: - { - integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} invariant@2.2.4: - resolution: - { - integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==, - } + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} ip@2.0.1: - resolution: - { - integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==, - } + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} is-accessor-descriptor@0.1.6: - resolution: - { - integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==} + engines: {node: '>=0.10.0'} deprecated: Please upgrade to v0.1.7 is-accessor-descriptor@1.0.0: - resolution: - { - integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==} + engines: {node: '>=0.10.0'} deprecated: Please upgrade to v1.0.1 is-array-buffer@3.0.2: - resolution: - { - integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==, - } + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} is-arrayish@0.2.1: - resolution: - { - integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, - } + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-bigint@1.0.4: - resolution: - { - integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==, - } + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} is-binary-path@1.0.1: - resolution: - { - integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} is-binary-path@2.1.0: - resolution: - { - integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} is-boolean-object@1.1.2: - resolution: - { - integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} is-buffer@1.1.6: - resolution: - { - integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==, - } + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} is-builtin-module@3.2.1: - resolution: - { - integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} is-callable@1.2.7: - resolution: - { - integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} is-core-module@2.13.0: - resolution: - { - integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==, - } + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} is-data-descriptor@0.1.4: - resolution: - { - integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} + engines: {node: '>=0.10.0'} deprecated: Please upgrade to v0.1.5 is-data-descriptor@1.0.0: - resolution: - { - integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==} + engines: {node: '>=0.10.0'} deprecated: Please upgrade to v1.0.1 is-date-object@1.0.5: - resolution: - { - integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} is-descriptor@0.1.6: - resolution: - { - integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} + engines: {node: '>=0.10.0'} is-descriptor@1.0.2: - resolution: - { - integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==} + engines: {node: '>=0.10.0'} is-extendable@0.1.1: - resolution: - { - integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} is-extendable@1.0.1: - resolution: - { - integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} is-fullwidth-code-point@3.0.0: - resolution: - { - integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} is-fullwidth-code-point@4.0.0: - resolution: - { - integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} is-fullwidth-code-point@5.0.0: - resolution: - { - integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} is-generator-fn@2.1.0: - resolution: - { - integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} is-glob@3.1.0: - resolution: - { - integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} is-glob@4.0.3: - resolution: - { - integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-https@2.0.2: - resolution: - { - integrity: sha512-UfUCKVQH/6PQRCh5Qk9vNu4feLZiFmV/gr8DjbtJD0IrCRIDTA6E+d/AVFGPulI5tqK5W45fYbn1Nir1O99rFw==, - } + resolution: {integrity: sha512-UfUCKVQH/6PQRCh5Qk9vNu4feLZiFmV/gr8DjbtJD0IrCRIDTA6E+d/AVFGPulI5tqK5W45fYbn1Nir1O99rFw==} is-negative-zero@2.0.2: - resolution: - { - integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} is-number-object@1.0.7: - resolution: - { - integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} is-number@3.0.0: - resolution: - { - integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} is-number@7.0.0: - resolution: - { - integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, - } - engines: { node: '>=0.12.0' } + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} is-obj@2.0.0: - resolution: - { - integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} is-path-inside@3.0.3: - resolution: - { - integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} is-plain-obj@1.1.0: - resolution: - { - integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} is-plain-obj@4.1.0: - resolution: - { - integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} is-plain-object@2.0.4: - resolution: - { - integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} is-plain-object@5.0.0: - resolution: - { - integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} is-potential-custom-element-name@1.0.1: - resolution: - { - integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, - } + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} is-regex@1.1.4: - resolution: - { - integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} is-shared-array-buffer@1.0.2: - resolution: - { - integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==, - } + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} is-ssh@1.4.0: - resolution: - { - integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==, - } + resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} is-stream-ended@0.1.4: - resolution: - { - integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==, - } + resolution: {integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==} is-stream@2.0.1: - resolution: - { - integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-stream@3.0.0: - resolution: - { - integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} is-string@1.0.7: - resolution: - { - integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} is-symbol@1.0.4: - resolution: - { - integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} is-typed-array@1.1.12: - resolution: - { - integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} is-typedarray@1.0.0: - resolution: - { - integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==, - } + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} is-weakref@1.0.2: - resolution: - { - integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==, - } + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} is-whitespace@0.3.0: - resolution: - { - integrity: sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==} + engines: {node: '>=0.10.0'} is-windows@1.0.2: - resolution: - { - integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} is-wsl@1.1.0: - resolution: - { - integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} isarray@1.0.0: - resolution: - { - integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, - } + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} isarray@2.0.5: - resolution: - { - integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, - } + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} isobject@2.1.0: - resolution: - { - integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} isobject@3.0.1: - resolution: - { - integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} istanbul-lib-coverage@3.2.2: - resolution: - { - integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} istanbul-lib-instrument@6.0.3: - resolution: - { - integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} istanbul-lib-report@3.0.1: - resolution: - { - integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} istanbul-lib-source-maps@5.0.6: - resolution: - { - integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} istanbul-reports@3.2.0: - resolution: - { - integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} jackspeak@3.4.3: - resolution: - { - integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, - } + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} jake@10.9.4: - resolution: - { - integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} hasBin: true jest-changed-files@30.2.0: - resolution: - { - integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-circus@30.2.0: - resolution: - { - integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-cli@30.2.0: - resolution: - { - integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -9383,11 +5650,8 @@ packages: optional: true jest-config@30.2.0: - resolution: - { - integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' esbuild-register: '>=3.4.0' @@ -9401,32 +5665,20 @@ packages: optional: true jest-diff@30.2.0: - resolution: - { - integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-docblock@30.2.0: - resolution: - { - integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-each@30.2.0: - resolution: - { - integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-environment-jsdom@30.3.0: - resolution: - { - integrity: sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -9434,67 +5686,40 @@ packages: optional: true jest-environment-node@30.2.0: - resolution: - { - integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-haste-map@30.2.0: - resolution: - { - integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-leak-detector@30.2.0: - resolution: - { - integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-matcher-utils@30.2.0: - resolution: - { - integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-message-util@30.2.0: - resolution: - { - integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-message-util@30.3.0: - resolution: - { - integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-mock@30.2.0: - resolution: - { - integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-mock@30.3.0: - resolution: - { - integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-pnp-resolver@1.2.3: - resolution: - { - integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} peerDependencies: jest-resolve: '*' peerDependenciesMeta: @@ -9502,116 +5727,68 @@ packages: optional: true jest-regex-util@30.0.1: - resolution: - { - integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-resolve-dependencies@30.2.0: - resolution: - { - integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-resolve@30.2.0: - resolution: - { - integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-runner@30.2.0: - resolution: - { - integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-runtime@30.2.0: - resolution: - { - integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-snapshot@30.2.0: - resolution: - { - integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-util@29.7.0: - resolution: - { - integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-util@30.2.0: - resolution: - { - integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-util@30.3.0: - resolution: - { - integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-validate@30.2.0: - resolution: - { - integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-watcher@30.2.0: - resolution: - { - integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@26.6.2: - resolution: - { - integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} jest-worker@27.5.1: - resolution: - { - integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} jest-worker@29.7.0: - resolution: - { - integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==, - } - engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-worker@30.2.0: - resolution: - { - integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest@30.2.0: - resolution: - { - integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -9620,93 +5797,54 @@ packages: optional: true jiti@1.20.0: - resolution: - { - integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==, - } + resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==} hasBin: true jiti@1.21.0: - resolution: - { - integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==, - } + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true jiti@1.21.6: - resolution: - { - integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==, - } + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true jiti@2.6.1: - resolution: - { - integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==, - } + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true jose@2.0.7: - resolution: - { - integrity: sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==, - } - engines: { node: '>=10.13.0 < 13 || >=13.7.0' } + resolution: {integrity: sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==} + engines: {node: '>=10.13.0 < 13 || >=13.7.0'} js-beautify@1.14.9: - resolution: - { - integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==} + engines: {node: '>=12'} hasBin: true js-tokens@3.0.2: - resolution: - { - integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==, - } + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-tokens@9.0.1: - resolution: - { - integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==, - } + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} js-yaml@3.14.1: - resolution: - { - integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, - } + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true js-yaml@4.1.0: - resolution: - { - integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, - } + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true js-yaml@4.1.1: - resolution: - { - integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, - } + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@26.1.0: - resolution: - { - integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -9714,1166 +5852,638 @@ packages: optional: true jsesc@0.5.0: - resolution: - { - integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==, - } + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true jsesc@2.5.2: - resolution: - { - integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} hasBin: true jsesc@3.1.0: - resolution: - { - integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-bigint@1.0.0: - resolution: - { - integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==, - } + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} json-buffer@3.0.1: - resolution: - { - integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, - } + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-better-errors@1.0.2: - resolution: - { - integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==, - } + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} json-parse-even-better-errors@2.3.1: - resolution: - { - integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, - } + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} json-schema-traverse@0.4.1: - resolution: - { - integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, - } + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-schema-traverse@1.0.0: - resolution: - { - integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==, - } + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-stable-stringify-without-jsonify@1.0.1: - resolution: - { - integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, - } + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} json5@0.5.1: - resolution: - { - integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==, - } + resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} hasBin: true json5@1.0.2: - resolution: - { - integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==, - } + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true json5@2.2.3: - resolution: - { - integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true jsonfile@4.0.0: - resolution: - { - integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, - } + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} jsonfile@6.1.0: - resolution: - { - integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==, - } + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} jsonwebtoken@8.5.1: - resolution: - { - integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==, - } - engines: { node: '>=4', npm: '>=1.4.28' } + resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} + engines: {node: '>=4', npm: '>=1.4.28'} jwa@1.4.1: - resolution: - { - integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==, - } + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} jwa@2.0.0: - resolution: - { - integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==, - } + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} jwks-rsa@2.1.5: - resolution: - { - integrity: sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==, - } - engines: { node: '>=10 < 13 || >=14' } + resolution: {integrity: sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==} + engines: {node: '>=10 < 13 || >=14'} jws@3.2.2: - resolution: - { - integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==, - } + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} jws@4.0.0: - resolution: - { - integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==, - } + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} kasi@2.0.1: - resolution: - { - integrity: sha512-8qhiHZ1BN26ig1+jQ9fWEk6dj8T1wuxs00QRJfXIANI4scto1EuPUgqj+mxHls52WBfdTNJGQ8yYw9rDpWUcgQ==, - } + resolution: {integrity: sha512-8qhiHZ1BN26ig1+jQ9fWEk6dj8T1wuxs00QRJfXIANI4scto1EuPUgqj+mxHls52WBfdTNJGQ8yYw9rDpWUcgQ==} keyv@4.5.3: - resolution: - { - integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==, - } + resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} kind-of@3.2.2: - resolution: - { - integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} kind-of@4.0.0: - resolution: - { - integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} kind-of@5.1.0: - resolution: - { - integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} + engines: {node: '>=0.10.0'} kind-of@6.0.3: - resolution: - { - integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} klona@2.0.6: - resolution: - { - integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} knitwork@1.0.0: - resolution: - { - integrity: sha512-dWl0Dbjm6Xm+kDxhPQJsCBTxrJzuGl0aP9rhr+TG8D3l+GL90N8O8lYUi7dTSAN2uuDqCtNgb6aEuQH5wsiV8Q==, - } + resolution: {integrity: sha512-dWl0Dbjm6Xm+kDxhPQJsCBTxrJzuGl0aP9rhr+TG8D3l+GL90N8O8lYUi7dTSAN2uuDqCtNgb6aEuQH5wsiV8Q==} knitwork@1.1.0: - resolution: - { - integrity: sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==, - } + resolution: {integrity: sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==} known-css-properties@0.29.0: - resolution: - { - integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==, - } + resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} last-call-webpack-plugin@3.0.0: - resolution: - { - integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==, - } + resolution: {integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==} launch-editor-middleware@2.8.0: - resolution: - { - integrity: sha512-0Az27jnPR2RgkUoZoLHluM5gg9zHeg7hPsUZESJxcTV8Rs6Fed+Nof7Lb2HmpsE8lN/3YzpU+mvK5exYWSftWw==, - } + resolution: {integrity: sha512-0Az27jnPR2RgkUoZoLHluM5gg9zHeg7hPsUZESJxcTV8Rs6Fed+Nof7Lb2HmpsE8lN/3YzpU+mvK5exYWSftWw==} launch-editor@2.8.0: - resolution: - { - integrity: sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==, - } + resolution: {integrity: sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==} leven@3.1.0: - resolution: - { - integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} levn@0.4.1: - resolution: - { - integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} libphonenumber-js@1.12.36: - resolution: - { - integrity: sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==, - } + resolution: {integrity: sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==} lilconfig@2.1.0: - resolution: - { - integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} lilconfig@3.1.3: - resolution: - { - integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} limiter@1.1.5: - resolution: - { - integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==, - } + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} lines-and-columns@1.2.4: - resolution: - { - integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, - } + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} lint-staged@16.1.4: - resolution: - { - integrity: sha512-xy7rnzQrhTVGKMpv6+bmIA3C0yET31x8OhKBYfvGo0/byeZ6E0BjGARrir3Kg/RhhYHutpsi01+2J5IpfVoueA==, - } - engines: { node: '>=20.17' } + resolution: {integrity: sha512-xy7rnzQrhTVGKMpv6+bmIA3C0yET31x8OhKBYfvGo0/byeZ6E0BjGARrir3Kg/RhhYHutpsi01+2J5IpfVoueA==} + engines: {node: '>=20.17'} hasBin: true listr2@9.0.1: - resolution: - { - integrity: sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==, - } - engines: { node: '>=20.0.0' } + resolution: {integrity: sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==} + engines: {node: '>=20.0.0'} loader-runner@2.4.0: - resolution: - { - integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==, - } - engines: { node: '>=4.3.0 <5.0.0 || >=5.10' } + resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} loader-runner@4.3.1: - resolution: - { - integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==, - } - engines: { node: '>=6.11.5' } + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} loader-utils@1.4.2: - resolution: - { - integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} loader-utils@2.0.4: - resolution: - { - integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==, - } - engines: { node: '>=8.9.0' } + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} local-pkg@0.4.3: - resolution: - { - integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} local-pkg@0.5.0: - resolution: - { - integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} locate-path@3.0.0: - resolution: - { - integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} locate-path@5.0.0: - resolution: - { - integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} locate-path@6.0.0: - resolution: - { - integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} lodash._reinterpolate@3.0.0: - resolution: - { - integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==, - } + resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} lodash.camelcase@4.3.0: - resolution: - { - integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==, - } + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} lodash.clonedeep@4.5.0: - resolution: - { - integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==, - } + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} lodash.debounce@4.0.8: - resolution: - { - integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, - } + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} lodash.includes@4.3.0: - resolution: - { - integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==, - } + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} lodash.isboolean@3.0.3: - resolution: - { - integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==, - } + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} lodash.isinteger@4.0.4: - resolution: - { - integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==, - } + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} lodash.isnumber@3.0.3: - resolution: - { - integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==, - } + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} lodash.isplainobject@4.0.6: - resolution: - { - integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==, - } + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} lodash.isstring@4.0.1: - resolution: - { - integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==, - } + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} lodash.kebabcase@4.1.1: - resolution: - { - integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==, - } + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} lodash.memoize@4.1.2: - resolution: - { - integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==, - } + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: - resolution: - { - integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, - } + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} lodash.mergewith@4.6.2: - resolution: - { - integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==, - } + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} lodash.once@4.1.1: - resolution: - { - integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==, - } + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} lodash.template@4.5.0: - resolution: - { - integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==, - } + resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.templatesettings@4.2.0: - resolution: - { - integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==, - } + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} lodash.truncate@4.4.2: - resolution: - { - integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==, - } + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} lodash.unionby@4.8.0: - resolution: - { - integrity: sha512-e60kn4GJIunNkw6v9MxRnUuLYI/Tyuanch7ozoCtk/1irJTYBj+qNTxr5B3qVflmJhwStJBv387Cb+9VOfABMg==, - } + resolution: {integrity: sha512-e60kn4GJIunNkw6v9MxRnUuLYI/Tyuanch7ozoCtk/1irJTYBj+qNTxr5B3qVflmJhwStJBv387Cb+9VOfABMg==} lodash.uniq@4.5.0: - resolution: - { - integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==, - } + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} lodash@4.17.21: - resolution: - { - integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, - } + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} log-update@6.1.0: - resolution: - { - integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} long@4.0.0: - resolution: - { - integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==, - } + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} long@5.2.3: - resolution: - { - integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==, - } + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} loose-envify@1.4.0: - resolution: - { - integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, - } + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true lower-case@2.0.2: - resolution: - { - integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==, - } + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} lru-cache@10.4.3: - resolution: - { - integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, - } + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@4.0.2: - resolution: - { - integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==, - } + resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} lru-cache@4.1.5: - resolution: - { - integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==, - } + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} lru-cache@5.1.1: - resolution: - { - integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, - } + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} lru-cache@6.0.0: - resolution: - { - integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} lru-memoizer@2.2.0: - resolution: - { - integrity: sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==, - } + resolution: {integrity: sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==} magic-string@0.30.10: - resolution: - { - integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==, - } + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} magic-string@0.30.4: - resolution: - { - integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==} + engines: {node: '>=12'} make-dir@1.3.0: - resolution: - { - integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} make-dir@2.1.0: - resolution: - { - integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} make-dir@3.1.0: - resolution: - { - integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} make-dir@4.0.0: - resolution: - { - integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} make-error@1.3.6: - resolution: - { - integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, - } + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} makeerror@1.0.12: - resolution: - { - integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==, - } + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} map-cache@0.2.2: - resolution: - { - integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} map-obj@1.0.1: - resolution: - { - integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} map-obj@4.3.0: - resolution: - { - integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} map-visit@1.0.0: - resolution: - { - integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} markdown-table@2.0.0: - resolution: - { - integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==, - } + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} material-design-lite@1.3.0: - resolution: - { - integrity: sha512-ao76b0bqSTKcEMt7Pui+J/S3eVF0b3GWfuKUwfe2lP5DKlLZOwBq37e0/bXEzxrw7/SuHAuYAdoCwY6mAYhrsg==, - } - engines: { node: '>=0.12.0' } + resolution: {integrity: sha512-ao76b0bqSTKcEMt7Pui+J/S3eVF0b3GWfuKUwfe2lP5DKlLZOwBq37e0/bXEzxrw7/SuHAuYAdoCwY6mAYhrsg==} + engines: {node: '>=0.12.0'} math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} mathml-tag-names@2.1.3: - resolution: - { - integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==, - } + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} md5.js@1.3.5: - resolution: - { - integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==, - } + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} mdn-data@2.0.14: - resolution: - { - integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==, - } + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} mdn-data@2.0.28: - resolution: - { - integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==, - } + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} mdn-data@2.0.30: - resolution: - { - integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==, - } + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} memfs@3.5.3: - resolution: - { - integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==, - } - engines: { node: '>= 4.0.0' } + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} memfs@4.9.3: - resolution: - { - integrity: sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==, - } - engines: { node: '>= 4.0.0' } + resolution: {integrity: sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==} + engines: {node: '>= 4.0.0'} memory-fs@0.4.1: - resolution: - { - integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==, - } + resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} memory-fs@0.5.0: - resolution: - { - integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==, - } - engines: { node: '>=4.3.0 <5.0.0 || >=5.10' } + resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} meow@10.1.5: - resolution: - { - integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} meow@12.1.1: - resolution: - { - integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==, - } - engines: { node: '>=16.10' } + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} meow@13.2.0: - resolution: - { - integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} merge-source-map@1.1.0: - resolution: - { - integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==, - } + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} merge-stream@2.0.0: - resolution: - { - integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, - } + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} merge2@1.4.1: - resolution: - { - integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} micromatch@3.1.10: - resolution: - { - integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} micromatch@4.0.5: - resolution: - { - integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} micromatch@4.0.8: - resolution: - { - integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} miller-rabin@4.0.1: - resolution: - { - integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==, - } + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} hasBin: true mime-db@1.52.0: - resolution: - { - integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mime-types@2.1.35: - resolution: - { - integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} mime@1.6.0: - resolution: - { - integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} hasBin: true mime@2.5.2: - resolution: - { - integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==} + engines: {node: '>=4.0.0'} hasBin: true mime@3.0.0: - resolution: - { - integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} hasBin: true mimic-fn@2.1.0: - resolution: - { - integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} mimic-fn@4.0.0: - resolution: - { - integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} mimic-function@5.0.1: - resolution: - { - integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} min-indent@1.0.1: - resolution: - { - integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} minimalistic-assert@1.0.1: - resolution: - { - integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==, - } + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} minimalistic-crypto-utils@1.0.1: - resolution: - { - integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==, - } + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} minimatch@3.0.8: - resolution: - { - integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==, - } + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} minimatch@3.1.2: - resolution: - { - integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, - } + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} minimatch@5.1.6: - resolution: - { - integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} minimatch@5.1.9: - resolution: - { - integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} minimatch@9.0.1: - resolution: - { - integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==, - } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} minimatch@9.0.5: - resolution: - { - integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, - } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} minimist-options@4.1.0: - resolution: - { - integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} minimist@1.2.8: - resolution: - { - integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, - } + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} minipass-collect@1.0.2: - resolution: - { - integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} minipass-flush@1.0.5: - resolution: - { - integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} minipass-pipeline@1.2.4: - resolution: - { - integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} minipass@3.3.6: - resolution: - { - integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} minipass@5.0.0: - resolution: - { - integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} minipass@7.1.2: - resolution: - { - integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, - } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} minizlib@2.1.2: - resolution: - { - integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} mississippi@3.0.0: - resolution: - { - integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==, - } - engines: { node: '>=4.0.0' } + resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} + engines: {node: '>=4.0.0'} mixin-deep@1.3.2: - resolution: - { - integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} mkdirp@0.5.6: - resolution: - { - integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==, - } + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true mkdirp@1.0.4: - resolution: - { - integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} hasBin: true mlly@1.4.2: - resolution: - { - integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==, - } + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} mlly@1.7.1: - resolution: - { - integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==, - } + resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} mlly@1.7.3: - resolution: - { - integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==, - } + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} moment@2.30.1: - resolution: - { - integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==, - } + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} move-concurrently@1.0.1: - resolution: - { - integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==, - } + resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} deprecated: This package is no longer supported. mri@1.2.0: - resolution: - { - integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} mrmime@1.0.1: - resolution: - { - integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} ms@2.0.0: - resolution: - { - integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==, - } + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.2: - resolution: - { - integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, - } + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} mustache@2.3.2: - resolution: - { - integrity: sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==, - } - engines: { npm: '>=1.4.0' } + resolution: {integrity: sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==} + engines: {npm: '>=1.4.0'} hasBin: true mute-stream@0.0.8: - resolution: - { - integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==, - } + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} nan@2.18.0: - resolution: - { - integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==, - } + resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} nano-spawn@1.0.2: - resolution: - { - integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==, - } - engines: { node: '>=20.17' } + resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} + engines: {node: '>=20.17'} nanoid@3.3.11: - resolution: - { - integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true nanoid@3.3.12: - resolution: - { - integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true nanoid@3.3.8: - resolution: - { - integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true nanomatch@1.2.13: - resolution: - { - integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} napi-postinstall@0.3.3: - resolution: - { - integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==, - } - engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true natural-compare@1.4.0: - resolution: - { - integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, - } + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} negotiator@0.6.3: - resolution: - { - integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} neo-async@2.6.2: - resolution: - { - integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==, - } + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} no-case@3.0.4: - resolution: - { - integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==, - } + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} node-addon-api@1.7.2: - resolution: - { - integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==, - } + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} node-cache@4.2.1: - resolution: - { - integrity: sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==, - } - engines: { node: '>= 0.4.6' } + resolution: {integrity: sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==} + engines: {node: '>= 0.4.6'} node-fetch-native@1.6.7: - resolution: - { - integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==, - } + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} node-fetch@2.7.0: - resolution: - { - integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, - } - engines: { node: 4.x || >=6.0.0 } + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: @@ -10881,1662 +6491,1011 @@ packages: optional: true node-forge@1.3.1: - resolution: - { - integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==, - } - engines: { node: '>= 6.13.0' } + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} node-html-parser@6.1.13: - resolution: - { - integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==, - } + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} node-int64@0.4.0: - resolution: - { - integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==, - } + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} node-libs-browser@2.2.1: - resolution: - { - integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==, - } + resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==} node-object-hash@1.4.2: - resolution: - { - integrity: sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==} + engines: {node: '>=0.10.0'} node-releases@2.0.27: - resolution: - { - integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, - } + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} node-res@5.0.1: - resolution: - { - integrity: sha512-YOleO9c7MAqoHC+Ccu2vzvV1fL6Ku49gShq3PIMKWHRgrMSih3XcwL05NbLBi6oU2J471gTBfdpVVxwT6Pfhxg==, - } + resolution: {integrity: sha512-YOleO9c7MAqoHC+Ccu2vzvV1fL6Ku49gShq3PIMKWHRgrMSih3XcwL05NbLBi6oU2J471gTBfdpVVxwT6Pfhxg==} nopt@6.0.0: - resolution: - { - integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==, - } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} hasBin: true normalize-package-data@2.5.0: - resolution: - { - integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==, - } + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} normalize-package-data@3.0.3: - resolution: - { - integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} normalize-path@2.1.1: - resolution: - { - integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} normalize-path@3.0.0: - resolution: - { - integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} normalize-range@0.1.2: - resolution: - { - integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} normalize-url@1.9.1: - resolution: - { - integrity: sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==} + engines: {node: '>=4'} normalize-url@6.1.0: - resolution: - { - integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} npm-run-path@4.0.1: - resolution: - { - integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} npm-run-path@5.3.0: - resolution: - { - integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} nth-check@2.1.1: - resolution: - { - integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, - } + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} nuxt-highlightjs@1.0.3: - resolution: - { - integrity: sha512-3UEEyVYwjN+tg+gFF2fC/K4+xMiGCQlZ+3c19f3MCa5l90JtV7QXfU/2NpTq3yY3BeAgAYSwLVbP1SWOhVsXaw==, - } + resolution: {integrity: sha512-3UEEyVYwjN+tg+gFF2fC/K4+xMiGCQlZ+3c19f3MCa5l90JtV7QXfU/2NpTq3yY3BeAgAYSwLVbP1SWOhVsXaw==} nuxt@2.18.1: - resolution: - { - integrity: sha512-SZFOLDKgCfLu23BrQE0YYNWeoi/h+fw07TNDNDzRfbmMvQlStgTBG7lqeELytXdQnaPKWjWAYo12K7pPPRZb9Q==, - } + resolution: {integrity: sha512-SZFOLDKgCfLu23BrQE0YYNWeoi/h+fw07TNDNDzRfbmMvQlStgTBG7lqeELytXdQnaPKWjWAYo12K7pPPRZb9Q==} deprecated: Nuxt 2 has reached EOL and is no longer actively maintained. See https://nuxt.com/blog/nuxt2-eol for more details. hasBin: true nwsapi@2.2.23: - resolution: - { - integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==, - } + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} nypm@0.3.9: - resolution: - { - integrity: sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==, - } - engines: { node: ^14.16.0 || >=16.10.0 } + resolution: {integrity: sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==} + engines: {node: ^14.16.0 || >=16.10.0} hasBin: true object-assign@4.1.1: - resolution: - { - integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} object-copy@0.1.0: - resolution: - { - integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} object-hash@3.0.0: - resolution: - { - integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} object-inspect@1.12.3: - resolution: - { - integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==, - } + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} object-keys@1.1.1: - resolution: - { - integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} object-visit@1.0.1: - resolution: - { - integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} object.assign@4.1.4: - resolution: - { - integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} object.fromentries@2.0.7: - resolution: - { - integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} object.getownpropertydescriptors@2.1.7: - resolution: - { - integrity: sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==} + engines: {node: '>= 0.8'} object.groupby@1.0.1: - resolution: - { - integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==, - } + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} object.pick@1.3.0: - resolution: - { - integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} object.values@1.1.7: - resolution: - { - integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} ohash@1.1.3: - resolution: - { - integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==, - } + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} ohash@1.1.4: - resolution: - { - integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==, - } + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} on-finished@2.3.0: - resolution: - { - integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} on-finished@2.4.1: - resolution: - { - integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} on-headers@1.0.2: - resolution: - { - integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} once@1.4.0: - resolution: - { - integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, - } + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} onetime@5.1.2: - resolution: - { - integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} onetime@6.0.0: - resolution: - { - integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} onetime@7.0.0: - resolution: - { - integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} opener@1.5.2: - resolution: - { - integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==, - } + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true optimize-css-assets-webpack-plugin@6.0.1: - resolution: - { - integrity: sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ==, - } + resolution: {integrity: sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ==} peerDependencies: webpack: ^4.0.0 optionator@0.9.3: - resolution: - { - integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} os-browserify@0.3.0: - resolution: - { - integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==, - } + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} os-tmpdir@1.0.2: - resolution: - { - integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} p-limit@2.3.0: - resolution: - { - integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} p-limit@3.1.0: - resolution: - { - integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} p-locate@3.0.0: - resolution: - { - integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} p-locate@4.1.0: - resolution: - { - integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} p-locate@5.0.0: - resolution: - { - integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} p-map@4.0.0: - resolution: - { - integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} p-try@2.2.0: - resolution: - { - integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} package-json-from-dist@1.0.1: - resolution: - { - integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, - } + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} pako@1.0.11: - resolution: - { - integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, - } + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} parallel-transform@1.2.0: - resolution: - { - integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==, - } + resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} param-case@3.0.4: - resolution: - { - integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==, - } + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} parent-module@1.0.1: - resolution: - { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} parse-asn1@5.1.6: - resolution: - { - integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==, - } + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} parse-git-config@3.0.0: - resolution: - { - integrity: sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==} + engines: {node: '>=8'} parse-json@4.0.0: - resolution: - { - integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} parse-json@5.2.0: - resolution: - { - integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} parse-path@7.0.0: - resolution: - { - integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==, - } + resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} parse-url@8.1.0: - resolution: - { - integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==, - } + resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} parse5@7.3.0: - resolution: - { - integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==, - } + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} parseurl@1.3.3: - resolution: - { - integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} pascal-case@3.1.2: - resolution: - { - integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==, - } + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} pascalcase@0.1.1: - resolution: - { - integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} path-browserify@0.0.1: - resolution: - { - integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==, - } + resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} path-dirname@1.0.2: - resolution: - { - integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==, - } + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} path-exists@3.0.0: - resolution: - { - integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} path-exists@4.0.0: - resolution: - { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} path-is-absolute@1.0.1: - resolution: - { - integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-key@4.0.0: - resolution: - { - integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} path-parse@1.0.7: - resolution: - { - integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, - } + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} path-scurry@1.11.1: - resolution: - { - integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, - } - engines: { node: '>=16 || 14 >=14.18' } + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} path-type@4.0.0: - resolution: - { - integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} path-type@5.0.0: - resolution: - { - integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} pathe@1.1.1: - resolution: - { - integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==, - } + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} pathe@1.1.2: - resolution: - { - integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, - } + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} pbkdf2@3.1.2: - resolution: - { - integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==, - } - engines: { node: '>=0.12' } + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} perfect-debounce@1.0.0: - resolution: - { - integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==, - } + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} picocolors@0.2.1: - resolution: - { - integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==, - } + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} picocolors@1.0.0: - resolution: - { - integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, - } + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: - resolution: - { - integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} picomatch@2.3.2: - resolution: - { - integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, - } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} picomatch@4.0.3: - resolution: - { - integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} picomatch@4.0.4: - resolution: - { - integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} pidtree@0.6.0: - resolution: - { - integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} hasBin: true pify@2.3.0: - resolution: - { - integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} pify@3.0.0: - resolution: - { - integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} pify@4.0.1: - resolution: - { - integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} pify@5.0.0: - resolution: - { - integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} pirates@4.0.7: - resolution: - { - integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} pkg-dir@3.0.0: - resolution: - { - integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} pkg-dir@4.2.0: - resolution: - { - integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} pkg-types@1.1.2: - resolution: - { - integrity: sha512-VEGf1he2DR5yowYRl0XJhWJq5ktm9gYIsH+y8sNJpHlxch7JPDaufgrsl4vYjd9hMUY8QVjoNncKbow9I7exyA==, - } + resolution: {integrity: sha512-VEGf1he2DR5yowYRl0XJhWJq5ktm9gYIsH+y8sNJpHlxch7JPDaufgrsl4vYjd9hMUY8QVjoNncKbow9I7exyA==} pkg-types@1.2.1: - resolution: - { - integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==, - } + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} pluralize@8.0.0: - resolution: - { - integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} pngjs@5.0.0: - resolution: - { - integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} pnp-webpack-plugin@1.7.0: - resolution: - { - integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} + engines: {node: '>=6'} posix-character-classes@0.1.1: - resolution: - { - integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} postcss-attribute-case-insensitive@6.0.3: - resolution: - { - integrity: sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-calc@10.0.0: - resolution: - { - integrity: sha512-OmjhudoNTP0QleZCwl1i6NeBwN+5MZbY5ersLZz69mjJiDVv/p57RjRuKDkHeDWr4T+S97wQfsqRTNoDHB2e3g==, - } - engines: { node: ^18.12 || ^20.9 || >=22.0 } + resolution: {integrity: sha512-OmjhudoNTP0QleZCwl1i6NeBwN+5MZbY5ersLZz69mjJiDVv/p57RjRuKDkHeDWr4T+S97wQfsqRTNoDHB2e3g==} + engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: postcss: ^8.4.38 postcss-calc@8.2.4: - resolution: - { - integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==, - } + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: postcss: ^8.2.2 postcss-clamp@4.1.0: - resolution: - { - integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==, - } - engines: { node: '>=7.6.0' } + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} peerDependencies: postcss: ^8.4.6 postcss-color-functional-notation@6.0.12: - resolution: - { - integrity: sha512-LGLWl6EDofJwDHMElYvt4YU9AeH+oijzOfeKhE0ebuu0aBSDeEg7CfFXMi0iiXWV1VKxn3MLGOtcBNnOiQS9Yg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-LGLWl6EDofJwDHMElYvt4YU9AeH+oijzOfeKhE0ebuu0aBSDeEg7CfFXMi0iiXWV1VKxn3MLGOtcBNnOiQS9Yg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-color-hex-alpha@9.0.4: - resolution: - { - integrity: sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-color-rebeccapurple@9.0.3: - resolution: - { - integrity: sha512-ruBqzEFDYHrcVq3FnW3XHgwRqVMrtEPLBtD7K2YmsLKVc2jbkxzzNEctJKsPCpDZ+LeMHLKRDoSShVefGc+CkQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-ruBqzEFDYHrcVq3FnW3XHgwRqVMrtEPLBtD7K2YmsLKVc2jbkxzzNEctJKsPCpDZ+LeMHLKRDoSShVefGc+CkQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-colormin@5.3.1: - resolution: - { - integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-colormin@7.0.1: - resolution: - { - integrity: sha512-uszdT0dULt3FQs47G5UHCduYK+FnkLYlpu1HpWu061eGsKZ7setoG7kA+WC9NQLsOJf69D5TxGHgnAdRgylnFQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-uszdT0dULt3FQs47G5UHCduYK+FnkLYlpu1HpWu061eGsKZ7setoG7kA+WC9NQLsOJf69D5TxGHgnAdRgylnFQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-convert-values@5.1.3: - resolution: - { - integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-convert-values@7.0.1: - resolution: - { - integrity: sha512-9x2ofb+hYPwHWMlWAzyWys2yMDZYGfkX9LodbaVTmLdlupmtH2AGvj8Up95wzzNPRDEzPIxQIkUaPJew3bT6xA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-9x2ofb+hYPwHWMlWAzyWys2yMDZYGfkX9LodbaVTmLdlupmtH2AGvj8Up95wzzNPRDEzPIxQIkUaPJew3bT6xA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-custom-media@10.0.7: - resolution: - { - integrity: sha512-o2k5nnvRZhF36pr1fGFM7a1EMTcNdKNO70Tp1g2lfpYgiwIctR7ic4acBCDHBMYRcQ8mFlaBB1QsEywqrSIaFQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-o2k5nnvRZhF36pr1fGFM7a1EMTcNdKNO70Tp1g2lfpYgiwIctR7ic4acBCDHBMYRcQ8mFlaBB1QsEywqrSIaFQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-custom-properties@13.3.11: - resolution: - { - integrity: sha512-CAIgz03I/GMhVbAKIi3u3P8j5JY2KHl0TlePcfUX3OUy8t0ynnWvyJaS1D92pEAw1LjmeKWi7+aIU0s53iYdOQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-CAIgz03I/GMhVbAKIi3u3P8j5JY2KHl0TlePcfUX3OUy8t0ynnWvyJaS1D92pEAw1LjmeKWi7+aIU0s53iYdOQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-custom-selectors@7.1.11: - resolution: - { - integrity: sha512-IoGprXOueDJL5t3ZuWR+QzPpmrQCFNhvoICsg0vDSehGwWNG0YV/Z4A+zouGRonC7NJThoV+A8A74IEMqMQUQw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-IoGprXOueDJL5t3ZuWR+QzPpmrQCFNhvoICsg0vDSehGwWNG0YV/Z4A+zouGRonC7NJThoV+A8A74IEMqMQUQw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-dir-pseudo-class@8.0.1: - resolution: - { - integrity: sha512-uULohfWBBVoFiZXgsQA24JV6FdKIidQ+ZqxOouhWwdE+qJlALbkS5ScB43ZTjPK+xUZZhlaO/NjfCt5h4IKUfw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-uULohfWBBVoFiZXgsQA24JV6FdKIidQ+ZqxOouhWwdE+qJlALbkS5ScB43ZTjPK+xUZZhlaO/NjfCt5h4IKUfw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-discard-comments@5.1.2: - resolution: - { - integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-discard-comments@7.0.1: - resolution: - { - integrity: sha512-GVrQxUOhmle1W6jX2SvNLt4kmN+JYhV7mzI6BMnkAWR9DtVvg8e67rrV0NfdWhn7x1zxvzdWkMBPdBDCls+uwQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-GVrQxUOhmle1W6jX2SvNLt4kmN+JYhV7mzI6BMnkAWR9DtVvg8e67rrV0NfdWhn7x1zxvzdWkMBPdBDCls+uwQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-discard-duplicates@5.1.0: - resolution: - { - integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-discard-duplicates@7.0.0: - resolution: - { - integrity: sha512-bAnSuBop5LpAIUmmOSsuvtKAAKREB6BBIYStWUTGq8oG5q9fClDMMuY8i4UPI/cEcDx2TN+7PMnXYIId20UVDw==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-bAnSuBop5LpAIUmmOSsuvtKAAKREB6BBIYStWUTGq8oG5q9fClDMMuY8i4UPI/cEcDx2TN+7PMnXYIId20UVDw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-discard-empty@5.1.1: - resolution: - { - integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-discard-empty@7.0.0: - resolution: - { - integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-discard-overridden@5.1.0: - resolution: - { - integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-discard-overridden@7.0.0: - resolution: - { - integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-double-position-gradients@5.0.6: - resolution: - { - integrity: sha512-QJ+089FKMaqDxOhhIHsJrh4IP7h4PIHNC5jZP5PMmnfUScNu8Hji2lskqpFWCvu+5sj+2EJFyzKd13sLEWOZmQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-QJ+089FKMaqDxOhhIHsJrh4IP7h4PIHNC5jZP5PMmnfUScNu8Hji2lskqpFWCvu+5sj+2EJFyzKd13sLEWOZmQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-focus-visible@9.0.1: - resolution: - { - integrity: sha512-N2VQ5uPz3Z9ZcqI5tmeholn4d+1H14fKXszpjogZIrFbhaq0zNAtq8sAnw6VLiqGbL8YBzsnu7K9bBkTqaRimQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-N2VQ5uPz3Z9ZcqI5tmeholn4d+1H14fKXszpjogZIrFbhaq0zNAtq8sAnw6VLiqGbL8YBzsnu7K9bBkTqaRimQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-focus-within@8.0.1: - resolution: - { - integrity: sha512-NFU3xcY/xwNaapVb+1uJ4n23XImoC86JNwkY/uduytSl2s9Ekc2EpzmRR63+ExitnW3Mab3Fba/wRPCT5oDILA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-NFU3xcY/xwNaapVb+1uJ4n23XImoC86JNwkY/uduytSl2s9Ekc2EpzmRR63+ExitnW3Mab3Fba/wRPCT5oDILA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-font-variant@5.0.0: - resolution: - { - integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==, - } + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} peerDependencies: postcss: ^8.1.0 postcss-gap-properties@5.0.1: - resolution: - { - integrity: sha512-k2z9Cnngc24c0KF4MtMuDdToROYqGMMUQGcE6V0odwjHyOHtaDBlLeRBV70y9/vF7KIbShrTRZ70JjsI1BZyWw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-k2z9Cnngc24c0KF4MtMuDdToROYqGMMUQGcE6V0odwjHyOHtaDBlLeRBV70y9/vF7KIbShrTRZ70JjsI1BZyWw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-html@1.8.1: - resolution: - { - integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==, - } - engines: { node: ^12 || >=14 } + resolution: {integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==} + engines: {node: ^12 || >=14} postcss-image-set-function@6.0.3: - resolution: - { - integrity: sha512-i2bXrBYzfbRzFnm+pVuxVePSTCRiNmlfssGI4H0tJQvDue+yywXwUxe68VyzXs7cGtMaH6MCLY6IbCShrSroCw==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-i2bXrBYzfbRzFnm+pVuxVePSTCRiNmlfssGI4H0tJQvDue+yywXwUxe68VyzXs7cGtMaH6MCLY6IbCShrSroCw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-import-resolver@2.0.0: - resolution: - { - integrity: sha512-y001XYgGvVwgxyxw9J1a5kqM/vtmIQGzx34g0A0Oy44MFcy/ZboZw1hu/iN3VYFjSTRzbvd7zZJJz0Kh0AGkTw==, - } + resolution: {integrity: sha512-y001XYgGvVwgxyxw9J1a5kqM/vtmIQGzx34g0A0Oy44MFcy/ZboZw1hu/iN3VYFjSTRzbvd7zZJJz0Kh0AGkTw==} postcss-import@15.1.0: - resolution: - { - integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 postcss-lab-function@6.0.17: - resolution: - { - integrity: sha512-QzjC6/3J6XKZzHGuUKhWNvlDMfWo+08dQOfQj4vWQdpZFdOxCh9QCR4w4XbV68EkdzywJie1mcm81jwFyV0+kg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-QzjC6/3J6XKZzHGuUKhWNvlDMfWo+08dQOfQj4vWQdpZFdOxCh9QCR4w4XbV68EkdzywJie1mcm81jwFyV0+kg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-loader@4.3.0: - resolution: - { - integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==} + engines: {node: '>= 10.13.0'} peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^4.0.0 || ^5.0.0 postcss-logical@7.0.1: - resolution: - { - integrity: sha512-8GwUQZE0ri0K0HJHkDv87XOLC8DE0msc+HoWLeKdtjDZEwpZ5xuK3QdV6FhmHSQW40LPkg43QzvATRAI3LsRkg==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-8GwUQZE0ri0K0HJHkDv87XOLC8DE0msc+HoWLeKdtjDZEwpZ5xuK3QdV6FhmHSQW40LPkg43QzvATRAI3LsRkg==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-merge-longhand@5.1.7: - resolution: - { - integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-merge-longhand@7.0.2: - resolution: - { - integrity: sha512-06vrW6ZWi9qeP7KMS9fsa9QW56+tIMW55KYqF7X3Ccn+NI2pIgPV6gFfvXTMQ05H90Y5DvnCDPZ2IuHa30PMUg==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-06vrW6ZWi9qeP7KMS9fsa9QW56+tIMW55KYqF7X3Ccn+NI2pIgPV6gFfvXTMQ05H90Y5DvnCDPZ2IuHa30PMUg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-merge-rules@5.1.4: - resolution: - { - integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-merge-rules@7.0.2: - resolution: - { - integrity: sha512-VAR47UNvRsdrTHLe7TV1CeEtF9SJYR5ukIB9U4GZyZOptgtsS20xSxy+k5wMrI3udST6O1XuIn7cjQkg7sDAAw==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-VAR47UNvRsdrTHLe7TV1CeEtF9SJYR5ukIB9U4GZyZOptgtsS20xSxy+k5wMrI3udST6O1XuIn7cjQkg7sDAAw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-minify-font-values@5.1.0: - resolution: - { - integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-minify-font-values@7.0.0: - resolution: - { - integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-minify-gradients@5.1.1: - resolution: - { - integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-minify-gradients@7.0.0: - resolution: - { - integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-minify-params@5.1.4: - resolution: - { - integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-minify-params@7.0.1: - resolution: - { - integrity: sha512-e+Xt8xErSRPgSRFxHeBCSxMiO8B8xng7lh8E0A5ep1VfwYhY8FXhu4Q3APMjgx9YDDbSp53IBGENrzygbUvgUQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-e+Xt8xErSRPgSRFxHeBCSxMiO8B8xng7lh8E0A5ep1VfwYhY8FXhu4Q3APMjgx9YDDbSp53IBGENrzygbUvgUQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-minify-selectors@5.2.1: - resolution: - { - integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-minify-selectors@7.0.2: - resolution: - { - integrity: sha512-dCzm04wqW1uqLmDZ41XYNBJfjgps3ZugDpogAmJXoCb5oCiTzIX4oPXXKxDpTvWOnKxQKR4EbV4ZawJBLcdXXA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-dCzm04wqW1uqLmDZ41XYNBJfjgps3ZugDpogAmJXoCb5oCiTzIX4oPXXKxDpTvWOnKxQKR4EbV4ZawJBLcdXXA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-modules-extract-imports@3.0.0: - resolution: - { - integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==, - } - engines: { node: ^10 || ^12 || >= 14 } + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 postcss-modules-local-by-default@4.0.3: - resolution: - { - integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==, - } - engines: { node: ^10 || ^12 || >= 14 } + resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 postcss-modules-scope@3.0.0: - resolution: - { - integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==, - } - engines: { node: ^10 || ^12 || >= 14 } + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 postcss-modules-values@4.0.0: - resolution: - { - integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==, - } - engines: { node: ^10 || ^12 || >= 14 } + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 postcss-nesting@12.1.5: - resolution: - { - integrity: sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-normalize-charset@5.1.0: - resolution: - { - integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-charset@7.0.0: - resolution: - { - integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-display-values@5.1.0: - resolution: - { - integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-display-values@7.0.0: - resolution: - { - integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-positions@5.1.1: - resolution: - { - integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-positions@7.0.0: - resolution: - { - integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-repeat-style@5.1.1: - resolution: - { - integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-repeat-style@7.0.0: - resolution: - { - integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-string@5.1.0: - resolution: - { - integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-string@7.0.0: - resolution: - { - integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-timing-functions@5.1.0: - resolution: - { - integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-timing-functions@7.0.0: - resolution: - { - integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-unicode@5.1.1: - resolution: - { - integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-unicode@7.0.1: - resolution: - { - integrity: sha512-PTPGdY9xAkTw+8ZZ71DUePb7M/Vtgkbbq+EoI33EuyQEzbKemEQMhe5QSr0VP5UfZlreANDPxSfcdSprENcbsg==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-PTPGdY9xAkTw+8ZZ71DUePb7M/Vtgkbbq+EoI33EuyQEzbKemEQMhe5QSr0VP5UfZlreANDPxSfcdSprENcbsg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-url@5.1.0: - resolution: - { - integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-url@7.0.0: - resolution: - { - integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-normalize-whitespace@5.1.1: - resolution: - { - integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-normalize-whitespace@7.0.0: - resolution: - { - integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-opacity-percentage@2.0.0: - resolution: - { - integrity: sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.2 postcss-ordered-values@5.1.3: - resolution: - { - integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-ordered-values@7.0.1: - resolution: - { - integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-overflow-shorthand@5.0.1: - resolution: - { - integrity: sha512-XzjBYKLd1t6vHsaokMV9URBt2EwC9a7nDhpQpjoPk2HRTSQfokPfyAS/Q7AOrzUu6q+vp/GnrDBGuj/FCaRqrQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-XzjBYKLd1t6vHsaokMV9URBt2EwC9a7nDhpQpjoPk2HRTSQfokPfyAS/Q7AOrzUu6q+vp/GnrDBGuj/FCaRqrQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-page-break@3.0.4: - resolution: - { - integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==, - } + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} peerDependencies: postcss: ^8 postcss-place@9.0.1: - resolution: - { - integrity: sha512-JfL+paQOgRQRMoYFc2f73pGuG/Aw3tt4vYMR6UA3cWVMxivviPTnMFnFTczUJOA4K2Zga6xgQVE+PcLs64WC8Q==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-JfL+paQOgRQRMoYFc2f73pGuG/Aw3tt4vYMR6UA3cWVMxivviPTnMFnFTczUJOA4K2Zga6xgQVE+PcLs64WC8Q==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-preset-env@9.5.15: - resolution: - { - integrity: sha512-z/2akOVQChOGAdzaUR4pQrDOM3xGZc5/k4THHWyREbWAfngaJATA2SkEQMkiyV5Y/EoSwE0nt0IiaIs6CMmxfQ==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-z/2akOVQChOGAdzaUR4pQrDOM3xGZc5/k4THHWyREbWAfngaJATA2SkEQMkiyV5Y/EoSwE0nt0IiaIs6CMmxfQ==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-pseudo-class-any-link@9.0.2: - resolution: - { - integrity: sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-reduce-initial@5.1.2: - resolution: - { - integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-reduce-initial@7.0.1: - resolution: - { - integrity: sha512-0JDUSV4bGB5FGM5g8MkS+rvqKukJZ7OTHw/lcKn7xPNqeaqJyQbUO8/dJpvyTpaVwPsd3Uc33+CfNzdVowp2WA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-0JDUSV4bGB5FGM5g8MkS+rvqKukJZ7OTHw/lcKn7xPNqeaqJyQbUO8/dJpvyTpaVwPsd3Uc33+CfNzdVowp2WA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-reduce-transforms@5.1.0: - resolution: - { - integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-reduce-transforms@7.0.0: - resolution: - { - integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-replace-overflow-wrap@4.0.0: - resolution: - { - integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==, - } + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} peerDependencies: postcss: ^8.0.3 postcss-resolve-nested-selector@0.1.1: - resolution: - { - integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==, - } + resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} postcss-safe-parser@6.0.0: - resolution: - { - integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==, - } - engines: { node: '>=12.0' } + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} peerDependencies: postcss: ^8.3.3 postcss-selector-not@7.0.2: - resolution: - { - integrity: sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==, - } - engines: { node: ^14 || ^16 || >=18 } + resolution: {integrity: sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: postcss: ^8.4 postcss-selector-parser@6.0.13: - resolution: - { - integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} postcss-selector-parser@6.1.2: - resolution: - { - integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} postcss-svgo@5.1.0: - resolution: - { - integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-svgo@7.0.1: - resolution: - { - integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >= 18 } + resolution: {integrity: sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==} + engines: {node: ^18.12.0 || ^20.9.0 || >= 18} peerDependencies: postcss: ^8.4.31 postcss-unique-selectors@5.1.1: - resolution: - { - integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 postcss-unique-selectors@7.0.1: - resolution: - { - integrity: sha512-MH7QE/eKUftTB5ta40xcHLl7hkZjgDFydpfTK+QWXeHxghVt3VoPqYL5/G+zYZPPIs+8GuqFXSTgxBSoB1RZtQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-MH7QE/eKUftTB5ta40xcHLl7hkZjgDFydpfTK+QWXeHxghVt3VoPqYL5/G+zYZPPIs+8GuqFXSTgxBSoB1RZtQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 postcss-url@10.1.3: - resolution: - { - integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==} + engines: {node: '>=10'} peerDependencies: postcss: ^8.0.0 postcss-value-parser@4.2.0: - resolution: - { - integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==, - } + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} postcss@7.0.39: - resolution: - { - integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} postcss@8.4.31: - resolution: - { - integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} postcss@8.5.14: - resolution: - { - integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: - resolution: - { - integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: - resolution: - { - integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} prepend-http@1.0.4: - resolution: - { - integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} + engines: {node: '>=0.10.0'} prettier@2.8.8: - resolution: - { - integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} hasBin: true prettier@3.8.1: - resolution: - { - integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} hasBin: true pretty-bytes@5.6.0: - resolution: - { - integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} pretty-error@2.1.2: - resolution: - { - integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==, - } + resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==} pretty-format@30.2.0: - resolution: - { - integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} pretty-format@30.3.0: - resolution: - { - integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==, - } - engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} pretty-time@1.1.0: - resolution: - { - integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} pretty@2.0.0: - resolution: - { - integrity: sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==} + engines: {node: '>=0.10.0'} process-nextick-args@2.0.1: - resolution: - { - integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, - } + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} process@0.11.10: - resolution: - { - integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==, - } - engines: { node: '>= 0.6.0' } + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} promise-inflight@1.0.1: - resolution: - { - integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==, - } + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: bluebird: '*' peerDependenciesMeta: @@ -12544,618 +7503,339 @@ packages: optional: true proper-lockfile@4.1.2: - resolution: - { - integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==, - } + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} proto-list@1.2.4: - resolution: - { - integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==, - } + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} proto3-json-serializer@0.1.9: - resolution: - { - integrity: sha512-A60IisqvnuI45qNRygJjrnNjX2TMdQGMY+57tR3nul3ZgO2zXkR9OGR8AXxJhkqx84g0FTnrfi3D5fWMSdANdQ==, - } + resolution: {integrity: sha512-A60IisqvnuI45qNRygJjrnNjX2TMdQGMY+57tR3nul3ZgO2zXkR9OGR8AXxJhkqx84g0FTnrfi3D5fWMSdANdQ==} protobufjs@6.11.3: - resolution: - { - integrity: sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==, - } + resolution: {integrity: sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==} hasBin: true protobufjs@6.11.6: - resolution: - { - integrity: sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==, - } + resolution: {integrity: sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==} hasBin: true protobufjs@7.5.8: - resolution: - { - integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==, - } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} + engines: {node: '>=12.0.0'} protocols@2.0.1: - resolution: - { - integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==, - } + resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} proxy-from-env@1.1.0: - resolution: - { - integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, - } + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} prr@1.0.1: - resolution: - { - integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==, - } + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} pseudomap@1.0.2: - resolution: - { - integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==, - } + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} public-encrypt@4.0.3: - resolution: - { - integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==, - } + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} pump@2.0.1: - resolution: - { - integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==, - } + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} pump@3.0.0: - resolution: - { - integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==, - } + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} pumpify@1.5.1: - resolution: - { - integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==, - } + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} pumpify@2.0.1: - resolution: - { - integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==, - } + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} punycode@1.4.1: - resolution: - { - integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==, - } + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} punycode@2.3.0: - resolution: - { - integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} punycode@2.3.1: - resolution: - { - integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} pure-rand@7.0.1: - resolution: - { - integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==, - } + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} pusher-js@8.4.0: - resolution: - { - integrity: sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==, - } + resolution: {integrity: sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==} qrcode@1.5.4: - resolution: - { - integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} hasBin: true qs@6.11.2: - resolution: - { - integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==, - } - engines: { node: '>=0.6' } + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} query-string@4.3.4: - resolution: - { - integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==} + engines: {node: '>=0.10.0'} querystring-es3@0.2.1: - resolution: - { - integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==, - } - engines: { node: '>=0.4.x' } + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} queue-microtask@1.2.3: - resolution: - { - integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, - } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quick-lru@5.1.1: - resolution: - { - integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} randombytes@2.1.0: - resolution: - { - integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==, - } + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} randomfill@1.0.4: - resolution: - { - integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==, - } + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} range-parser@1.2.1: - resolution: - { - integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} rc9@2.1.1: - resolution: - { - integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==, - } + resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==} rc9@2.1.2: - resolution: - { - integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==, - } + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} react-is@18.3.1: - resolution: - { - integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==, - } + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} read-cache@1.0.0: - resolution: - { - integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==, - } + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} read-pkg-up@7.0.1: - resolution: - { - integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} read-pkg-up@8.0.0: - resolution: - { - integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} read-pkg@5.2.0: - resolution: - { - integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} read-pkg@6.0.0: - resolution: - { - integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} readable-stream@2.3.8: - resolution: - { - integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, - } + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} readable-stream@3.6.2: - resolution: - { - integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} readdirp@2.2.1: - resolution: - { - integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} readdirp@3.6.0: - resolution: - { - integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, - } - engines: { node: '>=8.10.0' } + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} redent@4.0.0: - resolution: - { - integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} regenerate-unicode-properties@10.1.1: - resolution: - { - integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} regenerate@1.4.2: - resolution: - { - integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==, - } + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} regenerator-runtime@0.11.1: - resolution: - { - integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==, - } + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} regenerator-runtime@0.14.1: - resolution: - { - integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, - } + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} regenerator-transform@0.15.2: - resolution: - { - integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, - } + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} regex-not@1.0.2: - resolution: - { - integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} regexp-tree@0.1.27: - resolution: - { - integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==, - } + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true regexp.prototype.flags@1.5.1: - resolution: - { - integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} regexpp@3.2.0: - resolution: - { - integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} regexpu-core@5.3.2: - resolution: - { - integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} regjsparser@0.9.1: - resolution: - { - integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==, - } + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true relateurl@0.2.7: - resolution: - { - integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==, - } - engines: { node: '>= 0.10' } + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} remove-trailing-separator@1.1.0: - resolution: - { - integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==, - } + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} renderkid@2.0.7: - resolution: - { - integrity: sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==, - } + resolution: {integrity: sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==} repeat-element@1.1.4: - resolution: - { - integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} repeat-string@1.6.1: - resolution: - { - integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==, - } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} require-directory@2.1.1: - resolution: - { - integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} require-from-string@2.0.2: - resolution: - { - integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} require-main-filename@2.0.0: - resolution: - { - integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==, - } + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} resolve-cwd@3.0.0: - resolution: - { - integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} resolve-from@4.0.0: - resolution: - { - integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve-from@5.0.0: - resolution: - { - integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolve-url@0.2.1: - resolution: - { - integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==, - } + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated resolve@1.22.6: - resolution: - { - integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==, - } + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} hasBin: true restore-cursor@3.1.0: - resolution: - { - integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} restore-cursor@5.1.0: - resolution: - { - integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} ret@0.1.15: - resolution: - { - integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==, - } - engines: { node: '>=0.12' } + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} retry-request@4.2.2: - resolution: - { - integrity: sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==, - } - engines: { node: '>=8.10.0' } + resolution: {integrity: sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==} + engines: {node: '>=8.10.0'} retry@0.12.0: - resolution: - { - integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} retry@0.13.1: - resolution: - { - integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} reusify@1.0.4: - resolution: - { - integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==, - } - engines: { iojs: '>=1.0.0', node: '>=0.10.0' } + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rfdc@1.4.1: - resolution: - { - integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, - } + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rimraf@2.7.1: - resolution: - { - integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==, - } + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: - resolution: - { - integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, - } + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true ripemd160@2.0.2: - resolution: - { - integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==, - } + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} rollup@2.79.2: - resolution: - { - integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} hasBin: true rollup@3.30.0: - resolution: - { - integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==, - } - engines: { node: '>=14.18.0', npm: '>=8.0.0' } + resolution: {integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true rrweb-cssom@0.8.0: - resolution: - { - integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==, - } + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} run-async@2.4.1: - resolution: - { - integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==, - } - engines: { node: '>=0.12.0' } + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} run-parallel@1.2.0: - resolution: - { - integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, - } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} run-queue@1.0.3: - resolution: - { - integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==, - } + resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==} rxjs@6.6.7: - resolution: - { - integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==, - } - engines: { npm: '>=2.0.0' } + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} safe-array-concat@1.0.1: - resolution: - { - integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==, - } - engines: { node: '>=0.4' } + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} safe-buffer@5.1.2: - resolution: - { - integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, - } + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} safe-buffer@5.2.1: - resolution: - { - integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, - } + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safe-regex-test@1.0.0: - resolution: - { - integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==, - } + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} safe-regex@1.1.0: - resolution: - { - integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==, - } + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} safe-regex@2.1.1: - resolution: - { - integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==, - } + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} sass-loader@10.4.1: - resolution: - { - integrity: sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==} + engines: {node: '>= 10.13.0'} peerDependencies: fibers: '>= 3.1.0' node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -13170,962 +7850,548 @@ packages: optional: true sass@1.32.13: - resolution: - { - integrity: sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==, - } - engines: { node: '>=8.9.0' } + resolution: {integrity: sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==} + engines: {node: '>=8.9.0'} hasBin: true sax@1.4.1: - resolution: - { - integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==, - } + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} saxes@6.0.0: - resolution: - { - integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==, - } - engines: { node: '>=v12.22.7' } + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} schema-utils@1.0.0: - resolution: - { - integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==, - } - engines: { node: '>= 4' } + resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==} + engines: {node: '>= 4'} schema-utils@2.7.0: - resolution: - { - integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==, - } - engines: { node: '>= 8.9.0' } + resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} + engines: {node: '>= 8.9.0'} schema-utils@2.7.1: - resolution: - { - integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==, - } - engines: { node: '>= 8.9.0' } + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} schema-utils@3.3.0: - resolution: - { - integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} schema-utils@4.3.3: - resolution: - { - integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} scule@0.2.1: - resolution: - { - integrity: sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==, - } + resolution: {integrity: sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==} scule@1.0.0: - resolution: - { - integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==, - } + resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} scule@1.3.0: - resolution: - { - integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==, - } + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} semver@5.7.2: - resolution: - { - integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==, - } + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true semver@6.3.1: - resolution: - { - integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, - } + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true semver@7.5.4: - resolution: - { - integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} hasBin: true semver@7.7.2: - resolution: - { - integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} hasBin: true semver@7.7.3: - resolution: - { - integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} hasBin: true send@0.19.0: - resolution: - { - integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} serialize-javascript@4.0.0: - resolution: - { - integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==, - } + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} serialize-javascript@5.0.1: - resolution: - { - integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==, - } + resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==} serialize-javascript@6.0.1: - resolution: - { - integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==, - } + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} serialize-javascript@6.0.2: - resolution: - { - integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==, - } + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} serve-placeholder@2.0.2: - resolution: - { - integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==, - } + resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} serve-static@1.16.2: - resolution: - { - integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} server-destroy@1.0.1: - resolution: - { - integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==, - } + resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} set-blocking@2.0.0: - resolution: - { - integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==, - } + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} set-function-name@2.0.1: - resolution: - { - integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} set-value@2.0.1: - resolution: - { - integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} setimmediate@1.0.5: - resolution: - { - integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==, - } + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} setprototypeof@1.2.0: - resolution: - { - integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, - } + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} sha.js@2.4.11: - resolution: - { - integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==, - } + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} shell-quote@1.8.1: - resolution: - { - integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==, - } + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} side-channel@1.0.4: - resolution: - { - integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==, - } + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} signal-exit@3.0.7: - resolution: - { - integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, - } + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} signal-exit@4.1.0: - resolution: - { - integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, - } - engines: { node: '>=14' } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} sirv@2.0.3: - resolution: - { - integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==, - } - engines: { node: '>= 10' } + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} sitemap@4.1.1: - resolution: - { - integrity: sha512-+8yd66IxyIFEMFkFpVoPuoPwBvdiL7Ap/HS5YD7igqO4phkyTPFIprCAE9NMHehAY5ZGN3MkAze4lDrOAX3sVQ==, - } - engines: { node: '>=8.9.0', npm: '>=5.6.0' } + resolution: {integrity: sha512-+8yd66IxyIFEMFkFpVoPuoPwBvdiL7Ap/HS5YD7igqO4phkyTPFIprCAE9NMHehAY5ZGN3MkAze4lDrOAX3sVQ==} + engines: {node: '>=8.9.0', npm: '>=5.6.0'} hasBin: true slash@3.0.0: - resolution: - { - integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} slash@4.0.0: - resolution: - { - integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} slash@5.1.0: - resolution: - { - integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==, - } - engines: { node: '>=14.16' } + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} slice-ansi@4.0.0: - resolution: - { - integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} slice-ansi@5.0.0: - resolution: - { - integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} slice-ansi@7.1.0: - resolution: - { - integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} snapdragon-node@2.1.1: - resolution: - { - integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} snapdragon-util@3.0.1: - resolution: - { - integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} snapdragon@0.8.2: - resolution: - { - integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} sort-keys@1.1.2: - resolution: - { - integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} sort-keys@2.0.0: - resolution: - { - integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} source-list-map@2.0.1: - resolution: - { - integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==, - } + resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} source-map-js@1.0.2: - resolution: - { - integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} source-map-resolve@0.5.3: - resolution: - { - integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==, - } + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} deprecated: See https://github.com/lydell/source-map-resolve#deprecated source-map-support@0.5.13: - resolution: - { - integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==, - } + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} source-map-support@0.5.21: - resolution: - { - integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, - } + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} source-map-url@0.4.1: - resolution: - { - integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==, - } + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} deprecated: See https://github.com/lydell/source-map-url#deprecated source-map@0.5.6: - resolution: - { - integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} source-map@0.5.7: - resolution: - { - integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} source-map@0.6.1: - resolution: - { - integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} source-map@0.7.6: - resolution: - { - integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==, - } - engines: { node: '>= 12' } + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} spdx-correct@3.2.0: - resolution: - { - integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==, - } + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} spdx-exceptions@2.3.0: - resolution: - { - integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==, - } + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} spdx-expression-parse@3.0.1: - resolution: - { - integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==, - } + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} spdx-license-ids@3.0.15: - resolution: - { - integrity: sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==, - } + resolution: {integrity: sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==} split-string@3.1.0: - resolution: - { - integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} split2@4.2.0: - resolution: - { - integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==, - } - engines: { node: '>= 10.x' } + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} sprintf-js@1.0.3: - resolution: - { - integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, - } + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} ssri@6.0.2: - resolution: - { - integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==, - } + resolution: {integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==} ssri@8.0.1: - resolution: - { - integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} stable@0.1.8: - resolution: - { - integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==, - } + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' stack-trace@0.0.10: - resolution: - { - integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==, - } + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} stack-utils@2.0.6: - resolution: - { - integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} stackframe@1.3.4: - resolution: - { - integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==, - } + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} static-extend@0.1.2: - resolution: - { - integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} statuses@1.5.0: - resolution: - { - integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==, - } - engines: { node: '>= 0.6' } + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} statuses@2.0.1: - resolution: - { - integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} std-env@3.7.0: - resolution: - { - integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==, - } + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} stream-browserify@2.0.2: - resolution: - { - integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==, - } + resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} stream-each@1.2.3: - resolution: - { - integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==, - } + resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==} stream-events@1.0.5: - resolution: - { - integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==, - } + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} stream-http@2.8.3: - resolution: - { - integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==, - } + resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} stream-shift@1.0.1: - resolution: - { - integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==, - } + resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} strict-uri-encode@1.1.0: - resolution: - { - integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} string-argv@0.3.2: - resolution: - { - integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==, - } - engines: { node: '>=0.6.19' } + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} string-length@4.0.2: - resolution: - { - integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} string-width@4.2.3: - resolution: - { - integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} string-width@5.1.2: - resolution: - { - integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} string-width@7.2.0: - resolution: - { - integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} string.prototype.trim@1.2.8: - resolution: - { - integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} string.prototype.trimend@1.0.7: - resolution: - { - integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==, - } + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} string.prototype.trimstart@1.0.7: - resolution: - { - integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==, - } + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} string_decoder@1.1.1: - resolution: - { - integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, - } + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} string_decoder@1.3.0: - resolution: - { - integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, - } + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} strip-ansi@3.0.1: - resolution: - { - integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} strip-ansi@6.0.1: - resolution: - { - integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.1.0: - resolution: - { - integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} strip-ansi@7.1.2: - resolution: - { - integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} strip-bom@3.0.0: - resolution: - { - integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} strip-bom@4.0.0: - resolution: - { - integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} strip-final-newline@2.0.0: - resolution: - { - integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} strip-final-newline@3.0.0: - resolution: - { - integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} strip-indent@3.0.0: - resolution: - { - integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} strip-indent@4.0.0: - resolution: - { - integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} strip-json-comments@2.0.1: - resolution: - { - integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} strip-json-comments@3.1.1: - resolution: - { - integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} strip-literal@1.3.0: - resolution: - { - integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==, - } + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} strip-literal@2.1.0: - resolution: - { - integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==, - } + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} stubs@3.0.0: - resolution: - { - integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==, - } + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} style-resources-loader@1.5.0: - resolution: - { - integrity: sha512-fIfyvQ+uvXaCBGGAgfh+9v46ARQB1AWdaop2RpQw0PBVuROsTBqGvx8dj0kxwjGOAyq3vepe4AOK3M6+Q/q2jw==, - } - engines: { node: '>=8.9' } + resolution: {integrity: sha512-fIfyvQ+uvXaCBGGAgfh+9v46ARQB1AWdaop2RpQw0PBVuROsTBqGvx8dj0kxwjGOAyq3vepe4AOK3M6+Q/q2jw==} + engines: {node: '>=8.9'} peerDependencies: webpack: ^3.0.0 || ^4.0.0 || ^5.0.0 style-search@0.1.0: - resolution: - { - integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==, - } + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} stylehacks@5.1.1: - resolution: - { - integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==, - } - engines: { node: ^10 || ^12 || >=14.0 } + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 stylehacks@7.0.2: - resolution: - { - integrity: sha512-HdkWZS9b4gbgYTdMg4gJLmm7biAUug1qTqXjS+u8X+/pUd+9Px1E+520GnOW3rST9MNsVOVpsJG+mPHNosxjOQ==, - } - engines: { node: ^18.12.0 || ^20.9.0 || >=22.0 } + resolution: {integrity: sha512-HdkWZS9b4gbgYTdMg4gJLmm7biAUug1qTqXjS+u8X+/pUd+9Px1E+520GnOW3rST9MNsVOVpsJG+mPHNosxjOQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 stylelint-config-html@1.1.0: - resolution: - { - integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==, - } - engines: { node: ^12 || >=14 } + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} peerDependencies: postcss-html: ^1.0.0 stylelint: '>=14.0.0' stylelint-config-prettier@9.0.5: - resolution: - { - integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==, - } - engines: { node: '>= 12' } + resolution: {integrity: sha512-U44lELgLZhbAD/xy/vncZ2Pq8sh2TnpiPvo38Ifg9+zeioR+LAkHu0i6YORIOxFafZoVg0xqQwex6e6F25S5XA==} + engines: {node: '>= 12'} hasBin: true peerDependencies: stylelint: '>= 11.x < 15' stylelint-config-recommended-vue@1.5.0: - resolution: - { - integrity: sha512-65TAK/clUqkNtkZLcuytoxU0URQYlml+30Nhop7sRkCZ/mtWdXt7T+spPSB3KMKlb+82aEVJ4OrcstyDBdbosg==, - } - engines: { node: ^12 || >=14 } + resolution: {integrity: sha512-65TAK/clUqkNtkZLcuytoxU0URQYlml+30Nhop7sRkCZ/mtWdXt7T+spPSB3KMKlb+82aEVJ4OrcstyDBdbosg==} + engines: {node: ^12 || >=14} peerDependencies: postcss-html: ^1.0.0 stylelint: '>=14.0.0' stylelint-config-recommended@13.0.0: - resolution: - { - integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==, - } - engines: { node: ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==} + engines: {node: ^14.13.1 || >=16.0.0} peerDependencies: stylelint: ^15.10.0 stylelint-config-standard@34.0.0: - resolution: - { - integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==, - } - engines: { node: ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==} + engines: {node: ^14.13.1 || >=16.0.0} peerDependencies: stylelint: ^15.10.0 stylelint-webpack-plugin@5.0.1: - resolution: - { - integrity: sha512-07lpo1uVoFctKv0EOOg/YSrUppcLMjNBSMRqgooNnlbfAOgQfMzvLK+EbXz0HQiEgZobr+XQX9md/TgwTGdzbw==, - } - engines: { node: '>= 18.12.0' } + resolution: {integrity: sha512-07lpo1uVoFctKv0EOOg/YSrUppcLMjNBSMRqgooNnlbfAOgQfMzvLK+EbXz0HQiEgZobr+XQX9md/TgwTGdzbw==} + engines: {node: '>= 18.12.0'} peerDependencies: stylelint: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 webpack: ^5.0.0 stylelint@15.11.0: - resolution: - { - integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==, - } - engines: { node: ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==} + engines: {node: ^14.13.1 || >=16.0.0} hasBin: true supports-color@2.0.0: - resolution: - { - integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} supports-color@5.5.0: - resolution: - { - integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} supports-color@7.2.0: - resolution: - { - integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-color@8.1.1: - resolution: - { - integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} supports-hyperlinks@3.0.0: - resolution: - { - integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==, - } - engines: { node: '>=14.18' } + resolution: {integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==} + engines: {node: '>=14.18'} supports-preserve-symlinks-flag@1.0.0: - resolution: - { - integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} svg-tags@1.0.0: - resolution: - { - integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==, - } + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} svgo@2.8.0: - resolution: - { - integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} hasBin: true svgo@3.3.2: - resolution: - { - integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} hasBin: true symbol-tree@3.2.4: - resolution: - { - integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, - } + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} synckit@0.11.11: - resolution: - { - integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==, - } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} table@6.8.1: - resolution: - { - integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} tapable@1.1.3: - resolution: - { - integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} tapable@2.3.0: - resolution: - { - integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} tar@6.2.0: - resolution: - { - integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} + engines: {node: '>=10'} deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me teeny-request@7.2.0: - resolution: - { - integrity: sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==} + engines: {node: '>=10'} terser-webpack-plugin@1.4.6: - resolution: - { - integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==, - } - engines: { node: '>= 6.9.0' } + resolution: {integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==} + engines: {node: '>= 6.9.0'} peerDependencies: webpack: ^4.0.0 terser-webpack-plugin@4.2.3: - resolution: - { - integrity: sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==} + engines: {node: '>= 10.13.0'} peerDependencies: webpack: ^4.0.0 || ^5.0.0 terser-webpack-plugin@5.3.16: - resolution: - { - integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' esbuild: '*' @@ -14140,224 +8406,131 @@ packages: optional: true terser@4.8.1: - resolution: - { - integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==, - } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} + engines: {node: '>=6.0.0'} hasBin: true terser@5.44.1: - resolution: - { - integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + engines: {node: '>=10'} hasBin: true test-exclude@6.0.0: - resolution: - { - integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} text-decoding@1.0.0: - resolution: - { - integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==, - } + resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} text-table@0.2.0: - resolution: - { - integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==, - } + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} thingies@1.21.0: - resolution: - { - integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==, - } - engines: { node: '>=10.18' } + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} peerDependencies: tslib: ^2 thread-loader@3.0.4: - resolution: - { - integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==} + engines: {node: '>= 10.13.0'} peerDependencies: webpack: ^4.27.0 || ^5.0.0 through2@2.0.5: - resolution: - { - integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==, - } + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} through@2.3.8: - resolution: - { - integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, - } + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} time-fix-plugin@2.0.7: - resolution: - { - integrity: sha512-uVFet1LQToeUX0rTcSiYVYVoGuBpc8gP/2jnlUzuHMHe+gux6XLsNzxLUweabMwiUj5ejhoIMsUI55nVSEa/Vw==, - } + resolution: {integrity: sha512-uVFet1LQToeUX0rTcSiYVYVoGuBpc8gP/2jnlUzuHMHe+gux6XLsNzxLUweabMwiUj5ejhoIMsUI55nVSEa/Vw==} peerDependencies: webpack: '>=4.0.0' timers-browserify@2.0.12: - resolution: - { - integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==, - } - engines: { node: '>=0.6.0' } + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} tinyexec@1.0.2: - resolution: - { - integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} tldts-core@6.1.86: - resolution: - { - integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==, - } + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} tldts@6.1.86: - resolution: - { - integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==, - } + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true tmp@0.0.33: - resolution: - { - integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==, - } - engines: { node: '>=0.6.0' } + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} tmpl@1.0.5: - resolution: - { - integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==, - } + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} to-arraybuffer@1.0.1: - resolution: - { - integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==, - } + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} to-fast-properties@1.0.3: - resolution: - { - integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} + engines: {node: '>=0.10.0'} to-object-path@0.3.0: - resolution: - { - integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} to-regex-range@2.1.1: - resolution: - { - integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: '>=8.0' } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} to-regex@3.0.2: - resolution: - { - integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} toidentifier@1.0.1: - resolution: - { - integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, - } - engines: { node: '>=0.6' } + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} totalist@3.0.1: - resolution: - { - integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} tough-cookie@5.1.2: - resolution: - { - integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} tr46@0.0.3: - resolution: - { - integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, - } + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} tr46@5.1.1: - resolution: - { - integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} tree-dump@1.0.2: - resolution: - { - integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==, - } - engines: { node: '>=10.0' } + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} peerDependencies: tslib: '2' trim-newlines@4.1.1: - resolution: - { - integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} ts-api-utils@1.0.3: - resolution: - { - integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==, - } - engines: { node: '>=16.13.0' } + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' ts-jest@29.4.6: - resolution: - { - integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==, - } - engines: { node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0 } + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' @@ -14383,21 +8556,15 @@ packages: optional: true ts-loader@8.4.0: - resolution: - { - integrity: sha512-6nFY3IZ2//mrPc+ImY3hNWx1vCHyEhl6V+wLmL4CZcm6g1CqX7UKrkc6y0i4FwcfOhxyMPCfaEvh20f4r9GNpw==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-6nFY3IZ2//mrPc+ImY3hNWx1vCHyEhl6V+wLmL4CZcm6g1CqX7UKrkc6y0i4FwcfOhxyMPCfaEvh20f4r9GNpw==} + engines: {node: '>=10.0.0'} peerDependencies: typescript: '*' webpack: '*' ts-pnp@1.2.0: - resolution: - { - integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} + engines: {node: '>=6'} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -14405,389 +8572,215 @@ packages: optional: true tsconfig-paths@3.14.2: - resolution: - { - integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==, - } + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} tsconfig@7.0.0: - resolution: - { - integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==, - } + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} tslib@1.14.1: - resolution: - { - integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==, - } + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} tslib@2.6.2: - resolution: - { - integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==, - } + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tty-browserify@0.0.0: - resolution: - { - integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==, - } + resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} tweetnacl@1.0.3: - resolution: - { - integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==, - } + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} type-check@0.4.0: - resolution: - { - integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, - } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} type-detect@4.0.8: - resolution: - { - integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} type-fest@0.20.2: - resolution: - { - integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} type-fest@0.21.3: - resolution: - { - integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} type-fest@0.6.0: - resolution: - { - integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} type-fest@0.8.1: - resolution: - { - integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} type-fest@1.4.0: - resolution: - { - integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} type-fest@4.41.0: - resolution: - { - integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, - } - engines: { node: '>=16' } + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} typed-array-buffer@1.0.0: - resolution: - { - integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} typed-array-byte-length@1.0.0: - resolution: - { - integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} typed-array-byte-offset@1.0.0: - resolution: - { - integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} typed-array-length@1.0.4: - resolution: - { - integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==, - } + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} typedarray-to-buffer@3.1.5: - resolution: - { - integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==, - } + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} typedarray@0.0.6: - resolution: - { - integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==, - } + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} typescript@4.9.5: - resolution: - { - integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==, - } - engines: { node: '>=4.2.0' } + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} hasBin: true ua-parser-js@1.0.38: - resolution: - { - integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==, - } + resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} ufo@1.6.4: - resolution: - { - integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==, - } + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} uglify-js@3.19.3: - resolution: - { - integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} hasBin: true unbox-primitive@1.0.2: - resolution: - { - integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==, - } + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} uncrypto@0.1.3: - resolution: - { - integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==, - } + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} unctx@2.3.1: - resolution: - { - integrity: sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==, - } + resolution: {integrity: sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==} undici-types@7.16.0: - resolution: - { - integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==, - } + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} undici-types@7.19.2: - resolution: - { - integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, - } + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} undici@6.19.7: - resolution: - { - integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==, - } - engines: { node: '>=18.17' } + resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==} + engines: {node: '>=18.17'} unfetch@5.0.0: - resolution: - { - integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==, - } + resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==} unicode-canonical-property-names-ecmascript@2.0.0: - resolution: - { - integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} unicode-match-property-ecmascript@2.0.0: - resolution: - { - integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} unicode-match-property-value-ecmascript@2.1.0: - resolution: - { - integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} unicode-property-aliases-ecmascript@2.1.0: - resolution: - { - integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} unicorn-magic@0.1.0: - resolution: - { - integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} unimport@3.4.0: - resolution: - { - integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==, - } + resolution: {integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==} unimport@3.7.2: - resolution: - { - integrity: sha512-91mxcZTadgXyj3lFWmrGT8GyoRHWuE5fqPOjg5RVtF6vj+OfM5G6WCzXjuYtSgELE5ggB34RY4oiCSEP8I3AHw==, - } + resolution: {integrity: sha512-91mxcZTadgXyj3lFWmrGT8GyoRHWuE5fqPOjg5RVtF6vj+OfM5G6WCzXjuYtSgELE5ggB34RY4oiCSEP8I3AHw==} union-value@1.0.1: - resolution: - { - integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} unique-filename@1.1.1: - resolution: - { - integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==, - } + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} unique-slug@2.0.2: - resolution: - { - integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==, - } + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} unique-string@2.0.0: - resolution: - { - integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} universalify@0.1.2: - resolution: - { - integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, - } - engines: { node: '>= 4.0.0' } + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} universalify@2.0.0: - resolution: - { - integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==, - } - engines: { node: '>= 10.0.0' } + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} unpipe@1.0.0: - resolution: - { - integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} unplugin@1.11.0: - resolution: - { - integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==, - } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==} + engines: {node: '>=14.0.0'} unplugin@1.5.0: - resolution: - { - integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==, - } + resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==} unrs-resolver@1.11.1: - resolution: - { - integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==, - } + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} unset-value@1.0.0: - resolution: - { - integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} untyped@1.4.0: - resolution: - { - integrity: sha512-Egkr/s4zcMTEuulcIb7dgURS6QpN7DyqQYdf+jBtiaJvQ+eRsrtWUoX84SbvQWuLkXsOjM+8sJC9u6KoMK/U7Q==, - } + resolution: {integrity: sha512-Egkr/s4zcMTEuulcIb7dgURS6QpN7DyqQYdf+jBtiaJvQ+eRsrtWUoX84SbvQWuLkXsOjM+8sJC9u6KoMK/U7Q==} hasBin: true untyped@1.4.2: - resolution: - { - integrity: sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q==, - } + resolution: {integrity: sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q==} hasBin: true upath@1.2.0: - resolution: - { - integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} upath@2.0.1: - resolution: - { - integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} update-browserslist-db@1.2.3: - resolution: - { - integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, - } + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' uri-js@4.4.1: - resolution: - { - integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, - } + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} urix@0.1.0: - resolution: - { - integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==, - } + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} deprecated: Please see https://github.com/lydell/urix#deprecated url-loader@4.1.1: - resolution: - { - integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} peerDependencies: file-loader: '*' webpack: ^4.0.0 || ^5.0.0 @@ -14796,98 +8789,56 @@ packages: optional: true url@0.11.3: - resolution: - { - integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==, - } + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} use@3.1.1: - resolution: - { - integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==, - } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} util-deprecate@1.0.2: - resolution: - { - integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, - } + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} util.promisify@1.0.0: - resolution: - { - integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==, - } + resolution: {integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==} util@0.10.4: - resolution: - { - integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==, - } + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} util@0.11.1: - resolution: - { - integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==, - } + resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==} utila@0.4.0: - resolution: - { - integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==, - } + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} utils-merge@1.0.1: - resolution: - { - integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, - } - engines: { node: '>= 0.4.0' } + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} uuid@8.3.2: - resolution: - { - integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==, - } + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: - resolution: - { - integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==, - } - engines: { node: '>=10.12.0' } + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} validate-npm-package-license@3.0.4: - resolution: - { - integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==, - } + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} vary@1.1.2: - resolution: - { - integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, - } - engines: { node: '>= 0.8' } + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} vite-plugin-eslint@1.8.1: - resolution: - { - integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==, - } + resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' vite: '>=2' vite-plugin-stylelint@5.3.1: - resolution: - { - integrity: sha512-M/hSdfOwnOVghbJDeuuYIU2xO/MMukYR8QcEyNKFPG8ro1L+DlTdViix2B2d/FvAw14WPX88ckA5A7NvUjJz8w==, - } - engines: { node: '>=14.18' } + resolution: {integrity: sha512-M/hSdfOwnOVghbJDeuuYIU2xO/MMukYR8QcEyNKFPG8ro1L+DlTdViix2B2d/FvAw14WPX88ckA5A7NvUjJz8w==} + engines: {node: '>=14.18'} peerDependencies: '@types/stylelint': ^13.0.0 postcss: ^7.0.0 || ^8.0.0 @@ -14903,11 +8854,8 @@ packages: optional: true vite@4.5.3: - resolution: - { - integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==, - } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==} + engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: '@types/node': '>= 14' @@ -14934,79 +8882,49 @@ packages: optional: true vm-browserify@1.1.2: - resolution: - { - integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==, - } + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} vue-chartjs@5.3.3: - resolution: - { - integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==, - } + resolution: {integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==} peerDependencies: chart.js: ^4.1.1 vue: ^3.0.0-0 || ^2.7.0 vue-class-component@7.2.6: - resolution: - { - integrity: sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==, - } + resolution: {integrity: sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==} peerDependencies: vue: ^2.0.0 vue-client-only@2.1.0: - resolution: - { - integrity: sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA==, - } + resolution: {integrity: sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA==} vue-eslint-parser@9.3.1: - resolution: - { - integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==, - } - engines: { node: ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} + engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' vue-eslint-parser@9.4.3: - resolution: - { - integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==, - } - engines: { node: ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' vue-glow@1.4.2: - resolution: - { - integrity: sha512-MDC5Q817fH51OhCpYopAcXwMZ49yVAjEgiJ1sXlc3Kyul0AU343AbB0zflr+LnuiuS/EegfVkxYh0I67xSMYZw==, - } + resolution: {integrity: sha512-MDC5Q817fH51OhCpYopAcXwMZ49yVAjEgiJ1sXlc3Kyul0AU343AbB0zflr+LnuiuS/EegfVkxYh0I67xSMYZw==} vue-hot-reload-api@2.3.4: - resolution: - { - integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==, - } + resolution: {integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==} vue-jest@3.0.7: - resolution: - { - integrity: sha512-PIOxFM+wsBMry26ZpfBvUQ/DGH2hvp5khDQ1n51g3bN0TwFwTy4J85XVfxTRMukqHji/GnAoGUnlZ5Ao73K62w==, - } + resolution: {integrity: sha512-PIOxFM+wsBMry26ZpfBvUQ/DGH2hvp5khDQ1n51g3bN0TwFwTy4J85XVfxTRMukqHji/GnAoGUnlZ5Ao73K62w==} peerDependencies: babel-core: ^6.25.0 || ^7.0.0-0 vue: ^2.x vue-template-compiler: ^2.x vue-loader@15.11.1: - resolution: - { - integrity: sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==, - } + resolution: {integrity: sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==} peerDependencies: '@vue/compiler-sfc': ^3.0.8 cache-loader: '*' @@ -15025,70 +8943,40 @@ packages: optional: true vue-meta@2.4.0: - resolution: - { - integrity: sha512-XEeZUmlVeODclAjCNpWDnjgw+t3WA6gdzs6ENoIAgwO1J1d5p1tezDhtteLUFwcaQaTtayRrsx7GL6oXp/m2Jw==, - } + resolution: {integrity: sha512-XEeZUmlVeODclAjCNpWDnjgw+t3WA6gdzs6ENoIAgwO1J1d5p1tezDhtteLUFwcaQaTtayRrsx7GL6oXp/m2Jw==} vue-no-ssr@1.1.1: - resolution: - { - integrity: sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g==, - } + resolution: {integrity: sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g==} vue-property-decorator@9.1.2: - resolution: - { - integrity: sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==, - } + resolution: {integrity: sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==} peerDependencies: vue: '*' vue-class-component: '*' vue-router@3.6.5: - resolution: - { - integrity: sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==, - } + resolution: {integrity: sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==} peerDependencies: vue: ^2 vue-server-renderer@2.7.16: - resolution: - { - integrity: sha512-U7GgR4rYmHmbs3Z2gqsasfk7JNuTsy/xrR5EMMGRLkjN8+ryDlqQq6Uu3DcmbCATAei814YOxyl0eq2HNqgXyQ==, - } + resolution: {integrity: sha512-U7GgR4rYmHmbs3Z2gqsasfk7JNuTsy/xrR5EMMGRLkjN8+ryDlqQq6Uu3DcmbCATAei814YOxyl0eq2HNqgXyQ==} vue-style-loader@4.1.3: - resolution: - { - integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==, - } + resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==} vue-template-compiler@2.7.16: - resolution: - { - integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==, - } + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} vue-template-es2015-compiler@1.9.1: - resolution: - { - integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==, - } + resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} vue@2.7.16: - resolution: - { - integrity: sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==, - } + resolution: {integrity: sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==} deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details. vuetify-loader@1.9.2: - resolution: - { - integrity: sha512-8PP2w7aAs/rjA+Izec6qY7sHVb75MNrGQrDOTZJ5IEnvl+NiFhVpU2iWdRDZ3eMS842cWxSWStvkr+KJJKy+Iw==, - } + resolution: {integrity: sha512-8PP2w7aAs/rjA+Izec6qY7sHVb75MNrGQrDOTZJ5IEnvl+NiFhVpU2iWdRDZ3eMS842cWxSWStvkr+KJJKy+Iw==} peerDependencies: gm: ^1.23.0 pug: ^2.0.0 || ^3.0.0 @@ -15105,128 +8993,74 @@ packages: optional: true vuetify@2.7.2: - resolution: - { - integrity: sha512-qr04ww7uzAPQbpk751x4fSdjsJ+zREzjQ/rBlcQGuWS6MIMFMXcXcwvp4+/tnGsULZxPMWfQ0kmZmg5Yc/XzgQ==, - } + resolution: {integrity: sha512-qr04ww7uzAPQbpk751x4fSdjsJ+zREzjQ/rBlcQGuWS6MIMFMXcXcwvp4+/tnGsULZxPMWfQ0kmZmg5Yc/XzgQ==} deprecated: This version is deprecated peerDependencies: vue: ^2.6.4 vuex@3.6.2: - resolution: - { - integrity: sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==, - } + resolution: {integrity: sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==} peerDependencies: vue: ^2.0.0 w3c-xmlserializer@5.0.0: - resolution: - { - integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} walker@1.0.8: - resolution: - { - integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==, - } + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} watchpack-chokidar2@2.0.1: - resolution: - { - integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==, - } + resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==} watchpack@1.7.5: - resolution: - { - integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==, - } + resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==} watchpack@2.5.0: - resolution: - { - integrity: sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==} + engines: {node: '>=10.13.0'} webidl-conversions@3.0.1: - resolution: - { - integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, - } + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} webidl-conversions@7.0.0: - resolution: - { - integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} webpack-bundle-analyzer@4.10.2: - resolution: - { - integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==, - } - engines: { node: '>= 10.13.0' } + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} hasBin: true webpack-dev-middleware@5.3.4: - resolution: - { - integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==, - } - engines: { node: '>= 12.13.0' } + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^4.0.0 || ^5.0.0 webpack-hot-middleware@2.26.1: - resolution: - { - integrity: sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==, - } + resolution: {integrity: sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==} webpack-node-externals@3.0.0: - resolution: - { - integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} webpack-sources@1.4.3: - resolution: - { - integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==, - } + resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} webpack-sources@3.3.3: - resolution: - { - integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} webpack-virtual-modules@0.5.0: - resolution: - { - integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==, - } + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} webpack-virtual-modules@0.6.2: - resolution: - { - integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==, - } + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} webpack@4.47.0: - resolution: - { - integrity: sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==, - } - engines: { node: '>=6.11.5' } + resolution: {integrity: sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==} + engines: {node: '>=6.11.5'} hasBin: true peerDependencies: webpack-cli: '*' @@ -15238,11 +9072,8 @@ packages: optional: true webpack@5.104.1: - resolution: - { - integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==, - } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} + engines: {node: '>=10.13.0'} hasBin: true peerDependencies: webpack-cli: '*' @@ -15251,175 +9082,100 @@ packages: optional: true webpackbar@6.0.1: - resolution: - { - integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==, - } - engines: { node: '>=14.21.3' } + resolution: {integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==} + engines: {node: '>=14.21.3'} peerDependencies: webpack: 3 || 4 || 5 websocket-driver@0.7.4: - resolution: - { - integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} websocket-extensions@0.1.4: - resolution: - { - integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==, - } - engines: { node: '>=0.8.0' } + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} whatwg-encoding@3.1.1: - resolution: - { - integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: - resolution: - { - integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} whatwg-url@14.2.0: - resolution: - { - integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} whatwg-url@5.0.0: - resolution: - { - integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, - } + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} which-boxed-primitive@1.0.2: - resolution: - { - integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==, - } + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} which-module@2.0.1: - resolution: - { - integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==, - } + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} which-typed-array@1.1.11: - resolution: - { - integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==, - } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} which@1.3.1: - resolution: - { - integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==, - } + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: '>= 8' } + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} hasBin: true widest-line@3.1.0: - resolution: - { - integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} wordwrap@1.0.0: - resolution: - { - integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==, - } + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} worker-farm@1.7.0: - resolution: - { - integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==, - } + resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==} wrap-ansi@6.2.0: - resolution: - { - integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} wrap-ansi@7.0.0: - resolution: - { - integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} wrap-ansi@8.1.0: - resolution: - { - integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} wrap-ansi@9.0.0: - resolution: - { - integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} wrappy@1.0.2: - resolution: - { - integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, - } + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} write-file-atomic@2.4.3: - resolution: - { - integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==, - } + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} write-file-atomic@3.0.3: - resolution: - { - integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==, - } + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} write-file-atomic@5.0.1: - resolution: - { - integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==, - } - engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} write-json-file@2.3.0: - resolution: - { - integrity: sha512-84+F0igFp2dPD6UpAQjOUX3CdKUOqUzn6oE9sDBNzUXINR5VceJ1rauZltqQB/bcYsx3EpKys4C7/PivKUAiWQ==, - } - engines: { node: '>=4' } + resolution: {integrity: sha512-84+F0igFp2dPD6UpAQjOUX3CdKUOqUzn6oE9sDBNzUXINR5VceJ1rauZltqQB/bcYsx3EpKys4C7/PivKUAiWQ==} + engines: {node: '>=4'} ws@7.5.10: - resolution: - { - integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==, - } - engines: { node: '>=8.3.0' } + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -15430,11 +9186,8 @@ packages: optional: true ws@8.20.0: - resolution: - { - integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==, - } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: '>=5.0.2' @@ -15445,148 +9198,86 @@ packages: optional: true xdg-basedir@4.0.0: - resolution: - { - integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} xml-name-validator@4.0.0: - resolution: - { - integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} xml-name-validator@5.0.0: - resolution: - { - integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==, - } - engines: { node: '>=18' } + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} xmlbuilder@13.0.2: - resolution: - { - integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==, - } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} + engines: {node: '>=6.0'} xmlchars@2.2.0: - resolution: - { - integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, - } + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} xtend@4.0.2: - resolution: - { - integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, - } - engines: { node: '>=0.4' } + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} xxhashjs@0.2.2: - resolution: - { - integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==, - } + resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} y18n@4.0.3: - resolution: - { - integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==, - } + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} y18n@5.0.8: - resolution: - { - integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} yallist@2.1.2: - resolution: - { - integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==, - } + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} yallist@3.1.1: - resolution: - { - integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, - } + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} yallist@4.0.0: - resolution: - { - integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, - } + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} yaml@1.10.2: - resolution: - { - integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==, - } - engines: { node: '>= 6' } + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} yaml@2.8.1: - resolution: - { - integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==, - } - engines: { node: '>= 14.6' } + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@18.1.3: - resolution: - { - integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==, - } - engines: { node: '>=6' } + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} yargs-parser@20.2.9: - resolution: - { - integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} yargs-parser@21.1.1: - resolution: - { - integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} yargs@15.4.1: - resolution: - { - integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==, - } - engines: { node: '>=8' } + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} yargs@16.2.0: - resolution: - { - integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} yargs@17.7.2: - resolution: - { - integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, - } - engines: { node: '>=12' } + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} yocto-queue@0.1.0: - resolution: - { - integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, - } - engines: { node: '>=10' } + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} snapshots: + '@aashutoshrathi/word-wrap@1.2.6': {} '@ampproject/remapping@2.2.1': From 939fffde269e15f89c92e61004f811e80fc13dbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 10:05:51 +0300 Subject: [PATCH 175/381] fix(deps): bump @tootallnate/once from 2.0.0 to 2.0.1 in /web (#899) Bumps [@tootallnate/once](https://github.com/TooTallNate/once) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/TooTallNate/once/releases) - [Changelog](https://github.com/TooTallNate/once/blob/v2.0.1/CHANGELOG.md) - [Commits](https://github.com/TooTallNate/once/compare/2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: "@tootallnate/once" dependency-version: 2.0.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 0f8c6ae029913a3668c31974656720adc4074d8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 10:06:01 +0300 Subject: [PATCH 176/381] fix(deps): bump ws from 8.20.0 to 8.20.1 in /web (#900) Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.20.0...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From dc0a0f6f73a5e46cd3c2a7760290a9012f307868 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 10:09:18 +0300 Subject: [PATCH 177/381] fix(web): resolve stylelint errors in bulk-messages page (#902) * fix(web): resolve stylelint errors in bulk-messages page - Add empty line before rule as required by rule-empty-line-before - Use modern color-function notation rgb(0 0 0 / 4%) instead of rgba(0, 0, 0, 0.04) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci(web): run validation on PRs, deploy only on merge to main - Split workflow into 'validate' (lint, test, build) and 'deploy' jobs - Validate runs on both pull_request and push to main - Deploy only runs after merge to main, gated behind validate passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/web.yml | 29 +++++++++++++++++++++++++---- web/pages/bulk-messages/index.vue | 3 ++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index fe0ae7c8f..2fabd88f1 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -4,14 +4,17 @@ on: push: branches: - main + pull_request: + branches: + - main defaults: run: working-directory: ./web jobs: - ci: - name: Build & Deploy + validate: + name: Validate runs-on: ${{ matrix.os }} strategy: @@ -37,8 +40,26 @@ jobs: - name: Run tests 🧪 run: pnpm test - - name: Debug 🐛 - run: echo GITHUB_SHA=${GITHUB_SHA} + - name: Build 🏗️ + run: mv .env.production .env && echo GITHUB_SHA=${GITHUB_SHA} >> .env && pnpm run generate + + deploy: + name: Deploy + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + steps: + - name: Checkout 🛎 + uses: actions/checkout@master + + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + version: 10 + + - name: Install dependencies 📦 + run: pnpm install - name: Build 🏗️ run: mv .env.production .env && echo GITHUB_SHA=${GITHUB_SHA} >> .env && pnpm run generate diff --git a/web/pages/bulk-messages/index.vue b/web/pages/bulk-messages/index.vue index b2935bcd7..746322100 100644 --- a/web/pages/bulk-messages/index.vue +++ b/web/pages/bulk-messages/index.vue @@ -273,7 +273,8 @@ export default Vue.extend({ .clickable-row { cursor: pointer; } + .clickable-row:hover { - background-color: rgba(0, 0, 0, 0.04); + background-color: rgb(0 0 0 / 4%); } From f09cf6d8e80e6a36f85d0ba4eb32485df9f86441 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 10:10:01 +0300 Subject: [PATCH 178/381] ci(web): add permissions for contents read and pull-requests write Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/web.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 2fabd88f1..18f355c70 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -8,6 +8,10 @@ on: branches: - main +permissions: + contents: read + pull-requests: write + defaults: run: working-directory: ./web From 56a9a44be7bd63c60b31e2c487e9186d86968577 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 10:20:11 +0300 Subject: [PATCH 179/381] ci(web): add deployments write permission for Cloudflare/Firebase deploy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/web.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 18f355c70..ba4fe8ecf 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -11,6 +11,7 @@ on: permissions: contents: read pull-requests: write + deployments: write defaults: run: From 808c0e1deb3fe1128b967aa09c1a2f9e13ddd780 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Fri, 22 May 2026 10:50:49 +0300 Subject: [PATCH 180/381] Fix: parse time in user's location in CSV file --- api/pkg/handlers/bulk_message_handler.go | 8 ++--- api/pkg/requests/bulk_message_request.go | 29 ++++++++++----- .../bulk_message_handler_validator.go | 36 ++++++++----------- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index c388ef141..27d6286de 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -99,7 +99,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - messages, fileType, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) + messages, fileType, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) if len(validationErrors) != 0 { msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)) ctxLogger.Warn(stacktrace.NewError(msg)) @@ -121,7 +121,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { for _, message := range messages { wg.Add(1) var perPhoneIndex int - if message.GetSendTime() == nil { + if message.GetSendTime(userLocation) == nil { perPhoneIndex = phoneIndexCounter[message.FromPhoneNumber] phoneIndexCounter[message.FromPhoneNumber]++ } @@ -130,11 +130,11 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { count.Add(1) _, err = h.messageService.SendMessage( ctx, - message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index), + message.ToMessageSendParams(h.userIDFomContext(c), requestID, c.OriginalURL(), index, userLocation), ) if err != nil { count.Add(-1) - msg := fmt.Sprintf("cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index) + msg := fmt.Sprintf("cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index) ctxLogger.Error(stacktrace.Propagate(err, msg)) } wg.Done() diff --git a/api/pkg/requests/bulk_message_request.go b/api/pkg/requests/bulk_message_request.go index b84425fc1..345e24aa4 100644 --- a/api/pkg/requests/bulk_message_request.go +++ b/api/pkg/requests/bulk_message_request.go @@ -20,22 +20,33 @@ type BulkMessage struct { AttachmentURLs string `csv:"AttachmentURLs(optional)" validate:"optional"` // Comma separated list of URLs } -// GetSendTime parses the raw SendTime string into a *time.Time -func (input *BulkMessage) GetSendTime() *time.Time { +// GetSendTime parses the raw SendTime string into a *time.Time. +// For timezone-naive formats, the time is interpreted in the given location. +// For RFC3339 (which includes an offset), the embedded offset is used. +func (input *BulkMessage) GetSendTime(location *time.Location) *time.Time { raw := strings.TrimSpace(input.SendTime) if raw == "" { return nil } - formats := []string{ - time.RFC3339, + if location == nil { + location = time.UTC + } + + // RFC3339 already contains timezone offset, parse without location + if t, err := time.Parse(time.RFC3339, raw); err == nil { + utc := t.UTC() + return &utc + } + + // Naive formats: interpret in the user's location + naiveFormats := []string{ "2006-01-02T15:04:05", "2006-01-02 15:04:05", - "2006-01-02", } - for _, format := range formats { - if t, err := time.Parse(format, raw); err == nil { + for _, format := range naiveFormats { + if t, err := time.ParseInLocation(format, raw, location); err == nil { utc := t.UTC() return &utc } @@ -60,7 +71,7 @@ func (input *BulkMessage) Sanitize() *BulkMessage { } // ToMessageSendParams converts BulkMessage to services.MessageSendParams -func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID string, source string, index int) services.MessageSendParams { +func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID string, source string, index int, location *time.Location) services.MessageSendParams { from, _ := phonenumbers.Parse(input.FromPhoneNumber, phonenumbers.UNKNOWN_REGION) return services.MessageSendParams{ @@ -68,7 +79,7 @@ func (input *BulkMessage) ToMessageSendParams(userID entities.UserID, requestID Owner: from, RequestID: input.sanitizeStringPointer(requestID), UserID: userID, - SendAt: input.GetSendTime(), + SendAt: input.GetSendTime(location), RequestReceivedAt: time.Now().UTC(), Contact: input.sanitizeAddress(input.ToPhoneNumber), Content: input.Content, diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 5976c2cc0..f17f6adb6 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -52,7 +52,7 @@ func NewBulkMessageHandlerValidator( } // ValidateStore validates the requests.BillingUsageHistory request -func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) { +func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, string, *time.Location, url.Values) { ctx, span, ctxLogger := v.tracer.StartWithLogger(ctx, v.logger) defer span.End() @@ -61,39 +61,39 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID result := url.Values{} result.Add("document", "Cannot load your account. Please try again later or contact support.") ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s]", userID)))) - return nil, "", result + return nil, "", nil, result } messages, fileType, result := v.parseFile(ctxLogger, user, header) if len(result) != 0 { - return messages, fileType, result + return messages, fileType, user.Location(), result } if len(messages) == 0 { result.Add("document", "The uploaded file doesn't contain any valid records. Make sure you are using the official httpSMS template.") - return messages, fileType, result + return messages, fileType, user.Location(), result } if len(messages) > 1000 { result.Add("document", "The uploaded file must contain less than 1000 records.") - return messages, fileType, result + return messages, fileType, user.Location(), result } for index, message := range messages { messages[index] = message.Sanitize() } - result = v.validateMessages(ctx, messages) + result = v.validateMessages(ctx, messages, user.Location()) if len(result) != 0 { - return messages, fileType, result + return messages, fileType, user.Location(), result } result = v.validateOwners(ctx, userID, messages) if len(result) != 0 { - return messages, fileType, result + return messages, fileType, user.Location(), result } - return messages, fileType, result + return messages, fileType, user.Location(), result } func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) { @@ -143,8 +143,9 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user var sendTimeRaw string if len(row) > 3 && strings.TrimSpace(row[3]) != "" { ctxLogger.Info(fmt.Sprintf("excel time = [%s]", row[3])) - sendAt, err := v.convertExcelTime(user, row[3]) - if err != nil { + msg := &requests.BulkMessage{SendTime: strings.TrimSpace(row[3])} + sendAt := msg.GetSendTime(user.Location()) + if sendAt == nil { result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] is not in the correct format e.g [2006-01-02T15:04:05] where 2006 is the year, 01 is January, 02 is the second day of the month and the time is 15:04:05", index+1, row[3])) return nil, result } @@ -168,15 +169,6 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user return messages, url.Values{} } -func (v *BulkMessageHandlerValidator) convertExcelTime(user *entities.User, value string) (*time.Time, error) { - t, err := time.ParseInLocation("2006-01-02T15:04:05", value, user.Location()) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("cannot parse excel time [%s] as [%T]", value, t)) - } - - return &t, nil -} - func (v *BulkMessageHandlerValidator) parseBytes(ctxLogger telemetry.Logger, userID entities.UserID, header *multipart.FileHeader) ([]byte, url.Values) { result := url.Values{} @@ -223,7 +215,7 @@ func (v *BulkMessageHandlerValidator) parseCSV(ctxLogger telemetry.Logger, user return messages, url.Values{} } -func (v *BulkMessageHandlerValidator) validateMessages(_ context.Context, messages []*requests.BulkMessage) url.Values { +func (v *BulkMessageHandlerValidator) validateMessages(_ context.Context, messages []*requests.BulkMessage, location *time.Location) url.Values { result := url.Values{} for index, message := range messages { @@ -269,7 +261,7 @@ func (v *BulkMessageHandlerValidator) validateMessages(_ context.Context, messag } if strings.TrimSpace(message.SendTime) != "" { - sendTime := message.GetSendTime() + sendTime := message.GetSendTime(location) if sendTime == nil { result.Add("document", fmt.Sprintf("Row [%d]: The SendTime [%s] is not a valid date format. Use RFC3339 (e.g. 2023-11-11T02:10:01Z) or YYYY-MM-DDTHH:MM:SS.", index+2, message.SendTime)) } else if sendTime.After(time.Now().Add(420 * time.Hour)) { From 9a8a681ed93edbf03a3418111ea81128bf01cacc Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 24 May 2026 18:25:54 +0300 Subject: [PATCH 181/381] refactor: use timestamp+filename for bulk message request ID (#903) * refactor: use timestamp+filename for bulk message request ID - Generate request ID as bulk-{base62_timestamp}-{truncated_filename} - Encode unix timestamp as base62 for minimal length (~6 chars) - Truncate filename to max 32 chars preserving extension - Remove fileType return value from ValidateStore - Simplify frontend cleanName() to just strip 'bulk-' prefix - Stay on bulk-messages page after upload instead of redirecting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review comments - Add 4-char random base62 suffix to prevent same-second collisions - Sanitize filename by stripping non-alphanumeric chars (except . and -) - Restore backward-compat in cleanName for old bulk-csv-/bulk-xls- entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: allow space character in sanitizeFilename Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/go.mod | 1 - api/go.sum | 2 - api/pkg/handlers/bulk_message_handler.go | 53 +++++++++++++------ .../bulk_message_handler_validator.go | 28 +++++----- web/pages/bulk-messages/index.vue | 7 ++- 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/api/go.mod b/api/go.mod index 754baa145..1fe7dca1f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -33,7 +33,6 @@ require ( github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jszwec/csvutil v1.10.0 github.com/lib/pq v1.12.3 - github.com/matoous/go-nanoid/v2 v2.1.0 github.com/nyaruka/phonenumbers v1.7.2 github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/api/go.sum b/api/go.sum index cb11e652b..fa1163a2f 100644 --- a/api/go.sum +++ b/api/go.sum @@ -237,8 +237,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= -github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index 27d6286de..49b4f5914 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -1,10 +1,12 @@ package handlers import ( - "crypto/rand" "fmt" + "path/filepath" + "regexp" "sync" "sync/atomic" + "time" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" @@ -12,7 +14,6 @@ import ( "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v2" - gonanoid "github.com/matoous/go-nanoid/v2" "github.com/palantir/stacktrace" ) @@ -99,7 +100,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responseBadRequest(c, err) } - messages, fileType, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) + messages, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) if len(validationErrors) != 0 { msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)) ctxLogger.Warn(stacktrace.NewError(msg)) @@ -111,7 +112,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responsePaymentRequired(c, *msg) } - requestID := h.generateRequestID(fileType, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + requestID := h.generateRequestID(file.Filename) wg := sync.WaitGroup{} count := atomic.Int64{} @@ -145,21 +146,41 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages))) } -func (h *BulkMessageHandler) generateRequestID(fileType string, alphabet string) string { - id, err := gonanoid.Generate(alphabet, 10) - if err != nil { - id = h.randomAlphaNum(10, alphabet) +func (h *BulkMessageHandler) generateRequestID(filename string) string { + return fmt.Sprintf("bulk-%s-%s", encodeBase62(time.Now().Unix()), truncateFilename(sanitizeFilename(filename), 32)) +} + +func sanitizeFilename(filename string) string { + return regexp.MustCompile(`[^a-zA-Z0-9.\-_: ]`).ReplaceAllString(filename, "") +} + +func encodeBase62(n int64) string { + const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + if n == 0 { + return "0" + } + result := make([]byte, 0, 8) + for n > 0 { + result = append(result, charset[n%62]) + n /= 62 + } + // reverse + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] } - return fmt.Sprintf("bulk-%s-%s", fileType, id) + return string(result) } -func (h *BulkMessageHandler) randomAlphaNum(length int, alphabet string) string { - b := make([]byte, length) - if _, err := rand.Read(b); err != nil { - return alphabet[:length] +func truncateFilename(filename string, maxLen int) string { + if len(filename) <= maxLen { + return filename } - for i := range b { - b[i] = alphabet[int(b[i])%len(alphabet)] + ext := filepath.Ext(filename) + name := filename[:len(filename)-len(ext)] + available := maxLen - len(ext) + if available <= 0 { + return filename[:maxLen] } - return string(b) + half := available / 2 + return name[:half] + name[len(name)-(available-half):] + ext } diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index f17f6adb6..67537a93f 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -52,7 +52,7 @@ func NewBulkMessageHandlerValidator( } // ValidateStore validates the requests.BillingUsageHistory request -func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, string, *time.Location, url.Values) { +func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, *time.Location, url.Values) { ctx, span, ctxLogger := v.tracer.StartWithLogger(ctx, v.logger) defer span.End() @@ -61,22 +61,22 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID result := url.Values{} result.Add("document", "Cannot load your account. Please try again later or contact support.") ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s]", userID)))) - return nil, "", nil, result + return nil, nil, result } - messages, fileType, result := v.parseFile(ctxLogger, user, header) + messages, result := v.parseFile(ctxLogger, user, header) if len(result) != 0 { - return messages, fileType, user.Location(), result + return messages, user.Location(), result } if len(messages) == 0 { result.Add("document", "The uploaded file doesn't contain any valid records. Make sure you are using the official httpSMS template.") - return messages, fileType, user.Location(), result + return messages, user.Location(), result } if len(messages) > 1000 { result.Add("document", "The uploaded file must contain less than 1000 records.") - return messages, fileType, user.Location(), result + return messages, user.Location(), result } for index, message := range messages { @@ -85,32 +85,30 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID result = v.validateMessages(ctx, messages, user.Location()) if len(result) != 0 { - return messages, fileType, user.Location(), result + return messages, user.Location(), result } result = v.validateOwners(ctx, userID, messages) if len(result) != 0 { - return messages, fileType, user.Location(), result + return messages, user.Location(), result } - return messages, fileType, user.Location(), result + return messages, user.Location(), result } -func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) { +func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) { if header.Header.Get("Content-Type") == "text/csv" || strings.HasSuffix(header.Filename, ".csv") { - messages, result := v.parseCSV(ctxLogger, user, header) - return messages, "csv", result + return v.parseCSV(ctxLogger, user, header) } if header.Header.Get("Content-Type") == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || strings.HasSuffix(header.Filename, ".xlsx") { - messages, result := v.parseXlsx(ctxLogger, user, header) - return messages, "xls", result + return v.parseXlsx(ctxLogger, user, header) } ctxLogger.Error(stacktrace.NewError(fmt.Sprintf("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type")))) result := url.Values{} result.Add("document", fmt.Sprintf("The file [%s] is not a valid CSV or Excel file.", header.Filename)) - return nil, "", result + return nil, result } func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) { diff --git a/web/pages/bulk-messages/index.vue b/web/pages/bulk-messages/index.vue index 746322100..223822783 100644 --- a/web/pages/bulk-messages/index.vue +++ b/web/pages/bulk-messages/index.vue @@ -251,10 +251,9 @@ export default Vue.extend({ this.$store .dispatch('sendBulkMessages', this.formFile) .then(() => { - setTimeout(() => { - this.loading = false - this.$router.push({ name: 'threads' }) - }, 2000) + this.loading = false + this.formFile = null + this.fetchBulkOrders() }) .catch((error: AxiosError) => { this.errorTitle = capitalize( From d1098c23d318a1714a9e2377fa688b1c408bee74 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Tue, 26 May 2026 08:48:47 +0300 Subject: [PATCH 182/381] feat: use millisecond precision for bulk message request IDs and show filename in UI - Change generateRequestID to use time.Now().UnixMilli() for millisecond precision timestamps - Rename table column header from 'ID' to 'Name' - Update cleanName to extract filename from new bulk-{timestamp}-{name} format - Maintain backward compatibility for old bulk-csv-* and bulk-xls-* formats Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/pkg/handlers/bulk_message_handler.go | 2 +- web/pages/bulk-messages/index.vue | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index 49b4f5914..ce1f0354d 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -147,7 +147,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error { } func (h *BulkMessageHandler) generateRequestID(filename string) string { - return fmt.Sprintf("bulk-%s-%s", encodeBase62(time.Now().Unix()), truncateFilename(sanitizeFilename(filename), 32)) + return fmt.Sprintf("bulk-%s-%s", encodeBase62(time.Now().UnixMilli()), truncateFilename(sanitizeFilename(filename), 32)) } func sanitizeFilename(filename string) string { diff --git a/web/pages/bulk-messages/index.vue b/web/pages/bulk-messages/index.vue index 223822783..3e3b5700e 100644 --- a/web/pages/bulk-messages/index.vue +++ b/web/pages/bulk-messages/index.vue @@ -113,7 +113,7 @@