From c36e30f3a9496e5ddf40c9493e3d5c364ce6c645 Mon Sep 17 00:00:00 2001
From: Acho Arnold
Date: Sun, 19 Jul 2026 15:00:43 +0300
Subject: [PATCH 01/88] feat: add message thread read receipts (#956)
* docs: add read receipts design
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* docs: add read receipts implementation plan
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* docs: add read receipts integration coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* feat(api): add thread read state contract
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* refactor(api): make thread updates atomic
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* feat(api): track unread inbound threads
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* docs(api): publish thread read state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* style(api): format thread handler test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* feat(web): persist opened threads as read
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* chore(web): keep read store change scoped
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* fix(web): preserve thread update failures
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* feat(web): highlight unread message threads
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* test: cover message thread read receipts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* test: preserve event timestamp precision
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* fix(api): persist new unread threads
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* style(api): apply gofumpt to handler test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* test(api): update listener service setup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* fix: harden read receipt synchronization
Avoid duplicate realtime read updates and the post-write reload race.
Retry asynchronous phone-key binding in integration tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* docs: normalize plan code indentation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* fix(api): return updated thread atomically
Use PostgreSQL RETURNING so concurrent activity cannot make status update responses stale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* refactor(api): update activity atomically
Fold the unread watermark into the activity update so inbound events use one database query.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
* style: fix thread ui for unread threads
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
api/docs/docs.go | 41 +-
api/docs/swagger.json | 41 +-
api/docs/swagger.yaml | 30 +-
api/pkg/entities/message_thread.go | 2 +
api/pkg/entities/message_thread_test.go | 25 +
api/pkg/handlers/message_thread_handler.go | 6 +-
.../handlers/message_thread_handler_test.go | 110 ++
api/pkg/listeners/message_thread_listener.go | 44 +-
.../listeners/message_thread_listener_test.go | 72 +
.../read_receipts_test_helpers_test.go | 66 +
api/pkg/listeners/websocket_listener.go | 16 +
api/pkg/listeners/websocket_listener_test.go | 18 +
.../gorm_message_thread_repository.go | 123 +-
.../gorm_message_thread_repository_test.go | 204 ++
.../repositories/message_thread_repository.go | 37 +-
.../requests/message_thread_update_request.go | 4 +-
.../message_thread_update_request_test.go | 25 +
api/pkg/responses/message_thead_responses.go | 6 +
api/pkg/services/message_thread_service.go | 63 +-
.../services/message_thread_service_test.go | 214 ++
.../message_thread_handler_validator.go | 10 +-
.../message_thread_handler_validator_test.go | 34 +
.../plans/2026-07-18-read-receipts.md | 1760 +++++++++++++++++
.../specs/2026-07-18-read-receipts-design.md | 286 +++
tests/README.md | 1 +
tests/read_receipts_test.go | 206 ++
web/app/components/MessageThread.vue | 9 +-
web/app/pages/threads/[id]/index.vue | 23 +-
web/app/stores/threads.ts | 40 +
web/shared/types/api.ts | 14 +-
30 files changed, 3462 insertions(+), 68 deletions(-)
create mode 100644 api/pkg/entities/message_thread_test.go
create mode 100644 api/pkg/handlers/message_thread_handler_test.go
create mode 100644 api/pkg/listeners/message_thread_listener_test.go
create mode 100644 api/pkg/listeners/read_receipts_test_helpers_test.go
create mode 100644 api/pkg/listeners/websocket_listener_test.go
create mode 100644 api/pkg/repositories/gorm_message_thread_repository_test.go
create mode 100644 api/pkg/requests/message_thread_update_request_test.go
create mode 100644 api/pkg/validators/message_thread_handler_validator_test.go
create mode 100644 docs/superpowers/plans/2026-07-18-read-receipts.md
create mode 100644 docs/superpowers/specs/2026-07-18-read-receipts-design.md
create mode 100644 tests/read_receipts_test.go
diff --git a/api/docs/docs.go b/api/docs/docs.go
index c7fcd1145..8cd408cb1 100644
--- a/api/docs/docs.go
+++ b/api/docs/docs.go
@@ -871,7 +871,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/responses.PhoneResponse"
+ "$ref": "#/definitions/responses.MessageThreadResponse"
}
},
"400": {
@@ -886,6 +886,12 @@ const docTemplate = `{
"$ref": "#/definitions/responses.Unauthorized"
}
},
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/responses.NotFound"
+ }
+ },
"422": {
"description": "Unprocessable Entity",
"schema": {
@@ -3691,6 +3697,7 @@ const docTemplate = `{
"created_at",
"id",
"is_archived",
+ "is_read",
"last_message_content",
"last_message_id",
"order_timestamp",
@@ -3720,6 +3727,10 @@ const docTemplate = `{
"type": "boolean",
"example": false
},
+ "is_read": {
+ "type": "boolean",
+ "example": true
+ },
"last_message_content": {
"type": "string",
"example": "This is a sample message content"
@@ -4388,13 +4399,14 @@ const docTemplate = `{
},
"requests.MessageThreadUpdate": {
"type": "object",
- "required": [
- "is_archived"
- ],
"properties": {
"is_archived": {
"type": "boolean",
"example": true
+ },
+ "is_read": {
+ "type": "boolean",
+ "example": true
}
}
},
@@ -4904,6 +4916,27 @@ const docTemplate = `{
}
}
},
+ "responses.MessageThreadResponse": {
+ "type": "object",
+ "required": [
+ "data",
+ "message",
+ "status"
+ ],
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/entities.MessageThread"
+ },
+ "message": {
+ "type": "string",
+ "example": "Request handled successfully"
+ },
+ "status": {
+ "type": "string",
+ "example": "success"
+ }
+ }
+ },
"responses.MessageThreadsResponse": {
"type": "object",
"required": [
diff --git a/api/docs/swagger.json b/api/docs/swagger.json
index 25db6fc31..ac9c12c98 100644
--- a/api/docs/swagger.json
+++ b/api/docs/swagger.json
@@ -868,7 +868,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/responses.PhoneResponse"
+ "$ref": "#/definitions/responses.MessageThreadResponse"
}
},
"400": {
@@ -883,6 +883,12 @@
"$ref": "#/definitions/responses.Unauthorized"
}
},
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/responses.NotFound"
+ }
+ },
"422": {
"description": "Unprocessable Entity",
"schema": {
@@ -3688,6 +3694,7 @@
"created_at",
"id",
"is_archived",
+ "is_read",
"last_message_content",
"last_message_id",
"order_timestamp",
@@ -3717,6 +3724,10 @@
"type": "boolean",
"example": false
},
+ "is_read": {
+ "type": "boolean",
+ "example": true
+ },
"last_message_content": {
"type": "string",
"example": "This is a sample message content"
@@ -4385,13 +4396,14 @@
},
"requests.MessageThreadUpdate": {
"type": "object",
- "required": [
- "is_archived"
- ],
"properties": {
"is_archived": {
"type": "boolean",
"example": true
+ },
+ "is_read": {
+ "type": "boolean",
+ "example": true
}
}
},
@@ -4901,6 +4913,27 @@
}
}
},
+ "responses.MessageThreadResponse": {
+ "type": "object",
+ "required": [
+ "data",
+ "message",
+ "status"
+ ],
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/entities.MessageThread"
+ },
+ "message": {
+ "type": "string",
+ "example": "Request handled successfully"
+ },
+ "status": {
+ "type": "string",
+ "example": "success"
+ }
+ }
+ },
"responses.MessageThreadsResponse": {
"type": "object",
"required": [
diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml
index 892c9ae45..ddc6ae700 100644
--- a/api/docs/swagger.yaml
+++ b/api/docs/swagger.yaml
@@ -319,6 +319,9 @@ definitions:
is_archived:
example: false
type: boolean
+ is_read:
+ example: true
+ type: boolean
last_message_content:
example: This is a sample message content
type: string
@@ -346,6 +349,7 @@ definitions:
- created_at
- id
- is_archived
+ - is_read
- last_message_content
- last_message_id
- order_timestamp
@@ -853,8 +857,9 @@ definitions:
is_archived:
example: true
type: boolean
- required:
- - is_archived
+ is_read:
+ example: true
+ type: boolean
type: object
requests.PhoneAPIKeyStoreRequest:
properties:
@@ -1227,6 +1232,21 @@ definitions:
- message
- status
type: object
+ responses.MessageThreadResponse:
+ properties:
+ data:
+ $ref: '#/definitions/entities.MessageThread'
+ message:
+ example: Request handled successfully
+ type: string
+ status:
+ example: success
+ type: string
+ required:
+ - data
+ - message
+ - status
+ type: object
responses.MessageThreadsResponse:
properties:
data:
@@ -2182,7 +2202,7 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/responses.PhoneResponse'
+ $ref: '#/definitions/responses.MessageThreadResponse'
"400":
description: Bad Request
schema:
@@ -2191,6 +2211,10 @@ paths:
description: Unauthorized
schema:
$ref: '#/definitions/responses.Unauthorized'
+ "404":
+ description: Not Found
+ schema:
+ $ref: '#/definitions/responses.NotFound'
"422":
description: Unprocessable Entity
schema:
diff --git a/api/pkg/entities/message_thread.go b/api/pkg/entities/message_thread.go
index 2eb1e2d33..3766acc77 100644
--- a/api/pkg/entities/message_thread.go
+++ b/api/pkg/entities/message_thread.go
@@ -12,6 +12,8 @@ type MessageThread struct {
Owner string `json:"owner" example:"+18005550199"`
Contact string `json:"contact" example:"+18005550100"`
IsArchived bool `json:"is_archived" example:"false"`
+ IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"`
+ LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"`
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Color string `json:"color" example:"indigo"`
Status MessageStatus `json:"status" example:"PENDING"`
diff --git a/api/pkg/entities/message_thread_test.go b/api/pkg/entities/message_thread_test.go
new file mode 100644
index 000000000..1409dff29
--- /dev/null
+++ b/api/pkg/entities/message_thread_test.go
@@ -0,0 +1,25 @@
+package entities
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMessageThreadReadFieldsHaveBackwardCompatibleDefaults(t *testing.T) {
+ threadType := reflect.TypeOf(MessageThread{})
+
+ isRead, ok := threadType.FieldByName("IsRead")
+ require.True(t, ok)
+ assert.Contains(t, isRead.Tag.Get("gorm"), "not null")
+ assert.Contains(t, isRead.Tag.Get("gorm"), "default:true")
+ assert.Equal(t, "is_read", isRead.Tag.Get("json"))
+
+ lastReadAt, ok := threadType.FieldByName("LastReadAt")
+ require.True(t, ok)
+ assert.Contains(t, lastReadAt.Tag.Get("gorm"), "not null")
+ assert.Contains(t, lastReadAt.Tag.Get("gorm"), "default:CURRENT_TIMESTAMP")
+ assert.Equal(t, "-", lastReadAt.Tag.Get("json"))
+}
diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go
index 1a00f7737..9520b6634 100644
--- a/api/pkg/handlers/message_thread_handler.go
+++ b/api/pkg/handlers/message_thread_handler.go
@@ -100,9 +100,10 @@ func (h *MessageThreadHandler) Index(c fiber.Ctx) error {
// @Produce json
// @Param messageThreadID path string true "ID of the message thread" default(32343a19-da5e-4b1b-a767-3298a73703ca)
// @Param payload body requests.MessageThreadUpdate true "Payload of message thread details to update"
-// @Success 200 {object} responses.PhoneResponse
+// @Success 200 {object} responses.MessageThreadResponse
// @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 /message-threads/{messageThreadID} [put]
@@ -123,6 +124,9 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error {
}
thread, err := h.service.UpdateStatus(ctx, request.ToUpdateParams(h.userIDFomContext(c)))
+ if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
+ return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID))
+ }
if err != nil {
ctxLogger.Error(stacktrace.Propagate(err, "cannot update message thread with params [%+#v]", request))
return h.responseInternalServerError(c)
diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go
new file mode 100644
index 000000000..ab2d75962
--- /dev/null
+++ b/api/pkg/handlers/message_thread_handler_test.go
@@ -0,0 +1,110 @@
+package handlers
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/middlewares"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/services"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/gofiber/fiber/v3"
+ "github.com/google/uuid"
+ "github.com/palantir/stacktrace"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+ "gorm.io/gorm"
+)
+
+type messageThreadHandlerRepositoryStub struct{}
+
+func (stub *messageThreadHandlerRepositoryStub) Store(context.Context, *entities.MessageThread) error {
+ return nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) UpdateActivity(context.Context, repositories.MessageThreadActivityUpdate) error {
+ return nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
+ return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+}
+
+func (stub *messageThreadHandlerRepositoryStub) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return nil, nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) {
+ return nil, nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) {
+ return nil, nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error {
+ return nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error {
+ return nil
+}
+
+func (stub *messageThreadHandlerRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error {
+ return nil
+}
+
+func TestMessageThreadHandlerUpdate_ReturnsNotFoundWhenThreadIsMissing(t *testing.T) {
+ logger := &messageThreadHandlerNoopLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ service := services.NewMessageThreadService(logger, tracer, &messageThreadHandlerRepositoryStub{}, nil, nil)
+ handler := NewMessageThreadHandler(logger, tracer, validators.NewMessageThreadHandlerValidator(logger, tracer), service)
+
+ app := fiber.New()
+ app.Use(func(c fiber.Ctx) error {
+ c.Locals(middlewares.ContextKeyAuthUserID, entities.AuthContext{ID: entities.UserID("user-id"), Email: "user@example.com"})
+ return c.Next()
+ })
+ handler.RegisterRoutes(app)
+
+ messageThreadID := uuid.New()
+ req := httptest.NewRequest(http.MethodPut, "/v1/message-threads/"+messageThreadID.String(), bytes.NewBufferString(`{"is_read":true}`))
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := app.Test(req, fiber.TestConfig{Timeout: time.Second})
+
+ require.NoError(t, err)
+ require.Equal(t, http.StatusNotFound, resp.StatusCode)
+
+ var payload struct {
+ Message string `json:"message"`
+ }
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
+ require.Equal(t, "cannot find message thread with ID ["+messageThreadID.String()+"]", payload.Message)
+}
+
+type messageThreadHandlerNoopLogger struct{}
+
+var _ telemetry.Logger = (*messageThreadHandlerNoopLogger)(nil)
+
+func (logger *messageThreadHandlerNoopLogger) Error(_ error) {}
+func (logger *messageThreadHandlerNoopLogger) WithService(_ string) telemetry.Logger { return logger }
+
+func (logger *messageThreadHandlerNoopLogger) WithString(_, _ string) telemetry.Logger { return logger }
+
+func (logger *messageThreadHandlerNoopLogger) WithSpan(_ trace.SpanContext) telemetry.Logger {
+ return logger
+}
+func (logger *messageThreadHandlerNoopLogger) Trace(_ string) {}
+func (logger *messageThreadHandlerNoopLogger) Info(_ string) {}
+func (logger *messageThreadHandlerNoopLogger) Warn(_ error) {}
+func (logger *messageThreadHandlerNoopLogger) Debug(_ string) {}
+func (logger *messageThreadHandlerNoopLogger) Fatal(_ error) {}
+func (logger *messageThreadHandlerNoopLogger) Printf(_ string, _ ...interface{}) {}
diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go
index e5f90ca39..47621296e 100644
--- a/api/pkg/listeners/message_thread_listener.go
+++ b/api/pkg/listeners/message_thread_listener.go
@@ -40,6 +40,7 @@ func NewMessageThreadListener(
events.EventTypeMessagePhoneDelivered: l.OnMessagePhoneDelivered,
events.EventTypeMessageSendFailed: l.OnMessagePhoneFailed,
events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived,
+ events.MessageCallMissed: l.OnMessageCallMissed,
events.EventTypeMessageNotificationScheduled: l.onMessageNotificationScheduled,
events.EventTypeMessageSendExpired: l.onMessageExpired,
events.UserAccountDeleted: l.onUserAccountDeleted,
@@ -209,13 +210,15 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex
}
updateParams := services.MessageThreadUpdateParams{
- Owner: payload.Owner,
- Contact: payload.Contact,
- Timestamp: payload.Timestamp,
- UserID: payload.UserID,
- Status: entities.MessageStatusReceived,
- Content: payload.Content,
- MessageID: payload.MessageID,
+ Owner: payload.Owner,
+ Contact: payload.Contact,
+ Timestamp: payload.Timestamp,
+ UserID: payload.UserID,
+ Status: entities.MessageStatusReceived,
+ Content: payload.Content,
+ MessageID: payload.MessageID,
+ MarkAsUnread: true,
+ EventTimestamp: event.Time(),
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
@@ -225,6 +228,33 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex
return nil
}
+// OnMessageCallMissed handles the events.MessageCallMissed event
+func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
+ ctx, span := listener.tracer.Start(ctx)
+ defer span.End()
+
+ var payload events.MessageCallMissedPayload
+ if err := event.DataAs(&payload); err != nil {
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload))
+ }
+
+ params := services.MessageThreadUpdateParams{
+ Owner: payload.Owner,
+ Contact: payload.Contact,
+ UserID: payload.UserID,
+ Status: entities.MessageStatusReceived,
+ Timestamp: payload.Timestamp,
+ Content: "Missed phone call",
+ MessageID: payload.MessageID,
+ MarkAsUnread: true,
+ EventTimestamp: event.Time(),
+ }
+ if err := listener.service.UpdateThread(ctx, params); err != nil {
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID()))
+ }
+ return nil
+}
+
// onMessageNotificationScheduled handles the events.EventTypeMessageNotificationScheduled event
func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx context.Context, event cloudevents.Event) error {
ctx, span := listener.tracer.Start(ctx)
diff --git a/api/pkg/listeners/message_thread_listener_test.go b/api/pkg/listeners/message_thread_listener_test.go
new file mode 100644
index 000000000..39a444cce
--- /dev/null
+++ b/api/pkg/listeners/message_thread_listener_test.go
@@ -0,0 +1,72 @@
+package listeners
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/events"
+ "github.com/NdoleStudio/httpsms/pkg/services"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ cloudevents "github.com/cloudevents/sdk-go/v2"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMessageThreadListenerMarksInboundMessageUnread(t *testing.T) {
+ repository, routes := newMessageThreadListenerForTest()
+ event := cloudevents.NewEvent()
+ event.SetID(uuid.NewString())
+ event.SetSource("/v1/messages/phone-received")
+ event.SetType(events.EventTypeMessagePhoneReceived)
+ event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC))
+ require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessagePhoneReceivedPayload{
+ MessageID: uuid.New(),
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ Content: "hello",
+ Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC),
+ }))
+
+ err := routes[events.EventTypeMessagePhoneReceived](context.Background(), event)
+
+ require.NoError(t, err)
+ assert.True(t, repository.activity.MarkAsUnread)
+ assert.Equal(t, event.Time(), repository.activity.EventTimestamp)
+}
+
+func TestMessageThreadListenerMarksMissedCallUnread(t *testing.T) {
+ repository, routes := newMessageThreadListenerForTest()
+ event := cloudevents.NewEvent()
+ event.SetID(uuid.NewString())
+ event.SetSource("/v1/messages/call-missed")
+ event.SetType(events.MessageCallMissed)
+ event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC))
+ require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessageCallMissedPayload{
+ MessageID: uuid.New(),
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC),
+ }))
+ require.Contains(t, routes, events.MessageCallMissed)
+
+ err := routes[events.MessageCallMissed](context.Background(), event)
+
+ require.NoError(t, err)
+ assert.True(t, repository.activity.MarkAsUnread)
+ assert.Equal(t, "Missed phone call", repository.activity.Content)
+ assert.Equal(t, event.Time(), repository.activity.EventTimestamp)
+}
+
+func newMessageThreadListenerForTest() (*listenerMessageThreadRepository, map[string]events.EventListener) {
+ repository := &listenerMessageThreadRepository{}
+ logger := &noopListenerLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ service := services.NewMessageThreadService(logger, tracer, repository, nil, nil)
+ _, routes := NewMessageThreadListener(logger, tracer, service)
+ return repository, routes
+}
diff --git a/api/pkg/listeners/read_receipts_test_helpers_test.go b/api/pkg/listeners/read_receipts_test_helpers_test.go
new file mode 100644
index 000000000..60cd27e65
--- /dev/null
+++ b/api/pkg/listeners/read_receipts_test_helpers_test.go
@@ -0,0 +1,66 @@
+package listeners
+
+import (
+ "context"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/google/uuid"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type noopListenerLogger struct{}
+
+func (logger *noopListenerLogger) Error(error) {}
+func (logger *noopListenerLogger) WithService(string) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) WithString(string, string) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) Trace(string) {}
+func (logger *noopListenerLogger) Info(string) {}
+func (logger *noopListenerLogger) Warn(error) {}
+func (logger *noopListenerLogger) Debug(string) {}
+func (logger *noopListenerLogger) Fatal(error) {}
+func (logger *noopListenerLogger) Printf(string, ...interface{}) {}
+
+type listenerMessageThreadRepository struct {
+ activity repositories.MessageThreadActivityUpdate
+}
+
+func (repository *listenerMessageThreadRepository) Store(context.Context, *entities.MessageThread) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateActivity(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ repository.activity = params
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateStatus(_ context.Context, _ entities.UserID, threadID uuid.UUID, _ repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: threadID}, nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: uuid.New()}, nil
+}
+
+func (repository *listenerMessageThreadRepository) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) {
+ return &entities.MessageThread{}, nil
+}
+
+func (repository *listenerMessageThreadRepository) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) {
+ threads := []entities.MessageThread{}
+ return &threads, nil
+}
+
+func (repository *listenerMessageThreadRepository) Delete(context.Context, entities.UserID, uuid.UUID) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) DeleteAllForUser(context.Context, entities.UserID) error {
+ return nil
+}
diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go
index 3108ed671..8aa9054f0 100644
--- a/api/pkg/listeners/websocket_listener.go
+++ b/api/pkg/listeners/websocket_listener.go
@@ -36,9 +36,25 @@ func NewWebsocketListener(
events.EventTypeMessagePhoneSent: l.onMessagePhoneSent,
events.EventTypeMessageSendFailed: l.onMessagePhoneFailed,
events.EventTypeMessagePhoneReceived: l.onMessagePhoneReceived,
+ events.MessageCallMissed: l.onMessageCallMissed,
}
}
+func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
+ ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger)
+ defer span.End()
+
+ var payload events.MessageCallMissedPayload
+ if err := event.DataAs(&payload); err != nil {
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload))
+ }
+
+ if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ }
+ return nil
+}
+
// onMessagePhoneSent handles the events.EventTypeMessagePhoneSent event
func (listener *WebsocketListener) onMessagePhoneSent(ctx context.Context, event cloudevents.Event) error {
ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger)
diff --git a/api/pkg/listeners/websocket_listener_test.go b/api/pkg/listeners/websocket_listener_test.go
new file mode 100644
index 000000000..fdaf1c6d5
--- /dev/null
+++ b/api/pkg/listeners/websocket_listener_test.go
@@ -0,0 +1,18 @@
+package listeners
+
+import (
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/events"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/pusher/pusher-http-go/v5"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWebsocketListenerRegistersMissedCalls(t *testing.T) {
+ logger := &noopListenerLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ _, routes := NewWebsocketListener(logger, tracer, &pusher.Client{})
+
+ assert.Contains(t, routes, events.MessageCallMissed)
+}
diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go
index 7bc07f7b3..071f5cc79 100644
--- a/api/pkg/repositories/gorm_message_thread_repository.go
+++ b/api/pkg/repositories/gorm_message_thread_repository.go
@@ -35,6 +35,48 @@ func NewGormMessageThreadRepository(
}
}
+func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string]any {
+ updates := map[string]any{
+ "order_timestamp": params.Timestamp,
+ "last_message_id": params.MessageID,
+ "last_message_content": params.Content,
+ "status": params.Status,
+ }
+ if params.Unarchive {
+ updates["is_archived"] = false
+ }
+ if params.MarkAsUnread {
+ updates["is_read"] = gorm.Expr(
+ "CASE WHEN last_read_at < ? THEN ? ELSE is_read END",
+ params.EventTimestamp,
+ false,
+ )
+ }
+ return updates
+}
+
+func messageThreadDeletedUpdates(params MessageThreadDeletedUpdate) map[string]any {
+ return map[string]any{
+ "last_message_id": params.LastMessageID,
+ "last_message_content": params.LastMessageContent,
+ "status": params.LastMessageStatus,
+ }
+}
+
+func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any {
+ updates := make(map[string]any)
+ if params.IsArchived != nil {
+ updates["is_archived"] = *params.IsArchived
+ }
+ if params.IsRead != nil {
+ updates["is_read"] = *params.IsRead
+ if *params.IsRead {
+ updates["last_read_at"] = params.ReadAt
+ }
+ }
+ return updates
+}
+
func (repository *gormMessageThreadRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error {
ctx, span := repository.tracer.Start(ctx)
defer span.End()
@@ -60,20 +102,17 @@ func (repository *gormMessageThreadRepository) Delete(ctx context.Context, userI
}
// UpdateAfterDeletedMessage updates a thread after the original message has been deleted
-func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error {
+func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error {
ctx, span := repository.tracer.Start(ctx)
defer span.End()
- err := repository.db.WithContext(ctx).Model(&entities.MessageThread{}).
- Where("user_id = ?", userID).
- Where("last_message_id = ?", messageID).
- Updates(map[string]any{
- "last_message_id": nil,
- "last_message_content": nil,
- "status": entities.MessageStatusDeleted,
- }).Error
- if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread after message is deleted with userID [%s] and messageID [%s]", userID, messageID))
+ result := repository.db.WithContext(ctx).
+ Model(&entities.MessageThread{}).
+ Where("user_id = ?", params.UserID).
+ Where("id = ?", params.MessageThreadID).
+ Updates(messageThreadDeletedUpdates(params))
+ if result.Error != nil {
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID))
}
return nil
@@ -84,25 +123,77 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread
ctx, span := repository.tracer.Start(ctx)
defer span.End()
- if err := repository.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(thread).Error; err != nil {
+ isRead := thread.IsRead
+ err := repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(thread)
+ thread.IsRead = isRead
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 || isRead {
+ return nil
+ }
+
+ return tx.Model(&entities.MessageThread{}).
+ Where("user_id = ?", thread.UserID).
+ Where("id = ?", thread.ID).
+ UpdateColumn("is_read", false).
+ Error
+ })
+ if err != nil {
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message thread with ID [%s]", thread.ID))
}
return nil
}
-// Update a new entities.MessageThread
-func (repository *gormMessageThreadRepository) Update(ctx context.Context, thread *entities.MessageThread) error {
+// UpdateActivity persists the last-message activity fields for a thread
+func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error {
ctx, span := repository.tracer.Start(ctx)
defer span.End()
- if err := repository.db.WithContext(ctx).Save(thread).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread thread with ID [%s]", thread.ID))
+ result := repository.db.WithContext(ctx).
+ Model(&entities.MessageThread{}).
+ Where("user_id = ?", params.UserID).
+ Where("id = ?", params.MessageThreadID).
+ Updates(messageThreadActivityUpdates(params))
+ if result.Error != nil {
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID))
+ }
+ if result.RowsAffected == 0 {
+ return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID))
}
return nil
}
+// UpdateStatus persists archive/read status fields for a thread
+func (repository *gormMessageThreadRepository) UpdateStatus(
+ ctx context.Context,
+ userID entities.UserID,
+ messageThreadID uuid.UUID,
+ params MessageThreadStatusUpdate,
+) (*entities.MessageThread, error) {
+ ctx, span := repository.tracer.Start(ctx)
+ defer span.End()
+
+ thread := new(entities.MessageThread)
+ result := repository.db.WithContext(ctx).
+ Model(thread).
+ Clauses(clause.Returning{}).
+ Where("user_id = ?", userID).
+ Where("id = ?", messageThreadID).
+ Updates(messageThreadStatusUpdates(params))
+ if result.Error != nil {
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID))
+ }
+ if result.RowsAffected == 0 {
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID))
+ }
+
+ return thread, nil
+}
+
// LoadByOwnerContact a thread between 2 users
func (repository *gormMessageThreadRepository) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) {
ctx, span := repository.tracer.Start(ctx)
diff --git a/api/pkg/repositories/gorm_message_thread_repository_test.go b/api/pkg/repositories/gorm_message_thread_repository_test.go
new file mode 100644
index 000000000..31dc43ed5
--- /dev/null
+++ b/api/pkg/repositories/gorm_message_thread_repository_test.go
@@ -0,0 +1,204 @@
+package repositories
+
+import (
+ "context"
+ "database/sql"
+ "database/sql/driver"
+ "errors"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+)
+
+type messageThreadTestStatement struct {
+ query string
+ args []any
+}
+
+type messageThreadTestConnPool struct {
+ statements []messageThreadTestStatement
+}
+
+func (messageThreadTestConnPool) PrepareContext(context.Context, string) (*sql.Stmt, error) {
+ return nil, errors.New("unexpected PrepareContext")
+}
+
+func (pool *messageThreadTestConnPool) ExecContext(_ context.Context, query string, args ...any) (sql.Result, error) {
+ pool.statements = append(pool.statements, messageThreadTestStatement{
+ query: query,
+ args: append([]any(nil), args...),
+ })
+ return driver.RowsAffected(1), nil
+}
+
+func (messageThreadTestConnPool) QueryContext(context.Context, string, ...any) (*sql.Rows, error) {
+ return nil, errors.New("unexpected QueryContext")
+}
+
+func (messageThreadTestConnPool) QueryRowContext(context.Context, string, ...any) *sql.Row {
+ return &sql.Row{}
+}
+
+func (pool *messageThreadTestConnPool) BeginTx(context.Context, *sql.TxOptions) (gorm.ConnPool, error) {
+ return pool, nil
+}
+
+func (*messageThreadTestConnPool) Commit() error {
+ return nil
+}
+
+func (*messageThreadTestConnPool) Rollback() error {
+ return nil
+}
+
+type messageThreadTestLogger struct{}
+
+func (logger *messageThreadTestLogger) Error(error) {}
+func (logger *messageThreadTestLogger) WithService(string) telemetry.Logger { return logger }
+
+func (logger *messageThreadTestLogger) WithString(string, string) telemetry.Logger { return logger }
+
+func (logger *messageThreadTestLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger }
+func (logger *messageThreadTestLogger) Trace(string) {}
+func (logger *messageThreadTestLogger) Info(string) {}
+func (logger *messageThreadTestLogger) Warn(error) {}
+func (logger *messageThreadTestLogger) Debug(string) {}
+func (logger *messageThreadTestLogger) Fatal(error) {}
+func (logger *messageThreadTestLogger) Printf(string, ...interface{}) {}
+
+func TestMessageThreadStorePreservesExplicitUnreadState(t *testing.T) {
+ pool := &messageThreadTestConnPool{}
+ db, err := gorm.Open(
+ postgres.New(postgres.Config{
+ Conn: pool,
+ WithoutReturning: true,
+ }),
+ &gorm.Config{DisableAutomaticPing: true},
+ )
+ require.NoError(t, err)
+
+ logger := &messageThreadTestLogger{}
+ repository := NewGormMessageThreadRepository(logger, telemetry.NewOtelLogger("test", logger), db)
+ thread := &entities.MessageThread{
+ ID: uuid.New(),
+ IsRead: false,
+ }
+
+ require.NoError(t, repository.Store(context.Background(), thread))
+ assert.False(t, thread.IsRead)
+
+ require.NotEmpty(t, pool.statements)
+ update := pool.statements[len(pool.statements)-1]
+ assert.True(t, strings.HasPrefix(update.query, `UPDATE "message_threads"`))
+ assert.Contains(t, update.query, `"is_read"=$1`)
+ assert.Contains(t, update.args, false)
+}
+
+func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) {
+ messageID := uuid.New()
+ updates := messageThreadActivityUpdates(MessageThreadActivityUpdate{
+ Timestamp: time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC),
+ MessageID: messageID,
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ })
+
+ assert.Equal(t, map[string]any{
+ "order_timestamp": time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC),
+ "last_message_id": messageID,
+ "last_message_content": "hello",
+ "status": entities.MessageStatus(entities.MessageStatusReceived),
+ }, updates)
+ assert.NotContains(t, updates, "is_read")
+ assert.NotContains(t, updates, "is_archived")
+ assert.NotContains(t, updates, "last_read_at")
+}
+
+func TestUpdateActivityMarksUnreadWithOneQuery(t *testing.T) {
+ pool := &messageThreadTestConnPool{}
+ db, err := gorm.Open(
+ postgres.New(postgres.Config{
+ Conn: pool,
+ WithoutReturning: true,
+ }),
+ &gorm.Config{DisableAutomaticPing: true},
+ )
+ require.NoError(t, err)
+
+ logger := &messageThreadTestLogger{}
+ repository := NewGormMessageThreadRepository(logger, telemetry.NewOtelLogger("test", logger), db)
+
+ err = repository.UpdateActivity(context.Background(), MessageThreadActivityUpdate{
+ MessageThreadID: uuid.New(),
+ UserID: entities.UserID("user-id"),
+ Timestamp: time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC),
+ MessageID: uuid.New(),
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ MarkAsUnread: true,
+ EventTimestamp: time.Date(2026, 7, 19, 10, 0, 1, 0, time.UTC),
+ })
+
+ require.NoError(t, err)
+ var updates []messageThreadTestStatement
+ for _, statement := range pool.statements {
+ if strings.HasPrefix(statement.query, `UPDATE "message_threads"`) {
+ updates = append(updates, statement)
+ }
+ }
+ require.Len(t, updates, 1)
+ assert.Contains(t, updates[0].query, `"is_read"=CASE WHEN last_read_at <`)
+}
+
+func TestMessageThreadDeletedUpdatesPreserveStatusType(t *testing.T) {
+ messageID := uuid.New()
+ content := "previous message"
+ updates := messageThreadDeletedUpdates(MessageThreadDeletedUpdate{
+ LastMessageID: &messageID,
+ LastMessageContent: &content,
+ LastMessageStatus: entities.MessageStatusDelivered,
+ })
+
+ assert.Equal(t, map[string]any{
+ "last_message_id": &messageID,
+ "last_message_content": &content,
+ "status": entities.MessageStatus(entities.MessageStatusDelivered),
+ }, updates)
+}
+
+func TestMessageThreadStatusUpdatesReadOnly(t *testing.T) {
+ isRead := true
+ readAt := time.Date(2026, 7, 18, 7, 1, 0, 0, time.UTC)
+
+ updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
+ IsRead: &isRead,
+ ReadAt: readAt,
+ })
+
+ assert.Equal(t, map[string]any{
+ "is_read": true,
+ "last_read_at": readAt,
+ }, updates)
+ assert.NotContains(t, updates, "is_archived")
+}
+
+func TestMessageThreadStatusUpdatesArchiveOnly(t *testing.T) {
+ isArchived := true
+
+ updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
+ IsArchived: &isArchived,
+ })
+
+ assert.Equal(t, map[string]any{"is_archived": true}, updates)
+ assert.NotContains(t, updates, "is_read")
+ assert.NotContains(t, updates, "last_read_at")
+}
diff --git a/api/pkg/repositories/message_thread_repository.go b/api/pkg/repositories/message_thread_repository.go
index 542fd519b..e60931419 100644
--- a/api/pkg/repositories/message_thread_repository.go
+++ b/api/pkg/repositories/message_thread_repository.go
@@ -2,19 +2,50 @@ package repositories
import (
"context"
+ "time"
"github.com/google/uuid"
"github.com/NdoleStudio/httpsms/pkg/entities"
)
+type MessageThreadActivityUpdate struct {
+ MessageThreadID uuid.UUID
+ UserID entities.UserID
+ // Timestamp controls thread activity ordering; EventTimestamp is the server-side unread watermark.
+ Timestamp time.Time
+ MessageID uuid.UUID
+ Content string
+ Status entities.MessageStatus
+ MarkAsUnread bool
+ EventTimestamp time.Time
+ Unarchive bool
+}
+
+type MessageThreadStatusUpdate struct {
+ IsArchived *bool
+ IsRead *bool
+ ReadAt time.Time
+}
+
+type MessageThreadDeletedUpdate struct {
+ MessageThreadID uuid.UUID
+ UserID entities.UserID
+ LastMessageID *uuid.UUID
+ LastMessageContent *string
+ LastMessageStatus entities.MessageStatus
+}
+
// MessageThreadRepository loads and persists an entities.MessageThread
type MessageThreadRepository interface {
// Store a new entities.MessageThread
Store(ctx context.Context, thread *entities.MessageThread) error
- // Update a new entities.MessageThread
- Update(ctx context.Context, thread *entities.MessageThread) error
+ // UpdateActivity persists the last-message activity fields for a thread
+ UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error
+
+ // UpdateStatus persists archive/read status fields for a thread
+ UpdateStatus(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID, params MessageThreadStatusUpdate) (*entities.MessageThread, error)
// LoadByOwnerContact fetches a thread between owner and contact
LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error)
@@ -26,7 +57,7 @@ type MessageThreadRepository interface {
Index(ctx context.Context, userID entities.UserID, owner string, archived bool, params IndexParams) (*[]entities.MessageThread, error)
// UpdateAfterDeletedMessage updates a thread after the original message has been deleted
- UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error
+ UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error
// Delete an entities.MessageThread by ID
Delete(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID) error
diff --git a/api/pkg/requests/message_thread_update_request.go b/api/pkg/requests/message_thread_update_request.go
index d64d75c59..3309fc91d 100644
--- a/api/pkg/requests/message_thread_update_request.go
+++ b/api/pkg/requests/message_thread_update_request.go
@@ -10,7 +10,8 @@ import (
// MessageThreadUpdate is the payload for updating a message thread
type MessageThreadUpdate struct {
request
- IsArchived bool `json:"is_archived" example:"true"`
+ IsArchived *bool `json:"is_archived,omitempty" example:"true"`
+ IsRead *bool `json:"is_read,omitempty" example:"true"`
MessageThreadID string `json:"messageThreadID" swaggerignore:"true"` // used internally for validation
}
@@ -21,5 +22,6 @@ func (input *MessageThreadUpdate) ToUpdateParams(userID entities.UserID) service
UserID: userID,
MessageThreadID: uuid.MustParse(input.MessageThreadID),
IsArchived: input.IsArchived,
+ IsRead: input.IsRead,
}
}
diff --git a/api/pkg/requests/message_thread_update_request_test.go b/api/pkg/requests/message_thread_update_request_test.go
new file mode 100644
index 000000000..9f9579fda
--- /dev/null
+++ b/api/pkg/requests/message_thread_update_request_test.go
@@ -0,0 +1,25 @@
+package requests
+
+import (
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T) {
+ threadID := uuid.New()
+ isRead := true
+ input := MessageThreadUpdate{
+ MessageThreadID: threadID.String(),
+ IsRead: &isRead,
+ }
+
+ params := input.ToUpdateParams(entities.UserID("user-id"))
+
+ assert.Equal(t, threadID, params.MessageThreadID)
+ assert.Equal(t, entities.UserID("user-id"), params.UserID)
+ assert.Nil(t, params.IsArchived)
+ assert.Same(t, &isRead, params.IsRead)
+}
diff --git a/api/pkg/responses/message_thead_responses.go b/api/pkg/responses/message_thead_responses.go
index 917706a47..e47fec287 100644
--- a/api/pkg/responses/message_thead_responses.go
+++ b/api/pkg/responses/message_thead_responses.go
@@ -7,3 +7,9 @@ type MessageThreadsResponse struct {
response
Data []entities.MessageThread `json:"data"`
}
+
+// MessageThreadResponse is the payload containing entities.MessageThread
+type MessageThreadResponse struct {
+ response
+ Data entities.MessageThread `json:"data"`
+}
diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go
index c77458cbf..e3dc882c7 100644
--- a/api/pkg/services/message_thread_service.go
+++ b/api/pkg/services/message_thread_service.go
@@ -50,7 +50,10 @@ type MessageThreadUpdateParams struct {
Content string
UserID entities.UserID
MessageID uuid.UUID
- Timestamp time.Time
+ // Timestamp controls thread activity ordering; EventTimestamp is the server-side unread watermark.
+ Timestamp time.Time
+ MarkAsUnread bool
+ EventTimestamp time.Time
}
// shouldCheckUnarchive reports whether a thread update is a new inbound message
@@ -101,27 +104,39 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
return nil
}
+ activity := repositories.MessageThreadActivityUpdate{
+ MessageThreadID: thread.ID,
+ UserID: params.UserID,
+ Timestamp: params.Timestamp,
+ MessageID: params.MessageID,
+ Content: params.Content,
+ Status: params.Status,
+ MarkAsUnread: params.MarkAsUnread,
+ EventTimestamp: params.EventTimestamp,
+ }
+
if service.shouldCheckUnarchive(thread, params) {
phone, phoneErr := service.phoneRepository.Load(ctx, params.UserID, params.Owner)
if phoneErr != nil {
ctxLogger.Warn(stacktrace.Propagate(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID))
} else if phone.UnarchiveThread {
- thread.UpdateArchive(false)
+ activity.Unarchive = true
ctxLogger.Info(fmt.Sprintf("unarchiving thread [%s] after inbound message [%s]", thread.ID, params.MessageID))
}
}
- if err = service.repository.Update(ctx, thread.Update(params.Timestamp, params.MessageID, params.Content, params.Status)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID))
+ if err = service.repository.UpdateActivity(ctx, activity); err != nil {
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID))
}
- ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, thread.LastMessageID, thread.Status))
+ ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, params.MessageID, params.Status))
return nil
}
// MessageThreadStatusParams are parameters for updating a thread status
type MessageThreadStatusParams struct {
- IsArchived bool
+ IsArchived *bool
+ IsRead *bool
UserID entities.UserID
MessageThreadID uuid.UUID
}
@@ -131,18 +146,16 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me
ctx, span := service.tracer.Start(ctx)
defer span.End()
- ctxLogger := service.tracer.CtxLogger(service.logger, span)
-
- thread, err := service.repository.Load(ctx, params.UserID, params.MessageThreadID)
- if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread with id [%s]", params.MessageThreadID))
+ update := repositories.MessageThreadStatusUpdate{
+ IsArchived: params.IsArchived,
+ IsRead: params.IsRead,
+ ReadAt: time.Now().UTC(),
}
-
- if err = service.repository.Update(ctx, thread.UpdateArchive(params.IsArchived)); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread with id [%s] with archive status [%t]", thread.ID, params.IsArchived))
+ thread, err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update)
+ if err != nil {
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID))
}
- ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with archive status [%t]", thread.ID, thread.IsArchived))
return thread, nil
}
@@ -172,12 +185,13 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte
return nil
}
- thread.LastMessageContent = payload.PreviousMessageContent
- thread.LastMessageID = payload.PreviousMessageID
- thread.Status = *payload.PreviousMessageStatus
- thread.UpdatedAt = time.Now().UTC()
-
- if err = service.repository.Update(ctx, thread); err != nil {
+ if err = service.repository.UpdateAfterDeletedMessage(ctx, repositories.MessageThreadDeletedUpdate{
+ MessageThreadID: thread.ID,
+ UserID: thread.UserID,
+ LastMessageID: payload.PreviousMessageID,
+ LastMessageContent: payload.PreviousMessageContent,
+ LastMessageStatus: *payload.PreviousMessageStatus,
+ }); err != nil {
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID))
}
@@ -191,18 +205,21 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me
ctxLogger := service.tracer.CtxLogger(service.logger, span)
+ now := time.Now().UTC()
thread := &entities.MessageThread{
ID: uuid.New(),
Owner: params.Owner,
Contact: params.Contact,
UserID: params.UserID,
IsArchived: false,
+ IsRead: !params.MarkAsUnread,
+ LastReadAt: now,
Color: service.getColor(),
LastMessageContent: ¶ms.Content,
Status: params.Status,
LastMessageID: ¶ms.MessageID,
- CreatedAt: time.Now().UTC(),
- UpdatedAt: time.Now().UTC(),
+ CreatedAt: now,
+ UpdatedAt: now,
OrderTimestamp: params.Timestamp,
}
diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go
index 7e48ab433..fc1e84313 100644
--- a/api/pkg/services/message_thread_service_test.go
+++ b/api/pkg/services/message_thread_service_test.go
@@ -1,12 +1,226 @@
package services
import (
+ "context"
"testing"
+ "time"
"github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/google/uuid"
+ "github.com/palantir/stacktrace"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gorm.io/gorm"
)
+type messageThreadRepositoryStub struct {
+ loadByOwnerContact func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error)
+ load func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error)
+ store func(context.Context, *entities.MessageThread) error
+ updateActivity func(context.Context, repositories.MessageThreadActivityUpdate) error
+ updateStatus func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error)
+}
+
+func (stub *messageThreadRepositoryStub) Store(ctx context.Context, thread *entities.MessageThread) error {
+ if stub.store != nil {
+ return stub.store(ctx, thread)
+ }
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateActivity(ctx context.Context, params repositories.MessageThreadActivityUpdate) error {
+ if stub.updateActivity != nil {
+ return stub.updateActivity(ctx, params)
+ }
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateStatus(ctx context.Context, userID entities.UserID, threadID uuid.UUID, params repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
+ if stub.updateStatus != nil {
+ return stub.updateStatus(ctx, userID, threadID, params)
+ }
+ return &entities.MessageThread{ID: threadID}, nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error {
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) {
+ return stub.loadByOwnerContact(ctx, userID, owner, contact)
+}
+
+func (stub *messageThreadRepositoryStub) Load(ctx context.Context, userID entities.UserID, id uuid.UUID) (*entities.MessageThread, error) {
+ return stub.load(ctx, userID, id)
+}
+
+func (stub *messageThreadRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) {
+ threads := []entities.MessageThread{}
+ return &threads, nil
+}
+
+func (stub *messageThreadRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error {
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error {
+ return nil
+}
+
+func newMessageThreadServiceForTest(repository repositories.MessageThreadRepository) *MessageThreadService {
+ logger := &noopLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ return NewMessageThreadService(logger, tracer, repository, nil, nil)
+}
+
+func TestUpdateThreadPassesUnreadWatermarkForInboundActivity(t *testing.T) {
+ threadID := uuid.New()
+ eventTimestamp := time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)
+ var captured repositories.MessageThreadActivityUpdate
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: threadID}, nil
+ },
+ updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ captured = params
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ Timestamp: eventTimestamp,
+ MarkAsUnread: true,
+ EventTimestamp: eventTimestamp,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, captured.MarkAsUnread)
+ assert.Equal(t, eventTimestamp, captured.EventTimestamp)
+}
+
+func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) {
+ var captured repositories.MessageThreadActivityUpdate
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: uuid.New(), IsRead: false}, nil
+ },
+ updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ captured = params
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "outbound",
+ Status: entities.MessageStatusSent,
+ Timestamp: time.Now().UTC(),
+ })
+
+ require.NoError(t, err)
+ assert.False(t, captured.MarkAsUnread)
+}
+
+func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) {
+ tests := []struct {
+ name string
+ marksUnread bool
+ wantRead bool
+ }{
+ {name: "inbound", marksUnread: true, wantRead: false},
+ {name: "outbound", marksUnread: false, wantRead: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var stored *entities.MessageThread
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ },
+ store: func(_ context.Context, thread *entities.MessageThread) error {
+ stored = thread
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ Timestamp: time.Now().UTC(),
+ MarkAsUnread: test.marksUnread,
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, stored)
+ assert.Equal(t, test.wantRead, stored.IsRead)
+ assert.False(t, stored.LastReadAt.IsZero())
+ })
+ }
+}
+
+func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) {
+ threadID := uuid.New()
+ isRead := false
+ var captured repositories.MessageThreadStatusUpdate
+ repository := &messageThreadRepositoryStub{
+ updateStatus: func(_ context.Context, _ entities.UserID, _ uuid.UUID, params repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
+ captured = params
+ return &entities.MessageThread{ID: threadID, IsArchived: true, IsRead: false}, nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ thread, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
+ UserID: entities.UserID("user-id"),
+ MessageThreadID: threadID,
+ IsRead: &isRead,
+ })
+
+ require.NoError(t, err)
+ assert.Nil(t, captured.IsArchived)
+ assert.Same(t, &isRead, captured.IsRead)
+ assert.False(t, captured.ReadAt.IsZero())
+ assert.True(t, thread.IsArchived)
+ assert.False(t, thread.IsRead)
+}
+
+func TestUpdateStatusPreservesNotFoundCode(t *testing.T) {
+ repository := &messageThreadRepositoryStub{
+ updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
+ return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ isRead := true
+ _, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
+ UserID: entities.UserID("user-id"),
+ MessageThreadID: uuid.New(),
+ IsRead: &isRead,
+ })
+
+ assert.Equal(t, repositories.ErrCodeNotFound, stacktrace.GetCode(err))
+}
+
func TestShouldCheckUnarchive(t *testing.T) {
service := &MessageThreadService{}
diff --git a/api/pkg/validators/message_thread_handler_validator.go b/api/pkg/validators/message_thread_handler_validator.go
index a4e5cd647..72a64194f 100644
--- a/api/pkg/validators/message_thread_handler_validator.go
+++ b/api/pkg/validators/message_thread_handler_validator.go
@@ -72,5 +72,13 @@ func (validator *MessageThreadHandlerValidator) ValidateUpdate(_ context.Context
},
})
- return v.ValidateStruct()
+ errors := v.ValidateStruct()
+ if request.IsArchived == nil && request.IsRead == nil {
+ if errors == nil {
+ errors = url.Values{}
+ }
+ errors.Add("payload", "at least one of is_archived or is_read is required")
+ }
+
+ return errors
}
diff --git a/api/pkg/validators/message_thread_handler_validator_test.go b/api/pkg/validators/message_thread_handler_validator_test.go
new file mode 100644
index 000000000..e5ec7c1b7
--- /dev/null
+++ b/api/pkg/validators/message_thread_handler_validator_test.go
@@ -0,0 +1,34 @@
+package validators
+
+import (
+ "context"
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/requests"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestValidateUpdateRequiresAtLeastOneStatusField(t *testing.T) {
+ validator := &MessageThreadHandlerValidator{}
+ request := requests.MessageThreadUpdate{
+ MessageThreadID: uuid.NewString(),
+ }
+
+ errors := validator.ValidateUpdate(context.Background(), request)
+
+ assert.NotEmpty(t, errors.Get("payload"))
+}
+
+func TestValidateUpdateAcceptsReadOnlyUpdate(t *testing.T) {
+ validator := &MessageThreadHandlerValidator{}
+ isRead := true
+ request := requests.MessageThreadUpdate{
+ MessageThreadID: uuid.NewString(),
+ IsRead: &isRead,
+ }
+
+ errors := validator.ValidateUpdate(context.Background(), request)
+
+ assert.Empty(t, errors)
+}
diff --git a/docs/superpowers/plans/2026-07-18-read-receipts.md b/docs/superpowers/plans/2026-07-18-read-receipts.md
new file mode 100644
index 000000000..1887a1511
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-18-read-receipts.md
@@ -0,0 +1,1760 @@
+# Message Thread Read Receipts 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:** Persist message-thread read state, mark inbound SMS and missed calls unread, automatically mark opened threads read, and highlight unread threads in the web UI.
+
+**Architecture:** Store `is_read` plus an internal `last_read_at` watermark on each message thread. Replace full-row thread saves with field-scoped GORM updates so concurrent event listeners cannot overwrite a newer read action; inbound updates compare CloudEvent creation time against the stored watermark. Extend the existing thread update endpoint with optional archive/read fields, then have the Nuxt store call it automatically when the thread page opens or refreshes after inbound realtime events.
+
+**Tech Stack:** Go 1.25, Fiber v3, GORM/PostgreSQL, CloudEvents, Pusher, Nuxt 4 SPA, Vue 3, Pinia, Vuetify 4, TypeScript.
+
+## Global Constraints
+
+- Work only in `C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts` on branch `feat/read-receipts`, based on `origin/main`.
+- Existing message threads must migrate as read.
+- Only incoming SMS and missed-call activity marks a thread unread.
+- Outbound messages and delivery/status events preserve the existing read state.
+- Opening a thread marks it read automatically; inbound activity while it is open must leave it read.
+- Do not add a new endpoint or a manual read/unread control.
+- Use the existing `PUT /v1/message-threads/{messageThreadID}` endpoint with optional `is_archived` and `is_read`.
+- Use GORM query builders with `WithContext(ctx)`; do not use raw SQL.
+- Wrap API errors with `stacktrace.Propagate`/`PropagateWithCode`.
+- Web code uses single quotes, no semicolons, and 2-space indentation.
+- Every commit must end with:
+
+```text
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+```
+
+- Add end-to-end coverage in the existing `tests/` integration project.
+
+## File Structure
+
+- `api/pkg/entities/message_thread.go`: persisted read fields.
+- `api/pkg/entities/message_thread_test.go`: schema-default regression tests.
+- `api/pkg/repositories/message_thread_repository.go`: field-scoped update parameter types and interface methods.
+- `api/pkg/repositories/gorm_message_thread_repository.go`: transactional activity updates, conditional unread transition, partial user-status update, and partial deleted-message update.
+- `api/pkg/repositories/gorm_message_thread_repository_test.go`: update-map ownership tests.
+- `api/pkg/services/message_thread_service.go`: read-state business rules and repository orchestration.
+- `api/pkg/services/message_thread_service_test.go`: service behavior with a repository stub.
+- `api/pkg/requests/message_thread_update_request.go`: optional archive/read request fields.
+- `api/pkg/requests/message_thread_update_request_test.go`: request-to-service conversion tests.
+- `api/pkg/validators/message_thread_handler_validator.go`: require at least one update field.
+- `api/pkg/validators/message_thread_handler_validator_test.go`: empty-payload and valid-payload tests.
+- `api/pkg/responses/message_thead_responses.go`: single-thread Swagger response.
+- `api/pkg/handlers/message_thread_handler.go`: not-found mapping and corrected Swagger response.
+- `api/pkg/listeners/message_thread_listener.go`: incoming SMS and missed-call unread updates.
+- `api/pkg/listeners/websocket_listener.go`: missed-call Pusher publication.
+- `api/pkg/listeners/read_receipts_test_helpers_test.go`: listener logger and repository test doubles.
+- `api/pkg/listeners/message_thread_listener_test.go`: listener route and payload tests.
+- `api/pkg/listeners/websocket_listener_test.go`: missed-call route registration test.
+- `api/docs/docs.go`, `api/docs/swagger.json`, `api/docs/swagger.yaml`: regenerated API documentation.
+- `web/shared/types/api.ts`: regenerated thread/request types.
+- `web/app/stores/threads.ts`: automatic read update and local state replacement.
+- `web/app/pages/threads/[id]/index.vue`: non-blocking read calls and missed-call realtime refresh.
+- `web/app/components/MessageThread.vue`: unread visual treatment.
+- `tests/read_receipts_test.go`: Docker-stack read/unread lifecycle coverage.
+- `tests/README.md`: integration coverage documentation.
+
+---
+
+### Task 1: Add the read-state API contract
+
+**Files:**
+
+- Modify: `api/pkg/entities/message_thread.go`
+- Create: `api/pkg/entities/message_thread_test.go`
+- Modify: `api/pkg/requests/message_thread_update_request.go`
+- Create: `api/pkg/requests/message_thread_update_request_test.go`
+- Modify: `api/pkg/validators/message_thread_handler_validator.go`
+- Create: `api/pkg/validators/message_thread_handler_validator_test.go`
+- Modify: `api/pkg/responses/message_thead_responses.go`
+- Modify: `api/pkg/services/message_thread_service.go`
+
+**Interfaces:**
+
+- Produces: `MessageThread.IsRead bool`, `MessageThread.LastReadAt time.Time`
+- Produces: `MessageThreadUpdate.IsArchived *bool`, `MessageThreadUpdate.IsRead *bool`
+- Produces: `MessageThreadStatusParams.IsArchived *bool`, `MessageThreadStatusParams.IsRead *bool`
+- Produces: `responses.MessageThreadResponse`
+
+- [ ] **Step 1: Write failing entity schema tests**
+
+Create `api/pkg/entities/message_thread_test.go`:
+
+```go
+package entities
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMessageThreadReadFieldsHaveBackwardCompatibleDefaults(t *testing.T) {
+ threadType := reflect.TypeOf(MessageThread{})
+
+ isRead, ok := threadType.FieldByName("IsRead")
+ require.True(t, ok)
+ assert.Contains(t, isRead.Tag.Get("gorm"), "not null")
+ assert.Contains(t, isRead.Tag.Get("gorm"), "default:true")
+ assert.Equal(t, "is_read", isRead.Tag.Get("json"))
+
+ lastReadAt, ok := threadType.FieldByName("LastReadAt")
+ require.True(t, ok)
+ assert.Contains(t, lastReadAt.Tag.Get("gorm"), "not null")
+ assert.Contains(t, lastReadAt.Tag.Get("gorm"), "default:CURRENT_TIMESTAMP")
+ assert.Equal(t, "-", lastReadAt.Tag.Get("json"))
+}
+```
+
+- [ ] **Step 2: Write failing request and validator tests**
+
+Create `api/pkg/requests/message_thread_update_request_test.go`:
+
+```go
+package requests
+
+import (
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T) {
+ threadID := uuid.New()
+ isRead := true
+ input := MessageThreadUpdate{
+ MessageThreadID: threadID.String(),
+ IsRead: &isRead,
+ }
+
+ params := input.ToUpdateParams(entities.UserID("user-id"))
+
+ assert.Equal(t, threadID, params.MessageThreadID)
+ assert.Equal(t, entities.UserID("user-id"), params.UserID)
+ assert.Nil(t, params.IsArchived)
+ assert.Same(t, &isRead, params.IsRead)
+}
+```
+
+Create `api/pkg/validators/message_thread_handler_validator_test.go`:
+
+```go
+package validators
+
+import (
+ "context"
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/requests"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestValidateUpdateRequiresAtLeastOneStatusField(t *testing.T) {
+ validator := &MessageThreadHandlerValidator{}
+ request := requests.MessageThreadUpdate{
+ MessageThreadID: uuid.NewString(),
+ }
+
+ errors := validator.ValidateUpdate(context.Background(), request)
+
+ assert.NotEmpty(t, errors.Get("payload"))
+}
+
+func TestValidateUpdateAcceptsReadOnlyUpdate(t *testing.T) {
+ validator := &MessageThreadHandlerValidator{}
+ isRead := true
+ request := requests.MessageThreadUpdate{
+ MessageThreadID: uuid.NewString(),
+ IsRead: &isRead,
+ }
+
+ errors := validator.ValidateUpdate(context.Background(), request)
+
+ assert.Empty(t, errors)
+}
+```
+
+- [ ] **Step 3: Run the focused tests and confirm they fail**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api'
+go test ./pkg/entities ./pkg/requests ./pkg/validators
+```
+
+Expected: compile failures because the read fields and optional request fields do not exist.
+
+- [ ] **Step 4: Add the persisted fields and optional request contract**
+
+Add to `entities.MessageThread` after `IsArchived`:
+
+```go
+IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"`
+LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"`
+```
+
+Change `requests.MessageThreadUpdate` and its conversion:
+
+```go
+type MessageThreadUpdate struct {
+ request
+ IsArchived *bool `json:"is_archived,omitempty" example:"true"`
+ IsRead *bool `json:"is_read,omitempty" example:"true"`
+
+ MessageThreadID string `json:"messageThreadID" swaggerignore:"true"`
+}
+
+func (input *MessageThreadUpdate) ToUpdateParams(userID entities.UserID) services.MessageThreadStatusParams {
+ return services.MessageThreadStatusParams{
+ UserID: userID,
+ MessageThreadID: uuid.MustParse(input.MessageThreadID),
+ IsArchived: input.IsArchived,
+ IsRead: input.IsRead,
+ }
+}
+```
+
+Change the service parameter type:
+
+```go
+type MessageThreadStatusParams struct {
+ IsArchived *bool
+ IsRead *bool
+ UserID entities.UserID
+ MessageThreadID uuid.UUID
+}
+```
+
+After `v.ValidateStruct()` in `ValidateUpdate`, add an explicit payload check:
+
+```go
+errors := v.ValidateStruct()
+if request.IsArchived == nil && request.IsRead == nil {
+ errors.Add("payload", "at least one of is_archived or is_read is required")
+}
+return errors
+```
+
+Add a single-thread response beside `MessageThreadsResponse`:
+
+```go
+// MessageThreadResponse is the payload containing entities.MessageThread
+type MessageThreadResponse struct {
+ response
+ Data entities.MessageThread `json:"data"`
+}
+```
+
+- [ ] **Step 5: Run focused tests**
+
+Run:
+
+```powershell
+go test ./pkg/entities ./pkg/requests ./pkg/validators
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit the contract**
+
+```powershell
+git add api/pkg/entities/message_thread.go api/pkg/entities/message_thread_test.go api/pkg/requests/message_thread_update_request.go api/pkg/requests/message_thread_update_request_test.go api/pkg/validators/message_thread_handler_validator.go api/pkg/validators/message_thread_handler_validator_test.go api/pkg/responses/message_thead_responses.go api/pkg/services/message_thread_service.go
+@'
+feat(api): add thread read state contract
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 2: Add atomic thread persistence operations
+
+**Files:**
+
+- Modify: `api/pkg/repositories/message_thread_repository.go`
+- Modify: `api/pkg/repositories/gorm_message_thread_repository.go`
+- Create: `api/pkg/repositories/gorm_message_thread_repository_test.go`
+
+**Interfaces:**
+
+- Produces: `repositories.MessageThreadActivityUpdate`
+- Produces: `repositories.MessageThreadStatusUpdate`
+- Produces: `repositories.MessageThreadDeletedUpdate`
+- Produces: `MessageThreadRepository.UpdateActivity`, `UpdateStatus`, `UpdateAfterDeletedMessage`
+- Removes: `MessageThreadRepository.Update`
+
+- [ ] **Step 1: Write failing update-map ownership tests**
+
+Create `api/pkg/repositories/gorm_message_thread_repository_test.go`:
+
+```go
+package repositories
+
+import (
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) {
+ messageID := uuid.New()
+ updates := messageThreadActivityUpdates(MessageThreadActivityUpdate{
+ Timestamp: time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC),
+ MessageID: messageID,
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ })
+
+ assert.Equal(t, map[string]any{
+ "order_timestamp": time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC),
+ "last_message_id": messageID,
+ "last_message_content": "hello",
+ "status": entities.MessageStatusReceived,
+ }, updates)
+ assert.NotContains(t, updates, "is_read")
+ assert.NotContains(t, updates, "is_archived")
+ assert.NotContains(t, updates, "last_read_at")
+}
+
+func TestMessageThreadStatusUpdatesReadOnly(t *testing.T) {
+ isRead := true
+ readAt := time.Date(2026, 7, 18, 7, 1, 0, 0, time.UTC)
+
+ updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
+ IsRead: &isRead,
+ ReadAt: readAt,
+ })
+
+ assert.Equal(t, map[string]any{
+ "is_read": true,
+ "last_read_at": readAt,
+ }, updates)
+ assert.NotContains(t, updates, "is_archived")
+}
+
+func TestMessageThreadStatusUpdatesArchiveOnly(t *testing.T) {
+ isArchived := true
+
+ updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
+ IsArchived: &isArchived,
+ })
+
+ assert.Equal(t, map[string]any{"is_archived": true}, updates)
+ assert.NotContains(t, updates, "is_read")
+ assert.NotContains(t, updates, "last_read_at")
+}
+```
+
+- [ ] **Step 2: Run the repository test and confirm it fails**
+
+Run:
+
+```powershell
+go test ./pkg/repositories -run MessageThread -v
+```
+
+Expected: compile failures because the parameter types and helper functions do not exist.
+
+- [ ] **Step 3: Define repository update parameters and methods**
+
+In `message_thread_repository.go`, add:
+
+```go
+type MessageThreadActivityUpdate struct {
+ MessageThreadID uuid.UUID
+ UserID entities.UserID
+ Timestamp time.Time
+ MessageID uuid.UUID
+ Content string
+ Status entities.MessageStatus
+ MarksUnread bool
+ EventTimestamp time.Time
+}
+
+type MessageThreadStatusUpdate struct {
+ IsArchived *bool
+ IsRead *bool
+ ReadAt time.Time
+}
+
+type MessageThreadDeletedUpdate struct {
+ MessageThreadID uuid.UUID
+ UserID entities.UserID
+ LastMessageID *uuid.UUID
+ LastMessageContent *string
+ LastMessageStatus entities.MessageStatus
+}
+```
+
+Add the `time` import. Replace `Update` and the old deleted-message method in the interface with:
+
+```go
+UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error
+UpdateStatus(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID, params MessageThreadStatusUpdate) error
+UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error
+```
+
+- [ ] **Step 4: Implement field-scoped GORM updates**
+
+Add these helpers to `gorm_message_thread_repository.go`:
+
+```go
+func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string]any {
+ return map[string]any{
+ "order_timestamp": params.Timestamp,
+ "last_message_id": params.MessageID,
+ "last_message_content": params.Content,
+ "status": params.Status,
+ }
+}
+
+func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any {
+ updates := make(map[string]any)
+ if params.IsArchived != nil {
+ updates["is_archived"] = *params.IsArchived
+ }
+ if params.IsRead != nil {
+ updates["is_read"] = *params.IsRead
+ if *params.IsRead {
+ updates["last_read_at"] = params.ReadAt
+ }
+ }
+ return updates
+}
+```
+
+Replace the full-row `Update` implementation with:
+
+```go
+func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error {
+ ctx, span := repository.tracer.Start(ctx)
+ defer span.End()
+
+ err := repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ query := tx.Model(&entities.MessageThread{}).
+ Where("user_id = ?", params.UserID).
+ Where("id = ?", params.MessageThreadID)
+
+ result := query.Updates(messageThreadActivityUpdates(params))
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected == 0 {
+ return stacktrace.PropagateWithCode(
+ gorm.ErrRecordNotFound,
+ ErrCodeNotFound,
+ fmt.Sprintf("thread with id [%s] not found", params.MessageThreadID),
+ )
+ }
+
+ if !params.MarksUnread {
+ return nil
+ }
+
+ return tx.Model(&entities.MessageThread{}).
+ Where("user_id = ?", params.UserID).
+ Where("id = ?", params.MessageThreadID).
+ Where("last_read_at < ?", params.EventTimestamp).
+ Update("is_read", false).
+ Error
+ })
+ if err != nil {
+ msg := fmt.Sprintf("cannot update message activity for thread [%s]", params.MessageThreadID)
+ return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg))
+ }
+ return nil
+}
+
+func (repository *gormMessageThreadRepository) UpdateStatus(
+ ctx context.Context,
+ userID entities.UserID,
+ messageThreadID uuid.UUID,
+ params MessageThreadStatusUpdate,
+) error {
+ ctx, span := repository.tracer.Start(ctx)
+ defer span.End()
+
+ result := repository.db.WithContext(ctx).
+ Model(&entities.MessageThread{}).
+ Where("user_id = ?", userID).
+ Where("id = ?", messageThreadID).
+ Updates(messageThreadStatusUpdates(params))
+ if result.Error != nil {
+ msg := fmt.Sprintf("cannot update status for thread [%s]", messageThreadID)
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, msg))
+ }
+ if result.RowsAffected == 0 {
+ msg := fmt.Sprintf("thread with id [%s] not found", messageThreadID)
+ return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, msg))
+ }
+ return nil
+}
+
+func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error {
+ ctx, span := repository.tracer.Start(ctx)
+ defer span.End()
+
+ result := repository.db.WithContext(ctx).
+ Model(&entities.MessageThread{}).
+ Where("user_id = ?", params.UserID).
+ Where("id = ?", params.MessageThreadID).
+ Updates(map[string]any{
+ "last_message_id": params.LastMessageID,
+ "last_message_content": params.LastMessageContent,
+ "status": params.LastMessageStatus,
+ })
+ if result.Error != nil {
+ msg := fmt.Sprintf("cannot update deleted-message metadata for thread [%s]", params.MessageThreadID)
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, msg))
+ }
+ return nil
+}
+```
+
+Delete the old `Save(thread)` method and the old unused `UpdateAfterDeletedMessage(userID, messageID)` implementation.
+
+- [ ] **Step 5: Run repository tests**
+
+Run:
+
+```powershell
+go test ./pkg/repositories -run MessageThread -v
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit atomic persistence**
+
+```powershell
+git add api/pkg/repositories/message_thread_repository.go api/pkg/repositories/gorm_message_thread_repository.go api/pkg/repositories/gorm_message_thread_repository_test.go
+@'
+refactor(api): make thread updates atomic
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 3: Apply read rules in services and listeners
+
+**Files:**
+
+- Modify: `api/pkg/services/message_thread_service.go`
+- Create: `api/pkg/services/message_thread_service_test.go`
+- Modify: `api/pkg/listeners/message_thread_listener.go`
+- Create: `api/pkg/listeners/read_receipts_test_helpers_test.go`
+- Create: `api/pkg/listeners/message_thread_listener_test.go`
+- Modify: `api/pkg/listeners/websocket_listener.go`
+- Create: `api/pkg/listeners/websocket_listener_test.go`
+
+**Interfaces:**
+
+- Consumes: repository update types and methods from Task 2.
+- Produces: `MessageThreadUpdateParams.MarksUnread bool`
+- Produces: `MessageThreadUpdateParams.EventTimestamp time.Time`
+- Produces: missed-call thread and websocket listeners.
+
+- [ ] **Step 1: Write failing service behavior tests**
+
+Create `api/pkg/services/message_thread_service_test.go`. Implement a repository stub with all interface methods returning zero values by default and function hooks for the methods under test:
+
+```go
+package services
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/google/uuid"
+ "github.com/palantir/stacktrace"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gorm.io/gorm"
+)
+
+type messageThreadRepositoryStub struct {
+ loadByOwnerContact func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error)
+ load func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error)
+ store func(context.Context, *entities.MessageThread) error
+ updateActivity func(context.Context, repositories.MessageThreadActivityUpdate) error
+ updateStatus func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error
+}
+
+func (stub *messageThreadRepositoryStub) Store(ctx context.Context, thread *entities.MessageThread) error {
+ if stub.store != nil {
+ return stub.store(ctx, thread)
+ }
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateActivity(ctx context.Context, params repositories.MessageThreadActivityUpdate) error {
+ if stub.updateActivity != nil {
+ return stub.updateActivity(ctx, params)
+ }
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateStatus(ctx context.Context, userID entities.UserID, threadID uuid.UUID, params repositories.MessageThreadStatusUpdate) error {
+ if stub.updateStatus != nil {
+ return stub.updateStatus(ctx, userID, threadID, params)
+ }
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error {
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) {
+ return stub.loadByOwnerContact(ctx, userID, owner, contact)
+}
+
+func (stub *messageThreadRepositoryStub) Load(ctx context.Context, userID entities.UserID, id uuid.UUID) (*entities.MessageThread, error) {
+ return stub.load(ctx, userID, id)
+}
+
+func (stub *messageThreadRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) {
+ threads := []entities.MessageThread{}
+ return &threads, nil
+}
+
+func (stub *messageThreadRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error {
+ return nil
+}
+
+func (stub *messageThreadRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error {
+ return nil
+}
+
+func newMessageThreadServiceForTest(repository repositories.MessageThreadRepository) *MessageThreadService {
+ logger := &noopLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ return NewMessageThreadService(logger, tracer, repository, nil)
+}
+```
+
+Add these tests below the stub:
+
+```go
+func TestUpdateThreadPassesUnreadWatermarkForInboundActivity(t *testing.T) {
+ threadID := uuid.New()
+ eventTimestamp := time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)
+ var captured repositories.MessageThreadActivityUpdate
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: threadID}, nil
+ },
+ updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ captured = params
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ Timestamp: eventTimestamp,
+ MarksUnread: true,
+ EventTimestamp: eventTimestamp,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, captured.MarksUnread)
+ assert.Equal(t, eventTimestamp, captured.EventTimestamp)
+}
+
+func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) {
+ var captured repositories.MessageThreadActivityUpdate
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: uuid.New(), IsRead: false}, nil
+ },
+ updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ captured = params
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "outbound",
+ Status: entities.MessageStatusSent,
+ Timestamp: time.Now().UTC(),
+ })
+
+ require.NoError(t, err)
+ assert.False(t, captured.MarksUnread)
+}
+
+func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) {
+ tests := []struct {
+ name string
+ marksUnread bool
+ wantRead bool
+ }{
+ {name: "inbound", marksUnread: true, wantRead: false},
+ {name: "outbound", marksUnread: false, wantRead: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var stored *entities.MessageThread
+ repository := &messageThreadRepositoryStub{
+ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ },
+ store: func(_ context.Context, thread *entities.MessageThread) error {
+ stored = thread
+ return nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ MessageID: uuid.New(),
+ Content: "hello",
+ Status: entities.MessageStatusReceived,
+ Timestamp: time.Now().UTC(),
+ MarksUnread: test.marksUnread,
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, stored)
+ assert.Equal(t, test.wantRead, stored.IsRead)
+ assert.False(t, stored.LastReadAt.IsZero())
+ })
+ }
+}
+
+func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) {
+ threadID := uuid.New()
+ isRead := true
+ var captured repositories.MessageThreadStatusUpdate
+ repository := &messageThreadRepositoryStub{
+ updateStatus: func(_ context.Context, _ entities.UserID, _ uuid.UUID, params repositories.MessageThreadStatusUpdate) error {
+ captured = params
+ return nil
+ },
+ load: func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: threadID, IsArchived: true, IsRead: true}, nil
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ thread, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
+ UserID: entities.UserID("user-id"),
+ MessageThreadID: threadID,
+ IsRead: &isRead,
+ })
+
+ require.NoError(t, err)
+ assert.Nil(t, captured.IsArchived)
+ assert.Same(t, &isRead, captured.IsRead)
+ assert.False(t, captured.ReadAt.IsZero())
+ assert.True(t, thread.IsArchived)
+}
+
+func TestUpdateStatusPreservesNotFoundCode(t *testing.T) {
+ repository := &messageThreadRepositoryStub{
+ updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error {
+ return stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ },
+ }
+
+ service := newMessageThreadServiceForTest(repository)
+ isRead := true
+ _, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
+ UserID: entities.UserID("user-id"),
+ MessageThreadID: uuid.New(),
+ IsRead: &isRead,
+ })
+
+ assert.Equal(t, repositories.ErrCodeNotFound, stacktrace.GetCode(err))
+}
+```
+
+- [ ] **Step 2: Run service tests and confirm they fail**
+
+Run:
+
+```powershell
+go test ./pkg/services -run 'Test(UpdateThread|CreateThread|UpdateStatus)' -v
+```
+
+Expected: compile failures because the new update parameters and repository calls are not wired.
+
+- [ ] **Step 3: Implement service read rules**
+
+Extend `MessageThreadUpdateParams`:
+
+```go
+MarksUnread bool
+EventTimestamp time.Time
+```
+
+Replace the existing full-row update in `UpdateThread` with:
+
+```go
+if err = service.repository.UpdateActivity(ctx, repositories.MessageThreadActivityUpdate{
+ MessageThreadID: thread.ID,
+ UserID: params.UserID,
+ Timestamp: params.Timestamp,
+ MessageID: params.MessageID,
+ Content: params.Content,
+ Status: params.Status,
+ MarksUnread: params.MarksUnread,
+ EventTimestamp: params.EventTimestamp,
+}); err != nil {
+ msg := fmt.Sprintf("cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg))
+}
+```
+
+In `createThread`, create one `now := time.Now().UTC()` and initialize:
+
+```go
+IsRead: !params.MarksUnread,
+LastReadAt: now,
+CreatedAt: now,
+UpdatedAt: now,
+```
+
+Replace `UpdateStatus` with a partial repository update followed by a reload:
+
+```go
+func (service *MessageThreadService) UpdateStatus(ctx context.Context, params MessageThreadStatusParams) (*entities.MessageThread, error) {
+ ctx, span := service.tracer.Start(ctx)
+ defer span.End()
+
+ update := repositories.MessageThreadStatusUpdate{
+ IsArchived: params.IsArchived,
+ IsRead: params.IsRead,
+ ReadAt: time.Now().UTC(),
+ }
+ if err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update); err != nil {
+ msg := fmt.Sprintf("cannot update message thread with id [%s]", params.MessageThreadID)
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg))
+ }
+
+ thread, err := service.repository.Load(ctx, params.UserID, params.MessageThreadID)
+ if err != nil {
+ msg := fmt.Sprintf("cannot reload message thread with id [%s]", params.MessageThreadID)
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg))
+ }
+ return thread, nil
+}
+```
+
+In `UpdateAfterDeletedMessage`, replace the full-row update with:
+
+```go
+if err = service.repository.UpdateAfterDeletedMessage(ctx, repositories.MessageThreadDeletedUpdate{
+ MessageThreadID: thread.ID,
+ UserID: thread.UserID,
+ LastMessageID: payload.PreviousMessageID,
+ LastMessageContent: payload.PreviousMessageContent,
+ LastMessageStatus: *payload.PreviousMessageStatus,
+}); err != nil {
+ msg := fmt.Sprintf("cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
+}
+```
+
+- [ ] **Step 4: Add inbound and missed-call listener behavior**
+
+In `NewMessageThreadListener`, register:
+
+```go
+events.MessageCallMissed: l.OnMessageCallMissed,
+```
+
+In `OnMessagePhoneReceived`, add:
+
+```go
+MarksUnread: true,
+EventTimestamp: event.Time(),
+```
+
+Add:
+
+```go
+func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
+ ctx, span := listener.tracer.Start(ctx)
+ defer span.End()
+
+ var payload events.MessageCallMissedPayload
+ 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))
+ }
+
+ params := services.MessageThreadUpdateParams{
+ Owner: payload.Owner,
+ Contact: payload.Contact,
+ UserID: payload.UserID,
+ Status: entities.MessageStatusReceived,
+ Timestamp: payload.Timestamp,
+ Content: "Missed phone call",
+ MessageID: payload.MessageID,
+ MarksUnread: true,
+ EventTimestamp: event.Time(),
+ }
+ if err := listener.service.UpdateThread(ctx, params); err != nil {
+ msg := fmt.Sprintf("cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID())
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
+ }
+ return nil
+}
+```
+
+In `NewWebsocketListener`, register:
+
+```go
+events.MessageCallMissed: l.onMessageCallMissed,
+```
+
+Add:
+
+```go
+func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error {
+ ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger)
+ defer span.End()
+
+ var payload events.MessageCallMissedPayload
+ 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.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
+ msg := fmt.Sprintf("cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
+ }
+ return nil
+}
+```
+
+- [ ] **Step 5: Add listener tests**
+
+Create `api/pkg/listeners/read_receipts_test_helpers_test.go`:
+
+```go
+package listeners
+
+import (
+ "context"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/google/uuid"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type noopListenerLogger struct{}
+
+func (logger *noopListenerLogger) Error(error) {}
+func (logger *noopListenerLogger) WithService(string) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) WithString(string, string) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger }
+func (logger *noopListenerLogger) Trace(string) {}
+func (logger *noopListenerLogger) Info(string) {}
+func (logger *noopListenerLogger) Warn(error) {}
+func (logger *noopListenerLogger) Debug(string) {}
+func (logger *noopListenerLogger) Fatal(error) {}
+func (logger *noopListenerLogger) Printf(string, ...interface{}) {}
+
+type listenerMessageThreadRepository struct {
+ activity repositories.MessageThreadActivityUpdate
+}
+
+func (repository *listenerMessageThreadRepository) Store(context.Context, *entities.MessageThread) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateActivity(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
+ repository.activity = params
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
+ return &entities.MessageThread{ID: uuid.New()}, nil
+}
+
+func (repository *listenerMessageThreadRepository) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) {
+ return &entities.MessageThread{}, nil
+}
+
+func (repository *listenerMessageThreadRepository) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) {
+ threads := []entities.MessageThread{}
+ return &threads, nil
+}
+
+func (repository *listenerMessageThreadRepository) Delete(context.Context, entities.UserID, uuid.UUID) error {
+ return nil
+}
+
+func (repository *listenerMessageThreadRepository) DeleteAllForUser(context.Context, entities.UserID) error {
+ return nil
+}
+```
+
+Create `api/pkg/listeners/message_thread_listener_test.go`:
+
+```go
+package listeners
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/events"
+ "github.com/NdoleStudio/httpsms/pkg/services"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ cloudevents "github.com/cloudevents/sdk-go/v2"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMessageThreadListenerMarksMissedCallUnread(t *testing.T) {
+ repository := &listenerMessageThreadRepository{}
+ logger := &noopListenerLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ service := services.NewMessageThreadService(logger, tracer, repository, nil)
+ _, routes := NewMessageThreadListener(logger, tracer, service)
+
+ event := cloudevents.NewEvent()
+ event.SetID(uuid.NewString())
+ event.SetSource("/v1/messages/call-missed")
+ event.SetType(events.MessageCallMissed)
+ event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC))
+ require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessageCallMissedPayload{
+ MessageID: uuid.New(),
+ UserID: entities.UserID("user-id"),
+ Owner: "+18005550199",
+ Contact: "+18005550100",
+ Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC),
+ }))
+
+ err := routes[events.MessageCallMissed](context.Background(), event)
+
+ require.NoError(t, err)
+ assert.True(t, repository.activity.MarksUnread)
+ assert.Equal(t, "Missed phone call", repository.activity.Content)
+ assert.Equal(t, event.Time(), repository.activity.EventTimestamp)
+}
+```
+
+Create `api/pkg/listeners/websocket_listener_test.go`:
+
+```go
+package listeners
+
+import (
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/events"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/pusher/pusher-http-go/v5"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWebsocketListenerRegistersMissedCalls(t *testing.T) {
+ logger := &noopListenerLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ _, routes := NewWebsocketListener(logger, tracer, &pusher.Client{})
+
+ assert.Contains(t, routes, events.MessageCallMissed)
+}
+```
+
+- [ ] **Step 6: Run service and listener tests**
+
+Run:
+
+```powershell
+go test ./pkg/services ./pkg/listeners -run 'MessageThread|MissedCall|UpdateStatus|CreateThread' -v
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit service and realtime behavior**
+
+```powershell
+git add api/pkg/services/message_thread_service.go api/pkg/services/message_thread_service_test.go api/pkg/listeners/message_thread_listener.go api/pkg/listeners/read_receipts_test_helpers_test.go api/pkg/listeners/message_thread_listener_test.go api/pkg/listeners/websocket_listener.go api/pkg/listeners/websocket_listener_test.go
+@'
+feat(api): track unread inbound threads
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 4: Finish the HTTP behavior and regenerate Swagger
+
+**Files:**
+
+- Modify: `api/pkg/handlers/message_thread_handler.go`
+- Modify: `api/docs/docs.go`
+- Modify: `api/docs/swagger.json`
+- Modify: `api/docs/swagger.yaml`
+
+**Interfaces:**
+
+- Consumes: `responses.MessageThreadResponse` from Task 1.
+- Produces: 404 behavior for missing thread updates.
+- Produces: Swagger schema fields `is_read`, optional `is_archived`, optional `is_read`.
+
+- [ ] **Step 1: Update the handler error mapping and annotations**
+
+Change the update annotation:
+
+```go
+// @Success 200 {object} responses.MessageThreadResponse
+// @Failure 404 {object} responses.NotFound
+```
+
+Change the error block:
+
+```go
+thread, err := h.service.UpdateStatus(ctx, request.ToUpdateParams(h.userIDFomContext(c)))
+if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
+ return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID))
+}
+if err != nil {
+ msg := fmt.Sprintf("cannot update message thread with params [%+#v]", request)
+ ctxLogger.Error(stacktrace.Propagate(err, msg))
+ return h.responseInternalServerError(c)
+}
+```
+
+- [ ] **Step 2: Regenerate Swagger**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api'
+swag init --requiredByDefault --parseDependency --parseInternal
+```
+
+Expected: `api/docs/docs.go`, `api/docs/swagger.json`, and `api/docs/swagger.yaml` update successfully.
+
+- [ ] **Step 3: Run the full API suite**
+
+Run:
+
+```powershell
+go test -vet=off ./pkg/entities ./pkg/repositories ./pkg/requests ./pkg/services ./pkg/validators ./pkg/listeners
+go test -vet=off ./pkg/handlers -run '^$'
+```
+
+Expected: PASS. Vet is disabled because clean `origin/main` has unrelated Go
+1.25 non-constant-format vet failures. Handler tests are compile-checked only
+because the pre-existing suite expects an API server on `localhost:8000`; Task
+7 provides the HTTP integration coverage against the Docker stack.
+
+- [ ] **Step 4: Commit HTTP and documentation changes**
+
+```powershell
+git add api/pkg/handlers/message_thread_handler.go api/docs/docs.go api/docs/swagger.json api/docs/swagger.yaml
+@'
+docs(api): publish thread read state
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 5: Add automatic read updates to the web store
+
+**Files:**
+
+- Modify: `web/shared/types/api.ts`
+- Modify: `web/app/stores/threads.ts`
+
+**Interfaces:**
+
+- Consumes: API `PUT /v1/message-threads/{id}` with `{ is_read: true }`.
+- Produces: `markThreadRead(threadId: string, force?: boolean): Promise`.
+
+- [ ] **Step 1: Regenerate TypeScript API models**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web'
+pnpm api:models
+```
+
+Expected: `EntitiesMessageThread.is_read: boolean` and optional fields on `RequestsMessageThreadUpdate`.
+
+- [ ] **Step 2: Add a typed local replacement helper**
+
+In `web/app/stores/threads.ts`, add:
+
+```ts
+function replaceThread(updatedThread: EntitiesMessageThread) {
+ const index = threads.value.findIndex(
+ (thread) => thread.id === updatedThread.id,
+ );
+ if (index !== -1) threads.value[index] = updatedThread;
+}
+```
+
+- [ ] **Step 3: Implement non-silent automatic read persistence**
+
+Add:
+
+```ts
+async function markThreadRead(threadId: string, force = false) {
+ const thread = threads.value.find((item) => item.id === threadId);
+ if (!thread) throw new Error(`Cannot find thread with id ${threadId}`);
+ if (!force && thread.is_read) return;
+
+ try {
+ const response = await apiFetch<{ data: EntitiesMessageThread }>(
+ `/v1/message-threads/${threadId}`,
+ {
+ method: "PUT",
+ body: { is_read: true },
+ },
+ );
+ replaceThread(response.data);
+ } catch (error) {
+ notificationsStore.addNotification({
+ message: "The message thread could not be marked as read",
+ type: "error",
+ });
+ await loadThreads();
+ throw error;
+ }
+}
+```
+
+Add `markThreadRead` to the returned store API.
+
+Keep archive updates independent:
+
+```ts
+body: { is_archived: payload.isArchived },
+```
+
+Do not send `is_read` from `updateThread`.
+
+- [ ] **Step 4: Run web static checks**
+
+Run:
+
+```powershell
+pnpm lint:js
+pnpm lint:prettier
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit store and generated types**
+
+```powershell
+git add web/shared/types/api.ts web/app/stores/threads.ts
+@'
+feat(web): persist opened threads as read
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 6: Mark opened threads read and highlight unread threads
+
+**Files:**
+
+- Modify: `web/app/pages/threads/[id]/index.vue`
+- Modify: `web/app/components/MessageThread.vue`
+
+**Interfaces:**
+
+- Consumes: `threadsStore.markThreadRead(threadId, force)`.
+- Produces: automatic initial and realtime read updates.
+- Produces: shared unread list styling on mobile and desktop.
+
+- [ ] **Step 1: Make read updates independent from message loading**
+
+In `web/app/pages/threads/[id]/index.vue`, add:
+
+```ts
+async function markCurrentThreadRead(force = false) {
+ const threadId = route.params.id as string;
+ try {
+ await threadsStore.markThreadRead(threadId, force);
+ } catch (error) {
+ console.error(error);
+ }
+}
+```
+
+At the start of `loadMessages`, after computing `threadId`, call without awaiting:
+
+```ts
+void markCurrentThreadRead();
+```
+
+This lets message loading continue even if the read update fails.
+
+- [ ] **Step 2: Force a newer read watermark after inbound realtime events**
+
+Change the incoming SMS binding:
+
+```ts
+webhookChannel.bind("message.phone.received", () => {
+ if (!loadingMessages.value) {
+ void markCurrentThreadRead(true);
+ loadMessages(false);
+ }
+});
+```
+
+Add the missed-call binding with the same behavior:
+
+```ts
+webhookChannel.bind("message.call.missed", () => {
+ if (!loadingMessages.value) {
+ void markCurrentThreadRead(true);
+ loadMessages(false);
+ }
+});
+```
+
+The forced call is required because the local thread may still say `is_read:
+true` when the concurrent backend listener has not committed its unread update.
+
+- [ ] **Step 3: Add unread presentation**
+
+In `MessageThread.vue`, add the class binding to `v-list-item`:
+
+```vue
+:class="{ 'message-thread--unread': !thread.is_read }"
+```
+
+Change title and subtitle class bindings:
+
+```vue
+
+ {{ formatPhoneNumber(thread.contact) }}
+
+
+```
+
+Add:
+
+```vue
+
+```
+
+- [ ] **Step 4: Run web validation**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web'
+pnpm lint
+pnpm run generate
+```
+
+Expected: both commands PASS.
+
+- [ ] **Step 5: Commit UI behavior**
+
+```powershell
+git add web/app/pages/threads/[id]/index.vue web/app/components/MessageThread.vue
+@'
+feat(web): highlight unread message threads
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 7: Add integration coverage
+
+**Files:**
+
+- Create: `tests/read_receipts_test.go`
+- Modify: `tests/README.md`
+
+**Interfaces:**
+
+- Consumes: thread index and update HTTP endpoints from Tasks 1-4.
+- Consumes: incoming SMS, missed-call, and outbound message flows from Task 3.
+- Produces: `TestMessageThreadReadReceipts`.
+
+- [ ] **Step 1: Write the end-to-end read-receipts test**
+
+Create `tests/read_receipts_test.go`:
+
+```go
+package tests
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ httpsms "github.com/NdoleStudio/httpsms-go"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type integrationMessageThread struct {
+ ID string `json:"id"`
+ Contact string `json:"contact"`
+ IsRead bool `json:"is_read"`
+ LastMessageContent *string `json:"last_message_content"`
+}
+
+func requestJSON(
+ ctx context.Context,
+ t *testing.T,
+ method string,
+ path string,
+ apiKey string,
+ payload any,
+ expectedStatus int,
+ output any,
+) {
+ t.Helper()
+
+ var body io.Reader
+ if payload != nil {
+ encoded, err := json.Marshal(payload)
+ require.NoError(t, err)
+ body = bytes.NewReader(encoded)
+ }
+
+ request, err := http.NewRequestWithContext(ctx, method, apiBaseURL+path, body)
+ require.NoError(t, err)
+ request.Header.Set("x-api-key", apiKey)
+ request.Header.Set("Content-Type", "application/json")
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+ defer response.Body.Close()
+
+ responseBody, err := io.ReadAll(response.Body)
+ require.NoError(t, err)
+ require.Equal(t, expectedStatus, response.StatusCode, "response: %s", string(responseBody))
+
+ if output != nil {
+ require.NoError(t, json.Unmarshal(responseBody, output))
+ }
+}
+
+func fetchMessageThreads(ctx context.Context, t *testing.T, owner string) []integrationMessageThread {
+ t.Helper()
+
+ var response struct {
+ Data []integrationMessageThread `json:"data"`
+ }
+ path := fmt.Sprintf(
+ "/v1/message-threads?owner=%s&skip=0&limit=20&is_archived=false",
+ url.QueryEscape(owner),
+ )
+ requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response)
+ return response.Data
+}
+
+func waitForMessageThread(
+ ctx context.Context,
+ t *testing.T,
+ owner string,
+ contact string,
+ timeout time.Duration,
+ matches func(integrationMessageThread) bool,
+) integrationMessageThread {
+ t.Helper()
+
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ for _, thread := range fetchMessageThreads(ctx, t, owner) {
+ if thread.Contact == contact && matches(thread) {
+ return thread
+ }
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ t.Fatalf("thread %s -> %s did not reach the expected state within %v", owner, contact, timeout)
+ return integrationMessageThread{}
+}
+
+func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) integrationMessageThread {
+ t.Helper()
+
+ var response struct {
+ Data integrationMessageThread `json:"data"`
+ }
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPut,
+ "/v1/message-threads/"+threadID,
+ userAPIKey,
+ map[string]any{"is_read": true},
+ http.StatusOK,
+ &response,
+ )
+ return response.Data
+}
+
+func TestMessageThreadReadReceipts(t *testing.T) {
+ ctx := context.Background()
+ phone := setupPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPost,
+ "/v1/messages/receive",
+ phone.PhoneAPIKey,
+ map[string]any{
+ "from": contact,
+ "to": phone.PhoneNumber,
+ "content": "Unread inbound message",
+ "encrypted": false,
+ "sim": "SIM1",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ },
+ http.StatusOK,
+ nil,
+ )
+
+ thread := waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return !thread.IsRead
+ })
+ assert.False(t, thread.IsRead)
+
+ updated := markMessageThreadRead(ctx, t, thread.ID)
+ assert.True(t, updated.IsRead)
+ waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 10*time.Second, func(thread integrationMessageThread) bool {
+ return thread.IsRead
+ })
+
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPost,
+ "/v1/messages/calls/missed",
+ phone.PhoneAPIKey,
+ map[string]any{
+ "from": contact,
+ "to": phone.PhoneNumber,
+ "sim": "SIM1",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ },
+ http.StatusOK,
+ nil,
+ )
+
+ thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return !thread.IsRead &&
+ thread.LastMessageContent != nil &&
+ *thread.LastMessageContent == "Missed phone call"
+ })
+ assert.False(t, thread.IsRead)
+
+ outboundContent := "Outbound activity preserves unread"
+ client := newAPIClient()
+ _, response, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{
+ From: phone.PhoneNumber,
+ To: contact,
+ Content: outboundContent,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
+
+ thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return thread.LastMessageContent != nil &&
+ *thread.LastMessageContent == outboundContent
+ })
+ assert.False(t, thread.IsRead, "outbound activity must not clear unread state")
+}
+```
+
+- [ ] **Step 2: Run the test before implementation is complete**
+
+With the Docker integration stack running, execute:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\tests'
+go test -v -timeout 120s -run TestMessageThreadReadReceipts ./...
+```
+
+Expected before the feature implementation: FAIL because thread responses do not expose/persist `is_read`.
+
+- [ ] **Step 3: Update integration-test documentation**
+
+Add to the Test Coverage checklist in `tests/README.md`:
+
+```markdown
+- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state
+```
+
+- [ ] **Step 4: Run the integration test against the completed stack**
+
+Generate credentials, start the Docker stack, run the focused test, and tear down:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\tests'
+& 'C:\Program Files\Git\bin\bash.exe' generate-firebase-credentials.sh
+$env:FIREBASE_CREDENTIALS = (Get-Content firebase-credentials.json -Raw | ConvertFrom-Json | ConvertTo-Json -Compress)
+docker compose up -d --build --wait
+docker compose wait seed
+Start-Sleep -Seconds 2
+try {
+ go test -v -timeout 120s -run TestMessageThreadReadReceipts ./...
+} finally {
+ docker compose down -v
+}
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit integration coverage**
+
+```powershell
+git add tests/read_receipts_test.go tests/README.md
+@'
+test: cover message thread read receipts
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
+
+### Task 8: Verify the complete feature
+
+**Files:**
+
+- Review: all files changed since `origin/main`
+
+**Interfaces:**
+
+- Consumes: all previous tasks.
+- Produces: a clean, verified feature branch.
+
+- [ ] **Step 1: Run API formatting and tests**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api'
+go-fumpt -w pkg/entities/message_thread.go pkg/entities/message_thread_test.go pkg/repositories/message_thread_repository.go pkg/repositories/gorm_message_thread_repository.go pkg/repositories/gorm_message_thread_repository_test.go pkg/services/message_thread_service.go pkg/services/message_thread_service_test.go pkg/requests/message_thread_update_request.go pkg/requests/message_thread_update_request_test.go pkg/validators/message_thread_handler_validator.go pkg/validators/message_thread_handler_validator_test.go pkg/responses/message_thead_responses.go pkg/handlers/message_thread_handler.go pkg/listeners/message_thread_listener.go pkg/listeners/read_receipts_test_helpers_test.go pkg/listeners/message_thread_listener_test.go pkg/listeners/websocket_listener.go pkg/listeners/websocket_listener_test.go
+go test -vet=off ./pkg/entities ./pkg/repositories ./pkg/requests ./pkg/services ./pkg/validators ./pkg/listeners
+go test -vet=off ./pkg/handlers -run '^$'
+```
+
+Expected: formatting completes and all API tests PASS.
+
+- [ ] **Step 2: Run complete web validation**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web'
+pnpm lint
+pnpm run generate
+```
+
+Expected: PASS.
+
+- [ ] **Step 3: Inspect the final diff**
+
+Run:
+
+```powershell
+Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts'
+git --no-pager diff --check origin/main...HEAD
+git --no-pager diff --stat origin/main...HEAD
+git --no-pager status --short --branch
+```
+
+Expected: no whitespace errors and no uncommitted implementation changes.
+
+- [ ] **Step 4: Perform manual acceptance checks against a migrated local database**
+
+Verify:
+
+1. Existing rows return `"is_read": true`.
+2. A closed thread becomes highlighted after an incoming SMS.
+3. A closed thread becomes highlighted after a missed call.
+4. Opening the thread removes the highlight and refresh keeps it read.
+5. Incoming activity while the thread is open does not leave it unread.
+6. Sending and delivery-status changes do not clear an unread thread.
+7. Archiving and unarchiving preserve read state.
+
+- [ ] **Step 5: Commit any formatting-only corrections**
+
+Skip this step when Step 1 produced no changes. Otherwise:
+
+```powershell
+git add api web
+@'
+style: format read receipts changes
+
+Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
+Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229
+'@ | git commit -F -
+```
diff --git a/docs/superpowers/specs/2026-07-18-read-receipts-design.md b/docs/superpowers/specs/2026-07-18-read-receipts-design.md
new file mode 100644
index 000000000..6462cbaa5
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-18-read-receipts-design.md
@@ -0,0 +1,286 @@
+# Message Thread Read Receipts
+
+- Date: 2026-07-18
+- Status: Approved (design)
+- Scope: `api/` Go backend and `web/` Nuxt frontend. Android is unchanged.
+- Branch: `feat/read-receipts`, based on `origin/main`
+
+## Problem
+
+The message-thread list does not distinguish conversations with unseen inbound
+activity from conversations the user has already opened. Users need unread
+threads to stand out and become read automatically when opened.
+
+The change must be backward compatible: every thread that exists when the new
+schema is deployed starts as read.
+
+## Decisions
+
+- Unread state is stored per message thread.
+- Existing threads are read after migration.
+- A new incoming SMS or missed call marks its thread unread.
+- Outbound messages and later delivery/status updates do not change read state.
+- Opening a thread in the web UI marks it read automatically.
+- A thread that receives inbound activity while already open remains read.
+- There is no manual "Mark as read/unread" UI.
+- Unread threads use bold contact/preview text and a subtle primary-tinted
+ background.
+- The existing `PUT /v1/message-threads/{messageThreadID}` endpoint handles the
+ read update; no new endpoint is added.
+
+## Design
+
+### 1. Thread persistence
+
+Add these fields to `api/pkg/entities/message_thread.go`:
+
+```go
+IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"`
+LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"`
+```
+
+`IsRead` is the public binary state used by API clients and the web UI.
+`LastReadAt` is internal ordering metadata used to make concurrent event
+processing deterministic.
+
+GORM `AutoMigrate` adds both columns. The database defaults make all existing
+rows read and give them a migration-time read watermark. New-thread creation
+sets `IsRead` explicitly according to whether the triggering activity is
+inbound.
+
+Existing-row updates must not use the repository's current full-row `Save`
+method. A stale entity loaded by one event handler could otherwise overwrite a
+newer read action from the web request.
+
+### 2. Atomic repository updates
+
+Add field-scoped repository operations for the two update paths:
+
+- **Message activity update:** update only `order_timestamp`,
+ `last_message_id`, `last_message_content`, and `status`. For inbound activity,
+ conditionally set `is_read = false` only where the stored `last_read_at` is
+ older than the CloudEvent creation time.
+- **User status update:** update only the supplied `is_archived` and `is_read`
+ fields. Marking read writes `is_read = true` and `last_read_at = now` in the
+ same database update.
+
+The message activity update runs its metadata and conditional unread statements
+inside one GORM transaction. Both operations remain scoped by `user_id` and
+thread ID and use GORM's query builder with context propagation.
+
+After a user status update, reload the thread for the API response. Do not use
+`Save` in either path, because it writes unrelated columns and reintroduces lost
+updates.
+
+The stored timestamp condition prevents this race:
+
+1. An inbound event is created.
+2. The websocket listener notifies the open web page.
+3. The web page marks the thread read.
+4. The message-thread listener finishes later.
+
+Because CloudEvent creation time precedes the read request, the late listener
+checks the newer database watermark and does not overwrite the read action. If
+the message listener commits first, the later read update wins normally.
+
+### 3. Thread update parameters
+
+Extend `services.MessageThreadUpdateParams` with:
+
+```go
+MarksUnread bool
+EventTimestamp time.Time
+```
+
+Inbound SMS and missed-call listeners set `MarksUnread = true` and use
+`event.Time()` as `EventTimestamp`. Other message events leave `MarksUnread`
+false, so sending, scheduling, delivery, failure, and expiry updates preserve
+the current read state.
+
+For an existing thread, the service delegates the field-scoped message update
+and optional conditional unread transition to the repository.
+
+For a newly created thread:
+
+- inbound SMS or missed call: `IsRead = false`;
+- outbound activity: `IsRead = true`.
+
+### 4. Missed-call thread updates
+
+`MessageThreadListener` currently handles received SMS events but not
+`message.call.missed`. Register the missed-call event and update/create the
+corresponding thread with:
+
+- owner and contact from `MessageCallMissedPayload`;
+- message ID and event timestamp;
+- received status;
+- a concise preview such as `Missed phone call`;
+- `MarksUnread = true`.
+
+This ensures a missed call is visible as thread activity even when no auto-reply
+is configured.
+
+### 5. Existing update endpoint
+
+Change `requests.MessageThreadUpdate` to use optional fields:
+
+```go
+IsArchived *bool `json:"is_archived,omitempty" example:"true"`
+IsRead *bool `json:"is_read,omitempty" example:"true"`
+```
+
+The validator requires at least one supported field. Pointer fields distinguish
+"not supplied" from `false`, preventing a read-only update from unarchiving a
+thread and an archive-only update from changing read state.
+
+`MessageThreadStatusParams` receives the optional fields. `UpdateStatus`:
+
+- loads the authenticated user's thread;
+- builds an update map containing only supplied fields;
+- writes `last_read_at = time.Now().UTC()` alongside `is_read = true`;
+- supports `IsRead = false` at the API level without adding a web control;
+- persists one combined partial update and reloads the updated thread.
+
+The handler preserves repository error codes so a missing thread returns 404.
+Invalid IDs or empty/unsupported update bodies return validation errors.
+
+### 6. Web store behavior
+
+Regenerate the API model so `EntitiesMessageThread` includes `is_read` and
+`RequestsMessageThreadUpdate` has optional `is_archived` and `is_read`.
+
+In `web/app/stores/threads.ts`:
+
+- keep archive updates on the existing endpoint with
+ `{ is_archived: value }`;
+- add `markThreadRead(threadId)` using `{ is_read: true }`;
+- replace the matching thread in local state with the successful API response;
+- make the operation idempotent and skip the request when the local thread is
+ already read unless it is called after an inbound realtime event.
+
+Opening a thread invokes `markThreadRead` automatically as part of the thread
+loading flow. Message fetching remains independent: if the read update fails,
+messages still display, the thread remains/reloads as unread, and the existing
+notification system reports the failure.
+
+### 7. Realtime behavior
+
+The message detail page already reloads messages for incoming SMS websocket
+events. That reload also marks the currently selected thread read.
+
+Add `message.call.missed` to:
+
+- `WebsocketListener` registrations in the API;
+- the Pusher bindings on the thread detail page.
+
+The websocket payload can remain the event ID. The selected thread is marked
+read idempotently after any inbound realtime refresh; a different thread's
+backend state remains unread.
+
+### 8. Thread-list presentation
+
+In `web/app/components/MessageThread.vue`, unread list items receive:
+
+- bold contact title;
+- bold message preview;
+- a subtle background using the Vuetify primary theme color at low opacity.
+
+Read threads retain the current appearance. Active-route styling remains
+visible and takes precedence where Vuetify applies it.
+
+The highlight is applied in the shared thread-list component, so it works in
+both the mobile thread page and desktop navigation drawer, including archived
+threads.
+
+### 9. Swagger and generated types
+
+After changing the entity and request annotations:
+
+1. Run `swag init --requiredByDefault --parseDependency --parseInternal` in
+ `api/`.
+2. Run `pnpm api:models` in `web/`.
+
+Generated Swagger files and `web/shared/types/api.ts` are committed with the
+implementation.
+
+## Error Handling
+
+- Repository and service errors continue to use `stacktrace.Propagate`.
+- The update handler returns 404 for a thread that does not belong to the
+ authenticated user.
+- A failed automatic read update is not swallowed and does not block message
+ display.
+- Local UI state changes only from a successful response or a subsequent thread
+ reload; the UI does not claim a persisted read state after an API failure.
+- Realtime callbacks report failures through the existing notification/error
+ path rather than using empty catches.
+
+## Testing
+
+### API
+
+Add targeted tests for:
+
+- entity schema defaults and new-thread read-state initialization;
+- an inbound SMS making an existing thread unread;
+- a missed call creating or updating an unread thread;
+- outbound and delivery/status events preserving read state;
+- a read action winning when its timestamp is newer than an inbound event;
+- concurrent message/read updates changing only their owned columns;
+- archive-only updates preserving read state;
+- read-only updates preserving archive state;
+- missing threads preserving the not-found error code;
+- existing-row migration defaults (`is_read = true`).
+
+Run:
+
+```bash
+cd api
+go test ./...
+```
+
+### Web
+
+No frontend unit-test runner is configured. Validate the generated types,
+Pinia/Vue changes, styles, and production output with:
+
+```bash
+cd web
+pnpm lint
+pnpm run generate
+```
+
+### Integration
+
+Add `tests/read_receipts_test.go` to exercise the feature against the Docker
+integration stack:
+
+- an inbound SMS creates or updates an unread thread;
+- marking the thread read through the existing update endpoint persists;
+- a missed call makes the thread unread again;
+- subsequent outbound activity does not clear that unread state.
+
+Update `tests/README.md` coverage and run:
+
+```bash
+cd tests
+go test -v -timeout 120s -run TestMessageThreadReadReceipts ./...
+```
+
+Manual acceptance checks:
+
+1. Existing threads appear read immediately after migration.
+2. A new incoming SMS highlights its closed thread.
+3. A missed call highlights its closed thread.
+4. Opening an unread thread removes the highlight and persists across refresh.
+5. Incoming activity in the currently open thread does not leave it unread.
+6. Sending or receiving delivery updates does not clear another unread state.
+7. Archiving/unarchiving does not change read state.
+
+## Out of Scope
+
+- Per-message read receipts.
+- Android read/unread UI.
+- Unread counts or badges outside the thread list.
+- Manual read/unread controls.
+- Cross-user/group-conversation receipt tracking.
diff --git a/tests/README.md b/tests/README.md
index 345d84d1e..45bbb278f 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -53,6 +53,7 @@ The API's Firebase SDK is configured (via `FCM_ENDPOINT` env var) to redirect al
- [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
+- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state
- [x] **Unarchive Thread on Receive E2E** — Archived thread returns to the inbox on inbound message when the phone's `unarchive_thread` setting is enabled, and stays archived when disabled
## Prerequisites
diff --git a/tests/read_receipts_test.go b/tests/read_receipts_test.go
new file mode 100644
index 000000000..a65e7eb73
--- /dev/null
+++ b/tests/read_receipts_test.go
@@ -0,0 +1,206 @@
+package tests
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ httpsms "github.com/NdoleStudio/httpsms-go"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type integrationMessageThread struct {
+ ID string `json:"id"`
+ Contact string `json:"contact"`
+ IsRead bool `json:"is_read"`
+ LastMessageContent *string `json:"last_message_content"`
+}
+
+func requestJSON(
+ ctx context.Context,
+ t *testing.T,
+ method string,
+ path string,
+ apiKey string,
+ payload any,
+ expectedStatus int,
+ output any,
+) {
+ t.Helper()
+
+ var encoded []byte
+ if payload != nil {
+ var err error
+ encoded, err = json.Marshal(payload)
+ require.NoError(t, err)
+ }
+
+ deadline := time.Now().Add(20 * time.Second)
+ for {
+ request, err := http.NewRequestWithContext(ctx, method, apiBaseURL+path, bytes.NewReader(encoded))
+ require.NoError(t, err)
+ request.Header.Set("x-api-key", apiKey)
+ request.Header.Set("Content-Type", "application/json")
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+
+ responseBody, err := io.ReadAll(response.Body)
+ response.Body.Close()
+ require.NoError(t, err)
+
+ if response.StatusCode == http.StatusUnauthorized &&
+ apiKey != userAPIKey &&
+ time.Now().Before(deadline) {
+ time.Sleep(500 * time.Millisecond)
+ continue
+ }
+
+ require.Equal(t, expectedStatus, response.StatusCode, "response: %s", string(responseBody))
+ if output != nil {
+ require.NoError(t, json.Unmarshal(responseBody, output))
+ }
+ return
+ }
+}
+
+func fetchMessageThreads(ctx context.Context, t *testing.T, owner string) []integrationMessageThread {
+ t.Helper()
+
+ var response struct {
+ Data []integrationMessageThread `json:"data"`
+ }
+ path := fmt.Sprintf(
+ "/v1/message-threads?owner=%s&skip=0&limit=20&is_archived=false",
+ url.QueryEscape(owner),
+ )
+ requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response)
+ return response.Data
+}
+
+func waitForMessageThread(
+ ctx context.Context,
+ t *testing.T,
+ owner string,
+ contact string,
+ timeout time.Duration,
+ matches func(integrationMessageThread) bool,
+) integrationMessageThread {
+ t.Helper()
+
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ for _, thread := range fetchMessageThreads(ctx, t, owner) {
+ if thread.Contact == contact && matches(thread) {
+ return thread
+ }
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ t.Fatalf("thread %s -> %s did not reach the expected state within %v", owner, contact, timeout)
+ return integrationMessageThread{}
+}
+
+func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) integrationMessageThread {
+ t.Helper()
+
+ var response struct {
+ Data integrationMessageThread `json:"data"`
+ }
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPut,
+ "/v1/message-threads/"+threadID,
+ userAPIKey,
+ map[string]any{"is_read": true},
+ http.StatusOK,
+ &response,
+ )
+ return response.Data
+}
+
+func TestMessageThreadReadReceipts(t *testing.T) {
+ ctx := context.Background()
+ phone := setupPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPost,
+ "/v1/messages/receive",
+ phone.PhoneAPIKey,
+ map[string]any{
+ "from": contact,
+ "to": phone.PhoneNumber,
+ "content": "Unread inbound message",
+ "encrypted": false,
+ "sim": "SIM1",
+ "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
+ },
+ http.StatusOK,
+ nil,
+ )
+
+ thread := waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return !thread.IsRead
+ })
+ assert.False(t, thread.IsRead)
+
+ updated := markMessageThreadRead(ctx, t, thread.ID)
+ assert.True(t, updated.IsRead)
+ assert.Equal(t, contact, updated.Contact)
+ require.NotNil(t, updated.LastMessageContent)
+ assert.Equal(t, "Unread inbound message", *updated.LastMessageContent)
+ waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 10*time.Second, func(thread integrationMessageThread) bool {
+ return thread.IsRead
+ })
+
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPost,
+ "/v1/messages/calls/missed",
+ phone.PhoneAPIKey,
+ map[string]any{
+ "from": contact,
+ "to": phone.PhoneNumber,
+ "sim": "SIM1",
+ "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
+ },
+ http.StatusOK,
+ nil,
+ )
+
+ thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return !thread.IsRead &&
+ thread.LastMessageContent != nil &&
+ *thread.LastMessageContent == "Missed phone call"
+ })
+ assert.False(t, thread.IsRead)
+
+ outboundContent := "Outbound activity preserves unread"
+ client := newAPIClient()
+ _, response, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{
+ From: phone.PhoneNumber,
+ To: contact,
+ Content: outboundContent,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
+
+ thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
+ return thread.LastMessageContent != nil &&
+ *thread.LastMessageContent == outboundContent
+ })
+ assert.False(t, thread.IsRead, "outbound activity must not clear unread state")
+}
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index 029599180..2a9834317 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -99,7 +99,11 @@ function onInstallApp() {
:active="threadsStore.threadId === thread.id"
>
-
+
{{
mdiAccount
}}
@@ -108,11 +112,12 @@ function onInstallApp() {
}}
- {{
+ {{
formatPhoneNumber(thread.contact)
}}
{{ thread.last_message_content }}
diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue
index 1fd11eae1..73632a64c 100644
--- a/web/app/pages/threads/[id]/index.vue
+++ b/web/app/pages/threads/[id]/index.vue
@@ -112,9 +112,19 @@ function scrollToElement() {
hideMessages.value = false
}
-function loadMessages(hide = true) {
+async function markCurrentThreadRead(force = false) {
+ const threadId = route.params.id as string
+ try {
+ await threadsStore.markThreadRead(threadId, force)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+function loadMessages(hide = true, markRead = true) {
loadingMessages.value = true
const threadId = route.params.id as string
+ if (markRead) void markCurrentThreadRead()
threadsStore
.loadThreadMessages(threadId)
.then((response: EntitiesMessage[]) => {
@@ -232,7 +242,16 @@ onMounted(async () => {
if (!loadingMessages.value) loadMessages(false)
})
webhookChannel.bind('message.phone.received', () => {
- if (!loadingMessages.value) loadMessages(false)
+ if (!loadingMessages.value) {
+ void markCurrentThreadRead(true)
+ loadMessages(false, false)
+ }
+ })
+ webhookChannel.bind('message.call.missed', () => {
+ if (!loadingMessages.value) {
+ void markCurrentThreadRead(true)
+ loadMessages(false, false)
+ }
})
})
diff --git a/web/app/stores/threads.ts b/web/app/stores/threads.ts
index aef2b2947..7e3002624 100644
--- a/web/app/stores/threads.ts
+++ b/web/app/stores/threads.ts
@@ -24,6 +24,13 @@ export const useThreadsStore = defineStore('threads', () => {
return threads.value.find((x) => x.id === id) !== undefined
}
+ function replaceThread(updatedThread: EntitiesMessageThread) {
+ const index = threads.value.findIndex(
+ (thread) => thread.id === updatedThread.id,
+ )
+ if (index !== -1) threads.value[index] = updatedThread
+ }
+
async function loadThreads() {
const phonesStore = usePhonesStore()
if (phonesStore.owner === null && phonesStore.phones.length === 0) {
@@ -93,6 +100,38 @@ export const useThreadsStore = defineStore('threads', () => {
})
}
+ async function markThreadRead(threadId: string, force = false) {
+ const thread = threads.value.find((item) => item.id === threadId)
+ if (!thread) throw new Error(`Cannot find thread with id ${threadId}`)
+ if (!force && thread.is_read) return
+
+ try {
+ const response = await apiFetch<{ data: EntitiesMessageThread }>(
+ `/v1/message-threads/${threadId}`,
+ {
+ method: 'PUT',
+ body: { is_read: true },
+ },
+ )
+ replaceThread(response.data)
+ } catch (error) {
+ notificationsStore.addNotification({
+ message: 'The message thread could not be marked as read',
+ type: 'error',
+ })
+ try {
+ await loadThreads()
+ } catch (reloadError) {
+ throw new AggregateError(
+ [error, reloadError],
+ 'Could not mark the message thread as read or reload threads',
+ { cause: reloadError },
+ )
+ }
+ throw error
+ }
+ }
+
async function deleteThread(id: string) {
await apiFetch(`/v1/message-threads/${id}`, { method: 'DELETE' })
threadId.value = null
@@ -122,6 +161,7 @@ export const useThreadsStore = defineStore('threads', () => {
setThreadId,
toggleArchive,
updateThread,
+ markThreadRead,
deleteThread,
resetState,
}
diff --git a/web/shared/types/api.ts b/web/shared/types/api.ts
index 3f19b8534..27e2db111 100644
--- a/web/shared/types/api.ts
+++ b/web/shared/types/api.ts
@@ -205,6 +205,8 @@ export interface EntitiesMessageThread {
id: string;
/** @example false */
is_archived: boolean;
+ /** @example true */
+ is_read: boolean;
/** @example "This is a sample message content" */
last_message_content: string;
/** @example "32343a19-da5e-4b1b-a767-3298a73703ca" */
@@ -484,7 +486,9 @@ export interface RequestsMessageSendScheduleWindow {
export interface RequestsMessageThreadUpdate {
/** @example true */
- is_archived: boolean;
+ is_archived?: boolean;
+ /** @example true */
+ is_read?: boolean;
}
export interface RequestsPhoneAPIKeyStoreRequest {
@@ -684,6 +688,14 @@ export interface ResponsesMessageSendSchedulesResponse {
status: string;
}
+export interface ResponsesMessageThreadResponse {
+ data: EntitiesMessageThread;
+ /** @example "Request handled successfully" */
+ message: string;
+ /** @example "success" */
+ status: string;
+}
+
export interface ResponsesMessageThreadsResponse {
data: EntitiesMessageThread[];
/** @example "Request handled successfully" */
From 8b37ec7ef80e301d5a4aa29e89f5b40a8d6deb95 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 15:04:22 +0300
Subject: [PATCH 02/88] docs: add email number formatting design
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ddea6ddd-8c2a-4505-a002-64e92b067846
---
...26-07-19-email-number-formatting-design.md | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 docs/superpowers/specs/2026-07-19-email-number-formatting-design.md
diff --git a/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md b/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md
new file mode 100644
index 000000000..6e715b1d2
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md
@@ -0,0 +1,30 @@
+# Email Number Formatting Design
+
+## Goal
+
+Format integer quantities in generated emails with comma thousands separators. For example, `20000` becomes `20,000`.
+
+## Scope
+
+Apply formatting to message counts and subscription limits in usage-limit emails:
+
+- sent messages
+- received messages
+- total messages
+- plan message limits
+
+Do not change dates, percentages, HTTP status codes, phone numbers, years, or numeric event payload values.
+
+## Design
+
+Add a private integer-formatting helper to the shared email `factory`. The helper will produce deterministic comma-grouped decimal output without adding a dependency.
+
+Use the helper in `hermes_user_email_factory.go` anywhere a message count or subscription limit is rendered. Because Hermes generates both HTML and plain text from the same content model, both email formats will receive identical formatting.
+
+## Error Handling
+
+Formatting is a pure operation and introduces no new error path. Existing email generation errors continue to propagate unchanged.
+
+## Testing
+
+Add focused tests for integer grouping, including values below and above one thousand. Update usage-limit email tests to assert formatted quantities in both generated plain text and HTML.
From 9bac0f7a22a8f8ec177005c777e07aa07303cc1c Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 15:12:43 +0300
Subject: [PATCH 03/88] fix(api): format email quantities
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ddea6ddd-8c2a-4505-a002-64e92b067846
---
api/pkg/emails/factory.go | 10 +++++++++
api/pkg/emails/hermes_user_email_factory.go | 6 ++---
.../emails/hermes_user_email_factory_test.go | 22 ++++++++++---------
3 files changed, 25 insertions(+), 13 deletions(-)
diff --git a/api/pkg/emails/factory.go b/api/pkg/emails/factory.go
index 992f48646..63d85f5e2 100644
--- a/api/pkg/emails/factory.go
+++ b/api/pkg/emails/factory.go
@@ -3,6 +3,7 @@ package emails
import (
"fmt"
"net/http"
+ "strconv"
"github.com/nyaruka/phonenumbers"
)
@@ -21,6 +22,15 @@ func (factory *factory) formatBool(value bool) string {
return "No"
}
+func (factory *factory) formatQuantity(value uint) string {
+ formatted := strconv.FormatUint(uint64(value), 10)
+ for index := len(formatted) - 3; index > 0; index -= 3 {
+ formatted = formatted[:index] + "," + formatted[index:]
+ }
+
+ return formatted
+}
+
func (factory *factory) formatHTTPResponseCode(code *int) string {
responseCode := "-"
if code != nil {
diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go
index cae4a3b9a..174b0f420 100644
--- a/api/pkg/emails/hermes_user_email_factory.go
+++ b/api/pkg/emails/hermes_user_email_factory.go
@@ -73,8 +73,8 @@ func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User, u
email := hermes.Email{
Body: hermes.Body{
Intros: []string{
- fmt.Sprintf("You've reached your limit of %d messages on the %s plan, so new messages will not be processed until your usage resets.", user.SubscriptionName.Limit(), user.SubscriptionName),
- fmt.Sprintf("Between %s and %s you sent %d messages and received %d, for a total of %d.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), usage.SentMessages, usage.ReceivedMessages, usage.TotalMessages()),
+ fmt.Sprintf("You've reached your limit of %s messages on the %s plan, so new messages will not be processed until your usage resets.", factory.formatQuantity(user.SubscriptionName.Limit()), user.SubscriptionName),
+ fmt.Sprintf("Between %s and %s you sent %s messages and received %s, for a total of %s.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), factory.formatQuantity(usage.SentMessages), factory.formatQuantity(usage.ReceivedMessages), factory.formatQuantity(usage.TotalMessages())),
},
Actions: []hermes.Action{
{
@@ -120,7 +120,7 @@ func (factory *hermesUserEmailFactory) UsageLimitAlert(user *entities.User, usag
Body: hermes.Body{
Intros: []string{
fmt.Sprintf("This is a friendly heads-up that you've used %d%% of your monthly SMS limit on the %s plan.", percent, user.SubscriptionName),
- fmt.Sprintf("Between %s and %s you sent %d messages and received %d, for a total of %d out of your %d message limit.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), usage.SentMessages, usage.ReceivedMessages, usage.TotalMessages(), user.SubscriptionName.Limit()),
+ fmt.Sprintf("Between %s and %s you sent %s messages and received %s, for a total of %s out of your %s message limit.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), factory.formatQuantity(usage.SentMessages), factory.formatQuantity(usage.ReceivedMessages), factory.formatQuantity(usage.TotalMessages()), factory.formatQuantity(user.SubscriptionName.Limit())),
},
Actions: []hermes.Action{
{
diff --git a/api/pkg/emails/hermes_user_email_factory_test.go b/api/pkg/emails/hermes_user_email_factory_test.go
index df322e926..e10e3f487 100644
--- a/api/pkg/emails/hermes_user_email_factory_test.go
+++ b/api/pkg/emails/hermes_user_email_factory_test.go
@@ -51,10 +51,11 @@ func TestUsageLimitExceeded_IncludesBreakdownAndBillingPeriod(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "name@email.com", email.ToEmail)
assert.Equal(t, "⚠️ You have exceeded your plan limit", email.Subject)
- assert.Contains(t, email.Text, "limit of 5000 messages")
+ assert.Contains(t, email.Text, "limit of 5,000 messages")
assert.Contains(t, email.Text, "Between 19 June 2026 and 18 July 2026")
- assert.Contains(t, email.Text, "you sent 3000 messages and received 2000")
- assert.Contains(t, email.Text, "for a total of 5000")
+ assert.Contains(t, email.Text, "you sent 3,000 messages and received 2,000")
+ assert.Contains(t, email.Text, "for a total of 5,000")
+ assert.Contains(t, email.HTML, "you sent 3,000 messages and received 2,000")
}
func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) {
@@ -62,11 +63,11 @@ func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) {
user := &entities.User{
Email: "name@email.com",
Timezone: "UTC",
- SubscriptionName: entities.SubscriptionNameProMonthly,
+ SubscriptionName: entities.SubscriptionName20KMonthly,
}
usage := &entities.BillingUsage{
- SentMessages: 2500,
- ReceivedMessages: 1500,
+ SentMessages: 11_357,
+ ReceivedMessages: 6_643,
StartTimestamp: time.Date(2026, 6, 19, 0, 0, 0, 0, time.UTC),
EndTimestamp: time.Date(2026, 7, 18, 23, 59, 59, 0, time.UTC),
}
@@ -75,9 +76,10 @@ func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "name@email.com", email.ToEmail)
- assert.Equal(t, "⚠️ 80% Usage Limit Alert", email.Subject)
- assert.Contains(t, email.Text, "used 80% of your monthly SMS limit")
+ assert.Equal(t, "⚠️ 90% Usage Limit Alert", email.Subject)
+ assert.Contains(t, email.Text, "used 90% of your monthly SMS limit")
assert.Contains(t, email.Text, "Between 19 June 2026 and 18 July 2026")
- assert.Contains(t, email.Text, "you sent 2500 messages and received 1500")
- assert.Contains(t, email.Text, "for a total of 4000 out of your 5000 message limit")
+ assert.Contains(t, email.Text, "you sent 11,357 messages and received 6,643")
+ assert.Contains(t, email.Text, "for a total of 18,000 out of your 20,000 message limit")
+ assert.Contains(t, email.HTML, "for a total of 18,000 out of your 20,000 message limit")
}
From ef6225beeaeae8875209f1dc260bf64dd21d9a41 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 15:16:10 +0300
Subject: [PATCH 04/88] build(api): use NdoleStudio stacktrace fork
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 10c01eca-aeb5-4fc2-a546-02cee4261d2b
---
api/go.mod | 2 +-
api/go.sum | 4 ++--
api/pkg/cache/memory_cache.go | 2 +-
api/pkg/cache/redis_cache.go | 2 +-
api/pkg/di/container.go | 2 +-
api/pkg/emails/hermes_notification_email_factory.go | 2 +-
api/pkg/emails/hermes_user_email_factory.go | 2 +-
api/pkg/emails/smtp_mailer_service.go | 2 +-
api/pkg/handlers/attachment_handler.go | 2 +-
api/pkg/handlers/billing_handler.go | 2 +-
api/pkg/handlers/bulk_message_handler.go | 2 +-
api/pkg/handlers/discord_handler.go | 2 +-
api/pkg/handlers/events_handler.go | 2 +-
api/pkg/handlers/heartbeat_handler.go | 2 +-
api/pkg/handlers/integration_3cx_handler.go | 2 +-
api/pkg/handlers/lemonsqueezy_handler.go | 2 +-
api/pkg/handlers/message_handler.go | 2 +-
api/pkg/handlers/message_send_schedule_handler.go | 2 +-
api/pkg/handlers/message_thread_handler.go | 2 +-
api/pkg/handlers/message_thread_handler_test.go | 2 +-
api/pkg/handlers/phone_api_key_handler.go | 2 +-
api/pkg/handlers/phone_handler.go | 2 +-
api/pkg/handlers/user_handler.go | 2 +-
api/pkg/handlers/webhook_handler.go | 2 +-
api/pkg/listeners/billing_listener.go | 2 +-
api/pkg/listeners/discord_listener.go | 2 +-
api/pkg/listeners/email_notification_listener.go | 2 +-
api/pkg/listeners/heartbeat_listener.go | 2 +-
api/pkg/listeners/integration_3cx_listener.go | 2 +-
api/pkg/listeners/marketing_listener.go | 2 +-
api/pkg/listeners/message_listener.go | 2 +-
api/pkg/listeners/message_send_schedule_listener.go | 2 +-
api/pkg/listeners/message_thread_listener.go | 2 +-
api/pkg/listeners/phone_api_key_listener.go | 2 +-
api/pkg/listeners/phone_listener.go | 2 +-
api/pkg/listeners/phone_notification_listener.go | 2 +-
api/pkg/listeners/user_listener.go | 2 +-
api/pkg/listeners/webhook_listener.go | 2 +-
api/pkg/listeners/websocket_listener.go | 2 +-
api/pkg/middlewares/api_key_auth_middleware.go | 2 +-
api/pkg/middlewares/bearer_api_key_auth_middleware.go | 2 +-
api/pkg/middlewares/bearer_auth_middleware.go | 2 +-
api/pkg/middlewares/http_request_logger_middleware.go | 2 +-
api/pkg/middlewares/phone_api_key_auth_middleware.go | 2 +-
.../google_cloud_storage_attachment_repository.go | 2 +-
api/pkg/repositories/gorm_billing_usage_repository.go | 2 +-
api/pkg/repositories/gorm_discord_repository.go | 2 +-
api/pkg/repositories/gorm_heartbeat_monitor_repository.go | 2 +-
api/pkg/repositories/gorm_heartbeat_repository.go | 2 +-
api/pkg/repositories/gorm_integration_3cx_repository.go | 2 +-
api/pkg/repositories/gorm_message_repository.go | 2 +-
api/pkg/repositories/gorm_message_send_schedule_repository.go | 2 +-
api/pkg/repositories/gorm_message_thread_repository.go | 2 +-
api/pkg/repositories/gorm_phone_api_key_repository.go | 2 +-
api/pkg/repositories/gorm_phone_notification_repository.go | 2 +-
api/pkg/repositories/gorm_phone_repository.go | 2 +-
api/pkg/repositories/gorm_user_repository.go | 2 +-
api/pkg/repositories/gorm_webhook_repository.go | 2 +-
api/pkg/repositories/memory_attachment_repository.go | 2 +-
api/pkg/repositories/mongo_heartbeat_monitor_repository.go | 2 +-
api/pkg/repositories/mongo_heartbeat_repository.go | 2 +-
api/pkg/repositories/mongodb.go | 2 +-
api/pkg/repositories/repository.go | 2 +-
api/pkg/services/billing_service.go | 2 +-
api/pkg/services/discord_service.go | 2 +-
api/pkg/services/email_notification_service.go | 2 +-
api/pkg/services/emulator_fcm_client.go | 2 +-
api/pkg/services/emulator_push_queue.go | 2 +-
api/pkg/services/entitlement_service.go | 2 +-
api/pkg/services/event_dispatcher_service.go | 2 +-
api/pkg/services/google_cloud_push_queue_service.go | 2 +-
api/pkg/services/heartbeat_service.go | 2 +-
api/pkg/services/integration_3cx_service.go | 2 +-
api/pkg/services/lemonsqueezy_service.go | 2 +-
api/pkg/services/marketting_service.go | 2 +-
api/pkg/services/message_send_schedule_service.go | 2 +-
api/pkg/services/message_service.go | 2 +-
api/pkg/services/message_thread_service.go | 2 +-
api/pkg/services/message_thread_service_test.go | 2 +-
api/pkg/services/phone_api_key_service.go | 2 +-
api/pkg/services/phone_notification_service.go | 2 +-
api/pkg/services/phone_service.go | 2 +-
api/pkg/services/service.go | 2 +-
api/pkg/services/user_service.go | 2 +-
api/pkg/services/webhook_service.go | 2 +-
api/pkg/telemetry/gorm_logger.go | 2 +-
api/pkg/validators/bulk_message_handler_validator.go | 2 +-
api/pkg/validators/discord_handler_validator.go | 2 +-
api/pkg/validators/message_handler_validator.go | 2 +-
api/pkg/validators/turnstile_token_validator.go | 2 +-
api/pkg/validators/user_handler_validator.go | 2 +-
api/pkg/validators/webhook_handler_validator.go | 2 +-
92 files changed, 93 insertions(+), 93 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 565e15d4a..649489f65 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -12,6 +12,7 @@ require (
github.com/NdoleStudio/go-otelroundtripper v0.0.15
github.com/NdoleStudio/lemonsqueezy-go v1.3.1
github.com/NdoleStudio/plunk-go v0.0.2
+ github.com/NdoleStudio/stacktrace v1.0.0
github.com/avast/retry-go/v5 v5.0.0
github.com/axiomhq/axiom-go v0.32.0
github.com/carlmjohnson/requests v0.25.1
@@ -35,7 +36,6 @@ require (
github.com/jszwec/csvutil v1.10.0
github.com/lib/pq v1.12.3
github.com/nyaruka/phonenumbers v1.8.0
- 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
diff --git a/api/go.sum b/api/go.sum
index dc7298013..264094f1a 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -60,6 +60,8 @@ github.com/NdoleStudio/lemonsqueezy-go v1.3.1 h1:lMUVgdAx2onbOUJIVPR05xAANYuCMXB
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/NdoleStudio/stacktrace v1.0.0 h1:YIDg0twM2BXRKIFjorX7v2dC0l8z4VEITurI+MNqvVQ=
+github.com/NdoleStudio/stacktrace v1.0.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg=
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=
@@ -278,8 +280,6 @@ 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.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw=
diff --git a/api/pkg/cache/memory_cache.go b/api/pkg/cache/memory_cache.go
index 4500640c0..e749ca3e3 100644
--- a/api/pkg/cache/memory_cache.go
+++ b/api/pkg/cache/memory_cache.go
@@ -5,7 +5,7 @@ import (
"time"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
ttlCache "github.com/patrickmn/go-cache"
)
diff --git a/api/pkg/cache/redis_cache.go b/api/pkg/cache/redis_cache.go
index 4bbdebcee..990f09bcd 100644
--- a/api/pkg/cache/redis_cache.go
+++ b/api/pkg/cache/redis_cache.go
@@ -6,7 +6,7 @@ import (
"time"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/redis/go-redis/v9"
)
diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go
index 62fee7624..6d933ae86 100644
--- a/api/pkg/di/container.go
+++ b/api/pkg/di/container.go
@@ -67,10 +67,10 @@ import (
"github.com/NdoleStudio/httpsms/pkg/listeners"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/services"
+ "github.com/NdoleStudio/stacktrace"
swagger "github.com/gofiber/contrib/v3/swaggo"
"github.com/gofiber/fiber/v3"
fiberLogger "github.com/gofiber/fiber/v3/middleware/logger"
- "github.com/palantir/stacktrace"
ttlCache "github.com/patrickmn/go-cache"
"gorm.io/gorm"
diff --git a/api/pkg/emails/hermes_notification_email_factory.go b/api/pkg/emails/hermes_notification_email_factory.go
index 2229a502a..4c0440646 100644
--- a/api/pkg/emails/hermes_notification_email_factory.go
+++ b/api/pkg/emails/hermes_notification_email_factory.go
@@ -8,8 +8,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/stacktrace"
"github.com/go-hermes/hermes/v2"
- "github.com/palantir/stacktrace"
)
type hermesNotificationEmailFactory struct {
diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go
index 174b0f420..8070a78e7 100644
--- a/api/pkg/emails/hermes_user_email_factory.go
+++ b/api/pkg/emails/hermes_user_email_factory.go
@@ -5,8 +5,8 @@ import (
"time"
"github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/stacktrace"
"github.com/go-hermes/hermes/v2"
- "github.com/palantir/stacktrace"
)
type hermesUserEmailFactory struct {
diff --git a/api/pkg/emails/smtp_mailer_service.go b/api/pkg/emails/smtp_mailer_service.go
index bebea4a0c..5f57d1daf 100644
--- a/api/pkg/emails/smtp_mailer_service.go
+++ b/api/pkg/emails/smtp_mailer_service.go
@@ -6,8 +6,8 @@ import (
"net/smtp"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
mail "github.com/jordan-wright/email"
- "github.com/palantir/stacktrace"
)
// SMTPConfig is the config for setting up the smtpMailer
diff --git a/api/pkg/handlers/attachment_handler.go b/api/pkg/handlers/attachment_handler.go
index 0b058f505..e73f12530 100644
--- a/api/pkg/handlers/attachment_handler.go
+++ b/api/pkg/handlers/attachment_handler.go
@@ -6,8 +6,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// AttachmentHandler handles attachment download requests
diff --git a/api/pkg/handlers/billing_handler.go b/api/pkg/handlers/billing_handler.go
index 33f0b2f0d..a33ad2312 100644
--- a/api/pkg/handlers/billing_handler.go
+++ b/api/pkg/handlers/billing_handler.go
@@ -7,9 +7,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// BillingHandler handles billing http requests.
diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go
index 01e451469..28e0d0ad4 100644
--- a/api/pkg/handlers/bulk_message_handler.go
+++ b/api/pkg/handlers/bulk_message_handler.go
@@ -12,9 +12,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// BulkMessageHandler handles bulk SMS http requests
diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go
index 29da67a07..582cdb325 100644
--- a/api/pkg/handlers/discord_handler.go
+++ b/api/pkg/handlers/discord_handler.go
@@ -16,9 +16,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// DiscordHandler handles discord events
diff --git a/api/pkg/handlers/events_handler.go b/api/pkg/handlers/events_handler.go
index b2586ed21..5d4fc121b 100644
--- a/api/pkg/handlers/events_handler.go
+++ b/api/pkg/handlers/events_handler.go
@@ -7,9 +7,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// EventsHandler handles heartbeat http requests.
diff --git a/api/pkg/handlers/heartbeat_handler.go b/api/pkg/handlers/heartbeat_handler.go
index eb9dec405..663d16af3 100644
--- a/api/pkg/handlers/heartbeat_handler.go
+++ b/api/pkg/handlers/heartbeat_handler.go
@@ -10,9 +10,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// HeartbeatHandler handles heartbeat http requests.
diff --git a/api/pkg/handlers/integration_3cx_handler.go b/api/pkg/handlers/integration_3cx_handler.go
index e53213f42..db06e64df 100644
--- a/api/pkg/handlers/integration_3cx_handler.go
+++ b/api/pkg/handlers/integration_3cx_handler.go
@@ -8,8 +8,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// Integration3CXHandler handles 3CX events
diff --git a/api/pkg/handlers/lemonsqueezy_handler.go b/api/pkg/handlers/lemonsqueezy_handler.go
index ce3ac5aac..1b18d8baa 100644
--- a/api/pkg/handlers/lemonsqueezy_handler.go
+++ b/api/pkg/handlers/lemonsqueezy_handler.go
@@ -9,9 +9,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
lemonsqueezy "github.com/NdoleStudio/lemonsqueezy-go"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// LemonsqueezyHandler handles lemonsqueezy events
diff --git a/api/pkg/handlers/message_handler.go b/api/pkg/handlers/message_handler.go
index 542fc7d04..14a4e95e5 100644
--- a/api/pkg/handlers/message_handler.go
+++ b/api/pkg/handlers/message_handler.go
@@ -16,9 +16,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// MessageHandler handles message http requests.
diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go
index 90d65cca9..93fac9480 100644
--- a/api/pkg/handlers/message_send_schedule_handler.go
+++ b/api/pkg/handlers/message_send_schedule_handler.go
@@ -9,10 +9,10 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// MessageSendScheduleHandler handles HTTP requests for message send schedules.
diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go
index 9520b6634..996ca69be 100644
--- a/api/pkg/handlers/message_thread_handler.go
+++ b/api/pkg/handlers/message_thread_handler.go
@@ -10,9 +10,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// MessageThreadHandler handles message-thead http requests.
diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go
index ab2d75962..60b9d756e 100644
--- a/api/pkg/handlers/message_thread_handler_test.go
+++ b/api/pkg/handlers/message_thread_handler_test.go
@@ -15,9 +15,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
"gorm.io/gorm"
diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go
index 835ad893a..55b981cf4 100644
--- a/api/pkg/handlers/phone_api_key_handler.go
+++ b/api/pkg/handlers/phone_api_key_handler.go
@@ -9,10 +9,10 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// PhoneAPIKeyHandler handles phone API key http requests
diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go
index 291c071ad..678b89062 100644
--- a/api/pkg/handlers/phone_handler.go
+++ b/api/pkg/handlers/phone_handler.go
@@ -10,8 +10,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// PhoneHandler handles phone http requests.
diff --git a/api/pkg/handlers/user_handler.go b/api/pkg/handlers/user_handler.go
index b7066aa3f..8f73ba00c 100644
--- a/api/pkg/handlers/user_handler.go
+++ b/api/pkg/handlers/user_handler.go
@@ -10,8 +10,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// UserHandler handles user http requests.
diff --git a/api/pkg/handlers/webhook_handler.go b/api/pkg/handlers/webhook_handler.go
index 656d59ea0..6eb389e3c 100644
--- a/api/pkg/handlers/webhook_handler.go
+++ b/api/pkg/handlers/webhook_handler.go
@@ -11,9 +11,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/httpsms/pkg/validators"
+ "github.com/NdoleStudio/stacktrace"
"github.com/davecgh/go-spew/spew"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// WebhookHandler handles webhook requests
diff --git a/api/pkg/listeners/billing_listener.go b/api/pkg/listeners/billing_listener.go
index 59f166f10..2645e27c3 100644
--- a/api/pkg/listeners/billing_listener.go
+++ b/api/pkg/listeners/billing_listener.go
@@ -9,8 +9,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// BillingListener handles cloud events which affect billing
diff --git a/api/pkg/listeners/discord_listener.go b/api/pkg/listeners/discord_listener.go
index 531aa2080..aebed46b7 100644
--- a/api/pkg/listeners/discord_listener.go
+++ b/api/pkg/listeners/discord_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// DiscordListener sends messages to discord
diff --git a/api/pkg/listeners/email_notification_listener.go b/api/pkg/listeners/email_notification_listener.go
index 1f7662855..fc6065398 100644
--- a/api/pkg/listeners/email_notification_listener.go
+++ b/api/pkg/listeners/email_notification_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// EmailNotificationListener listens for events about failed and expired messages
diff --git a/api/pkg/listeners/heartbeat_listener.go b/api/pkg/listeners/heartbeat_listener.go
index 00767a955..eef4933f5 100644
--- a/api/pkg/listeners/heartbeat_listener.go
+++ b/api/pkg/listeners/heartbeat_listener.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/davecgh/go-spew/spew"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
diff --git a/api/pkg/listeners/integration_3cx_listener.go b/api/pkg/listeners/integration_3cx_listener.go
index 2aafebf9f..529916f86 100644
--- a/api/pkg/listeners/integration_3cx_listener.go
+++ b/api/pkg/listeners/integration_3cx_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// Integration3CXListener sends 3CX events to users
diff --git a/api/pkg/listeners/marketing_listener.go b/api/pkg/listeners/marketing_listener.go
index c2bc0a20a..4bcf15d11 100644
--- a/api/pkg/listeners/marketing_listener.go
+++ b/api/pkg/listeners/marketing_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// MarketingListener handled marketing events
diff --git a/api/pkg/listeners/message_listener.go b/api/pkg/listeners/message_listener.go
index b6193626c..49fd18a3d 100644
--- a/api/pkg/listeners/message_listener.go
+++ b/api/pkg/listeners/message_listener.go
@@ -8,8 +8,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// MessageListener handles cloud events which need to update entities.Message
diff --git a/api/pkg/listeners/message_send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go
index 0fb1f00c0..59f93b847 100644
--- a/api/pkg/listeners/message_send_schedule_listener.go
+++ b/api/pkg/listeners/message_send_schedule_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// MessageSendScheduleListener handles cloud events related to message send schedules.
diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go
index 47621296e..ad22e810f 100644
--- a/api/pkg/listeners/message_thread_listener.go
+++ b/api/pkg/listeners/message_thread_listener.go
@@ -9,8 +9,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// MessageThreadListener handles cloud events which need to update entities.MessageThread
diff --git a/api/pkg/listeners/phone_api_key_listener.go b/api/pkg/listeners/phone_api_key_listener.go
index 04fc0ce2e..45333ac56 100644
--- a/api/pkg/listeners/phone_api_key_listener.go
+++ b/api/pkg/listeners/phone_api_key_listener.go
@@ -6,9 +6,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/davecgh/go-spew/spew"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
diff --git a/api/pkg/listeners/phone_listener.go b/api/pkg/listeners/phone_listener.go
index 08d1990d8..f39448483 100644
--- a/api/pkg/listeners/phone_listener.go
+++ b/api/pkg/listeners/phone_listener.go
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go
index d397cdda7..40452a946 100644
--- a/api/pkg/listeners/phone_notification_listener.go
+++ b/api/pkg/listeners/phone_notification_listener.go
@@ -9,8 +9,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// PhoneNotificationListener handles cloud events which sends notifications
diff --git a/api/pkg/listeners/user_listener.go b/api/pkg/listeners/user_listener.go
index 960ce6c90..e4a15d32f 100644
--- a/api/pkg/listeners/user_listener.go
+++ b/api/pkg/listeners/user_listener.go
@@ -9,8 +9,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// UserListener handles cloud events which sends notifications
diff --git a/api/pkg/listeners/webhook_listener.go b/api/pkg/listeners/webhook_listener.go
index 5ff410467..ff9ab5700 100644
--- a/api/pkg/listeners/webhook_listener.go
+++ b/api/pkg/listeners/webhook_listener.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// WebhookListener sends webhook events to users
diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go
index 8aa9054f0..b06381d78 100644
--- a/api/pkg/listeners/websocket_listener.go
+++ b/api/pkg/listeners/websocket_listener.go
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
diff --git a/api/pkg/middlewares/api_key_auth_middleware.go b/api/pkg/middlewares/api_key_auth_middleware.go
index 0d65be307..f2b184194 100644
--- a/api/pkg/middlewares/api_key_auth_middleware.go
+++ b/api/pkg/middlewares/api_key_auth_middleware.go
@@ -6,8 +6,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// APIKeyAuth authenticates a user from the X-API-Key header
diff --git a/api/pkg/middlewares/bearer_api_key_auth_middleware.go b/api/pkg/middlewares/bearer_api_key_auth_middleware.go
index 7806aa7d0..f52d0ba09 100644
--- a/api/pkg/middlewares/bearer_api_key_auth_middleware.go
+++ b/api/pkg/middlewares/bearer_api_key_auth_middleware.go
@@ -6,8 +6,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// BearerAPIKeyAuth authenticates an API key using the Bearer header
diff --git a/api/pkg/middlewares/bearer_auth_middleware.go b/api/pkg/middlewares/bearer_auth_middleware.go
index 1573413c3..6fb57a648 100644
--- a/api/pkg/middlewares/bearer_auth_middleware.go
+++ b/api/pkg/middlewares/bearer_auth_middleware.go
@@ -8,8 +8,8 @@ import (
"firebase.google.com/go/auth"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// BearerAuth authenticates a user based on the bearer token
diff --git a/api/pkg/middlewares/http_request_logger_middleware.go b/api/pkg/middlewares/http_request_logger_middleware.go
index 388c02917..06f5e06da 100644
--- a/api/pkg/middlewares/http_request_logger_middleware.go
+++ b/api/pkg/middlewares/http_request_logger_middleware.go
@@ -5,8 +5,8 @@ import (
"slices"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
const (
diff --git a/api/pkg/middlewares/phone_api_key_auth_middleware.go b/api/pkg/middlewares/phone_api_key_auth_middleware.go
index 5fb01c5ba..fb054aa4a 100644
--- a/api/pkg/middlewares/phone_api_key_auth_middleware.go
+++ b/api/pkg/middlewares/phone_api_key_auth_middleware.go
@@ -6,8 +6,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// PhoneAPIKeyAuth authenticates a user from the X-API-Key header
diff --git a/api/pkg/repositories/google_cloud_storage_attachment_repository.go b/api/pkg/repositories/google_cloud_storage_attachment_repository.go
index 34d389eb7..de0f83007 100644
--- a/api/pkg/repositories/google_cloud_storage_attachment_repository.go
+++ b/api/pkg/repositories/google_cloud_storage_attachment_repository.go
@@ -8,7 +8,7 @@ import (
"cloud.google.com/go/storage"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// GoogleCloudStorageAttachmentRepository stores attachments in Google Cloud Storage
diff --git a/api/pkg/repositories/gorm_billing_usage_repository.go b/api/pkg/repositories/gorm_billing_usage_repository.go
index 47c7fadb6..02509e27d 100644
--- a/api/pkg/repositories/gorm_billing_usage_repository.go
+++ b/api/pkg/repositories/gorm_billing_usage_repository.go
@@ -8,9 +8,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_discord_repository.go b/api/pkg/repositories/gorm_discord_repository.go
index a1d70faa4..462ee37b1 100644
--- a/api/pkg/repositories/gorm_discord_repository.go
+++ b/api/pkg/repositories/gorm_discord_repository.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
index c53e2368b..364e64351 100644
--- a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
+++ b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
@@ -11,7 +11,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_heartbeat_repository.go b/api/pkg/repositories/gorm_heartbeat_repository.go
index 8f77ef21e..544d7eed7 100644
--- a/api/pkg/repositories/gorm_heartbeat_repository.go
+++ b/api/pkg/repositories/gorm_heartbeat_repository.go
@@ -8,7 +8,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_integration_3cx_repository.go b/api/pkg/repositories/gorm_integration_3cx_repository.go
index f6bbf0f7c..c63e5370b 100644
--- a/api/pkg/repositories/gorm_integration_3cx_repository.go
+++ b/api/pkg/repositories/gorm_integration_3cx_repository.go
@@ -7,7 +7,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_message_repository.go b/api/pkg/repositories/gorm_message_repository.go
index 0eea42d1d..bb9c55be7 100644
--- a/api/pkg/repositories/gorm_message_repository.go
+++ b/api/pkg/repositories/gorm_message_repository.go
@@ -9,9 +9,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_message_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go
index 54e588431..49538ce05 100644
--- a/api/pkg/repositories/gorm_message_send_schedule_repository.go
+++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go
index 071f5cc79..88506f485 100644
--- a/api/pkg/repositories/gorm_message_thread_repository.go
+++ b/api/pkg/repositories/gorm_message_thread_repository.go
@@ -11,7 +11,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go
index 7f912b28b..51502d1a7 100644
--- a/api/pkg/repositories/gorm_phone_api_key_repository.go
+++ b/api/pkg/repositories/gorm_phone_api_key_repository.go
@@ -10,9 +10,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/dgraph-io/ristretto/v2"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go
index ffe9352e9..1301ecc02 100644
--- a/api/pkg/repositories/gorm_phone_notification_repository.go
+++ b/api/pkg/repositories/gorm_phone_notification_repository.go
@@ -11,7 +11,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_phone_repository.go b/api/pkg/repositories/gorm_phone_repository.go
index f57cdbe10..98c2f3fb0 100644
--- a/api/pkg/repositories/gorm_phone_repository.go
+++ b/api/pkg/repositories/gorm_phone_repository.go
@@ -8,9 +8,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/dgraph-io/ristretto/v2"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_user_repository.go b/api/pkg/repositories/gorm_user_repository.go
index a2c95e5ef..da46e5e73 100644
--- a/api/pkg/repositories/gorm_user_repository.go
+++ b/api/pkg/repositories/gorm_user_repository.go
@@ -15,7 +15,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/gorm_webhook_repository.go b/api/pkg/repositories/gorm_webhook_repository.go
index 86b2dd1d9..c56bd3a73 100644
--- a/api/pkg/repositories/gorm_webhook_repository.go
+++ b/api/pkg/repositories/gorm_webhook_repository.go
@@ -7,8 +7,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"gorm.io/gorm"
)
diff --git a/api/pkg/repositories/memory_attachment_repository.go b/api/pkg/repositories/memory_attachment_repository.go
index c640d5f8d..eab19336a 100644
--- a/api/pkg/repositories/memory_attachment_repository.go
+++ b/api/pkg/repositories/memory_attachment_repository.go
@@ -6,7 +6,7 @@ import (
"sync"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// MemoryAttachmentRepository stores attachments in memory
diff --git a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
index c6f44dc89..2f6f71cdb 100644
--- a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
+++ b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
@@ -11,7 +11,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// mongoHeartbeatMonitorRepository is responsible for persisting entities.HeartbeatMonitor in MongoDB
diff --git a/api/pkg/repositories/mongo_heartbeat_repository.go b/api/pkg/repositories/mongo_heartbeat_repository.go
index 72bef3b67..e97e696a2 100644
--- a/api/pkg/repositories/mongo_heartbeat_repository.go
+++ b/api/pkg/repositories/mongo_heartbeat_repository.go
@@ -10,7 +10,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// mongoHeartbeatRepository is responsible for persisting entities.Heartbeat in MongoDB
diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go
index 5f68f9c9b..74ea7446f 100644
--- a/api/pkg/repositories/mongodb.go
+++ b/api/pkg/repositories/mongodb.go
@@ -6,8 +6,8 @@ import (
"reflect"
"time"
+ "github.com/NdoleStudio/stacktrace"
"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"
diff --git a/api/pkg/repositories/repository.go b/api/pkg/repositories/repository.go
index 32ba4337d..5d2541fd4 100644
--- a/api/pkg/repositories/repository.go
+++ b/api/pkg/repositories/repository.go
@@ -3,7 +3,7 @@ package repositories
import (
"time"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// IndexParams parameters for indexing a database table
diff --git a/api/pkg/services/billing_service.go b/api/pkg/services/billing_service.go
index 262067130..6b5749933 100644
--- a/api/pkg/services/billing_service.go
+++ b/api/pkg/services/billing_service.go
@@ -11,8 +11,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// BillingService is responsible for tracking usages and billing users
diff --git a/api/pkg/services/discord_service.go b/api/pkg/services/discord_service.go
index 9d5f2680f..e919109b3 100644
--- a/api/pkg/services/discord_service.go
+++ b/api/pkg/services/discord_service.go
@@ -15,8 +15,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// DiscordService is responsible for handling discordIntegrations
diff --git a/api/pkg/services/email_notification_service.go b/api/pkg/services/email_notification_service.go
index ef6d7d853..a87fcf227 100644
--- a/api/pkg/services/email_notification_service.go
+++ b/api/pkg/services/email_notification_service.go
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/cache"
"github.com/NdoleStudio/httpsms/pkg/emails"
diff --git a/api/pkg/services/emulator_fcm_client.go b/api/pkg/services/emulator_fcm_client.go
index a83767753..adf3ed7c5 100644
--- a/api/pkg/services/emulator_fcm_client.go
+++ b/api/pkg/services/emulator_fcm_client.go
@@ -10,7 +10,7 @@ import (
"firebase.google.com/go/messaging"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// EmulatorFCMClient sends FCM messages to the phone emulator via HTTP.
diff --git a/api/pkg/services/emulator_push_queue.go b/api/pkg/services/emulator_push_queue.go
index f861cebfb..3e99a2369 100644
--- a/api/pkg/services/emulator_push_queue.go
+++ b/api/pkg/services/emulator_push_queue.go
@@ -6,8 +6,8 @@ import (
"net/http"
"time"
+ "github.com/NdoleStudio/stacktrace"
"github.com/carlmjohnson/requests"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/google/uuid"
diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go
index 875b27915..a4a4ab891 100644
--- a/api/pkg/services/entitlement_service.go
+++ b/api/pkg/services/entitlement_service.go
@@ -9,8 +9,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
pluralize "github.com/gertd/go-pluralize"
- "github.com/palantir/stacktrace"
)
// entityLimits maps entity name → subscription plan → max count.
diff --git a/api/pkg/services/event_dispatcher_service.go b/api/pkg/services/event_dispatcher_service.go
index 9b2710b59..a090c943a 100644
--- a/api/pkg/services/event_dispatcher_service.go
+++ b/api/pkg/services/event_dispatcher_service.go
@@ -15,8 +15,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// EventDispatcher dispatches a new event
diff --git a/api/pkg/services/google_cloud_push_queue_service.go b/api/pkg/services/google_cloud_push_queue_service.go
index b8854616e..7f9555207 100644
--- a/api/pkg/services/google_cloud_push_queue_service.go
+++ b/api/pkg/services/google_cloud_push_queue_service.go
@@ -11,7 +11,7 @@ import (
cloudtasks "cloud.google.com/go/cloudtasks/apiv2"
"cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"google.golang.org/protobuf/types/known/timestamppb"
)
diff --git a/api/pkg/services/heartbeat_service.go b/api/pkg/services/heartbeat_service.go
index a67899b44..1f02e9c4b 100644
--- a/api/pkg/services/heartbeat_service.go
+++ b/api/pkg/services/heartbeat_service.go
@@ -11,7 +11,7 @@ import (
"github.com/google/uuid"
"github.com/NdoleStudio/httpsms/pkg/repositories"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
diff --git a/api/pkg/services/integration_3cx_service.go b/api/pkg/services/integration_3cx_service.go
index ef9417df3..b3d046ef7 100644
--- a/api/pkg/services/integration_3cx_service.go
+++ b/api/pkg/services/integration_3cx_service.go
@@ -15,8 +15,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "github.com/palantir/stacktrace"
)
// Integration3CXService is responsible for handling webhooks
diff --git a/api/pkg/services/lemonsqueezy_service.go b/api/pkg/services/lemonsqueezy_service.go
index 40b5aaec0..48347335c 100644
--- a/api/pkg/services/lemonsqueezy_service.go
+++ b/api/pkg/services/lemonsqueezy_service.go
@@ -12,7 +12,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/lemonsqueezy-go"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// LemonsqueezyService is responsible for managing lemonsqueezy events
diff --git a/api/pkg/services/marketting_service.go b/api/pkg/services/marketting_service.go
index b2fc42109..aabc2d94a 100644
--- a/api/pkg/services/marketting_service.go
+++ b/api/pkg/services/marketting_service.go
@@ -11,8 +11,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
plunk "github.com/NdoleStudio/plunk-go"
+ "github.com/NdoleStudio/stacktrace"
"github.com/gofiber/fiber/v3"
- "github.com/palantir/stacktrace"
)
// MarketingService is handles marketing requests
diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go
index 21cfb1273..85627116e 100644
--- a/api/pkg/services/message_send_schedule_service.go
+++ b/api/pkg/services/message_send_schedule_service.go
@@ -10,8 +10,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// MessageSendScheduleService manages message send schedules for a user.
diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go
index 315c4d8f3..4e6c91128 100644
--- a/api/pkg/services/message_service.go
+++ b/api/pkg/services/message_service.go
@@ -13,9 +13,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/events"
"github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go
index e3dc882c7..687482ddd 100644
--- a/api/pkg/services/message_thread_service.go
+++ b/api/pkg/services/message_thread_service.go
@@ -11,8 +11,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// MessageThreadService is handles message requests
diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go
index fc1e84313..7825be755 100644
--- a/api/pkg/services/message_thread_service_test.go
+++ b/api/pkg/services/message_thread_service_test.go
@@ -8,8 +8,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go
index 796e42129..9762f116e 100644
--- a/api/pkg/services/phone_api_key_service.go
+++ b/api/pkg/services/phone_api_key_service.go
@@ -12,8 +12,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
// PhoneAPIKeyService is responsible for managing entities.PhoneAPIKey
diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go
index 6e7935fb0..aed806ef1 100644
--- a/api/pkg/services/phone_notification_service.go
+++ b/api/pkg/services/phone_notification_service.go
@@ -13,8 +13,8 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"go.opentelemetry.io/otel/trace"
)
diff --git a/api/pkg/services/phone_service.go b/api/pkg/services/phone_service.go
index 517925231..c61bd1210 100644
--- a/api/pkg/services/phone_service.go
+++ b/api/pkg/services/phone_service.go
@@ -12,7 +12,7 @@ import (
"github.com/nyaruka/phonenumbers"
"github.com/NdoleStudio/httpsms/pkg/repositories"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
diff --git a/api/pkg/services/service.go b/api/pkg/services/service.go
index f733d8b38..605921fa6 100644
--- a/api/pkg/services/service.go
+++ b/api/pkg/services/service.go
@@ -7,9 +7,9 @@ import (
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/nyaruka/phonenumbers"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
)
type service struct{}
diff --git a/api/pkg/services/user_service.go b/api/pkg/services/user_service.go
index bbd5ad463..265ad10fa 100644
--- a/api/pkg/services/user_service.go
+++ b/api/pkg/services/user_service.go
@@ -13,8 +13,8 @@ import (
"github.com/NdoleStudio/lemonsqueezy-go"
"github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
- "github.com/palantir/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
diff --git a/api/pkg/services/webhook_service.go b/api/pkg/services/webhook_service.go
index b8ff84900..581fa202d 100644
--- a/api/pkg/services/webhook_service.go
+++ b/api/pkg/services/webhook_service.go
@@ -21,11 +21,11 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/lib/pq"
- "github.com/palantir/stacktrace"
)
// WebhookService is responsible for handling webhooks
diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go
index 2a6ef3e9e..9107afcce 100644
--- a/api/pkg/telemetry/gorm_logger.go
+++ b/api/pkg/telemetry/gorm_logger.go
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"gorm.io/gorm/logger"
)
diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go
index 53a18a4e0..8b9b926d2 100644
--- a/api/pkg/validators/bulk_message_handler_validator.go
+++ b/api/pkg/validators/bulk_message_handler_validator.go
@@ -18,10 +18,10 @@ import (
"github.com/NdoleStudio/httpsms/pkg/requests"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
"github.com/dustin/go-humanize"
"github.com/jszwec/csvutil"
"github.com/nyaruka/phonenumbers"
- "github.com/palantir/stacktrace"
)
// BulkMessageHandlerValidator validates models used in handlers.BillingHandler
diff --git a/api/pkg/validators/discord_handler_validator.go b/api/pkg/validators/discord_handler_validator.go
index f9e486f45..035777ceb 100644
--- a/api/pkg/validators/discord_handler_validator.go
+++ b/api/pkg/validators/discord_handler_validator.go
@@ -6,7 +6,7 @@ import (
"net/url"
"github.com/NdoleStudio/httpsms/pkg/discord"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/requests"
diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go
index f48109c80..998b4d804 100644
--- a/api/pkg/validators/message_handler_validator.go
+++ b/api/pkg/validators/message_handler_validator.go
@@ -11,7 +11,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/cache"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/services"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/entities"
diff --git a/api/pkg/validators/turnstile_token_validator.go b/api/pkg/validators/turnstile_token_validator.go
index 8a4d65c68..5d30b4601 100644
--- a/api/pkg/validators/turnstile_token_validator.go
+++ b/api/pkg/validators/turnstile_token_validator.go
@@ -10,7 +10,7 @@ import (
"time"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
)
// TurnstileTokenValidator validates the token used to validate captchas from cloudflare
diff --git a/api/pkg/validators/user_handler_validator.go b/api/pkg/validators/user_handler_validator.go
index 31ec84023..ee163d821 100644
--- a/api/pkg/validators/user_handler_validator.go
+++ b/api/pkg/validators/user_handler_validator.go
@@ -9,7 +9,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/requests"
"github.com/NdoleStudio/httpsms/pkg/services"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/thedevsaddam/govalidator"
)
diff --git a/api/pkg/validators/webhook_handler_validator.go b/api/pkg/validators/webhook_handler_validator.go
index 88c7c5ff5..f7b6c101e 100644
--- a/api/pkg/validators/webhook_handler_validator.go
+++ b/api/pkg/validators/webhook_handler_validator.go
@@ -8,7 +8,7 @@ import (
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/services"
- "github.com/palantir/stacktrace"
+ "github.com/NdoleStudio/stacktrace"
"github.com/NdoleStudio/httpsms/pkg/requests"
From af57e73b5c7e4ab418d764c627803a8d41257893 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 15:29:49 +0300
Subject: [PATCH 05/88] fix(web): format billing message counts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 10c01eca-aeb5-4fc2-a546-02cee4261d2b
---
web/app/pages/billing/index.vue | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/web/app/pages/billing/index.vue b/web/app/pages/billing/index.vue
index 9235a0a5b..d06736e00 100644
--- a/web/app/pages/billing/index.vue
+++ b/web/app/pages/billing/index.vue
@@ -310,7 +310,10 @@ onMounted(async () => {
}}
- {{ totalMessages }}/{{ plan.messagesPerMonth }} messages
+ {{ formatDecimal(totalMessages) }}/{{
+ formatDecimal(plan.messagesPerMonth)
+ }}
+ messages
From 129dcedc9a43a490a564155f3332bca6d341dca7 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 18:51:44 +0300
Subject: [PATCH 06/88] refactor(api): use stacktrace formatting helpers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ebe8f80e-c697-4bb6-b456-e13081b5bd0f
---
api/go.mod | 2 +-
api/go.sum | 2 +
api/pkg/cache/memory_cache.go | 2 +-
api/pkg/cache/redis_cache.go | 6 +-
api/pkg/di/container.go | 74 +++++-----
.../hermes_notification_email_factory.go | 16 +--
api/pkg/emails/hermes_user_email_factory.go | 16 +--
api/pkg/emails/smtp_mailer_service.go | 2 +-
api/pkg/handlers/attachment_handler.go | 2 +-
api/pkg/handlers/billing_handler.go | 8 +-
api/pkg/handlers/bulk_message_handler.go | 10 +-
api/pkg/handlers/discord_handler.go | 40 +++---
api/pkg/handlers/events_handler.go | 8 +-
api/pkg/handlers/heartbeat_handler.go | 14 +-
api/pkg/handlers/integration_3cx_handler.go | 6 +-
api/pkg/handlers/lemonsqueezy_handler.go | 14 +-
api/pkg/handlers/message_handler.go | 74 +++++-----
.../handlers/message_send_schedule_handler.go | 14 +-
api/pkg/handlers/message_thread_handler.go | 18 +--
.../handlers/message_thread_handler_test.go | 2 +-
api/pkg/handlers/phone_api_key_handler.go | 22 +--
api/pkg/handlers/phone_handler.go | 22 +--
api/pkg/handlers/user_handler.go | 30 ++--
api/pkg/handlers/webhook_handler.go | 26 ++--
api/pkg/listeners/billing_listener.go | 12 +-
api/pkg/listeners/discord_listener.go | 8 +-
.../listeners/email_notification_listener.go | 16 +--
api/pkg/listeners/heartbeat_listener.go | 20 +--
api/pkg/listeners/integration_3cx_listener.go | 20 +--
api/pkg/listeners/marketing_listener.go | 8 +-
api/pkg/listeners/message_listener.go | 52 +++----
.../message_send_schedule_listener.go | 4 +-
api/pkg/listeners/message_thread_listener.go | 44 +++---
api/pkg/listeners/phone_api_key_listener.go | 12 +-
api/pkg/listeners/phone_listener.go | 8 +-
.../listeners/phone_notification_listener.go | 24 ++--
api/pkg/listeners/user_listener.go | 28 ++--
api/pkg/listeners/webhook_listener.go | 36 ++---
api/pkg/listeners/websocket_listener.go | 20 +--
.../middlewares/api_key_auth_middleware.go | 2 +-
.../bearer_api_key_auth_middleware.go | 2 +-
api/pkg/middlewares/bearer_auth_middleware.go | 2 +-
.../http_request_logger_middleware.go | 2 +-
.../phone_api_key_auth_middleware.go | 2 +-
...gle_cloud_storage_attachment_repository.go | 12 +-
.../gorm_billing_usage_repository.go | 8 +-
.../repositories/gorm_discord_repository.go | 18 +--
.../gorm_heartbeat_monitor_repository.go | 18 +--
.../repositories/gorm_heartbeat_repository.go | 10 +-
.../gorm_integration_3cx_repository.go | 8 +-
.../repositories/gorm_message_repository.go | 30 ++--
.../gorm_message_send_schedule_repository.go | 16 +--
.../gorm_message_thread_repository.go | 26 ++--
.../gorm_phone_api_key_repository.go | 30 ++--
.../gorm_phone_notification_repository.go | 18 +--
api/pkg/repositories/gorm_phone_repository.go | 22 +--
api/pkg/repositories/gorm_user_repository.go | 38 ++---
.../repositories/gorm_webhook_repository.go | 14 +-
.../memory_attachment_repository.go | 2 +-
.../mongo_heartbeat_monitor_repository.go | 16 +--
.../mongo_heartbeat_repository.go | 12 +-
api/pkg/repositories/mongodb.go | 16 +--
api/pkg/services/billing_service.go | 24 ++--
api/pkg/services/discord_service.go | 30 ++--
.../services/email_notification_service.go | 26 ++--
api/pkg/services/emulator_fcm_client.go | 12 +-
api/pkg/services/emulator_push_queue.go | 2 +-
api/pkg/services/entitlement_service.go | 4 +-
api/pkg/services/event_dispatcher_service.go | 14 +-
.../google_cloud_push_queue_service.go | 4 +-
api/pkg/services/heartbeat_service.go | 48 +++----
api/pkg/services/integration_3cx_service.go | 24 ++--
api/pkg/services/lemonsqueezy_service.go | 28 ++--
api/pkg/services/marketting_service.go | 8 +-
.../services/message_send_schedule_service.go | 12 +-
api/pkg/services/message_service.go | 130 +++++++++---------
api/pkg/services/message_thread_service.go | 32 ++---
.../services/message_thread_service_test.go | 4 +-
api/pkg/services/phone_api_key_service.go | 30 ++--
.../services/phone_notification_service.go | 44 +++---
api/pkg/services/phone_service.go | 28 ++--
api/pkg/services/service.go | 6 +-
api/pkg/services/user_service.go | 76 +++++-----
api/pkg/services/webhook_service.go | 40 +++---
api/pkg/telemetry/gorm_logger.go | 2 +-
.../bulk_message_handler_validator.go | 16 +--
.../validators/discord_handler_validator.go | 8 +-
.../validators/message_handler_validator.go | 4 +-
.../validators/turnstile_token_validator.go | 10 +-
api/pkg/validators/user_handler_validator.go | 2 +-
90 files changed, 868 insertions(+), 866 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 649489f65..094bb1ac4 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -12,7 +12,7 @@ require (
github.com/NdoleStudio/go-otelroundtripper v0.0.15
github.com/NdoleStudio/lemonsqueezy-go v1.3.1
github.com/NdoleStudio/plunk-go v0.0.2
- github.com/NdoleStudio/stacktrace v1.0.0
+ github.com/NdoleStudio/stacktrace v1.1.0
github.com/avast/retry-go/v5 v5.0.0
github.com/axiomhq/axiom-go v0.32.0
github.com/carlmjohnson/requests v0.25.1
diff --git a/api/go.sum b/api/go.sum
index 264094f1a..d0272fa1d 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -62,6 +62,8 @@ github.com/NdoleStudio/plunk-go v0.0.2 h1:afPW7MHK4Z3rsybpJBnmTmxKCLKF1M7sPI+BNG
github.com/NdoleStudio/plunk-go v0.0.2/go.mod h1:pqG3zKhpn/A2bL1K+WsWzvfTpOeSkYgXhNk5H65uEc8=
github.com/NdoleStudio/stacktrace v1.0.0 h1:YIDg0twM2BXRKIFjorX7v2dC0l8z4VEITurI+MNqvVQ=
github.com/NdoleStudio/stacktrace v1.0.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg=
+github.com/NdoleStudio/stacktrace v1.1.0 h1:Ai0j//4tDo/So3nR8bZgS3pQ4NYmViHlarpIReiyb5k=
+github.com/NdoleStudio/stacktrace v1.1.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg=
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=
diff --git a/api/pkg/cache/memory_cache.go b/api/pkg/cache/memory_cache.go
index e749ca3e3..a5b0487d7 100644
--- a/api/pkg/cache/memory_cache.go
+++ b/api/pkg/cache/memory_cache.go
@@ -30,7 +30,7 @@ func (cache *memoryCache) Get(ctx context.Context, key string) (value string, er
response, ok := cache.store.Get(key)
if !ok {
- return "", stacktrace.NewError("no item found in cache with key [%s]", key)
+ return "", stacktrace.NewErrorf("no item found in cache with key [%s]", key)
}
return response.(string), nil
diff --git a/api/pkg/cache/redis_cache.go b/api/pkg/cache/redis_cache.go
index 990f09bcd..46d9044bd 100644
--- a/api/pkg/cache/redis_cache.go
+++ b/api/pkg/cache/redis_cache.go
@@ -31,10 +31,10 @@ func (cache *redisCache) Get(ctx context.Context, key string) (value string, err
response, err := cache.client.Get(ctx, key).Result()
if errors.Is(err, redis.Nil) {
- return "", stacktrace.Propagate(err, "no item found in redis with key [%s]", key)
+ return "", stacktrace.Propagatef(err, "no item found in redis with key [%s]", key)
}
if err != nil {
- return "", stacktrace.Propagate(err, "cannot get item in redis with key [%s]", key)
+ return "", stacktrace.Propagatef(err, "cannot get item in redis with key [%s]", key)
}
return response, nil
}
@@ -46,7 +46,7 @@ func (cache *redisCache) Set(ctx context.Context, key string, value string, ttl
err := cache.client.Set(ctx, key, value, ttl).Err()
if err != nil {
- return cache.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot set item in redis"))
+ return cache.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot set item in redis"))
}
return nil
}
diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go
index 6d933ae86..ad9a18858 100644
--- a/api/pkg/di/container.go
+++ b/api/pkg/di/container.go
@@ -268,7 +268,7 @@ func (container *Container) DedicatedDB() (db *gorm.DB) {
}
if err = db.Use(tracing.NewPlugin()); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin"))
}
container.dedicatedDB = db
@@ -279,11 +279,11 @@ func (container *Container) DedicatedDB() (db *gorm.DB) {
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, "cannot migrate %T", &entities.Heartbeat{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Heartbeat{}))
}
if err = db.AutoMigrate(&entities.HeartbeatMonitor{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.HeartbeatMonitor{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.HeartbeatMonitor{}))
}
return container.dedicatedDB
@@ -326,7 +326,7 @@ func (container *Container) DBWithoutMigration() (db *gorm.DB) {
container.db = db
if err = db.Use(tracing.NewPlugin()); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin"))
}
return container.db
}
@@ -351,7 +351,7 @@ func (container *Container) DB() (db *gorm.DB) {
container.db = db
if err = db.Use(tracing.NewPlugin()); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin"))
}
if os.Getenv("DATABASE_MIGRATION_SKIP") != "" {
@@ -371,47 +371,47 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK (
}
if err = db.AutoMigrate(&entities.Message{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Message{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Message{}))
}
if err = db.AutoMigrate(&entities.MessageThread{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.MessageThread{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageThread{}))
}
if err = db.AutoMigrate(&entities.User{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.User{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.User{}))
}
if err = db.AutoMigrate(&entities.MessageSendSchedule{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.MessageSendSchedule{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageSendSchedule{}))
}
if err = db.AutoMigrate(&entities.Phone{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Phone{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Phone{}))
}
if err = db.AutoMigrate(&entities.PhoneNotification{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.PhoneNotification{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneNotification{}))
}
if err = db.AutoMigrate(&entities.BillingUsage{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.BillingUsage{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.BillingUsage{}))
}
if err = db.AutoMigrate(&entities.Webhook{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Webhook{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Webhook{}))
}
if err = db.AutoMigrate(&entities.Discord{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Discord{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Discord{}))
}
if err = db.AutoMigrate(&entities.Integration3CX{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Integration3CX{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Integration3CX{}))
}
if err = db.AutoMigrate(&entities.PhoneAPIKey{}); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.PhoneAPIKey{}))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneAPIKey{}))
}
return container.db
@@ -423,7 +423,7 @@ func (container *Container) FirebaseApp() (app *firebase.App) {
app, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsJSON(container.FirebaseCredentials()))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase application"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase application"))
}
return app
}
@@ -444,7 +444,7 @@ func (container *Container) Cache() cache.Cache {
container.logger.Debug("creating cache.Cache")
opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot parse redis url [%s]", os.Getenv("REDIS_URL")))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot parse redis url [%s]", os.Getenv("REDIS_URL")))
}
if strings.HasPrefix(os.Getenv("REDIS_URL"), "rediss://") {
opt.TLSConfig = &tls.Config{
@@ -456,12 +456,12 @@ func (container *Container) Cache() cache.Cache {
// Enable tracing instrumentation.
if err = redisotel.InstrumentTracing(redisClient); err != nil {
- container.logger.Error(stacktrace.Propagate(err, "cannot instrument redis tracing"))
+ container.logger.Error(stacktrace.Propagatef(err, "cannot instrument redis tracing"))
}
// Enable metrics instrumentation.
if err = redisotel.InstrumentMetrics(redisClient); err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot instrument redis metrics"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot instrument redis metrics"))
}
return cache.NewRedisCache(container.Tracer(), redisClient)
@@ -472,7 +472,7 @@ func (container *Container) FirebaseAuthClient() (client *auth.Client) {
container.logger.Debug(fmt.Sprintf("creating %T", client))
authClient, err := container.FirebaseApp().Auth(context.Background())
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase auth client"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase auth client"))
}
return authClient
}
@@ -483,7 +483,7 @@ func (container *Container) CloudTasksClient() (client *cloudtasks.Client) {
client, err := cloudtasks.NewClient(context.Background(), option.WithCredentialsJSON(container.FirebaseCredentials()))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize cloud tasks client"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize cloud tasks client"))
}
return client
@@ -551,7 +551,7 @@ func (container *Container) FCMClient() services.FCMClient {
container.logger.Debug("creating FirebaseFCMClient")
messagingClient, err := container.FirebaseApp().Messaging(context.Background())
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase messaging client"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase messaging client"))
}
return services.NewFirebaseFCMClient(messagingClient)
}
@@ -745,7 +745,7 @@ func (container *Container) Float64Histogram(name, unit, description string) ote
)
histogram, err := meter.Float64Histogram(name, otelMetric.WithUnit(unit), otelMetric.WithDescription(description))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create float64 histogram"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create float64 histogram"))
}
return histogram
}
@@ -1525,7 +1525,7 @@ func (container *Container) RegisterWebsocketListeners() {
container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.WebsocketListener{}))
if os.Getenv("PUSHER_SECRET") == "" {
- container.logger.Warn(stacktrace.NewError("skipping websocket listeners because the PUSHER_SECRET env variable is not set"))
+ container.logger.Warn(stacktrace.NewErrorf("skipping websocket listeners because the PUSHER_SECRET env variable is not set"))
return
}
@@ -1579,7 +1579,7 @@ func (container *Container) AttachmentRepository() repositories.AttachmentReposi
container.logger.Debug("creating GoogleCloudStorageAttachmentRepository")
client, err := storage.NewClient(context.Background(), option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials()))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create GCS client"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create GCS client"))
}
container.attachmentRepository = repositories.NewGoogleCloudStorageAttachmentRepository(
container.Logger(),
@@ -1767,7 +1767,7 @@ func (container *Container) PhoneRistrettoCache() *ristretto.Cache[string, *enti
BufferItems: 64,
})
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create phone ristretto cache"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create phone ristretto cache"))
}
container.phoneRistrettoCache = ristrettoCache
return container.phoneRistrettoCache
@@ -1785,7 +1785,7 @@ func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entiti
BufferItems: 64,
})
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create user ristretto cache"))
}
container.userRistrettoCache = ristrettoCache
return ristrettoCache
@@ -1801,7 +1801,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp
traceExporter, err := cloudtrace.New(cloudtrace.WithProjectID(os.Getenv("GCP_PROJECT_ID")))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud trace traceExporter"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud trace traceExporter"))
}
tp := trace.NewTracerProvider(
@@ -1813,7 +1813,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp
metricExporter, err := mexporter.New(mexporter.WithProjectID(os.Getenv("GCP_PROJECT_ID")))
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud metric traceExporter"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud metric traceExporter"))
}
meterProvider := metric.NewMeterProvider(
@@ -1824,10 +1824,10 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp
return func() {
if err = metricExporter.Shutdown(context.Background()); err != nil {
- container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud metric metric exporter"))
+ container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud metric metric exporter"))
}
if err = traceExporter.Shutdown(context.Background()); err != nil {
- container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud trace trace exporter"))
+ container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud trace trace exporter"))
}
}
}
@@ -1846,7 +1846,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa
otlptracehttp.WithHeaders(traceHeaders),
)
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP trace exporter"))
}
tp := trace.NewTracerProvider(
@@ -1872,7 +1872,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa
otlpmetrichttp.WithHeaders(metricHeaders),
)
if err != nil {
- container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter"))
+ container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP metric exporter"))
}
meterProvider := metric.NewMeterProvider(
@@ -1883,10 +1883,10 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa
return func() {
if err := tp.Shutdown(context.Background()); err != nil {
- container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom trace provider"))
+ container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom trace provider"))
}
if err := meterProvider.Shutdown(context.Background()); err != nil {
- container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom meter provider"))
+ container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom meter provider"))
}
}
}
@@ -1937,7 +1937,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger {
axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")),
)
if err != nil {
- log.Fatal(stacktrace.Propagate(err, "cannot create axiom zerolog writer"))
+ log.Fatal(stacktrace.Propagatef(err, "cannot create axiom zerolog writer"))
}
zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger()
diff --git a/api/pkg/emails/hermes_notification_email_factory.go b/api/pkg/emails/hermes_notification_email_factory.go
index 4c0440646..d80515e72 100644
--- a/api/pkg/emails/hermes_notification_email_factory.go
+++ b/api/pkg/emails/hermes_notification_email_factory.go
@@ -62,12 +62,12 @@ func (factory *hermesNotificationEmailFactory) DiscordSendFailed(user *entities.
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -120,7 +120,7 @@ func (factory *hermesNotificationEmailFactory) WebhookSendFailed(user *entities.
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
eventPayloadEntryIndex := len(email.Body.Dictionary) - 1
@@ -129,7 +129,7 @@ func (factory *hermesNotificationEmailFactory) WebhookSendFailed(user *entities.
return factory.generator.GeneratePlainText(email)
}, formattedPayload)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -198,12 +198,12 @@ func (factory *hermesNotificationEmailFactory) MessageExpired(user *entities.Use
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -249,12 +249,12 @@ func (factory *hermesNotificationEmailFactory) MessageFailed(user *entities.User
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go
index 8070a78e7..7f2ef25cb 100644
--- a/api/pkg/emails/hermes_user_email_factory.go
+++ b/api/pkg/emails/hermes_user_email_factory.go
@@ -52,12 +52,12 @@ func (factory *hermesUserEmailFactory) APIKeyRotated(emailAddress string, timest
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -97,12 +97,12 @@ func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User, u
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -143,12 +143,12 @@ func (factory *hermesUserEmailFactory) UsageLimitAlert(user *entities.User, usag
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
@@ -201,12 +201,12 @@ func (factory *hermesUserEmailFactory) PhoneDead(user *entities.User, lastHeartb
html, err := factory.generator.GenerateHTML(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate html email")
+ return nil, stacktrace.Propagatef(err, "cannot generate html email")
}
text, err := factory.generator.GeneratePlainText(email)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate text email")
+ return nil, stacktrace.Propagatef(err, "cannot generate text email")
}
return &Email{
diff --git a/api/pkg/emails/smtp_mailer_service.go b/api/pkg/emails/smtp_mailer_service.go
index 5f57d1daf..ffe18432f 100644
--- a/api/pkg/emails/smtp_mailer_service.go
+++ b/api/pkg/emails/smtp_mailer_service.go
@@ -51,7 +51,7 @@ func (mailer *smtpMailer) Send(ctx context.Context, email *Email) (err error) {
err = e.Send(mailer.address, mailer.auth)
if err != nil {
- return stacktrace.Propagate(err, "cannot send email")
+ return stacktrace.Propagatef(err, "cannot send email")
}
return nil
diff --git a/api/pkg/handlers/attachment_handler.go b/api/pkg/handlers/attachment_handler.go
index e73f12530..696521b61 100644
--- a/api/pkg/handlers/attachment_handler.go
+++ b/api/pkg/handlers/attachment_handler.go
@@ -66,7 +66,7 @@ func (h *AttachmentHandler) GetAttachment(c fiber.Ctx) error {
data, err := h.storage.Download(ctx, path)
if err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot download attachment from path [%s]", path))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot download attachment from path [%s]", path))
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
return h.responseNotFound(c, "attachment not found")
}
diff --git a/api/pkg/handlers/billing_handler.go b/api/pkg/handlers/billing_handler.go
index a33ad2312..bcbdef4ee 100644
--- a/api/pkg/handlers/billing_handler.go
+++ b/api/pkg/handlers/billing_handler.go
@@ -65,18 +65,18 @@ func (h *BillingHandler) UsageHistory(c fiber.Ctx) error {
var request requests.BillingUsageHistory
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateHistory(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching usage history")
}
heartbeats, err := h.service.GetUsageHistory(ctx, h.userIDFomContext(c), request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get billing usage history with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get billing usage history with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -104,7 +104,7 @@ func (h *BillingHandler) Usage(c fiber.Ctx) error {
billingUsage, err := h.service.GetCurrentUsage(ctx, h.userIDFomContext(c))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get current usage record for user [%s]", h.userFromContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get current usage record for user [%s]", h.userFromContext(c)))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go
index 28e0d0ad4..ef087afbd 100644
--- a/api/pkg/handlers/bulk_message_handler.go
+++ b/api/pkg/handlers/bulk_message_handler.go
@@ -67,7 +67,7 @@ func (h *BulkMessageHandler) Index(c fiber.Ctx) error {
orders, err := h.messageService.GetBulkMessages(ctx, h.userIDFomContext(c))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot fetch bulk messages for user [%s]", h.userIDFomContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot fetch bulk messages for user [%s]", h.userIDFomContext(c)))
return h.responseInternalServerError(c)
}
@@ -94,18 +94,18 @@ func (h *BulkMessageHandler) Store(c fiber.Ctx) error {
file, err := c.FormFile("document")
if err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot fetch file with name [%s] from request", "document"))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot fetch file with name [%s] from request", "document"))
return h.responseBadRequest(c, err)
}
messages, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file)
if len(validationErrors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c)))
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("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages)))
return h.responsePaymentRequired(c, *msg)
}
@@ -133,7 +133,7 @@ func (h *BulkMessageHandler) Store(c fiber.Ctx) error {
if err != nil {
count.Add(-1)
- ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index))
}
wg.Done()
}(message, perPhoneIndex)
diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go
index 582cdb325..6f7403f14 100644
--- a/api/pkg/handlers/discord_handler.go
+++ b/api/pkg/handlers/discord_handler.go
@@ -88,18 +88,18 @@ func (h *DiscordHandler) Index(c fiber.Ctx) error {
var request requests.DiscordIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching discord integrations [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching discord integrations [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching discord integrations")
}
discordIntegrations, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get discord integrations with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get discord integrations with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -126,13 +126,13 @@ func (h *DiscordHandler) Delete(c fiber.Ctx) error {
discordID := c.Params("discordID")
if errors := h.validator.ValidateUUID(discordID, "discordID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID))
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting discord integration")
}
err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(discordID))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete discord integration with ID [%+#v]", discordID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete discord integration with ID [%+#v]", discordID))
return h.responseInternalServerError(c)
}
@@ -160,19 +160,19 @@ func (h *DiscordHandler) Update(c fiber.Ctx) error {
var request requests.DiscordUpdate
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into [%T]", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into [%T]", c.Body(), request))
return h.responseBadRequest(c, err)
}
request.DiscordID = c.Params("discordID")
if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating discord integration")
}
user, err := h.service.Update(ctx, request.ToUpdateParams(h.userFromContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update discord integration with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update discord integration with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -201,29 +201,29 @@ func (h *DiscordHandler) Store(c fiber.Ctx) error {
var request requests.DiscordStore
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall body [%s] into [%T]", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall body [%s] into [%T]", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing discord integration [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing discord integration [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing discord integration")
}
discordIntegrations, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 1})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot index discord integrations for user [%s]", h.userIDFomContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot index discord integrations for user [%s]", h.userIDFomContext(c)))
return h.responseInternalServerError(c)
}
if len(discordIntegrations) > 0 {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] wants to create more than 1 discord integration", h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] wants to create more than 1 discord integration", h.userIDFomContext(c)))
return h.responsePaymentRequired(c, "You can't create more than 1 discord integration contact us to upgrade your account.")
}
discordIntegration, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot store discord integration with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot store discord integration with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -252,7 +252,7 @@ func (h *DiscordHandler) Event(c fiber.Ctx) error {
var payload map[string]any
if err := json.Unmarshal(c.Body(), &payload); err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot unmarshall [%s] to [%T]", string(c.Body()), payload)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot unmarshall [%s] to [%T]", string(c.Body()), payload)))
return h.responseBadRequest(c, err)
}
@@ -266,7 +266,7 @@ func (h *DiscordHandler) Event(c fiber.Ctx) error {
return h.sendSMS(ctx, c, payload)
}
- return h.responseBadRequest(c, stacktrace.NewError("unknown type [%d]", payload["type"]))
+ return h.responseBadRequest(c, stacktrace.NewErrorf("unknown type [%d]", payload["type"]))
}
func (h *DiscordHandler) createRequest(payload map[string]any) requests.MessageSend {
@@ -291,7 +291,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s
discord, err := h.service.GetByServerID(ctx, payload["guild_id"].(string))
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get discord integration by server ID [%s]", payload["guild_id"].(string))))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get discord integration by server ID [%s]", payload["guild_id"].(string))))
return c.JSON(
fiber.Map{
"type": 4,
@@ -329,7 +329,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s
}
if errors := h.messageValidator.ValidateMessageSend(ctx, discord.UserID, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
var embeds []fiber.Map
for _, value := range errors {
@@ -351,7 +351,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s
}
if msg := h.billingService.IsEntitled(ctx, discord.UserID); msg != nil {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a message", discord.UserID))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a message", discord.UserID))
return c.JSON(
fiber.Map{
"type": 4,
@@ -370,7 +370,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s
message, err := h.messageService.SendMessage(ctx, request.ToMessageSendParams(discord.UserID, c.OriginalURL()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s] from discord server [%s]", c.Body(), discord.ServerID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s] from discord server [%s]", c.Body(), discord.ServerID))
return c.JSON(
fiber.Map{
"type": 4,
@@ -436,7 +436,7 @@ func (h *DiscordHandler) verifyInteraction(ctxLogger telemetry.Logger, c fiber.C
key, err := hex.DecodeString(os.Getenv("DISCORD_PUBLIC_KEY"))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot decode DISCORD_PUBLIC_KEY env variable [%s]", os.Getenv("DISCORD_PUBLIC_KEY")))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot decode DISCORD_PUBLIC_KEY env variable [%s]", os.Getenv("DISCORD_PUBLIC_KEY")))
return false
}
diff --git a/api/pkg/handlers/events_handler.go b/api/pkg/handlers/events_handler.go
index 5d4fc121b..5e856cc70 100644
--- a/api/pkg/handlers/events_handler.go
+++ b/api/pkg/handlers/events_handler.go
@@ -49,24 +49,24 @@ func (h *EventsHandler) Dispatch(c fiber.Ctx) error {
var request cloudevents.Event
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if err := request.Validate(); err != nil {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while dispatching event [%+#v]", spew.Sdump(err.Error()), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while dispatching event [%+#v]", spew.Sdump(err.Error()), request))
return h.responseUnprocessableEntity(c, map[string][]string{"event": {err.Error()}}, "validation errors while dispatching event")
}
if h.userIDFomContext(c) != h.queueConfig.UserID {
- ctxLogger.Error(stacktrace.NewError("user with ID [%s], cannot dispatch event [%+#v]", h.userIDFomContext(c), request))
+ ctxLogger.Error(stacktrace.NewErrorf("user with ID [%s], cannot dispatch event [%+#v]", h.userIDFomContext(c), request))
return h.responseForbidden(c)
}
ctxLogger.Info(fmt.Sprintf("handling [%s] event with ID [%s]", request.Type(), request.ID()))
err := h.service.DispatchSync(ctx, request)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event with ID [%s]", request.Type(), request.ID()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event with ID [%s]", request.Type(), request.ID()))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/heartbeat_handler.go b/api/pkg/handlers/heartbeat_handler.go
index 663d16af3..660bdeed4 100644
--- a/api/pkg/handlers/heartbeat_handler.go
+++ b/api/pkg/handlers/heartbeat_handler.go
@@ -74,18 +74,18 @@ func (h *HeartbeatHandler) Index(c fiber.Ctx) error {
var request requests.HeartbeatIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching heartbeats")
}
heartbeats, err := h.service.Index(ctx, h.userIDFomContext(c), request.Owner, request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get messgaes with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get messgaes with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -114,18 +114,18 @@ func (h *HeartbeatHandler) Store(c fiber.Ctx) error {
var request requests.HeartbeatStore
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing heartbeat [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing heartbeat [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing heartbeat")
}
for _, phoneNumber := range request.PhoneNumbers {
if !h.authorizePhoneAPIKey(c, phoneNumber) {
- ctxLogger.Warn(stacktrace.NewError("phone API Key ID [%s] is not authorized to store heartbeat for phone number [%s]", h.userFromContext(c).PhoneAPIKeyID, phoneNumber))
+ ctxLogger.Warn(stacktrace.NewErrorf("phone API Key ID [%s] is not authorized to store heartbeat for phone number [%s]", h.userFromContext(c).PhoneAPIKeyID, phoneNumber))
return h.responsePhoneAPIKeyUnauthorized(c, phoneNumber, h.userFromContext(c))
}
}
@@ -139,7 +139,7 @@ func (h *HeartbeatHandler) Store(c fiber.Ctx) error {
go func(input services.HeartbeatStoreParams, index int) {
response, err := h.service.Store(ctx, input)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot store heartbeat with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot store heartbeat with params [%+#v]", request))
}
responses[index] = response
wg.Done()
diff --git a/api/pkg/handlers/integration_3cx_handler.go b/api/pkg/handlers/integration_3cx_handler.go
index db06e64df..63c88bffe 100644
--- a/api/pkg/handlers/integration_3cx_handler.go
+++ b/api/pkg/handlers/integration_3cx_handler.go
@@ -62,19 +62,19 @@ func (h *Integration3CXHandler) Messages(c fiber.Ctx) error {
var request requests.Integration3CXMessage
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a [3cx] message", h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a [3cx] message", h.userIDFomContext(c)))
return h.responsePaymentRequired(c, *msg)
}
request.Sanitize()
message, err := h.messageService.SendMessage(ctx, request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot send [3cx] message with payload [%s]", c.Body()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot send [3cx] message with payload [%s]", c.Body()))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/lemonsqueezy_handler.go b/api/pkg/handlers/lemonsqueezy_handler.go
index 1b18d8baa..9147cd311 100644
--- a/api/pkg/handlers/lemonsqueezy_handler.go
+++ b/api/pkg/handlers/lemonsqueezy_handler.go
@@ -51,12 +51,12 @@ func (h *LemonsqueezyHandler) Event(c fiber.Ctx) error {
signature := c.Get("X-Signature")
if errors := h.validator.ValidateEvent(ctx, signature, c.Body()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing request [%s] and signature [%s]", spew.Sdump(errors), c.Body(), signature))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing request [%s] and signature [%s]", spew.Sdump(errors), c.Body(), signature))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing lemonsqueezy event")
}
if err := h.handleRequest(ctx, c); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot handle lemonsqueezy event [%s]", c.Body()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot handle lemonsqueezy event [%s]", c.Body()))
return h.responseInternalServerError(c)
}
@@ -70,31 +70,31 @@ func (h *LemonsqueezyHandler) handleRequest(ctx context.Context, c fiber.Ctx) er
var request lemonsqueezy.WebhookRequestSubscription
err := json.Unmarshal(c.Body(), &request)
if err != nil {
- return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request)
+ return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request)
}
return h.service.HandleSubscriptionCreatedEvent(ctx, c.OriginalURL(), &request)
case "subscription_cancelled":
var request lemonsqueezy.WebhookRequestSubscription
err := json.Unmarshal(c.Body(), &request)
if err != nil {
- return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request)
+ return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request)
}
return h.service.HandleSubscriptionCanceledEvent(ctx, c.OriginalURL(), &request)
case "subscription_expired":
var request lemonsqueezy.WebhookRequestSubscription
err := json.Unmarshal(c.Body(), &request)
if err != nil {
- return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request)
+ return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request)
}
return h.service.HandleSubscriptionExpiredEvent(ctx, c.OriginalURL(), &request)
case "subscription_updated":
var request lemonsqueezy.WebhookRequestSubscription
err := json.Unmarshal(c.Body(), &request)
if err != nil {
- return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request)
+ return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request)
}
return h.service.HandleSubscriptionUpdatedEvent(ctx, c.OriginalURL(), &request)
default:
- return stacktrace.NewError("invalid event [%s] received with request [%s]", eventName, c.Body())
+ return stacktrace.NewErrorf("invalid event [%s] received with request [%s]", eventName, c.Body())
}
}
diff --git a/api/pkg/handlers/message_handler.go b/api/pkg/handlers/message_handler.go
index 14a4e95e5..8c7a1420a 100644
--- a/api/pkg/handlers/message_handler.go
+++ b/api/pkg/handlers/message_handler.go
@@ -88,23 +88,23 @@ func (h *MessageHandler) PostSend(c fiber.Ctx) error {
var request requests.MessageSend
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageSend(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
return h.responseUnprocessableEntity(c, errors, "validation errors while sending message")
}
if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a message", h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a message", h.userIDFomContext(c)))
return h.responsePaymentRequired(c, *msg)
}
message, err := h.service.SendMessage(ctx, request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s]", c.Body()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s]", c.Body()))
return h.responseInternalServerError(c)
}
@@ -133,17 +133,17 @@ func (h *MessageHandler) BulkSend(c fiber.Ctx) error {
var request requests.MessageBulkSend
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageBulkSend(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
return h.responseUnprocessableEntity(c, errors, "validation errors while sending messages")
}
if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(request.To))); msg != nil {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(request.To)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(request.To)))
return h.responsePaymentRequired(c, *msg)
}
@@ -160,7 +160,7 @@ func (h *MessageHandler) BulkSend(c fiber.Ctx) error {
if err != nil {
count.Add(-1)
- ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index))
}
responses[index] = response
wg.Done()
@@ -194,24 +194,24 @@ func (h *MessageHandler) GetOutstanding(c fiber.Ctx) error {
var request requests.MessageOutstanding
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageOutstanding(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching outstanding messages [%s]", spew.Sdump(errors), c.OriginalURL()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching outstanding messages [%s]", spew.Sdump(errors), c.OriginalURL()))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching outstanding messages")
}
message, err := h.service.GetOutstanding(ctx, request.ToGetOutstandingParams(c.Path(), h.userFromContext(c), timestamp))
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
msg := fmt.Sprintf("Cannot find outstanding message with ID [%s]", request.MessageID)
- ctxLogger.Warn(stacktrace.Propagate(err, "Cannot find outstanding message with ID [%s]", request.MessageID))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "Cannot find outstanding message with ID [%s]", request.MessageID))
return h.responseNotFound(c, msg)
}
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get outstanding messgage with ID [%s]", request.MessageID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get outstanding messgage with ID [%s]", request.MessageID))
return h.responseInternalServerError(c)
}
@@ -244,18 +244,18 @@ func (h *MessageHandler) Index(c fiber.Ctx) error {
var request requests.MessageIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching messages [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching messages [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching messages")
}
messages, err := h.service.GetMessages(ctx, request.ToGetParams(h.userIDFomContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get messages with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get messages with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -286,7 +286,7 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error {
var request requests.MessageEvent
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
@@ -296,7 +296,7 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error {
}
if errors := h.validator.ValidateMessageEvent(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing event [%s] for message [%s]", spew.Sdump(errors), c.Body(), request.MessageID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing event [%s] for message [%s]", spew.Sdump(errors), c.Body(), request.MessageID))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
}
@@ -306,18 +306,18 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", request.MessageID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", request.MessageID)))
return h.responseInternalServerError(c)
}
if !h.authorizePhoneAPIKey(c, message.Owner) {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to send event for message with ID [%s]", h.userIDFomContext(c), request.MessageID))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to send event for message with ID [%s]", h.userIDFomContext(c), request.MessageID))
return h.responsePhoneAPIKeyUnauthorized(c, message.Owner, h.userFromContext(c))
}
message, err = h.service.StoreEvent(ctx, message, request.ToMessageStoreEventParams(c.OriginalURL()))
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store event for message [%s] with paylod [%s]", request.MessageID, c.Body())))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store event for message [%s] with paylod [%s]", request.MessageID, c.Body())))
return h.responseInternalServerError(c)
}
@@ -345,28 +345,28 @@ func (h *MessageHandler) PostReceive(c fiber.Ctx) error {
var request requests.MessageReceive
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageReceive(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body()))
return h.responseUnprocessableEntity(c, errors, "validation errors while receiving message")
}
if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't receive a message becasuse they have exceeded the limit", h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't receive a message becasuse they have exceeded the limit", h.userIDFomContext(c)))
return h.responsePaymentRequired(c, *msg)
}
if !h.authorizePhoneAPIKey(c, request.To) {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to receive message to phone number [%s]", h.userIDFomContext(c), request.To))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to receive message to phone number [%s]", h.userIDFomContext(c), request.To))
return h.responsePhoneAPIKeyUnauthorized(c, request.To, h.userFromContext(c))
}
message, err := h.service.ReceiveMessage(ctx, request.ToMessageReceiveParams(h.userIDFomContext(c), c.OriginalURL()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot receive message with payload [%s]", c.Body()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot receive message with payload [%s]", c.Body()))
return h.responseInternalServerError(c)
}
@@ -396,7 +396,7 @@ func (h *MessageHandler) Delete(c fiber.Ctx) error {
messageID := c.Params("messageID")
if errors := h.validator.ValidateUUID(messageID, "messageID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
}
@@ -406,12 +406,12 @@ func (h *MessageHandler) Delete(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", messageID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", messageID)))
return h.responseInternalServerError(c)
}
if err = h.service.DeleteMessage(ctx, c.OriginalURL(), message); err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, message.UserID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, message.UserID)))
return h.responseInternalServerError(c)
}
@@ -441,7 +441,7 @@ func (h *MessageHandler) Get(c fiber.Ctx) error {
messageID := c.Params("messageID")
if errors := h.validator.ValidateUUID(messageID, "messageID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
}
@@ -451,7 +451,7 @@ func (h *MessageHandler) Get(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", messageID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", messageID)))
return h.responseInternalServerError(c)
}
@@ -481,23 +481,23 @@ func (h *MessageHandler) PostCallMissed(c fiber.Ctx) error {
var request requests.MessageCallMissed
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateCallMissed(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], for missed call event [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], for missed call event [%s]", spew.Sdump(errors), c.Body()))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing missed call event")
}
if !h.authorizePhoneAPIKey(c, request.To) {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to register missed phone call for phone number [%s]", h.userIDFomContext(c), request.To))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to register missed phone call for phone number [%s]", h.userIDFomContext(c), request.To))
return h.responsePhoneAPIKeyUnauthorized(c, request.To, h.userFromContext(c))
}
message, err := h.service.RegisterMissedCall(ctx, request.ToCallMissedParams(h.userIDFomContext(c), c.OriginalURL()))
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store missed call event for user [%s] with paylod [%s]", h.userIDFomContext(c), c.Body())))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store missed call event for user [%s] with paylod [%s]", h.userIDFomContext(c), c.Body())))
return h.responseInternalServerError(c)
}
@@ -528,7 +528,7 @@ func (h *MessageHandler) Search(c fiber.Ctx) error {
var request requests.MessageSearch
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params in [%s] into [%T]", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params in [%s] into [%T]", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
@@ -536,13 +536,13 @@ func (h *MessageHandler) Search(c fiber.Ctx) error {
request.Token = c.Get("token")
if errors := h.validator.ValidateMessageSearch(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while searching messages [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while searching messages [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while searching messages")
}
messages, err := h.service.SearchMessages(ctx, request.ToSearchParams(h.userIDFomContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot search messages with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot search messages with params [%+#v]", request))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go
index 93fac9480..1847bb713 100644
--- a/api/pkg/handlers/message_send_schedule_handler.go
+++ b/api/pkg/handlers/message_send_schedule_handler.go
@@ -69,7 +69,7 @@ func (h *MessageSendScheduleHandler) Index(c fiber.Ctx) error {
schedules, err := h.service.Index(ctx, userID)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot list send schedules for user [%s]", userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot list send schedules for user [%s]", userID))
return h.responseInternalServerError(c)
}
@@ -102,7 +102,7 @@ func (h *MessageSendScheduleHandler) Store(c fiber.Ctx) error {
return h.service.CountByUser(ctx, userID)
})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for send schedules for user [%s]", userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot check entitlement for send schedules for user [%s]", userID))
return h.responseInternalServerError(c)
}
if !result.Allowed {
@@ -116,7 +116,7 @@ func (h *MessageSendScheduleHandler) Store(c fiber.Ctx) error {
request = request.Sanitize()
if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError(
+ ctxLogger.Warn(stacktrace.NewErrorf(
"validation errors [%s], while storing send schedule [%+#v]",
spew.Sdump(errors),
request,
@@ -126,7 +126,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 for user [%s]", userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot create send schedule for user [%s]", userID))
return h.responseInternalServerError(c)
}
@@ -173,7 +173,7 @@ func (h *MessageSendScheduleHandler) Update(c fiber.Ctx) error {
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 for user [%s] and schedule [%s]", userID, scheduleID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update send schedule for user [%s] and schedule [%s]", userID, scheduleID))
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
return h.responseNotFound(c, err.Error())
}
@@ -209,7 +209,7 @@ func (h *MessageSendScheduleHandler) Delete(c fiber.Ctx) error {
userID := h.userIDFomContext(c)
if _, err = h.service.Load(ctx, userID, scheduleID); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load send schedule for deletion for user [%s] and schedule [%s]", userID, scheduleID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "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())
}
@@ -217,7 +217,7 @@ func (h *MessageSendScheduleHandler) Delete(c fiber.Ctx) error {
}
if err = h.service.Delete(ctx, userID, scheduleID); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete send schedule for user [%s] and schedule [%s]", userID, scheduleID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete send schedule for user [%s] and schedule [%s]", userID, scheduleID))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go
index 996ca69be..a931302dc 100644
--- a/api/pkg/handlers/message_thread_handler.go
+++ b/api/pkg/handlers/message_thread_handler.go
@@ -73,18 +73,18 @@ func (h *MessageThreadHandler) Index(c fiber.Ctx) error {
var request requests.MessageThreadIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateMessageThreadIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching message threads [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching message threads [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching message threads")
}
threads, err := h.service.GetThreads(ctx, request.ToGetParams(h.userIDFomContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get message threads with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get message threads with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -113,13 +113,13 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error {
var request requests.MessageThreadUpdate
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
request.MessageThreadID = c.Params("messageThreadID")
if errors := h.validator.ValidateUpdate(ctx, request); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating message thread [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating message thread [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating message thread")
}
@@ -128,7 +128,7 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error {
return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID))
}
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update message thread with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update message thread with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -156,7 +156,7 @@ func (h *MessageThreadHandler) Delete(c fiber.Ctx) error {
messageThreadID := c.Params("messageThreadID")
if errors := h.validator.ValidateUUID(messageThreadID, "messageThreadID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a thread thread with ID [%s]", spew.Sdump(errors), messageThreadID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a thread thread with ID [%s]", spew.Sdump(errors), messageThreadID))
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting a thread thread")
}
@@ -166,12 +166,12 @@ func (h *MessageThreadHandler) Delete(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread thread with id [%s]", messageThreadID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread thread with id [%s]", messageThreadID)))
return h.responseInternalServerError(c)
}
if err = h.service.DeleteThread(ctx, c.OriginalURL(), thread); err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete thread thread with ID [%s] for user with ID [%s]", messageThreadID, thread.UserID)))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete thread thread with ID [%s] for user with ID [%s]", messageThreadID, thread.UserID)))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go
index 60b9d756e..fefb81895 100644
--- a/api/pkg/handlers/message_thread_handler_test.go
+++ b/api/pkg/handlers/message_thread_handler_test.go
@@ -34,7 +34,7 @@ func (stub *messageThreadHandlerRepositoryStub) UpdateActivity(context.Context,
}
func (stub *messageThreadHandlerRepositoryStub) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
- return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
}
func (stub *messageThreadHandlerRepositoryStub) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go
index 55b981cf4..09f893404 100644
--- a/api/pkg/handlers/phone_api_key_handler.go
+++ b/api/pkg/handlers/phone_api_key_handler.go
@@ -72,18 +72,18 @@ func (h *PhoneAPIKeyHandler) index(c fiber.Ctx) error {
var request requests.PhoneAPIKeyIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching phone API keys [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching phone API keys [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phone API keys")
}
apiKeys, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot index phone API keys with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot index phone API keys with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -114,7 +114,7 @@ func (h *PhoneAPIKeyHandler) store(c fiber.Ctx) error {
return h.service.CountByUser(ctx, userID)
})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for phone API keys for user [%s]", userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot check entitlement for phone API keys for user [%s]", userID))
return h.responseInternalServerError(c)
}
if !result.Allowed {
@@ -123,18 +123,18 @@ func (h *PhoneAPIKeyHandler) store(c fiber.Ctx) error {
var request requests.PhoneAPIKeyStoreRequest
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
}
phoneAPIKey, err := h.service.Create(ctx, h.userFromContext(c), request.Name)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update phones with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update phones with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -161,7 +161,7 @@ func (h *PhoneAPIKeyHandler) delete(c fiber.Ctx) error {
phoneAPIKeyID := c.Params("phoneAPIKeyID")
if errors := h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
}
@@ -171,7 +171,7 @@ func (h *PhoneAPIKeyHandler) delete(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone API key with ID [%s] for user with ID [%s]", phoneAPIKeyID, h.userIDFomContext(c))))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone API key with ID [%s] for user with ID [%s]", phoneAPIKeyID, h.userIDFomContext(c))))
return h.responseInternalServerError(c)
}
@@ -200,7 +200,7 @@ func (h *PhoneAPIKeyHandler) deletePhone(c fiber.Ctx) error {
phoneAPIKeyID := c.Params("phoneAPIKeyID")
phoneID := c.Params("phoneID")
if errors := h.mergeErrors(h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"), h.validator.ValidateUUID(phoneID, "phoneID")); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing event")
}
@@ -210,7 +210,7 @@ func (h *PhoneAPIKeyHandler) deletePhone(c fiber.Ctx) error {
}
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone API key with ID [%s] for user with ID [%s]", phoneID, phoneAPIKeyID, h.userIDFomContext(c))))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone API key with ID [%s] for user with ID [%s]", phoneID, phoneAPIKeyID, h.userIDFomContext(c))))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go
index 678b89062..55380ed91 100644
--- a/api/pkg/handlers/phone_handler.go
+++ b/api/pkg/handlers/phone_handler.go
@@ -74,18 +74,18 @@ func (h *PhoneHandler) Index(c fiber.Ctx) error {
var request requests.PhoneIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phones")
}
phones, err := h.service.Index(ctx, h.userFromContext(c), request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot index phones with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot index phones with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -114,18 +114,18 @@ func (h *PhoneHandler) Upsert(c fiber.Ctx) error {
var request requests.PhoneUpsert
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateUpsert(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
}
phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL(), c.Body()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update phones with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update phones with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -154,7 +154,7 @@ func (h *PhoneHandler) Delete(c fiber.Ctx) error {
request := requests.PhoneDelete{PhoneID: c.Params("phoneID")}
if errors := h.validator.ValidateDelete(ctx, request); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting phone [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting phone [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting phone")
}
@@ -163,7 +163,7 @@ func (h *PhoneHandler) Delete(c fiber.Ctx) error {
return h.responseNotFound(c, fmt.Sprintf("cannot find phone with ID [%s]", request.PhoneID))
}
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete phones with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete phones with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -192,18 +192,18 @@ func (h *PhoneHandler) UpsertFCMToken(c fiber.Ctx) error {
var request requests.PhoneFCMToken
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateFCMToken(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
}
phone, err := h.service.UpsertFCMToken(ctx, request.ToPhoneFCMTokenParams(h.userFromContext(c), c.OriginalURL()))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete phones with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete phones with params [%+#v]", request))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/user_handler.go b/api/pkg/handlers/user_handler.go
index 8f73ba00c..b5f224fd2 100644
--- a/api/pkg/handlers/user_handler.go
+++ b/api/pkg/handlers/user_handler.go
@@ -71,7 +71,7 @@ func (h *UserHandler) Show(c fiber.Ctx) error {
authUser := h.userFromContext(c)
user, err := h.service.Get(ctx, c.OriginalURL(), authUser)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID))
return h.responseInternalServerError(c)
}
@@ -100,18 +100,18 @@ func (h *UserHandler) Update(c fiber.Ctx) error {
var request requests.UserUpdate
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating user")
}
user, err := h.service.Update(ctx, c.OriginalURL(), h.userFromContext(c), request.ToUpdateParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update user with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update user with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -134,7 +134,7 @@ func (h *UserHandler) Delete(c fiber.Ctx) error {
defer span.End()
if err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c)); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete user user with ID [%s]", h.userIDFomContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete user user with ID [%s]", h.userIDFomContext(c)))
return h.responseInternalServerError(c)
}
@@ -162,13 +162,13 @@ func (h *UserHandler) UpdateNotifications(c fiber.Ctx) error {
var request requests.UserNotificationUpdate
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
user, err := h.service.UpdateNotificationSettings(ctx, h.userIDFomContext(c), request.ToUserNotificationUpdateParams())
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update notification for [%T] with ID [%s]", user, h.userIDFomContext(c))))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update notification for [%T] with ID [%s]", user, h.userIDFomContext(c))))
return h.responseInternalServerError(c)
}
@@ -196,7 +196,7 @@ func (h *UserHandler) subscriptionUpdateURL(c fiber.Ctx) error {
url, err := h.service.GetSubscriptionUpdateURL(ctx, authUser.ID)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID))
return h.responseInternalServerError(c)
}
@@ -224,7 +224,7 @@ func (h *UserHandler) cancelSubscription(c fiber.Ctx) error {
err := h.service.InitiateSubscriptionCancel(ctx, authUser.ID)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID))
return h.responseInternalServerError(c)
}
@@ -257,7 +257,7 @@ func (h *UserHandler) DeleteAPIKey(c fiber.Ctx) error {
user, err := h.service.RotateAPIKey(ctx, c.OriginalURL(), h.userIDFomContext(c))
if err != nil {
- ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c))))
+ ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c))))
return h.responseInternalServerError(c)
}
@@ -283,7 +283,7 @@ func (h *UserHandler) subscriptionPayments(c fiber.Ctx) error {
invoices, err := h.service.GetSubscriptionPayments(ctx, h.userIDFomContext(c))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get current subscription invoices for user [%s]", h.userFromContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get current subscription invoices for user [%s]", h.userFromContext(c)))
return h.responseInternalServerError(c)
}
@@ -311,19 +311,19 @@ func (h *UserHandler) subscriptionInvoice(c fiber.Ctx) error {
var request requests.UserPaymentInvoice
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.Body(), request))
return h.responseBadRequest(c, err)
}
request.SubscriptionInvoiceID = c.Params("subscriptionInvoiceID")
if errors := h.validator.ValidatePaymentInvoice(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while validating subscription payment invoice request [%s]", spew.Sdump(errors), c.Body()))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while validating subscription payment invoice request [%s]", spew.Sdump(errors), c.Body()))
return h.responseUnprocessableEntity(c, errors, "validation errors while generating payment invoice")
}
reader, err := h.service.GenerateReceipt(ctx, request.UserInvoiceGenerateParams(h.userIDFomContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot generate receipt for invoice ID [%s] and user [%s]", request.SubscriptionInvoiceID, h.userFromContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot generate receipt for invoice ID [%s] and user [%s]", request.SubscriptionInvoiceID, h.userFromContext(c)))
return h.responseInternalServerError(c)
}
@@ -332,7 +332,7 @@ func (h *UserHandler) subscriptionInvoice(c fiber.Ctx) error {
data, err := io.ReadAll(reader)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot read invoice data with ID [%s] for user with ID [%s]", request.SubscriptionInvoiceID, h.userIDFomContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot read invoice data with ID [%s] for user with ID [%s]", request.SubscriptionInvoiceID, h.userIDFomContext(c)))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/handlers/webhook_handler.go b/api/pkg/handlers/webhook_handler.go
index 6eb389e3c..6866da6bc 100644
--- a/api/pkg/handlers/webhook_handler.go
+++ b/api/pkg/handlers/webhook_handler.go
@@ -70,18 +70,18 @@ func (h *WebhookHandler) Index(c fiber.Ctx) error {
var request requests.WebhookIndex
if err := c.Bind().Query(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching webhooks [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching webhooks [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching webhooks")
}
webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get webhooks with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get webhooks with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -108,13 +108,13 @@ func (h *WebhookHandler) Delete(c fiber.Ctx) error {
webhookID := c.Params("webhookID")
if errors := h.validator.ValidateUUID(webhookID, "webhookID"); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting webhook with ID [%s]", spew.Sdump(errors), webhookID))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting webhook with ID [%s]", spew.Sdump(errors), webhookID))
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting webhook")
}
err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(webhookID))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete webhook with ID [%+#v]", webhookID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete webhook with ID [%+#v]", webhookID))
return h.responseInternalServerError(c)
}
@@ -143,29 +143,29 @@ func (h *WebhookHandler) Store(c fiber.Ctx) error {
var request requests.WebhookStore
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall body [%s] into [%T]", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall body [%s] into [%T]", c.Body(), request))
return h.responseBadRequest(c, err)
}
if errors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing webhook [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing webhook [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while storing webhook")
}
webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 10})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot index webhooks for user [%s]", h.userIDFomContext(c)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot index webhooks for user [%s]", h.userIDFomContext(c)))
return h.responseInternalServerError(c)
}
if len(webhooks) == 10 {
- ctxLogger.Warn(stacktrace.NewError("user with ID [%s] wants to create more than 10 webhooks", h.userIDFomContext(c)))
+ ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] wants to create more than 10 webhooks", h.userIDFomContext(c)))
return h.responsePaymentRequired(c, "You can't create more than 10 webhooks contact us to upgrade to our enterprise plan.")
}
webhook, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot store webhoook with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot store webhoook with params [%+#v]", request))
return h.responseInternalServerError(c)
}
@@ -193,19 +193,19 @@ func (h *WebhookHandler) Update(c fiber.Ctx) error {
var request requests.WebhookUpdate
if err := c.Bind().Body(&request); err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into [%T]", c.Body(), request))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into [%T]", c.Body(), request))
return h.responseBadRequest(c, err)
}
request.WebhookID = c.Params("webhookID")
if errors := h.validator.ValidateUpdate(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
- ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
+ ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request))
return h.responseUnprocessableEntity(c, errors, "validation errors while updating webhook")
}
user, err := h.service.Update(ctx, request.ToUpdateParams(h.userFromContext(c)))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update user with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update user with params [%+#v]", request))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/listeners/billing_listener.go b/api/pkg/listeners/billing_listener.go
index 2645e27c3..08badc22d 100644
--- a/api/pkg/listeners/billing_listener.go
+++ b/api/pkg/listeners/billing_listener.go
@@ -46,11 +46,11 @@ func (listener *BillingListener) OnMessageAPISent(ctx context.Context, event clo
var payload events.MessageAPISentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.RegisterSentMessage(ctx, payload.MessageID, payload.RequestReceivedAt, payload.UserID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot register sent message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot register sent message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
}
return nil
@@ -63,11 +63,11 @@ func (listener *BillingListener) OnMessagePhoneReceived(ctx context.Context, eve
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.RegisterReceivedMessage(ctx, payload.MessageID, payload.Timestamp, payload.UserID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot register received message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot register received message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
}
return nil
@@ -79,11 +79,11 @@ func (listener *BillingListener) onUserAccountDeleted(ctx context.Context, event
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.Propagatef(err, "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.BillingUsage] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.BillingUsage] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/discord_listener.go b/api/pkg/listeners/discord_listener.go
index aebed46b7..e0514177e 100644
--- a/api/pkg/listeners/discord_listener.go
+++ b/api/pkg/listeners/discord_listener.go
@@ -43,11 +43,11 @@ func (listener *DiscordListener) OnMessagePhoneReceived(ctx context.Context, eve
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.HandleMessageReceived(ctx, payload.UserID, event); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -59,11 +59,11 @@ func (listener *DiscordListener) onUserAccountDeleted(ctx context.Context, event
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.Propagatef(err, "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.Discord] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Discord] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/email_notification_listener.go b/api/pkg/listeners/email_notification_listener.go
index fc6065398..bc78f993e 100644
--- a/api/pkg/listeners/email_notification_listener.go
+++ b/api/pkg/listeners/email_notification_listener.go
@@ -45,11 +45,11 @@ func (listener *EmailNotificationListener) OnMessageSendExpired(ctx context.Cont
payload := new(events.MessageSendExpiredPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.NotifyMessageExpired(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -62,11 +62,11 @@ func (listener *EmailNotificationListener) OnMessageSendFailed(ctx context.Conte
payload := new(events.MessageSendFailedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.NotifyMessageFailed(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -79,11 +79,11 @@ func (listener *EmailNotificationListener) OnWebhookSendFailed(ctx context.Conte
payload := new(events.WebhookSendFailedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.NotifyWebhookSendFailed(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -96,11 +96,11 @@ func (listener *EmailNotificationListener) OnDiscordSendFailed(ctx context.Conte
payload := new(events.DiscordSendFailedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.NotifyDiscordSendFailed(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/heartbeat_listener.go b/api/pkg/listeners/heartbeat_listener.go
index eef4933f5..4e0cdc0d7 100644
--- a/api/pkg/listeners/heartbeat_listener.go
+++ b/api/pkg/listeners/heartbeat_listener.go
@@ -48,7 +48,7 @@ func (listener *HeartbeatListener) onPhoneUpdated(ctx context.Context, event clo
var payload events.PhoneUpdatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
storeParams := &services.HeartbeatMonitorStoreParams{
@@ -59,7 +59,7 @@ func (listener *HeartbeatListener) onPhoneUpdated(ctx context.Context, event clo
}
if _, err := listener.service.StoreMonitor(ctx, storeParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(storeParams), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(storeParams), event.ID()))
}
return nil
@@ -72,11 +72,11 @@ func (listener *HeartbeatListener) onPhoneDeleted(ctx context.Context, event clo
var payload events.PhoneDeletedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.DeleteMonitor(ctx, payload.UserID, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
}
return nil
@@ -89,7 +89,7 @@ func (listener *HeartbeatListener) onPhoneHeartbeatCheck(ctx context.Context, ev
var payload events.PhoneHeartbeatCheckPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
monitorParams := &services.HeartbeatMonitorParams{
@@ -101,7 +101,7 @@ func (listener *HeartbeatListener) onPhoneHeartbeatCheck(ctx context.Context, ev
}
if err := listener.service.Monitor(ctx, monitorParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot monitor heartbeats for userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot monitor heartbeats for userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
}
return nil
@@ -114,11 +114,11 @@ func (listener *HeartbeatListener) onPhoneHeartbeatOffline(ctx context.Context,
var payload events.PhoneHeartbeatOfflinePayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.UpdatePhoneOnline(ctx, payload.UserID, payload.MonitorID, false); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID()))
}
return nil
@@ -130,11 +130,11 @@ func (listener *HeartbeatListener) onUserAccountDeleted(ctx context.Context, eve
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.Propagatef(err, "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.Heartbeat] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Heartbeat] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/integration_3cx_listener.go b/api/pkg/listeners/integration_3cx_listener.go
index 529916f86..314d6a291 100644
--- a/api/pkg/listeners/integration_3cx_listener.go
+++ b/api/pkg/listeners/integration_3cx_listener.go
@@ -46,11 +46,11 @@ func (listener *Integration3CXListener) OnMessagePhoneReceived(ctx context.Conte
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -63,11 +63,11 @@ func (listener *Integration3CXListener) OnMessageSendFailed(ctx context.Context,
var payload events.MessageSendFailedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -80,11 +80,11 @@ func (listener *Integration3CXListener) OnMessagePhoneSent(ctx context.Context,
var payload events.MessagePhoneSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -97,11 +97,11 @@ func (listener *Integration3CXListener) OnMessagePhoneDelivered(ctx context.Cont
var payload events.MessagePhoneDeliveredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -113,11 +113,11 @@ func (listener *Integration3CXListener) onUserAccountDeleted(ctx context.Context
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.Propagatef(err, "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.Integration3CX] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Integration3CX] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/marketing_listener.go b/api/pkg/listeners/marketing_listener.go
index 4bcf15d11..fd2272a07 100644
--- a/api/pkg/listeners/marketing_listener.go
+++ b/api/pkg/listeners/marketing_listener.go
@@ -42,11 +42,11 @@ func (listener *MarketingListener) onUserAccountCreated(ctx context.Context, eve
var payload events.UserAccountCreatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.CreateContact(ctx, payload.UserID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
@@ -58,11 +58,11 @@ func (listener *MarketingListener) onUserAccountDeleted(ctx context.Context, eve
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.DeleteContact(ctx, payload.UserEmail); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/message_listener.go b/api/pkg/listeners/message_listener.go
index 49fd18a3d..254bccf82 100644
--- a/api/pkg/listeners/message_listener.go
+++ b/api/pkg/listeners/message_listener.go
@@ -54,7 +54,7 @@ func (listener *MessageListener) OnMessagePhoneSending(ctx context.Context, even
var payload events.MessagePhoneSendingPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
handleParams := services.HandleMessageParams{
@@ -65,7 +65,7 @@ func (listener *MessageListener) OnMessagePhoneSending(ctx context.Context, even
}
if err := listener.service.HandleMessageSending(ctx, handleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle sending for message with ID [%s] for event with ID [%s]", handleParams.ID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle sending for message with ID [%s] for event with ID [%s]", handleParams.ID, event.ID()))
}
return nil
@@ -78,7 +78,7 @@ func (listener *MessageListener) OnMessagePhoneSent(ctx context.Context, event c
var payload events.MessagePhoneSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
handleParams := services.HandleMessageParams{
@@ -89,7 +89,7 @@ func (listener *MessageListener) OnMessagePhoneSent(ctx context.Context, event c
}
if err := listener.service.HandleMessageSent(ctx, handleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
}
return nil
}
@@ -101,7 +101,7 @@ func (listener *MessageListener) OnMessagePhoneDelivered(ctx context.Context, ev
var payload events.MessagePhoneDeliveredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
handleParams := services.HandleMessageParams{
@@ -111,7 +111,7 @@ func (listener *MessageListener) OnMessagePhoneDelivered(ctx context.Context, ev
}
if err := listener.service.HandleMessageDelivered(ctx, handleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
}
return nil
@@ -124,7 +124,7 @@ func (listener *MessageListener) OnMessagePhoneFailed(ctx context.Context, event
var payload events.MessageSendFailedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
handleParams := services.HandleMessageFailedParams{
@@ -135,7 +135,7 @@ func (listener *MessageListener) OnMessagePhoneFailed(ctx context.Context, event
}
if err := listener.service.HandleMessageFailed(ctx, handleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID()))
}
return nil
@@ -148,12 +148,12 @@ func (listener *MessageListener) onMessageNotificationFailed(ctx context.Context
var payload events.MessageNotificationFailedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
message, err := listener.service.GetMessage(ctx, payload.UserID, payload.MessageID)
if err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with id [%s] and user id [%s]", payload.MessageID, payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with id [%s] and user id [%s]", payload.MessageID, payload.UserID))
}
storeParams := services.MessageStoreEventParams{
@@ -164,7 +164,7 @@ func (listener *MessageListener) onMessageNotificationFailed(ctx context.Context
Source: event.Source(),
}
if _, err = listener.service.StoreEvent(ctx, message, storeParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store message event [%s] for message with ID [%s]", storeParams.EventName, storeParams.MessageID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store message event [%s] for message with ID [%s]", storeParams.EventName, storeParams.MessageID))
}
return nil
@@ -177,7 +177,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context,
var payload events.MessageNotificationSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
checkParams := services.MessageScheduleExpirationParams{
@@ -189,7 +189,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context,
MessageExpirationDuration: payload.MessageExpirationDuration,
}
if err := listener.service.ScheduleExpirationCheck(ctx, checkParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot exchedule expiration check for ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot exchedule expiration check for ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID))
}
handleParams := services.HandleMessageParams{
@@ -199,7 +199,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context,
Timestamp: payload.NotificationSentAt,
}
if err := listener.service.HandleMessageNotificationSent(ctx, handleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for message [%s] and userID [%s]", event.Type(), checkParams.MessageID, checkParams.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for message [%s] and userID [%s]", event.Type(), checkParams.MessageID, checkParams.UserID))
}
return nil
@@ -212,7 +212,7 @@ func (listener *MessageListener) onMessageSendExpiredCheck(ctx context.Context,
var payload events.MessageSendExpiredCheckPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
checkParams := services.MessageCheckExpired{
@@ -221,7 +221,7 @@ func (listener *MessageListener) onMessageSendExpiredCheck(ctx context.Context,
Source: event.Source(),
}
if err := listener.service.CheckExpired(ctx, checkParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check expiration for message with ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check expiration for message with ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID))
}
return nil
@@ -234,7 +234,7 @@ func (listener *MessageListener) onMessageSendExpired(ctx context.Context, event
var payload events.MessageSendExpiredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
expiredParams := services.HandleMessageParams{
@@ -244,7 +244,7 @@ func (listener *MessageListener) onMessageSendExpired(ctx context.Context, event
Timestamp: payload.Timestamp,
}
if err := listener.service.HandleMessageExpired(ctx, expiredParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID))
}
return nil
@@ -257,7 +257,7 @@ func (listener *MessageListener) onMessageNotificationScheduled(ctx context.Cont
var payload events.MessageNotificationScheduledPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
expiredParams := services.HandleMessageParams{
@@ -267,7 +267,7 @@ func (listener *MessageListener) onMessageNotificationScheduled(ctx context.Cont
Timestamp: payload.ScheduledAt,
}
if err := listener.service.HandleMessageNotificationScheduled(ctx, expiredParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID))
}
return nil
@@ -280,11 +280,11 @@ func (listener *MessageListener) onMessageThreadAPIDeleted(ctx context.Context,
var payload events.MessageThreadAPIDeletedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.DeleteByOwnerAndContact(ctx, payload.UserID, payload.Owner, payload.Contact); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
@@ -297,11 +297,11 @@ func (listener *MessageListener) onMessageCallMissed(ctx context.Context, event
payload := new(events.MessageCallMissedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.RespondToMissedCall(ctx, event.Source(), payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
@@ -313,11 +313,11 @@ func (listener *MessageListener) onUserAccountDeleted(ctx context.Context, event
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.Propagatef(err, "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.Message] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Message] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/message_send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go
index 59f93b847..8e2838c72 100644
--- a/api/pkg/listeners/message_send_schedule_listener.go
+++ b/api/pkg/listeners/message_send_schedule_listener.go
@@ -45,11 +45,11 @@ 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.Propagatef(err, "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()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.MessageSendSchedule] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go
index ad22e810f..f6d2c52c6 100644
--- a/api/pkg/listeners/message_thread_listener.go
+++ b/api/pkg/listeners/message_thread_listener.go
@@ -54,7 +54,7 @@ func (listener *MessageThreadListener) OnMessageAPISent(ctx context.Context, eve
var payload events.MessageAPISentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -68,7 +68,7 @@ func (listener *MessageThreadListener) OnMessageAPISent(ctx context.Context, eve
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -81,11 +81,11 @@ func (listener *MessageThreadListener) onMessageDeleted(ctx context.Context, eve
payload := new(events.MessageAPIDeletedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.UpdateAfterDeletedMessage(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", payload.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", payload.MessageID, event.ID()))
}
return nil
@@ -98,7 +98,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSending(ctx context.Context
var payload events.MessagePhoneSendingPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -112,7 +112,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSending(ctx context.Context
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -125,7 +125,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSent(ctx context.Context, e
var payload events.MessagePhoneSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -139,7 +139,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSent(ctx context.Context, e
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -152,7 +152,7 @@ func (listener *MessageThreadListener) OnMessagePhoneDelivered(ctx context.Conte
var payload events.MessagePhoneDeliveredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -166,7 +166,7 @@ func (listener *MessageThreadListener) OnMessagePhoneDelivered(ctx context.Conte
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -179,7 +179,7 @@ func (listener *MessageThreadListener) OnMessagePhoneFailed(ctx context.Context,
var payload events.MessageSendFailedPayload
if err := event.DataAs(&payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T] for event [%s]", event.Data(), payload, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T] for event [%s]", event.Data(), payload, event.ID()))
}
updateParams := services.MessageThreadUpdateParams{
@@ -193,7 +193,7 @@ func (listener *MessageThreadListener) OnMessagePhoneFailed(ctx context.Context,
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -206,7 +206,7 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -222,7 +222,7 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -235,7 +235,7 @@ func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context,
var payload events.MessageCallMissedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
params := services.MessageThreadUpdateParams{
@@ -250,7 +250,7 @@ func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context,
EventTimestamp: event.Time(),
}
if err := listener.service.UpdateThread(ctx, params); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID()))
}
return nil
}
@@ -262,7 +262,7 @@ func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx contex
var payload events.MessageNotificationScheduledPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -276,7 +276,7 @@ func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx contex
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -289,7 +289,7 @@ func (listener *MessageThreadListener) onMessageExpired(ctx context.Context, eve
var payload events.MessageSendExpiredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
updateParams := services.MessageThreadUpdateParams{
@@ -303,7 +303,7 @@ func (listener *MessageThreadListener) onMessageExpired(ctx context.Context, eve
}
if err := listener.service.UpdateThread(ctx, updateParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID()))
}
return nil
@@ -315,11 +315,11 @@ func (listener *MessageThreadListener) onUserAccountDeleted(ctx context.Context,
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.Propagatef(err, "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.MessageThread] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.MessageThread] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/phone_api_key_listener.go b/api/pkg/listeners/phone_api_key_listener.go
index 45333ac56..8eb3621e0 100644
--- a/api/pkg/listeners/phone_api_key_listener.go
+++ b/api/pkg/listeners/phone_api_key_listener.go
@@ -48,7 +48,7 @@ func (listener *PhoneAPIKeyListener) onPhoneUpdated(ctx context.Context, event c
var payload events.PhoneUpdatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if payload.PhoneAPIKeyID == nil {
@@ -57,7 +57,7 @@ func (listener *PhoneAPIKeyListener) onPhoneUpdated(ctx context.Context, event c
}
if err := listener.service.AddPhone(ctx, payload.UserID, *payload.PhoneAPIKeyID, payload.PhoneID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
}
return nil
@@ -70,11 +70,11 @@ func (listener *PhoneAPIKeyListener) onPhoneDeleted(ctx context.Context, event c
var payload events.PhoneDeletedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.RemovePhoneByID(ctx, payload.UserID, payload.PhoneID, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone api key for [%s] event with ID [%s]", payload.PhoneID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone api key for [%s] event with ID [%s]", payload.PhoneID, event.Type(), event.ID()))
}
return nil
@@ -87,11 +87,11 @@ func (listener *PhoneAPIKeyListener) onUserAccountDeleted(ctx context.Context, e
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.Propagatef(err, "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 all [%T] for user with ID [%s] for [%s] event with ID [%s]", entities.PhoneAPIKey{}, payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s] for [%s] event with ID [%s]", entities.PhoneAPIKey{}, payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/phone_listener.go b/api/pkg/listeners/phone_listener.go
index f39448483..6f3418c80 100644
--- a/api/pkg/listeners/phone_listener.go
+++ b/api/pkg/listeners/phone_listener.go
@@ -44,11 +44,11 @@ func (listener *PhoneListener) onMessageSendScheduleDeleted(ctx context.Context,
var payload events.MessageSendScheduleDeletedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.NullifyScheduleID(ctx, payload.UserID, payload.ScheduleID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "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.Propagatef(err, "cannot nullify schedule ID [%s] for user [%s] on [%s] event with ID [%s]", payload.ScheduleID, payload.UserID, event.Type(), event.ID()))
}
return nil
@@ -61,11 +61,11 @@ func (listener *PhoneListener) onUserAccountDeleted(ctx context.Context, event c
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.Propagatef(err, "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 all [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go
index 40452a946..5bac8d73e 100644
--- a/api/pkg/listeners/phone_notification_listener.go
+++ b/api/pkg/listeners/phone_notification_listener.go
@@ -49,7 +49,7 @@ func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context,
var payload events.MessageAPISentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
sendParams := &services.PhoneNotificationScheduleParams{
@@ -66,7 +66,7 @@ func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context,
}
if err := listener.service.Schedule(ctx, sendParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
}
return nil
@@ -79,7 +79,7 @@ func (listener *PhoneNotificationListener) onMessageSendRetry(ctx context.Contex
var payload events.MessageSendRetryPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
sendParams := &services.PhoneNotificationScheduleParams{
@@ -94,7 +94,7 @@ func (listener *PhoneNotificationListener) onMessageSendRetry(ctx context.Contex
}
if err := listener.service.Schedule(ctx, sendParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
}
return nil
@@ -107,11 +107,11 @@ func (listener *PhoneNotificationListener) onPhoneHeartbeatMissed(ctx context.Co
payload := new(events.PhoneHeartbeatMissedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.SendHeartbeatFCM(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule send heartbeat FCM with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule send heartbeat FCM with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
}
return nil
@@ -124,7 +124,7 @@ func (listener *PhoneNotificationListener) onMessageNotificationSend(ctx context
var payload events.MessageNotificationSendPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
scheduleParams := &services.PhoneNotificationSendParams{
@@ -137,7 +137,7 @@ func (listener *PhoneNotificationListener) onMessageNotificationSend(ctx context
}
if err := listener.service.Send(ctx, scheduleParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule notification with params [%s] for event with ID [%s]", spew.Sdump(scheduleParams), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule notification with params [%s] for event with ID [%s]", spew.Sdump(scheduleParams), event.ID()))
}
return nil
@@ -149,11 +149,11 @@ func (listener *PhoneNotificationListener) onUserAccountDeleted(ctx context.Cont
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.Propagatef(err, "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.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
@@ -166,11 +166,11 @@ func (listener *PhoneNotificationListener) onMessageAPIDeleted(ctx context.Conte
var payload events.MessageAPIDeletedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.DeleteByMessageID(ctx, payload.UserID, payload.MessageID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "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.Propagatef(err, "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 nil
diff --git a/api/pkg/listeners/user_listener.go b/api/pkg/listeners/user_listener.go
index e4a15d32f..f681cb1ca 100644
--- a/api/pkg/listeners/user_listener.go
+++ b/api/pkg/listeners/user_listener.go
@@ -50,7 +50,7 @@ func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cl
var payload events.PhoneHeartbeatOfflinePayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
sendParams := &services.UserSendPhoneDeadEmailParams{
@@ -61,7 +61,7 @@ func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cl
}
if err := listener.service.SendPhoneDeadEmail(ctx, sendParams); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID()))
}
return nil
@@ -74,11 +74,11 @@ func (listener *UserListener) onUserAPIKeyRotated(ctx context.Context, event clo
payload := new(events.UserAPIKeyRotatedPayload)
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.SendAPIKeyRotatedEmail(ctx, payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID()))
}
return nil
@@ -91,11 +91,11 @@ func (listener *UserListener) OnUserSubscriptionCreated(ctx context.Context, eve
var payload events.UserSubscriptionCreatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.StartSubscription(ctx, &payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot start subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot start subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
}
return nil
@@ -108,11 +108,11 @@ func (listener *UserListener) OnUserSubscriptionCancelled(ctx context.Context, e
var payload events.UserSubscriptionCancelledPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.CancelSubscription(ctx, &payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot cancell subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot cancell subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
}
return nil
@@ -125,11 +125,11 @@ func (listener *UserListener) OnUserSubscriptionExpired(ctx context.Context, eve
var payload events.UserSubscriptionExpiredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.ExpireSubscription(ctx, &payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
}
return nil
@@ -142,11 +142,11 @@ func (listener *UserListener) OnUserSubscriptionUpdated(ctx context.Context, eve
var payload events.UserSubscriptionUpdatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.UpdateSubscription(ctx, &payload); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID()))
}
return nil
@@ -158,11 +158,11 @@ func (listener *UserListener) onUserAccountDeleted(ctx context.Context, event cl
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.DeleteAuthUser(ctx, payload.UserID); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.AuthUser] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.AuthUser] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/webhook_listener.go b/api/pkg/listeners/webhook_listener.go
index ff9ab5700..d14008b13 100644
--- a/api/pkg/listeners/webhook_listener.go
+++ b/api/pkg/listeners/webhook_listener.go
@@ -50,11 +50,11 @@ func (listener *WebhookListener) OnMessagePhoneReceived(ctx context.Context, eve
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -67,11 +67,11 @@ func (listener *WebhookListener) OnMessageSendExpired(ctx context.Context, event
var payload events.MessageSendExpiredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -84,11 +84,11 @@ func (listener *WebhookListener) OnMessageSendFailed(ctx context.Context, event
var payload events.MessageSendFailedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -101,11 +101,11 @@ func (listener *WebhookListener) OnMessagePhoneSent(ctx context.Context, event c
var payload events.MessagePhoneSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -118,11 +118,11 @@ func (listener *WebhookListener) OnMessagePhoneDelivered(ctx context.Context, ev
var payload events.MessagePhoneDeliveredPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -135,11 +135,11 @@ func (listener *WebhookListener) onPhoneHeartbeatOffline(ctx context.Context, ev
var payload events.PhoneHeartbeatOfflinePayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -152,11 +152,11 @@ func (listener *WebhookListener) onPhoneHeartbeatOnline(ctx context.Context, eve
var payload events.PhoneHeartbeatOnlinePayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -169,11 +169,11 @@ func (listener *WebhookListener) onMessageCallMissed(ctx context.Context, event
var payload events.MessageCallMissedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID()))
}
return nil
@@ -185,11 +185,11 @@ func (listener *WebhookListener) onUserAccountDeleted(ctx context.Context, event
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.Propagatef(err, "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.Webhook] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Webhook] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID()))
}
return nil
diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go
index b06381d78..bdc43f938 100644
--- a/api/pkg/listeners/websocket_listener.go
+++ b/api/pkg/listeners/websocket_listener.go
@@ -46,11 +46,11 @@ func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, even
var payload events.MessageCallMissedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
}
@@ -62,11 +62,11 @@ func (listener *WebsocketListener) onMessagePhoneSent(ctx context.Context, event
var payload events.MessagePhoneSentPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
@@ -79,11 +79,11 @@ func (listener *WebsocketListener) onMessagePhoneReceived(ctx context.Context, e
var payload events.MessagePhoneReceivedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
@@ -96,11 +96,11 @@ func (listener *WebsocketListener) onMessagePhoneFailed(ctx context.Context, eve
var payload events.MessageSendFailedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
@@ -113,11 +113,11 @@ func (listener *WebsocketListener) onPhoneUpdated(ctx context.Context, event clo
var payload events.PhoneUpdatedPayload
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.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload))
}
if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil {
- return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
+ return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID))
}
return nil
diff --git a/api/pkg/middlewares/api_key_auth_middleware.go b/api/pkg/middlewares/api_key_auth_middleware.go
index f2b184194..cec70976e 100644
--- a/api/pkg/middlewares/api_key_auth_middleware.go
+++ b/api/pkg/middlewares/api_key_auth_middleware.go
@@ -28,7 +28,7 @@ func APIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepository
authUser, err := userRepository.LoadAuthContext(ctx, apiKey)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with api key [%s]", apiKey))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with api key [%s]", apiKey))
return c.Next()
}
diff --git a/api/pkg/middlewares/bearer_api_key_auth_middleware.go b/api/pkg/middlewares/bearer_api_key_auth_middleware.go
index f52d0ba09..6125dca87 100644
--- a/api/pkg/middlewares/bearer_api_key_auth_middleware.go
+++ b/api/pkg/middlewares/bearer_api_key_auth_middleware.go
@@ -26,7 +26,7 @@ func BearerAPIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepo
authUser, err := userRepository.LoadAuthContext(ctx, apiKey)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with api key [%s] using header [%s]", apiKey, c.Get(authHeaderBearer)))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with api key [%s] using header [%s]", apiKey, c.Get(authHeaderBearer)))
return c.Next()
}
diff --git a/api/pkg/middlewares/bearer_auth_middleware.go b/api/pkg/middlewares/bearer_auth_middleware.go
index 6fb57a648..3391e8759 100644
--- a/api/pkg/middlewares/bearer_auth_middleware.go
+++ b/api/pkg/middlewares/bearer_auth_middleware.go
@@ -33,7 +33,7 @@ func BearerAuth(logger telemetry.Logger, tracer telemetry.Tracer, authClient *au
token, err := authClient.VerifyIDToken(context.Background(), authToken)
if err != nil {
- ctxLogger.Warn(tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "invalid firebase id token [%s]", authToken)))
+ ctxLogger.Warn(tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "invalid firebase id token [%s]", authToken)))
return c.Next()
}
diff --git a/api/pkg/middlewares/http_request_logger_middleware.go b/api/pkg/middlewares/http_request_logger_middleware.go
index 06f5e06da..e8de42e2a 100644
--- a/api/pkg/middlewares/http_request_logger_middleware.go
+++ b/api/pkg/middlewares/http_request_logger_middleware.go
@@ -24,7 +24,7 @@ func HTTPRequestLogger(tracer telemetry.Tracer, logger telemetry.Logger) fiber.H
statusCode := c.Response().StatusCode()
span.AddEvent(fmt.Sprintf("finished handling request with traceID: [%s], statusCode: [%d]", span.SpanContext().TraceID().String(), statusCode))
if statusCode >= 300 && len(c.Request().Body()) > 0 && !slices.Contains([]int{401, 402}, statusCode) {
- ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewError("http.status [%d], body [%s]", statusCode, string(c.Request().Body())))
+ ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewErrorf("http.status [%d], body [%s]", statusCode, string(c.Request().Body())))
}
return response
diff --git a/api/pkg/middlewares/phone_api_key_auth_middleware.go b/api/pkg/middlewares/phone_api_key_auth_middleware.go
index fb054aa4a..22ae1f5f4 100644
--- a/api/pkg/middlewares/phone_api_key_auth_middleware.go
+++ b/api/pkg/middlewares/phone_api_key_auth_middleware.go
@@ -26,7 +26,7 @@ func PhoneAPIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, repositor
authUser, err := repository.LoadAuthContext(ctx, apiKey)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with phone api key [%s]", apiKey))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with phone api key [%s]", apiKey))
return c.Next()
}
diff --git a/api/pkg/repositories/google_cloud_storage_attachment_repository.go b/api/pkg/repositories/google_cloud_storage_attachment_repository.go
index de0f83007..3c8b9e8c2 100644
--- a/api/pkg/repositories/google_cloud_storage_attachment_repository.go
+++ b/api/pkg/repositories/google_cloud_storage_attachment_repository.go
@@ -43,11 +43,11 @@ func (s *GoogleCloudStorageAttachmentRepository) Upload(ctx context.Context, pat
writer.ContentType = contentType
if _, err := writer.Write(data); err != nil {
- return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot write attachment to GCS path [%s]", path))
+ return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot write attachment to GCS path [%s]", path))
}
if err := writer.Close(); err != nil {
- return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot close GCS writer for path [%s]", path))
+ return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot close GCS writer for path [%s]", path))
}
ctxLogger.Info(fmt.Sprintf("uploaded attachment to GCS path [%s/%s] with size [%d]", s.bucket, path, len(data)))
@@ -62,15 +62,15 @@ func (s *GoogleCloudStorageAttachmentRepository) Download(ctx context.Context, p
reader, err := s.client.Bucket(s.bucket).Object(path).NewReader(ctx)
if err != nil {
if errors.Is(err, storage.ErrObjectNotExist) {
- return nil, s.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "cannot open GCS reader for path [%s]", path))
+ return nil, s.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "cannot open GCS reader for path [%s]", path))
}
- return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot open GCS reader for path [%s]", path))
+ return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "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, "cannot read attachment from GCS path [%s]", path))
+ return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot read attachment from GCS path [%s]", path))
}
ctxLogger.Info(fmt.Sprintf("downloaded attachment from GCS path [%s/%s] with size [%d]", s.bucket, path, len(data)))
@@ -83,7 +83,7 @@ func (s *GoogleCloudStorageAttachmentRepository) Delete(ctx context.Context, pat
defer span.End()
if err := s.client.Bucket(s.bucket).Object(path).Delete(ctx); err != nil {
- return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete GCS object at path [%s]", path))
+ return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete GCS object at path [%s]", path))
}
ctxLogger.Info(fmt.Sprintf("deleted attachment from GCS path [%s/%s]", s.bucket, path))
diff --git a/api/pkg/repositories/gorm_billing_usage_repository.go b/api/pkg/repositories/gorm_billing_usage_repository.go
index 02509e27d..740cd0f6e 100644
--- a/api/pkg/repositories/gorm_billing_usage_repository.go
+++ b/api/pkg/repositories/gorm_billing_usage_repository.go
@@ -40,7 +40,7 @@ func (repository *gormBillingUsageRepository) DeleteAllForUser(ctx context.Conte
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.BillingUsage{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.BillingUsage{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.BillingUsage{}, userID))
}
return nil
@@ -133,7 +133,7 @@ func (repository *gormBillingUsageRepository) GetCurrent(ctx context.Context, us
},
)
if err != nil {
- return &usage, stacktrace.Propagate(err, "cannot load billing usage for user [%s]", userID)
+ return &usage, stacktrace.Propagatef(err, "cannot load billing usage for user [%s]", userID)
}
return &usage, nil
@@ -156,7 +156,7 @@ func (repository *gormBillingUsageRepository) GetHistory(ctx context.Context, us
Find(&usages).
Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch billing usage history for userID [%s] and params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch billing usage history for userID [%s] and params [%+#v]", userID, params))
}
return usages, nil
@@ -167,7 +167,7 @@ func (repository *gormBillingUsageRepository) GetHistory(ctx context.Context, us
func (repository *gormBillingUsageRepository) createBillingUsageForUser(ctx context.Context, tx *gorm.DB, userID entities.UserID, timestamp time.Time, sent uint, received uint) (*entities.BillingUsage, error) {
user := new(entities.User)
if err := tx.WithContext(ctx).First(user, userID).Error; err != nil {
- return nil, stacktrace.Propagate(err, "cannot load user [%s] to compute billing cycle", userID)
+ return nil, stacktrace.Propagatef(err, "cannot load user [%s] to compute billing cycle", userID)
}
start, end := computeBillingCycle(timestamp, user.GetBillingAnchorDay())
diff --git a/api/pkg/repositories/gorm_discord_repository.go b/api/pkg/repositories/gorm_discord_repository.go
index 462ee37b1..9102b382a 100644
--- a/api/pkg/repositories/gorm_discord_repository.go
+++ b/api/pkg/repositories/gorm_discord_repository.go
@@ -37,7 +37,7 @@ func (repository *gormDiscordRepository) DeleteAllForUser(ctx context.Context, u
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Discord{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Discord{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Discord{}, userID))
}
return nil
@@ -48,7 +48,7 @@ func (repository *gormDiscordRepository) Save(ctx context.Context, Discord *enti
defer span.End()
if err := repository.db.WithContext(ctx).Save(Discord).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update discord integration with ID [%s]", Discord.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update discord integration with ID [%s]", Discord.ID))
}
return nil
@@ -67,7 +67,7 @@ func (repository *gormDiscordRepository) Index(ctx context.Context, userID entit
discords := make([]*entities.Discord, 0)
if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&discords).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord integrations for user [%s] and params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord integrations for user [%s] and params [%+#v]", userID, params))
}
return discords, nil
@@ -85,7 +85,7 @@ func (repository *gormDiscordRepository) FetchHavingIncomingChannel(ctx context.
Where("incoming_channel_id != ?", "").
Find(&discords).Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integrations for user with ID [%s] having a valid [incoming_channel_id]", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integrations for user with ID [%s] having a valid [incoming_channel_id]", userID))
}
return discords, nil
@@ -98,11 +98,11 @@ func (repository *gormDiscordRepository) Load(ctx context.Context, userID entiti
discord := new(entities.Discord)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", discordID).First(&discord).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "discord integration with ID [%s] for user [%s] does not exist", discordID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "discord integration with ID [%s] for user [%s] does not exist", discordID, userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integration with ID [%s] for user [%s]", discordID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integration with ID [%s] for user [%s]", discordID, userID))
}
return discord, nil
@@ -115,11 +115,11 @@ func (repository *gormDiscordRepository) FindByServerID(ctx context.Context, ser
discord := new(entities.Discord)
err := repository.db.WithContext(ctx).Where("server_id = ?", serverID).First(&discord).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "discord integration with server ID [%s] does not exist", serverID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "discord integration with server ID [%s] does not exist", serverID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integration with serverID [%s]", serverID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integration with serverID [%s]", serverID))
}
return discord, nil
@@ -134,7 +134,7 @@ func (repository *gormDiscordRepository) Delete(ctx context.Context, userID enti
Where("id = ?", discordID).
Delete(&entities.Discord{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete discord integration with ID [%s] and userID [%s]", discordID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete discord integration with ID [%s] and userID [%s]", discordID, userID))
}
return nil
diff --git a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
index 364e64351..50a2cefb4 100644
--- a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
+++ b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go
@@ -40,7 +40,7 @@ func (repository *gormHeartbeatMonitorRepository) DeleteAllForUser(ctx context.C
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.HeartbeatMonitor{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID))
}
return nil
}
@@ -62,7 +62,7 @@ func (repository *gormHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.
"updated_at": time.Now().UTC(),
}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID))
}
return nil
}
@@ -83,7 +83,7 @@ func (repository *gormHeartbeatMonitorRepository) UpdateQueueID(ctx context.Cont
"updated_at": time.Now().UTC(),
}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s]", monitorID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s]", monitorID))
}
return nil
}
@@ -100,7 +100,7 @@ func (repository *gormHeartbeatMonitorRepository) Delete(ctx context.Context, us
Where("owner = ?", owner).
Delete(&entities.HeartbeatMonitor{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", owner, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", owner, userID))
}
return nil
@@ -117,7 +117,7 @@ func (repository *gormHeartbeatMonitorRepository) Index(ctx context.Context, use
query := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("owner = ?", owner)
heartbeats := new([]entities.Heartbeat)
if err := query.Order("timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&heartbeats).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
}
return heartbeats, nil
@@ -132,7 +132,7 @@ func (repository *gormHeartbeatMonitorRepository) Store(ctx context.Context, hea
defer cancel()
if err := repository.db.WithContext(ctx).Create(heartbeatMonitor).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeatMonitor monitor with ID [%s]", heartbeatMonitor.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeatMonitor monitor with ID [%s]", heartbeatMonitor.ID))
}
return nil
@@ -152,11 +152,11 @@ func (repository *gormHeartbeatMonitorRepository) Load(ctx context.Context, user
Where("owner = ?", owner).
First(&phone).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, owner))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, owner))
}
return phone, nil
@@ -178,7 +178,7 @@ func (repository *gormHeartbeatMonitorRepository) Exists(ctx context.Context, us
Where("id = ?", monitorID).
Find(&exists).Error
if err != nil {
- return exists, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if heartbeat monitor exists with userID [%s] and montior ID [%s]", userID, monitorID))
+ return exists, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if heartbeat monitor exists with userID [%s] and montior ID [%s]", userID, monitorID))
}
return exists, nil
diff --git a/api/pkg/repositories/gorm_heartbeat_repository.go b/api/pkg/repositories/gorm_heartbeat_repository.go
index 544d7eed7..940718171 100644
--- a/api/pkg/repositories/gorm_heartbeat_repository.go
+++ b/api/pkg/repositories/gorm_heartbeat_repository.go
@@ -38,7 +38,7 @@ func (repository *gormHeartbeatRepository) DeleteAllForUser(ctx context.Context,
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Heartbeat{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID))
}
return nil
@@ -58,11 +58,11 @@ func (repository *gormHeartbeatRepository) Last(ctx context.Context, userID enti
Order("timestamp DESC").
First(&heartbeat).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner))
}
return heartbeat, nil
@@ -85,7 +85,7 @@ func (repository *gormHeartbeatRepository) Index(ctx context.Context, userID ent
heartbeats := new([]entities.Heartbeat)
err := query.Order("timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&heartbeats).Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
}
return heartbeats, nil
@@ -100,7 +100,7 @@ func (repository *gormHeartbeatRepository) Store(ctx context.Context, heartbeat
defer cancel()
if err := repository.db.WithContext(ctx).Create(heartbeat).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with ID [%s]", heartbeat.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with ID [%s]", heartbeat.ID))
}
return nil
diff --git a/api/pkg/repositories/gorm_integration_3cx_repository.go b/api/pkg/repositories/gorm_integration_3cx_repository.go
index c63e5370b..233f316c1 100644
--- a/api/pkg/repositories/gorm_integration_3cx_repository.go
+++ b/api/pkg/repositories/gorm_integration_3cx_repository.go
@@ -36,7 +36,7 @@ func (repository *gormIntegration3CxRepository) DeleteAllForUser(ctx context.Con
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Integration3CX{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Integration3CX{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Integration3CX{}, userID))
}
return nil
@@ -50,11 +50,11 @@ func (repository *gormIntegration3CxRepository) Load(ctx context.Context, userID
integration := new(entities.Integration3CX)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).First(&integration).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "[3cx] integration for user [%s] does not exist", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "[3cx] integration for user [%s] does not exist", userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load [3cx] integration for user [%s]", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load [3cx] integration for user [%s]", userID))
}
return integration, nil
@@ -66,7 +66,7 @@ func (repository *gormIntegration3CxRepository) Save(ctx context.Context, integr
defer span.End()
if err := repository.db.WithContext(ctx).Save(integration).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save [%T] with ID [%s]", integration, integration.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save [%T] with ID [%s]", integration, integration.ID))
}
return nil
diff --git a/api/pkg/repositories/gorm_message_repository.go b/api/pkg/repositories/gorm_message_repository.go
index bb9c55be7..a4bf309f5 100644
--- a/api/pkg/repositories/gorm_message_repository.go
+++ b/api/pkg/repositories/gorm_message_repository.go
@@ -40,7 +40,7 @@ func (repository *gormMessageRepository) DeleteAllForUser(ctx context.Context, u
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Message{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Message{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Message{}, userID))
}
return nil
@@ -58,7 +58,7 @@ func (repository *gormMessageRepository) DeleteByOwnerAndContact(ctx context.Con
Delete(&entities.Message{}).
Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete messages between owner [%s] and contact [%s] for user with ID [%s]", owner, contact, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete messages between owner [%s] and contact [%s] for user with ID [%s]", owner, contact, userID))
}
return nil
@@ -71,7 +71,7 @@ func (repository *gormMessageRepository) Delete(ctx context.Context, userID enti
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageID).Delete(&entities.Message{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, userID))
}
return nil
@@ -94,7 +94,7 @@ func (repository *gormMessageRepository) Index(ctx context.Context, userID entit
messages := new([]entities.Message)
if err := query.Order("order_timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&messages).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch messges with owner [%s] and contact [%s] and params [%+#v]", owner, contact, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch messges with owner [%s] and contact [%s] and params [%+#v]", owner, contact, params))
}
return messages, nil
@@ -114,11 +114,11 @@ func (repository *gormMessageRepository) LastMessage(ctx context.Context, userID
err := query.Order("order_timestamp DESC").First(&message).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact))
}
return message, nil
@@ -163,7 +163,7 @@ func (repository *gormMessageRepository) Search(ctx context.Context, userID enti
Find(&messages).
Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot search messages with for user [%s] params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot search messages with for user [%s] params [%+#v]", userID, params))
}
return messages, nil
@@ -193,7 +193,7 @@ func (repository *gormMessageRepository) GetBulkMessages(ctx context.Context, us
LIMIT ?
`, userID, limit).Scan(&orders).Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch bulk message orders for user [%s]", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch bulk message orders for user [%s]", userID))
}
return orders, nil
@@ -205,7 +205,7 @@ func (repository *gormMessageRepository) Store(ctx context.Context, message *ent
defer span.End()
if err := repository.db.WithContext(ctx).Create(message).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with ID [%s]", message.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with ID [%s]", message.ID))
}
return nil
@@ -219,11 +219,11 @@ func (repository *gormMessageRepository) Load(ctx context.Context, userID entiti
message := new(entities.Message)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageID).First(message).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "message with ID [%s] and userID [%s] does not exist", messageID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "message with ID [%s] and userID [%s] does not exist", messageID, userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with ID [%s]", messageID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with ID [%s]", messageID))
}
return message, nil
@@ -235,7 +235,7 @@ func (repository *gormMessageRepository) Update(ctx context.Context, message *en
defer span.End()
if err := repository.db.WithContext(ctx).Save(message).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with ID [%s]", message.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with ID [%s]", message.ID))
}
return nil
@@ -264,15 +264,15 @@ func (repository *gormMessageRepository) GetOutstanding(ctx context.Context, use
},
)
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch outstanding message with userID [%s] and messageID [%s]", userID, messageID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch outstanding message with userID [%s] and messageID [%s]", userID, messageID))
}
if message == nil || message.ID == uuid.Nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCode(ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCodef(ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID))
}
return message, nil
diff --git a/api/pkg/repositories/gorm_message_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go
index 49538ce05..a3d0be6c1 100644
--- a/api/pkg/repositories/gorm_message_send_schedule_repository.go
+++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go
@@ -41,7 +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.Propagatef(err, "cannot store send schedule [%s]", schedule.ID))
}
return nil
@@ -56,7 +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.Propagatef(err, "cannot update send schedule [%s]", schedule.ID))
}
return nil
@@ -79,11 +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 for user with ID [%s]", scheduleID, userID),
+ stacktrace.PropagateWithCodef(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.Propagatef(err, "cannot load send schedule [%s]", scheduleID))
}
return item, nil
@@ -103,7 +103,7 @@ func (r *gormMessageSendScheduleRepository) Index(
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 nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot index send schedules for user [%s]", userID))
}
return items, nil
@@ -123,7 +123,7 @@ func (r *gormMessageSendScheduleRepository) Delete(
Where("id = ?", scheduleID).
Delete(&entities.MessageSendSchedule{}).Error
if err != nil {
- return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete send schedule [%s]", scheduleID))
+ return r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete send schedule [%s]", scheduleID))
}
return nil
@@ -141,7 +141,7 @@ func (r *gormMessageSendScheduleRepository) DeleteAllForUser(
Where("user_id = ?", 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 r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete send schedules for user [%s]", userID))
}
return nil
@@ -161,7 +161,7 @@ func (r *gormMessageSendScheduleRepository) CountByUser(
Where("user_id = ?", 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 0, r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot count send schedules for user [%s]", userID))
}
return int(count), nil
diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go
index 88506f485..092418040 100644
--- a/api/pkg/repositories/gorm_message_thread_repository.go
+++ b/api/pkg/repositories/gorm_message_thread_repository.go
@@ -82,7 +82,7 @@ func (repository *gormMessageThreadRepository) DeleteAllForUser(ctx context.Cont
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.MessageThread{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.MessageThread{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.MessageThread{}, userID))
}
return nil
@@ -95,7 +95,7 @@ func (repository *gormMessageThreadRepository) Delete(ctx context.Context, userI
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageThreadID).Delete(&entities.MessageThread{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message thread with ID [%s] for user with ID [%s]", messageThreadID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message thread with ID [%s] for user with ID [%s]", messageThreadID, userID))
}
return nil
@@ -112,7 +112,7 @@ func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx con
Where("id = ?", params.MessageThreadID).
Updates(messageThreadDeletedUpdates(params))
if result.Error != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID))
}
return nil
@@ -141,7 +141,7 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread
Error
})
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message thread with ID [%s]", thread.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message thread with ID [%s]", thread.ID))
}
return nil
@@ -158,10 +158,10 @@ func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Contex
Where("id = ?", params.MessageThreadID).
Updates(messageThreadActivityUpdates(params))
if result.Error != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID))
}
if result.RowsAffected == 0 {
- return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID))
}
return nil
@@ -185,10 +185,10 @@ func (repository *gormMessageThreadRepository) UpdateStatus(
Where("id = ?", messageThreadID).
Updates(messageThreadStatusUpdates(params))
if result.Error != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID))
}
if result.RowsAffected == 0 {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID))
}
return thread, nil
@@ -209,11 +209,11 @@ func (repository *gormMessageThreadRepository) LoadByOwnerContact(ctx context.Co
First(thread).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "thread with owner [%s] and contact [%s] does not exist", owner, contact))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "thread with owner [%s] and contact [%s] does not exist", owner, contact))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load thread with owner [%s] and contact [%s]", owner, contact))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load thread with owner [%s] and contact [%s]", owner, contact))
}
return thread, nil
@@ -233,11 +233,11 @@ func (repository *gormMessageThreadRepository) Load(ctx context.Context, userID
First(thread).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "thread with id [%s] not found", ID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "thread with id [%s] not found", ID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "thread with id [%s]", ID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "thread with id [%s]", ID))
}
return thread, nil
@@ -270,7 +270,7 @@ func (repository *gormMessageThreadRepository) Index(ctx context.Context, userID
threads := new([]entities.MessageThread)
if err := query.Order("order_timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&threads).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch message threads with owner [%s] and params [%+#v]", owner, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch message threads with owner [%s] and params [%+#v]", owner, params))
}
return threads, nil
diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go
index 51502d1a7..aa64436c6 100644
--- a/api/pkg/repositories/gorm_phone_api_key_repository.go
+++ b/api/pkg/repositories/gorm_phone_api_key_repository.go
@@ -53,7 +53,7 @@ WHERE user_id = ? AND array_position(phone_ids, ?) IS NOT NULL;
Exec(query, phoneID, phoneNumber, userID, phoneID).
Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] and number [%s] for user with ID [%s] ", phoneID, phoneNumber, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] and number [%s] for user with ID [%s] ", phoneID, phoneNumber, userID))
}
repository.cache.Clear()
@@ -71,7 +71,7 @@ func (repository *gormPhoneAPIKeyRepository) CountByUser(ctx context.Context, us
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 0, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot count phone API keys for user [%s]", userID))
}
return int(count), nil
@@ -85,11 +85,11 @@ func (repository *gormPhoneAPIKeyRepository) Load(ctx context.Context, userID en
phoneAPIKey := new(entities.PhoneAPIKey)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", phoneAPIKeyID).First(&phoneAPIKey).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "[%T] with ID [%s] for user with ID [%s] does not exist", phoneAPIKey, phoneAPIKeyID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "[%T] with ID [%s] for user with ID [%s] does not exist", phoneAPIKey, phoneAPIKeyID, userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user with ID [%s]", phoneAPIKey, phoneAPIKeyID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user with ID [%s]", phoneAPIKey, phoneAPIKeyID, userID))
}
return phoneAPIKey, nil
@@ -100,7 +100,7 @@ func (repository *gormPhoneAPIKeyRepository) Create(ctx context.Context, phoneAP
defer span.End()
if err := repository.db.WithContext(ctx).Create(phoneAPIKey).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save phone API key with ID [%s] for user with ID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save phone API key with ID [%s] for user with ID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID))
}
return nil
@@ -117,11 +117,11 @@ func (repository *gormPhoneAPIKeyRepository) LoadAuthContext(ctx context.Context
phoneAPIKey := new(entities.PhoneAPIKey)
err := repository.db.WithContext(ctx).Where("api_key = ?", apiKey).First(phoneAPIKey).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone api key [%s] does not exist", apiKey))
+ return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone api key [%s] does not exist", apiKey))
}
if err != nil {
- return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone api key [%s]", apiKey))
+ return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone api key [%s]", apiKey))
}
authUser := entities.AuthContext{
@@ -132,7 +132,7 @@ func (repository *gormPhoneAPIKeyRepository) LoadAuthContext(ctx context.Context
}
if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 15*time.Second); !result {
- ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", authUser, phoneAPIKey.ID, result)))
+ ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", authUser, phoneAPIKey.ID, result)))
}
return authUser, nil
@@ -150,7 +150,7 @@ func (repository *gormPhoneAPIKeyRepository) Index(ctx context.Context, userID e
phoneAPIKeys := new([]*entities.PhoneAPIKey)
if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(phoneAPIKeys).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch phone API Keys with userID [%s] and params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch phone API Keys with userID [%s] and params [%+#v]", userID, params))
}
return *phoneAPIKeys, nil
@@ -162,7 +162,7 @@ func (repository *gormPhoneAPIKeyRepository) Delete(ctx context.Context, phoneAP
err := repository.db.WithContext(ctx).Delete(phoneAPIKey).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID))
}
repository.cache.Del(phoneAPIKey.APIKey)
@@ -184,7 +184,7 @@ WHERE user_id = ?;
Exec(query, phone.ID, phone.PhoneNumber, phone.UserID).
Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone.ID, phoneAPIKey.ID, phone.UserID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone.ID, phoneAPIKey.ID, phone.UserID))
}
query = `
@@ -197,13 +197,13 @@ WHERE array_position(phone_ids, ?) IS NULL AND id = ?;
Exec(query, phone.ID, phone.PhoneNumber, phone.ID, phoneAPIKey.ID).
Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID))
}
return nil
})
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID))
}
repository.cache.Clear()
return nil
@@ -223,7 +223,7 @@ WHERE id = ?;
Exec(query, phone.ID, phone.PhoneNumber, phoneAPIKey.ID).
Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone API key with ID [%s]", phone.ID, phoneAPIKey.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone API key with ID [%s]", phone.ID, phoneAPIKey.ID))
}
return nil
@@ -234,7 +234,7 @@ func (repository *gormPhoneAPIKeyRepository) DeleteAllForUser(ctx context.Contex
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.PhoneAPIKey{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.PhoneAPIKey{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.PhoneAPIKey{}, userID))
}
return nil
diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go
index 1301ecc02..c83aaf380 100644
--- a/api/pkg/repositories/gorm_phone_notification_repository.go
+++ b/api/pkg/repositories/gorm_phone_notification_repository.go
@@ -48,7 +48,7 @@ func (repository *gormPhoneNotificationRepository) DeleteAllForUser(
Delete(&entities.PhoneNotification{}).Error; err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot delete all [%T] for user with ID [%s]",
&entities.PhoneNotification{},
@@ -71,7 +71,7 @@ func (repository *gormPhoneNotificationRepository) DeleteByMessageID(ctx context
if err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(err, "cannot delete [%T] for user [%s] and message with ID [%s]", &entities.PhoneNotification{}, userID, messageID),
+ stacktrace.Propagatef(err, "cannot delete [%T] for user [%s] and message with ID [%s]", &entities.PhoneNotification{}, userID, messageID),
)
}
@@ -91,7 +91,7 @@ func (repository *gormPhoneNotificationRepository) UpdateStatus(ctx context.Cont
if err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot update notification [%s] with status [%s]",
notificationID,
@@ -132,7 +132,7 @@ func (repository *gormPhoneNotificationRepository) Schedule(
First(lastNotification).
Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
- return stacktrace.Propagate(
+ return stacktrace.Propagatef(
err,
"cannot fetch last notification with phone ID [%s]",
notification.PhoneID,
@@ -151,7 +151,7 @@ func (repository *gormPhoneNotificationRepository) Schedule(
}
if err = tx.WithContext(ctx).Create(notification).Error; err != nil {
- return stacktrace.Propagate(
+ return stacktrace.Propagatef(
err,
"cannot create new notification with id [%s] and schedule [%s]",
notification.ID,
@@ -164,7 +164,7 @@ func (repository *gormPhoneNotificationRepository) Schedule(
if err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot schedule phone notification with ID [%s]",
notification.ID,
@@ -207,7 +207,7 @@ func (repository *gormPhoneNotificationRepository) insert(
if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot store notification with id [%s]",
notification.ID,
@@ -235,7 +235,7 @@ func (repository *gormPhoneNotificationRepository) ScheduleExact(
Count(&count).Error; err != nil {
return repository.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(err, "cannot check for existing notification for message [%s]", notification.MessageID),
+ stacktrace.Propagatef(err, "cannot check for existing notification for message [%s]", notification.MessageID),
)
}
@@ -246,7 +246,7 @@ func (repository *gormPhoneNotificationRepository) ScheduleExact(
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),
+ stacktrace.Propagatef(err, "cannot create exact-time notification with id [%s]", notification.ID),
)
}
diff --git a/api/pkg/repositories/gorm_phone_repository.go b/api/pkg/repositories/gorm_phone_repository.go
index 98c2f3fb0..bf6d69e43 100644
--- a/api/pkg/repositories/gorm_phone_repository.go
+++ b/api/pkg/repositories/gorm_phone_repository.go
@@ -42,7 +42,7 @@ func (repository *gormPhoneRepository) DeleteAllForUser(ctx context.Context, use
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Phone{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Phone{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Phone{}, userID))
}
repository.cache.Clear()
@@ -60,7 +60,7 @@ func (repository *gormPhoneRepository) NullifyScheduleID(ctx context.Context, us
Where("message_send_schedule_id = ?", scheduleID).
Update("message_send_schedule_id", nil).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot nullify message_send_schedule_id [%s] for user [%s]", scheduleID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot nullify message_send_schedule_id [%s] for user [%s]", scheduleID, userID))
}
repository.cache.Clear()
@@ -78,11 +78,11 @@ func (repository *gormPhoneRepository) LoadByID(ctx context.Context, userID enti
Where("id = ?", phoneID).
First(&phone).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone with ID [%s] does not exist", phoneID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone with ID [%s] does not exist", phoneID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with ID [%s]", phoneID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with ID [%s]", phoneID))
}
return phone, nil
@@ -98,7 +98,7 @@ func (repository *gormPhoneRepository) Delete(ctx context.Context, userID entiti
Where("id = ?", phoneID).
Delete(&entities.Phone{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone with ID [%s] and userID [%s]", phoneID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone with ID [%s] and userID [%s]", phoneID, userID))
}
repository.cache.Clear()
@@ -115,14 +115,14 @@ func (repository *gormPhoneRepository) Save(ctx context.Context, phone *entities
ctxLogger.Info(fmt.Sprintf("phone with user [%s] and number[%s] already exists", phone.UserID, phone.PhoneNumber))
loadedPhone, err := repository.Load(ctx, phone.UserID, phone.PhoneNumber)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone for user [%s] and number [%s]", phone.UserID, phone.PhoneNumber))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone for user [%s] and number [%s]", phone.UserID, phone.PhoneNumber))
}
*phone = *loadedPhone
return nil
}
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save phone with ID [%s]", phone.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save phone with ID [%s]", phone.ID))
}
repository.cache.Del(repository.getCacheKey(phone.UserID, phone.PhoneNumber))
@@ -141,15 +141,15 @@ func (repository *gormPhoneRepository) Load(ctx context.Context, userID entities
phone := new(entities.Phone)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("phone_number = ?", phoneNumber).First(phone).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone with userID [%s] and phoneNumber [%s] does not exist", userID, phoneNumber))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone with userID [%s] and phoneNumber [%s] does not exist", userID, phoneNumber))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone phone with userID [%s] and phoneNumber [%s]", userID, phoneNumber))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone phone with userID [%s] and phoneNumber [%s]", userID, phoneNumber))
}
if result := repository.cache.SetWithTTL(repository.getCacheKey(userID, phoneNumber), phone, 1, 30*time.Minute); !result {
- ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", phone, phone.ID, result)))
+ ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", phone, phone.ID, result)))
}
return phone, nil
@@ -167,7 +167,7 @@ func (repository *gormPhoneRepository) Index(ctx context.Context, userID entitie
phones := new([]entities.Phone)
if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&phones).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch phones with userID [%s] and params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch phones with userID [%s] and params [%+#v]", userID, params))
}
return phones, nil
diff --git a/api/pkg/repositories/gorm_user_repository.go b/api/pkg/repositories/gorm_user_repository.go
index da46e5e73..f7c74f318 100644
--- a/api/pkg/repositories/gorm_user_repository.go
+++ b/api/pkg/repositories/gorm_user_repository.go
@@ -48,7 +48,7 @@ func (repository *gormUserRepository) Delete(ctx context.Context, user *entities
defer span.End()
if err := repository.db.WithContext(ctx).Delete(user).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete user with ID [%s]", user.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete user with ID [%s]", user.ID))
}
return nil
@@ -60,7 +60,7 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e
apiKey, err := repository.generateAPIKey(64)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate apiKey for user [%s]", userID)
+ return nil, stacktrace.Propagatef(err, "cannot generate apiKey for user [%s]", userID)
}
user := new(entities.User)
@@ -79,7 +79,7 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e
},
)
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID))
}
if err == nil && oldAPIKey != "" {
@@ -102,11 +102,11 @@ func (repository *gormUserRepository) LoadBySubscriptionID(ctx context.Context,
First(user).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with subscriptionID [%s] does not exist", subscriptionID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with subscriptionID [%s] does not exist", subscriptionID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", subscriptionID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", subscriptionID))
}
return user, nil
@@ -122,11 +122,11 @@ func (repository *gormUserRepository) LoadByEmail(ctx context.Context, email str
First(user).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with email [%s] does not exist", email))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with email [%s] does not exist", email))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with email ID [%s]", email))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with email ID [%s]", email))
}
return user, nil
@@ -137,7 +137,7 @@ func (repository *gormUserRepository) Store(ctx context.Context, user *entities.
defer span.End()
if err := repository.db.WithContext(ctx).Create(user).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with ID [%s]", user.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with ID [%s]", user.ID))
}
return nil
@@ -148,7 +148,7 @@ func (repository *gormUserRepository) Update(ctx context.Context, user *entities
defer span.End()
if err := repository.db.WithContext(ctx).Save(user).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update user with ID [%s]", user.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update user with ID [%s]", user.ID))
}
return nil
@@ -160,7 +160,7 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe
if authUser, found := repository.cache.Get(apiKey); found {
if authUser.IsNoop() {
- return authUser, repository.tracer.WrapErrorSpan(span, stacktrace.NewError("user with api key [%s] does not exist", apiKey))
+ return authUser, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("user with api key [%s] does not exist", apiKey))
}
return authUser, nil
}
@@ -170,11 +170,11 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe
if errors.Is(err, gorm.ErrRecordNotFound) {
repository.cache.SetWithTTL(apiKey, entities.AuthContext{}, 1, 2*time.Hour)
- return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with api key [%s] does not exist", apiKey))
+ return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with api key [%s] does not exist", apiKey))
}
if err != nil {
- return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with api key [%s]", apiKey))
+ return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with api key [%s]", apiKey))
}
authUser := entities.AuthContext{
@@ -183,7 +183,7 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe
}
if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 2*time.Hour); !result {
- ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", authUser, user.ID, result)))
+ ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", authUser, user.ID, result)))
}
return authUser, nil
@@ -196,11 +196,11 @@ func (repository *gormUserRepository) Load(ctx context.Context, userID entities.
user := new(entities.User)
err := repository.db.WithContext(ctx).First(user, userID).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s]", userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s]", userID))
}
return user, nil
@@ -217,7 +217,7 @@ func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser
apiKey, err := repository.generateAPIKey(64)
if err != nil {
- return nil, false, stacktrace.Propagate(err, "cannot generate apiKey for user [%s]", authUser.ID)
+ return nil, false, stacktrace.Propagatef(err, "cannot generate apiKey for user [%s]", authUser.ID)
}
user = &entities.User{
@@ -241,7 +241,7 @@ func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser
return result.Error
})
if err != nil {
- return user, isNew, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create user from auth user [%+#v]", authUser))
+ return user, isNew, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create user from auth user [%+#v]", authUser))
}
return user, isNew, nil
@@ -255,7 +255,7 @@ func (repository *gormUserRepository) generateRandomBytes(n int) ([]byte, error)
b := make([]byte, n)
// Note that err == nil only if we read len(b) bytes.
if _, err := rand.Read(b); err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate [%d] random bytes", n)
+ return nil, stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n)
}
return b, nil
@@ -268,5 +268,5 @@ func (repository *gormUserRepository) generateRandomBytes(n int) ([]byte, error)
// case the caller should not continue.
func (repository *gormUserRepository) generateAPIKey(n int) (string, error) {
b, err := repository.generateRandomBytes(n)
- return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagate(err, "cannot generate random bytes")
+ return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagatef(err, "cannot generate random bytes")
}
diff --git a/api/pkg/repositories/gorm_webhook_repository.go b/api/pkg/repositories/gorm_webhook_repository.go
index c56bd3a73..5cf23b077 100644
--- a/api/pkg/repositories/gorm_webhook_repository.go
+++ b/api/pkg/repositories/gorm_webhook_repository.go
@@ -37,7 +37,7 @@ func (repository *gormWebhookRepository) DeleteAllForUser(ctx context.Context, u
defer span.End()
if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Webhook{}).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Webhook{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Webhook{}, userID))
}
return nil
@@ -48,7 +48,7 @@ func (repository *gormWebhookRepository) Save(ctx context.Context, webhook *enti
defer span.End()
if err := repository.db.WithContext(ctx).Save(webhook).Error; err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update webhook with ID [%s]", webhook.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update webhook with ID [%s]", webhook.ID))
}
return nil
@@ -67,7 +67,7 @@ func (repository *gormWebhookRepository) Index(ctx context.Context, userID entit
webhooks := make([]*entities.Webhook, 0)
if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&webhooks).Error; err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch webhooks for user [%s] and params [%+#v]", userID, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch webhooks for user [%s] and params [%+#v]", userID, params))
}
return webhooks, nil
@@ -83,7 +83,7 @@ func (repository *gormWebhookRepository) LoadByEvent(ctx context.Context, userID
Scan(&webhooks).
Error
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load webhooks for user with ID [%s] and event [%s]", userID, event))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load webhooks for user with ID [%s] and event [%s]", userID, event))
}
return webhooks, nil
@@ -96,11 +96,11 @@ func (repository *gormWebhookRepository) Load(ctx context.Context, userID entiti
webhook := new(entities.Webhook)
err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", webhookID).First(&webhook).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "webhook with ID [%s] for user [%s] does not exist", webhookID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "webhook with ID [%s] for user [%s] does not exist", webhookID, userID))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load webhook with ID [%s] for user [%s]", webhookID, userID))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load webhook with ID [%s] for user [%s]", webhookID, userID))
}
return webhook, nil
@@ -115,7 +115,7 @@ func (repository *gormWebhookRepository) Delete(ctx context.Context, userID enti
Where("id = ?", webhookID).
Delete(&entities.Webhook{}).Error
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete webhook with ID [%s] and userID [%s]", webhookID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete webhook with ID [%s] and userID [%s]", webhookID, userID))
}
return nil
diff --git a/api/pkg/repositories/memory_attachment_repository.go b/api/pkg/repositories/memory_attachment_repository.go
index eab19336a..d6070d65d 100644
--- a/api/pkg/repositories/memory_attachment_repository.go
+++ b/api/pkg/repositories/memory_attachment_repository.go
@@ -44,7 +44,7 @@ func (s *MemoryAttachmentRepository) Download(ctx context.Context, path string)
value, ok := s.data.Load(path)
if !ok {
- return nil, s.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCode(ErrCodeNotFound, "attachment not found at path [%s]", path))
+ return nil, s.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCodef(ErrCodeNotFound, "attachment not found at path [%s]", path))
}
return value.([]byte), nil
}
diff --git a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
index 2f6f71cdb..fea05e993 100644
--- a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
+++ b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go
@@ -43,7 +43,7 @@ func (repository *mongoHeartbeatMonitorRepository) Store(ctx context.Context, mo
_, err := repository.collection.InsertOne(ctx, monitor)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat monitor with ID [%s]", monitor.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat monitor with ID [%s]", monitor.ID))
}
return nil
@@ -64,10 +64,10 @@ func (repository *mongoHeartbeatMonitorRepository) Load(ctx context.Context, use
var monitor entities.HeartbeatMonitor
err := repository.collection.FindOne(ctx, filter).Decode(&monitor)
if err == mongo.ErrNoDocuments {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber))
}
return &monitor, nil
@@ -87,7 +87,7 @@ func (repository *mongoHeartbeatMonitorRepository) Exists(ctx context.Context, u
count, err := repository.collection.CountDocuments(ctx, filter)
if err != nil {
- return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID))
+ return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID))
}
return count > 0, nil
@@ -108,7 +108,7 @@ func (repository *mongoHeartbeatMonitorRepository) UpdateQueueID(ctx context.Con
_, err := repository.collection.UpdateOne(ctx, filter, update)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s]", monitorID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s]", monitorID))
}
return nil
@@ -128,7 +128,7 @@ func (repository *mongoHeartbeatMonitorRepository) Delete(ctx context.Context, u
_, err := repository.collection.DeleteMany(ctx, filter)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID))
}
return nil
@@ -152,7 +152,7 @@ func (repository *mongoHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context
_, err := repository.collection.UpdateOne(ctx, filter, update)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID))
}
return nil
@@ -167,7 +167,7 @@ func (repository *mongoHeartbeatMonitorRepository) DeleteAllForUser(ctx context.
_, err := repository.collection.DeleteMany(ctx, bson.D{{Key: "user_id", Value: string(userID)}})
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID))
}
return nil
diff --git a/api/pkg/repositories/mongo_heartbeat_repository.go b/api/pkg/repositories/mongo_heartbeat_repository.go
index e97e696a2..44ed280db 100644
--- a/api/pkg/repositories/mongo_heartbeat_repository.go
+++ b/api/pkg/repositories/mongo_heartbeat_repository.go
@@ -42,7 +42,7 @@ func (repository *mongoHeartbeatRepository) Store(ctx context.Context, heartbeat
_, err := repository.collection.InsertOne(ctx, heartbeat)
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with ID [%s]", heartbeat.ID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with ID [%s]", heartbeat.ID))
}
return nil
@@ -71,13 +71,13 @@ func (repository *mongoHeartbeatRepository) Index(ctx context.Context, userID en
cursor, err := repository.collection.Find(ctx, filter, opts)
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params))
}
defer cursor.Close(ctx)
var heartbeats []entities.Heartbeat
if err = cursor.All(ctx, &heartbeats); err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode heartbeats for owner [%s]", owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode heartbeats for owner [%s]", owner))
}
if heartbeats == nil {
@@ -104,10 +104,10 @@ func (repository *mongoHeartbeatRepository) Last(ctx context.Context, userID ent
var heartbeat entities.Heartbeat
err := repository.collection.FindOne(ctx, filter, opts).Decode(&heartbeat)
if err == mongo.ErrNoDocuments {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner))
}
if err != nil {
- return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner))
+ return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner))
}
return &heartbeat, nil
@@ -122,7 +122,7 @@ func (repository *mongoHeartbeatRepository) DeleteAllForUser(ctx context.Context
_, err := repository.collection.DeleteMany(ctx, bson.D{{Key: "user_id", Value: string(userID)}})
if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID))
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID))
}
return nil
diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go
index 74ea7446f..ebefc0f8b 100644
--- a/api/pkg/repositories/mongodb.go
+++ b/api/pkg/repositories/mongodb.go
@@ -52,7 +52,7 @@ func newMongoRegistry() *bson.Registry {
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")
+ return nil, stacktrace.Propagatef(err, "cannot parse database name from MongoDB URI")
}
pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -68,11 +68,11 @@ func NewMongoDB(uri string) (*mongo.Database, error) {
client, err := mongo.Connect(opts)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot connect to MongoDB Atlas")
+ return nil, stacktrace.Propagatef(err, "cannot connect to MongoDB Atlas")
}
if err = client.Ping(pingCtx, nil); err != nil {
- return nil, stacktrace.Propagate(err, "cannot ping MongoDB Atlas")
+ return nil, stacktrace.Propagatef(err, "cannot ping MongoDB Atlas")
}
db := client.Database(dbName)
@@ -81,7 +81,7 @@ func NewMongoDB(uri string) (*mongo.Database, error) {
defer indexCancel()
if err = createMongoIndexes(indexCtx, db); err != nil {
- return nil, stacktrace.Propagate(err, "cannot create MongoDB indexes")
+ return nil, stacktrace.Propagatef(err, "cannot create MongoDB indexes")
}
return db, nil
@@ -91,12 +91,12 @@ func NewMongoDB(uri string) (*mongo.Database, error) {
func parseMongoDBName(uri string) (string, error) {
parsed, err := url.Parse(uri)
if err != nil {
- return "", stacktrace.Propagate(err, "cannot parse MongoDB URI [%s]", uri)
+ return "", stacktrace.Propagatef(err, "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 "", stacktrace.NewErrorf("MongoDB URI is missing the 'appName' query parameter which is used as the database name")
}
return appName, nil
@@ -110,7 +110,7 @@ func createMongoIndexes(ctx context.Context, db *mongo.Database) error {
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "owner", Value: 1}, {Key: "timestamp", Value: -1}}},
})
if err != nil {
- return stacktrace.Propagate(err, "cannot create indexes on heartbeats collection")
+ return stacktrace.Propagatef(err, "cannot create indexes on heartbeats collection")
}
// Heartbeat monitors indexes
@@ -119,7 +119,7 @@ func createMongoIndexes(ctx context.Context, db *mongo.Database) error {
{Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "owner", Value: 1}}},
})
if err != nil {
- return stacktrace.Propagate(err, "cannot create indexes on heartbeat_monitors collection")
+ return stacktrace.Propagatef(err, "cannot create indexes on heartbeat_monitors collection")
}
return nil
diff --git a/api/pkg/services/billing_service.go b/api/pkg/services/billing_service.go
index 6b5749933..e94002d1d 100644
--- a/api/pkg/services/billing_service.go
+++ b/api/pkg/services/billing_service.go
@@ -55,13 +55,13 @@ func (service *BillingService) IsEntitledWithCount(ctx context.Context, userID e
user, err := service.userRepository.Load(ctx, userID)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s], entitlement successful", userID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s], entitlement successful", userID)))
return nil
}
usage, err := service.billingUsageRepository.GetCurrent(ctx, userID)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load billing usage for user with ID [%s], entitlement successful", userID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load billing usage for user with ID [%s], entitlement successful", userID)))
return nil
}
@@ -102,18 +102,18 @@ func (service *BillingService) sendLimitExceededEmail(ctx context.Context, user
email, err := service.emailFactory.UsageLimitExceeded(user, usage)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot create usage limit email for user [%s]", user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot create usage limit email for user [%s]", user.ID))
return
}
if err = service.mailer.Send(ctx, email); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "canot send usage limit exceeded notification to user [%s]", user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "canot send usage limit exceeded notification to user [%s]", user.ID))
return
}
ctxLogger.Info(fmt.Sprintf("usage limit exceeded email sent to user [%s]", user.ID))
if err = service.cache.Set(ctx, key, "", time.Hour*12); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot set item in redis with key [%s]", key))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot set item in redis with key [%s]", key))
}
}
@@ -141,7 +141,7 @@ func (service *BillingService) RegisterSentMessage(ctx context.Context, messageI
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.billingUsageRepository.RegisterSentMessage(ctx, timestamp, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not register [sent] message with ID [%s] for user with ID [%s]", messageID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not register [sent] message with ID [%s] for user with ID [%s]", messageID, userID))
}
ctxLogger.Info(fmt.Sprintf("registered [sent] message with ID [%s] for user [%s]", messageID, userID))
@@ -157,7 +157,7 @@ func (service *BillingService) RegisterReceivedMessage(ctx context.Context, mess
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.billingUsageRepository.RegisterReceivedMessage(ctx, timestamp, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not register [received] message with ID [%s] for user with ID [%s]", messageID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not register [received] message with ID [%s] for user with ID [%s]", messageID, userID))
}
ctxLogger.Info(fmt.Sprintf("registered [received] message with ID [%s] for user [%s]", messageID, userID))
@@ -171,7 +171,7 @@ func (service *BillingService) DeleteAllForUser(ctx context.Context, userID enti
defer span.End()
if err := service.billingUsageRepository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.BillingUsage] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.BillingUsage] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.BillingUsage] for user with ID [%s]", userID))
@@ -184,13 +184,13 @@ func (service *BillingService) sendUsageAlert(ctx context.Context, userID entiti
user, err := service.userRepository.Load(ctx, userID)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s]", userID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s]", userID)))
return
}
billingUsage, err := service.billingUsageRepository.GetCurrent(ctx, userID)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load billing usage for user with ID [%s]", userID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load billing usage for user with ID [%s]", userID)))
return
}
@@ -200,12 +200,12 @@ func (service *BillingService) sendUsageAlert(ctx context.Context, userID entiti
email, err := service.emailFactory.UsageLimitAlert(user, billingUsage)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot create usage alert email for user [%s]", user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot create usage alert email for user [%s]", user.ID))
return
}
if err = service.mailer.Send(ctx, email); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "canot send usage alert notification to user [%s]", user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "canot send usage alert notification to user [%s]", user.ID))
}
ctxLogger.Info(fmt.Sprintf("usage alert email sent to user [%s]", user.ID))
diff --git a/api/pkg/services/discord_service.go b/api/pkg/services/discord_service.go
index e919109b3..68bbbdc7b 100644
--- a/api/pkg/services/discord_service.go
+++ b/api/pkg/services/discord_service.go
@@ -59,7 +59,7 @@ func (service *DiscordService) DeleteAllForUser(ctx context.Context, userID enti
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Discord] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Discord] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Discord] for user with ID [%s]", userID))
@@ -75,7 +75,7 @@ func (service *DiscordService) Index(ctx context.Context, userID entities.UserID
discordIntegrations, err := service.repository.Index(ctx, userID, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch discord integrations with params [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch discord integrations with params [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] discord integrations with prams [%+#v]", len(discordIntegrations), params))
@@ -90,11 +90,11 @@ func (service *DiscordService) Delete(ctx context.Context, userID entities.UserI
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if _, err := service.repository.Load(ctx, userID, discordID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", userID, discordID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", userID, discordID))
}
if err := service.repository.Delete(ctx, userID, discordID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete discord integration with id [%s] and discordID [%s]", discordID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete discord integration with id [%s] and discordID [%s]", discordID, userID))
}
ctxLogger.Info(fmt.Sprintf("deleted discord integration with id [%s] and user id [%s]", discordID, userID))
@@ -115,7 +115,7 @@ func (service *DiscordService) Store(ctx context.Context, params *DiscordStorePa
defer span.End()
if err := service.createSlashCommand(ctx, params.ServerID); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", params.ServerID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", params.ServerID))
}
discordIntegration := &entities.Discord{
@@ -129,7 +129,7 @@ func (service *DiscordService) Store(ctx context.Context, params *DiscordStorePa
}
if err := service.repository.Save(ctx, discordIntegration); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save discord integration with id [%s]", discordIntegration.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save discord integration with id [%s]", discordIntegration.ID))
}
ctxLogger.Info(fmt.Sprintf("discord integration saved with id [%s] in the [%T]", discordIntegration.ID, service.repository))
@@ -172,7 +172,7 @@ func (service *DiscordService) createSlashCommand(ctx context.Context, serverID
},
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", serverID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", serverID))
}
ctxLogger.Info(fmt.Sprintf("upserted a slash command with ID [%s] for discord server [%s] and applicationID [%s]", command.ID, serverID, command.ApplicationID))
@@ -195,11 +195,11 @@ func (service *DiscordService) Update(ctx context.Context, params *DiscordUpdate
discordIntegration, err := service.repository.Load(ctx, params.UserID, params.DiscordID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", params.UserID, params.DiscordID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", params.UserID, params.DiscordID))
}
if err = service.createSlashCommand(ctx, params.ServerID); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", params.ServerID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", params.ServerID))
}
discordIntegration.Name = params.Name
@@ -207,7 +207,7 @@ func (service *DiscordService) Update(ctx context.Context, params *DiscordUpdate
discordIntegration.IncomingChannelID = params.IncomingChannelID
if err = service.repository.Save(ctx, discordIntegration); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save discord integration with id [%s] after update", discordIntegration.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save discord integration with id [%s] after update", discordIntegration.ID))
}
ctxLogger.Info(fmt.Sprintf("discord integration updated with id [%s] in the [%T]", discordIntegration.ID, service.repository))
@@ -221,7 +221,7 @@ func (service *DiscordService) HandleMessageReceived(ctx context.Context, userID
discordIntegrations, err := service.repository.FetchHavingIncomingChannel(ctx, userID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integrations for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integrations for user with ID [%s]", userID))
}
if len(discordIntegrations) == 0 {
@@ -248,14 +248,14 @@ func (service *DiscordService) sendMessage(ctx context.Context, event cloudevent
payload := new(events.MessagePhoneReceivedPayload)
if err := event.DataAs(payload); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload))
return
}
request := service.createDiscordMessage(ctxLogger, payload)
message, response, err := service.client.Channel.CreateMessage(ctx, discord.IncomingChannelID, request)
if err != nil {
- ctxLogger.Warn(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] event to discord channel [%s] for user [%s]", event.Type(), discord.IncomingChannelID, discord.UserID)))
+ ctxLogger.Warn(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] event to discord channel [%s] for user [%s]", event.Type(), discord.IncomingChannelID, discord.UserID)))
eventPayload := &events.DiscordSendFailedPayload{
DiscordID: discord.ID,
@@ -315,12 +315,12 @@ func (service *DiscordService) handleDiscordMessageFailed(ctx context.Context, s
event, err := service.createEvent(events.EventTypeDiscordSendFailed, source, payload)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user with id [%s]", events.EventTypeDiscordSendFailed, payload.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user with id [%s]", events.EventTypeDiscordSendFailed, payload.UserID)))
return
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID)))
return
}
diff --git a/api/pkg/services/email_notification_service.go b/api/pkg/services/email_notification_service.go
index a87fcf227..d72b3ee0c 100644
--- a/api/pkg/services/email_notification_service.go
+++ b/api/pkg/services/email_notification_service.go
@@ -66,7 +66,7 @@ func (service *EmailNotificationService) NotifyMessageExpired(ctx context.Contex
user, err := service.userRepository.Load(ctx, payload.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
}
if !user.NotificationMessageStatusEnabled {
@@ -76,11 +76,11 @@ func (service *EmailNotificationService) NotifyMessageExpired(ctx context.Contex
email, err := service.factory.MessageExpired(user, payload)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID))
}
ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for message with ID [%s]", events.EventTypeMessageSendExpired, user.ID, payload.MessageID))
@@ -101,7 +101,7 @@ func (service *EmailNotificationService) NotifyMessageFailed(ctx context.Context
user, err := service.userRepository.Load(ctx, payload.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
}
if !user.NotificationMessageStatusEnabled {
@@ -111,11 +111,11 @@ func (service *EmailNotificationService) NotifyMessageFailed(ctx context.Context
email, err := service.factory.MessageFailed(user, payload)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID))
}
ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for message with ID [%s]", events.EventTypeMessageSendFailed, user.ID, payload.ID))
@@ -136,7 +136,7 @@ func (service *EmailNotificationService) NotifyWebhookSendFailed(ctx context.Con
user, err := service.userRepository.Load(ctx, payload.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event with ID [%s]", payload.UserID, events.EventTypeWebhookSendFailed, payload.EventID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event with ID [%s]", payload.UserID, events.EventTypeWebhookSendFailed, payload.EventID))
}
if !user.NotificationWebhookEnabled {
@@ -146,11 +146,11 @@ func (service *EmailNotificationService) NotifyWebhookSendFailed(ctx context.Con
email, err := service.factory.WebhookSendFailed(user, payload)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID))
}
ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, user.ID, payload.EventType, payload.EventID))
@@ -171,7 +171,7 @@ func (service *EmailNotificationService) NotifyDiscordSendFailed(ctx context.Con
user, err := service.userRepository.Load(ctx, payload.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event for message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event for message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID))
}
if !user.NotificationWebhookEnabled {
@@ -181,11 +181,11 @@ func (service *EmailNotificationService) NotifyDiscordSendFailed(ctx context.Con
email, err := service.factory.DiscordSendFailed(user, payload)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] for [%s] event and message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] for [%s] event and message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.MessageID))
}
ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for [%s] event with ID [%s]", payload.EventType, user.ID, payload.EventType, payload.MessageID))
@@ -209,6 +209,6 @@ func (service *EmailNotificationService) addToCache(ctx context.Context, timeout
cacheKey := service.getCacheKey(event, owner)
if err := service.cache.Set(ctx, cacheKey, "", timeout); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot set item in redis with key [%s] for owner [%s]", cacheKey, owner))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot set item in redis with key [%s] for owner [%s]", cacheKey, owner))
}
}
diff --git a/api/pkg/services/emulator_fcm_client.go b/api/pkg/services/emulator_fcm_client.go
index adf3ed7c5..6b890a14a 100644
--- a/api/pkg/services/emulator_fcm_client.go
+++ b/api/pkg/services/emulator_fcm_client.go
@@ -65,34 +65,34 @@ func (c *EmulatorFCMClient) Send(ctx context.Context, message *messaging.Message
body, err := json.Marshal(payload)
if err != nil {
- return "", stacktrace.Propagate(err, "cannot marshal FCM request for emulator")
+ return "", stacktrace.Propagatef(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")
+ return "", stacktrace.Propagatef(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, "cannot send FCM to emulator at [%s]", url)
+ return "", stacktrace.Propagatef(err, "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")
+ return "", stacktrace.Propagatef(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))
+ return "", stacktrace.NewErrorf("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")
+ return "", stacktrace.Propagatef(err, "cannot decode emulator FCM response")
}
c.logger.Info(fmt.Sprintf("emulator FCM sent successfully: %s", result.Name))
diff --git a/api/pkg/services/emulator_push_queue.go b/api/pkg/services/emulator_push_queue.go
index 3e99a2369..634689f1e 100644
--- a/api/pkg/services/emulator_push_queue.go
+++ b/api/pkg/services/emulator_push_queue.go
@@ -74,7 +74,7 @@ func (queue *emulatorPushQueue) push(task PushQueueTask, queueID string) func()
request.Header("Content-Type", "application/json")
if err := request.Fetch(ctx); err != nil {
- queue.logger.Error(stacktrace.Propagate(err, "cannot send http request to [%s] for queue task [%s]", task.URL, queueID))
+ queue.logger.Error(stacktrace.Propagatef(err, "cannot send http request to [%s] for queue task [%s]", task.URL, queueID))
return
}
diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go
index a4a4ab891..96a5af144 100644
--- a/api/pkg/services/entitlement_service.go
+++ b/api/pkg/services/entitlement_service.go
@@ -79,7 +79,7 @@ func (service *EntitlementService) Check(
if err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(err, "cannot load user [%s] for entitlement check", userID),
+ stacktrace.Propagatef(err, "cannot load user [%s] for entitlement check", userID),
)
}
@@ -92,7 +92,7 @@ func (service *EntitlementService) Check(
if err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(err, "cannot count entities [%s] for user [%s]", entityName, userID),
+ stacktrace.Propagatef(err, "cannot count entities [%s] for user [%s]", entityName, userID),
)
}
diff --git a/api/pkg/services/event_dispatcher_service.go b/api/pkg/services/event_dispatcher_service.go
index a090c943a..245ab0b2d 100644
--- a/api/pkg/services/event_dispatcher_service.go
+++ b/api/pkg/services/event_dispatcher_service.go
@@ -53,7 +53,7 @@ func (dispatcher *EventDispatcher) DispatchSync(ctx context.Context, event cloud
defer span.End()
if err := event.Validate(); err != nil {
- return dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type()))
+ return dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type()))
}
dispatcher.Publish(ctx, event)
@@ -66,16 +66,16 @@ func (dispatcher *EventDispatcher) DispatchWithTimeout(ctx context.Context, even
defer span.End()
if err = event.Validate(); err != nil {
- return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type()))
+ return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type()))
}
task, err := dispatcher.createCloudTask(event)
if err != nil {
- return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create cloud task for event [%s] with id [%s]", event.Type(), event.ID()))
+ return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create cloud task for event [%s] with id [%s]", event.Type(), event.ID()))
}
if queueID, err = dispatcher.enqueue(ctx, event, task, timeout); err != nil {
- return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot enqueue event with ID [%s] and type [%s]", event.ID(), event.Type()))
+ return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot enqueue event with ID [%s] and type [%s]", event.ID(), event.Type()))
}
return queueID, nil
@@ -87,7 +87,7 @@ func (dispatcher *EventDispatcher) enqueue(ctx context.Context, event cloudevent
queueID, err := dispatcher.queue.Enqueue(ctx, task, timeout)
if errors.Is(err, context.DeadlineExceeded) {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot enqueue event with ID [%s] and type [%s] to [%T]", event.ID(), event.Type(), dispatcher.queue))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot enqueue event with ID [%s] and type [%s] to [%T]", event.ID(), event.Type(), dispatcher.queue))
queueID, err = fmt.Sprintf("local-%s", event.ID()), nil
time.AfterFunc(timeout, func() {
dispatcher.Publish(ctx, event)
@@ -133,7 +133,7 @@ func (dispatcher *EventDispatcher) Publish(ctx context.Context, event cloudevent
wg.Add(1)
go func(ctx context.Context, sub events.EventListener) {
if err := sub(ctx, event); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "subscriber [%T] cannot handle event [%s]", sub, event.Type()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "subscriber [%T] cannot handle event [%s]", sub, event.Type()))
}
wg.Done()
}(ctx, sub)
@@ -163,7 +163,7 @@ func (dispatcher *EventDispatcher) addCloudEventAttributes(span trace.Span, even
func (dispatcher *EventDispatcher) createCloudTask(event cloudevents.Event) (*PushQueueTask, error) {
eventContent, err := json.Marshal(event)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot marshall [%T] with ID [%s]", event, event.ID())
+ return nil, stacktrace.Propagatef(err, "cannot marshall [%T] with ID [%s]", event, event.ID())
}
return &PushQueueTask{
diff --git a/api/pkg/services/google_cloud_push_queue_service.go b/api/pkg/services/google_cloud_push_queue_service.go
index 7f9555207..2208cf218 100644
--- a/api/pkg/services/google_cloud_push_queue_service.go
+++ b/api/pkg/services/google_cloud_push_queue_service.go
@@ -81,8 +81,8 @@ func (queue *googlePushQueue) enqueueImpl(ctx context.Context, task *PushQueueTa
queueTask, err := queue.client.CreateTask(requestCtx, req)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL))
- return queueID, queue.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL))
+ return queueID, queue.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL))
}
ctxLogger.Info(fmt.Sprintf(
diff --git a/api/pkg/services/heartbeat_service.go b/api/pkg/services/heartbeat_service.go
index 1f02e9c4b..7441d2aa9 100644
--- a/api/pkg/services/heartbeat_service.go
+++ b/api/pkg/services/heartbeat_service.go
@@ -55,11 +55,11 @@ func (service *HeartbeatService) DeleteAllForUser(ctx context.Context, userID en
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Heartbeat] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Heartbeat] for user with ID [%s]", userID))
}
if err := service.monitorRepository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.HeartbeatMonitor] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.HeartbeatMonitor] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Heartbeat] and [entities.HeartbeatMonitor] for user with ID [%s]", userID))
@@ -75,7 +75,7 @@ func (service *HeartbeatService) Index(ctx context.Context, userID entities.User
heartbeats, err := service.repository.Index(ctx, userID, owner, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch heartbeats with parms [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch heartbeats with parms [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*heartbeats), params))
@@ -109,7 +109,7 @@ func (service *HeartbeatService) Store(ctx context.Context, params HeartbeatStor
}
if err := service.repository.Store(ctx, heartbeat); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with id [%s]", heartbeat.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with id [%s]", heartbeat.ID))
}
ctxLogger.Info(fmt.Sprintf("heartbeat saved with id [%s] for user [%s]", heartbeat.ID, heartbeat.UserID))
@@ -120,7 +120,7 @@ func (service *HeartbeatService) Store(ctx context.Context, params HeartbeatStor
return heartbeat, nil
}
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load heartbeat monitor for owner [%s] and user [%s]", params.Owner, params.UserID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot load heartbeat monitor for owner [%s] and user [%s]", params.Owner, params.UserID))
return heartbeat, nil
}
@@ -145,7 +145,7 @@ func (service *HeartbeatService) handleHeartbeatWhenPhoneWasOffline(ctx context.
defer span.End()
if err := service.UpdatePhoneOnline(ctx, monitor.UserID, monitor.ID, true); err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone online status for heartbeat monitor [%s]", monitor.ID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone online status for heartbeat monitor [%s]", monitor.ID)))
}
event, err := service.createEvent(events.EventTypePhoneHeartbeatOnline, source, &events.PhoneHeartbeatOnlinePayload{
@@ -157,12 +157,12 @@ func (service *HeartbeatService) handleHeartbeatWhenPhoneWasOffline(ctx context.
Owner: heartbeat.Owner,
})
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for monitor with ID [%s]", events.EventTypePhoneHeartbeatOnline, monitor.ID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for monitor with ID [%s]", events.EventTypePhoneHeartbeatOnline, monitor.ID)))
return
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), monitor.PhoneID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), monitor.PhoneID)))
return
}
@@ -177,7 +177,7 @@ func (service *HeartbeatService) StoreMonitor(ctx context.Context, params *Heart
monitor, scheduleCheck, err := service.phoneMonitor(ctx, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create monitor for with userID [%s] and owner [%s]", params.UserID, params.Owner))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create monitor for with userID [%s] and owner [%s]", params.UserID, params.Owner))
}
if !scheduleCheck {
@@ -195,7 +195,7 @@ func (service *HeartbeatService) StoreMonitor(ctx context.Context, params *Heart
Source: params.Source,
}
if err = service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), monitorParams); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule healthcheck for monitor [%s] with owner [%s] and userID [%s]", monitor.ID, params.Owner, params.UserID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule healthcheck for monitor [%s] with owner [%s] and userID [%s]", monitor.ID, params.Owner, params.UserID))
}
return monitor, nil
@@ -218,7 +218,7 @@ func (service *HeartbeatService) phoneMonitor(ctx context.Context, params *Heart
}
if err = service.monitorRepository.Store(ctx, monitor); err != nil {
- return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat monitor for owner [%s] and user [%s]", monitor.Owner, monitor.UserID))
+ return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat monitor for owner [%s] and user [%s]", monitor.Owner, monitor.UserID))
}
ctxLogger.Info(fmt.Sprintf("heartbeat monitor saved with id [%s] for owner [%s] and user [%s]", monitor.ID, monitor.Owner, monitor.UserID))
@@ -226,7 +226,7 @@ func (service *HeartbeatService) phoneMonitor(ctx context.Context, params *Heart
}
if err != nil {
- return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner))
+ return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner))
}
return monitor, monitor.RequiresCheck(), nil
@@ -240,7 +240,7 @@ func (service *HeartbeatService) DeleteMonitor(ctx context.Context, userID entit
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.monitorRepository.Delete(ctx, userID, owner); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s]", userID, owner))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s]", userID, owner))
}
ctxLogger.Info(fmt.Sprintf("heartbeat monitor deleted for userID [%s] and owner [%s]", userID, owner))
@@ -255,7 +255,7 @@ func (service *HeartbeatService) UpdatePhoneOnline(ctx context.Context, userID e
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.monitorRepository.UpdatePhoneOnline(ctx, userID, monitorID, phoneOnline); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor [%s] with userID [%s] and status [%t]", monitorID, userID, phoneOnline))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor [%s] with userID [%s] and status [%t]", monitorID, userID, phoneOnline))
}
ctxLogger.Info(fmt.Sprintf("heartbeat monitor [%s] updated for userID [%s] and status [%t]", monitorID, userID, phoneOnline))
@@ -285,7 +285,7 @@ func (service *HeartbeatService) Monitor(ctx context.Context, params *HeartbeatM
}
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner))
return service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), params)
}
@@ -295,7 +295,7 @@ func (service *HeartbeatService) Monitor(ctx context.Context, params *HeartbeatM
heartbeat, err := service.repository.Last(ctx, params.UserID, params.Owner)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot fetch last heartbeat for userID [%s] and owner [%s] and ID [%s] removing check", params.UserID, params.Owner, params.MonitorID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot fetch last heartbeat for userID [%s] and owner [%s] and ID [%s] removing check", params.UserID, params.Owner, params.MonitorID))
return nil
}
@@ -326,12 +326,12 @@ func (service *HeartbeatService) handleMissedMonitor(ctx context.Context, lastTi
Owner: params.Owner,
})
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor [%s] missed heartbeat", params.MonitorID.String())))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor [%s] missed heartbeat", params.MonitorID.String())))
return
}
if _, err = service.dispatcher.DispatchWithTimeout(ctx, event, heartbeatCheckInterval); err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)))
}
}
@@ -341,7 +341,7 @@ func (service *HeartbeatService) handleFailedMonitor(ctx context.Context, lastTi
err := service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), params)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule healthcheck for monitor with owner [%s] and userID [%s]", params.Owner, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule healthcheck for monitor with owner [%s] and userID [%s]", params.Owner, params.UserID))
}
event, err := service.createPhoneHeartbeatOfflineEvent(params.Source, &events.PhoneHeartbeatOfflinePayload{
@@ -353,11 +353,11 @@ func (service *HeartbeatService) handleFailedMonitor(ctx context.Context, lastTi
Owner: params.Owner,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor failed"))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor failed"))
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))
}
ctxLogger.Info(fmt.Sprintf("heartbeat monitor with id [%s] and phone id [%s] failed for user [%s]", params.MonitorID, params.PhoneID, params.UserID))
@@ -376,16 +376,16 @@ func (service *HeartbeatService) scheduleHeartbeatCheck(ctx context.Context, las
Owner: params.Owner,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor failed"))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor failed"))
}
queueID, err := service.dispatcher.DispatchWithTimeout(ctx, event, heartbeatCheckInterval)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))
}
if err = service.monitorRepository.UpdateQueueID(ctx, params.MonitorID, queueID); err != nil {
- service.logger.Error(stacktrace.Propagate(err, "cannot update monitor with id [%s] with queue with ID [%s]", params.MonitorID, queueID))
+ service.logger.Error(stacktrace.Propagatef(err, "cannot update monitor with id [%s] with queue with ID [%s]", params.MonitorID, queueID))
}
ctxLogger.Info(fmt.Sprintf("heartbeat check scheduled for monitor with id [%s] and phone id [%s] and queue id [%s] for user [%s]", params.MonitorID, params.PhoneID, queueID, params.UserID))
diff --git a/api/pkg/services/integration_3cx_service.go b/api/pkg/services/integration_3cx_service.go
index b3d046ef7..4b87d39f8 100644
--- a/api/pkg/services/integration_3cx_service.go
+++ b/api/pkg/services/integration_3cx_service.go
@@ -49,7 +49,7 @@ func (service *Integration3CXService) DeleteAllForUser(ctx context.Context, user
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Integration3CX] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Integration3CX] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Integration3CX] for user with ID [%s]", userID))
@@ -68,7 +68,7 @@ func (service *Integration3CXService) Send(ctx context.Context, userID entities.
}
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load [3cx] integration for user [%s] and event [%s]", userID, event.Type()))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load [3cx] integration for user [%s] and event [%s]", userID, event.Type()))
}
service.sendNotification(ctx, event, webhooks)
@@ -84,30 +84,30 @@ func (service *Integration3CXService) sendNotification(ctx context.Context, even
payload, err := service.getPayload(event)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot generate [3cx] payload from [%s] event with ID [%s] for user [%s]", event.Type(), event.ID(), integration.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot generate [3cx] payload from [%s] event with ID [%s] for user [%s]", event.Type(), event.ID(), integration.UserID)))
return
}
body, err := json.Marshal(payload)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot marshal [%T] for [%s] event with ID [%s] for user [%s]", payload, event.Type(), event.ID(), integration.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot marshal [%T] for [%s] event with ID [%s] for user [%s]", payload, event.Type(), event.ID(), integration.UserID)))
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, integration.WebhookURL, bytes.NewBuffer(body))
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for [%s] event with ID [%s] for user [%s]", request, event.Type(), event.ID(), integration.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for [%s] event with ID [%s] for user [%s]", request, event.Type(), event.ID(), integration.UserID)))
return
}
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
response, err := service.client.Do(request)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] event to [3cx] webhook [%s] for user [%s]", event.Type(), integration.WebhookURL, integration.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] event to [3cx] webhook [%s] for user [%s]", event.Type(), integration.WebhookURL, integration.UserID)))
}
defer func() {
err = response.Body.Close()
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot close response body"))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot close response body"))
}
}()
@@ -135,14 +135,14 @@ func (service *Integration3CXService) getPayload(event cloudevents.Event) (fiber
case events.EventTypeMessageSendFailed:
return service.getMessageSendFailedPayload(event)
default:
- return nil, stacktrace.NewError("cannot generate [3cx] payload for event [%s] with ID [%s]", event.Type(), event.ID())
+ return nil, stacktrace.NewErrorf("cannot generate [3cx] payload for event [%s] with ID [%s]", event.Type(), event.ID())
}
}
func (service *Integration3CXService) getEventDeliveredPayload(event cloudevents.Event) (fiber.Map, error) {
payload := new(events.MessagePhoneDeliveredPayload)
if err := event.DataAs(payload); err != nil {
- return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
+ return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
}
return fiber.Map{
@@ -172,7 +172,7 @@ func (service *Integration3CXService) getEventDeliveredPayload(event cloudevents
func (service *Integration3CXService) getMessageSentPayload(event cloudevents.Event) (fiber.Map, error) {
payload := new(events.MessagePhoneSentPayload)
if err := event.DataAs(payload); err != nil {
- return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
+ return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
}
return fiber.Map{
@@ -202,7 +202,7 @@ func (service *Integration3CXService) getMessageSentPayload(event cloudevents.Ev
func (service *Integration3CXService) getMessageSendFailedPayload(event cloudevents.Event) (fiber.Map, error) {
payload := new(events.MessageSendFailedPayload)
if err := event.DataAs(payload); err != nil {
- return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
+ return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
}
return fiber.Map{
@@ -232,7 +232,7 @@ func (service *Integration3CXService) getMessageSendFailedPayload(event cloudeve
func (service *Integration3CXService) getMessageReceivedPayload(event cloudevents.Event) (fiber.Map, error) {
payload := new(events.MessagePhoneReceivedPayload)
if err := event.DataAs(payload); err != nil {
- return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
+ return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)
}
return fiber.Map{
diff --git a/api/pkg/services/lemonsqueezy_service.go b/api/pkg/services/lemonsqueezy_service.go
index 48347335c..00df1fcd4 100644
--- a/api/pkg/services/lemonsqueezy_service.go
+++ b/api/pkg/services/lemonsqueezy_service.go
@@ -52,7 +52,7 @@ func (service *LemonsqueezyService) GetUserID(ctx context.Context, request *lemo
ctxLogger.Info(fmt.Sprintf("user_id not found in request meta data. Searching via email [%s]", request.Data.Attributes.UserEmail))
user, err := service.userRepository.LoadByEmail(ctx, request.Data.Attributes.UserEmail)
if err != nil {
- return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with email [%s]", request.Data.Attributes.UserEmail))
+ return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with email [%s]", request.Data.Attributes.UserEmail))
}
return user.ID, nil
@@ -65,7 +65,7 @@ func (service *LemonsqueezyService) HandleSubscriptionCreatedEvent(ctx context.C
userID, err := service.GetUserID(ctx, request)
if err != nil {
- return stacktrace.Propagate(err, "cannot get user ID for subscription created event with ID [%s]", request.Data.ID)
+ return stacktrace.Propagatef(err, "cannot get user ID for subscription created event with ID [%s]", request.Data.ID)
}
payload := &events.UserSubscriptionCreatedPayload{
@@ -79,11 +79,11 @@ func (service *LemonsqueezyService) HandleSubscriptionCreatedEvent(ctx context.C
event, err := service.createEvent(events.UserSubscriptionCreated, source, payload)
if err != nil {
- return stacktrace.Propagate(err, "cannot create [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot create [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID)
}
ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] created for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID))
return nil
@@ -96,7 +96,7 @@ func (service *LemonsqueezyService) HandleSubscriptionCanceledEvent(ctx context.
user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID)
+ return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID)
}
payload := &events.UserSubscriptionCancelledPayload{
@@ -110,11 +110,11 @@ func (service *LemonsqueezyService) HandleSubscriptionCanceledEvent(ctx context.
event, err := service.createEvent(events.UserSubscriptionCancelled, source, payload)
if err != nil {
- return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID)
}
ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] cancelled for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID))
return nil
@@ -132,7 +132,7 @@ func (service *LemonsqueezyService) HandleSubscriptionUpdatedEvent(ctx context.C
user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID)
+ return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID)
}
payload := &events.UserSubscriptionUpdatedPayload{
@@ -147,11 +147,11 @@ func (service *LemonsqueezyService) HandleSubscriptionUpdatedEvent(ctx context.C
event, err := service.createEvent(events.UserSubscriptionUpdated, source, payload)
if err != nil {
- return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionUpdated, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionUpdated, payload.UserID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID)
+ return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID)
}
ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] updated for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID))
return nil
@@ -164,7 +164,7 @@ func (service *LemonsqueezyService) HandleSubscriptionExpiredEvent(ctx context.C
user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID)
+ return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID)
}
payload := &events.UserSubscriptionExpiredPayload{
@@ -179,11 +179,11 @@ func (service *LemonsqueezyService) HandleSubscriptionExpiredEvent(ctx context.C
event, err := service.createEvent(events.UserSubscriptionExpired, source, payload)
if err != nil {
- return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionExpired, payload.UserID)
+ return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionExpired, payload.UserID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID)
+ return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID)
}
ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] expired for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID))
return nil
@@ -229,6 +229,6 @@ func (service *LemonsqueezyService) subscriptionName(variant string) entities.Su
}
}
- service.logger.Warn(stacktrace.NewError("unknown subscription variant [%s], defaulting to free", variant))
+ service.logger.Warn(stacktrace.NewErrorf("unknown subscription variant [%s], defaulting to free", variant))
return entities.SubscriptionNameFree
}
diff --git a/api/pkg/services/marketting_service.go b/api/pkg/services/marketting_service.go
index aabc2d94a..65f608543 100644
--- a/api/pkg/services/marketting_service.go
+++ b/api/pkg/services/marketting_service.go
@@ -45,7 +45,7 @@ func (service *MarketingService) DeleteContact(ctx context.Context, email string
response, _, err := service.plunkClient.Contacts.List(ctx, map[string]string{"search": email})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot search for contact with email [%s]", email))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot search for contact with email [%s]", email))
}
if len(response.Data) == 0 {
@@ -55,7 +55,7 @@ func (service *MarketingService) DeleteContact(ctx context.Context, email string
contact := response.Data[0]
if _, err = service.plunkClient.Contacts.Delete(ctx, contact.ID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete user with ID [%s] from contacts", contact.Data[string(semconv.EnduserIDKey)]))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete user with ID [%s] from contacts", contact.Data[string(semconv.EnduserIDKey)]))
}
ctxLogger.Info(fmt.Sprintf("deleted user with ID [%s] from as marketting contact with ID [%s]", contact.Data[string(semconv.EnduserIDKey)], contact.ID))
@@ -69,7 +69,7 @@ func (service *MarketingService) CreateContact(ctx context.Context, userID entit
userRecord, err := service.authClient.GetUser(ctx, userID.String())
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get auth user with id [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get auth user with id [%s]", userID))
}
data := service.attributes(userRecord)
@@ -83,7 +83,7 @@ func (service *MarketingService) CreateContact(ctx context.Context, userID entit
Data: data,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create contact for user with id [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create contact for user with id [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("user [%s] added to marketting list with contact ID [%s] and event ID [%s]", userID, event.Data.Contact, event.Data.Event))
diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go
index 85627116e..e9e128f03 100644
--- a/api/pkg/services/message_send_schedule_service.go
+++ b/api/pkg/services/message_send_schedule_service.go
@@ -92,7 +92,7 @@ func (service *MessageSendScheduleService) Store(
if err := service.repository.Store(ctx, schedule); err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot store message send schedule [%s]", schedule.ID,
),
@@ -125,7 +125,7 @@ func (service *MessageSendScheduleService) Update(
if err = service.repository.Update(ctx, schedule); err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot update message send schedule [%s]", schedule.ID,
),
@@ -145,7 +145,7 @@ func (service *MessageSendScheduleService) Delete(
defer span.End()
if err := service.repository.Delete(ctx, userID, scheduleID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message send schedule with ID [%s] for user [%s]", scheduleID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message send schedule with ID [%s] for user [%s]", scheduleID, userID))
}
event, err := service.createEvent(events.EventTypeMessageSendScheduleDeleted, fmt.Sprintf("%T", service), events.MessageSendScheduleDeletedPayload{
@@ -154,11 +154,11 @@ func (service *MessageSendScheduleService) Delete(
Timestamp: time.Now().UTC(),
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for schedule [%s]", events.EventTypeMessageSendScheduleDeleted, scheduleID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for schedule [%s]", events.EventTypeMessageSendScheduleDeleted, scheduleID))
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for schedule [%s]", event.Type(), scheduleID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for schedule [%s]", event.Type(), scheduleID))
}
return nil
@@ -199,7 +199,7 @@ func (service *MessageSendScheduleService) DeleteAllForUser(
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
return service.tracer.WrapErrorSpan(
span,
- stacktrace.Propagate(
+ stacktrace.Propagatef(
err,
"cannot delete message send schedules for user [%s]", userID,
),
diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go
index 4e6c91128..89c216817 100644
--- a/api/pkg/services/message_service.go
+++ b/api/pkg/services/message_service.go
@@ -79,7 +79,7 @@ func (service *MessageService) GetOutstanding(ctx context.Context, params Messag
message, err := service.repository.GetOutstanding(ctx, params.UserID, params.MessageID, params.PhoneNumbers)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch outstanding messages with params [%s]", spew.Sdump(params)))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch outstanding messages with params [%s]", spew.Sdump(params)))
}
event, err := service.createMessagePhoneSendingEvent(params.Source, events.MessagePhoneSendingPayload{
@@ -93,13 +93,13 @@ func (service *MessageService) GetOutstanding(ctx context.Context, params Messag
SIM: message.SIM,
})
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message with ID [%s]", event, message.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message with ID [%s]", event, message.ID))
}
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
}
ctxLogger.Info(fmt.Sprintf("dispatched event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
@@ -112,7 +112,7 @@ func (service *MessageService) DeleteAllForUser(ctx context.Context, userID enti
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.Message] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.Message] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Message] for user with ID [%s]", userID))
@@ -126,7 +126,7 @@ func (service *MessageService) GetBulkMessages(ctx context.Context, userID entit
orders, err := service.repository.GetBulkMessages(ctx, userID, 10)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch bulk messages for user [%s]", userID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch bulk messages for user [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] bulk messages for user [%s]", len(orders), userID))
@@ -141,7 +141,7 @@ func (service *MessageService) DeleteMessage(ctx context.Context, source string,
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.repository.Delete(ctx, message.UserID, message.ID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not delete message with ID [%s] for user wit ID [%s]", message.ID, message.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not delete message with ID [%s] for user wit ID [%s]", message.ID, message.UserID))
}
var prevID *uuid.UUID
@@ -169,12 +169,12 @@ func (service *MessageService) DeleteMessage(ctx context.Context, source string,
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message with ID [%s]", event, message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message with ID [%s]", event, message.ID))
}
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
}
ctxLogger.Info(fmt.Sprintf("dispatched event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
@@ -189,7 +189,7 @@ func (service *MessageService) DeleteByOwnerAndContact(ctx context.Context, user
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if err := service.repository.DeleteByOwnerAndContact(ctx, userID, owner, contact); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not all delete messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not all delete messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact))
}
ctxLogger.Info(fmt.Sprintf("deleted all messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact))
@@ -203,7 +203,7 @@ func (service *MessageService) RespondToMissedCall(ctx context.Context, source s
phone, err := service.phoneService.Load(ctx, payload.UserID, payload.Owner)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find phone with owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find phone with owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID))
}
if phone.MissedCallAutoReply == nil || strings.TrimSpace(*phone.MissedCallAutoReply) == "" {
@@ -225,7 +225,7 @@ func (service *MessageService) RespondToMissedCall(ctx context.Context, source s
RequestReceivedAt: time.Now().UTC(),
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send auto response message for owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send auto response message for owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID))
}
ctxLogger.Info(fmt.Sprintf("created response message with ID [%s] for missed call event [%s] for user [%s]", message.ID, payload.MessageID, message.UserID))
@@ -249,7 +249,7 @@ func (service *MessageService) GetMessages(ctx context.Context, params MessageGe
messages, err := service.repository.Index(ctx, params.UserID, params.Owner, params.Contact, params.IndexParams)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch messages with parms [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch messages with parms [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*messages), params))
@@ -263,7 +263,7 @@ func (service *MessageService) GetMessage(ctx context.Context, userID entities.U
message, err := service.repository.Load(ctx, userID, messageID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch messages with ID [%s]", messageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch messages with ID [%s]", messageID))
}
return message, nil
@@ -293,11 +293,11 @@ func (service *MessageService) StoreEvent(ctx context.Context, message *entities
case entities.MessageEventNameFailed:
err = service.handleMessageFailedEvent(ctx, params, message)
default:
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot handle message event [%s]", params.EventName))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot handle message event [%s]", params.EventName))
}
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not handle phone event [%s] for message with id [%s]", params.EventName, message.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not handle phone event [%s] for message with id [%s]", params.EventName, message.ID))
}
return service.repository.Load(ctx, message.UserID, params.MessageID)
@@ -328,7 +328,7 @@ func (service *MessageService) ReceiveMessage(ctx context.Context, params *Messa
ctxLogger.Info(fmt.Sprintf("uploading [%d] attachments for user [%s] message [%s]", len(params.Attachments), params.UserID, messageID))
attachmentURLs, err := service.uploadAttachments(ctx, params.UserID, messageID, params.Attachments)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upload attachments for user [%s] message [%s]", params.UserID, messageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upload attachments for user [%s] message [%s]", params.UserID, messageID))
}
owner := phonenumbers.Format(params.Owner, phonenumbers.E164)
@@ -349,13 +349,13 @@ func (service *MessageService) ReceiveMessage(ctx context.Context, params *Messa
event, err := service.createMessagePhoneReceivedEvent(params.Source, eventPayload)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID))
}
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 {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
}
ctxLogger.Info(fmt.Sprintf("event [%s] dispatched successfully", event.ID()))
@@ -378,11 +378,11 @@ func (service *MessageService) handleMessageSentEvent(ctx context.Context, param
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID))
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
}
return nil
}
@@ -403,11 +403,11 @@ func (service *MessageService) handleMessageDeliveredEvent(ctx context.Context,
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID))
}
if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, time.Second); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s] for message [%s]", event.Type(), event.ID(), message.ID))
}
return nil
}
@@ -434,11 +434,11 @@ func (service *MessageService) handleMessageFailedEvent(ctx context.Context, par
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessageSendFailed, message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessageSendFailed, message.ID))
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
}
return nil
}
@@ -485,18 +485,18 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe
event, err := service.createMessageAPISentEvent(params.Source, eventPayload)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID))
}
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 {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store message with id [%s]", eventPayload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store message with id [%s]", eventPayload.MessageID))
}
timeout := service.getSendDelay(ctxLogger, eventPayload, params, messagesPerMinute)
if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, timeout); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
}
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))
@@ -531,18 +531,18 @@ func (service *MessageService) RegisterMissedCall(ctx context.Context, params *M
event, err := service.createEvent(events.MessageCallMissed, params.Source, eventPayload)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] from payload with message id [%s]", event, eventPayload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] from payload with message id [%s]", event, eventPayload.MessageID))
}
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.storeMissedCallMessage(ctx, eventPayload)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store missed call message message with id [%s]", eventPayload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store missed call message message with id [%s]", eventPayload.MessageID))
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID()))
}
ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID))
@@ -595,7 +595,7 @@ func (service *MessageService) storeReceivedMessage(ctx context.Context, params
}
if err := service.repository.Store(ctx, message); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with id [%s]", params.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with id [%s]", params.MessageID))
}
ctxLogger.Info(fmt.Sprintf("message saved with id [%s]", message.ID))
@@ -619,7 +619,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent
g.Go(func() error {
decoded, err := base64.StdEncoding.DecodeString(attachment.Content)
if err != nil {
- return stacktrace.Propagate(err, "cannot decode base64 content for attachment [%d]", i)
+ return stacktrace.Propagatef(err, "cannot decode base64 content for attachment [%d]", i)
}
sanitizedName := repositories.SanitizeFilename(attachment.Name, i)
@@ -630,7 +630,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent
paths[i] = path
if err = service.attachmentRepository.Upload(gCtx, path, decoded, attachment.ContentType); err != nil {
- return stacktrace.Propagate(err, "cannot upload attachment [%d] to path [%s]", i, path)
+ return stacktrace.Propagatef(err, "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)
@@ -645,7 +645,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent
_ = service.attachmentRepository.Delete(ctx, path)
}
}
- return nil, stacktrace.Propagate(err, "cannot upload attachments")
+ return nil, stacktrace.Propagatef(err, "cannot upload attachments")
}
return urls, nil
@@ -668,15 +668,15 @@ func (service *MessageService) HandleMessageSending(ctx context.Context, params
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if !message.IsSending() {
- return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected %s", message.Status, entities.MessageStatusSending))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected %s", message.Status, entities.MessageStatusSending))
}
if err = service.repository.Update(ctx, message.AddSendAttempt(params.Timestamp)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] after sending", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] after sending", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] updated after adding send attempt", message.ID))
@@ -692,7 +692,7 @@ func (service *MessageService) HandleMessageSent(ctx context.Context, params Han
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if message.IsSent() || message.IsDelivered() {
@@ -701,11 +701,11 @@ func (service *MessageService) HandleMessageSent(ctx context.Context, params Han
}
if !message.IsSending() && !message.IsExpired() && !message.IsScheduled() {
- return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusExpired, entities.MessageStatusScheduled))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusExpired, entities.MessageStatusScheduled))
}
if err = service.repository.Update(ctx, message.Sent(params.Timestamp)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as sent", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as sent", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status))
@@ -729,15 +729,15 @@ func (service *MessageService) HandleMessageFailed(ctx context.Context, params H
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if message.IsDelivered() {
- return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has already been delivered with status [%s]", message.Status))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has already been delivered with status [%s]", message.Status))
}
if err = service.repository.Update(ctx, message.Failed(params.Timestamp, params.ErrorMessage)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as sent", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as sent", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status))
@@ -753,16 +753,16 @@ func (service *MessageService) HandleMessageDelivered(ctx context.Context, param
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if !message.IsSent() && !message.IsSending() && !message.IsExpired() && !message.IsScheduled() {
- ctxLogger.Warn(stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s, %s]", message.Status, entities.MessageStatusSent, entities.MessageStatusScheduled, entities.MessageStatusSending, entities.MessageStatusExpired))
+ ctxLogger.Warn(stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s, %s]", message.Status, entities.MessageStatusSent, entities.MessageStatusScheduled, entities.MessageStatusSending, entities.MessageStatusExpired))
return nil
}
if err = service.repository.Update(ctx, message.Delivered(params.Timestamp)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as delivered", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as delivered", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status))
@@ -778,15 +778,15 @@ func (service *MessageService) HandleMessageNotificationScheduled(ctx context.Co
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if !message.IsPending() && !message.IsExpired() && !message.IsSending() {
- ctxLogger.Warn(stacktrace.NewError("received scheduled event for message with id [%s] message has status [%s]", message.ID, message.Status))
+ ctxLogger.Warn(stacktrace.NewErrorf("received scheduled event for message with id [%s] message has status [%s]", message.ID, message.Status))
}
if err = service.repository.Update(ctx, message.NotificationScheduled(params.Timestamp)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] has been scheduled to send at [%s]", message.ID, message.NotificationScheduledAt.String()))
@@ -802,11 +802,11 @@ func (service *MessageService) HandleMessageNotificationSent(ctx context.Context
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if err = service.repository.Update(ctx, message.AddSendAttemptCount()); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID))
}
ctxLogger.Info(fmt.Sprintf("notification for message with id [%s] has been sent at [%s]", message.ID, params.Timestamp.String()))
@@ -822,15 +822,15 @@ func (service *MessageService) HandleMessageExpired(ctx context.Context, params
message, err := service.repository.Load(ctx, params.UserID, params.ID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID))
}
if !message.IsSending() && !message.IsScheduled() && !message.IsPending() {
- return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusScheduled, entities.MessageStatusPending))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusScheduled, entities.MessageStatusPending))
}
if err = service.repository.Update(ctx, message.Expired(params.Timestamp)); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID))
}
ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status))
@@ -850,11 +850,11 @@ func (service *MessageService) HandleMessageExpired(ctx context.Context, params
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for expired message with ID [%s]", events.EventTypeMessageSendRetry, message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for expired message with ID [%s]", events.EventTypeMessageSendRetry, message.ID))
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for message with ID [%s]", event.Type(), message.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for message with ID [%s]", event.Type(), message.ID))
}
ctxLogger.Info(fmt.Sprintf("retried sending message with ID [%s]", message.ID))
@@ -889,11 +889,11 @@ func (service *MessageService) ScheduleExpirationCheck(ctx context.Context, para
UserID: params.UserID,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpiredCheck, params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpiredCheck, params.MessageID))
}
if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, params.MessageExpirationDuration); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID))
}
ctxLogger.Info(fmt.Sprintf("scheduled message id [%s] to expire at [%s]", params.MessageID, params.NotificationSentAt.Add(params.MessageExpirationDuration)))
@@ -921,7 +921,7 @@ func (service *MessageService) CheckExpired(ctx context.Context, params MessageC
}
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with userID [%s] and messageID [%s]", params.UserID, params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with userID [%s] and messageID [%s]", params.UserID, params.MessageID))
}
if !message.IsPending() && !message.IsSending() && !message.IsScheduled() {
@@ -943,11 +943,11 @@ func (service *MessageService) CheckExpired(ctx context.Context, params MessageC
SIM: message.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpired, params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpired, params.MessageID))
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID))
}
ctxLogger.Info(fmt.Sprintf("message [%s] has expired with status [%s]", params.MessageID, message.Status))
@@ -972,7 +972,7 @@ func (service *MessageService) SearchMessages(ctx context.Context, params *Messa
messages, err := service.repository.Search(ctx, params.UserID, params.Owners, params.Types, params.Statuses, params.IndexParams)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not search messages with parms [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not search messages with parms [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(messages), params))
@@ -987,7 +987,7 @@ func (service *MessageService) phoneSettings(ctx context.Context, userID entitie
phone, err := service.phoneService.Load(ctx, userID, owner)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner))
return 2, entities.SIM1, 0
}
@@ -1027,7 +1027,7 @@ func (service *MessageService) storeSentMessage(ctx context.Context, payload eve
}
if err := service.repository.Store(ctx, message); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with id [%s]", payload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with id [%s]", payload.MessageID))
}
ctxLogger.Info(fmt.Sprintf("message saved with id [%s]", payload.MessageID))
@@ -1056,7 +1056,7 @@ func (service *MessageService) storeMissedCallMessage(ctx context.Context, paylo
}
if err := service.repository.Store(ctx, message); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save missed call message with id [%s]", payload.MessageID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save missed call message with id [%s]", payload.MessageID))
}
ctxLogger.Info(fmt.Sprintf("missed call message saved with id [%s]", payload.MessageID))
diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go
index 687482ddd..532a223e2 100644
--- a/api/pkg/services/message_thread_service.go
+++ b/api/pkg/services/message_thread_service.go
@@ -70,7 +70,7 @@ func (service *MessageThreadService) DeleteAllForUser(ctx context.Context, userI
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.MessageThread] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.MessageThread] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.MessageThread] for user with ID [%s]", userID))
@@ -91,16 +91,16 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
}
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread with owner [%s], and contact [%s]. creating new thread", params.Owner, params.Contact))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread with owner [%s], and contact [%s]. creating new thread", params.Owner, params.Contact))
}
if thread.OrderTimestamp.Unix() > params.Timestamp.Unix() && thread.Status != entities.MessageStatusSending && thread.HasLastMessage(params.MessageID) {
- ctxLogger.Warn(stacktrace.NewError("thread [%s] has timestamp [%s] and status [%s] which is greater than timestamp [%s] for message [%s] and status [%s]", thread.ID, thread.OrderTimestamp, thread.Status, params.Timestamp, params.MessageID, params.Status))
+ ctxLogger.Warn(stacktrace.NewErrorf("thread [%s] has timestamp [%s] and status [%s] which is greater than timestamp [%s] for message [%s] and status [%s]", thread.ID, thread.OrderTimestamp, thread.Status, params.Timestamp, params.MessageID, params.Status))
return nil
}
if thread.Status == entities.MessageStatusDelivered && thread.LastMessageID != nil && thread.HasLastMessage(params.MessageID) {
- ctxLogger.Warn(stacktrace.NewError("thread [%s] already has status [%s] not updating with status [%s] for message [%s]", thread.ID, thread.Status, params.Status, params.MessageID))
+ ctxLogger.Warn(stacktrace.NewErrorf("thread [%s] already has status [%s] not updating with status [%s] for message [%s]", thread.ID, thread.Status, params.Status, params.MessageID))
return nil
}
@@ -118,7 +118,7 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
if service.shouldCheckUnarchive(thread, params) {
phone, phoneErr := service.phoneRepository.Load(ctx, params.UserID, params.Owner)
if phoneErr != nil {
- ctxLogger.Warn(stacktrace.Propagate(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID))
+ ctxLogger.Warn(stacktrace.Propagatef(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID))
} else if phone.UnarchiveThread {
activity.Unarchive = true
ctxLogger.Info(fmt.Sprintf("unarchiving thread [%s] after inbound message [%s]", thread.ID, params.MessageID))
@@ -126,7 +126,7 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
}
if err = service.repository.UpdateActivity(ctx, activity); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID))
}
ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, params.MessageID, params.Status))
@@ -153,7 +153,7 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me
}
thread, err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID))
}
return thread, nil
@@ -166,12 +166,12 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte
thread, err := service.repository.LoadByOwnerContact(ctx, payload.UserID, payload.Owner, payload.Contact)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread for user [%s] with owner [%s], and contact [%s]", payload.UserID, payload.Owner, payload.Contact))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread for user [%s] with owner [%s], and contact [%s]", payload.UserID, payload.Owner, payload.Contact))
}
if payload.PreviousMessageID == nil {
if err = service.repository.Delete(ctx, thread.UserID, thread.ID); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot delete thread with ID [%s] for user [%s] and owner [%s]", thread.ID, thread.UserID, thread.Owner))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete thread with ID [%s] for user [%s] and owner [%s]", thread.ID, thread.UserID, thread.Owner))
return nil
}
msg := fmt.Sprintf("previous message ID is nil for thread with ID [%s] and user [%s]", thread.ID, thread.UserID)
@@ -192,7 +192,7 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte
LastMessageContent: payload.PreviousMessageContent,
LastMessageStatus: *payload.PreviousMessageStatus,
}); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID))
}
ctxLogger.Info(fmt.Sprintf("last message has been removed from thread with ID [%s] and userID [%s]", thread.ID, thread.UserID))
@@ -224,7 +224,7 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me
}
if err := service.repository.Store(ctx, thread); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store thread with id [%s] for message with ID [%s]", thread.ID, params.MessageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store thread with id [%s] for message with ID [%s]", thread.ID, params.MessageID))
}
ctxLogger.Info(fmt.Sprintf(
@@ -279,7 +279,7 @@ func (service *MessageThreadService) GetThreads(ctx context.Context, params Mess
threads, err := service.repository.Index(ctx, params.UserID, params.Owner, params.IsArchived, params.IndexParams)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch messages threads for params [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch messages threads for params [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] threads with params [%+#v]", len(*threads), params))
@@ -293,7 +293,7 @@ func (service *MessageThreadService) GetThread(ctx context.Context, userID entit
thread, err := service.repository.Load(ctx, userID, messageThreadID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch thread with ID [%s] for user [%s]", messageThreadID, userID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch thread with ID [%s] for user [%s]", messageThreadID, userID))
}
return thread, nil
@@ -305,7 +305,7 @@ func (service *MessageThreadService) DeleteThread(ctx context.Context, source st
defer span.End()
if err := service.repository.Delete(ctx, thread.UserID, thread.ID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not delete message thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not delete message thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID))
}
event, err := service.createEvent(events.MessageThreadAPIDeleted, source, &events.MessageThreadAPIDeletedPayload{
@@ -319,12 +319,12 @@ func (service *MessageThreadService) DeleteThread(ctx context.Context, source st
Timestamp: time.Now().UTC(),
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message thread dleted with ID [%s]", event, thread.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message thread dleted with ID [%s]", event, thread.ID))
}
ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID))
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID))
}
ctxLogger.Info(fmt.Sprintf("dispatched [%s] event with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID))
diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go
index 7825be755..8cc8bdcbe 100644
--- a/api/pkg/services/message_thread_service_test.go
+++ b/api/pkg/services/message_thread_service_test.go
@@ -149,7 +149,7 @@ func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) {
var stored *entities.MessageThread
repository := &messageThreadRepositoryStub{
loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
- return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
},
store: func(_ context.Context, thread *entities.MessageThread) error {
stored = thread
@@ -206,7 +206,7 @@ func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) {
func TestUpdateStatusPreservesNotFoundCode(t *testing.T) {
repository := &messageThreadRepositoryStub{
updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
- return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
+ return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found")
},
}
diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go
index 9762f116e..83e9e696a 100644
--- a/api/pkg/services/phone_api_key_service.go
+++ b/api/pkg/services/phone_api_key_service.go
@@ -55,7 +55,7 @@ func (service *PhoneAPIKeyService) Index(ctx context.Context, userID entities.Us
phoneAPIKeys, err := service.repository.Index(ctx, userID, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch phone API Keys with params [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch phone API Keys with params [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] phone API Keys with prams [%+#v]", len(phoneAPIKeys), params))
@@ -69,7 +69,7 @@ func (service *PhoneAPIKeyService) Create(ctx context.Context, authContext entit
apiKey, err := service.generateAPIKey(64)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate API key")
+ return nil, stacktrace.Propagatef(err, "cannot generate API key")
}
phoneAPIKey := &entities.PhoneAPIKey{
@@ -85,7 +85,7 @@ func (service *PhoneAPIKeyService) Create(ctx context.Context, authContext entit
}
if err = service.repository.Create(ctx, phoneAPIKey); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create PhoneAPIKey for user [%s]", authContext.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create PhoneAPIKey for user [%s]", authContext.ID))
}
ctxLogger.Info(fmt.Sprintf("created [%T] with ID [%s] for user ID [%s]", phoneAPIKey, phoneAPIKey.ID, authContext.ID))
@@ -99,11 +99,11 @@ func (service *PhoneAPIKeyService) Delete(ctx context.Context, userID entities.U
phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
+ return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
}
if err = service.repository.Delete(ctx, phoneAPIKey); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [%T] with ID [%s] for user [%s]", phoneAPIKey, phoneAPIKey.ID, phoneAPIKey.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [%T] with ID [%s] for user [%s]", phoneAPIKey, phoneAPIKey.ID, phoneAPIKey.UserID))
}
ctxLogger.Info(fmt.Sprintf("deleted [%T] with ID [%s] for user ID [%s]", phoneAPIKey, phoneAPIKey.ID, userID))
@@ -117,16 +117,16 @@ func (service *PhoneAPIKeyService) RemovePhone(ctx context.Context, userID entit
phone, err := service.phoneRepository.LoadByID(ctx, userID, phoneID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String())
+ return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String())
}
phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
+ return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
}
if err = service.repository.RemovePhone(ctx, phoneAPIKey, phone); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove [%T] with ID [%s] from phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove [%T] with ID [%s] from phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID))
}
ctxLogger.Info(fmt.Sprintf("removed [%T] with ID [%s] from [%T] with ID [%s] for user ID [%s]", phone, phoneID, phoneAPIKey, phoneAPIKeyID, userID))
@@ -139,7 +139,7 @@ func (service *PhoneAPIKeyService) RemovePhoneByID(ctx context.Context, userID e
defer span.End()
if err := service.repository.RemovePhoneByID(ctx, userID, phoneID, phoneNumber); err != nil {
- return stacktrace.Propagate(err, "cannot remove [%T] with ID [%s] and number [%s] for user [%s]", &entities.Phone{}, phoneID, phoneNumber, userID.String())
+ return stacktrace.Propagatef(err, "cannot remove [%T] with ID [%s] and number [%s] for user [%s]", &entities.Phone{}, phoneID, phoneNumber, userID.String())
}
ctxLogger.Info(fmt.Sprintf("removed phone with ID [%s] from [%T] for user ID [%s]", phoneID, &entities.PhoneAPIKey{}, userID))
@@ -152,7 +152,7 @@ func (service *PhoneAPIKeyService) DeleteAllForUser(ctx context.Context, userID
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return stacktrace.Propagate(err, "cannot delete all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID)
+ return stacktrace.Propagatef(err, "cannot delete all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID)
}
ctxLogger.Info(fmt.Sprintf("deleted all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID))
@@ -166,16 +166,16 @@ func (service *PhoneAPIKeyService) AddPhone(ctx context.Context, userID entities
phone, err := service.phoneRepository.LoadByID(ctx, userID, phoneID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String())
+ return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String())
}
phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID)
if err != nil {
- return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
+ return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String())
}
if err = service.repository.AddPhone(ctx, phoneAPIKey, phone); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] to phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] to phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID))
}
ctxLogger.Info(fmt.Sprintf("added [%T] with ID [%s] to [%T] with ID [%s] for user ID [%s]", phone, phone.ID, phoneAPIKey, phoneAPIKeyID, userID))
@@ -186,7 +186,7 @@ func (service *PhoneAPIKeyService) generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
// Note that err == nil only if we read len(b) bytes.
if _, err := rand.Read(b); err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate [%d] random bytes", n)
+ return nil, stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n)
}
return b, nil
@@ -194,5 +194,5 @@ func (service *PhoneAPIKeyService) generateRandomBytes(n int) ([]byte, error) {
func (service *PhoneAPIKeyService) generateAPIKey(n int) (string, error) {
b, err := service.generateRandomBytes(n)
- return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagate(err, "cannot generate [%d] random bytes", n)
+ return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n)
}
diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go
index aed806ef1..0d4d8d209 100644
--- a/api/pkg/services/phone_notification_service.go
+++ b/api/pkg/services/phone_notification_service.go
@@ -57,7 +57,7 @@ func (service *PhoneNotificationService) DeleteAllForUser(ctx context.Context, u
defer span.End()
if err := service.phoneNotificationRepository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.PhoneNotification] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.PhoneNotification] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.PhoneNotification] for user with ID [%s]", userID))
@@ -70,7 +70,7 @@ func (service *PhoneNotificationService) DeleteByMessageID(ctx context.Context,
defer span.End()
if err := service.phoneNotificationRepository.DeleteByMessageID(ctx, userID, messageID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID))
}
ctxLogger.Info(fmt.Sprintf("deleted [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID))
@@ -84,11 +84,11 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p
phone, err := service.phoneRepository.LoadByID(ctx, payload.UserID, payload.PhoneID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phoneID [%s]", payload.UserID, payload.PhoneID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phoneID [%s]", payload.UserID, payload.PhoneID))
}
if phone.FcmToken == nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "phone with id [%s] has no FCM token", phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "phone with id [%s] has no FCM token", phone.ID))
}
result, err := service.messagingClient.Send(ctx, &messaging.Message{
@@ -101,7 +101,7 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p
Token: *phone.FcmToken,
})
if err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot send heartbeat FCM to phone with id [%s] for user [%s]", phone.ID, phone.UserID))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send heartbeat FCM to phone with id [%s] for user [%s]", phone.ID, phone.UserID))
return nil
}
@@ -153,7 +153,7 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone
Token: *phone.FcmToken,
})
if err != nil {
- ctxLogger.Warn(stacktrace.Propagate(
+ ctxLogger.Warn(stacktrace.Propagatef(
err,
"cannot send FCM to phone with ID [%s] for user with ID [%s] and message [%s]",
@@ -191,7 +191,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P
phone, err := service.phoneRepository.Load(ctx, params.UserID, params.Owner)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner))
}
notification := &entities.PhoneNotification{
@@ -213,7 +213,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P
if phone.MessageSendScheduleID != nil {
schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.MessageSendScheduleID)
if err != nil && stacktrace.GetCode(err) != repositories.ErrCodeNotFound {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID))
}
}
@@ -222,7 +222,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P
}
if err = service.phoneNotificationRepository.Schedule(ctx, phone.MessagesPerMinute, schedule, notification); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID))
}
if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil {
@@ -258,7 +258,7 @@ func (service *PhoneNotificationService) scheduleExact(
notification.ScheduledAt = scheduledAt
if err := service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID))
}
if err := service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil {
@@ -291,11 +291,11 @@ func (service *PhoneNotificationService) dispatchMessageNotificationSend(
NotificationID: notification.ID,
})
if err != nil {
- return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSend, notification.ID)
+ return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSend, notification.ID)
}
if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, notification.ScheduledAt.Sub(time.Now())); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID)
+ return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID)
}
return nil
}
@@ -318,11 +318,11 @@ func (service *PhoneNotificationService) dispatchMessageNotificationScheduled(
NotificationID: notification.ID,
})
if err != nil {
- return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationScheduled, notification.ID)
+ return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationScheduled, notification.ID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID)
+ return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID)
}
return nil
}
@@ -333,15 +333,15 @@ func (service *PhoneNotificationService) handleNotificationFailed(ctx context.Co
ctxLogger := service.tracer.CtxLogger(service.logger, span)
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot send notification for message [%s] to phone [%s]", params.MessageID, params.PhoneNotificationID))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send notification for message [%s] to phone [%s]", params.MessageID, params.PhoneNotificationID))
event, err := service.createMessageNotificationFailedEvent(params.Source, err.Error(), params)
if err != nil {
- return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationFailed, params.PhoneNotificationID)
+ return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationFailed, params.PhoneNotificationID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID)
+ return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID)
}
service.updateStatus(ctx, params.PhoneNotificationID, entities.PhoneNotificationStatusFailed)
@@ -363,11 +363,11 @@ func (service *PhoneNotificationService) handleNotificationSent(
event, err := service.createMessageNotificationSentEvent(params.Source, phone, result, params)
if err != nil {
- return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSent, params.PhoneNotificationID)
+ return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSent, params.PhoneNotificationID)
}
if err = service.eventDispatcher.Dispatch(ctx, event); err != nil {
- return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID)
+ return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID)
}
service.updateStatus(ctx, params.PhoneNotificationID, entities.PhoneNotificationStatusSent)
@@ -413,7 +413,7 @@ func (service *PhoneNotificationService) createMessageNotificationSentEvent(
}
if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil {
- return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload)
+ return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload)
}
return event, nil
@@ -441,7 +441,7 @@ func (service *PhoneNotificationService) createMessageNotificationFailedEvent(
}
if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil {
- return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload)
+ return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload)
}
return event, nil
@@ -459,7 +459,7 @@ func (service *PhoneNotificationService) updateStatus(
err := service.phoneNotificationRepository.UpdateStatus(ctx, notificationID, status)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot update status of notification with id [%s] to [%s]", notificationID, status))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot update status of notification 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/services/phone_service.go b/api/pkg/services/phone_service.go
index c61bd1210..c7078375b 100644
--- a/api/pkg/services/phone_service.go
+++ b/api/pkg/services/phone_service.go
@@ -48,7 +48,7 @@ func (service *PhoneService) DeleteAllForUser(ctx context.Context, userID entiti
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Phone] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Phone] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Phone] for user with ID [%s]", userID))
@@ -61,7 +61,7 @@ func (service *PhoneService) NullifyScheduleID(ctx context.Context, userID entit
defer span.End()
if err := service.repository.NullifyScheduleID(ctx, userID, scheduleID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot nullify schedule ID [%s] for user [%s]", scheduleID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot nullify schedule ID [%s] for user [%s]", scheduleID, userID))
}
service.tracer.CtxLogger(service.logger, span).Info(fmt.Sprintf("nullified schedule ID [%s] on phones for user [%s]", scheduleID, userID))
@@ -77,7 +77,7 @@ func (service *PhoneService) Index(ctx context.Context, authUser entities.AuthCo
phones, err := service.repository.Index(ctx, authUser.ID, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch phones with parms [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch phones with parms [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] phones with prams [%+#v]", len(*phones), params))
@@ -129,11 +129,11 @@ func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertPara
}
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upsert phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upsert phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
}
if err = service.repository.Save(ctx, service.update(phone, params)); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
}
ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
@@ -161,11 +161,11 @@ func (service *PhoneService) dispatchPhoneUpdatedEvent(ctx context.Context, phon
SIM: phone.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone [%s] is updated for user [%s]", phone.ID, phone.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone [%s] is updated for user [%s]", phone.ID, phone.UserID))
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID))
}
return nil
}
@@ -179,11 +179,11 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e
phone, err := service.repository.LoadByID(ctx, userID, phoneID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID))
}
if err = service.repository.Delete(ctx, userID, phoneID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone with id [%s] and user id [%s]", phoneID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone with id [%s] and user id [%s]", phoneID, userID))
}
ctxLogger.Info(fmt.Sprintf("deleted phone with id [%s] and user id [%s]", phoneID, userID))
@@ -196,11 +196,11 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e
SIM: phone.SIM,
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone is deleted"))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone is deleted"))
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID))
}
return nil
@@ -228,14 +228,14 @@ func (service *PhoneService) UpsertFCMToken(ctx context.Context, params *PhoneFC
}
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upsert FCM token for user with id [%s] and number [%s]", params.UserID, params.PhoneNumber))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upsert FCM token for user with id [%s] and number [%s]", params.UserID, params.PhoneNumber))
}
phone.FcmToken = params.FcmToken
phone.SIM = params.SIM
if err = service.repository.Save(ctx, phone); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
}
ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
@@ -264,7 +264,7 @@ func (service *PhoneService) createPhone(ctx context.Context, params *PhoneFCMTo
}
if err := service.repository.Save(ctx, phone); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber))
}
ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
diff --git a/api/pkg/services/service.go b/api/pkg/services/service.go
index 605921fa6..2705bec2d 100644
--- a/api/pkg/services/service.go
+++ b/api/pkg/services/service.go
@@ -23,7 +23,7 @@ func (service *service) createEvent(eventType string, source string, payload any
event.SetID(uuid.New().String())
if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil {
- return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload)
+ return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload)
}
return event, nil
@@ -34,7 +34,7 @@ func (service *service) getFormattedNumber(ctxLogger telemetry.Logger, phoneNumb
matched, err := regexp.MatchString(phoneNumberPattern, phoneNumber)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "error while matching phoneNumber [%s] with regex [%s]", phoneNumber, phoneNumberPattern))
+ ctxLogger.Error(stacktrace.Propagatef(err, "error while matching phoneNumber [%s] with regex [%s]", phoneNumber, phoneNumberPattern))
return phoneNumber
}
if !matched {
@@ -43,7 +43,7 @@ func (service *service) getFormattedNumber(ctxLogger telemetry.Logger, phoneNumb
number, err := phonenumbers.Parse(phoneNumber, phonenumbers.UNKNOWN_REGION)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot parse number [%s]", phoneNumber))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot parse number [%s]", phoneNumber))
return phoneNumber
}
diff --git a/api/pkg/services/user_service.go b/api/pkg/services/user_service.go
index 265ad10fa..131b72d10 100644
--- a/api/pkg/services/user_service.go
+++ b/api/pkg/services/user_service.go
@@ -66,7 +66,7 @@ func (service *UserService) GetSubscriptionPayments(ctx context.Context, userID
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID))
+ return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID))
}
if user.SubscriptionID == nil {
@@ -77,7 +77,7 @@ func (service *UserService) GetSubscriptionPayments(ctx context.Context, userID
ctxLogger.Info(fmt.Sprintf("fetching subscription payments for [%T] with ID [%s] and subscription [%s]", user, user.ID, *user.SubscriptionID))
invoicesResponse, _, err := service.lemonsqueezyClient.SubscriptionInvoices.List(ctx, map[string]string{"filter[subscription_id]": *user.SubscriptionID})
if err != nil {
- return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get invoices for subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
+ return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get invoices for subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] payments for [%T] with ID [%s] and subscription ID [%s]", len(invoicesResponse.Data), user, user.ID, *user.SubscriptionID))
@@ -115,12 +115,12 @@ func (service *UserService) GenerateReceipt(ctx context.Context, params *UserInv
invoice, _, err := service.lemonsqueezyClient.SubscriptionInvoices.Generate(ctx, params.SubscriptionInvoiceID, payload)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not generate subscription payment invoice user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not generate subscription payment invoice user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID))
}
response, err := service.httpClient.Get(invoice.Meta.Urls.DownloadInvoice)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not download subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not download subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID))
}
ctxLogger.Info(fmt.Sprintf("generated subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID))
@@ -134,7 +134,7 @@ func (service *UserService) Get(ctx context.Context, source string, authUser ent
user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with from [%+#v]", user, authUser))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with from [%+#v]", user, authUser))
}
if isNew {
@@ -153,12 +153,12 @@ func (service *UserService) dispatchUserCreatedEvent(ctx context.Context, source
Timestamp: time.Now().UTC(),
})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAccountCreated, user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAccountCreated, user.ID))
return
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID))
return
}
}
@@ -170,7 +170,7 @@ func (service *UserService) GetByID(ctx context.Context, userID entities.UserID)
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with ID [%s]", user, userID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with ID [%s]", user, userID))
}
return user, nil
@@ -191,7 +191,7 @@ func (service *UserService) Update(ctx context.Context, source string, authUser
user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with from [%+#v]", user, authUser))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with from [%+#v]", user, authUser))
}
if isNew {
@@ -202,7 +202,7 @@ func (service *UserService) Update(ctx context.Context, source string, authUser
user.ActivePhoneID = params.ActivePhoneID
if err = service.repository.Update(ctx, user); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with id [%s]", user.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with id [%s]", user.ID))
}
ctxLogger.Info(fmt.Sprintf("user saved with id [%s] in the userRepository", user.ID))
@@ -224,7 +224,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load [%T] with ID [%s]", user, userID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load [%T] with ID [%s]", user, userID))
}
user.NotificationWebhookEnabled = params.WebhookEnabled
@@ -233,7 +233,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user
user.NotificationNewsletterEnabled = params.NewsletterEnabled
if err = service.repository.Update(ctx, user); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with id [%s] in [%T]", user.ID, service.repository))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with id [%s] in [%T]", user.ID, service.repository))
}
ctxLogger.Info(fmt.Sprintf("updated notification settings for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
@@ -247,7 +247,7 @@ func (service *UserService) RotateAPIKey(ctx context.Context, source string, use
user, err := service.repository.RotateAPIKey(ctx, userID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not rotate API key for [%T] with ID [%s]", user, userID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not rotate API key for [%T] with ID [%s]", user, userID))
}
ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
@@ -259,12 +259,12 @@ func (service *UserService) RotateAPIKey(ctx context.Context, source string, use
Timezone: user.Timezone,
})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID))
return user, nil
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID))
return user, nil
}
@@ -278,15 +278,15 @@ func (service *UserService) Delete(ctx context.Context, source string, userID en
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s] from the [%T]", userID, service.repository))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s] from the [%T]", userID, service.repository))
}
if !user.IsOnFreePlan() && user.SubscriptionRenewsAt != nil && user.SubscriptionRenewsAt.After(time.Now()) {
- return service.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot delete user with ID [%s] because they are have an active [%s] subscription which renews at [%s]", userID, user.SubscriptionName, user.SubscriptionRenewsAt))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot delete user with ID [%s] because they are have an active [%s] subscription which renews at [%s]", userID, user.SubscriptionName, user.SubscriptionRenewsAt))
}
if err = service.repository.Delete(ctx, user); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete user with ID [%s] from the [%T]", userID, service.repository))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete user with ID [%s] from the [%T]", userID, service.repository))
}
ctxLogger.Info(fmt.Sprintf("sucessfully deleted user with ID [%s] in the [%T]", userID, service.repository))
@@ -297,11 +297,11 @@ func (service *UserService) Delete(ctx context.Context, source string, userID en
Timestamp: time.Now().UTC(),
})
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID))
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), userID))
}
return nil
@@ -316,11 +316,11 @@ func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload
email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create api key rotated email for user [%s]", payload.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create api key rotated email for user [%s]", payload.UserID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "canot create api key rotated email to user [%s]", payload.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "canot create api key rotated email to user [%s]", payload.UserID))
}
ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID [%s]", payload.Email, payload.UserID))
@@ -344,7 +344,7 @@ func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *User
user, err := service.repository.Load(ctx, params.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with ID [%s]", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with ID [%s]", user, params.UserID))
}
if !user.NotificationHeartbeatEnabled {
@@ -354,11 +354,11 @@ func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *User
email, err := service.emailFactory.PhoneDead(user, params.LastHeartbeatTimestamp, params.Owner)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create phone dead email for user [%s]", params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create phone dead email for user [%s]", params.UserID))
}
if err = service.mailer.Send(ctx, email); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "canot send phone dead notification to user [%s]", params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "canot send phone dead notification to user [%s]", params.UserID))
}
ctxLogger.Info(fmt.Sprintf("phone dead notification sent successfully to [%s] about [%s]", user.Email, params.Owner))
@@ -372,7 +372,7 @@ func (service *UserService) StartSubscription(ctx context.Context, params *event
user, err := service.repository.Load(ctx, params.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID))
}
user.SubscriptionID = ¶ms.SubscriptionID
@@ -382,7 +382,7 @@ func (service *UserService) StartSubscription(ctx context.Context, params *event
user.SubscriptionEndsAt = nil
if err = service.repository.Update(ctx, user); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after update", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after update", user, params.UserID))
}
return nil
@@ -395,11 +395,11 @@ func (service *UserService) InitiateSubscriptionCancel(ctx context.Context, user
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID))
}
if _, _, err = service.lemonsqueezyClient.Subscriptions.Cancel(ctx, *user.SubscriptionID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
}
ctxLogger.Info(fmt.Sprintf("cancelled subscription [%s] for user [%s]", *user.SubscriptionID, user.ID))
@@ -413,12 +413,12 @@ func (service *UserService) GetSubscriptionUpdateURL(ctx context.Context, userID
user, err := service.repository.Load(ctx, userID)
if err != nil {
- return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID))
+ return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID))
}
subscription, _, err := service.lemonsqueezyClient.Subscriptions.Get(ctx, *user.SubscriptionID)
if err != nil {
- return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
+ return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID))
}
return subscription.Data.Attributes.Urls.CustomerPortal, nil
@@ -431,7 +431,7 @@ func (service *UserService) CancelSubscription(ctx context.Context, params *even
user, err := service.repository.Load(ctx, params.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID))
}
user.SubscriptionID = ¶ms.SubscriptionID
@@ -441,7 +441,7 @@ func (service *UserService) CancelSubscription(ctx context.Context, params *even
user.SubscriptionEndsAt = ¶ms.SubscriptionEndsAt
if err = service.repository.Update(ctx, user); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after update", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after update", user, params.UserID))
}
return nil
@@ -454,7 +454,7 @@ func (service *UserService) ExpireSubscription(ctx context.Context, params *even
user, err := service.repository.Load(ctx, params.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID))
}
user.SubscriptionID = nil
@@ -464,7 +464,7 @@ func (service *UserService) ExpireSubscription(ctx context.Context, params *even
user.SubscriptionEndsAt = nil
if err = service.repository.Update(ctx, user); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID))
}
return nil
@@ -477,7 +477,7 @@ func (service *UserService) UpdateSubscription(ctx context.Context, params *even
user, err := service.repository.Load(ctx, params.UserID)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID))
}
if params.SubscriptionStatus != "active" {
@@ -493,7 +493,7 @@ func (service *UserService) UpdateSubscription(ctx context.Context, params *even
user.SubscriptionStatus = ¶ms.SubscriptionStatus
if err = service.repository.Update(ctx, user); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after subscription update", user, params.UserID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after subscription update", user, params.UserID))
}
return nil
@@ -505,7 +505,7 @@ func (service *UserService) DeleteAuthUser(ctx context.Context, userID entities.
defer span.End()
if err := service.authClient.DeleteUser(ctx, userID.String()); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.AuthContext] from firebase with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.AuthContext] from firebase with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted [entities.AuthContext] from firebase for user with ID [%s]", userID))
diff --git a/api/pkg/services/webhook_service.go b/api/pkg/services/webhook_service.go
index 581fa202d..ee5918d37 100644
--- a/api/pkg/services/webhook_service.go
+++ b/api/pkg/services/webhook_service.go
@@ -61,7 +61,7 @@ func (service *WebhookService) DeleteAllForUser(ctx context.Context, userID enti
defer span.End()
if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Webhook] for user with ID [%s]", userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Webhook] for user with ID [%s]", userID))
}
ctxLogger.Info(fmt.Sprintf("deleted all [entities.Webhook] for user with ID [%s]", userID))
@@ -77,7 +77,7 @@ func (service *WebhookService) Index(ctx context.Context, userID entities.UserID
webhooks, err := service.repository.Index(ctx, userID, params)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch webhooks with params [%+#v]", params))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch webhooks with params [%+#v]", params))
}
ctxLogger.Info(fmt.Sprintf("fetched [%d] webhooks with prams [%+#v]", len(webhooks), params))
@@ -92,11 +92,11 @@ func (service *WebhookService) Delete(ctx context.Context, userID entities.UserI
ctxLogger := service.tracer.CtxLogger(service.logger, span)
if _, err := service.repository.Load(ctx, userID, webhookID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", userID, webhookID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", userID, webhookID))
}
if err := service.repository.Delete(ctx, userID, webhookID); err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete webhook with id [%s] and user id [%s]", webhookID, userID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete webhook with id [%s] and user id [%s]", webhookID, userID))
}
ctxLogger.Info(fmt.Sprintf("deleted webhook with id [%s] and user id [%s]", webhookID, userID))
@@ -131,7 +131,7 @@ func (service *WebhookService) Store(ctx context.Context, params *WebhookStorePa
}
if err := service.repository.Save(ctx, webhook); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save webhook with id [%s]", webhook.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save webhook with id [%s]", webhook.ID))
}
ctxLogger.Info(fmt.Sprintf("webhook saved with id [%s] for user [%s] in the [%T]", webhook.ID, webhook.UserID, service.repository))
@@ -155,7 +155,7 @@ func (service *WebhookService) Update(ctx context.Context, params *WebhookUpdate
webhook, err := service.repository.Load(ctx, params.UserID, params.WebhookID)
if err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", params.UserID, params.WebhookID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", params.UserID, params.WebhookID))
}
webhook.URL = params.URL
@@ -164,7 +164,7 @@ func (service *WebhookService) Update(ctx context.Context, params *WebhookUpdate
webhook.PhoneNumbers = params.PhoneNumbers
if err = service.repository.Save(ctx, webhook); err != nil {
- return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save webhook with id [%s] after update", webhook.ID))
+ return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save webhook with id [%s] after update", webhook.ID))
}
ctxLogger.Info(fmt.Sprintf("webhook updated with id [%s] in the [%T]", webhook.ID, service.repository))
@@ -178,7 +178,7 @@ func (service *WebhookService) Send(ctx context.Context, userID entities.UserID,
webhooks, err := service.repository.LoadByEvent(ctx, userID, event.Type(), phoneNumber)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhooks for userID [%s] and event [%s]", userID, event.Type()))
+ return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhooks for userID [%s] and event [%s]", userID, event.Type()))
}
if len(webhooks) == 0 {
@@ -212,12 +212,12 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud
request, err := service.createRequest(requestCtx, event, webhook)
if err != nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))
+ return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))
}
response, err := service.client.Do(request)
if err != nil {
- ctxLogger.Warn(stacktrace.Propagate(err, "cannot send [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))
+ ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))
if attempts == 1 {
return err
}
@@ -228,16 +228,16 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud
defer func() {
err = response.Body.Close()
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot close response body for [%s] event with ID [%s] after [%d] attempts", event.Type(), event.ID(), attempts))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot close response body for [%s] event with ID [%s] after [%d] attempts", event.Type(), event.ID(), attempts))
}
}()
if response.StatusCode >= 400 {
ctxLogger.Info(fmt.Sprintf("cannot send [%s] event to webhook [%s] for user [%s] with response code [%d] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, response.StatusCode, attempts))
if attempts == 1 {
- return stacktrace.NewError("%s", http.StatusText(response.StatusCode))
+ return stacktrace.NewErrorf("%s", http.StatusText(response.StatusCode))
}
- service.handleWebhookSendFailed(ctx, event, webhook, owner, stacktrace.NewError("%s", http.StatusText(response.StatusCode)), response)
+ service.handleWebhookSendFailed(ctx, event, webhook, owner, stacktrace.NewErrorf("%s", http.StatusText(response.StatusCode)), response)
return nil
}
@@ -245,7 +245,7 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud
return nil
})
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)))
}
}
@@ -255,12 +255,12 @@ func (service *WebhookService) createRequest(ctx context.Context, event cloudeve
payload, err := json.Marshal(service.getPayload(ctxLogger, event, webhook))
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot marshal payload for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID())
+ return nil, stacktrace.Propagatef(err, "cannot marshal payload for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID())
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, webhook.URL, bytes.NewReader(payload))
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot create request for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID())
+ return nil, stacktrace.Propagatef(err, "cannot create request for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID())
}
request.Header.Add("X-Event-Type", event.Type())
@@ -269,7 +269,7 @@ func (service *WebhookService) createRequest(ctx context.Context, event cloudeve
if strings.TrimSpace(webhook.SigningKey) != "" {
token, err := service.getAuthToken(webhook)
if err != nil {
- return nil, stacktrace.Propagate(err, "cannot generate auth token for user [%s] and webhook [%s]", webhook.UserID, webhook.ID)
+ return nil, stacktrace.Propagatef(err, "cannot generate auth token for user [%s] and webhook [%s]", webhook.UserID, webhook.ID)
}
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
@@ -290,7 +290,7 @@ func (service *WebhookService) getPayload(ctxLogger telemetry.Logger, event clou
err := event.DataAs(payload)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload))
return event
}
@@ -369,12 +369,12 @@ func (service *WebhookService) handleWebhookSendFailed(ctx context.Context, even
event, err = service.createEvent(events.EventTypeWebhookSendFailed, event.Source(), payload)
if err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user with id [%s]", events.EventTypeWebhookSendFailed, payload.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user with id [%s]", events.EventTypeWebhookSendFailed, payload.UserID)))
return
}
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
- ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID)))
+ ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID)))
return
}
diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go
index 9107afcce..7f9d9517c 100644
--- a/api/pkg/telemetry/gorm_logger.go
+++ b/api/pkg/telemetry/gorm_logger.go
@@ -46,7 +46,7 @@ func (gorm *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (s
msg := fmt.Sprintf("[ROWS:%d][%s]", rows, sql)
if err != nil {
- l.Error(stacktrace.Propagate(err, "[ROWS:%d][%s]", rows, sql))
+ l.Error(stacktrace.Propagatef(err, "[ROWS:%d][%s]", rows, sql))
return
}
diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go
index 8b9b926d2..588407ac8 100644
--- a/api/pkg/validators/bulk_message_handler_validator.go
+++ b/api/pkg/validators/bulk_message_handler_validator.go
@@ -60,7 +60,7 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID
if err != nil {
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, "cannot load user [%s]", userID)))
+ ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user [%s]", userID)))
return nil, nil, result
}
@@ -104,7 +104,7 @@ func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user
return v.parseXlsx(ctxLogger, user, header)
}
- ctxLogger.Error(stacktrace.NewError("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type")))
+ ctxLogger.Error(stacktrace.NewErrorf("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))
@@ -119,7 +119,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user
excel, err := excelize.OpenReader(bytes.NewReader(content))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot generate excel file from [%s] for user [%s]", header.Filename, user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot generate excel file from [%s] for user [%s]", header.Filename, user.ID))
result.Add("document", fmt.Sprintf("Cannot parse the uploaded excel file with name [%s].", header.Filename))
return nil, result
}
@@ -127,7 +127,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user
rows, err := excel.GetRows(excel.GetSheetName(0))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot get rows from excel file [%s] for user [%s]", header.Filename, user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get rows from excel file [%s] for user [%s]", header.Filename, user.ID))
result.Add("document", fmt.Sprintf("Cannot parse the uploaded excel file with name [%s].", header.Filename))
return nil, result
}
@@ -177,19 +177,19 @@ func (v *BulkMessageHandlerValidator) parseBytes(ctxLogger telemetry.Logger, use
file, err := header.Open()
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot open file [%s] for reading for user [%s]", header.Filename, userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot open file [%s] for reading for user [%s]", header.Filename, userID))
result.Add("document", fmt.Sprintf("Cannot open the uploaded file with name [%s].", header.Filename))
return nil, result
}
defer func() {
if e := file.Close(); e != nil {
- ctxLogger.Error(stacktrace.Propagate(e, "cannot close file [%s] for user [%s]", header.Filename, userID))
+ ctxLogger.Error(stacktrace.Propagatef(e, "cannot close file [%s] for user [%s]", header.Filename, userID))
}
}()
b := new(bytes.Buffer)
if _, err = io.Copy(b, file); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "cannot copy file [%s] to buffer for user [%s]", header.Filename, userID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot copy file [%s] to buffer for user [%s]", header.Filename, userID))
result.Add("document", fmt.Sprintf("Cannot read the conents of the uploaded file [%s].", header.Filename))
return nil, result
}
@@ -205,7 +205,7 @@ 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, "cannot unmarshall contents [%s] into type [%T] for file [%s] and user [%s]", content, messages, header.Filename, user.ID))
+ ctxLogger.Error(stacktrace.Propagatef(err, "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 contents of the uploaded file [%s].", header.Filename))
return nil, result
}
diff --git a/api/pkg/validators/discord_handler_validator.go b/api/pkg/validators/discord_handler_validator.go
index 035777ceb..69f4284ef 100644
--- a/api/pkg/validators/discord_handler_validator.go
+++ b/api/pkg/validators/discord_handler_validator.go
@@ -91,12 +91,12 @@ func (validator *DiscordHandlerValidator) ValidateStore(ctx context.Context, req
}
if _, _, err := validator.client.Channel.Get(ctx, request.IncomingChannelID); err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID)))
result.Add("incoming_channel_id", fmt.Sprintf("cannot fetch discord channel with ID [%s] make sure the bot has access to the channel", request.IncomingChannelID))
}
if _, _, err := validator.client.Guild.Get(ctx, request.ServerID); err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord server with ID [%s]", request.ServerID)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord server with ID [%s]", request.ServerID)))
result.Add("server_id", fmt.Sprintf("cannot fetch discord server with ID [%s] make sure the bot has access to the server", request.ServerID))
}
@@ -139,12 +139,12 @@ func (validator *DiscordHandlerValidator) ValidateUpdate(ctx context.Context, re
}
if _, _, err := validator.client.Channel.Get(ctx, request.IncomingChannelID); err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID)))
result.Add("incoming_channel_id", fmt.Sprintf("cannot fetch discord channel with ID [%s] make sure the bot has access to the channel", request.IncomingChannelID))
}
if _, _, err := validator.client.Guild.Get(ctx, request.ServerID); err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord server with ID [%s]", request.ServerID)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord server with ID [%s]", request.ServerID)))
result.Add("server_id", fmt.Sprintf("cannot fetch discord server with ID [%s] make sure the bot has access to the server", request.ServerID))
}
diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go
index 998b4d804..d4da9e109 100644
--- a/api/pkg/validators/message_handler_validator.go
+++ b/api/pkg/validators/message_handler_validator.go
@@ -179,7 +179,7 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context
}
if err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load phone for user [%s] and phone [%s]", userID, request.From)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load phone for user [%s] and phone [%s]", userID, request.From)))
result.Add("from", fmt.Sprintf("could not validate 'from' number [%s], please try again later", request.From))
}
@@ -229,7 +229,7 @@ func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Con
}
if err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load phone for user [%s] and phone [%s]", userID, request.From)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load phone for user [%s] and phone [%s]", userID, request.From)))
result.Add("from", fmt.Sprintf("could not validate 'from' number [%s], please try again later", request.From))
}
diff --git a/api/pkg/validators/turnstile_token_validator.go b/api/pkg/validators/turnstile_token_validator.go
index 5d30b4601..ac1a3d3e5 100644
--- a/api/pkg/validators/turnstile_token_validator.go
+++ b/api/pkg/validators/turnstile_token_validator.go
@@ -55,27 +55,27 @@ func (v *TurnstileTokenValidator) ValidateToken(ctx context.Context, ipAddress,
"remoteip": ipAddress,
})
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "failed to marshal payload"))
+ ctxLogger.Error(stacktrace.Propagatef(err, "failed to marshal payload"))
return false
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://challenges.cloudflare.com/turnstile/v0/siteverify", bytes.NewBuffer(payload))
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "failed to create http request request"))
+ ctxLogger.Error(stacktrace.Propagatef(err, "failed to create http request request"))
return false
}
request.Header.Set("Content-Type", "application/json")
response, err := v.httpClient.Do(request)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "failed to send http request to [%s]", request.URL.String()))
+ ctxLogger.Error(stacktrace.Propagatef(err, "failed to send http request to [%s]", request.URL.String()))
return false
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "failed to read response body from cloudflare turnstile"))
+ ctxLogger.Error(stacktrace.Propagatef(err, "failed to read response body from cloudflare turnstile"))
return false
}
@@ -83,7 +83,7 @@ func (v *TurnstileTokenValidator) ValidateToken(ctx context.Context, ipAddress,
data := new(turnstileVerifyResponse)
if err = json.Unmarshal(body, data); err != nil {
- ctxLogger.Error(stacktrace.Propagate(err, "failed to unmarshal response from cloudflare turnstile"))
+ ctxLogger.Error(stacktrace.Propagatef(err, "failed to unmarshal response from cloudflare turnstile"))
return false
}
diff --git a/api/pkg/validators/user_handler_validator.go b/api/pkg/validators/user_handler_validator.go
index ee163d821..42a6b91f9 100644
--- a/api/pkg/validators/user_handler_validator.go
+++ b/api/pkg/validators/user_handler_validator.go
@@ -112,7 +112,7 @@ func (validator *UserHandlerValidator) ValidatePaymentInvoice(ctx context.Contex
payments, err := validator.service.GetSubscriptionPayments(ctx, userID)
if err != nil {
- ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get subscription payments for user with ID [%s]", userID)))
+ ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get subscription payments for user with ID [%s]", userID)))
validationErrors.Add("subscriptionInvoiceID", "failed to validate subscription payment invoice ID")
return validationErrors
}
From 96ffb5c7788602e2f743a0d876de58967bca666f Mon Sep 17 00:00:00 2001
From: Acho Arnold
Date: Sun, 19 Jul 2026 19:50:37 +0300
Subject: [PATCH 07/88] feat(web): redirect logged-in users from root to
/threads (opt-in) (#950)
* docs: add design spec for root->threads redirect
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
* docs: add implementation plan for root->threads redirect
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
* feat(web): add redirectPreference store for per-browser threads redirect
* feat(web): optimistically redirect root to /threads when opted in
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(web): add redirect opt-in popover component
* feat(web): mount redirect opt-in popover under Dashboard button
* feat(web): clear threads-redirect preference on logout
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(web): use replace navigation in redirect enable() to avoid stuck Back button
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
* ci(web): pin pnpm to 11.11.0 to avoid installer bug
pnpm 11.12.0 crashes pnpm/action-setup (pnpm/action-setup#276).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
* fix(web): import ref from vue, drop gitignored docs
Resolves Codacy type findings; removes gitignored docs/ files.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce
* fix(web): persist redirect before navigation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f
* fix(web): gate landing nav auth buttons on authStateChanged
On CTRL+R reload of /, Firebase restores the session asynchronously. The website layout rendered
Login/Get Started while authUser was still null, hiding the Dashboard button until the state
resolved. Wait for authStateChanged before showing any auth-dependent button, matching the
default (dashboard) layout.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f
* refactor(web): render redirect prompt as v-menu not v-card
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/workflows/web.yml | 4 +-
web/app/components/MessageThreadHeader.vue | 2 +
web/app/components/RedirectPromptPopover.vue | 59 ++++++++++++++++++++
web/app/layouts/website.vue | 31 ++++++----
web/app/middleware/redirectToThreads.ts | 11 ++++
web/app/pages/index.vue | 1 +
web/app/pages/settings/index.vue | 2 +
web/app/stores/redirectPreference.ts | 47 ++++++++++++++++
8 files changed, 144 insertions(+), 13 deletions(-)
create mode 100644 web/app/components/RedirectPromptPopover.vue
create mode 100644 web/app/middleware/redirectToThreads.ts
create mode 100644 web/app/stores/redirectPreference.ts
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
index 350bf3256..fd5ea84e6 100644
--- a/.github/workflows/web.yml
+++ b/.github/workflows/web.yml
@@ -34,7 +34,7 @@ jobs:
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
- version: 11
+ version: 11.11.0
- name: Install dependencies 📦
run: pnpm install
@@ -66,7 +66,7 @@ jobs:
- uses: pnpm/action-setup@v6
name: Install pnpm
with:
- version: 11
+ version: 11.11.0
- name: Install dependencies 📦
run: pnpm install
diff --git a/web/app/components/MessageThreadHeader.vue b/web/app/components/MessageThreadHeader.vue
index 6d13a7e20..1e1a81eb2 100644
--- a/web/app/components/MessageThreadHeader.vue
+++ b/web/app/components/MessageThreadHeader.vue
@@ -27,6 +27,7 @@ const phonesStore = usePhonesStore()
const threadsStore = useThreadsStore()
const appStore = useAppStore()
const notificationsStore = useNotificationsStore()
+const redirectPreferenceStore = useRedirectPreferenceStore()
const selectedMenuItem = ref(-1)
@@ -71,6 +72,7 @@ async function logout() {
authStore.resetState()
phonesStore.resetState()
threadsStore.resetState()
+ redirectPreferenceStore.resetState()
notificationsStore.addNotification({
type: 'info',
message: 'You have successfully logged out',
diff --git a/web/app/components/RedirectPromptPopover.vue b/web/app/components/RedirectPromptPopover.vue
new file mode 100644
index 000000000..859a71dac
--- /dev/null
+++ b/web/app/components/RedirectPromptPopover.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ Skip this page next time?
+
+
+
+
+
+
+ Always open dashboard
+
+
+
+
+
+
+
diff --git a/web/app/layouts/website.vue b/web/app/layouts/website.vue
index 1aadb1b64..3b6565a5a 100644
--- a/web/app/layouts/website.vue
+++ b/web/app/layouts/website.vue
@@ -80,7 +80,11 @@ function goToPricing() {
Blog
For Free
-
- Dashboard
-
+
+ Dashboard
+
+
+
diff --git a/web/app/middleware/redirectToThreads.ts b/web/app/middleware/redirectToThreads.ts
new file mode 100644
index 000000000..a605286fb
--- /dev/null
+++ b/web/app/middleware/redirectToThreads.ts
@@ -0,0 +1,11 @@
+import { STORAGE_KEY } from '~/stores/redirectPreference'
+
+export default defineNuxtRouteMiddleware(() => {
+ try {
+ if (localStorage.getItem(STORAGE_KEY) === 'true') {
+ return navigateTo('/threads', { replace: true })
+ }
+ } catch (error) {
+ console.error(error)
+ }
+})
diff --git a/web/app/pages/index.vue b/web/app/pages/index.vue
index 524c6637b..9790b5e7f 100644
--- a/web/app/pages/index.vue
+++ b/web/app/pages/index.vue
@@ -28,6 +28,7 @@ import {
definePageMeta({
layout: 'website',
+ middleware: ['redirect-to-threads'],
})
useSeoMeta({
diff --git a/web/app/pages/settings/index.vue b/web/app/pages/settings/index.vue
index ea1752533..1f381416b 100644
--- a/web/app/pages/settings/index.vue
+++ b/web/app/pages/settings/index.vue
@@ -47,6 +47,7 @@ const authStore = useAuthStore()
const phonesStore = usePhonesStore()
const billingStore = useBillingStore()
const notificationsStore = useNotificationsStore()
+const redirectPreferenceStore = useRedirectPreferenceStore()
const firebaseUser = ref(null)
const gravatarUrl = ref(null)
@@ -773,6 +774,7 @@ async function deleteUserAccount() {
await signOut(auth)
authStore.resetState()
phonesStore.resetState()
+ redirectPreferenceStore.resetState()
notificationsStore.addNotification({
type: 'info',
message: 'You have successfully logged out',
diff --git a/web/app/stores/redirectPreference.ts b/web/app/stores/redirectPreference.ts
new file mode 100644
index 000000000..030def894
--- /dev/null
+++ b/web/app/stores/redirectPreference.ts
@@ -0,0 +1,47 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const STORAGE_KEY = 'httpsms_redirect_to_threads'
+
+function readFlag(): boolean {
+ try {
+ return localStorage.getItem(STORAGE_KEY) === 'true'
+ } catch (error) {
+ console.error(error)
+ return false
+ }
+}
+
+export const useRedirectPreferenceStore = defineStore(
+ 'redirectPreference',
+ () => {
+ const enabled = ref(readFlag())
+ const dismissedThisSession = ref(false)
+
+ function enable() {
+ try {
+ localStorage.setItem(STORAGE_KEY, 'true')
+ enabled.value = true
+ navigateTo('/threads', { replace: true })
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ function dismiss() {
+ dismissedThisSession.value = true
+ }
+
+ function resetState() {
+ enabled.value = false
+ dismissedThisSession.value = false
+ try {
+ localStorage.removeItem(STORAGE_KEY)
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ return { enabled, dismissedThisSession, enable, dismiss, resetState }
+ },
+)
From 9444545e67e9cd904095fc1acc81c15d315f5d0b Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 19:51:31 +0300
Subject: [PATCH 08/88] style: add arrow position
---
web/app/components/RedirectPromptPopover.vue | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/web/app/components/RedirectPromptPopover.vue b/web/app/components/RedirectPromptPopover.vue
index 859a71dac..c2de18200 100644
--- a/web/app/components/RedirectPromptPopover.vue
+++ b/web/app/components/RedirectPromptPopover.vue
@@ -31,7 +31,7 @@ const menuOpen = computed({
:open-on-click="false"
:close-on-content-click="false"
>
-
+
Skip this page next time?
@@ -49,10 +49,9 @@ const menuOpen = computed({
- Always open dashboard
-
-
-
+ Always open dashboard
From 78e1d183a13f5e43143fba2c6dae1d883c05dcc5 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Sun, 19 Jul 2026 20:13:15 +0300
Subject: [PATCH 09/88] docs: add contacts feature design spec
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8dd82cae-8bc6-4eaa-9b90-95073720c577
---
.../2026-07-19-contacts-feature-design.md | 252 ++++++++++++++++++
1 file changed, 252 insertions(+)
create mode 100644 docs/superpowers/specs/2026-07-19-contacts-feature-design.md
diff --git a/docs/superpowers/specs/2026-07-19-contacts-feature-design.md b/docs/superpowers/specs/2026-07-19-contacts-feature-design.md
new file mode 100644
index 000000000..58f9d9e40
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-19-contacts-feature-design.md
@@ -0,0 +1,252 @@
+# Contacts Feature — Design
+
+Date: 2026-07-19
+Status: Approved (pending implementation plan)
+
+## 1. Summary
+
+Add a **Contacts** feature to httpSMS. Users can store contacts (name, emails,
+phone numbers, and free-form properties) and manage them through a dedicated web
+page and a REST API. Once contacts exist, the threads page displays the
+contact's **name** instead of the raw phone number.
+
+Design priority (explicit user goal): resolve contact names into the threads
+page with **as few database queries as possible**, and make **changing a
+contact's name cheap** (no write amplification across threads).
+
+## 2. Goals
+
+- CRUD for contacts via a REST API under `/v1/contacts`.
+- A single create endpoint that accepts **one or many** contacts in one request.
+- Import contacts from **CSV** (Excel/XLSX is **not** supported for contacts).
+- A contact has: `Name` (required), `Emails` (optional array), `PhoneNumbers`
+ (array, >= 1), and free-form `Properties` (`map[string]string`).
+- Contacts are **global to the user account** (name shows across all the user's
+ owner phones).
+- Threads display the resolved contact name (and expose full contact details)
+ when requested, everywhere the peer number is shown (threads list + thread
+ header/title).
+- Dedicated Contacts web page with full CRUD + import.
+
+## 3. Non-goals / Explicit decisions
+
+- **No uniqueness constraint** on phone numbers. Multiple contacts may share a
+ phone number. There is **no** normalized `contact_phone_numbers` lookup table
+ and **no** DB-level unique index on numbers.
+- When a thread's number matches **multiple** contacts, the **most recently
+ updated** contact wins for display.
+- Thread-name resolution is **opt-in** per request (default off).
+- `Properties` are API/JSON only; not part of the flat CSV template.
+
+## 4. Data model
+
+### 4.1 `Contact` entity (`api/pkg/entities/contact.go`)
+
+| Field | Type | GORM / notes |
+| -------------- | ------------------- | --------------------------------------------- |
+| `ID` | `uuid.UUID` | `primaryKey;type:uuid` |
+| `UserID` | `entities.UserID` | indexed |
+| `Name` | `string` | required |
+| `Emails` | `pq.StringArray` | `gorm:"type:text[]"` `swaggertype:"array,string"`; optional, each validated |
+| `PhoneNumbers` | `pq.StringArray` | `gorm:"type:text[]"` `swaggertype:"array,string"`; >= 1, each valid E.164 |
+| `Properties` | `ContactProperties` | `gorm:"type:jsonb"`; free-form key/value map |
+| `CreatedAt` | `time.Time` | |
+| `UpdatedAt` | `time.Time` | |
+
+Follows the existing `pq.StringArray` convention used in `webhook.go` and
+`phone_api_key.go`. Auto-migrated in `pkg/di/container.go` alongside the other
+entities.
+
+### 4.2 `ContactProperties` custom type
+
+A small custom type to avoid adding a new dependency (`gorm.io/datatypes` is not
+currently used):
+
+```go
+// ContactProperties is a free-form key/value map persisted as a jsonb column.
+type ContactProperties map[string]string
+
+func (p ContactProperties) Value() (driver.Value, error) // json.Marshal -> []byte
+func (p *ContactProperties) Scan(src any) error // json.Unmarshal from []byte/string
+```
+
+Includes unit tests for `Value`/`Scan` round-trips (nil, empty, populated).
+
+### 4.3 `MessageThread` additions (`api/pkg/entities/message_thread.go`)
+
+Add a **non-persisted** field carrying the resolved contact:
+
+```go
+ContactDetails *Contact `json:"contact_details,omitempty" gorm:"-"`
+```
+
+- `gorm:"-"` — never read/written to the DB.
+- `omitempty` — absent from responses unless resolution attached it.
+- Named `ContactDetails` (JSON `contact_details`) to avoid colliding with the
+ existing `Contact string json:"contact"` field, which holds the peer phone
+ number.
+
+## 5. API
+
+New `ContactHandler` mirroring existing handler/service/repository/validator
+layering. Routes registered via the `handler.register(...)` helper (Fiber v3),
+wired in the DI container. All routes are under `/v1/` and behind the standard
+auth middleware chain.
+
+| Method | Route | Purpose |
+| -------- | ------------------------ | ------------------------------------------------------------- |
+| `GET` | `/v1/contacts` | List (paginated; `?query=` searches name / email / number) |
+| `POST` | `/v1/contacts` | Create **one or many** — body is a JSON array of contacts |
+| `POST` | `/v1/contacts/upload` | Import from CSV (`document` multipart form field; CSV only) |
+| `PUT` | `/v1/contacts/:contactID`| Update name / emails / phone numbers / properties |
+| `DELETE` | `/v1/contacts/:contactID`| Delete a contact |
+
+### 5.1 Create (one or many)
+
+Request body (array form or `{ "contacts": [...] }`):
+
+```json
+{
+ "contacts": [
+ {
+ "name": "Alice Smith",
+ "emails": ["alice@example.com"],
+ "phone_numbers": ["+18005550199", "+18005550100"],
+ "properties": { "company": "Acme", "role": "CTO" }
+ }
+ ]
+}
+```
+
+- Per-item validation with row-indexed error messages (mirroring bulk-message
+ validation style using `url.Values`).
+- Batch capped at <= 1000 contacts.
+- Cache invalidated **once** after the batch commits.
+
+### 5.2 CSV import (CSV only)
+
+- Reuses the bulk-message CSV parsing pattern (`csvutil`; <= 500 KB; <= 1000
+ rows). **Excel/XLSX is not supported** — only `text/csv` / `.csv` is accepted;
+ any other file type returns a validation error.
+- Columns: `Name, Emails, PhoneNumbers`. `Emails` and `PhoneNumbers` cells hold
+ multiple values separated by `;` (or `,`). `Properties` is not part of the
+ flat template.
+- Published template: `httpsms-contacts.csv` (same location/convention as the
+ bulk-message template).
+
+### 5.3 Validation rules
+
+- `Name` required and non-empty per contact.
+- Each phone number must parse as E.164 (`nyaruka/phonenumbers`); >= 1 required.
+- Each email (if present) must be a valid email.
+- `properties` keys/values are free-form strings.
+
+## 6. Thread name resolution + caching
+
+### 6.1 Contact map cache
+
+- A per-user map `phone_number -> *Contact` is built from **all** the user's
+ contacts. On phone-number collisions, the **most recently updated** contact
+ wins (sort by `UpdatedAt` ascending while building so later writes overwrite).
+- Serialized as JSON in `cache.Cache` under key `contacts.map.`,
+ TTL ~24h.
+- `cache.Cache` exposes only `Get`/`Set` (no delete), so **invalidation =
+ overwrite** the key with an empty marker after any contact mutation; the next
+ request that needs it lazily rebuilds from the DB.
+
+### 6.2 `GetThreads` flow
+
+1. Load threads (existing single query, unchanged).
+2. **If** the request's `Contacts` flag is true: fetch the user's contact map
+ (cache hit → 0 DB queries; miss → one query to rebuild), then attach
+ `ContactDetails` to each thread in memory by looking up `thread.contact`.
+3. If the flag is false/absent: skip resolution entirely (no cache/DB work);
+ behavior identical to today.
+
+Cost profile:
+
+- Threads read: **1 DB query** (unchanged).
+- Contact resolution: usually a **cache hit → 0 DB queries**; DB touched only on
+ cache miss or right after a contact write.
+- Contact **name change**: single-row `UPDATE` + cache invalidation.
+ **Zero thread writes / zero propagation.**
+
+### 6.3 Opt-in filter
+
+- `GET /v1/message-threads` gains an optional query param `?contacts=true`
+ bound onto `requests.MessageThreadIndex` (`Contacts bool`, default `false`).
+- Threaded through `MessageThreadGetParams` to the service, controlling whether
+ step 2 above runs.
+
+## 7. Frontend (Nuxt 4 / Pinia)
+
+- **`useContactsStore`** (`web/app/stores/contacts.ts`): `loadContacts`,
+ `saveContacts` (array), `updateContact`, `deleteContact`, `uploadCsv`.
+- **Contacts page** (`web/app/pages/contacts/index.vue`) — layout modeled on the
+ provided reference (Plunk-style):
+ - **Header row**: large `Contacts` title (`text-display-large`, no glowing
+ gradient) with a subtitle like `Manage your contacts. {total} total`, and,
+ aligned top-right, an outlined **Import CSV** button and a filled primary
+ **Add Contact** button.
+ - **Search bar** below the header filtering by name / email / phone number
+ (drives the `?query=` API param, debounced).
+ - **Data table** (`VDataTable`) with columns: `Name`, `Phone Numbers`,
+ `Emails`, `Created`, `Updated`, and a right-aligned **Actions** column with
+ per-row **edit** (pencil, `mdiPencil`) and **delete** (trash,
+ `mdiDelete`) icon buttons.
+ - Timestamps rendered relatively (e.g. "31 minutes ago") via `useFilters()`.
+ - Pagination for large lists.
+ - **Modals** (all `VDialog` with `opacity="0.9"`, Close button
+ `color="warning"`):
+ - **Add / Edit Contact** dialog — form for `Name`, repeatable
+ `PhoneNumbers`, repeatable `Emails`, and free-form `Properties`
+ (key/value rows). Same dialog component for create and edit.
+ - **Delete Contact** confirmation dialog.
+ - **Import CSV** dialog — file input accepting `.csv` only, a link to the
+ `httpsms-contacts.csv` template, and inline row-indexed error display.
+ - Hyperlinks use `text-decoration-none hover:text-decoration-underline`.
+- **Threads UI**:
+ - `threads` store `loadThreads` passes `contacts: true`.
+ - `MessageThread.vue` title and `MessageThreadHeader.vue` render
+ `thread.contact_details?.name ?? formatPhoneNumber(thread.contact)`; the
+ avatar uses the resolved name's first letter when present.
+ - Filter helpers pulled from `useFilters()` in `
+
+
+
+
+
+
+
+ Contacts
+
+
+
+
+
+
+
+
+
Contacts
+
+ {{ contactsStore.total }}
+ {{ contactsStore.total === 1 ? 'contact' : 'contacts' }}
+
+
+
+
+
+ Import CSV
+
+
+ Add Contact
+
+
+
+
+
+
+
+
+
+ | Name |
+ Emails |
+ Phone Numbers |
+ Actions |
+
+
+
+
+ | {{ contact.name }} |
+
+ {{ (contact.emails ?? []).join(', ') }}
+ |
+
+ {{
+ (contact.phone_numbers ?? [])
+ .map((number) => formatPhoneNumber(number))
+ .join(', ')
+ }}
+ |
+
+
+
+
+
+
+
+ |
+
+
+ |
+ No contacts found.
+ |
+
+
+
+
+
+
+
+
+
+
+
+ {{ dialogTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
+ Save
+
+
+
+
+
+
+
+
+
+ Delete Contact
+
+
+
+
+
+
+ Are you sure you want to delete this contact? This action cannot be
+ undone.
+
+
+
+
+ Close
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+ Import Contacts from CSV
+
+
+
+
+
+
+
+ Download our
+ CSV template, fill it in and upload it here. Columns: Name, Emails,
+ PhoneNumbers (multiple values separated by ";").
+
+
+
+
+
+
+ Close
+
+
+ Import
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Lint the page**
+
+Run: `cd web && pnpm lint:js && pnpm lint:prettier`
+Expected: no errors for `app/pages/contacts/index.vue`. If Prettier reports formatting, run `pnpm lintfix` and re-check.
+
+- [ ] **Step 3: Build the site to confirm it compiles**
+
+Run: `cd web && pnpm generate`
+Expected: build completes without type errors (the `/contacts` route is emitted).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add web/app/pages/contacts/index.vue
+git commit -m "feat(web): add contacts page with add/edit/delete/import dialogs"
+```
+
+---
+
+### Task 13: Show contact names on threads + Contacts nav link
+
+**Files:**
+- Modify: `web/app/stores/threads.ts` (send `contacts: true`)
+- Modify: `web/app/components/MessageThread.vue` (list title + avatar initial)
+- Modify: `web/app/pages/threads/[id]/index.vue` (toolbar title)
+- Modify: `web/app/components/MessageThreadHeader.vue` (Contacts nav item + icon import)
+
+**Interfaces:**
+- Consumes: `EntitiesMessageThread.contact_details?: EntitiesContact` (Task 10), the `/contacts` route (Task 12).
+- Produces: no new exports — UI wiring only.
+
+- [ ] **Step 1: Request contact resolution when loading threads**
+
+In `web/app/stores/threads.ts`, add `contacts: true` to the `loadThreads` request params:
+
+```ts
+ const response = await apiFetch<{ data: EntitiesMessageThread[] }>(
+ '/v1/message-threads',
+ {
+ params: {
+ owner: phonesStore.owner ?? phonesStore.phones[0]?.phone_number,
+ limit: 100,
+ is_archived: archivedThreads.value,
+ contacts: true,
+ },
+ },
+ )
+```
+
+- [ ] **Step 2: Show the contact name in the threads list**
+
+In `web/app/components/MessageThread.vue`, replace the list-item title expression:
+
+```vue
+ {{
+ thread.contact_details?.name ?? formatPhoneNumber(thread.contact)
+ }}
+```
+
+And update the avatar initial to prefer the contact name (replace the existing `` avatar content):
+
+```vue
+
+
+ {{ mdiAccount }}
+ {{
+ (thread.contact_details?.name ?? thread.contact).substring(0, 1)
+ }}
+
+
+```
+
+- [ ] **Step 3: Show the contact name in the thread header**
+
+In `web/app/pages/threads/[id]/index.vue`, replace the toolbar title expression (around line 271):
+
+```vue
+
+ {{
+ threadsStore.currentThread.contact_details?.name ??
+ formatPhoneNumber(threadsStore.currentThread.contact)
+ }}
+
+```
+
+- [ ] **Step 4: Add the Contacts navigation item**
+
+In `web/app/components/MessageThreadHeader.vue`, add `mdiAccountMultiple` to the `@mdi/js` import block:
+
+```ts
+ mdiCommentTextMultipleOutline,
+ mdiAccountMultiple,
+ mdiCircle,
+```
+
+Add a nav item right after the "Search Messages" `v-list-item` (before the Settings item):
+
+```vue
+
+
+ Contacts
+
+```
+
+- [ ] **Step 5: Lint and build**
+
+Run: `cd web && pnpm lint:js && pnpm generate`
+Expected: no errors; the build succeeds.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add web/app/stores/threads.ts web/app/components/MessageThread.vue web/app/pages/threads/[id]/index.vue web/app/components/MessageThreadHeader.vue
+git commit -m "feat(web): display contact names on threads and add Contacts nav link"
+```
+
+---
+
+## Final Verification
+
+- [ ] **API full test + build**
+
+```bash
+cd api && go test -vet=off ./... && go build ./...
+```
+Expected: all packages pass; build succeeds.
+
+- [ ] **Web lint + build**
+
+```bash
+cd web && pnpm lint && pnpm generate
+```
+Expected: lint clean; static build succeeds.
+
+- [ ] **Manual smoke test (optional, with Docker stack)**
+
+```bash
+docker compose up --build
+```
+Then: create a contact via `POST /v1/contacts`, load `/threads` and confirm the contact name shows for a matching phone number; open `/contacts` and exercise add/edit/delete/import.
diff --git a/docs/superpowers/specs/2026-08-21-contact-phone-input-design.md b/docs/superpowers/specs/2026-08-21-contact-phone-input-design.md
new file mode 100644
index 000000000..2bca6d00b
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-21-contact-phone-input-design.md
@@ -0,0 +1,29 @@
+# Contact Phone Input Design
+
+## Goal
+
+Use the existing `v-phone-input` component for phone numbers in the contact
+add/edit dialog.
+
+## Design
+
+Represent each form phone row with a phone number value and its own country.
+Bind those fields to `v-phone-input` using `v-model` and `v-model:country`,
+following the messages page configuration.
+
+Existing E.164 phone numbers derive their country with `libphonenumber-js`.
+Numbers without a detectable country and newly added rows default to `US`.
+Removing a row removes both its number and country because they are stored in
+one object.
+
+## Behavior
+
+- Keep support for multiple phone numbers.
+- Preserve current required-phone validation and server error display.
+- Submit only trimmed, non-empty phone number values to the API.
+- Keep add, edit, and remove behavior unchanged apart from the country-aware
+ input.
+
+## Validation
+
+Run ESLint, Stylelint, and Prettier against the changed contact page.
diff --git a/docs/superpowers/specs/2026-08-21-contacts-table-header-design.md b/docs/superpowers/specs/2026-08-21-contacts-table-header-design.md
new file mode 100644
index 000000000..afa59dfd4
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-21-contacts-table-header-design.md
@@ -0,0 +1,29 @@
+# Contacts Table Header Styling Design
+
+## Goal
+
+Make every header in the contacts data table uppercase and grey using Vuetify's
+standard text utility classes.
+
+## Design
+
+Add the `headers` slot to the existing `VDataTableServer` in
+`web/app/pages/contacts/index.vue`. Render each header title with
+`text-uppercase text-medium-emphasis` while retaining Vuetify's existing header
+layout and per-column alignment.
+
+This approach keeps presentation in the template, avoids brittle selectors
+against Vuetify internals, and leaves the header definitions and table data flow
+unchanged.
+
+## Scope
+
+- Apply the styling to Name, Phone Numbers, Emails, Created, Updated, and
+ Actions.
+- Preserve the existing data, pagination, loading, and item slot behavior.
+- Do not introduce component-level CSS or change the displayed header labels.
+
+## Validation
+
+Run the existing web lint command and confirm the contacts page template passes
+Vue and TypeScript validation.
diff --git a/tests/.env.test b/tests/.env.test
index 78bb614b2..c4c1e3ef9 100644
--- a/tests/.env.test
+++ b/tests/.env.test
@@ -30,4 +30,5 @@ GCS_BUCKET_NAME=
UPTRACE_DSN=
CLOUDFLARE_TURNSTILE_SECRET_KEY=
HEARTBEAT_DB_BACKEND=mongodb
+CONTACT_DB_BACKEND=mongodb
MONGODB_URI=mongodb://httpsms:testpassword@mongodb:27017/?authSource=admin&appName=httpsms
diff --git a/tests/README.md b/tests/README.md
index 45bbb278f..20e4f5e12 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -55,6 +55,7 @@ The API's Firebase SDK is configured (via `FCM_ENDPOINT` env var) to redirect al
- [x] **Receive SMS E2E** — Phone submits received message to API → message is stored and retrievable via GET endpoint
- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state
- [x] **Unarchive Thread on Receive E2E** — Archived thread returns to the inbox on inbound message when the phone's `unarchive_thread` setting is enabled, and stays archived when disabled
+- [x] **Contacts E2E** — JSON CRUD, search and pagination totals, CSV import normalization, and contact details attached to message threads
## Prerequisites
diff --git a/tests/contacts_integration_test.go b/tests/contacts_integration_test.go
new file mode 100644
index 000000000..2dcd4d5c0
--- /dev/null
+++ b/tests/contacts_integration_test.go
@@ -0,0 +1,332 @@
+package tests
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type integrationContact struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Emails []string `json:"emails"`
+ PhoneNumbers []string `json:"phone_numbers"`
+ Properties map[string]string `json:"properties"`
+}
+
+type integrationContactsResponse struct {
+ Data []integrationContact `json:"data"`
+ Total int64 `json:"total"`
+}
+
+type integrationContactResponse struct {
+ Data integrationContact `json:"data"`
+}
+
+type integrationContactThread struct {
+ ID string `json:"id"`
+ Contact string `json:"contact"`
+ ContactDetails *integrationContact `json:"contact_details"`
+}
+
+func createIntegrationContacts(
+ ctx context.Context,
+ t *testing.T,
+ contacts []map[string]any,
+) []integrationContact {
+ t.Helper()
+
+ var response integrationContactsResponse
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPost,
+ "/v1/contacts",
+ userAPIKey,
+ contacts,
+ http.StatusCreated,
+ &response,
+ )
+ require.Len(t, response.Data, len(contacts))
+ return response.Data
+}
+
+func deleteIntegrationContact(ctx context.Context, t *testing.T, contactID string) {
+ t.Helper()
+
+ requestJSON(
+ ctx,
+ t,
+ http.MethodDelete,
+ "/v1/contacts/"+contactID,
+ userAPIKey,
+ nil,
+ http.StatusNoContent,
+ nil,
+ )
+}
+
+func cleanupIntegrationContact(t *testing.T, contactID string) {
+ t.Helper()
+
+ request, err := http.NewRequest(
+ http.MethodDelete,
+ apiBaseURL+"/v1/contacts/"+contactID,
+ nil,
+ )
+ require.NoError(t, err)
+ request.Header.Set("x-api-key", userAPIKey)
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+ defer response.Body.Close()
+
+ responseBody, err := io.ReadAll(response.Body)
+ require.NoError(t, err)
+ require.Contains(
+ t,
+ []int{http.StatusNoContent, http.StatusNotFound},
+ response.StatusCode,
+ "response: %s",
+ string(responseBody),
+ )
+}
+
+func listIntegrationContacts(
+ ctx context.Context,
+ t *testing.T,
+ query string,
+ skip int,
+ limit int,
+) integrationContactsResponse {
+ t.Helper()
+
+ path := fmt.Sprintf(
+ "/v1/contacts?query=%s&skip=%d&limit=%d",
+ url.QueryEscape(query),
+ skip,
+ limit,
+ )
+ var response integrationContactsResponse
+ requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response)
+ return response
+}
+
+func uploadIntegrationContactsCSV(
+ ctx context.Context,
+ t *testing.T,
+ contents string,
+) integrationContactsResponse {
+ t.Helper()
+
+ var body bytes.Buffer
+ writer := multipart.NewWriter(&body)
+ part, err := writer.CreateFormFile("document", "contacts.csv")
+ require.NoError(t, err)
+ _, err = part.Write([]byte(contents))
+ require.NoError(t, err)
+ require.NoError(t, writer.Close())
+
+ request, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodPost,
+ apiBaseURL+"/v1/contacts/upload",
+ &body,
+ )
+ require.NoError(t, err)
+ request.Header.Set("x-api-key", userAPIKey)
+ request.Header.Set("Content-Type", writer.FormDataContentType())
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+ defer response.Body.Close()
+
+ responseBody, err := io.ReadAll(response.Body)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusCreated, response.StatusCode, "response: %s", string(responseBody))
+
+ var result integrationContactsResponse
+ require.NoError(t, json.Unmarshal(responseBody, &result))
+ return result
+}
+
+func waitForThreadWithContact(
+ ctx context.Context,
+ t *testing.T,
+ owner string,
+ phoneNumber string,
+ timeout time.Duration,
+) integrationContactThread {
+ t.Helper()
+
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ path := fmt.Sprintf(
+ "/v1/message-threads?owner=%s&contacts=true&skip=0&limit=20",
+ url.QueryEscape(owner),
+ )
+ var response struct {
+ Data []integrationContactThread `json:"data"`
+ }
+ requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response)
+ for _, thread := range response.Data {
+ if thread.Contact == phoneNumber && thread.ContactDetails != nil {
+ return thread
+ }
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ t.Fatalf("thread for %s did not include contact details within %v", phoneNumber, timeout)
+ return integrationContactThread{}
+}
+
+func TestContacts_CRUDSearchAndPagination(t *testing.T) {
+ ctx := context.Background()
+ marker := strings.ToLower(uuid.New().String())
+ firstPhone := randomPhoneNumber()
+ secondPhone := randomPhoneNumber()
+
+ created := createIntegrationContacts(ctx, t, []map[string]any{
+ {
+ "name": " Alice " + marker + " ",
+ "emails": []string{" ALICE-" + marker + "@EXAMPLE.COM "},
+ "phone_numbers": []string{strings.TrimPrefix(firstPhone, "+")},
+ "properties": map[string]string{"company": "Acme"},
+ },
+ {
+ "name": "Bob " + marker,
+ "emails": []string{"bob-" + marker + "@example.com"},
+ "phone_numbers": []string{secondPhone},
+ },
+ })
+ for _, contact := range created {
+ contactID := contact.ID
+ t.Cleanup(func() {
+ cleanupIntegrationContact(t, contactID)
+ })
+ }
+
+ require.Len(t, created, 2)
+ assert.Equal(t, "Alice "+marker, created[0].Name)
+ assert.Equal(t, []string{"alice-" + marker + "@example.com"}, created[0].Emails)
+ assert.Equal(t, []string{firstPhone}, created[0].PhoneNumbers)
+ assert.Equal(t, map[string]string{"company": "Acme"}, created[0].Properties)
+
+ page := listIntegrationContacts(ctx, t, marker, 0, 1)
+ assert.EqualValues(t, 2, page.Total)
+ require.Len(t, page.Data, 1)
+
+ emailMatch := listIntegrationContacts(ctx, t, "ALICE-"+marker+"@EXAMPLE.COM", 0, 20)
+ assert.EqualValues(t, 1, emailMatch.Total)
+ require.Len(t, emailMatch.Data, 1)
+ assert.Equal(t, created[0].ID, emailMatch.Data[0].ID)
+
+ var updated integrationContactResponse
+ requestJSON(
+ ctx,
+ t,
+ http.MethodPut,
+ "/v1/contacts/"+created[0].ID,
+ userAPIKey,
+ map[string]any{
+ "name": "Updated " + marker,
+ "emails": []string{"updated-" + marker + "@example.com"},
+ "phone_numbers": []string{firstPhone},
+ "properties": map[string]string{"company": "NdoleStudio"},
+ },
+ http.StatusOK,
+ &updated,
+ )
+ assert.Equal(t, "Updated "+marker, updated.Data.Name)
+ assert.Equal(t, []string{"updated-" + marker + "@example.com"}, updated.Data.Emails)
+ assert.Equal(t, map[string]string{"company": "NdoleStudio"}, updated.Data.Properties)
+
+ deleteIntegrationContact(ctx, t, created[0].ID)
+
+ deletedMatch := listIntegrationContacts(ctx, t, "updated-"+marker+"@example.com", 0, 20)
+ assert.Zero(t, deletedMatch.Total)
+ assert.Empty(t, deletedMatch.Data)
+}
+
+func TestContacts_CSVUpload(t *testing.T) {
+ ctx := context.Background()
+ marker := strings.ToLower(uuid.New().String())
+ phoneNumber := randomPhoneNumber()
+ csv := fmt.Sprintf(
+ "Name,Emails,PhoneNumbers\nCSV %s,\"FIRST-%s@EXAMPLE.COM;second-%s@example.com\",%s\n",
+ marker,
+ marker,
+ marker,
+ strings.TrimPrefix(phoneNumber, "+"),
+ )
+
+ response := uploadIntegrationContactsCSV(ctx, t, csv)
+ require.Len(t, response.Data, 1)
+ contact := response.Data[0]
+ t.Cleanup(func() {
+ cleanupIntegrationContact(t, contact.ID)
+ })
+
+ assert.Equal(t, "CSV "+marker, contact.Name)
+ assert.Equal(t, []string{
+ "first-" + marker + "@example.com",
+ "second-" + marker + "@example.com",
+ }, contact.Emails)
+ assert.Equal(t, []string{phoneNumber}, contact.PhoneNumbers)
+
+ listed := listIntegrationContacts(ctx, t, marker, 0, 20)
+ assert.EqualValues(t, 1, listed.Total)
+ require.Len(t, listed.Data, 1)
+ assert.Equal(t, contact.ID, listed.Data[0].ID)
+}
+
+func TestContacts_MessageThreadsIncludeContactDetails(t *testing.T) {
+ ctx := context.Background()
+ phone := setupPhone(ctx, t, 60)
+ contactPhone := randomPhoneNumber()
+ marker := strings.ToLower(uuid.New().String())
+
+ created := createIntegrationContacts(ctx, t, []map[string]any{
+ {
+ "name": "Thread Contact " + marker,
+ "emails": []string{"thread-" + marker + "@example.com"},
+ "phone_numbers": []string{contactPhone},
+ "properties": map[string]string{"source": "integration-test"},
+ },
+ })
+ contact := created[0]
+ t.Cleanup(func() {
+ cleanupIntegrationContact(t, contact.ID)
+ })
+
+ messageID := receiveInbound(
+ ctx,
+ t,
+ phone.PhoneAPIKey,
+ contactPhone,
+ phone.PhoneNumber,
+ "Contact enrichment "+marker,
+ time.Now(),
+ )
+ pollMessageStatus(ctx, t, messageID, "received", 15*time.Second)
+
+ thread := waitForThreadWithContact(ctx, t, phone.PhoneNumber, contactPhone, 20*time.Second)
+ require.NotNil(t, thread.ContactDetails)
+ assert.Equal(t, contact.ID, thread.ContactDetails.ID)
+ assert.Equal(t, contact.Name, thread.ContactDetails.Name)
+ assert.Equal(t, contact.PhoneNumbers, thread.ContactDetails.PhoneNumbers)
+ assert.Equal(t, contact.Properties, thread.ContactDetails.Properties)
+}
diff --git a/tests/helpers_test.go b/tests/helpers_test.go
index 49a7a6545..dfbf28856 100644
--- a/tests/helpers_test.go
+++ b/tests/helpers_test.go
@@ -110,6 +110,8 @@ func setupPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) testP
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode, "fcm token bind failed")
+ waitForPhoneAuthorization(ctx, t, phoneAPIKeyValue, phoneNumber, 20*time.Second)
+
return testPhone{
PhoneNumber: phoneNumber,
PhoneAPIKey: phoneAPIKeyValue,
@@ -117,6 +119,55 @@ func setupPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) testP
}
}
+func waitForPhoneAuthorization(
+ ctx context.Context,
+ t *testing.T,
+ phoneAPIKey string,
+ phoneNumber string,
+ timeout time.Duration,
+) {
+ t.Helper()
+
+ body, err := json.Marshal(map[string]interface{}{
+ "phone_numbers": []string{phoneNumber},
+ "charging": true,
+ })
+ require.NoError(t, err)
+
+ var responseBody []byte
+ var statusCode int
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ request, requestErr := http.NewRequestWithContext(
+ ctx,
+ http.MethodPost,
+ apiBaseURL+"/v1/heartbeats",
+ bytes.NewReader(body),
+ )
+ require.NoError(t, requestErr)
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("x-api-key", phoneAPIKey)
+
+ response, requestErr := http.DefaultClient.Do(request)
+ require.NoError(t, requestErr)
+ responseBody, requestErr = io.ReadAll(response.Body)
+ response.Body.Close()
+ require.NoError(t, requestErr)
+
+ statusCode = response.StatusCode
+ if statusCode == http.StatusCreated {
+ return
+ }
+ if statusCode != http.StatusUnauthorized {
+ require.Equal(t, http.StatusCreated, statusCode, "phone authorization check failed: %s", string(responseBody))
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ require.Equal(t, http.StatusCreated, statusCode, "phone authorization was not ready within %v: %s", timeout, string(responseBody))
+}
+
func setupWebhook(ctx context.Context, t *testing.T, phoneNumber string, events []string) (signingKey string, webhookPath string) {
t.Helper()
diff --git a/tests/integration_test.go b/tests/integration_test.go
index 66abecdd5..7cc16809a 100644
--- a/tests/integration_test.go
+++ b/tests/integration_test.go
@@ -56,7 +56,7 @@ func TestSendSMS_Encrypted(t *testing.T) {
assert.NotEqual(t, plaintext, outstanding["content"])
fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "SENT")
- time.Sleep(200 * time.Millisecond)
+ pollMessageStatus(ctx, t, messageID, "sent", 15*time.Second)
fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "DELIVERED")
msg := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second)
@@ -200,8 +200,10 @@ func TestSendSMS_RateLimit(t *testing.T) {
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")
+ pollMessageStatus(ctx, t, msgID1, "sent", 15*time.Second)
fireEvent(ctx, t, phone.PhoneAPIKey, msgID2, "SENT")
+ pollMessageStatus(ctx, t, msgID2, "sent", 15*time.Second)
+ fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "DELIVERED")
fireEvent(ctx, t, phone.PhoneAPIKey, msgID2, "DELIVERED")
msg1 := pollMessageStatus(ctx, t, msgID1, "delivered", 15*time.Second)
@@ -335,7 +337,7 @@ func TestSendSMS_OutstandingFlow(t *testing.T) {
assert.Equal(t, contactNumber, outstanding["contact"])
fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "SENT")
- time.Sleep(200 * time.Millisecond)
+ pollMessageStatus(ctx, t, messageID, "sent", 15*time.Second)
fireEvent(ctx, t, phone.PhoneAPIKey, messageID, "DELIVERED")
msg := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second)
@@ -538,7 +540,7 @@ func TestBulkSMS_Excel(t *testing.T) {
// Fire SENT then DELIVERED on message 1, leave message 2 pending
fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "SENT")
- time.Sleep(200 * time.Millisecond)
+ pollMessageStatus(ctx, t, msgID1, "sent", 15*time.Second)
fireEvent(ctx, t, phone.PhoneAPIKey, msgID1, "DELIVERED")
// Poll until message 1 reaches "delivered"
diff --git a/web/app/components/ContactDialog.vue b/web/app/components/ContactDialog.vue
new file mode 100644
index 000000000..2df329a93
--- /dev/null
+++ b/web/app/components/ContactDialog.vue
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+ {{ dialogTitle }}
+
+
+
+
+
+
+
+
+
+
+
+ Phone Numbers
+
+
+ Add
+
+
+
+
+
+ Email Addresses
+
+
+ Add
+
+
+
+
+
+
+
+
+ Properties
+
+
+ Add
+
+
+
+
+
+
+
+
+
+
+ Save Contact
+
+
+ Close
+
+
+
+
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index 2a9834317..d62eeb098 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -7,12 +7,13 @@ import {
mdiAlert,
mdiAccount,
} from '@mdi/js'
-import { formatPhoneNumber, startsWithLetter } from '~/utils/filters'
+import type { EntitiesMessageThread } from '~~/shared/types/api'
const threadsStore = useThreadsStore()
const phonesStore = usePhonesStore()
const appStore = useAppStore()
const notificationsStore = useNotificationsStore()
+const { formatPhoneNumber, startsWithLetter } = useFilters()
function threadDate(date: string): string {
return new Date(date).toLocaleString(undefined, {
@@ -27,6 +28,21 @@ function onInstallApp() {
message: 'Downloading the httpSMS Android App',
})
}
+
+function threadContactName(thread: EntitiesMessageThread): string {
+ return thread.contact_details?.name?.trim() ?? ''
+}
+
+function threadContactTitle(thread: EntitiesMessageThread): string {
+ return threadContactName(thread) || formatPhoneNumber(thread.contact)
+}
+
+function threadAvatarInitial(thread: EntitiesMessageThread): string {
+ const contactName = threadContactName(thread)
+ if (contactName) return contactName.substring(0, 1)
+
+ return startsWithLetter(thread.contact) ? thread.contact.substring(0, 1) : ''
+}
@@ -104,16 +120,16 @@ function onInstallApp() {
size="40"
:badge="thread.is_read ? false : { color: 'primary', dotSize: 12 }"
>
- {{
+ {{
mdiAccount
}}
{{
- thread.contact.substring(0, 1)
+ threadAvatarInitial(thread)
}}
{{
- formatPhoneNumber(thread.contact)
+ threadContactTitle(thread)
}}
Search Messages
+
+
+ Contacts
+
Settings
diff --git a/web/app/composables/useFilters.ts b/web/app/composables/useFilters.ts
index c84157ea5..2b54bbb43 100644
--- a/web/app/composables/useFilters.ts
+++ b/web/app/composables/useFilters.ts
@@ -7,6 +7,8 @@ import {
formatBillingPeriod,
formatBillingPeriodDateOrdinal,
humanizeTime,
+ humanizeTimeShort,
+ startsWithLetter,
} from '../utils/filters'
import { capitalize } from '../utils/capitalize'
@@ -18,8 +20,10 @@ export function useFilters() {
formatMoney,
formatDecimal,
formatBillingPeriod,
+ humanizeTimeShort,
formatBillingPeriodDateOrdinal,
humanizeTime,
+ startsWithLetter,
capitalize,
}
}
diff --git a/web/app/pages/billing/index.vue b/web/app/pages/billing/index.vue
index 555bd129c..c5af811e3 100644
--- a/web/app/pages/billing/index.vue
+++ b/web/app/pages/billing/index.vue
@@ -27,7 +27,7 @@ useHead({
})
const config = useRuntimeConfig()
-const { lgAndUp } = useDisplay()
+const { lgAndUp } = useVDisplay()
const authStore = useAuthStore()
const billingStore = useBillingStore()
const notificationsStore = useNotificationsStore()
diff --git a/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue b/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue
index 5e7e0a6f9..bda47bca9 100644
--- a/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue
+++ b/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue
@@ -1,10 +1,8 @@
+
+
+
+
+
+
+
+ Contacts
+
+
+
+
+
+
+
+
Contacts
+
+
+
+ Import CSV
+
+
+ Add Contact
+
+
+
+
+ Use httpSMS as a lightweight CRM by adding your contacts here. Your
+ message threads will show contact names instead of phone numbers,
+ making conversations easier to recognize and manage. Add contacts
+ individually, or fill in our
+ CSV template
+ and upload it to import your contact list in bulk.
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+ {{ formatPhoneNumber(phone) }}
+
+
+ —
+
+
+
+
+
+
+ {{ email }}
+
+
+ —
+
+
+
+
+
+
+ {{ humanizeTimeShort(item.created_at) }}
+
+
+
+
+
+
+
+
+
+ {{ humanizeTimeShort(item.updated_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ searchTerm
+ ? 'No contacts match your search'
+ : 'No contacts yet'
+ }}
+
+
+ {{
+ searchTerm
+ ? 'Try a different name, phone number or email.'
+ : 'Add your first contact or import them from a CSV file.'
+ }}
+
+
+ Add Contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete Contact
+
+
+
+
+ Are you sure you want to delete
+ {{ pendingDelete?.name }}? This action cannot be undone.
+
+
+
+ Delete Contact
+
+
+
+ Close
+
+
+
+
+
+
+
+
+
+ Import Contacts from CSV
+
+
+
+
+
+ Download the
+ CSV template, fill it in and upload it here. Separate multiple emails or phone
+ numbers within a cell using a semicolon (;).
+
+
+
+ We couldn't import your file:
+
+
+
+
+
+ Import
+
+
+
+ Close
+
+
+
+
+
+
diff --git a/web/app/pages/heartbeats/[id].vue b/web/app/pages/heartbeats/[id].vue
index 8a05d6944..3ef5d212c 100644
--- a/web/app/pages/heartbeats/[id].vue
+++ b/web/app/pages/heartbeats/[id].vue
@@ -17,7 +17,7 @@ useHead({
})
const route = useRoute()
-const { mdAndDown, mdAndUp, lgAndUp } = useDisplay()
+const { mdAndDown, mdAndUp, lgAndUp } = useVDisplay()
const authStore = useAuthStore()
const phonesStore = usePhonesStore()
const { formatPhoneNumber, formatTimestamp } = useFilters()
diff --git a/web/app/pages/index.vue b/web/app/pages/index.vue
index 281745691..be30493f7 100644
--- a/web/app/pages/index.vue
+++ b/web/app/pages/index.vue
@@ -43,7 +43,7 @@ useSeoMeta({
})
const config = useRuntimeConfig()
-const { lgAndUp, mdAndUp, mdAndDown, md, smAndDown, xl } = useDisplay()
+const { lgAndUp, mdAndUp, mdAndDown, md, smAndDown, xl } = useVDisplay()
const selectedTab = ref('javascript')
const yearlyPricing = ref(false)
diff --git a/web/app/pages/messages/index.vue b/web/app/pages/messages/index.vue
index 0a6d53a99..3dd1a544a 100644
--- a/web/app/pages/messages/index.vue
+++ b/web/app/pages/messages/index.vue
@@ -17,7 +17,7 @@ useHead({
})
const router = useRouter()
-const { mdAndDown, mdAndUp } = useDisplay()
+const { mdAndDown, mdAndUp } = useVDisplay()
const notificationsStore = useNotificationsStore()
const phonesStore = usePhonesStore()
const { useApi } = useApiComposable()
diff --git a/web/app/pages/phone-api-keys/index.vue b/web/app/pages/phone-api-keys/index.vue
index 27804cbde..df13c5a67 100644
--- a/web/app/pages/phone-api-keys/index.vue
+++ b/web/app/pages/phone-api-keys/index.vue
@@ -16,7 +16,7 @@ useHead({
})
const config = useRuntimeConfig()
-const { lgAndUp } = useDisplay()
+const { lgAndUp } = useVDisplay()
const authStore = useAuthStore()
const appStore = useAppStore()
const phonesStore = usePhonesStore()
diff --git a/web/app/pages/search-messages/index.vue b/web/app/pages/search-messages/index.vue
index 925800d75..0e5419f71 100644
--- a/web/app/pages/search-messages/index.vue
+++ b/web/app/pages/search-messages/index.vue
@@ -42,7 +42,7 @@ useHead({
const route = useRoute()
const config = useRuntimeConfig()
-const { mdAndUp, smAndDown, lgAndUp } = useDisplay()
+const { mdAndUp, smAndDown, lgAndUp } = useVDisplay()
const messagesStore = useMessagesStore()
const phonesStore = usePhonesStore()
const authStore = useAuthStore()
diff --git a/web/app/pages/settings/index.vue b/web/app/pages/settings/index.vue
index 1f381416b..63a8c2975 100644
--- a/web/app/pages/settings/index.vue
+++ b/web/app/pages/settings/index.vue
@@ -42,7 +42,7 @@ useHead({
const config = useRuntimeConfig()
const route = useRoute()
const router = useRouter()
-const { mdAndDown, mdAndUp, lgAndUp, xlAndUp, smAndUp } = useDisplay()
+const { mdAndDown, mdAndUp, lgAndUp, xlAndUp, smAndUp } = useVDisplay()
const authStore = useAuthStore()
const phonesStore = usePhonesStore()
const billingStore = useBillingStore()
diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue
index 73632a64c..f933973dd 100644
--- a/web/app/pages/threads/[id]/index.vue
+++ b/web/app/pages/threads/[id]/index.vue
@@ -14,12 +14,14 @@ import {
mdiAccount,
mdiRefresh,
mdiContentCopy,
+ mdiAccountPlus,
+ mdiSquareEditOutline,
} from '@mdi/js'
import Pusher from 'pusher-js'
import type { Channel } from 'pusher-js'
import { isValidPhoneNumber } from 'libphonenumber-js'
+import { storeToRefs } from 'pinia'
import type { EntitiesMessage } from '~~/shared/types/api'
-import { startsWithLetter } from '~/utils/filters'
definePageMeta({
middleware: ['auth'],
@@ -32,13 +34,14 @@ useHead({
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
-const { lgAndUp, mdAndDown, mdAndUp } = useDisplay()
-const { formatPhoneNumber } = useFilters()
+const { lgAndUp, mdAndDown, mdAndUp } = useVDisplay()
+const { formatPhoneNumber, startsWithLetter } = useFilters()
const notificationsStore = useNotificationsStore()
const authStore = useAuthStore()
const phonesStore = usePhonesStore()
const threadsStore = useThreadsStore()
const messagesStore = useMessagesStore()
+const { currentThread } = storeToRefs(threadsStore)
const formMessage = ref('')
const submitting = ref(false)
@@ -46,6 +49,7 @@ const loadingMessages = ref(false)
const hideMessages = ref(true)
const messages = ref([])
const selectedMenuItem = ref(-1)
+const contactDialog = ref(false)
const messageBody = ref(null)
const form = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null)
@@ -59,7 +63,7 @@ const formMessageRules = [
let webhookChannel: Channel | null = null
const contactIsPhoneNumber = computed(() => {
- const thread = threadsStore.currentThread
+ const thread = currentThread.value
if (!thread) return false
return isValidPhoneNumber(thread.contact) || !isNaN(Number(thread.contact))
})
@@ -67,7 +71,7 @@ const contactIsPhoneNumber = computed(() => {
const messageVisibility = computed(() =>
hideMessages.value ? 'hidden' : 'visible',
)
-const contact = computed(() => threadsStore.currentThread?.contact ?? '')
+const contact = computed(() => currentThread.value?.contact ?? '')
function isMT(message: EntitiesMessage): boolean {
return message.type === 'mobile-terminated'
@@ -104,6 +108,18 @@ function formatAttachmentName(url: string): string {
return url
}
+function currentThreadContactTitle(): string {
+ const thread = currentThread.value
+ if (!thread) return ''
+ return (
+ thread.contact_details?.name?.trim() || formatPhoneNumber(thread.contact)
+ )
+}
+
+async function refreshThreadContact() {
+ await threadsStore.loadThreads()
+}
+
function scrollToElement() {
const el = messageBody.value
if (el) {
@@ -155,7 +171,7 @@ async function loadData() {
async function archiveThread() {
await threadsStore.updateThread({
- threadId: threadsStore.currentThread!.id,
+ threadId: currentThread.value!.id,
isArchived: true,
})
await router.push('/threads')
@@ -163,7 +179,7 @@ async function archiveThread() {
async function unArchiveThread() {
await threadsStore.updateThread({
- threadId: threadsStore.currentThread!.id,
+ threadId: currentThread.value!.id,
isArchived: false,
})
await router.push('/threads')
@@ -218,7 +234,7 @@ async function sendMessage(event: KeyboardEvent | Event) {
submitting.value = true
await messagesStore.sendMessage({
from: phonesStore.owner!,
- to: threadsStore.currentThread!.contact,
+ to: currentThread.value!.contact,
content: formMessage.value,
sim: 'DEFAULT',
})
@@ -267,8 +283,17 @@ onBeforeUnmount(() => {
-
- {{ formatPhoneNumber(threadsStore.currentThread.contact) }}
+
+
+
+ {{ currentThreadContactTitle() }}
+
+
+ {{ currentThreadContactTitle() }}
@@ -282,10 +307,25 @@ onBeforeUnmount(() => {
:density="mdAndDown ? 'compact' : 'default'"
>
+
+
+
+ Edit Contact
+
+
+
+
+
+ Add Contact
+
+
@@ -294,10 +334,7 @@ onBeforeUnmount(() => {
Archive
@@ -306,8 +343,8 @@ onBeforeUnmount(() => {
Unarchive
@@ -323,7 +360,7 @@ onBeforeUnmount(() => {
color="primary"
indeterminate
/>
-
+
{
'ml-2': !mdAndUp,
'ml-4': mdAndUp,
}"
- :color="threadsStore.currentThread!.color"
+ :color="currentThread.color"
size="40"
>
{
+
diff --git a/web/app/pages/threads/index.vue b/web/app/pages/threads/index.vue
index 03bd1ad50..c1698825d 100644
--- a/web/app/pages/threads/index.vue
+++ b/web/app/pages/threads/index.vue
@@ -7,7 +7,7 @@ useHead({
title: 'Threads - httpSMS',
})
-const { lgAndUp } = useDisplay()
+const { lgAndUp } = useVDisplay()
const authStore = useAuthStore()
const phonesStore = usePhonesStore()
const threadsStore = useThreadsStore()
diff --git a/web/app/stores/contacts.ts b/web/app/stores/contacts.ts
new file mode 100644
index 000000000..afa798f72
--- /dev/null
+++ b/web/app/stores/contacts.ts
@@ -0,0 +1,263 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import type { EntitiesContact } from '~~/shared/types/api'
+import { getApiErrorMessage } from '~/utils/api-error'
+
+export interface ContactInput {
+ name: string
+ emails: string[]
+ phone_numbers: string[]
+ properties?: Record
+}
+
+export interface LoadContactsOptions {
+ force?: boolean
+ skip?: number
+ limit?: number
+ sortBy?: 'name' | 'updated_at'
+ sortDescending?: boolean
+}
+
+export interface ContactMutationOptions {
+ refresh?: boolean
+}
+
+// DEFAULT_LIMIT mirrors the contacts page's initial items-per-page. It is only
+// used when a caller (e.g. a mutation refresh) does not specify its own limit.
+const DEFAULT_LIMIT = 10
+
+export const useContactsStore = defineStore('contacts', () => {
+ const contacts = ref([])
+ const total = ref(0)
+ const loading = ref(false)
+ const search = ref('')
+ const { apiFetch } = useApi()
+ const notificationsStore = useNotificationsStore()
+ let loadContactsGeneration = 0
+
+ // The pagination window last requested by the page. Mutation-triggered
+ // refreshes reuse it so the user stays on the page they were viewing.
+ let currentSkip = 0
+ let currentLimit = DEFAULT_LIMIT
+ let currentSortBy: 'name' | 'updated_at' = 'updated_at'
+ let currentSortDescending = true
+
+ function normalizeOptions(
+ options: LoadContactsOptions | boolean,
+ ): LoadContactsOptions {
+ if (typeof options === 'boolean') {
+ return { force: options }
+ }
+ return options
+ }
+
+ async function loadContacts(
+ options: LoadContactsOptions | boolean = {},
+ ): Promise {
+ const {
+ force = false,
+ skip,
+ limit,
+ sortBy,
+ sortDescending,
+ } = normalizeOptions(options)
+
+ if (skip !== undefined) {
+ currentSkip = skip
+ }
+ if (limit !== undefined) {
+ currentLimit = limit
+ }
+ if (sortBy !== undefined) {
+ currentSortBy = sortBy
+ }
+ if (sortDescending !== undefined) {
+ currentSortDescending = sortDescending
+ }
+
+ if (contacts.value.length > 0 && !force) return
+
+ const generation = ++loadContactsGeneration
+ loading.value = true
+ try {
+ const term = search.value.trim()
+ const params: Record = {
+ skip: currentSkip,
+ limit: currentLimit,
+ sort_by: currentSortBy,
+ sort_descending: currentSortDescending,
+ }
+ if (term) {
+ params.query = term
+ }
+ const response = await apiFetch<{
+ data: EntitiesContact[]
+ total?: number
+ }>('/v1/contacts', { params })
+ if (generation === loadContactsGeneration) {
+ contacts.value = response.data ?? []
+ total.value = response.total ?? contacts.value.length
+ }
+ } catch (error: unknown) {
+ if (generation !== loadContactsGeneration) {
+ return
+ }
+ notificationsStore.addNotification({
+ message: getApiErrorMessage(error, 'Error while loading contacts'),
+ type: 'error',
+ })
+ throw error
+ } finally {
+ if (generation === loadContactsGeneration) {
+ loading.value = false
+ }
+ }
+ }
+
+ async function saveContacts(
+ items: ContactInput | ContactInput[],
+ options: ContactMutationOptions = {},
+ ): Promise {
+ const contactsToSave = Array.isArray(items) ? items : [items]
+ const { refresh = true } = options
+
+ loading.value = true
+ try {
+ try {
+ await apiFetch('/v1/contacts', {
+ method: 'POST',
+ body: contactsToSave,
+ })
+ } catch (error: unknown) {
+ notificationsStore.addNotification({
+ message: getApiErrorMessage(error, 'Error while saving contacts'),
+ type: 'error',
+ })
+ throw error
+ }
+
+ notificationsStore.addNotification({
+ message:
+ contactsToSave.length > 1
+ ? 'Contacts created successfully'
+ : 'Contact created successfully',
+ type: 'success',
+ })
+ if (refresh) {
+ await loadContacts(true)
+ }
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function updateContact(
+ id: string,
+ payload: ContactInput,
+ options: ContactMutationOptions = {},
+ ): Promise {
+ const { refresh = true } = options
+ loading.value = true
+ try {
+ try {
+ await apiFetch(`/v1/contacts/${id}`, {
+ method: 'PUT',
+ body: payload,
+ })
+ } catch (error: unknown) {
+ notificationsStore.addNotification({
+ message: getApiErrorMessage(error, 'Error while updating contact'),
+ type: 'error',
+ })
+ throw error
+ }
+
+ notificationsStore.addNotification({
+ message: 'Contact updated',
+ type: 'success',
+ })
+ if (refresh) {
+ await loadContacts(true)
+ }
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function deleteContact(id: string): Promise {
+ loading.value = true
+ try {
+ await apiFetch(`/v1/contacts/${id}`, { method: 'DELETE' })
+ // Invalidate any in-flight load so a stale response cannot resurrect the
+ // just-deleted contact. Bumping the generation makes loadContacts skip
+ // its assignment (and its finally toggling loading) for the older request.
+ loadContactsGeneration++
+ contacts.value = contacts.value.filter((contact) => contact.id !== id)
+ total.value = Math.max(0, total.value - 1)
+ notificationsStore.addNotification({
+ message: 'Contact deleted successfully',
+ type: 'success',
+ })
+ } catch (error: unknown) {
+ notificationsStore.addNotification({
+ message: getApiErrorMessage(error, 'Error while deleting contact'),
+ type: 'error',
+ })
+ throw error
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function uploadCsv(file: File): Promise {
+ loading.value = true
+ try {
+ try {
+ const formData = new FormData()
+ formData.append('document', file)
+
+ await apiFetch('/v1/contacts/upload', {
+ method: 'POST',
+ body: formData,
+ })
+ } catch (error: unknown) {
+ notificationsStore.addNotification({
+ message: getApiErrorMessage(error, 'Error while importing contacts'),
+ type: 'error',
+ })
+ throw error
+ }
+
+ notificationsStore.addNotification({
+ message: 'Contacts imported successfully',
+ type: 'success',
+ })
+ await loadContacts(true)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function resetState() {
+ loadContactsGeneration++
+ contacts.value = []
+ total.value = 0
+ loading.value = false
+ search.value = ''
+ currentSkip = 0
+ currentLimit = DEFAULT_LIMIT
+ }
+
+ return {
+ contacts,
+ total,
+ loading,
+ search,
+ loadContacts,
+ saveContacts,
+ updateContact,
+ deleteContact,
+ uploadCsv,
+ resetState,
+ }
+})
diff --git a/web/app/stores/threads.ts b/web/app/stores/threads.ts
index 7e3002624..fdc7094ea 100644
--- a/web/app/stores/threads.ts
+++ b/web/app/stores/threads.ts
@@ -28,7 +28,19 @@ export const useThreadsStore = defineStore('threads', () => {
const index = threads.value.findIndex(
(thread) => thread.id === updatedThread.id,
)
- if (index !== -1) threads.value[index] = updatedThread
+ if (index !== -1) {
+ const existingThread = threads.value[index]
+ threads.value[index] = {
+ ...updatedThread,
+ // Mark-read PUT responses are not contact-enriched; keep the resolved
+ // contact so display names do not disappear after the thread updates.
+ contact_details:
+ updatedThread.contact_details ??
+ (updatedThread.contact === existingThread.contact
+ ? existingThread.contact_details
+ : undefined),
+ }
+ }
}
async function loadThreads() {
@@ -45,6 +57,7 @@ export const useThreadsStore = defineStore('threads', () => {
owner: phonesStore.owner ?? phonesStore.phones[0]?.phone_number,
limit: 100,
is_archived: archivedThreads.value,
+ contacts: true,
},
},
)
diff --git a/web/app/utils/filters.ts b/web/app/utils/filters.ts
index 5ed17d175..2592293e4 100644
--- a/web/app/utils/filters.ts
+++ b/web/app/utils/filters.ts
@@ -1,4 +1,11 @@
-import { intervalToDuration, formatDuration } from 'date-fns'
+import {
+ intervalToDuration,
+ formatDuration,
+ differenceInSeconds,
+ differenceInMinutes,
+ differenceInDays,
+ differenceInHours,
+} from 'date-fns'
import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js'
export function formatPhoneNumber(value: string): string {
@@ -103,3 +110,16 @@ export function humanizeTime(value: string): string {
export function startsWithLetter(value: string): boolean {
return /^[a-zA-Z]/.test(value)
}
+
+export function humanizeTimeShort(date: string) {
+ const now = new Date()
+ const seconds = differenceInSeconds(now, new Date(date))
+ const minutes = differenceInMinutes(now, new Date(date))
+ const hours = differenceInHours(now, new Date(date))
+ const days = differenceInDays(now, new Date(date))
+
+ if (seconds < 60) return 'just now'
+ if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`
+ if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`
+ return `${days} day${days !== 1 ? 's' : ''} ago`
+}
diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts
index 1ee80e734..c574ce308 100644
--- a/web/nuxt.config.ts
+++ b/web/nuxt.config.ts
@@ -97,6 +97,9 @@ export default defineNuxtConfig({
},
vuetify: {
+ moduleOptions: {
+ prefixComposables: true,
+ },
vuetifyOptions: {
theme: {
defaultTheme: 'dark',
diff --git a/web/public/templates/httpsms-contacts.csv b/web/public/templates/httpsms-contacts.csv
new file mode 100644
index 000000000..8ba7c01d2
--- /dev/null
+++ b/web/public/templates/httpsms-contacts.csv
@@ -0,0 +1,4 @@
+Name,Emails,PhoneNumbers
+John Doe,john@example.com,+18005550199
+Jane Smith,jane@example.com;jane.work@example.com,+18005550100;+18005550111
+Acme Support,,+18005550122
diff --git a/web/shared/types/api.ts b/web/shared/types/api.ts
index 27e2db111..3f85c4fb7 100644
--- a/web/shared/types/api.ts
+++ b/web/shared/types/api.ts
@@ -71,6 +71,24 @@ export interface EntitiesBulkMessage {
total: number;
}
+export interface EntitiesContact {
+ /** @example "2022-06-05T14:26:02.302718+03:00" */
+ created_at: string;
+ /** @example ["alice@example.com"] */
+ emails: string[];
+ /** @example "32343a19-da5e-4b1b-a767-3298a73703cb" */
+ id: string;
+ /** @example "Alice Smith" */
+ name: string;
+ /** @example ["+18005550199","+18005550100"] */
+ phone_numbers: string[];
+ properties: Record;
+ /** @example "2022-06-05T14:26:02.302718+03:00" */
+ updated_at: string;
+ /** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */
+ user_id: string;
+}
+
export interface EntitiesDiscord {
/** @example "2022-06-05T14:26:02.302718+03:00" */
created_at: string;
@@ -199,6 +217,8 @@ export interface EntitiesMessageThread {
color: string;
/** @example "+18005550100" */
contact: string;
+ /** ContactDetails is resolved at read time and never persisted. */
+ contact_details?: EntitiesContact;
/** @example "2022-06-05T14:26:09.527976+03:00" */
created_at: string;
/** @example "32343a19-da5e-4b1b-a767-3298a73703ca" */
@@ -332,6 +352,26 @@ export interface EntitiesWebhook {
user_id: string;
}
+export interface RequestsContactItem {
+ emails?: string[];
+ /** @example "Alice Smith" */
+ name: string;
+ phone_numbers: string[];
+ properties?: Record;
+}
+
+export interface RequestsContactStoreRequest {
+ contacts: RequestsContactItem[];
+}
+
+export interface RequestsContactUpdateRequest {
+ emails?: string[];
+ /** @example "Alice Smith" */
+ name: string;
+ phone_numbers: string[];
+ properties?: Record;
+}
+
export interface RequestsDiscordStore {
incoming_channel_id: string;
name: string;
@@ -625,6 +665,36 @@ export interface ResponsesBulkMessagesResponse {
status: string;
}
+export interface ResponsesContactResponse {
+ data: EntitiesContact;
+ /** @example "Request handled successfully" */
+ message: string;
+ /** @example "success" */
+ status: string;
+}
+
+export interface ResponsesContactsCreatedResponse {
+ data: EntitiesContact[];
+ /** @example "Request handled successfully" */
+ message: string;
+ /** @example "success" */
+ status: string;
+}
+
+export interface ResponsesContactsResponse {
+ data: EntitiesContact[];
+ /** @example "Request handled successfully" */
+ message: string;
+ /** @example "success" */
+ status: string;
+ /**
+ * Total is the number of contacts matching the request filter for the
+ * user, independent of the pagination skip/limit applied to Data.
+ * @example 57
+ */
+ total: number;
+}
+
export interface ResponsesDiscordResponse {
data: EntitiesDiscord;
/** @example "Request handled successfully" */
From 1a85d912cd57d18f36b6f9ece8ff8d3b68f9b77e Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Wed, 26 Aug 2026 10:25:28 +0300
Subject: [PATCH 37/88] fix: add style and logging
---
api/pkg/entities/contact.go | 2 +-
api/pkg/handlers/heartbeat_handler.go | 2 +-
api/pkg/services/contact_service.go | 5 +-
api/pkg/services/heartbeat_service.go | 2 +-
web/app/components/MessageThread.vue | 67 ++++++++++++++-------------
5 files changed, 43 insertions(+), 35 deletions(-)
diff --git a/api/pkg/entities/contact.go b/api/pkg/entities/contact.go
index 71723bf1a..d2733218d 100644
--- a/api/pkg/entities/contact.go
+++ b/api/pkg/entities/contact.go
@@ -17,7 +17,7 @@ const EntityNameContact = "Contact"
type ContactProperties map[string]string
// Value implements driver.Valuer, serializing the map to JSON bytes.
-func (p ContactProperties) Value() (driver.Value, error) {
+func (p *ContactProperties) Value() (driver.Value, error) {
if p == nil {
return []byte("{}"), nil
}
diff --git a/api/pkg/handlers/heartbeat_handler.go b/api/pkg/handlers/heartbeat_handler.go
index 660bdeed4..20554b049 100644
--- a/api/pkg/handlers/heartbeat_handler.go
+++ b/api/pkg/handlers/heartbeat_handler.go
@@ -85,7 +85,7 @@ func (h *HeartbeatHandler) Index(c fiber.Ctx) error {
heartbeats, err := h.service.Index(ctx, h.userIDFomContext(c), request.Owner, request.ToIndexParams())
if err != nil {
- ctxLogger.Error(stacktrace.Propagatef(err, "cannot get messgaes with params [%+#v]", request))
+ ctxLogger.Error(stacktrace.Propagatef(err, "cannot get heartbeats with params [%+#v]", request))
return h.responseInternalServerError(c)
}
diff --git a/api/pkg/services/contact_service.go b/api/pkg/services/contact_service.go
index de6fa40f0..fd7df59ea 100644
--- a/api/pkg/services/contact_service.go
+++ b/api/pkg/services/contact_service.go
@@ -147,13 +147,15 @@ func (service *ContactService) Get(ctx context.Context, userID entities.UserID,
// Index lists contacts for a user with the provided search/pagination params.
func (service *ContactService) Index(ctx context.Context, userID entities.UserID, params repositories.IndexParams) (*[]entities.Contact, error) {
- ctx, span := service.tracer.Start(ctx)
+ ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
defer span.End()
contacts, err := service.repository.Index(ctx, userID, params)
if err != nil {
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot index contacts for user [%s]", userID))
}
+
+ ctxLogger.Info(fmt.Sprintf("fetched [%d] contacts with params [%+#v]", len(*contacts), params))
return contacts, nil
}
@@ -252,6 +254,7 @@ func (service *ContactService) GetContactMap(ctx context.Context, userID entitie
return result, nil
}
+ ctxLogger.Info(fmt.Sprintf("fetching [%d] missing contacts by phone numbers for user [%s]", len(missing), userID))
contacts, err := service.repository.FetchByPhoneNumbers(ctx, userID, missing)
if err != nil {
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch contacts by phone numbers for user [%s]", userID))
diff --git a/api/pkg/services/heartbeat_service.go b/api/pkg/services/heartbeat_service.go
index 7441d2aa9..d6c442347 100644
--- a/api/pkg/services/heartbeat_service.go
+++ b/api/pkg/services/heartbeat_service.go
@@ -78,7 +78,7 @@ func (service *HeartbeatService) Index(ctx context.Context, userID entities.User
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch heartbeats with parms [%+#v]", params))
}
- ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*heartbeats), params))
+ ctxLogger.Info(fmt.Sprintf("fetched [%d] entities.Heartbeat with prams [%+#v]", len(*heartbeats), params))
return heartbeats, nil
}
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index d62eeb098..51de93fc4 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -143,37 +143,42 @@ function threadAvatarInitial(thread: EntitiesMessageThread): string {
{{ threadDate(thread.order_timestamp) }}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {{ thread.status }}
+
From 583397f74f2c874670e0efe571ae068e19a816da Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Wed, 26 Aug 2026 10:40:39 +0300
Subject: [PATCH 38/88] fix(api): restore contact property serialization
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80fa4141-33c6-474f-939a-f1a637639f96
---
api/pkg/entities/contact.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/api/pkg/entities/contact.go b/api/pkg/entities/contact.go
index d2733218d..a6c38d7b8 100644
--- a/api/pkg/entities/contact.go
+++ b/api/pkg/entities/contact.go
@@ -16,8 +16,10 @@ const EntityNameContact = "Contact"
// ContactProperties is a free-form key/value map persisted as a jsonb column.
type ContactProperties map[string]string
+var _ driver.Valuer = ContactProperties{}
+
// Value implements driver.Valuer, serializing the map to JSON bytes.
-func (p *ContactProperties) Value() (driver.Value, error) {
+func (p ContactProperties) Value() (driver.Value, error) {
if p == nil {
return []byte("{}"), nil
}
From 85c2167385d890931e5765f499768580fe657a78 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Wed, 26 Aug 2026 10:51:47 +0300
Subject: [PATCH 39/88] fix(api): support standalone MongoDB contacts
Contact batch inserts used transactions, which standalone MongoDB rejects.
Insert the batch directly so non-replica deployments work.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 80fa4141-33c6-474f-939a-f1a637639f96
---
api/pkg/repositories/mongo_contact_repository.go | 11 +----------
1 file changed, 1 insertion(+), 10 deletions(-)
diff --git a/api/pkg/repositories/mongo_contact_repository.go b/api/pkg/repositories/mongo_contact_repository.go
index 6cca028ce..23c783577 100644
--- a/api/pkg/repositories/mongo_contact_repository.go
+++ b/api/pkg/repositories/mongo_contact_repository.go
@@ -50,16 +50,7 @@ func (repository *mongoContactRepository) Store(ctx context.Context, contacts []
documents[index] = contact
}
- session, err := repository.collection.Database().Client().StartSession()
- if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot start transaction to store [%d] contacts", len(contacts)))
- }
- defer session.EndSession(ctx)
-
- _, err = session.WithTransaction(ctx, func(transactionCtx context.Context) (any, error) {
- return repository.collection.InsertMany(transactionCtx, documents)
- })
- if err != nil {
+ if _, err := repository.collection.InsertMany(ctx, documents); err != nil {
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store [%d] contacts", len(contacts)))
}
From 21a3e509964d57806fd35266972424f6dcef959b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:13 +0000
Subject: [PATCH 40/88] fix(deps): bump
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric
Bumps [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) from 0.59.0 to 0.60.0.
- [Release notes](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/releases)
- [Commits](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/compare/v0.59.0...v0.60.0)
---
updated-dependencies:
- dependency-name: github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric
dependency-version: 0.60.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 4 ++--
api/go.sum | 12 ++++++------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..9a6be79ad 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -7,7 +7,7 @@ require (
cloud.google.com/go/compute/metadata v0.9.0
cloud.google.com/go/storage v1.64.0
firebase.google.com/go v3.13.0+incompatible
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.60.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0
github.com/NdoleStudio/go-otelroundtripper v0.0.15
github.com/NdoleStudio/lemonsqueezy-go v1.3.2
@@ -91,7 +91,7 @@ require (
github.com/ClickHouse/ch-go v0.72.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.46.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/PuerkitoBio/goquery v1.12.0 // indirect
diff --git a/api/go.sum b/api/go.sum
index bea96100b..112c97b6b 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -38,14 +38,14 @@ github.com/ClickHouse/clickhouse-go/v2 v2.46.0 h1:s3eRy+hYmu5uzotB6ZhDofgHu8kDgG
github.com/ClickHouse/clickhouse-go/v2 v2.46.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 h1:c/Ivw7FuawPLfrr+zB0LZKeCchO2cAHQpF2qZ6OV7rQ=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0/go.mod h1:Zba7lknY/d78oxbKqFTmCsaGwfpzeJ3ktrrLXtnTV6g=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.60.0 h1:HldzheTs05E3ybqSitI/wHaof6+XERRudgZLjYbs3eE=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.60.0/go.mod h1:evkqaSczW9g2BQm1veCtgNhJ4wCCsRrOsSgNIn9LHQk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0 h1:PaC2zW6tfeh9rQydOQq6Pff4xpWwUDMCMolQXiypq0o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0/go.mod h1:3zRs1e/YFBfj7yv/2HFUlH1EKaP0priee5IqmYUHMP4=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0 h1:xTXsqDOj5k9mK3VVWHYUryryJCIdYfXxdjKFwpzINUw=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0/go.mod h1:V9g30lTKzfUsEW+gpWssck6u9IhARajmipodImLLcwI=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 h1:18FRm6ZcN/x9+ZmhMr96hLcTtlLn2/gHPuDLVeg7XcY=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.60.0 h1:Fx8NtDCmKH4ML2hUkPz4Dq250903vRDojMjVCDKwQuc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.60.0/go.mod h1:V9g30lTKzfUsEW+gpWssck6u9IhARajmipodImLLcwI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0 h1:Oblia1QXBJlM/wOY9ARRUtsXdDYiMCzk3eCMikqoLbI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0/go.mod h1:SRAbhyZ4R4FagHMM9VtRgSY/lheRoht2fKelZXQUenk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
From d71ec671c020fb91bd21b41fb663afd8c3dfedb9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:18 +0000
Subject: [PATCH 41/88] fix(deps): bump go.mongodb.org/mongo-driver/v2 in /api
Bumps [go.mongodb.org/mongo-driver/v2](https://github.com/mongodb/mongo-go-driver) from 2.8.0 to 2.8.2.
- [Release notes](https://github.com/mongodb/mongo-go-driver/releases)
- [Commits](https://github.com/mongodb/mongo-go-driver/compare/v2.8.0...v2.8.2)
---
updated-dependencies:
- dependency-name: go.mongodb.org/mongo-driver/v2
dependency-version: 2.8.2
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 6e3d5bdb0..f7bc469d6 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -47,7 +47,7 @@ require (
github.com/thedevsaddam/govalidator v1.9.10
github.com/uptrace/uptrace-go v1.43.0
github.com/xuri/excelize/v2 v2.11.0
- go.mongodb.org/mongo-driver/v2 v2.8.0
+ go.mongodb.org/mongo-driver/v2 v2.8.2
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
diff --git a/api/go.sum b/api/go.sum
index bea96100b..c18ac2c14 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -381,8 +381,8 @@ 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.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
-go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
+go.mongodb.org/mongo-driver/v2 v2.8.2 h1:b6o2m7zL8g2URuO8urBedAylxojybKXNZTxgkOcl+2w=
+go.mongodb.org/mongo-driver/v2 v2.8.2/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.44.0 h1:cVL0yu3uyrXkAmxonxvzYysIo5EZa8jKh3740MzxBzI=
From e59e12a96e79a09ba2e346e2d2c224e3c4dda21f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:24 +0000
Subject: [PATCH 42/88] fix(deps): bump go.opentelemetry.io/otel/sdk/metric in
/api
Bumps [go.opentelemetry.io/otel/sdk/metric](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.46.0)
---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk/metric
dependency-version: 1.46.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 18 ++++++++----------
api/go.sum | 38 ++++++++++++++++++--------------------
2 files changed, 26 insertions(+), 30 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..6bb7911c8 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -42,20 +42,20 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.21.0
github.com/redis/go-redis/v9 v9.21.0
github.com/rs/zerolog v1.35.1
- github.com/stretchr/testify v1.11.1
+ github.com/stretchr/testify v1.12.1
github.com/swaggo/swag v1.16.6
github.com/thedevsaddam/govalidator v1.9.10
github.com/uptrace/uptrace-go v1.43.0
github.com/xuri/excelize/v2 v2.11.0
go.mongodb.org/mongo-driver/v2 v2.8.0
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708
- go.opentelemetry.io/otel v1.44.0
+ go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
- go.opentelemetry.io/otel/metric v1.44.0
- go.opentelemetry.io/otel/sdk v1.44.0
- go.opentelemetry.io/otel/sdk/metric v1.44.0
- go.opentelemetry.io/otel/trace v1.44.0
+ go.opentelemetry.io/otel/metric v1.46.0
+ go.opentelemetry.io/otel/sdk v1.46.0
+ go.opentelemetry.io/otel/sdk/metric v1.46.0
+ go.opentelemetry.io/otel/trace v1.46.0
golang.org/x/sync v0.22.0
google.golang.org/api v0.287.1
google.golang.org/protobuf v1.36.11
@@ -111,7 +111,7 @@ require (
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.4 // indirect
- github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.24.0 // indirect
github.com/go-openapi/jsonreference v0.21.6 // indirect
@@ -158,7 +158,6 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // 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.21.0 // indirect
github.com/richardlehane/mscfb v1.0.7 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
@@ -195,7 +194,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
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
+ go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
@@ -209,7 +208,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.82.1 // 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 bea96100b..9f5290135 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -133,8 +133,8 @@ github.com/go-hermes/hermes/v2 v2.6.2/go.mod h1:RLVNk31/1KqF35vK3mAaQVuJvMH+K5//
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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
+github.com/go-logr/logr v1.4.4/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.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M=
@@ -328,8 +328,8 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
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.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=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
@@ -399,8 +399,8 @@ go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMAL
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac=
go.opentelemetry.io/contrib/processors/minsev v0.16.1 h1:DYL02u57VGQjrG8c09i6Bx5R74h4XxLj75wEk5h8/DA=
go.opentelemetry.io/contrib/processors/minsev v0.16.1/go.mod h1:VnF+kZnkagrTfTb2mV+6PTMAtPulgjpcUajm8sRk3tQ=
-go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
-go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
+go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
@@ -415,20 +415,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
-go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
-go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
-go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
-go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
-go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
-go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
+go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
+go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
+go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
+go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
+go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
+go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
-go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
-go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
-go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
-go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
+go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
+go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
+go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
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=
@@ -439,8 +439,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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=
@@ -507,8 +507,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
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-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=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From 9300a6b499c7068e78a1f646ea5e90cb19e3899e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:31 +0000
Subject: [PATCH 43/88] fix(deps): bump
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace
Bumps [github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go) from 1.35.0 to 1.36.0.
- [Release notes](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/releases)
- [Commits](https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/compare/detectors/gcp/v1.35.0...detectors/gcp/v1.36.0)
---
updated-dependencies:
- dependency-name: github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace
dependency-version: 1.36.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 4 ++--
api/go.sum | 12 ++++++------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..315785e33 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -8,7 +8,7 @@ require (
cloud.google.com/go/storage v1.64.0
firebase.google.com/go v3.13.0+incompatible
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.36.0
github.com/NdoleStudio/go-otelroundtripper v0.0.15
github.com/NdoleStudio/lemonsqueezy-go v1.3.2
github.com/NdoleStudio/plunk-go v0.0.2
@@ -91,7 +91,7 @@ require (
github.com/ClickHouse/ch-go v0.72.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.46.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/PuerkitoBio/goquery v1.12.0 // indirect
diff --git a/api/go.sum b/api/go.sum
index bea96100b..37956dec2 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -40,12 +40,12 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0 h1:c/Ivw7FuawPLfrr+zB0LZKeCchO2cAHQpF2qZ6OV7rQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.59.0/go.mod h1:Zba7lknY/d78oxbKqFTmCsaGwfpzeJ3ktrrLXtnTV6g=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0 h1:PaC2zW6tfeh9rQydOQq6Pff4xpWwUDMCMolQXiypq0o=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.35.0/go.mod h1:3zRs1e/YFBfj7yv/2HFUlH1EKaP0priee5IqmYUHMP4=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0 h1:xTXsqDOj5k9mK3VVWHYUryryJCIdYfXxdjKFwpzINUw=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.59.0/go.mod h1:V9g30lTKzfUsEW+gpWssck6u9IhARajmipodImLLcwI=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0 h1:18FRm6ZcN/x9+ZmhMr96hLcTtlLn2/gHPuDLVeg7XcY=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.59.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.36.0 h1:gBlHWOs3hqcN7cqRbyrvI6q1QwSZHyp76FajMUzgdpQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.36.0/go.mod h1:LmYRYS/wzrxoHi0OKmwhCujGpXFmab6c9xCyP58Hh9U=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.60.0 h1:Fx8NtDCmKH4ML2hUkPz4Dq250903vRDojMjVCDKwQuc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.60.0/go.mod h1:V9g30lTKzfUsEW+gpWssck6u9IhARajmipodImLLcwI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0 h1:Oblia1QXBJlM/wOY9ARRUtsXdDYiMCzk3eCMikqoLbI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.60.0/go.mod h1:SRAbhyZ4R4FagHMM9VtRgSY/lheRoht2fKelZXQUenk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
From aa45256739f4b464c8b1b89b33662d26dc5c2c51 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:36 +0000
Subject: [PATCH 44/88] fix(deps): bump github.com/gofiber/fiber/v3 from 3.4.0
to 3.5.0 in /api
Bumps [github.com/gofiber/fiber/v3](https://github.com/gofiber/fiber) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v3.4.0...v3.5.0)
---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v3
dependency-version: 3.5.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 12 ++++++------
api/go.sum | 24 ++++++++++++------------
2 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..bec355a49 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -25,7 +25,7 @@ require (
github.com/go-hermes/hermes/v2 v2.6.2
github.com/gofiber/contrib/v3/otel v1.2.2
github.com/gofiber/contrib/v3/swaggo v1.0.8
- github.com/gofiber/fiber/v3 v3.4.0
+ github.com/gofiber/fiber/v3 v3.5.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8
@@ -125,8 +125,8 @@ require (
github.com/go-openapi/swag/yamlutils v0.27.0 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
- github.com/gofiber/schema v1.8.2 // indirect
- github.com/gofiber/utils/v2 v2.2.0 // indirect
+ github.com/gofiber/schema v1.8.3 // indirect
+ github.com/gofiber/utils/v2 v2.4.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
@@ -144,9 +144,9 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.19.0 // indirect
+ github.com/klauspost/compress v1.19.2 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
- github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -170,7 +170,7 @@ require (
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasthttp v1.72.0 // indirect
+ github.com/valyala/fasthttp v1.73.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
github.com/vanng822/go-premailer v1.34.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
diff --git a/api/go.sum b/api/go.sum
index bea96100b..b1d173d2c 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -172,12 +172,12 @@ github.com/gofiber/contrib/v3/otel v1.2.2 h1:DQ0S5dIDb5UsnUT2HI5rXAwzy/pIiOMZkO6
github.com/gofiber/contrib/v3/otel v1.2.2/go.mod h1:icVCf1XJoqCuY7Fq/EAbaTk30ngA1Fc9r4zq/P2FBvk=
github.com/gofiber/contrib/v3/swaggo v1.0.8 h1:11YmMVbkMO7zv6YR0gdNfRWcs5ZVUkDpR21AhjhbK00=
github.com/gofiber/contrib/v3/swaggo v1.0.8/go.mod h1:7jyBZxqQ4MjOxIidOvLQ9jNAxNk05yKpid40OYb3pLE=
-github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw=
-github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk=
-github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU=
-github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g=
-github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE=
-github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE=
+github.com/gofiber/fiber/v3 v3.5.0 h1:dk7TOUH6DXJGtOLsN2XEG+0ZML7cznzHILTVozbNEK8=
+github.com/gofiber/fiber/v3 v3.5.0/go.mod h1:GOVDTW+gjJvfe0iJyVujbQ1Lnx+JUjFySJRI/9/xX/w=
+github.com/gofiber/schema v1.8.3 h1:06ZedxIYjngzc0095PYy7uWnFnbRflWFpikvZH61fDc=
+github.com/gofiber/schema v1.8.3/go.mod h1:jWnnZdhcW1mHyV+VnfRxKJDPNcepJsTZ9RIWxrr32Ng=
+github.com/gofiber/utils/v2 v2.4.1 h1:E2X9G8O5Mn7b2GDb0JU3IUk42Rw2npuhhepIbuJQ2po=
+github.com/gofiber/utils/v2 v2.4.1/go.mod h1:I+RTsgMUdzFuifVc3LOEkfh32wQW9BfRl7l5RYjamW4=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -243,8 +243,8 @@ 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/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
-github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
+github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -255,8 +255,8 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
-github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
@@ -352,8 +352,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.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M=
-github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk=
+github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
+github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
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.34.0 h1:CW7RUnjCfXrkuCbgC2wi/Cub7IwKslJWD/OkIBlcQUk=
From d3529121cc9f70536fe917a8bed86fa5e95de367 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:41 +0000
Subject: [PATCH 45/88] fix(deps): bump go.opentelemetry.io/otel/trace in /api
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.46.0)
---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
dependency-version: 1.46.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 14 ++++++--------
api/go.sum | 26 ++++++++++++--------------
2 files changed, 18 insertions(+), 22 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..2eb08a9ad 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -42,20 +42,20 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.21.0
github.com/redis/go-redis/v9 v9.21.0
github.com/rs/zerolog v1.35.1
- github.com/stretchr/testify v1.11.1
+ github.com/stretchr/testify v1.12.1
github.com/swaggo/swag v1.16.6
github.com/thedevsaddam/govalidator v1.9.10
github.com/uptrace/uptrace-go v1.43.0
github.com/xuri/excelize/v2 v2.11.0
go.mongodb.org/mongo-driver/v2 v2.8.0
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708
- go.opentelemetry.io/otel v1.44.0
+ go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
- go.opentelemetry.io/otel/metric v1.44.0
+ go.opentelemetry.io/otel/metric v1.46.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
- go.opentelemetry.io/otel/trace v1.44.0
+ go.opentelemetry.io/otel/trace v1.46.0
golang.org/x/sync v0.22.0
google.golang.org/api v0.287.1
google.golang.org/protobuf v1.36.11
@@ -111,7 +111,7 @@ require (
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.4 // indirect
- github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.24.0 // indirect
github.com/go-openapi/jsonreference v0.21.6 // indirect
@@ -158,7 +158,6 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // 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.21.0 // indirect
github.com/richardlehane/mscfb v1.0.7 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
@@ -195,7 +194,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
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
+ go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
@@ -209,7 +208,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.82.1 // 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 bea96100b..44410d456 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -133,8 +133,8 @@ github.com/go-hermes/hermes/v2 v2.6.2/go.mod h1:RLVNk31/1KqF35vK3mAaQVuJvMH+K5//
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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
+github.com/go-logr/logr v1.4.4/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.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M=
@@ -328,8 +328,8 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
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.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=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
@@ -399,8 +399,8 @@ go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMAL
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac=
go.opentelemetry.io/contrib/processors/minsev v0.16.1 h1:DYL02u57VGQjrG8c09i6Bx5R74h4XxLj75wEk5h8/DA=
go.opentelemetry.io/contrib/processors/minsev v0.16.1/go.mod h1:VnF+kZnkagrTfTb2mV+6PTMAtPulgjpcUajm8sRk3tQ=
-go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
-go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
+go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
@@ -415,8 +415,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
-go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
-go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
+go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
+go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
@@ -427,8 +427,8 @@ go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkH
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
-go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
-go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
+go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
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=
@@ -439,8 +439,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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=
@@ -507,8 +507,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
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-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=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From 689b561ff5acf65ff63a37cd5e028ccfe819454c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:50 +0000
Subject: [PATCH 46/88] fix(deps): bump google.golang.org/api from 0.287.1 to
0.295.0 in /api
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.287.1 to 0.295.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.287.1...v0.295.0)
---
updated-dependencies:
- dependency-name: google.golang.org/api
dependency-version: 0.295.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 34 +++++++++++++-------------
api/go.sum | 72 +++++++++++++++++++++++++++---------------------------
2 files changed, 53 insertions(+), 53 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..db6a3e3ee 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -57,8 +57,8 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/sync v0.22.0
- google.golang.org/api v0.287.1
- google.golang.org/protobuf v1.36.11
+ google.golang.org/api v0.295.0
+ google.golang.org/protobuf v1.36.12
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.2
gorm.io/plugin/opentelemetry v0.1.16
@@ -79,12 +79,12 @@ require (
require (
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
- cloud.google.com/go/auth v0.20.0 // indirect
+ cloud.google.com/go/auth v0.23.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
- cloud.google.com/go/firestore v1.22.0 // indirect
- cloud.google.com/go/iam v1.11.0 // indirect
+ cloud.google.com/go/firestore v1.24.0 // indirect
+ cloud.google.com/go/iam v1.12.0 // indirect
cloud.google.com/go/longrunning v1.2.0 // indirect
- cloud.google.com/go/monitoring v1.29.0 // indirect
+ cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/trace v1.16.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
@@ -130,8 +130,8 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect
- github.com/googleapis/gax-go/v2 v2.23.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
+ github.com/googleapis/gax-go/v2 v2.24.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -196,19 +196,19 @@ 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.54.0 // indirect
- golang.org/x/mod v0.37.0 // indirect
- golang.org/x/net v0.57.0 // indirect
+ golang.org/x/crypto v0.55.0 // indirect
+ golang.org/x/mod v0.38.0 // indirect
+ golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
- golang.org/x/text v0.40.0 // indirect
+ golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
- golang.org/x/tools v0.47.0 // indirect
+ golang.org/x/tools v0.48.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
- google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
- google.golang.org/grpc v1.82.1 // indirect
+ google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
+ google.golang.org/grpc v1.83.2 // 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 bea96100b..3155c354a 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -4,24 +4,24 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/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.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
-cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
+cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
+cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
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.19.0 h1:+RK0lPIB6TlcBP7JyqmmhCNihp1Iw4QQ8uxcvlKhBVQ=
cloud.google.com/go/cloudtasks v1.19.0/go.mod h1:8q8wNubq0jFvXW5Pz8P3O7QWJBXOmfrY918FqTgIqHA=
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.22.0 h1:avooeboIq37vKXobrbPUFhFBxS/c3FqmWoX0xs8dO6E=
-cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU=
-cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
-cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
-cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
-cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
+cloud.google.com/go/firestore v1.24.0 h1:x0Z3hrgjYgo2wI9whuBRQcNc2hYwzZDQy/7pkUXbXcs=
+cloud.google.com/go/firestore v1.24.0/go.mod h1:5aojyjN4olKUnBZDCRWwM+NsdrrCX3t1qfyERZGOonM=
+cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk=
+cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY=
+cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
+cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
-cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
-cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
+cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY=
+cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM=
cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg=
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
@@ -199,10 +199,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.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg=
-github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
-github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
-github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
+github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
+github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
+github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
+github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
@@ -444,19 +444,19 @@ 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-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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
-golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
-golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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=
@@ -477,35 +477,35 @@ 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=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
+golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.287.1 h1:LiyJx32VU3cwQfLchn/513qKhc25hq0pEANYJoWNnnI=
-google.golang.org/api v0.287.1/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY=
+google.golang.org/api v0.295.0 h1:SSqFeEVjnK5SKo6t7D0E0M7EfX8SP7K0+OJd2Ly5FzU=
+google.golang.org/api v0.295.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
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-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY=
-google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA=
-google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
-google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
-google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
+google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
+google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
+google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
+google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
+google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
+google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
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=
-google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
+google.golang.org/protobuf v1.36.12/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
From 1a6d3dce8f71b64b43ef5af614737cac5dd57c80 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:53 +0000
Subject: [PATCH 47/88] fix(deps): bump github.com/stretchr/testify in /api
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.1)
---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
dependency-version: 1.12.1
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 6 ++----
api/go.sum | 10 ++++------
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..cea2c2a63 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -42,7 +42,7 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.21.0
github.com/redis/go-redis/v9 v9.21.0
github.com/rs/zerolog v1.35.1
- github.com/stretchr/testify v1.11.1
+ github.com/stretchr/testify v1.12.1
github.com/swaggo/swag v1.16.6
github.com/thedevsaddam/govalidator v1.9.10
github.com/uptrace/uptrace-go v1.43.0
@@ -158,7 +158,6 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // 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.21.0 // indirect
github.com/richardlehane/mscfb v1.0.7 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
@@ -195,7 +194,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
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
+ go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
@@ -209,7 +208,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.82.1 // 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 bea96100b..698221567 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -328,8 +328,8 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
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.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=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
@@ -439,8 +439,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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=
@@ -507,8 +507,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
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-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=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From 1df4eedb8e0a16dc805e067584aa995ff74e352d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:08:57 +0000
Subject: [PATCH 48/88] fix(deps): bump github.com/carlmjohnson/requests in
/api
Bumps [github.com/carlmjohnson/requests](https://github.com/carlmjohnson/requests) from 0.25.1 to 0.26.1.
- [Release notes](https://github.com/carlmjohnson/requests/releases)
- [Commits](https://github.com/carlmjohnson/requests/compare/v0.25.1...v0.26.1)
---
updated-dependencies:
- dependency-name: github.com/carlmjohnson/requests
dependency-version: 0.26.1
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 12 ++++++------
api/go.sum | 24 ++++++++++++------------
2 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..38fc1a3cb 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -15,7 +15,7 @@ require (
github.com/NdoleStudio/stacktrace v1.1.0
github.com/avast/retry-go/v5 v5.0.0
github.com/axiomhq/axiom-go v0.32.0
- github.com/carlmjohnson/requests v0.25.1
+ github.com/carlmjohnson/requests v0.26.1
github.com/cloudevents/sdk-go/v2 v2.16.2
github.com/cockroachdb/cockroach-go/v2 v2.4.3
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
@@ -196,14 +196,14 @@ 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.54.0 // indirect
- golang.org/x/mod v0.37.0 // indirect
- golang.org/x/net v0.57.0 // indirect
+ golang.org/x/crypto v0.55.0 // indirect
+ golang.org/x/mod v0.38.0 // indirect
+ golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
- golang.org/x/text v0.40.0 // indirect
+ golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
- golang.org/x/tools v0.47.0 // indirect
+ golang.org/x/tools v0.48.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
diff --git a/api/go.sum b/api/go.sum
index bea96100b..648e6aea4 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -78,8 +78,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g=
github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA=
-github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM=
+github.com/carlmjohnson/requests v0.26.1 h1:kFt5j1XsynNKoladW2qJdP9S+UdigvGLW+53DXbN5F4=
+github.com/carlmjohnson/requests v0.26.1/go.mod h1:qMGUsG9/8MbvYRMGwvMBVSQQmRswVcVMAzexVyy038g=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -444,19 +444,19 @@ 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-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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
-golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
-golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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=
@@ -477,15 +477,15 @@ 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=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
+golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
From c88d625a5cd76565ff9c809e68a09777f57806d0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:09:03 +0000
Subject: [PATCH 49/88] fix(deps): bump github.com/gofiber/contrib/v3/otel in
/api
Bumps [github.com/gofiber/contrib/v3/otel](https://github.com/gofiber/contrib) from 1.2.2 to 1.2.4.
- [Release notes](https://github.com/gofiber/contrib/releases)
- [Commits](https://github.com/gofiber/contrib/compare/paseto/v1.2.2...paseto/v1.2.4)
---
updated-dependencies:
- dependency-name: github.com/gofiber/contrib/v3/otel
dependency-version: 1.2.4
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
api/go.mod | 44 ++++++++++++-------------
api/go.sum | 94 ++++++++++++++++++++++++++----------------------------
2 files changed, 67 insertions(+), 71 deletions(-)
diff --git a/api/go.mod b/api/go.mod
index 6e3d5bdb0..955e386b2 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -23,9 +23,9 @@ require (
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/v3/otel v1.2.2
+ github.com/gofiber/contrib/v3/otel v1.2.4
github.com/gofiber/contrib/v3/swaggo v1.0.8
- github.com/gofiber/fiber/v3 v3.4.0
+ github.com/gofiber/fiber/v3 v3.5.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8
@@ -42,20 +42,20 @@ require (
github.com/redis/go-redis/extra/redisotel/v9 v9.21.0
github.com/redis/go-redis/v9 v9.21.0
github.com/rs/zerolog v1.35.1
- github.com/stretchr/testify v1.11.1
+ github.com/stretchr/testify v1.12.1
github.com/swaggo/swag v1.16.6
github.com/thedevsaddam/govalidator v1.9.10
github.com/uptrace/uptrace-go v1.43.0
github.com/xuri/excelize/v2 v2.11.0
go.mongodb.org/mongo-driver/v2 v2.8.0
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708
- go.opentelemetry.io/otel v1.44.0
+ go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
- go.opentelemetry.io/otel/metric v1.44.0
- go.opentelemetry.io/otel/sdk v1.44.0
- go.opentelemetry.io/otel/sdk/metric v1.44.0
- go.opentelemetry.io/otel/trace v1.44.0
+ go.opentelemetry.io/otel/metric v1.46.0
+ go.opentelemetry.io/otel/sdk v1.46.0
+ go.opentelemetry.io/otel/sdk/metric v1.46.0
+ go.opentelemetry.io/otel/trace v1.46.0
golang.org/x/sync v0.22.0
google.golang.org/api v0.287.1
google.golang.org/protobuf v1.36.11
@@ -111,7 +111,7 @@ require (
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.4 // indirect
- github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.24.0 // indirect
github.com/go-openapi/jsonreference v0.21.6 // indirect
@@ -125,8 +125,8 @@ require (
github.com/go-openapi/swag/yamlutils v0.27.0 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
- github.com/gofiber/schema v1.8.2 // indirect
- github.com/gofiber/utils/v2 v2.2.0 // indirect
+ github.com/gofiber/schema v1.8.4 // indirect
+ github.com/gofiber/utils/v2 v2.4.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
@@ -144,9 +144,9 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.19.0 // indirect
+ github.com/klauspost/compress v1.19.2 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
- github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -158,7 +158,6 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // 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.21.0 // indirect
github.com/richardlehane/mscfb v1.0.7 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
@@ -170,7 +169,7 @@ require (
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasthttp v1.72.0 // indirect
+ github.com/valyala/fasthttp v1.73.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
github.com/vanng822/go-premailer v1.34.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -180,7 +179,7 @@ require (
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.44.0 // indirect
+ go.opentelemetry.io/contrib v1.46.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
@@ -195,21 +194,20 @@ require (
go.uber.org/atomic v1.11.0 // indirect
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.54.0 // indirect
- golang.org/x/mod v0.37.0 // indirect
- golang.org/x/net v0.57.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.5 // indirect
+ golang.org/x/crypto v0.55.0 // indirect
+ golang.org/x/mod v0.38.0 // indirect
+ golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
- golang.org/x/text v0.40.0 // indirect
+ golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
- golang.org/x/tools v0.47.0 // indirect
+ golang.org/x/tools v0.48.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.82.1 // 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 bea96100b..25bb7fea4 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -120,8 +120,8 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
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/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
-github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
+github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
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=
@@ -133,8 +133,8 @@ github.com/go-hermes/hermes/v2 v2.6.2/go.mod h1:RLVNk31/1KqF35vK3mAaQVuJvMH+K5//
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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
+github.com/go-logr/logr v1.4.4/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.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M=
@@ -168,16 +168,16 @@ github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxo
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
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/v3/otel v1.2.2 h1:DQ0S5dIDb5UsnUT2HI5rXAwzy/pIiOMZkO6izQr8Fok=
-github.com/gofiber/contrib/v3/otel v1.2.2/go.mod h1:icVCf1XJoqCuY7Fq/EAbaTk30ngA1Fc9r4zq/P2FBvk=
+github.com/gofiber/contrib/v3/otel v1.2.4 h1:wID72pBdMhWcoUzmj4T8OYH2mVabDHtw/hZG5BmRvWQ=
+github.com/gofiber/contrib/v3/otel v1.2.4/go.mod h1:aK0uuKzhSJ8X6lN6I7AlDCVoAr4ES0ZjkLl0Cny/WXE=
github.com/gofiber/contrib/v3/swaggo v1.0.8 h1:11YmMVbkMO7zv6YR0gdNfRWcs5ZVUkDpR21AhjhbK00=
github.com/gofiber/contrib/v3/swaggo v1.0.8/go.mod h1:7jyBZxqQ4MjOxIidOvLQ9jNAxNk05yKpid40OYb3pLE=
-github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw=
-github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk=
-github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU=
-github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g=
-github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE=
-github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE=
+github.com/gofiber/fiber/v3 v3.5.0 h1:dk7TOUH6DXJGtOLsN2XEG+0ZML7cznzHILTVozbNEK8=
+github.com/gofiber/fiber/v3 v3.5.0/go.mod h1:GOVDTW+gjJvfe0iJyVujbQ1Lnx+JUjFySJRI/9/xX/w=
+github.com/gofiber/schema v1.8.4 h1:ctANnOE2uXft17l5cw78qYqoLt2nfZGRgZ2QUugefFQ=
+github.com/gofiber/schema v1.8.4/go.mod h1:JxOlqaEBpuyGKBLI9wY8BAsnWt9z+cFGLaijlAF/IF0=
+github.com/gofiber/utils/v2 v2.4.2 h1:OA4LAEG3zKkPwFU7Jippxdk//N5hKMU12qXSCd0C4lQ=
+github.com/gofiber/utils/v2 v2.4.2/go.mod h1:AGosyllO+RVdfXyyKoUp91D0qj86BuknRN4I6mciqe8=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -243,8 +243,8 @@ 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/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
-github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
+github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -255,8 +255,8 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
-github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
@@ -328,8 +328,8 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
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.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=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
@@ -352,8 +352,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.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M=
-github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk=
+github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
+github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
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.34.0 h1:CW7RUnjCfXrkuCbgC2wi/Cub7IwKslJWD/OkIBlcQUk=
@@ -385,8 +385,8 @@ go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4Dh
go.mongodb.org/mongo-driver/v2 v2.8.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.44.0 h1:cVL0yu3uyrXkAmxonxvzYysIo5EZa8jKh3740MzxBzI=
-go.opentelemetry.io/contrib v1.44.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk=
+go.opentelemetry.io/contrib v1.46.0 h1:PVrao7T/FJZ814/+QbMcIMIIpdjQXC3pi3V11+6DT40=
+go.opentelemetry.io/contrib v1.46.0/go.mod h1:1DF/GVJ9wpq32v0EOs2sbV77TFtM3IRzgAVPyjGSe88=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260624193928-df9c7a836708 h1:s5k/FTgmE4Mj2jFG38Dj5TQgxDRMG92fRK3puTdMHew=
@@ -399,8 +399,8 @@ go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0 h1:MtkMsuRo3zEXTTMAL
go.opentelemetry.io/contrib/instrumentation/runtime v0.69.0/go.mod h1:FYTxnpsm+UPD0erZNq20GvnM8T2YQHiHtT2vokdpoac=
go.opentelemetry.io/contrib/processors/minsev v0.16.1 h1:DYL02u57VGQjrG8c09i6Bx5R74h4XxLj75wEk5h8/DA=
go.opentelemetry.io/contrib/processors/minsev v0.16.1/go.mod h1:VnF+kZnkagrTfTb2mV+6PTMAtPulgjpcUajm8sRk3tQ=
-go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
-go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
+go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
@@ -415,20 +415,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
-go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
-go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
-go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
-go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
-go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
-go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
+go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
+go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
+go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
+go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
+go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
+go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
-go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
-go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
-go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
-go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
+go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
+go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
+go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
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=
@@ -439,24 +439,24 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
-golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
-golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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=
@@ -477,15 +477,15 @@ 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=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
-golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
+golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
@@ -507,8 +507,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
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-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=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From c2d357c2a66c9e84e9aa60b6cc81f4487c6e9ca5 Mon Sep 17 00:00:00 2001
From: Acho Arnold
Date: Tue, 1 Sep 2026 10:12:25 +0300
Subject: [PATCH 50/88] feat(api): track unread thread count (#978)
* feat(api)!: track unread thread count
Replace the boolean read state with an unread message counter so repeated
inbound activity remains visible until the thread is explicitly cleared.
BREAKING CHANGE: Message thread responses and updates use unread_count
instead of is_read.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8efee0ef-a8be-4d9c-b4ec-33dcbf0dfdfd
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: missed phone call string and webhook refresh
* fix(api): add thread conflict index
Create the composite unique index required by the message-thread upsert so
fresh databases can execute its ON CONFLICT target.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8efee0ef-a8be-4d9c-b4ec-33dcbf0dfdfd
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot-Session: 8efee0ef-a8be-4d9c-b4ec-33dcbf0dfdfd
---
api/docs/docs.go | 16 +++----
api/docs/swagger.json | 16 +++----
api/docs/swagger.yaml | 14 +++---
api/pkg/entities/message_thread.go | 9 ++--
api/pkg/entities/message_thread_test.go | 29 +++++++----
.../handlers/message_thread_handler_test.go | 2 +-
.../gorm_message_thread_repository.go | 48 ++++++++-----------
.../gorm_message_thread_repository_test.go | 37 ++++++--------
.../repositories/message_thread_repository.go | 5 +-
.../requests/message_thread_update_request.go | 6 +--
.../message_thread_update_request_test.go | 6 +--
api/pkg/services/message_thread_service.go | 17 +++----
.../services/message_thread_service_test.go | 30 ++++++------
.../message_thread_handler_validator.go | 4 +-
.../message_thread_handler_validator_test.go | 6 +--
tests/README.md | 2 +-
tests/read_receipts_test.go | 24 +++++-----
web/app/components/MessageThread.vue | 34 +++++++++++--
web/app/pages/threads/[id]/index.vue | 39 +++++++++------
web/app/stores/messages.ts | 8 ++++
web/app/stores/threads.ts | 10 ++--
web/shared/types/api.ts | 8 ++--
22 files changed, 200 insertions(+), 170 deletions(-)
diff --git a/api/docs/docs.go b/api/docs/docs.go
index 8eb6dafe6..96cf0e559 100644
--- a/api/docs/docs.go
+++ b/api/docs/docs.go
@@ -4129,12 +4129,12 @@ const docTemplate = `{
"created_at",
"id",
"is_archived",
- "is_read",
"last_message_content",
"last_message_id",
"order_timestamp",
"owner",
"status",
+ "unread_count",
"updated_at",
"user_id"
],
@@ -4167,10 +4167,6 @@ const docTemplate = `{
"type": "boolean",
"example": false
},
- "is_read": {
- "type": "boolean",
- "example": true
- },
"last_message_content": {
"type": "string",
"example": "This is a sample message content"
@@ -4191,6 +4187,10 @@ const docTemplate = `{
"type": "string",
"example": "PENDING"
},
+ "unread_count": {
+ "type": "integer",
+ "example": 0
+ },
"updated_at": {
"type": "string",
"example": "2022-06-05T14:26:09.527976+03:00"
@@ -4920,9 +4920,9 @@ const docTemplate = `{
"type": "boolean",
"example": true
},
- "is_read": {
- "type": "boolean",
- "example": true
+ "unread_count": {
+ "type": "integer",
+ "example": 0
}
}
},
diff --git a/api/docs/swagger.json b/api/docs/swagger.json
index 21c6bbf80..4926b5e55 100644
--- a/api/docs/swagger.json
+++ b/api/docs/swagger.json
@@ -4126,12 +4126,12 @@
"created_at",
"id",
"is_archived",
- "is_read",
"last_message_content",
"last_message_id",
"order_timestamp",
"owner",
"status",
+ "unread_count",
"updated_at",
"user_id"
],
@@ -4164,10 +4164,6 @@
"type": "boolean",
"example": false
},
- "is_read": {
- "type": "boolean",
- "example": true
- },
"last_message_content": {
"type": "string",
"example": "This is a sample message content"
@@ -4188,6 +4184,10 @@
"type": "string",
"example": "PENDING"
},
+ "unread_count": {
+ "type": "integer",
+ "example": 0
+ },
"updated_at": {
"type": "string",
"example": "2022-06-05T14:26:09.527976+03:00"
@@ -4917,9 +4917,9 @@
"type": "boolean",
"example": true
},
- "is_read": {
- "type": "boolean",
- "example": true
+ "unread_count": {
+ "type": "integer",
+ "example": 0
}
}
},
diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml
index 4fe2f2b7c..9cfefbf56 100644
--- a/api/docs/swagger.yaml
+++ b/api/docs/swagger.yaml
@@ -367,9 +367,6 @@ definitions:
is_archived:
example: false
type: boolean
- is_read:
- example: true
- type: boolean
last_message_content:
example: This is a sample message content
type: string
@@ -385,6 +382,9 @@ definitions:
status:
example: PENDING
type: string
+ unread_count:
+ example: 0
+ type: integer
updated_at:
example: "2022-06-05T14:26:09.527976+03:00"
type: string
@@ -397,12 +397,12 @@ definitions:
- created_at
- id
- is_archived
- - is_read
- last_message_content
- last_message_id
- order_timestamp
- owner
- status
+ - unread_count
- updated_at
- user_id
type: object
@@ -956,9 +956,9 @@ definitions:
is_archived:
example: true
type: boolean
- is_read:
- example: true
- type: boolean
+ unread_count:
+ example: 0
+ type: integer
type: object
requests.PhoneAPIKeyStoreRequest:
properties:
diff --git a/api/pkg/entities/message_thread.go b/api/pkg/entities/message_thread.go
index d4d893a49..e8f1f687f 100644
--- a/api/pkg/entities/message_thread.go
+++ b/api/pkg/entities/message_thread.go
@@ -9,12 +9,11 @@ import (
// MessageThread represents a message thread between 2 phone numbers
type MessageThread struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
- Owner string `json:"owner" example:"+18005550199"`
- Contact string `json:"contact" example:"+18005550100"`
+ Owner string `json:"owner" gorm:"uniqueIndex:idx_message_threads_user_owner_contact,priority:2" example:"+18005550199"`
+ Contact string `json:"contact" gorm:"uniqueIndex:idx_message_threads_user_owner_contact,priority:3" example:"+18005550100"`
IsArchived bool `json:"is_archived" example:"false"`
- IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"`
- LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"`
- UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
+ UnreadCount uint `json:"unread_count" gorm:"not null;default:0" example:"0"`
+ UserID UserID `json:"user_id" gorm:"uniqueIndex:idx_message_threads_user_owner_contact,priority:1" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
Color string `json:"color" example:"indigo"`
Status MessageStatus `json:"status" example:"PENDING"`
LastMessageContent *string `json:"last_message_content" example:"This is a sample message content"`
diff --git a/api/pkg/entities/message_thread_test.go b/api/pkg/entities/message_thread_test.go
index b587bacbc..47eaf2778 100644
--- a/api/pkg/entities/message_thread_test.go
+++ b/api/pkg/entities/message_thread_test.go
@@ -9,20 +9,29 @@ import (
"github.com/stretchr/testify/require"
)
-func TestMessageThreadReadFieldsHaveBackwardCompatibleDefaults(t *testing.T) {
+func TestMessageThreadUnreadCountHasDefault(t *testing.T) {
threadType := reflect.TypeOf(MessageThread{})
- isRead, ok := threadType.FieldByName("IsRead")
+ unreadCount, ok := threadType.FieldByName("UnreadCount")
require.True(t, ok)
- assert.Contains(t, isRead.Tag.Get("gorm"), "not null")
- assert.Contains(t, isRead.Tag.Get("gorm"), "default:true")
- assert.Equal(t, "is_read", isRead.Tag.Get("json"))
+ assert.Contains(t, unreadCount.Tag.Get("gorm"), "not null")
+ assert.Contains(t, unreadCount.Tag.Get("gorm"), "default:0")
+ assert.Equal(t, "unread_count", unreadCount.Tag.Get("json"))
+}
- lastReadAt, ok := threadType.FieldByName("LastReadAt")
- require.True(t, ok)
- assert.Contains(t, lastReadAt.Tag.Get("gorm"), "not null")
- assert.Contains(t, lastReadAt.Tag.Get("gorm"), "default:CURRENT_TIMESTAMP")
- assert.Equal(t, "-", lastReadAt.Tag.Get("json"))
+func TestMessageThreadHasUniqueOwnerContactPerUser(t *testing.T) {
+ threadType := reflect.TypeOf(MessageThread{})
+
+ expectedTags := map[string]string{
+ "UserID": "uniqueIndex:idx_message_threads_user_owner_contact,priority:1",
+ "Owner": "uniqueIndex:idx_message_threads_user_owner_contact,priority:2",
+ "Contact": "uniqueIndex:idx_message_threads_user_owner_contact,priority:3",
+ }
+ for fieldName, expectedTag := range expectedTags {
+ field, ok := threadType.FieldByName(fieldName)
+ require.True(t, ok)
+ assert.Contains(t, field.Tag.Get("gorm"), expectedTag)
+ }
}
func TestMessageThreadContactDetailsAreTransientAndOmittedWhenNil(t *testing.T) {
diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go
index 56dfa0c79..a304f4ad3 100644
--- a/api/pkg/handlers/message_thread_handler_test.go
+++ b/api/pkg/handlers/message_thread_handler_test.go
@@ -75,7 +75,7 @@ func TestMessageThreadHandlerUpdate_ReturnsNotFoundWhenThreadIsMissing(t *testin
handler.RegisterRoutes(app)
messageThreadID := uuid.New()
- req := httptest.NewRequest(http.MethodPut, "/v1/message-threads/"+messageThreadID.String(), bytes.NewBufferString(`{"is_read":true}`))
+ req := httptest.NewRequest(http.MethodPut, "/v1/message-threads/"+messageThreadID.String(), bytes.NewBufferString(`{"unread_count":0}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, fiber.TestConfig{Timeout: time.Second})
diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go
index 092418040..3989cc4e3 100644
--- a/api/pkg/repositories/gorm_message_thread_repository.go
+++ b/api/pkg/repositories/gorm_message_thread_repository.go
@@ -46,11 +46,7 @@ func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string
updates["is_archived"] = false
}
if params.MarkAsUnread {
- updates["is_read"] = gorm.Expr(
- "CASE WHEN last_read_at < ? THEN ? ELSE is_read END",
- params.EventTimestamp,
- false,
- )
+ updates["unread_count"] = gorm.Expr("unread_count + ?", 1)
}
return updates
}
@@ -68,11 +64,8 @@ func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any
if params.IsArchived != nil {
updates["is_archived"] = *params.IsArchived
}
- if params.IsRead != nil {
- updates["is_read"] = *params.IsRead
- if *params.IsRead {
- updates["last_read_at"] = params.ReadAt
- }
+ if params.UnreadCount != nil {
+ updates["unread_count"] = *params.UnreadCount
}
return updates
}
@@ -123,27 +116,24 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread
ctx, span := repository.tracer.Start(ctx)
defer span.End()
- isRead := thread.IsRead
- err := repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
- result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(thread)
- thread.IsRead = isRead
- if result.Error != nil {
- return result.Error
- }
- if result.RowsAffected == 0 || isRead {
- return nil
- }
-
- return tx.Model(&entities.MessageThread{}).
- Where("user_id = ?", thread.UserID).
- Where("id = ?", thread.ID).
- UpdateColumn("is_read", false).
- Error
- })
- if err != nil {
- return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message thread with ID [%s]", thread.ID))
+ db := repository.db.WithContext(ctx).Session(&gorm.Session{SkipDefaultTransaction: true})
+ onConflict := clause.OnConflict{
+ Columns: []clause.Column{
+ {Name: "user_id"},
+ {Name: "owner"},
+ {Name: "contact"},
+ },
+ DoNothing: thread.UnreadCount == 0,
+ }
+ if thread.UnreadCount > 0 {
+ onConflict.DoUpdates = clause.Assignments(map[string]any{
+ "unread_count": gorm.Expr("unread_count + ?", 1),
+ })
}
+ if result := db.Clauses(onConflict).Create(thread); result.Error != nil {
+ return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot insert message thread with ID [%s]", thread.ID))
+ }
return nil
}
diff --git a/api/pkg/repositories/gorm_message_thread_repository_test.go b/api/pkg/repositories/gorm_message_thread_repository_test.go
index 31dc43ed5..c30ed4fc3 100644
--- a/api/pkg/repositories/gorm_message_thread_repository_test.go
+++ b/api/pkg/repositories/gorm_message_thread_repository_test.go
@@ -75,7 +75,7 @@ func (logger *messageThreadTestLogger) Debug(string)
func (logger *messageThreadTestLogger) Fatal(error) {}
func (logger *messageThreadTestLogger) Printf(string, ...interface{}) {}
-func TestMessageThreadStorePreservesExplicitUnreadState(t *testing.T) {
+func TestMessageThreadStoreIncrementsUnreadCountOnConflict(t *testing.T) {
pool := &messageThreadTestConnPool{}
db, err := gorm.Open(
postgres.New(postgres.Config{
@@ -89,18 +89,17 @@ func TestMessageThreadStorePreservesExplicitUnreadState(t *testing.T) {
logger := &messageThreadTestLogger{}
repository := NewGormMessageThreadRepository(logger, telemetry.NewOtelLogger("test", logger), db)
thread := &entities.MessageThread{
- ID: uuid.New(),
- IsRead: false,
+ ID: uuid.New(),
+ UnreadCount: 1,
}
require.NoError(t, repository.Store(context.Background(), thread))
- assert.False(t, thread.IsRead)
+ assert.Equal(t, uint(1), thread.UnreadCount)
require.NotEmpty(t, pool.statements)
- update := pool.statements[len(pool.statements)-1]
- assert.True(t, strings.HasPrefix(update.query, `UPDATE "message_threads"`))
- assert.Contains(t, update.query, `"is_read"=$1`)
- assert.Contains(t, update.args, false)
+ insert := pool.statements[len(pool.statements)-1]
+ assert.True(t, strings.HasPrefix(insert.query, `INSERT INTO "message_threads"`))
+ assert.Contains(t, insert.query, `ON CONFLICT ("user_id","owner","contact") DO UPDATE SET "unread_count"=unread_count + $`)
}
func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) {
@@ -118,9 +117,8 @@ func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) {
"last_message_content": "hello",
"status": entities.MessageStatus(entities.MessageStatusReceived),
}, updates)
- assert.NotContains(t, updates, "is_read")
+ assert.NotContains(t, updates, "unread_count")
assert.NotContains(t, updates, "is_archived")
- assert.NotContains(t, updates, "last_read_at")
}
func TestUpdateActivityMarksUnreadWithOneQuery(t *testing.T) {
@@ -156,7 +154,7 @@ func TestUpdateActivityMarksUnreadWithOneQuery(t *testing.T) {
}
}
require.Len(t, updates, 1)
- assert.Contains(t, updates[0].query, `"is_read"=CASE WHEN last_read_at <`)
+ assert.Contains(t, updates[0].query, `"unread_count"=unread_count + $`)
}
func TestMessageThreadDeletedUpdatesPreserveStatusType(t *testing.T) {
@@ -175,19 +173,14 @@ func TestMessageThreadDeletedUpdatesPreserveStatusType(t *testing.T) {
}, updates)
}
-func TestMessageThreadStatusUpdatesReadOnly(t *testing.T) {
- isRead := true
- readAt := time.Date(2026, 7, 18, 7, 1, 0, 0, time.UTC)
+func TestMessageThreadStatusUpdatesUnreadCountOnly(t *testing.T) {
+ unreadCount := uint(0)
updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
- IsRead: &isRead,
- ReadAt: readAt,
+ UnreadCount: &unreadCount,
})
- assert.Equal(t, map[string]any{
- "is_read": true,
- "last_read_at": readAt,
- }, updates)
+ assert.Equal(t, map[string]any{"unread_count": uint(0)}, updates)
assert.NotContains(t, updates, "is_archived")
}
@@ -197,8 +190,6 @@ func TestMessageThreadStatusUpdatesArchiveOnly(t *testing.T) {
updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{
IsArchived: &isArchived,
})
-
assert.Equal(t, map[string]any{"is_archived": true}, updates)
- assert.NotContains(t, updates, "is_read")
- assert.NotContains(t, updates, "last_read_at")
+ assert.NotContains(t, updates, "unread_count")
}
diff --git a/api/pkg/repositories/message_thread_repository.go b/api/pkg/repositories/message_thread_repository.go
index e60931419..28efc11ee 100644
--- a/api/pkg/repositories/message_thread_repository.go
+++ b/api/pkg/repositories/message_thread_repository.go
@@ -23,9 +23,8 @@ type MessageThreadActivityUpdate struct {
}
type MessageThreadStatusUpdate struct {
- IsArchived *bool
- IsRead *bool
- ReadAt time.Time
+ IsArchived *bool
+ UnreadCount *uint
}
type MessageThreadDeletedUpdate struct {
diff --git a/api/pkg/requests/message_thread_update_request.go b/api/pkg/requests/message_thread_update_request.go
index 3309fc91d..0832ad819 100644
--- a/api/pkg/requests/message_thread_update_request.go
+++ b/api/pkg/requests/message_thread_update_request.go
@@ -10,8 +10,8 @@ import (
// MessageThreadUpdate is the payload for updating a message thread
type MessageThreadUpdate struct {
request
- IsArchived *bool `json:"is_archived,omitempty" example:"true"`
- IsRead *bool `json:"is_read,omitempty" example:"true"`
+ IsArchived *bool `json:"is_archived,omitempty" example:"true"`
+ UnreadCount *uint `json:"unread_count,omitempty" example:"0"`
MessageThreadID string `json:"messageThreadID" swaggerignore:"true"` // used internally for validation
}
@@ -22,6 +22,6 @@ func (input *MessageThreadUpdate) ToUpdateParams(userID entities.UserID) service
UserID: userID,
MessageThreadID: uuid.MustParse(input.MessageThreadID),
IsArchived: input.IsArchived,
- IsRead: input.IsRead,
+ UnreadCount: input.UnreadCount,
}
}
diff --git a/api/pkg/requests/message_thread_update_request_test.go b/api/pkg/requests/message_thread_update_request_test.go
index 9f9579fda..58ba7e4f7 100644
--- a/api/pkg/requests/message_thread_update_request_test.go
+++ b/api/pkg/requests/message_thread_update_request_test.go
@@ -10,10 +10,10 @@ import (
func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T) {
threadID := uuid.New()
- isRead := true
+ unreadCount := uint(0)
input := MessageThreadUpdate{
MessageThreadID: threadID.String(),
- IsRead: &isRead,
+ UnreadCount: &unreadCount,
}
params := input.ToUpdateParams(entities.UserID("user-id"))
@@ -21,5 +21,5 @@ func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T)
assert.Equal(t, threadID, params.MessageThreadID)
assert.Equal(t, entities.UserID("user-id"), params.UserID)
assert.Nil(t, params.IsArchived)
- assert.Same(t, &isRead, params.IsRead)
+ assert.Same(t, &unreadCount, params.UnreadCount)
}
diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go
index 8431672b7..efcfadd17 100644
--- a/api/pkg/services/message_thread_service.go
+++ b/api/pkg/services/message_thread_service.go
@@ -143,7 +143,7 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me
// MessageThreadStatusParams are parameters for updating a thread status
type MessageThreadStatusParams struct {
IsArchived *bool
- IsRead *bool
+ UnreadCount *uint
UserID entities.UserID
MessageThreadID uuid.UUID
}
@@ -154,9 +154,8 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me
defer span.End()
update := repositories.MessageThreadStatusUpdate{
- IsArchived: params.IsArchived,
- IsRead: params.IsRead,
- ReadAt: time.Now().UTC(),
+ IsArchived: params.IsArchived,
+ UnreadCount: params.UnreadCount,
}
thread, err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update)
if err != nil {
@@ -207,10 +206,13 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte
}
func (service *MessageThreadService) createThread(ctx context.Context, params MessageThreadUpdateParams) error {
- ctx, span := service.tracer.Start(ctx)
+ ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
defer span.End()
- ctxLogger := service.tracer.CtxLogger(service.logger, span)
+ unreadCount := uint(0)
+ if params.MarkAsUnread {
+ unreadCount = 1
+ }
now := time.Now().UTC()
thread := &entities.MessageThread{
@@ -219,8 +221,7 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me
Contact: params.Contact,
UserID: params.UserID,
IsArchived: false,
- IsRead: !params.MarkAsUnread,
- LastReadAt: now,
+ UnreadCount: unreadCount,
Color: service.getColor(),
LastMessageContent: ¶ms.Content,
Status: params.Status,
diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go
index 44a3bcfdd..1ef9b4bde 100644
--- a/api/pkg/services/message_thread_service_test.go
+++ b/api/pkg/services/message_thread_service_test.go
@@ -107,11 +107,11 @@ func TestUpdateThreadPassesUnreadWatermarkForInboundActivity(t *testing.T) {
assert.Equal(t, eventTimestamp, captured.EventTimestamp)
}
-func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) {
+func TestUpdateThreadDoesNotIncrementUnreadCountForOutboundActivity(t *testing.T) {
var captured repositories.MessageThreadActivityUpdate
repository := &messageThreadRepositoryStub{
loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) {
- return &entities.MessageThread{ID: uuid.New(), IsRead: false}, nil
+ return &entities.MessageThread{ID: uuid.New(), UnreadCount: 2}, nil
},
updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error {
captured = params
@@ -134,14 +134,14 @@ func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) {
assert.False(t, captured.MarkAsUnread)
}
-func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) {
+func TestCreateThreadSetsUnreadCountFromActivityDirection(t *testing.T) {
tests := []struct {
name string
marksUnread bool
- wantRead bool
+ wantUnread uint
}{
- {name: "inbound", marksUnread: true, wantRead: false},
- {name: "outbound", marksUnread: false, wantRead: true},
+ {name: "inbound", marksUnread: true, wantUnread: 1},
+ {name: "outbound", marksUnread: false, wantUnread: 0},
}
for _, test := range tests {
@@ -171,20 +171,19 @@ func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, stored)
- assert.Equal(t, test.wantRead, stored.IsRead)
- assert.False(t, stored.LastReadAt.IsZero())
+ assert.Equal(t, test.wantUnread, stored.UnreadCount)
})
}
}
func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) {
threadID := uuid.New()
- isRead := false
+ unreadCount := uint(0)
var captured repositories.MessageThreadStatusUpdate
repository := &messageThreadRepositoryStub{
updateStatus: func(_ context.Context, _ entities.UserID, _ uuid.UUID, params repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) {
captured = params
- return &entities.MessageThread{ID: threadID, IsArchived: true, IsRead: false}, nil
+ return &entities.MessageThread{ID: threadID, IsArchived: true, UnreadCount: 0}, nil
},
}
@@ -192,15 +191,14 @@ func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) {
thread, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
UserID: entities.UserID("user-id"),
MessageThreadID: threadID,
- IsRead: &isRead,
+ UnreadCount: &unreadCount,
})
require.NoError(t, err)
assert.Nil(t, captured.IsArchived)
- assert.Same(t, &isRead, captured.IsRead)
- assert.False(t, captured.ReadAt.IsZero())
+ assert.Same(t, &unreadCount, captured.UnreadCount)
assert.True(t, thread.IsArchived)
- assert.False(t, thread.IsRead)
+ assert.Zero(t, thread.UnreadCount)
}
func TestUpdateStatusPreservesNotFoundCode(t *testing.T) {
@@ -211,11 +209,11 @@ func TestUpdateStatusPreservesNotFoundCode(t *testing.T) {
}
service := newMessageThreadServiceForTest(repository)
- isRead := true
+ unreadCount := uint(0)
_, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{
UserID: entities.UserID("user-id"),
MessageThreadID: uuid.New(),
- IsRead: &isRead,
+ UnreadCount: &unreadCount,
})
assert.Equal(t, repositories.ErrCodeNotFound, stacktrace.GetCode(err))
diff --git a/api/pkg/validators/message_thread_handler_validator.go b/api/pkg/validators/message_thread_handler_validator.go
index 72a64194f..d4f08002f 100644
--- a/api/pkg/validators/message_thread_handler_validator.go
+++ b/api/pkg/validators/message_thread_handler_validator.go
@@ -73,11 +73,11 @@ func (validator *MessageThreadHandlerValidator) ValidateUpdate(_ context.Context
})
errors := v.ValidateStruct()
- if request.IsArchived == nil && request.IsRead == nil {
+ if request.IsArchived == nil && request.UnreadCount == nil {
if errors == nil {
errors = url.Values{}
}
- errors.Add("payload", "at least one of is_archived or is_read is required")
+ errors.Add("payload", "at least one of is_archived or unread_count is required")
}
return errors
diff --git a/api/pkg/validators/message_thread_handler_validator_test.go b/api/pkg/validators/message_thread_handler_validator_test.go
index e5ec7c1b7..4f7453662 100644
--- a/api/pkg/validators/message_thread_handler_validator_test.go
+++ b/api/pkg/validators/message_thread_handler_validator_test.go
@@ -20,12 +20,12 @@ func TestValidateUpdateRequiresAtLeastOneStatusField(t *testing.T) {
assert.NotEmpty(t, errors.Get("payload"))
}
-func TestValidateUpdateAcceptsReadOnlyUpdate(t *testing.T) {
+func TestValidateUpdateAcceptsUnreadCountOnlyUpdate(t *testing.T) {
validator := &MessageThreadHandlerValidator{}
- isRead := true
+ unreadCount := uint(0)
request := requests.MessageThreadUpdate{
MessageThreadID: uuid.NewString(),
- IsRead: &isRead,
+ UnreadCount: &unreadCount,
}
errors := validator.ValidateUpdate(context.Background(), request)
diff --git a/tests/README.md b/tests/README.md
index 20e4f5e12..df91dee24 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -53,7 +53,7 @@ The API's Firebase SDK is configured (via `FCM_ENDPOINT` env var) to redirect al
- [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
-- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state
+- [x] **Message thread unread count E2E** — Incoming SMS and missed calls increment the unread count, the existing thread update endpoint clears it, and outbound activity preserves the count
- [x] **Unarchive Thread on Receive E2E** — Archived thread returns to the inbox on inbound message when the phone's `unarchive_thread` setting is enabled, and stays archived when disabled
- [x] **Contacts E2E** — JSON CRUD, search and pagination totals, CSV import normalization, and contact details attached to message threads
diff --git a/tests/read_receipts_test.go b/tests/read_receipts_test.go
index a65e7eb73..178c720de 100644
--- a/tests/read_receipts_test.go
+++ b/tests/read_receipts_test.go
@@ -19,7 +19,7 @@ import (
type integrationMessageThread struct {
ID string `json:"id"`
Contact string `json:"contact"`
- IsRead bool `json:"is_read"`
+ UnreadCount uint `json:"unread_count"`
LastMessageContent *string `json:"last_message_content"`
}
@@ -109,7 +109,7 @@ func waitForMessageThread(
return integrationMessageThread{}
}
-func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) integrationMessageThread {
+func clearMessageThreadUnreadCount(ctx context.Context, t *testing.T, threadID string) integrationMessageThread {
t.Helper()
var response struct {
@@ -121,14 +121,14 @@ func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) i
http.MethodPut,
"/v1/message-threads/"+threadID,
userAPIKey,
- map[string]any{"is_read": true},
+ map[string]any{"unread_count": 0},
http.StatusOK,
&response,
)
return response.Data
}
-func TestMessageThreadReadReceipts(t *testing.T) {
+func TestMessageThreadUnreadCount(t *testing.T) {
ctx := context.Background()
phone := setupPhone(ctx, t, 60)
contact := randomPhoneNumber()
@@ -152,17 +152,17 @@ func TestMessageThreadReadReceipts(t *testing.T) {
)
thread := waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
- return !thread.IsRead
+ return thread.UnreadCount == 1
})
- assert.False(t, thread.IsRead)
+ assert.Equal(t, uint(1), thread.UnreadCount)
- updated := markMessageThreadRead(ctx, t, thread.ID)
- assert.True(t, updated.IsRead)
+ updated := clearMessageThreadUnreadCount(ctx, t, thread.ID)
+ assert.Zero(t, updated.UnreadCount)
assert.Equal(t, contact, updated.Contact)
require.NotNil(t, updated.LastMessageContent)
assert.Equal(t, "Unread inbound message", *updated.LastMessageContent)
waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 10*time.Second, func(thread integrationMessageThread) bool {
- return thread.IsRead
+ return thread.UnreadCount == 0
})
requestJSON(
@@ -182,11 +182,11 @@ func TestMessageThreadReadReceipts(t *testing.T) {
)
thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool {
- return !thread.IsRead &&
+ return thread.UnreadCount == 1 &&
thread.LastMessageContent != nil &&
*thread.LastMessageContent == "Missed phone call"
})
- assert.False(t, thread.IsRead)
+ assert.Equal(t, uint(1), thread.UnreadCount)
outboundContent := "Outbound activity preserves unread"
client := newAPIClient()
@@ -202,5 +202,5 @@ func TestMessageThreadReadReceipts(t *testing.T) {
return thread.LastMessageContent != nil &&
*thread.LastMessageContent == outboundContent
})
- assert.False(t, thread.IsRead, "outbound activity must not clear unread state")
+ assert.Equal(t, uint(1), thread.UnreadCount, "outbound activity must not clear unread count")
}
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index 51de93fc4..93c77324d 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -22,6 +22,22 @@ function threadDate(date: string): string {
})
}
+function hasUnreadMessages(unreadCount: number): boolean {
+ return unreadCount > 0
+}
+
+function unreadBadge(
+ unreadCount: number,
+): false | { color: string; content: string; dot: false } {
+ if (unreadCount === 0) return false
+
+ return {
+ color: 'primary',
+ content: unreadCount > 99 ? '99+' : String(unreadCount),
+ dot: false,
+ }
+}
+
function onInstallApp() {
notificationsStore.addNotification({
type: 'info',
@@ -118,7 +134,7 @@ function threadAvatarInitial(thread: EntitiesMessageThread): string {
{{
mdiAccount
@@ -128,12 +144,20 @@ function threadAvatarInitial(thread: EntitiesMessageThread): string {
}}
- {{
- threadContactTitle(thread)
- }}
+
+ {{ threadContactTitle(thread) }}
+
{{ thread.last_message_content }}
diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue
index f933973dd..87f4e51ed 100644
--- a/web/app/pages/threads/[id]/index.vue
+++ b/web/app/pages/threads/[id]/index.vue
@@ -128,19 +128,36 @@ function scrollToElement() {
hideMessages.value = false
}
-async function markCurrentThreadRead(force = false) {
+async function resetCurrentThreadUnreadCount(force = false) {
const threadId = route.params.id as string
try {
- await threadsStore.markThreadRead(threadId, force)
+ await threadsStore.resetThreadUnreadCount(threadId, force)
} catch (error) {
console.error(error)
}
}
-function loadMessages(hide = true, markRead = true) {
+async function handleInboundMessage() {
+ if (loadingMessages.value) return
+
+ try {
+ const thread = currentThread.value
+ await threadsStore.loadThreads()
+ if (!thread || loadingMessages.value) {
+ return
+ }
+
+ await resetCurrentThreadUnreadCount(true)
+ loadMessages(false, false)
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+function loadMessages(hide = true, resetUnreadCount = true) {
loadingMessages.value = true
const threadId = route.params.id as string
- if (markRead) void markCurrentThreadRead()
+ if (resetUnreadCount) void resetCurrentThreadUnreadCount()
threadsStore
.loadThreadMessages(threadId)
.then((response: EntitiesMessage[]) => {
@@ -257,17 +274,11 @@ onMounted(async () => {
webhookChannel.bind('message.send.failed', () => {
if (!loadingMessages.value) loadMessages(false)
})
- webhookChannel.bind('message.phone.received', () => {
- if (!loadingMessages.value) {
- void markCurrentThreadRead(true)
- loadMessages(false, false)
- }
+ webhookChannel.bind('message.phone.received', (_: never) => {
+ void handleInboundMessage()
})
- webhookChannel.bind('message.call.missed', () => {
- if (!loadingMessages.value) {
- void markCurrentThreadRead(true)
- loadMessages(false, false)
- }
+ webhookChannel.bind('message.call.missed', (_: never) => {
+ void handleInboundMessage()
})
})
diff --git a/web/app/stores/messages.ts b/web/app/stores/messages.ts
index abaf47b60..ae0df1751 100644
--- a/web/app/stores/messages.ts
+++ b/web/app/stores/messages.ts
@@ -48,6 +48,13 @@ export const useMessagesStore = defineStore('messages', () => {
})
}
+ async function getMessage(messageId: string): Promise {
+ const response = await apiFetch<{ data: EntitiesMessage }>(
+ `/v1/messages/${messageId}`,
+ )
+ return response.data
+ }
+
async function searchMessages(
payload: SearchMessagesRequest,
): Promise {
@@ -88,6 +95,7 @@ export const useMessagesStore = defineStore('messages', () => {
return {
sendMessage,
deleteMessage,
+ getMessage,
searchMessages,
sendBulkMessages,
fetchBulkMessageOrders,
diff --git a/web/app/stores/threads.ts b/web/app/stores/threads.ts
index fdc7094ea..1abd68bea 100644
--- a/web/app/stores/threads.ts
+++ b/web/app/stores/threads.ts
@@ -29,7 +29,7 @@ export const useThreadsStore = defineStore('threads', () => {
(thread) => thread.id === updatedThread.id,
)
if (index !== -1) {
- const existingThread = threads.value[index]
+ const existingThread = threads.value[index]!
threads.value[index] = {
...updatedThread,
// Mark-read PUT responses are not contact-enriched; keep the resolved
@@ -113,17 +113,17 @@ export const useThreadsStore = defineStore('threads', () => {
})
}
- async function markThreadRead(threadId: string, force = false) {
+ async function resetThreadUnreadCount(threadId: string, force = false) {
const thread = threads.value.find((item) => item.id === threadId)
if (!thread) throw new Error(`Cannot find thread with id ${threadId}`)
- if (!force && thread.is_read) return
+ if (!force && thread.unread_count === 0) return
try {
const response = await apiFetch<{ data: EntitiesMessageThread }>(
`/v1/message-threads/${threadId}`,
{
method: 'PUT',
- body: { is_read: true },
+ body: { unread_count: 0 },
},
)
replaceThread(response.data)
@@ -174,7 +174,7 @@ export const useThreadsStore = defineStore('threads', () => {
setThreadId,
toggleArchive,
updateThread,
- markThreadRead,
+ resetThreadUnreadCount,
deleteThread,
resetState,
}
diff --git a/web/shared/types/api.ts b/web/shared/types/api.ts
index 3f85c4fb7..becb20636 100644
--- a/web/shared/types/api.ts
+++ b/web/shared/types/api.ts
@@ -225,8 +225,6 @@ export interface EntitiesMessageThread {
id: string;
/** @example false */
is_archived: boolean;
- /** @example true */
- is_read: boolean;
/** @example "This is a sample message content" */
last_message_content: string;
/** @example "32343a19-da5e-4b1b-a767-3298a73703ca" */
@@ -237,6 +235,8 @@ export interface EntitiesMessageThread {
owner: string;
/** @example "PENDING" */
status: string;
+ /** @example 0 */
+ unread_count: number;
/** @example "2022-06-05T14:26:09.527976+03:00" */
updated_at: string;
/** @example "WB7DRDWrJZRGbYrv2CKGkqbzvqdC" */
@@ -527,8 +527,8 @@ export interface RequestsMessageSendScheduleWindow {
export interface RequestsMessageThreadUpdate {
/** @example true */
is_archived?: boolean;
- /** @example true */
- is_read?: boolean;
+ /** @example 0 */
+ unread_count?: number;
}
export interface RequestsPhoneAPIKeyStoreRequest {
From a83d4369dfd7c21577c031af5358c75602d0b4d4 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Tue, 1 Sep 2026 10:27:02 +0300
Subject: [PATCH 51/88] fix(api): qualify unread count upsert
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a67aa179-b4af-42fe-9175-4ad080517347
---
api/pkg/repositories/gorm_message_thread_repository.go | 2 +-
api/pkg/repositories/gorm_message_thread_repository_test.go | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go
index 3989cc4e3..c30091b9b 100644
--- a/api/pkg/repositories/gorm_message_thread_repository.go
+++ b/api/pkg/repositories/gorm_message_thread_repository.go
@@ -127,7 +127,7 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread
}
if thread.UnreadCount > 0 {
onConflict.DoUpdates = clause.Assignments(map[string]any{
- "unread_count": gorm.Expr("unread_count + ?", 1),
+ "unread_count": gorm.Expr("message_threads.unread_count + ?", 1),
})
}
diff --git a/api/pkg/repositories/gorm_message_thread_repository_test.go b/api/pkg/repositories/gorm_message_thread_repository_test.go
index c30ed4fc3..5ca7d4a3f 100644
--- a/api/pkg/repositories/gorm_message_thread_repository_test.go
+++ b/api/pkg/repositories/gorm_message_thread_repository_test.go
@@ -99,7 +99,7 @@ func TestMessageThreadStoreIncrementsUnreadCountOnConflict(t *testing.T) {
require.NotEmpty(t, pool.statements)
insert := pool.statements[len(pool.statements)-1]
assert.True(t, strings.HasPrefix(insert.query, `INSERT INTO "message_threads"`))
- assert.Contains(t, insert.query, `ON CONFLICT ("user_id","owner","contact") DO UPDATE SET "unread_count"=unread_count + $`)
+ assert.Contains(t, insert.query, `ON CONFLICT ("user_id","owner","contact") DO UPDATE SET "unread_count"=message_threads.unread_count + $`)
}
func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) {
From 91b3f02d67fe34229f3e82def379b08717d2c331 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Wed, 2 Sep 2026 08:17:30 +0300
Subject: [PATCH 52/88] fix(web): surface API error messages
Preserve server-provided entitlement failures and keep toast notifications above open dialogs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dfc75c3c-5f63-4c2e-8a00-765e45f24c67
---
web/app/components/AppToast.vue | 1 +
web/app/pages/billing/index.vue | 15 +++++--
web/app/pages/messages/index.vue | 7 +++-
web/app/pages/phone-api-keys/index.vue | 19 +++++----
web/app/pages/search-messages/index.vue | 18 +++++++--
web/app/pages/settings/index.vue | 52 ++++++++++++++-----------
6 files changed, 73 insertions(+), 39 deletions(-)
diff --git a/web/app/components/AppToast.vue b/web/app/components/AppToast.vue
index cc62eb097..8cdf5f836 100644
--- a/web/app/components/AppToast.vue
+++ b/web/app/components/AppToast.vue
@@ -17,6 +17,7 @@ const notificationActive = computed({
variant="tonal"
:color="notificationsStore.notification.type"
:timeout="notificationsStore.notification.timeout"
+ :z-index="10000"
>
void): void
@@ -261,9 +261,12 @@ async function deleteMessages() {
type: 'success',
})
selectedIds.value = []
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Error while deleting the selected messages',
+ message: getApiErrorMessage(
+ error,
+ 'Error while deleting the selected messages',
+ ),
type: 'error',
})
} finally {
@@ -300,8 +303,15 @@ async function resendMessages() {
})
selectedIds.value = []
} else if (failed.length === results.length) {
+ const firstFailure = failed[0]
notificationsStore.addNotification({
- message: 'Error while resending the selected messages',
+ message:
+ firstFailure?.status === 'rejected'
+ ? getApiErrorMessage(
+ firstFailure.reason,
+ 'Error while resending the selected messages',
+ )
+ : 'Error while resending the selected messages',
type: 'error',
})
} else {
diff --git a/web/app/pages/settings/index.vue b/web/app/pages/settings/index.vue
index 63a8c2975..f4c312035 100644
--- a/web/app/pages/settings/index.vue
+++ b/web/app/pages/settings/index.vue
@@ -23,7 +23,7 @@ import {
} from 'firebase/auth'
import QRCode from 'qrcode'
import { ErrorMessages } from '~/utils/errors'
-import { toApiError } from '~/utils/api-error'
+import { getApiErrorMessage, toApiError } from '~/utils/api-error'
import type {
EntitiesPhone,
EntitiesWebhook,
@@ -157,9 +157,9 @@ async function rotateApiKey() {
message: 'API Key rotated successfully',
type: 'success',
})
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to rotate API Key',
+ message: getApiErrorMessage(error, 'Failed to rotate API Key'),
type: 'error',
})
} finally {
@@ -198,9 +198,9 @@ async function updateTimezone(timezone: string) {
message: 'Timezone updated successfully',
type: 'success',
})
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to update timezone',
+ message: getApiErrorMessage(error, 'Failed to update timezone'),
type: 'error',
})
}
@@ -298,7 +298,7 @@ async function saveWebhook() {
errorMessages.value = parseErrors(error)
if (errorMessages.value.size() === 0) {
notificationsStore.addNotification({
- message: 'Failed to save webhook',
+ message: getApiErrorMessage(error, 'Failed to save webhook'),
type: 'error',
})
}
@@ -317,9 +317,9 @@ async function deleteWebhook(id: string) {
})
showWebhookEdit.value = false
await loadWebhooks()
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to delete webhook',
+ message: getApiErrorMessage(error, 'Failed to delete webhook'),
type: 'error',
})
} finally {
@@ -406,7 +406,10 @@ async function saveDiscord() {
errorMessages.value = parseErrors(error)
if (errorMessages.value.size() === 0) {
notificationsStore.addNotification({
- message: 'Failed to save discord integration',
+ message: getApiErrorMessage(
+ error,
+ 'Failed to save discord integration',
+ ),
type: 'error',
})
}
@@ -425,9 +428,12 @@ async function deleteDiscord(id: string) {
})
showDiscordEdit.value = false
await loadDiscordIntegrations()
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to delete discord integration',
+ message: getApiErrorMessage(
+ error,
+ 'Failed to delete discord integration',
+ ),
type: 'error',
})
} finally {
@@ -472,9 +478,9 @@ async function deletePhone(phoneId: string) {
})
showPhoneEdit.value = false
activePhone.value = null
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to delete phone',
+ message: getApiErrorMessage(error, 'Failed to delete phone'),
type: 'error',
})
} finally {
@@ -675,10 +681,10 @@ async function saveSchedule() {
await loadSendSchedules()
} catch (error: unknown) {
errorMessages.value = parseErrors(error)
- if (errorMessages.value.size() != 0) {
+ if (errorMessages.value.size() === 0) {
notificationsStore.addNotification({
type: 'error',
- message: 'Failed to save send schedule',
+ message: getApiErrorMessage(error, 'Failed to save send schedule'),
})
}
} finally {
@@ -702,10 +708,10 @@ async function deleteSchedule() {
showScheduleDelete.value = false
showScheduleEdit.value = false
await loadSendSchedules()
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
type: 'error',
- message: 'Failed to delete send schedule',
+ message: getApiErrorMessage(error, 'Failed to delete send schedule'),
})
} finally {
savingSchedule.value = false
@@ -746,9 +752,9 @@ async function saveEmailNotifications() {
type: 'success',
})
syncEmailNotifications()
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message: 'Failed to save email notifications',
+ message: getApiErrorMessage(error, 'Failed to save email notifications'),
type: 'error',
})
} finally {
@@ -780,10 +786,12 @@ async function deleteUserAccount() {
message: 'You have successfully logged out',
})
await router.push({ name: 'index' })
- } catch {
+ } catch (error: unknown) {
notificationsStore.addNotification({
- message:
- 'We ran into an internal error while deleteing your account please contact us.',
+ message: getApiErrorMessage(
+ error,
+ 'We ran into an internal error while deleting your account. Please contact us.',
+ ),
type: 'error',
})
} finally {
From 5bca4ca2159584eeb527b6cc99d26d9861168659 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Wed, 2 Sep 2026 08:25:19 +0300
Subject: [PATCH 53/88] style: set max width as 50% in message thread page
---
web/app/pages/threads/[id]/index.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue
index 87f4e51ed..ef1833673 100644
--- a/web/app/pages/threads/[id]/index.vue
+++ b/web/app/pages/threads/[id]/index.vue
@@ -450,7 +450,7 @@ onBeforeUnmount(() => {
-
+
Date: Wed, 2 Sep 2026 08:36:37 +0300
Subject: [PATCH 54/88] perf(web): speed up thread switching
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
web/app/pages/threads/[id]/index.vue | 67 +++++++++++++++++++---------
1 file changed, 46 insertions(+), 21 deletions(-)
diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue
index ef1833673..0bb52b7de 100644
--- a/web/app/pages/threads/[id]/index.vue
+++ b/web/app/pages/threads/[id]/index.vue
@@ -52,6 +52,9 @@ const selectedMenuItem = ref(-1)
const contactDialog = ref(false)
const messageBody = ref(null)
const form = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null)
+let messageRequestId = 0
+
+threadsStore.setThreadId(route.params.id as string)
const formMessageRules = [
(v: string) =>
@@ -128,8 +131,10 @@ function scrollToElement() {
hideMessages.value = false
}
-async function resetCurrentThreadUnreadCount(force = false) {
- const threadId = route.params.id as string
+async function resetCurrentThreadUnreadCount(
+ force = false,
+ threadId = route.params.id as string,
+) {
try {
await threadsStore.resetThreadUnreadCount(threadId, force)
} catch (error) {
@@ -154,36 +159,46 @@ async function handleInboundMessage() {
}
}
-function loadMessages(hide = true, resetUnreadCount = true) {
+async function loadMessages(hide = true, resetUnreadCount = true) {
+ const requestId = ++messageRequestId
loadingMessages.value = true
const threadId = route.params.id as string
- if (resetUnreadCount) void resetCurrentThreadUnreadCount()
- threadsStore
- .loadThreadMessages(threadId)
- .then((response: EntitiesMessage[]) => {
- messages.value = [...response].reverse()
- })
- .finally(() => {
- setTimeout(() => {
- loadingMessages.value = false
- }, 1100)
- })
hideMessages.value = hide
- setTimeout(() => {
+
+ if (resetUnreadCount) {
+ void resetCurrentThreadUnreadCount(false, threadId)
+ }
+
+ try {
+ const response = await threadsStore.loadThreadMessages(threadId)
+ if (requestId !== messageRequestId) return
+
+ messages.value = [...response].reverse()
+ await nextTick()
+ if (requestId !== messageRequestId) return
+
scrollToElement()
- }, 950)
+ } finally {
+ if (requestId === messageRequestId) {
+ loadingMessages.value = false
+ }
+ }
}
async function loadData() {
- await authStore.loadUser()
- await phonesStore.loadPhones()
- await threadsStore.loadThreads()
+ const threadId = route.params.id as string
- if (!threadsStore.hasThreadId(route.params.id as string)) {
+ if (!threadsStore.hasThreadId(threadId)) {
+ if (!authStore.user) await authStore.loadUser()
+ await phonesStore.loadPhones()
+ await threadsStore.loadThreads()
+ }
+
+ if (!threadsStore.hasThreadId(threadId)) {
await router.push('/threads')
return
}
- loadMessages()
+ await loadMessages()
}
async function archiveThread() {
@@ -282,6 +297,16 @@ onMounted(async () => {
})
})
+watch(
+ () => route.params.id as string,
+ (threadId, previousThreadId) => {
+ if (!previousThreadId || threadId === previousThreadId) return
+
+ threadsStore.setThreadId(threadId)
+ void loadMessages()
+ },
+)
+
onBeforeUnmount(() => {
if (webhookChannel) webhookChannel.unsubscribe()
})
From d5774ca57eb14d277354e5ddab8d23d0ae6973ce Mon Sep 17 00:00:00 2001
From: Acho Arnold
Date: Fri, 4 Sep 2026 08:15:38 +0300
Subject: [PATCH 55/88] feat(api): support URL-backed phone gateways (#989)
* docs(api): design URL-backed notifications
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(api): plan adapter gateway integration
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: ignore linked worktrees
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(api): classify phone notification tokens
Add Phone.NotificationTransport and Phone.NotificationURL helpers with entity-level validation for FCM tokens and public HTTPS endpoints.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): classify phone notification tokens
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): tighten phone token classification
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): validate adapter endpoints
* refactor(api): dispatch gateway notifications
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): send notifications to adapters
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): harden notification TLS transport
Clear TLS dial hooks and ServerName so policy validation and hostname verification cannot be bypassed. Restrict retries to standard 5xx statuses.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): route phone gateway wake-ups
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* feat(api): enable URL-backed phone gateways
Validate adapter URLs with the same cached endpoint policy used by the secure notification dialer. Ignore private-host allowlists outside local environments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): preserve secured telemetry transport
Keep trusted HTTP middleware around the endpoint-policy transport so
notification delivery retains telemetry and connection hardening.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): restrict trusted HTTP transports
Only service-created marked transports may preserve middleware. Opaque caller transports are replaced with policy-hardened transports so they cannot bypass SSRF dialing and TLS controls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* test(api): cover URL-backed phone gateways
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): harden adapter notifications
Enforce transport provenance and per-attempt endpoint checks to close
policy bypass and DNS timeout gaps.
Redact callback tokens from logs and telemetry. Encode TTLs with
protobuf JSON duration syntax.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): relax adapter URL validation
Allow standard URL user information while retaining HTTPS and SSRF checks.
Construct endpoint policies on demand and share one only within each HTTP
sender graph so secured transport identity remains intact.
Keep FCM-token examples opaque to preserve the existing API guidance.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* refactor(api): simplify adapter delivery
Use the existing OpenTelemetry HTTP transport and retry-go delivery pattern.
Remove endpoint network policy and custom dialing while preserving callback
URL redaction in telemetry.
Reuse EventDispatcher directly and clarify the phone transport dispatcher
name.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* refactor(api): use standard logging paths
Use the same OpenTelemetry HTTP client as webhooks and preserve default
request, database, and notification logging without feature-specific
redaction.
Keep GORM query variables in traces.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* refactor(api): focus notification delivery
Reuse Firebase messages across transports and initialize one reusable retry
policy per HTTP sender. Split phone transport dispatch into its own component
and rely on the existing HTTP instrumentation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* refactor(api): map notification clients
Reuse the existing FCMClient contract across Firebase and HTTP so new phone transports only require DI map configuration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
* fix(api): restore phone request logs
Undo request-log scrubbing so phone handler failures retain the original URL and complete request parameters.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2884b08e-2828-4b50-a9e6-702dce51ec0d
---
.github/workflows/api.yml | 3 +
.gitignore | 2 +
api/docs/docs.go | 6 +-
api/docs/swagger.json | 6 +-
api/docs/swagger.yaml | 13 +-
api/pkg/di/container.go | 20 +-
api/pkg/di/container_test.go | 36 +
api/pkg/entities/phone.go | 71 +
api/pkg/entities/phone_notification.go | 2 +-
api/pkg/entities/phone_test.go | 85 +
api/pkg/handlers/phone_handler.go | 4 +-
.../http_request_logger_middleware.go | 2 +-
api/pkg/requests/phone_fcm_token_request.go | 3 +-
api/pkg/requests/phone_update_request.go | 1 +
api/pkg/services/fcm_client.go | 4 +-
api/pkg/services/http_notification_sender.go | 230 ++
.../services/http_notification_sender_test.go | 426 ++++
.../services/phone_notification_service.go | 101 +-
.../phone_notification_service_test.go | 325 +++
api/pkg/validators/phone_handler_validator.go | 29 +-
.../phone_handler_validator_test.go | 144 ++
...2-url-backed-phone-notification-adapter.md | 1863 +++++++++++++++++
...acked-phone-notification-adapter-design.md | 514 +++++
tests/README.md | 278 +--
tests/adapter-emulator/Dockerfile | 12 +
tests/adapter-emulator/api_client.go | 155 ++
tests/adapter-emulator/control_handler.go | 124 ++
tests/adapter-emulator/emulator.go | 119 ++
tests/adapter-emulator/emulator_test.go | 448 ++++
tests/adapter-emulator/go.mod | 3 +
tests/adapter-emulator/main.go | 85 +
.../adapter-emulator/notification_handler.go | 96 +
tests/adapter_integration_test.go | 91 +
tests/docker-compose.yml | 22 +
tests/generate-adapter-certificates.sh | 36 +
tests/helpers_test.go | 245 +++
36 files changed, 5435 insertions(+), 169 deletions(-)
create mode 100644 api/pkg/di/container_test.go
create mode 100644 api/pkg/entities/phone_test.go
create mode 100644 api/pkg/services/http_notification_sender.go
create mode 100644 api/pkg/services/http_notification_sender_test.go
create mode 100644 api/pkg/services/phone_notification_service_test.go
create mode 100644 api/pkg/validators/phone_handler_validator_test.go
create mode 100644 docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
create mode 100644 docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md
create mode 100644 tests/adapter-emulator/Dockerfile
create mode 100644 tests/adapter-emulator/api_client.go
create mode 100644 tests/adapter-emulator/control_handler.go
create mode 100644 tests/adapter-emulator/emulator.go
create mode 100644 tests/adapter-emulator/emulator_test.go
create mode 100644 tests/adapter-emulator/go.mod
create mode 100644 tests/adapter-emulator/main.go
create mode 100644 tests/adapter-emulator/notification_handler.go
create mode 100644 tests/adapter_integration_test.go
create mode 100644 tests/generate-adapter-certificates.sh
diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml
index f9b756ed6..7d3a8a4ea 100644
--- a/.github/workflows/api.yml
+++ b/.github/workflows/api.yml
@@ -30,6 +30,9 @@ jobs:
bash tests/generate-firebase-credentials.sh tests/firebase-credentials.json
echo "FIREBASE_CREDENTIALS=$(jq -c . tests/firebase-credentials.json)" >> $GITHUB_ENV
+ - name: Generate adapter certificates
+ run: bash tests/generate-adapter-certificates.sh tests/certs
+
- name: Start Services
working-directory: ./tests
run: docker compose up -d --build
diff --git a/.gitignore b/.gitignore
index 8f4caf4b5..3bd4ee820 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ android/app/debug/
android/app/release/
tests/firebase-credentials.json
+tests/certs/
tests/emulator/emulator.exe
SECURITY_AUDIT_REPORT.md
@@ -16,3 +17,4 @@ SECURITY_AUDIT_REPORT.md
.output
.agents/
skills-lock.json
+.worktrees/
diff --git a/api/docs/docs.go b/api/docs/docs.go
index 96cf0e559..eb72ac76d 100644
--- a/api/docs/docs.go
+++ b/api/docs/docs.go
@@ -2420,7 +2420,7 @@ const docTemplate = `{
"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'",
+ "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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.",
"consumes": [
"application/json"
],
@@ -2483,7 +2483,7 @@ const docTemplate = `{
"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'",
+ "description": "Updates the FCM token or adapter callback URL 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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.",
"consumes": [
"application/json"
],
@@ -4947,6 +4947,7 @@ const docTemplate = `{
],
"properties": {
"fcm_token": {
+ "description": "FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.",
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."
},
@@ -4975,6 +4976,7 @@ const docTemplate = `{
],
"properties": {
"fcm_token": {
+ "description": "FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.",
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."
},
diff --git a/api/docs/swagger.json b/api/docs/swagger.json
index 4926b5e55..59c5637ba 100644
--- a/api/docs/swagger.json
+++ b/api/docs/swagger.json
@@ -2417,7 +2417,7 @@
"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'",
+ "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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.",
"consumes": [
"application/json"
],
@@ -2480,7 +2480,7 @@
"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'",
+ "description": "Updates the FCM token or adapter callback URL 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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.",
"consumes": [
"application/json"
],
@@ -4944,6 +4944,7 @@
],
"properties": {
"fcm_token": {
+ "description": "FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.",
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."
},
@@ -4972,6 +4973,7 @@
],
"properties": {
"fcm_token": {
+ "description": "FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.",
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."
},
diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml
index 9cfefbf56..6dc240216 100644
--- a/api/docs/swagger.yaml
+++ b/api/docs/swagger.yaml
@@ -971,6 +971,8 @@ definitions:
requests.PhoneFCMToken:
properties:
fcm_token:
+ description: FcmToken is either a Firebase registration token or a public
+ HTTPS adapter callback URL.
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd.....
type: string
phone_number:
@@ -989,6 +991,8 @@ definitions:
requests.PhoneUpsert:
properties:
fcm_token:
+ description: FcmToken is either a Firebase registration token or a public
+ HTTPS adapter callback URL.
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd.....
type: string
max_send_attempts:
@@ -3337,7 +3341,8 @@ paths:
consumes:
- 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'
+ does not exist, a new one will be created. Think of this method like an 'upsert'.
+ URL-backed phone gateways receive FCM-compatible HTTP wake-ups.
parameters:
- description: Payload of new phone number.
in: body
@@ -3417,8 +3422,10 @@ paths:
put:
consumes:
- 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'
+ description: Updates the FCM token or adapter callback URL 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'. URL-backed phone gateways receive FCM-compatible
+ HTTP wake-ups.
parameters:
- description: Payload of new FCM token.
in: body
diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go
index ebe57662e..8d9e778d4 100644
--- a/api/pkg/di/container.go
+++ b/api/pkg/di/container.go
@@ -565,6 +565,24 @@ func (container *Container) FCMClient() services.FCMClient {
return services.NewFirebaseFCMClient(messagingClient)
}
+// NotificationHTTPClient creates the OpenTelemetry-instrumented client for phone notification adapters.
+func (container *Container) NotificationHTTPClient() *http.Client {
+ return &http.Client{
+ Transport: container.HTTPRoundTripperWithoutRetry("phone_notification_http"),
+ }
+}
+
+// PhoneNotificationClients creates notification clients keyed by phone transport.
+func (container *Container) PhoneNotificationClients() map[entities.NotificationTransport]services.FCMClient {
+ return map[entities.NotificationTransport]services.FCMClient{
+ entities.NotificationTransportFCM: container.FCMClient(),
+ entities.NotificationTransportHTTP: services.NewHTTPNotificationSender(
+ container.Logger(),
+ container.NotificationHTTPClient(),
+ ),
+ }
+}
+
// FirebaseCredentials returns firebase credentials as bytes.
func (container *Container) FirebaseCredentials() []byte {
container.logger.Debug("creating firebase credentials")
@@ -1715,7 +1733,7 @@ func (container *Container) NotificationService() (service *services.PhoneNotifi
return services.NewNotificationService(
container.Logger(),
container.Tracer(),
- container.FCMClient(),
+ container.PhoneNotificationClients(),
container.PhoneRepository(),
container.PhoneNotificationRepository(),
container.MessageSendScheduleRepository(),
diff --git a/api/pkg/di/container_test.go b/api/pkg/di/container_test.go
new file mode 100644
index 000000000..611708d31
--- /dev/null
+++ b/api/pkg/di/container_test.go
@@ -0,0 +1,36 @@
+package di
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNotificationHTTPClientUsesOTelRoundTripperWithoutRetries(t *testing.T) {
+ t.Setenv("ENV", "local")
+ client := NewLiteContainer().NotificationHTTPClient()
+
+ assert.Zero(t, client.Timeout)
+ assert.Equal(t, "*otelroundtripper.otelRoundTripper", reflect.TypeOf(client.Transport).String())
+ assert.Nil(t, client.CheckRedirect)
+}
+
+func TestPhoneNotificationClientsMapsConfiguredTransports(t *testing.T) {
+ t.Setenv("ENV", "local")
+ t.Setenv("FCM_ENDPOINT", "http://localhost")
+
+ clients := NewLiteContainer().PhoneNotificationClients()
+
+ require.Len(t, clients, 2)
+ assert.IsType(t, &services.EmulatorFCMClient{}, clients[entities.NotificationTransportFCM])
+ httpSender, ok := clients[entities.NotificationTransportHTTP].(*services.HTTPNotificationSender)
+ require.True(t, ok)
+ client := reflect.ValueOf(httpSender).Elem().FieldByName("client").Elem()
+ transport := client.FieldByName("Transport").Elem()
+
+ assert.Equal(t, "*otelroundtripper.otelRoundTripper", transport.Type().String())
+}
diff --git a/api/pkg/entities/phone.go b/api/pkg/entities/phone.go
index 4f3c33f28..36be28aeb 100644
--- a/api/pkg/entities/phone.go
+++ b/api/pkg/entities/phone.go
@@ -1,8 +1,11 @@
package entities
import (
+ "net/url"
+ "strings"
"time"
+ "github.com/NdoleStudio/stacktrace"
"github.com/google/uuid"
)
@@ -31,6 +34,25 @@ type Phone struct {
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
}
+// NotificationTransport identifies how a phone receives wake-up notifications.
+type NotificationTransport string
+
+const (
+ // NotificationTransportFCM sends notifications through Firebase.
+ NotificationTransportFCM NotificationTransport = "fcm"
+ // NotificationTransportHTTP sends notifications to an HTTPS endpoint.
+ NotificationTransportHTTP NotificationTransport = "http"
+)
+
+func isNotificationURLCandidate(token string) bool {
+ lower := strings.ToLower(token)
+
+ return strings.Contains(token, "://") ||
+ strings.HasPrefix(lower, "http:") ||
+ strings.HasPrefix(lower, "https:") ||
+ strings.HasPrefix(lower, "ftp:")
+}
+
// MessageExpirationDuration returns the message expiration as time.Duration
func (phone *Phone) MessageExpirationDuration() time.Duration {
return time.Duration(int(phone.MessageExpirationSecondsSanitized())) * time.Second
@@ -51,3 +73,52 @@ func (phone *Phone) MaxSendAttemptsSanitized() uint {
}
return phone.MaxSendAttempts
}
+
+// NotificationTransport returns the transport encoded by FcmToken.
+func (phone *Phone) NotificationTransport() (NotificationTransport, error) {
+ if phone == nil || phone.FcmToken == nil {
+ return "", stacktrace.NewErrorf("phone has no notification token")
+ }
+
+ token := strings.TrimSpace(*phone.FcmToken)
+ if token == "" {
+ return "", stacktrace.NewErrorf("phone has no notification token")
+ }
+
+ if !isNotificationURLCandidate(token) {
+ return NotificationTransportFCM, nil
+ }
+
+ endpoint, err := url.Parse(token)
+ if err != nil {
+ return "", stacktrace.NewError("invalid notification URL")
+ }
+
+ if !strings.EqualFold(endpoint.Scheme, "https") {
+ return "", stacktrace.NewErrorf("notification URL must use https")
+ }
+ if endpoint.Hostname() == "" {
+ return "", stacktrace.NewErrorf("notification URL must include a hostname")
+ }
+
+ return NotificationTransportHTTP, nil
+}
+
+// NotificationURL returns the parsed endpoint for an HTTP notification token.
+func (phone *Phone) NotificationURL() (*url.URL, error) {
+ transport, err := phone.NotificationTransport()
+ if err != nil {
+ return nil, err
+ }
+
+ if transport != NotificationTransportHTTP {
+ return nil, stacktrace.NewErrorf("phone notification transport is [%s], not HTTP", transport)
+ }
+
+ endpoint, err := url.Parse(strings.TrimSpace(*phone.FcmToken))
+ if err != nil {
+ return nil, stacktrace.NewError("cannot parse notification URL")
+ }
+
+ return endpoint, nil
+}
diff --git a/api/pkg/entities/phone_notification.go b/api/pkg/entities/phone_notification.go
index 8720579ae..ab3fc5908 100644
--- a/api/pkg/entities/phone_notification.go
+++ b/api/pkg/entities/phone_notification.go
@@ -18,7 +18,7 @@ const (
// PhoneNotificationStatus is the status of a phone notification
type PhoneNotificationStatus string
-// PhoneNotification represents an FCM notification to a mobile phone
+// PhoneNotification represents a scheduled wake-up notification for a phone gateway.
type PhoneNotification struct {
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;"`
MessageID uuid.UUID `json:"message_id"`
diff --git a/api/pkg/entities/phone_test.go b/api/pkg/entities/phone_test.go
new file mode 100644
index 000000000..886d31f6d
--- /dev/null
+++ b/api/pkg/entities/phone_test.go
@@ -0,0 +1,85 @@
+package entities
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func stringPointer(value string) *string {
+ return &value
+}
+
+func TestPhoneNotificationTransport(t *testing.T) {
+ tests := []struct {
+ name string
+ token *string
+ transport NotificationTransport
+ hasError bool
+ }{
+ {name: "firebase token with colon", token: stringPointer("fcm-token:value"), transport: NotificationTransportFCM},
+ {name: "opaque token with slash", token: stringPointer("projects/alpha/messages/123"), transport: NotificationTransportFCM},
+ {name: "public https url", token: stringPointer("https://adapter.example.com/notify"), transport: NotificationTransportHTTP},
+ {name: "missing token", token: nil, hasError: true},
+ {name: "empty token", token: stringPointer(" "), hasError: true},
+ {name: "http url", token: stringPointer("http://adapter.example.com/notify"), hasError: true},
+ {name: "ftp url", token: stringPointer("ftp://adapter.example.com/notify"), hasError: true},
+ {name: "scheme-like https token", token: stringPointer("https:adapter.example.com"), hasError: true},
+ {name: "scheme-like http token", token: stringPointer("http:foo"), hasError: true},
+ {name: "scheme-like ftp token", token: stringPointer("ftp:foo"), hasError: true},
+ {name: "missing host", token: stringPointer("https:///notify"), hasError: true},
+ {name: "embedded user information", token: stringPointer("https://user:password@adapter.example.com/notify"), transport: NotificationTransportHTTP},
+ {name: "malformed url", token: stringPointer("https://[::1"), hasError: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ phone := &Phone{FcmToken: test.token}
+
+ transport, err := phone.NotificationTransport()
+
+ if test.hasError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.transport, transport)
+ })
+ }
+}
+
+func TestPhoneNotificationURL(t *testing.T) {
+ phone := &Phone{FcmToken: stringPointer("https://user:password@adapter.example.com/notify?tenant=42")}
+
+ endpoint, err := phone.NotificationURL()
+
+ require.NoError(t, err)
+ assert.Equal(t, "https", endpoint.Scheme)
+ assert.Equal(t, "user", endpoint.User.Username())
+ password, hasPassword := endpoint.User.Password()
+ assert.True(t, hasPassword)
+ assert.Equal(t, "password", password)
+ assert.Equal(t, "adapter.example.com", endpoint.Hostname())
+ assert.Equal(t, "/notify", endpoint.Path)
+ assert.Equal(t, "tenant=42", endpoint.RawQuery)
+}
+
+func TestPhoneNotificationURLRejectsFCMToken(t *testing.T) {
+ phone := &Phone{FcmToken: stringPointer("fcm-token:value")}
+
+ _, err := phone.NotificationURL()
+
+ require.Error(t, err)
+}
+
+func TestPhoneNotificationTransportDoesNotExposeMalformedToken(t *testing.T) {
+ token := "https://[::1/secret?token=customer-secret"
+ phone := &Phone{FcmToken: &token}
+
+ _, err := phone.NotificationTransport()
+
+ require.Error(t, err)
+ assert.NotContains(t, err.Error(), token)
+ assert.NotContains(t, err.Error(), "customer-secret")
+}
diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go
index 55380ed91..c81ef4ddd 100644
--- a/api/pkg/handlers/phone_handler.go
+++ b/api/pkg/handlers/phone_handler.go
@@ -94,7 +94,7 @@ func (h *PhoneHandler) Index(c fiber.Ctx) error {
// Upsert a phone
// @Summary Upsert Phone
-// @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'
+// @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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.
// @Security ApiKeyAuth
// @Tags Phones
// @Accept json
@@ -172,7 +172,7 @@ func (h *PhoneHandler) Delete(c fiber.Ctx) error {
// UpsertFCMToken upserts the FCM token of a phone
// @Summary Upserts the FCM token of a phone
-// @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'
+// @Description Updates the FCM token or adapter callback URL 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'. URL-backed phone gateways receive FCM-compatible HTTP wake-ups.
// @Security ApiKeyAuth
// @Tags Phones
// @Accept json
diff --git a/api/pkg/middlewares/http_request_logger_middleware.go b/api/pkg/middlewares/http_request_logger_middleware.go
index e8de42e2a..cd11c6009 100644
--- a/api/pkg/middlewares/http_request_logger_middleware.go
+++ b/api/pkg/middlewares/http_request_logger_middleware.go
@@ -24,7 +24,7 @@ func HTTPRequestLogger(tracer telemetry.Tracer, logger telemetry.Logger) fiber.H
statusCode := c.Response().StatusCode()
span.AddEvent(fmt.Sprintf("finished handling request with traceID: [%s], statusCode: [%d]", span.SpanContext().TraceID().String(), statusCode))
if statusCode >= 300 && len(c.Request().Body()) > 0 && !slices.Contains([]int{401, 402}, statusCode) {
- ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewErrorf("http.status [%d], body [%s]", statusCode, string(c.Request().Body())))
+ ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewErrorf("http.status [%d], body [%s]", statusCode, c.Request().Body()))
}
return response
diff --git a/api/pkg/requests/phone_fcm_token_request.go b/api/pkg/requests/phone_fcm_token_request.go
index dde935b53..bdd3ab1a6 100644
--- a/api/pkg/requests/phone_fcm_token_request.go
+++ b/api/pkg/requests/phone_fcm_token_request.go
@@ -13,7 +13,8 @@ import (
type PhoneFCMToken struct {
request
PhoneNumber string `json:"phone_number" example:"[+18005550199]"`
- FcmToken string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
+ // FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.
+ FcmToken string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
// SIM is the SIM slot of the phone in case the phone has more than 1 SIM slot
SIM string `json:"sim" example:"SIM1"`
}
diff --git a/api/pkg/requests/phone_update_request.go b/api/pkg/requests/phone_update_request.go
index 96b2882e1..cedd51a0d 100644
--- a/api/pkg/requests/phone_update_request.go
+++ b/api/pkg/requests/phone_update_request.go
@@ -25,6 +25,7 @@ type PhoneUpsert struct {
// MaxSendAttempts is the number of attempts when sending an SMS message to handle the case where the phone is offline.
MaxSendAttempts uint `json:"max_send_attempts" example:"2"`
+ // FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.
FcmToken string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
MissedCallAutoReply *string `json:"missed_call_auto_reply" example:"e.g. This phone cannot receive calls. Please send an SMS instead."`
diff --git a/api/pkg/services/fcm_client.go b/api/pkg/services/fcm_client.go
index 4e56f3167..6b60b8247 100644
--- a/api/pkg/services/fcm_client.go
+++ b/api/pkg/services/fcm_client.go
@@ -6,9 +6,9 @@ import (
"firebase.google.com/go/messaging"
)
-// FCMClient is the interface for sending Firebase Cloud Messaging notifications.
+// FCMClient sends Firebase-compatible messages through a phone notification transport.
type FCMClient interface {
- // Send sends a message via FCM and returns the message name on success.
+ // Send sends a message and returns the transport's delivery identifier on success.
Send(ctx context.Context, message *messaging.Message) (string, error)
}
diff --git a/api/pkg/services/http_notification_sender.go b/api/pkg/services/http_notification_sender.go
new file mode 100644
index 000000000..9477611af
--- /dev/null
+++ b/api/pkg/services/http_notification_sender.go
@@ -0,0 +1,230 @@
+package services
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+
+ "firebase.google.com/go/messaging"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/NdoleStudio/stacktrace"
+ "github.com/avast/retry-go/v5"
+)
+
+const (
+ maxNotificationResponseDiscardBytes = 4 * 1024
+ notificationHTTPAttempts = 3
+ notificationHTTPTimeout = 5 * time.Second
+ notificationHTTPRetryDelay = 250 * time.Millisecond
+)
+
+// HTTPNotificationSender sends FCM-compatible gateway notifications to HTTPS adapters.
+type HTTPNotificationSender struct {
+ logger telemetry.Logger
+ client *http.Client
+ retrier *retry.Retrier
+ timeout time.Duration
+}
+
+var _ FCMClient = (*HTTPNotificationSender)(nil)
+
+// NewHTTPNotificationSender creates an HTTP notification sender.
+func NewHTTPNotificationSender(
+ logger telemetry.Logger,
+ client *http.Client,
+) *HTTPNotificationSender {
+ return newHTTPNotificationSenderWithRetrier(
+ logger,
+ client,
+ newHTTPNotificationRetrier(notificationHTTPRetryDelay),
+ )
+}
+
+func newHTTPNotificationSenderWithRetrier(
+ logger telemetry.Logger,
+ client *http.Client,
+ retrier *retry.Retrier,
+) *HTTPNotificationSender {
+ return &HTTPNotificationSender{
+ logger: logger,
+ client: client,
+ retrier: retrier,
+ timeout: notificationHTTPTimeout,
+ }
+}
+
+// Send delivers a notification to an HTTPS adapter. A successful response only accepts wake-up delivery.
+func (sender *HTTPNotificationSender) Send(
+ ctx context.Context,
+ message *messaging.Message,
+) (string, error) {
+ if message == nil {
+ return "", sender.notificationError("", "notification message is nil")
+ }
+
+ endpoint, err := url.Parse(message.Token)
+ if err != nil {
+ return "", sender.notificationError("", "cannot parse notification endpoint")
+ }
+ hostname := endpoint.Hostname()
+
+ body, err := encodeHTTPNotificationPayload(message)
+ if err != nil {
+ return "", sender.notificationError(hostname, "cannot encode notification")
+ }
+
+ err = sender.retrier.Do(func() error {
+ return sender.deliver(ctx, endpoint, body)
+ })
+ if err == nil {
+ return "http/success", nil
+ }
+ if ctx.Err() != nil {
+ return "", sender.notificationError(hostname, "notification request cancelled")
+ }
+
+ return "", sender.notificationError(hostname, "notification request failed")
+}
+
+func encodeHTTPNotificationPayload(message *messaging.Message) ([]byte, error) {
+ return json.Marshal(map[string]any{
+ "message": message,
+ })
+}
+
+func (sender *HTTPNotificationSender) deliver(
+ ctx context.Context,
+ endpoint *url.URL,
+ body []byte,
+) error {
+ if err := ctx.Err(); err != nil {
+ return terminalNotificationRequestError{cause: err}
+ }
+
+ attemptCtx, cancel := context.WithTimeout(ctx, sender.timeout)
+ defer cancel()
+
+ request, err := createHTTPNotificationRequest(attemptCtx, endpoint, body)
+ if err != nil {
+ return terminalNotificationRequestError{cause: err}
+ }
+
+ err = sender.sendAttempt(request)
+ if ctx.Err() != nil {
+ return terminalNotificationRequestError{cause: ctx.Err()}
+ }
+ return err
+}
+
+func createHTTPNotificationRequest(
+ ctx context.Context,
+ endpoint *url.URL,
+ body []byte,
+) (*http.Request, error) {
+ request, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodPost,
+ endpoint.String(),
+ bytes.NewReader(body),
+ )
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Set("Content-Type", "application/json")
+ return request, nil
+}
+
+func (sender *HTTPNotificationSender) sendAttempt(request *http.Request) error {
+ response, err := sender.client.Do(request)
+ if err != nil {
+ return err
+ }
+ if response.Body != nil {
+ _, _ = io.CopyN(io.Discard, response.Body, maxNotificationResponseDiscardBytes)
+ _ = response.Body.Close()
+ }
+ if response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices {
+ return nil
+ }
+ if isRetryableNotificationStatus(response.StatusCode) {
+ return retryableNotificationStatusError{statusCode: response.StatusCode}
+ }
+ return terminalNotificationStatusError{statusCode: response.StatusCode}
+}
+
+func newHTTPNotificationRetrier(delay time.Duration) *retry.Retrier {
+ return retry.New(
+ retry.Attempts(notificationHTTPAttempts),
+ retry.Delay(delay),
+ retry.DelayType(retry.BackOffDelay),
+ retry.LastErrorOnly(true),
+ retry.RetryIf(isRetryableNotificationError),
+ )
+}
+
+func (sender *HTTPNotificationSender) notificationError(hostname string, message string) error {
+ if hostname == "" {
+ hostname = "unknown"
+ }
+ err := stacktrace.Propagatef(stacktrace.NewErrorf("%s", message), "cannot send notification to [%s]", hostname)
+ if sender.logger != nil {
+ sender.logger.Error(err)
+ }
+ return err
+}
+
+func isRetryableNotificationStatus(statusCode int) bool {
+ return statusCode == http.StatusRequestTimeout ||
+ statusCode == http.StatusTooManyRequests ||
+ (statusCode >= http.StatusInternalServerError && statusCode < 600)
+}
+
+type retryableNotificationStatusError struct {
+ statusCode int
+}
+
+func (error retryableNotificationStatusError) Error() string {
+ return http.StatusText(error.statusCode)
+}
+
+type terminalNotificationStatusError struct {
+ statusCode int
+}
+
+func (error terminalNotificationStatusError) Error() string {
+ return http.StatusText(error.statusCode)
+}
+
+type terminalNotificationRequestError struct {
+ cause error
+}
+
+func (notificationError terminalNotificationRequestError) Error() string {
+ return notificationError.cause.Error()
+}
+
+func (notificationError terminalNotificationRequestError) Unwrap() error {
+ return notificationError.cause
+}
+
+func isRetryableNotificationError(err error) bool {
+ if err == nil || isTerminalNotificationError(err) {
+ return false
+ }
+ return true
+}
+
+func isTerminalNotificationError(err error) bool {
+ var statusError terminalNotificationStatusError
+ if errors.As(err, &statusError) {
+ return true
+ }
+
+ var requestError terminalNotificationRequestError
+ return errors.As(err, &requestError)
+}
diff --git a/api/pkg/services/http_notification_sender_test.go b/api/pkg/services/http_notification_sender_test.go
new file mode 100644
index 000000000..075f99c87
--- /dev/null
+++ b/api/pkg/services/http_notification_sender_test.go
@@ -0,0 +1,426 @@
+package services
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "testing"
+ "time"
+
+ "firebase.google.com/go/messaging"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type roundTripFunc func(*http.Request) (*http.Response, error)
+
+func (roundTrip roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
+ return roundTrip(request)
+}
+
+type httpNotificationPayload struct {
+ Message struct {
+ Token string `json:"token"`
+ Data map[string]string `json:"data"`
+ Android struct {
+ Priority string `json:"priority"`
+ TTL string `json:"ttl,omitempty"`
+ } `json:"android"`
+ } `json:"message"`
+}
+
+func TestHTTPNotificationSenderSendsFCMCompatiblePayload(t *testing.T) {
+ ttl := 10 * time.Minute
+ message := &messaging.Message{
+ Token: "https://adapter.example.com/notify",
+ Data: map[string]string{"KEY_MESSAGE_ID": "message-1"},
+ Android: &messaging.AndroidConfig{
+ Priority: "high",
+ TTL: &ttl,
+ },
+ }
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ assert.Equal(t, http.MethodPost, request.Method)
+ assert.Equal(t, "application/json", request.Header.Get("Content-Type"))
+
+ var payload httpNotificationPayload
+ require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
+ assert.Equal(t, "https://adapter.example.com/notify", request.URL.String())
+ assert.Equal(t, "https://adapter.example.com/notify", payload.Message.Token)
+ assert.Equal(t, map[string]string{"KEY_MESSAGE_ID": "message-1"}, payload.Message.Data)
+ assert.Equal(t, "high", payload.Message.Android.Priority)
+ assert.Equal(t, "600s", payload.Message.Android.TTL)
+
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+
+ result, err := sender.Send(context.Background(), message)
+
+ require.NoError(t, err)
+ assert.Equal(t, "http/success", result)
+}
+
+func TestHTTPNotificationSenderRetriesOnlyTransientFailures(t *testing.T) {
+ tests := []struct {
+ name string
+ outcomes []roundTripOutcome
+ wantCalls int
+ wantErr bool
+ }{
+ {
+ name: "network error then accepted",
+ outcomes: []roundTripOutcome{
+ {err: errors.New("connection reset")},
+ {statusCode: http.StatusAccepted},
+ },
+ wantCalls: 2,
+ },
+ {
+ name: "request timeout then success",
+ outcomes: []roundTripOutcome{
+ {statusCode: http.StatusRequestTimeout},
+ {statusCode: http.StatusOK},
+ },
+ wantCalls: 2,
+ },
+ {
+ name: "rate limited then no content",
+ outcomes: []roundTripOutcome{
+ {statusCode: http.StatusTooManyRequests},
+ {statusCode: http.StatusNoContent},
+ },
+ wantCalls: 2,
+ },
+ {
+ name: "server errors then no content",
+ outcomes: []roundTripOutcome{
+ {statusCode: http.StatusInternalServerError},
+ {statusCode: http.StatusBadGateway},
+ {statusCode: http.StatusNoContent},
+ },
+ wantCalls: 3,
+ },
+ {
+ name: "bad request fails immediately",
+ outcomes: []roundTripOutcome{{statusCode: http.StatusBadRequest}},
+ wantCalls: 1,
+ wantErr: true,
+ },
+ {
+ name: "three service unavailable responses fail",
+ outcomes: []roundTripOutcome{
+ {statusCode: http.StatusServiceUnavailable},
+ {statusCode: http.StatusServiceUnavailable},
+ {statusCode: http.StatusServiceUnavailable},
+ },
+ wantCalls: 3,
+ wantErr: true,
+ },
+ {
+ name: "redirect fails immediately",
+ outcomes: []roundTripOutcome{{statusCode: http.StatusFound}},
+ wantCalls: 1,
+ wantErr: true,
+ },
+ {
+ name: "nonstandard 6xx response fails immediately",
+ outcomes: []roundTripOutcome{{statusCode: 600}},
+ wantCalls: 1,
+ wantErr: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ calls := 0
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(_ *http.Request) (*http.Response, error) {
+ outcome := test.outcomes[calls]
+ calls++
+ if outcome.err != nil {
+ return nil, outcome.err
+ }
+ return response(outcome.statusCode, http.NoBody), nil
+ }))
+ result, err := sender.Send(
+ context.Background(),
+ &messaging.Message{Token: "https://adapter.example.com/notify"},
+ )
+
+ if test.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ assert.Equal(t, test.wantCalls, calls)
+ if !test.wantErr {
+ assert.Equal(t, "http/success", result)
+ }
+ })
+ }
+}
+
+func TestHTTPNotificationSenderReusesRetrierAcrossSends(t *testing.T) {
+ calls := 0
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(_ *http.Request) (*http.Response, error) {
+ calls++
+ if calls%2 == 1 {
+ return response(http.StatusServiceUnavailable, http.NoBody), nil
+ }
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+
+ for range 2 {
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{Token: "https://adapter.example.com/notify"},
+ )
+ require.NoError(t, err)
+ }
+
+ assert.Equal(t, 4, calls)
+}
+
+func TestHTTPNotificationSenderCreatesFreshRequestAndBodyForEveryAttempt(t *testing.T) {
+ var requests []*http.Request
+ var bodies [][]byte
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ requests = append(requests, request)
+ body, err := io.ReadAll(request.Body)
+ require.NoError(t, err)
+ bodies = append(bodies, body)
+ if len(requests) < 3 {
+ return response(http.StatusServiceUnavailable, http.NoBody), nil
+ }
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{
+ Token: "https://adapter.example.com/notify",
+ Data: map[string]string{"KEY_MESSAGE_ID": "message-1"},
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, requests, 3)
+ assert.NotSame(t, requests[0], requests[1])
+ assert.NotSame(t, requests[1], requests[2])
+ require.Len(t, bodies, 3)
+ assert.NotEmpty(t, bodies[0])
+ assert.Equal(t, bodies[0], bodies[1])
+ assert.Equal(t, bodies[1], bodies[2])
+}
+
+func TestHTTPNotificationSenderBoundsResponseBodyDiscard(t *testing.T) {
+ body := &boundedReadCloser{remaining: 8192}
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(_ *http.Request) (*http.Response, error) {
+ return response(http.StatusNoContent, body), nil
+ }))
+
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{Token: "https://adapter.example.com/notify"},
+ )
+
+ require.NoError(t, err)
+ assert.Equal(t, int64(4096), body.read)
+ assert.True(t, body.closed)
+}
+
+func TestHTTPNotificationSenderOmitsTTLForHeartbeat(t *testing.T) {
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ var payload httpNotificationPayload
+ require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
+ assert.Equal(t, "high", payload.Message.Android.Priority)
+ assert.Empty(t, payload.Message.Android.TTL)
+ assert.Equal(t, "heartbeat-1", payload.Message.Data["KEY_HEARTBEAT_ID"])
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{
+ Token: "https://adapter.example.com/notify",
+ Data: map[string]string{"KEY_HEARTBEAT_ID": "heartbeat-1"},
+ Android: &messaging.AndroidConfig{
+ Priority: "high",
+ },
+ },
+ )
+
+ require.NoError(t, err)
+}
+
+func TestHTTPNotificationSenderUsesInjectedHTTPClientUnchanged(t *testing.T) {
+ transport := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
+ return response(http.StatusNoContent, http.NoBody), nil
+ })
+ client := &http.Client{
+ Transport: transport,
+ Timeout: time.Minute,
+ }
+
+ sender := NewHTTPNotificationSender(nil, client)
+
+ assert.Same(t, client, sender.client)
+ assert.Equal(t, reflect.ValueOf(transport).Pointer(), reflect.ValueOf(sender.client.Transport).Pointer())
+ assert.Equal(t, time.Minute, sender.client.Timeout)
+}
+
+func TestHTTPNotificationSenderAllowsEndpointUserInformation(t *testing.T) {
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ username, password, ok := request.BasicAuth()
+ assert.True(t, ok)
+ assert.Equal(t, "adapter-user", username)
+ assert.Equal(t, "adapter-password", password)
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+ endpoint := &url.URL{
+ Scheme: "https",
+ User: url.UserPassword("adapter-user", "adapter-password"),
+ Host: "adapter.example.com",
+ Path: "/notify",
+ }
+
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{Token: endpoint.String()},
+ )
+
+ require.NoError(t, err)
+}
+
+func TestHTTPNotificationSenderBoundsEveryAttemptByTimeout(t *testing.T) {
+ calls := 0
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ calls++
+ <-request.Context().Done()
+ return nil, request.Context().Err()
+ }))
+ sender.timeout = 10 * time.Millisecond
+
+ _, err := sender.Send(
+ context.Background(),
+ &messaging.Message{Token: "https://adapter.example.com/notify"},
+ )
+
+ require.Error(t, err)
+ assert.Equal(t, 3, calls)
+}
+
+func TestHTTPNotificationSenderStopsRetriesWhenParentContextIsCancelled(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ calls := 0
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(request *http.Request) (*http.Response, error) {
+ calls++
+ cancel()
+ <-request.Context().Done()
+ return nil, request.Context().Err()
+ }))
+
+ _, err := sender.Send(
+ ctx,
+ &messaging.Message{Token: "https://adapter.example.com/notify"},
+ )
+
+ require.Error(t, err)
+ assert.Equal(t, 1, calls)
+}
+
+func TestHTTPNotificationSenderRejectsNilMessage(t *testing.T) {
+ sender := newHTTPNotificationSender(t, roundTripFunc(func(_ *http.Request) (*http.Response, error) {
+ return response(http.StatusNoContent, http.NoBody), nil
+ }))
+
+ _, err := sender.Send(context.Background(), nil)
+
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "notification message is nil")
+}
+
+type roundTripOutcome struct {
+ statusCode int
+ err error
+}
+
+type boundedReadCloser struct {
+ remaining int64
+ read int64
+ closed bool
+}
+
+func (reader *boundedReadCloser) Read(buffer []byte) (int, error) {
+ if reader.remaining == 0 {
+ return 0, io.EOF
+ }
+ read := int64(len(buffer))
+ if read > reader.remaining {
+ read = reader.remaining
+ }
+ reader.remaining -= read
+ reader.read += read
+ return int(read), nil
+}
+
+func (reader *boundedReadCloser) Close() error {
+ reader.closed = true
+ return nil
+}
+
+type httpNotificationRecordingLogger struct {
+ errors []string
+}
+
+func (logger *httpNotificationRecordingLogger) Error(err error) {
+ logger.errors = append(logger.errors, err.Error())
+}
+
+func (logger *httpNotificationRecordingLogger) WithService(string) telemetry.Logger { return logger }
+
+func (logger *httpNotificationRecordingLogger) WithString(string, string) telemetry.Logger {
+ return logger
+}
+
+func (logger *httpNotificationRecordingLogger) WithSpan(trace.SpanContext) telemetry.Logger {
+ return logger
+}
+
+func (logger *httpNotificationRecordingLogger) Trace(string) {}
+func (logger *httpNotificationRecordingLogger) Info(string) {}
+func (logger *httpNotificationRecordingLogger) Warn(error) {}
+func (logger *httpNotificationRecordingLogger) Debug(string) {}
+func (logger *httpNotificationRecordingLogger) Fatal(error) {}
+func (logger *httpNotificationRecordingLogger) Printf(string, ...interface{}) {}
+
+func newHTTPNotificationSender(t *testing.T, transport roundTripFunc) *HTTPNotificationSender {
+ t.Helper()
+ return newHTTPNotificationSenderWithLogger(t, nil, transport)
+}
+
+func newHTTPNotificationSenderWithLogger(
+ t *testing.T,
+ logger telemetry.Logger,
+ transport roundTripFunc,
+) *HTTPNotificationSender {
+ t.Helper()
+ return newHTTPNotificationSenderWithRetrier(
+ logger,
+ &http.Client{Transport: transport},
+ newHTTPNotificationRetrier(0),
+ )
+}
+
+func response(statusCode int, body io.ReadCloser) *http.Response {
+ return &http.Response{
+ StatusCode: statusCode,
+ Body: body,
+ Header: make(http.Header),
+ }
+}
diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go
index 0d4d8d209..d3166854a 100644
--- a/api/pkg/services/phone_notification_service.go
+++ b/api/pkg/services/phone_notification_service.go
@@ -4,12 +4,13 @@ import (
"context"
"errors"
"fmt"
+ "strings"
"time"
+ "firebase.google.com/go/messaging"
"github.com/NdoleStudio/httpsms/pkg/events"
cloudevents "github.com/cloudevents/sdk-go/v2"
- "firebase.google.com/go/messaging"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
@@ -18,7 +19,7 @@ import (
"go.opentelemetry.io/otel/trace"
)
-// PhoneNotificationService sends out notifications to mobile phones
+// PhoneNotificationService sends wake-up notifications to phone gateways.
type PhoneNotificationService struct {
service
logger telemetry.Logger
@@ -26,7 +27,7 @@ type PhoneNotificationService struct {
phoneNotificationRepository repositories.PhoneNotificationRepository
phoneRepository repositories.PhoneRepository
messageSendScheduleRepository repositories.MessageSendScheduleRepository
- messagingClient FCMClient
+ phoneNotificationClients map[entities.NotificationTransport]FCMClient
eventDispatcher *EventDispatcher
}
@@ -34,20 +35,20 @@ type PhoneNotificationService struct {
func NewNotificationService(
logger telemetry.Logger,
tracer telemetry.Tracer,
- messagingClient FCMClient,
+ phoneNotificationClients map[entities.NotificationTransport]FCMClient,
phoneRepository repositories.PhoneRepository,
phoneNotificationRepository repositories.PhoneNotificationRepository,
messageSendScheduleRepository repositories.MessageSendScheduleRepository,
- dispatcher *EventDispatcher,
+ eventDispatcher *EventDispatcher,
) (s *PhoneNotificationService) {
return &PhoneNotificationService{
logger: logger.WithService(fmt.Sprintf("%T", &PhoneNotificationService{})),
tracer: tracer,
- messagingClient: messagingClient,
+ phoneNotificationClients: phoneNotificationClients,
phoneNotificationRepository: phoneNotificationRepository,
phoneRepository: phoneRepository,
messageSendScheduleRepository: messageSendScheduleRepository,
- eventDispatcher: dispatcher,
+ eventDispatcher: eventDispatcher,
}
}
@@ -77,7 +78,7 @@ func (service *PhoneNotificationService) DeleteByMessageID(ctx context.Context,
return nil
}
-// SendHeartbeatFCM sends a heartbeat message so the phone can request a heartbeat
+// SendHeartbeatFCM sends a heartbeat notification so the phone gateway can request a heartbeat.
func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, payload *events.PhoneHeartbeatMissedPayload) error {
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
defer span.End()
@@ -88,25 +89,27 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p
}
if phone.FcmToken == nil {
- return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "phone with id [%s] has no FCM token", phone.ID))
+ return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("phone with id [%s] has no notification token", phone.ID))
}
- result, err := service.messagingClient.Send(ctx, &messaging.Message{
+ result, _, err := service.sendPhoneNotification(ctx, phone, &messaging.Message{
Data: map[string]string{
"KEY_HEARTBEAT_ID": time.Now().UTC().Format(time.RFC3339),
},
- Android: &messaging.AndroidConfig{
- Priority: "high",
- },
- Token: *phone.FcmToken,
+ Android: &messaging.AndroidConfig{Priority: "high"},
})
if err != nil {
- ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send heartbeat FCM to phone with id [%s] for user [%s]", phone.ID, phone.UserID))
+ ctxLogger.Warn(stacktrace.Propagatef(
+ err,
+ "cannot send heartbeat notification to phone with id [%s] for user [%s]",
+ phone.ID,
+ phone.UserID,
+ ))
return nil
}
ctxLogger.Info(fmt.Sprintf(
- "successfully sent heartbeat FCM [%s] to phone with ID [%s] for user [%s] and monitor [%s]",
+ "successfully sent heartbeat notification [%s] to phone with ID [%s] for user [%s] and monitor [%s]",
result,
payload.PhoneID,
payload.UserID,
@@ -125,7 +128,7 @@ type PhoneNotificationSendParams struct {
MessageID uuid.UUID
}
-// Send sends a message when a message is sent
+// Send sends a phone gateway notification when a message is sent.
func (service *PhoneNotificationService) Send(ctx context.Context, params *PhoneNotificationSendParams) error {
ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
defer span.End()
@@ -142,7 +145,7 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone
}
ttl := phone.MessageExpirationDuration()
- result, err := service.messagingClient.Send(ctx, &messaging.Message{
+ result, transport, err := service.sendPhoneNotification(ctx, phone, &messaging.Message{
Data: map[string]string{
"KEY_MESSAGE_ID": params.MessageID.String(),
},
@@ -150,24 +153,80 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone
Priority: "normal",
TTL: &ttl,
},
- Token: *phone.FcmToken,
})
if err != nil {
+ if transport == "" {
+ ctxLogger.Warn(stacktrace.Propagatef(
+ err,
+ "cannot determine notification transport for phone with ID [%s] for user with ID [%s] and message [%s]",
+ phone.ID,
+ phone.UserID,
+ params.MessageID,
+ ))
+ msg := fmt.Sprintf("cannot send notification to phone [%s]. Check the notification configuration.", phone.PhoneNumber)
+ return service.handleNotificationFailed(ctx, errors.New(msg), params)
+ }
+
ctxLogger.Warn(stacktrace.Propagatef(
err,
-
- "cannot send FCM to phone with ID [%s] for user with ID [%s] and message [%s]",
+ "cannot send %s notification to phone with ID [%s] for user with ID [%s] and message [%s]",
+ transport,
phone.ID,
phone.UserID,
params.MessageID,
))
msg := fmt.Sprintf("cannot send notification to your phone [%s]. Reinstall the httpSMS app on your Android phone.", phone.PhoneNumber)
+ if transport == entities.NotificationTransportHTTP {
+ msg = fmt.Sprintf(
+ "cannot notify the configured adapter for phone [%s]. Check the adapter URL and availability.",
+ phone.PhoneNumber,
+ )
+ }
return service.handleNotificationFailed(ctx, errors.New(msg), params)
}
return service.handleNotificationSent(ctx, phone, result, params)
}
+func (service *PhoneNotificationService) sendPhoneNotification(
+ ctx context.Context,
+ phone *entities.Phone,
+ message *messaging.Message,
+) (string, entities.NotificationTransport, error) {
+ if message == nil {
+ return "", "", stacktrace.NewErrorf("notification message is nil")
+ }
+
+ transport, err := phone.NotificationTransport()
+ if err != nil {
+ return "", "", stacktrace.Propagatef(
+ err,
+ "cannot determine notification transport for phone [%s]",
+ phone.ID,
+ )
+ }
+
+ client, ok := service.phoneNotificationClients[transport]
+ if !ok || client == nil {
+ return "", transport, stacktrace.NewErrorf(
+ "notification client is not configured for transport [%s]",
+ transport,
+ )
+ }
+
+ message.Token = strings.TrimSpace(*phone.FcmToken)
+ result, err := client.Send(ctx, message)
+ if err != nil {
+ return "", transport, stacktrace.Propagatef(
+ err,
+ "cannot send [%s] notification to phone [%s]",
+ transport,
+ phone.ID,
+ )
+ }
+ return result, transport, nil
+}
+
// PhoneNotificationScheduleParams are parameters for sending a notification
type PhoneNotificationScheduleParams struct {
UserID entities.UserID
diff --git a/api/pkg/services/phone_notification_service_test.go b/api/pkg/services/phone_notification_service_test.go
new file mode 100644
index 000000000..9908f5d97
--- /dev/null
+++ b/api/pkg/services/phone_notification_service_test.go
@@ -0,0 +1,325 @@
+package services
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "testing"
+ "time"
+
+ "firebase.google.com/go/messaging"
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/events"
+ "github.com/NdoleStudio/httpsms/pkg/repositories"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ cloudevents "github.com/cloudevents/sdk-go/v2"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+)
+
+type phoneNotificationPhoneRepository struct {
+ repositories.PhoneRepository
+ phone *entities.Phone
+ err error
+}
+
+func (repository *phoneNotificationPhoneRepository) LoadByID(
+ _ context.Context,
+ _ entities.UserID,
+ _ uuid.UUID,
+) (*entities.Phone, error) {
+ return repository.phone, repository.err
+}
+
+type phoneNotificationRepository struct {
+ repositories.PhoneNotificationRepository
+ notificationID uuid.UUID
+ status entities.PhoneNotificationStatus
+}
+
+func (repository *phoneNotificationRepository) UpdateStatus(
+ _ context.Context,
+ notificationID uuid.UUID,
+ status entities.PhoneNotificationStatus,
+) error {
+ repository.notificationID = notificationID
+ repository.status = status
+ return nil
+}
+
+type phoneNotificationEventQueue struct {
+ events []cloudevents.Event
+}
+
+func (queue *phoneNotificationEventQueue) Enqueue(
+ _ context.Context,
+ task *PushQueueTask,
+ _ time.Duration,
+) (string, error) {
+ var event cloudevents.Event
+ if err := json.Unmarshal(task.Body, &event); err != nil {
+ return "", err
+ }
+ queue.events = append(queue.events, event)
+ return "", nil
+}
+
+type phoneNotificationLogger struct {
+ warnings []string
+}
+
+var _ telemetry.Logger = (*phoneNotificationLogger)(nil)
+
+func (logger *phoneNotificationLogger) Error(error) {}
+func (logger *phoneNotificationLogger) WithService(string) telemetry.Logger { return logger }
+func (logger *phoneNotificationLogger) WithString(string, string) telemetry.Logger {
+ return logger
+}
+
+func (logger *phoneNotificationLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger }
+func (logger *phoneNotificationLogger) Trace(string) {}
+func (logger *phoneNotificationLogger) Info(string) {}
+func (logger *phoneNotificationLogger) Warn(err error) {
+ logger.warnings = append(logger.warnings, err.Error())
+}
+func (logger *phoneNotificationLogger) Debug(string) {}
+func (logger *phoneNotificationLogger) Fatal(error) {}
+func (logger *phoneNotificationLogger) Printf(string, ...interface{}) {}
+
+type recordingPhoneNotificationClient struct {
+ message *messaging.Message
+ result string
+ err error
+ calls int
+}
+
+func (client *recordingPhoneNotificationClient) Send(
+ _ context.Context,
+ message *messaging.Message,
+) (string, error) {
+ client.calls++
+ client.message = message
+ return client.result, client.err
+}
+
+func TestPhoneNotificationServiceSendPhoneNotificationUsesMappedClient(t *testing.T) {
+ endpoint := " https://adapter.example.com/notify "
+ phone := &entities.Phone{ID: uuid.New(), FcmToken: &endpoint}
+ httpClient := &recordingPhoneNotificationClient{result: "accepted"}
+ service := &PhoneNotificationService{
+ phoneNotificationClients: map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportHTTP: httpClient,
+ },
+ }
+ message := &messaging.Message{Data: map[string]string{"KEY_MESSAGE_ID": uuid.NewString()}}
+
+ result, transport, err := service.sendPhoneNotification(context.Background(), phone, message)
+
+ require.NoError(t, err)
+ assert.Equal(t, "accepted", result)
+ assert.Equal(t, entities.NotificationTransportHTTP, transport)
+ assert.Equal(t, "https://adapter.example.com/notify", message.Token)
+ assert.Same(t, message, httpClient.message)
+ assert.Equal(t, 1, httpClient.calls)
+}
+
+func TestPhoneNotificationServiceSendPhoneNotificationRejectsMissingClient(t *testing.T) {
+ endpoint := "https://adapter.example.com/notify"
+ phone := &entities.Phone{ID: uuid.New(), FcmToken: &endpoint}
+ service := &PhoneNotificationService{
+ phoneNotificationClients: map[entities.NotificationTransport]FCMClient{},
+ }
+
+ _, transport, err := service.sendPhoneNotification(
+ context.Background(),
+ phone,
+ &messaging.Message{},
+ )
+
+ require.Error(t, err)
+ assert.Equal(t, entities.NotificationTransportHTTP, transport)
+ assert.Contains(t, err.Error(), "notification client is not configured for transport [http]")
+}
+
+func TestPhoneNotificationServiceSendPhoneNotificationRejectsNilMessage(t *testing.T) {
+ token := "fcm-token"
+ phone := &entities.Phone{ID: uuid.New(), FcmToken: &token}
+ fcmClient := &recordingPhoneNotificationClient{}
+ service := &PhoneNotificationService{
+ phoneNotificationClients: map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportFCM: fcmClient,
+ },
+ }
+
+ _, transport, err := service.sendPhoneNotification(context.Background(), phone, nil)
+
+ require.Error(t, err)
+ assert.Empty(t, transport)
+ assert.Contains(t, err.Error(), "notification message is nil")
+ assert.Zero(t, fcmClient.calls)
+}
+
+func TestPhoneNotificationServiceSendUsesHTTPSMessage(t *testing.T) {
+ endpoint := "https://adapter.example.com/notify"
+ phone := &entities.Phone{
+ ID: uuid.New(),
+ UserID: "user-1",
+ FcmToken: &endpoint,
+ PhoneNumber: "+18005550199",
+ MessageExpirationSeconds: 90,
+ }
+ httpClient := &recordingPhoneNotificationClient{result: "http/success"}
+ eventQueue := &phoneNotificationEventQueue{}
+ notificationRepository := &phoneNotificationRepository{}
+ service := newPhoneNotificationServiceForTest(
+ phone,
+ notificationRepository,
+ eventQueue,
+ map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportFCM: &recordingPhoneNotificationClient{},
+ entities.NotificationTransportHTTP: httpClient,
+ },
+ )
+ params := &PhoneNotificationSendParams{
+ UserID: phone.UserID,
+ PhoneID: phone.ID,
+ PhoneNotificationID: uuid.New(),
+ Source: "test",
+ ScheduledAt: time.Now().UTC(),
+ MessageID: uuid.New(),
+ }
+
+ require.NoError(t, service.Send(context.Background(), params))
+
+ require.NotNil(t, httpClient.message)
+ assert.Equal(t, endpoint, httpClient.message.Token)
+ assert.Equal(t, params.MessageID.String(), httpClient.message.Data["KEY_MESSAGE_ID"])
+ require.NotNil(t, httpClient.message.Android)
+ assert.Equal(t, "normal", httpClient.message.Android.Priority)
+ require.NotNil(t, httpClient.message.Android.TTL)
+ assert.Equal(t, phone.MessageExpirationDuration(), *httpClient.message.Android.TTL)
+ require.Len(t, eventQueue.events, 1)
+ assert.Equal(t, events.EventTypeMessageNotificationSent, eventQueue.events[0].Type())
+ assert.Equal(t, params.PhoneNotificationID, notificationRepository.notificationID)
+ assert.Equal(t, entities.PhoneNotificationStatus(entities.PhoneNotificationStatusSent), notificationRepository.status)
+}
+
+func TestPhoneNotificationServiceSendHTTPFailureUsesAdapterGuidance(t *testing.T) {
+ endpoint := "https://adapter.example.com/notify"
+ phone := &entities.Phone{ID: uuid.New(), UserID: "user-1", FcmToken: &endpoint, PhoneNumber: "+18005550199"}
+ eventQueue := &phoneNotificationEventQueue{}
+ notificationRepository := &phoneNotificationRepository{}
+ service := newPhoneNotificationServiceForTest(
+ phone,
+ notificationRepository,
+ eventQueue,
+ map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportFCM: &recordingPhoneNotificationClient{},
+ entities.NotificationTransportHTTP: &recordingPhoneNotificationClient{
+ err: errors.New("adapter unavailable"),
+ },
+ },
+ )
+ params := &PhoneNotificationSendParams{
+ UserID: phone.UserID,
+ PhoneID: phone.ID,
+ PhoneNotificationID: uuid.New(),
+ Source: "test",
+ MessageID: uuid.New(),
+ }
+
+ require.NoError(t, service.Send(context.Background(), params))
+
+ require.Len(t, eventQueue.events, 1)
+ assert.Equal(t, events.EventTypeMessageNotificationFailed, eventQueue.events[0].Type())
+ var payload events.MessageNotificationFailedPayload
+ require.NoError(t, eventQueue.events[0].DataAs(&payload))
+ assert.Equal(t, "cannot notify the configured adapter for phone [+18005550199]. Check the adapter URL and availability.", payload.ErrorMessage)
+ assert.NotContains(t, payload.ErrorMessage, "Reinstall the httpSMS app")
+ assert.Equal(t, entities.PhoneNotificationStatus(entities.PhoneNotificationStatusFailed), notificationRepository.status)
+}
+
+func TestPhoneNotificationServiceSendFCMFailurePreservesAndroidGuidance(t *testing.T) {
+ token := "fcm-token"
+ phone := &entities.Phone{ID: uuid.New(), UserID: "user-1", FcmToken: &token, PhoneNumber: "+18005550199"}
+ eventQueue := &phoneNotificationEventQueue{}
+ fcmClient := &recordingPhoneNotificationClient{err: errors.New("firebase unavailable")}
+ service := newPhoneNotificationServiceForTest(
+ phone,
+ &phoneNotificationRepository{},
+ eventQueue,
+ map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportFCM: fcmClient,
+ entities.NotificationTransportHTTP: &recordingPhoneNotificationClient{},
+ },
+ )
+ params := &PhoneNotificationSendParams{
+ UserID: phone.UserID,
+ PhoneID: phone.ID,
+ PhoneNotificationID: uuid.New(),
+ Source: "test",
+ MessageID: uuid.New(),
+ }
+
+ require.NoError(t, service.Send(context.Background(), params))
+
+ require.Len(t, eventQueue.events, 1)
+ require.NotNil(t, fcmClient.message)
+ assert.Equal(t, token, fcmClient.message.Token)
+ assert.Equal(t, 1, fcmClient.calls)
+ var payload events.MessageNotificationFailedPayload
+ require.NoError(t, eventQueue.events[0].DataAs(&payload))
+ assert.Equal(t, "cannot send notification to your phone [+18005550199]. Reinstall the httpSMS app on your Android phone.", payload.ErrorMessage)
+}
+
+func TestPhoneNotificationServiceSendHeartbeatFCMUsesHTTPSMessage(t *testing.T) {
+ endpoint := "https://adapter.example.com/notify"
+ phone := &entities.Phone{ID: uuid.New(), UserID: "user-1", FcmToken: &endpoint}
+ httpClient := &recordingPhoneNotificationClient{err: errors.New("adapter unavailable")}
+ service := newPhoneNotificationServiceForTest(
+ phone,
+ &phoneNotificationRepository{},
+ &phoneNotificationEventQueue{},
+ map[entities.NotificationTransport]FCMClient{
+ entities.NotificationTransportFCM: &recordingPhoneNotificationClient{},
+ entities.NotificationTransportHTTP: httpClient,
+ },
+ )
+
+ err := service.SendHeartbeatFCM(context.Background(), &events.PhoneHeartbeatMissedPayload{
+ UserID: phone.UserID,
+ PhoneID: phone.ID,
+ MonitorID: uuid.New(),
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, httpClient.message)
+ assert.Equal(t, endpoint, httpClient.message.Token)
+ heartbeatID := httpClient.message.Data["KEY_HEARTBEAT_ID"]
+ _, err = time.Parse(time.RFC3339, heartbeatID)
+ require.NoError(t, err)
+ require.NotNil(t, httpClient.message.Android)
+ assert.Equal(t, "high", httpClient.message.Android.Priority)
+ assert.Nil(t, httpClient.message.Android.TTL)
+}
+
+func newPhoneNotificationServiceForTest(
+ phone *entities.Phone,
+ notificationRepository repositories.PhoneNotificationRepository,
+ eventQueue *phoneNotificationEventQueue,
+ clients map[entities.NotificationTransport]FCMClient,
+) *PhoneNotificationService {
+ logger := &phoneNotificationLogger{}
+ tracer := telemetry.NewOtelLogger("test", logger)
+ return NewNotificationService(
+ logger,
+ tracer,
+ clients,
+ &phoneNotificationPhoneRepository{phone: phone},
+ notificationRepository,
+ nil,
+ NewEventDispatcher(logger, tracer, nil, eventQueue, PushQueueConfig{}),
+ )
+}
diff --git a/api/pkg/validators/phone_handler_validator.go b/api/pkg/validators/phone_handler_validator.go
index e9d4274e7..9e21d26a9 100644
--- a/api/pkg/validators/phone_handler_validator.go
+++ b/api/pkg/validators/phone_handler_validator.go
@@ -103,6 +103,11 @@ func (validator *PhoneHandlerValidator) ValidateUpsert(ctx context.Context, user
return result
}
+ validator.validateNotificationToken(request.FcmToken, result)
+ if len(result) > 0 {
+ return result
+ }
+
if strings.TrimSpace(request.MessageSendScheduleID) != "" {
scheduleID, _ := uuid.Parse(strings.TrimSpace(request.MessageSendScheduleID))
if _, err := validator.scheduleService.Load(ctx, userID, scheduleID); err != nil {
@@ -133,7 +138,29 @@ func (validator *PhoneHandlerValidator) ValidateFCMToken(_ context.Context, requ
},
})
- return v.ValidateStruct()
+ result := v.ValidateStruct()
+ if len(result) > 0 {
+ return result
+ }
+
+ validator.validateNotificationToken(request.FcmToken, result)
+ return result
+}
+
+func (validator *PhoneHandlerValidator) validateNotificationToken(
+ token string,
+ result url.Values,
+) {
+ token = strings.TrimSpace(token)
+ if token == "" {
+ return
+ }
+
+ phone := &entities.Phone{FcmToken: &token}
+ _, err := phone.NotificationTransport()
+ if err != nil {
+ result.Add("fcm_token", err.Error())
+ }
}
// ValidateDelete ValidateUpsert validates requests.PhoneDelete
diff --git a/api/pkg/validators/phone_handler_validator_test.go b/api/pkg/validators/phone_handler_validator_test.go
new file mode 100644
index 000000000..c9dc72b63
--- /dev/null
+++ b/api/pkg/validators/phone_handler_validator_test.go
@@ -0,0 +1,144 @@
+package validators
+
+import (
+ "context"
+ "net/url"
+ "testing"
+
+ "github.com/NdoleStudio/httpsms/pkg/entities"
+ "github.com/NdoleStudio/httpsms/pkg/requests"
+ "github.com/NdoleStudio/httpsms/pkg/telemetry"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPhoneHandlerValidatorAcceptsHTTPSNotificationURL(t *testing.T) {
+ validator := newPhoneHandlerValidator()
+
+ errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: "https://adapter.example.com/notify",
+ SIM: entities.SIM1.String(),
+ })
+
+ assert.Empty(t, errors)
+}
+
+func TestPhoneHandlerValidatorAcceptsHTTPSNotificationURLOnUpsert(t *testing.T) {
+ validator := newPhoneHandlerValidator()
+
+ errors := validator.ValidateUpsert(context.Background(), "", requests.PhoneUpsert{
+ PhoneNumber: "+18005550199",
+ FcmToken: "https://adapter.example.com/notify",
+ SIM: entities.SIM1.String(),
+ MessageExpirationSeconds: 60,
+ })
+
+ assert.Empty(t, errors)
+}
+
+func TestPhoneHandlerValidatorAcceptsPrivateAndLoopbackNotificationHosts(t *testing.T) {
+ validator := newPhoneHandlerValidator()
+
+ for _, token := range []string{
+ "https://localhost/notify",
+ "https://127.0.0.1/notify",
+ "https://10.0.0.5/notify",
+ } {
+ errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: token,
+ SIM: entities.SIM1.String(),
+ })
+
+ assert.Empty(t, errors, token)
+ }
+}
+
+func TestPhoneHandlerValidatorRejectsInvalidNotificationURLs(t *testing.T) {
+ tests := []struct {
+ name string
+ token string
+ }{
+ {name: "insecure HTTP", token: "http://adapter.example.com/notify"},
+ {name: "missing host", token: "https:///notify"},
+ {name: "malformed HTTPS", token: "https://%"},
+ }
+
+ validationPaths := []struct {
+ name string
+ validate func(*PhoneHandlerValidator, string) map[string][]string
+ }{
+ {
+ name: "FCM token upsert",
+ validate: func(validator *PhoneHandlerValidator, token string) map[string][]string {
+ return validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: token,
+ SIM: entities.SIM1.String(),
+ })
+ },
+ },
+ {
+ name: "phone upsert",
+ validate: func(validator *PhoneHandlerValidator, token string) map[string][]string {
+ return validator.ValidateUpsert(context.Background(), "", requests.PhoneUpsert{
+ PhoneNumber: "+18005550199",
+ FcmToken: token,
+ SIM: entities.SIM1.String(),
+ MessageExpirationSeconds: 60,
+ })
+ },
+ },
+ }
+
+ for _, validationPath := range validationPaths {
+ t.Run(validationPath.name, func(t *testing.T) {
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ validationErrors := validationPath.validate(newPhoneHandlerValidator(), test.token)
+
+ assert.NotEmpty(t, validationErrors["fcm_token"])
+ })
+ }
+ })
+ }
+}
+
+func TestPhoneHandlerValidatorAcceptsOpaqueFirebaseNotificationToken(t *testing.T) {
+ validator := newPhoneHandlerValidator()
+
+ errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: "opaque-firebase-registration-token",
+ SIM: entities.SIM1.String(),
+ })
+
+ assert.Empty(t, errors)
+}
+
+func TestPhoneHandlerValidatorAcceptsNotificationURLWithUserInformation(t *testing.T) {
+ validator := newPhoneHandlerValidator()
+ endpoint := &url.URL{
+ Scheme: "https",
+ User: url.UserPassword("adapter-user", "adapter-password"),
+ Host: "adapter.example.com",
+ Path: "/notify",
+ }
+
+ errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: endpoint.String(),
+ SIM: entities.SIM1.String(),
+ })
+
+ assert.Empty(t, errors)
+}
+
+func newPhoneHandlerValidator() *PhoneHandlerValidator {
+ logger := &contactValidatorNoopLogger{}
+ return NewPhoneHandlerValidator(
+ logger,
+ telemetry.NewOtelLogger("test", logger),
+ nil,
+ )
+}
diff --git a/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md b/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
new file mode 100644
index 000000000..6b81ee359
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
@@ -0,0 +1,1863 @@
+# URL-backed Phone Notification Adapter 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 a phone whose existing `fcm_token` is an HTTPS URL to receive message and heartbeat wake-ups over HTTP while preserving the current scheduling, backpressure, outstanding-message, and status-event flows.
+
+**Architecture:** Add transport helpers to `entities.Phone`, inject a map of existing `FCMClient` implementations keyed by transport, and let `PhoneNotificationService` select the client through map lookup. The HTTP path uses a standard OpenTelemetry-wrapped `http.Client`, application retries with `github.com/avast/retry-go/v5`, FCM-compatible JSON, and the existing notification success/failure state transitions.
+
+**Tech Stack:** Go 1.25.8, Fiber v3, Firebase Admin Messaging, OpenTelemetry, `net/http`, `retry-go/v5`, Testify, Docker Compose.
+
+**Spec:** `docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md`
+
+## Final Accepted Architecture
+
+This section supersedes later historical task text and code samples that
+introduce an endpoint policy, DNS/IP filtering, custom dialing, or a
+private-host allowlist.
+
+- `Phone.NotificationTransport` performs only URL syntax, HTTPS scheme, and
+ hostname classification; URL user information remains valid.
+- `Container.NotificationHTTPClient` uses the existing
+ `go-otelroundtripper` pattern with the `phone_notification_http` name and
+ the default transport, matching the webhook client style without custom URL
+ redaction.
+- `HTTPNotificationSender` creates one reusable `*retry.Retrier` during
+ initialization. It owns exactly three application attempts with exponential
+ backoff, a fresh request/body, and a five-second child context per attempt;
+ caller contexts are enforced per operation rather than stored on the
+ reusable retrier.
+- Payload encoding, request creation, one-attempt delivery, and retry
+ configuration are separate focused methods. OpenTelemetry HTTP telemetry is
+ owned by the injected round-tripper, with no sender-specific attempt metrics
+ or spans.
+- No endpoint DNS/IP/SSRF policy, custom notification transport, or private-host
+ allowlist is part of the final implementation.
+- `PhoneNotificationService` receives
+ `map[entities.NotificationTransport]FCMClient`, determines transport once,
+ selects the client by map lookup, sets the trimmed destination in
+ `message.Token`, and passes the same pointer to `FCMClient.Send`. There is no
+ extra sender interface, Firebase wrapper, or dispatcher class.
+- `HTTPNotificationSender` implements `FCMClient` exactly and returns
+ `http/success` after a successful callback response.
+- Domain events continue to use the existing `EventDispatcher` directly.
+
+## Global Constraints
+
+- Reuse the existing `Phone.FcmToken` database field and `fcm_token` API field; add no transport or endpoint columns.
+- A valid `https://` URL with a hostname selects HTTP; an opaque non-URL token selects Firebase.
+- URL-like malformed or unsupported tokens are invalid and must never fall through to Firebase.
+- Send both outstanding-message and heartbeat notifications through the selected transport.
+- HTTP callback requests are unsigned and contain no message content, user API key, phone API key, or other credentials.
+- HTTPS callback URLs may include standard URL user information.
+- Accept any HTTP `2xx`; ignore response content.
+- Make at most three HTTP attempts with a five-second timeout per attempt.
+- Retry network failures, `408`, `429`, and `5xx`; do not retry other non-`2xx` responses.
+- Use the standard OpenTelemetry-wrapped HTTP transport without destination
+ DNS/IP filtering, custom dialing, or a private-host allowlist.
+- Preserve existing schedules, per-minute backpressure, message expiration, send-attempt counting, outstanding-message fetching, and message event routes.
+- Use `stacktrace.Propagate` or `stacktrace.Propagatef` for returned errors.
+- Use GORM query builders with context propagation; this feature requires no database query changes or migration.
+- Format Go code with `go-fumpt` through the repository's existing tooling.
+
+---
+
+## File Structure
+
+### Create
+
+- `api/pkg/entities/phone_test.go` - table-driven transport classification tests.
+- `api/pkg/services/notification_endpoint_policy.go` - public HTTPS URL validation, reserved-IP rejection, and validated dialing.
+- `api/pkg/services/notification_endpoint_policy_test.go` - deterministic resolver/dialer tests, including DNS rebinding protection.
+- `api/pkg/services/fcm_client.go` - common phone notification client contract and Firebase client.
+- `api/pkg/services/phone_notification_service.go` - message construction and transport-client lookup.
+- `api/pkg/services/http_notification_sender.go` - HTTP request encoding, retry classification, and timeout.
+- `api/pkg/services/http_notification_sender_test.go` - payload, retry, and response tests.
+- `api/pkg/services/phone_notification_service_test.go` - message and heartbeat integration tests with hand-written fakes.
+- `api/pkg/validators/phone_handler_validator_test.go` - URL token validation tests for both phone update routes.
+- `tests/adapter-emulator/Dockerfile` - container image for the HTTPS adapter emulator.
+- `tests/adapter-emulator/go.mod` - isolated emulator module.
+- `tests/adapter-emulator/main.go` - HTTPS callback and HTTP control server startup.
+- `tests/adapter-emulator/emulator.go` - gateway registry, deduplication, and callback records.
+- `tests/adapter-emulator/api_client.go` - existing httpSMS phone API calls.
+- `tests/adapter-emulator/notification_handler.go` - FCM-envelope message and heartbeat handling.
+- `tests/adapter-emulator/control_handler.go` - test registration, incoming-message, and record endpoints.
+- `tests/adapter_integration_test.go` - outgoing, incoming, and heartbeat end-to-end tests.
+- `tests/generate-adapter-certificates.sh` - throwaway CA and server-certificate generation.
+
+### Modify
+
+- `api/pkg/entities/phone.go` - add `NotificationTransport`, `NotificationTransport()`, and `NotificationURL()`.
+- `api/pkg/services/fcm_client.go` - keep the SDK wrapper; document its role as the low-level Firebase client.
+- `api/pkg/services/phone_notification_service.go` - replace direct Firebase messages with neutral notifications and transport-aware failure text.
+- `api/pkg/validators/phone_handler_validator.go` - inject and apply the endpoint policy for URL-like tokens.
+- `api/pkg/di/container.go` - construct the HTTP client, transport-client map,
+ and updated service dependencies.
+- `api/pkg/requests/phone_update_request.go` - document dual-purpose `fcm_token`.
+- `api/pkg/requests/phone_fcm_token_request.go` - document dual-purpose `fcm_token`.
+- `api/pkg/entities/phone_notification.go` - update FCM-specific comments to transport-neutral wording.
+- `api/docs/docs.go` - regenerate with `swag`.
+- `api/docs/swagger.json` - regenerate with `swag`.
+- `api/docs/swagger.yaml` - regenerate with `swag`.
+- `tests/docker-compose.yml` - run the emulator and mount TLS material.
+- `tests/.env.test` - allowlist the emulator hostname only in local mode.
+- `tests/helpers_test.go` - adapter setup, control client, and internal-event helpers.
+- `tests/README.md` - document emulator architecture and commands.
+- `.github/workflows/api.yml` - generate adapter certificates before Docker startup.
+- `.gitignore` - ignore generated adapter certificates.
+
+---
+
+### Task 1: Classify the Existing Token Field
+
+**Files:**
+- Modify: `api/pkg/entities/phone.go`
+- Create: `api/pkg/entities/phone_test.go`
+
+**Interfaces:**
+- Consumes: `Phone.FcmToken *string`
+- Produces:
+
+```go
+type NotificationTransport string
+
+const (
+ NotificationTransportFCM NotificationTransport = "fcm"
+ NotificationTransportHTTP NotificationTransport = "http"
+)
+
+func (phone *Phone) NotificationTransport() (NotificationTransport, error)
+func (phone *Phone) NotificationURL() (*url.URL, error)
+```
+
+- [ ] **Step 1: Write failing transport-classification tests**
+
+Create `api/pkg/entities/phone_test.go`:
+
+```go
+package entities
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func stringPointer(value string) *string {
+ return &value
+}
+
+func TestPhoneNotificationTransport(t *testing.T) {
+ tests := []struct {
+ name string
+ token *string
+ transport NotificationTransport
+ hasError bool
+ }{
+ {name: "firebase token", token: stringPointer("fcm-token:value"), transport: NotificationTransportFCM},
+ {name: "public https url", token: stringPointer("https://adapter.example.com/notify"), transport: NotificationTransportHTTP},
+ {name: "missing token", token: nil, hasError: true},
+ {name: "empty token", token: stringPointer(" "), hasError: true},
+ {name: "http url", token: stringPointer("http://adapter.example.com/notify"), hasError: true},
+ {name: "ftp url", token: stringPointer("ftp://adapter.example.com/notify"), hasError: true},
+ {name: "missing host", token: stringPointer("https:///notify"), hasError: true},
+ {name: "embedded credentials", token: stringPointer("https://user:pass@adapter.example.com/notify"), hasError: true},
+ {name: "malformed url", token: stringPointer("https://[::1"), hasError: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ phone := &Phone{FcmToken: test.token}
+
+ transport, err := phone.NotificationTransport()
+
+ if test.hasError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.transport, transport)
+ })
+ }
+}
+
+func TestPhoneNotificationURL(t *testing.T) {
+ phone := &Phone{FcmToken: stringPointer("https://adapter.example.com/notify?tenant=42")}
+
+ endpoint, err := phone.NotificationURL()
+
+ require.NoError(t, err)
+ assert.Equal(t, "https", endpoint.Scheme)
+ assert.Equal(t, "adapter.example.com", endpoint.Hostname())
+ assert.Equal(t, "/notify", endpoint.Path)
+ assert.Equal(t, "tenant=42", endpoint.RawQuery)
+}
+
+func TestPhoneNotificationURLRejectsFCMToken(t *testing.T) {
+ phone := &Phone{FcmToken: stringPointer("fcm-token:value")}
+
+ _, err := phone.NotificationURL()
+
+ require.Error(t, err)
+}
+```
+
+- [ ] **Step 2: Run the entity tests and confirm the new API is missing**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/entities -run 'TestPhoneNotification' -count=1
+```
+
+Expected: compilation fails because `NotificationTransport`,
+`NotificationTransportFCM`, `NotificationTransportHTTP`,
+`Phone.NotificationTransport`, and `Phone.NotificationURL` do not exist.
+
+- [ ] **Step 3: Implement token classification once on `Phone`**
+
+Add imports for `fmt`, `net/url`, and `strings` in
+`api/pkg/entities/phone.go`, then add:
+
+```go
+// NotificationTransport identifies how a phone receives wake-up notifications.
+type NotificationTransport string
+
+const (
+ // NotificationTransportFCM sends notifications through Firebase.
+ NotificationTransportFCM NotificationTransport = "fcm"
+ // NotificationTransportHTTP sends notifications to a public HTTPS endpoint.
+ NotificationTransportHTTP NotificationTransport = "http"
+)
+
+// NotificationTransport returns the transport encoded by FcmToken.
+func (phone *Phone) NotificationTransport() (NotificationTransport, error) {
+ if phone.FcmToken == nil || strings.TrimSpace(*phone.FcmToken) == "" {
+ return "", fmt.Errorf("phone has no notification token")
+ }
+
+ token := strings.TrimSpace(*phone.FcmToken)
+ endpoint, err := url.Parse(token)
+ if err != nil {
+ if strings.Contains(token, "://") {
+ return "", fmt.Errorf("invalid notification URL: %w", err)
+ }
+ return NotificationTransportFCM, nil
+ }
+
+ if endpoint.Scheme == "" {
+ return NotificationTransportFCM, nil
+ }
+ if endpoint.Scheme != "https" {
+ return "", fmt.Errorf("notification URL must use https")
+ }
+ if endpoint.Hostname() == "" {
+ return "", fmt.Errorf("notification URL must include a hostname")
+ }
+ return NotificationTransportHTTP, nil
+}
+
+// NotificationURL returns the parsed endpoint for an HTTP notification token.
+func (phone *Phone) NotificationURL() (*url.URL, error) {
+ transport, err := phone.NotificationTransport()
+ if err != nil {
+ return nil, err
+ }
+ if transport != NotificationTransportHTTP {
+ return nil, fmt.Errorf("phone notification transport is [%s], not HTTP", transport)
+ }
+
+ endpoint, err := url.Parse(strings.TrimSpace(*phone.FcmToken))
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse notification URL: %w", err)
+ }
+ return endpoint, nil
+}
+```
+
+Before finishing this step, replace the plain `fmt.Errorf` wrappers with
+`stacktrace.Propagatef` or `stacktrace.NewErrorf` to match repository error
+conventions. Preserve the exact public method signatures above.
+
+- [ ] **Step 4: Run the focused entity tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/entities -run 'TestPhoneNotification' -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/entities/phone.go pkg/entities/phone_test.go
+git add pkg/entities/phone.go pkg/entities/phone_test.go
+git commit -m "feat(api): classify phone notification tokens"
+```
+
+---
+
+### Task 2: Enforce Public HTTPS Endpoint Policy
+
+**Files:**
+- Create: `api/pkg/services/notification_endpoint_policy.go`
+- Create: `api/pkg/services/notification_endpoint_policy_test.go`
+
+**Interfaces:**
+- Consumes: parsed HTTPS endpoints from `Phone.NotificationURL()`
+- Produces:
+
+```go
+type HostResolver interface {
+ LookupNetIP(ctx context.Context, network string, host string) ([]netip.Addr, error)
+}
+
+type NotificationEndpointPolicy struct {
+ resolver HostResolver
+ allowedPrivateHosts map[string]struct{}
+}
+
+func NewNotificationEndpointPolicy(resolver HostResolver, allowedPrivateHosts []string) *NotificationEndpointPolicy
+func (policy *NotificationEndpointPolicy) Validate(ctx context.Context, endpoint *url.URL) ([]netip.Addr, error)
+func (policy *NotificationEndpointPolicy) DialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error)
+```
+
+- [ ] **Step 1: Write failing policy tests with a deterministic resolver**
+
+Create `api/pkg/services/notification_endpoint_policy_test.go` with:
+
+```go
+package services
+
+import (
+ "context"
+ "net/netip"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type staticHostResolver struct {
+ addresses map[string][]netip.Addr
+ err error
+}
+
+func (resolver *staticHostResolver) LookupNetIP(_ context.Context, _ string, host string) ([]netip.Addr, error) {
+ if resolver.err != nil {
+ return nil, resolver.err
+ }
+ return resolver.addresses[host], nil
+}
+
+func TestNotificationEndpointPolicyValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ rawURL string
+ addresses []netip.Addr
+ hasError bool
+ }{
+ {name: "public IPv4", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}},
+ {name: "public IPv6", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("2606:4700:4700::1111")}},
+ {name: "loopback", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, hasError: true},
+ {name: "private", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("10.0.0.5")}, hasError: true},
+ {name: "link local", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("169.254.169.254")}, hasError: true},
+ {name: "carrier grade NAT", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, hasError: true},
+ {name: "documentation range", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, hasError: true},
+ {name: "unique local IPv6", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("fd00::1")}, hasError: true},
+ {name: "mixed public and private", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("10.0.0.5")}, hasError: true},
+ {name: "embedded credentials", rawURL: "https://user:pass@adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, hasError: true},
+ {name: "insecure scheme", rawURL: "http://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, hasError: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ endpoint, err := url.Parse(test.rawURL)
+ require.NoError(t, err)
+ policy := NewNotificationEndpointPolicy(&staticHostResolver{
+ addresses: map[string][]netip.Addr{endpoint.Hostname(): test.addresses},
+ }, nil)
+
+ addresses, err := policy.Validate(context.Background(), endpoint)
+
+ if test.hasError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.addresses, addresses)
+ })
+ }
+}
+```
+
+Add a connection-time test whose resolver returns a public address during the
+first `Validate` call and `127.0.0.1` during `DialContext`. Assert that the
+recording dialer is never invoked. Implement the recording dialer as a local
+function injected through an unexported `dialValidated` helper so the test does
+not make a real network connection.
+
+Add an exact-host allowlist test:
+
+```go
+func TestNotificationEndpointPolicyAllowsPrivateAddressForExactLocalHost(t *testing.T) {
+ endpoint, err := url.Parse("https://adapter-emulator:9091/notifications/gateway-1")
+ require.NoError(t, err)
+ policy := NewNotificationEndpointPolicy(&staticHostResolver{
+ addresses: map[string][]netip.Addr{
+ "adapter-emulator": {netip.MustParseAddr("172.20.0.8")},
+ },
+ }, []string{"adapter-emulator"})
+
+ addresses, err := policy.Validate(context.Background(), endpoint)
+
+ require.NoError(t, err)
+ assert.Equal(t, []netip.Addr{netip.MustParseAddr("172.20.0.8")}, addresses)
+}
+```
+
+Also assert that `adapter-emulator.example.com`, a private IP-literal URL, and
+any non-allowlisted private hostname remain rejected.
+
+- [ ] **Step 2: Run the policy tests and confirm the types are missing**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestNotificationEndpointPolicy' -count=1
+```
+
+Expected: compilation fails because `NotificationEndpointPolicy` and its
+constructor do not exist.
+
+- [ ] **Step 3: Implement reserved-range checks**
+
+Create `api/pkg/services/notification_endpoint_policy.go`. Define the resolver
+interface above and these blocked prefixes:
+
+```go
+var blockedNotificationPrefixes = []netip.Prefix{
+ netip.MustParsePrefix("0.0.0.0/8"),
+ netip.MustParsePrefix("10.0.0.0/8"),
+ netip.MustParsePrefix("100.64.0.0/10"),
+ netip.MustParsePrefix("127.0.0.0/8"),
+ netip.MustParsePrefix("169.254.0.0/16"),
+ netip.MustParsePrefix("172.16.0.0/12"),
+ netip.MustParsePrefix("192.0.0.0/24"),
+ netip.MustParsePrefix("192.0.2.0/24"),
+ netip.MustParsePrefix("192.168.0.0/16"),
+ netip.MustParsePrefix("198.18.0.0/15"),
+ netip.MustParsePrefix("198.51.100.0/24"),
+ netip.MustParsePrefix("203.0.113.0/24"),
+ netip.MustParsePrefix("224.0.0.0/4"),
+ netip.MustParsePrefix("240.0.0.0/4"),
+ netip.MustParsePrefix("::/128"),
+ netip.MustParsePrefix("::1/128"),
+ netip.MustParsePrefix("100::/64"),
+ netip.MustParsePrefix("2001:db8::/32"),
+ netip.MustParsePrefix("fc00::/7"),
+ netip.MustParsePrefix("fe80::/10"),
+ netip.MustParsePrefix("ff00::/8"),
+}
+```
+
+Implement:
+
+```go
+func isPublicNotificationAddress(address netip.Addr) bool {
+ address = address.Unmap()
+ if !address.IsValid() || !address.IsGlobalUnicast() {
+ return false
+ }
+ for _, prefix := range blockedNotificationPrefixes {
+ if prefix.Contains(address) {
+ return false
+ }
+ }
+ return true
+}
+```
+
+`Validate` must verify HTTPS, hostname presence, at least one DNS result, and
+every resolved address passing
+`isPublicNotificationAddress`. Private addresses are accepted only when the
+lowercased hostname exactly matches `allowedPrivateHosts`; never wildcard or
+suffix-match. Wrap resolver and validation errors with stacktrace context
+without including the raw URL.
+
+- [ ] **Step 4: Implement validated dialing**
+
+Add:
+
+```go
+func (policy *NotificationEndpointPolicy) DialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
+ return func(ctx context.Context, network string, address string) (net.Conn, error) {
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ return nil, stacktrace.Propagatef(err, "cannot split notification endpoint address")
+ }
+
+ endpoint := &url.URL{Scheme: "https", Host: net.JoinHostPort(host, port)}
+ addresses, err := policy.Validate(ctx, endpoint)
+ if err != nil {
+ return nil, stacktrace.Propagatef(err, "notification endpoint is not public")
+ }
+
+ var lastErr error
+ for _, resolved := range addresses {
+ connection, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(resolved.String(), port))
+ if dialErr == nil {
+ return connection, nil
+ }
+ lastErr = dialErr
+ }
+ return nil, stacktrace.Propagatef(lastErr, "cannot connect to notification endpoint")
+ }
+}
+```
+
+Factor the final connection loop through an unexported function variable or
+method that accepts a dial function, allowing the DNS-rebinding test to assert
+the selected address without opening a socket. Do not perform a normal second
+hostname dial after validation.
+
+- [ ] **Step 5: Run focused tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestNotificationEndpointPolicy' -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/services/notification_endpoint_policy.go pkg/services/notification_endpoint_policy_test.go
+git add pkg/services/notification_endpoint_policy.go pkg/services/notification_endpoint_policy_test.go
+git commit -m "feat(api): validate adapter endpoints"
+```
+
+---
+
+### Task 3: Add the Transport Client Map
+
+**Files:**
+- Modify: `api/pkg/services/fcm_client.go`
+- Modify: `api/pkg/services/phone_notification_service.go`
+- Modify: `api/pkg/services/phone_notification_service_test.go`
+
+**Interfaces:**
+- Consumes:
+
+```go
+func (phone *entities.Phone) NotificationTransport() (entities.NotificationTransport, error)
+```
+
+- Produces:
+
+```go
+type FCMClient interface {
+ Send(context.Context, *messaging.Message) (string, error)
+}
+```
+
+- [ ] **Step 1: Write failing transport-client map tests**
+
+Create a recording client in `phone_notification_service_test.go`:
+
+```go
+type recordingPhoneNotificationClient struct {
+ message *messaging.Message
+ result string
+ err error
+ calls int
+}
+
+func (client *recordingPhoneNotificationClient) Send(_ context.Context, message *messaging.Message) (string, error) {
+ client.calls++
+ client.message = message
+ return client.result, client.err
+}
+```
+
+Add tests for map-based FCM and HTTP selection, the same message pointer,
+trimmed `message.Token`, absent transport entries, invalid tokens, and nil
+messages.
+
+- [ ] **Step 2: Run the service tests and confirm the map dependency is missing**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
+```
+
+Expected: compilation fails because the service does not accept the client map.
+
+- [ ] **Step 3: Implement focused map-based delivery**
+
+Store `map[entities.NotificationTransport]FCMClient` on
+`PhoneNotificationService` and implement:
+
+```go
+func (service *PhoneNotificationService) sendPhoneNotification(
+ ctx context.Context,
+ phone *entities.Phone,
+ message *messaging.Message,
+) (string, entities.NotificationTransport, error) {
+ // Validate message, classify once, map lookup, set trimmed token, send.
+}
+```
+
+Update comments in `api/pkg/services/fcm_client.go` to describe `FCMClient` as
+the common phone notification transport boundary. Use the Firebase client
+directly; do not add a wrapper or dispatcher.
+
+- [ ] **Step 4: Run focused tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/services/fcm_client.go pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go
+git add pkg/services/fcm_client.go pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go
+git commit -m "refactor(api): dispatch gateway notifications"
+```
+
+---
+
+### Task 4: Implement the SSRF-safe HTTP Sender
+
+**Files:**
+- Create: `api/pkg/services/http_notification_sender.go`
+- Create: `api/pkg/services/http_notification_sender_test.go`
+
+**Interfaces:**
+- Consumes:
+
+```go
+type FCMClient interface {
+ Send(context.Context, *messaging.Message) (string, error)
+}
+```
+
+- Produces:
+
+```go
+type HTTPNotificationSender struct {
+ logger telemetry.Logger
+ client *http.Client
+ retrier *retry.Retrier
+ timeout time.Duration
+}
+
+func NewHTTPNotificationSender(
+ logger telemetry.Logger,
+ client *http.Client,
+) *HTTPNotificationSender
+
+func (sender *HTTPNotificationSender) Send(
+ ctx context.Context,
+ message *messaging.Message,
+) (string, error)
+```
+
+- [ ] **Step 1: Write failing payload and success tests**
+
+Create `api/pkg/services/http_notification_sender_test.go`. Use a custom
+`roundTripFunc`:
+
+```go
+type roundTripFunc func(*http.Request) (*http.Response, error)
+
+func (roundTrip roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
+ return roundTrip(request)
+}
+```
+
+Construct the sender in tests with a reusable retrier configured through
+`newHTTPNotificationRetrier(0)` so retry delay is zero from initialization.
+Use a public address from the test resolver, and a client whose custom
+RoundTripper does not dial.
+
+Assert the request:
+
+```go
+assert.Equal(t, http.MethodPost, request.Method)
+assert.Equal(t, "application/json", request.Header.Get("Content-Type"))
+```
+
+Keep a test-only decoding struct and assert this structure:
+
+```go
+type httpNotificationRequest struct {
+ Message struct {
+ Token string `json:"token"`
+ Data map[string]string `json:"data"`
+ Android struct {
+ Priority string `json:"priority"`
+ TTL string `json:"ttl,omitempty"`
+ } `json:"android"`
+ } `json:"message"`
+}
+```
+
+Return `204 No Content` and assert `Send` succeeds with result `http/success`.
+
+- [ ] **Step 2: Write failing retry-classification tests**
+
+Add table-driven tests for:
+
+- network error then `202`: two calls, success;
+- `408` then `200`: two calls, success;
+- `429` then `204`: two calls, success;
+- `500`, `502`, then `204`: three calls, success;
+- `400`: one call, error;
+- three `503` responses: three calls, error;
+- redirects use Go's default client behavior; do not add terminal redirect
+ handling;
+- response with a body larger than the discard limit: success without reading
+ unbounded content.
+
+Assert retry attempts receive fresh requests and bodies.
+
+- [ ] **Step 3: Run the HTTP sender tests and confirm the type is missing**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestHTTPNotificationSender' -count=1
+```
+
+Expected: compilation fails because `HTTPNotificationSender` does not exist.
+
+- [ ] **Step 4: Implement the FCM-compatible HTTP payload**
+
+Create `api/pkg/services/http_notification_sender.go` and build the one-use
+payload directly with `map[string]any`:
+
+```go
+payload := map[string]any{
+ "message": message,
+}
+```
+
+Obtain the destination from `message.Token`. Rely on
+`messaging.Message.MarshalJSON` and `messaging.AndroidConfig.MarshalJSON` for
+the FCM shape and protobuf TTL, including `10*time.Minute` as `"600s"`. Build a
+new request body for every attempt so retries never reuse a consumed reader.
+Reject nil messages explicitly with a stacktrace-consistent error.
+
+- [ ] **Step 5: Implement bounded retries**
+
+Implement these helpers:
+
+```go
+func isRetryableNotificationStatus(statusCode int) bool {
+ return statusCode == http.StatusRequestTimeout ||
+ statusCode == http.StatusTooManyRequests ||
+ statusCode >= http.StatusInternalServerError
+}
+
+func notificationRetryDelay(attempt uint) time.Duration {
+ return time.Duration(1<<(attempt-1)) * 250 * time.Millisecond
+}
+```
+
+Create one reusable retrier during sender initialization:
+
+```go
+func newHTTPNotificationRetrier(delay time.Duration) *retry.Retrier {
+ return retry.New(
+ retry.Attempts(3),
+ retry.Delay(delay),
+ retry.DelayType(retry.BackOffDelay),
+ retry.LastErrorOnly(true),
+ retry.RetryIf(isRetryableNotificationError),
+ )
+}
+```
+
+Do not configure this retrier with `retry.Context`: each `Send` has a different
+caller context. Instead, check caller cancellation in the operation and return
+a terminal error, while deriving each attempt's five-second context from that
+caller.
+
+Focused sender methods must:
+
+1. parse `message.Token`;
+2. marshal the request body once, then create a fresh reader per request;
+3. create a child context with the configured timeout per attempt;
+4. set `Content-Type`;
+5. call `client.Do`;
+6. close each response body after copying at most 4 KiB to `io.Discard`;
+7. return `http/success` for any `2xx`;
+8. retry only network errors, `408`, `429`, and `5xx` while attempts remain;
+9. treat request construction, caller cancellation, and other statuses as
+ terminal;
+10. return a stacktrace-wrapped error.
+
+Do not use the container's retrying HTTP client; retries belong in this sender
+so status classification and attempt count are explicit.
+
+- [ ] **Step 6: Add heartbeat tests**
+
+Add a heartbeat payload test with `KEY_HEARTBEAT_ID`, high priority, and nil
+TTL; assert the `ttl` field is omitted.
+Use the standard HTTP telemetry and logging path without feature-specific URL
+redaction.
+
+- [ ] **Step 7: Run focused tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestHTTPNotificationSender' -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/services/http_notification_sender.go pkg/services/http_notification_sender_test.go
+git add pkg/services/http_notification_sender.go pkg/services/http_notification_sender_test.go
+git commit -m "feat(api): send notifications to adapters"
+```
+
+---
+
+### Task 5: Integrate Message and Heartbeat Notifications
+
+**Files:**
+- Modify: `api/pkg/services/phone_notification_service.go`
+- Create: `api/pkg/services/phone_notification_service_test.go`
+- Modify: `api/pkg/entities/phone_notification.go`
+
+**Interfaces:**
+- Consumes:
+
+```go
+map[entities.NotificationTransport]FCMClient
+```
+
+- Produces:
+
+```go
+type NotificationEventDispatcher interface {
+ Dispatch(ctx context.Context, event cloudevents.Event) error
+ DispatchWithTimeout(ctx context.Context, event cloudevents.Event, timeout time.Duration) (string, error)
+}
+
+func NewNotificationService(
+ logger telemetry.Logger,
+ tracer telemetry.Tracer,
+ phoneNotificationClients map[entities.NotificationTransport]FCMClient,
+ phoneRepository repositories.PhoneRepository,
+ phoneNotificationRepository repositories.PhoneNotificationRepository,
+ messageSendScheduleRepository repositories.MessageSendScheduleRepository,
+ dispatcher NotificationEventDispatcher,
+) *PhoneNotificationService
+```
+
+- [ ] **Step 1: Write failing service tests with hand-written fakes**
+
+Create `api/pkg/services/phone_notification_service_test.go`.
+
+Define repository fakes by embedding the interfaces and overriding only methods
+used by the tests:
+
+```go
+type phoneNotificationPhoneRepository struct {
+ repositories.PhoneRepository
+ phone *entities.Phone
+ err error
+}
+
+func (repository *phoneNotificationPhoneRepository) LoadByID(
+ _ context.Context,
+ _ entities.UserID,
+ _ uuid.UUID,
+) (*entities.Phone, error) {
+ return repository.phone, repository.err
+}
+
+type phoneNotificationRepository struct {
+ repositories.PhoneNotificationRepository
+ notificationID uuid.UUID
+ status entities.PhoneNotificationStatus
+}
+
+func (repository *phoneNotificationRepository) UpdateStatus(
+ _ context.Context,
+ notificationID uuid.UUID,
+ status entities.PhoneNotificationStatus,
+) error {
+ repository.notificationID = notificationID
+ repository.status = status
+ return nil
+}
+```
+
+Add a fake event dispatcher that records CloudEvents and returns no error. Add
+recording `FCMClient` implementations through a transport-keyed map.
+
+Test `Send` with an HTTPS token and assert:
+
+- `KEY_MESSAGE_ID` equals `params.MessageID.String()`;
+- priority is `normal`;
+- TTL equals `phone.MessageExpirationDuration()`;
+- the same `messaging.Message` pointer reaches the HTTP client;
+- a `message.notification.sent` event is dispatched;
+- phone-notification status becomes sent.
+
+Test an HTTP sender error and assert:
+
+- `message.notification.failed` is dispatched;
+- status becomes failed;
+- the payload error says the adapter endpoint could not be notified;
+- the payload does not tell the user to reinstall Android.
+
+Test an FCM sender error and assert the existing Android reinstallation guidance
+is preserved.
+
+Test `SendHeartbeatFCM` with an HTTPS token and assert:
+
+- `KEY_HEARTBEAT_ID` parses as RFC3339;
+- priority is `high`;
+- TTL is nil;
+- heartbeat sender errors are logged and return nil, preserving current
+ heartbeat behavior.
+
+Also test explicit nil-message handling and a missing transport map entry.
+
+- [ ] **Step 2: Run the service tests and confirm the constructor mismatch**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
+```
+
+Expected: compilation fails because `PhoneNotificationService` still consumes
+`FCMClient` and a concrete `*EventDispatcher`.
+
+- [ ] **Step 3: Replace direct Firebase message creation**
+
+In `api/pkg/services/phone_notification_service.go`:
+
+- construct Firebase `messaging.Message` values directly;
+- replace the direct client with
+ `phoneNotificationClients map[entities.NotificationTransport]FCMClient`;
+- change the constructor to the exact signature above;
+- change `eventDispatcher` to `NotificationEventDispatcher`.
+
+Add a private `sendPhoneNotification` helper that validates the message,
+classifies transport once, performs a map lookup, sets the trimmed token, calls
+the selected client, and returns the transport with the result or error.
+
+For message notifications, construct:
+
+```go
+ttl := phone.MessageExpirationDuration()
+message := &messaging.Message{
+ Data: map[string]string{
+ "KEY_MESSAGE_ID": params.MessageID.String(),
+ },
+ Android: &messaging.AndroidConfig{
+ Priority: "normal",
+ TTL: &ttl,
+ },
+}
+```
+
+For heartbeat notifications, call:
+
+```go
+message := &messaging.Message{
+ Data: map[string]string{
+ "KEY_HEARTBEAT_ID": time.Now().UTC().Format(time.RFC3339),
+ },
+ Android: &messaging.AndroidConfig{Priority: "high"},
+}
+```
+
+- [ ] **Step 4: Add transport-aware failure text**
+
+Use the transport returned by `sendPhoneNotification`; do not classify the
+phone a second time.
+
+For HTTP use:
+
+```go
+msg := fmt.Sprintf(
+ "cannot notify the configured adapter for phone [%s]. Check the adapter URL and availability.",
+ phone.PhoneNumber,
+)
+```
+
+For Firebase preserve:
+
+```go
+msg := fmt.Sprintf(
+ "cannot send notification to your phone [%s]. Reinstall the httpSMS app on your Android phone.",
+ phone.PhoneNumber,
+)
+```
+
+Log the technical wrapped error without logging the raw token. If transport
+classification unexpectedly fails here, send that error through
+`handleNotificationFailed` with a generic notification-configuration message.
+
+- [ ] **Step 5: Update transport-specific comments**
+
+In `api/pkg/entities/phone_notification.go`, change:
+
+```go
+// PhoneNotification represents an FCM notification to a mobile phone
+```
+
+to:
+
+```go
+// PhoneNotification represents a scheduled wake-up notification for a phone gateway.
+```
+
+Update `PhoneNotificationService` and `SendHeartbeatFCM` comments so they refer
+to phone gateway notifications rather than only mobile phones. Keep the method
+name `SendHeartbeatFCM` in this change to avoid an unrelated listener rename.
+
+- [ ] **Step 6: Run focused tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/services -run 'TestPhoneNotificationService|TestHTTPNotificationSender' -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go pkg/entities/phone_notification.go
+git add pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go pkg/entities/phone_notification.go
+git commit -m "feat(api): route phone gateway wake-ups"
+```
+
+---
+
+### Task 6: Validate URL Tokens, Wire Dependencies, and Regenerate Swagger
+
+**Files:**
+- Modify: `api/pkg/validators/phone_handler_validator.go`
+- Create: `api/pkg/validators/phone_handler_validator_test.go`
+- Modify: `api/pkg/di/container.go`
+- Modify: `api/pkg/requests/phone_update_request.go`
+- Modify: `api/pkg/requests/phone_fcm_token_request.go`
+- Modify: `api/docs/docs.go`
+- Modify: `api/docs/swagger.json`
+- Modify: `api/docs/swagger.yaml`
+
+**Interfaces:**
+- Consumes:
+
+```go
+func (container *Container) FCMClient() services.FCMClient
+func NewHTTPNotificationSender(logger telemetry.Logger, client *http.Client) *HTTPNotificationSender
+```
+
+- Produces container factories:
+
+```go
+func (container *Container) NotificationHTTPClient() *http.Client
+func (container *Container) PhoneNotificationClients() map[entities.NotificationTransport]services.FCMClient
+```
+
+- Produces validator constructor:
+
+```go
+func NewPhoneHandlerValidator(
+ logger telemetry.Logger,
+ tracer telemetry.Tracer,
+ scheduleService *services.MessageSendScheduleService,
+ endpointPolicy *services.NotificationEndpointPolicy,
+) *PhoneHandlerValidator
+```
+
+- [ ] **Step 1: Write failing validator tests**
+
+Create `api/pkg/validators/phone_handler_validator_test.go`.
+
+Build the validator with a static public resolver and nil schedule service for
+requests without a schedule ID. Add tests for both `ValidateUpsert` and
+`ValidateFCMToken`:
+
+```go
+func TestPhoneHandlerValidatorAcceptsPublicHTTPSNotificationURL(t *testing.T) {
+ validator := newPhoneHandlerValidatorWithAddresses(map[string][]netip.Addr{
+ "adapter.example.com": {netip.MustParseAddr("8.8.8.8")},
+ })
+
+ errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
+ PhoneNumber: "+18005550199",
+ FcmToken: "https://adapter.example.com/notify",
+ SIM: entities.SIM1.String(),
+ })
+
+ assert.Empty(t, errors)
+}
+```
+
+Add rejection tests for `http://`, loopback resolution, private resolution,
+mixed public/private resolution, and malformed HTTPS.
+Add an opaque FCM token test to prove the resolver is not required for Firebase
+tokens.
+
+- [ ] **Step 2: Run validator tests and confirm unsafe URLs are accepted**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/validators -run 'TestPhoneHandlerValidator.*Notification' -count=1
+```
+
+Expected: tests fail because the validator only checks token length.
+
+- [ ] **Step 3: Inject and apply endpoint policy**
+
+Add `endpointPolicy *services.NotificationEndpointPolicy` to
+`PhoneHandlerValidator` and its constructor.
+
+Add:
+
+```go
+func (validator *PhoneHandlerValidator) validateNotificationToken(
+ ctx context.Context,
+ token string,
+ result url.Values,
+) {
+ token = strings.TrimSpace(token)
+ if token == "" {
+ return
+ }
+
+ phone := &entities.Phone{FcmToken: &token}
+ transport, err := phone.NotificationTransport()
+ if err != nil {
+ result.Add("fcm_token", err.Error())
+ return
+ }
+ if transport != entities.NotificationTransportHTTP {
+ return
+ }
+
+ endpoint, err := phone.NotificationURL()
+ if err != nil {
+ result.Add("fcm_token", err.Error())
+ return
+ }
+ if _, err = validator.endpointPolicy.Validate(ctx, endpoint); err != nil {
+ result.Add("fcm_token", "fcm_token must be a public HTTPS adapter URL")
+ }
+}
+```
+
+Call it after structural validation succeeds in both `ValidateUpsert` and
+`ValidateFCMToken`. Change `ValidateFCMToken` to use its context argument.
+
+- [ ] **Step 4: Add SSRF-safe container factories**
+
+In `api/pkg/di/container.go`, add:
+
+```go
+func (container *Container) NotificationEndpointPolicy() *services.NotificationEndpointPolicy {
+ if container.notificationEndpointPolicy != nil {
+ return container.notificationEndpointPolicy
+ }
+
+ allowedPrivateHosts := []string{} // Superseded: no private-host allowlist is configured.
+ container.notificationEndpointPolicy = services.NewNotificationEndpointPolicy(
+ net.DefaultResolver,
+ allowedPrivateHosts,
+ )
+ return container.notificationEndpointPolicy
+}
+```
+
+Add a client factory:
+
+```go
+func (container *Container) NotificationHTTPClient() *http.Client {
+ policy := container.NotificationEndpointPolicy()
+ transport := &http.Transport{
+ Proxy: nil,
+ DialContext: policy.DialContext(&net.Dialer{
+ Timeout: 5 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }),
+ ForceAttemptHTTP2: true,
+ TLSClientConfig: &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ },
+ }
+
+ return &http.Client{
+ Transport: otelroundtripper.New(
+ otelroundtripper.WithName("phone_notification_http"),
+ otelroundtripper.WithParent(transport),
+ otelroundtripper.WithMeter(otel.GetMeterProvider().Meter(container.projectID)),
+ ),
+ }
+}
+```
+
+Do not set a client-wide timeout; the sender creates the approved five-second
+context for each attempt.
+
+Add:
+
+```go
+func (container *Container) PhoneNotificationClients() map[entities.NotificationTransport]services.FCMClient {
+ return map[entities.NotificationTransport]services.FCMClient{
+ entities.NotificationTransportFCM: container.FCMClient(),
+ entities.NotificationTransportHTTP: services.NewHTTPNotificationSender(
+ container.Logger(),
+ container.NotificationHTTPClient(),
+ ),
+ }
+}
+```
+
+Add this field to `Container`:
+
+```go
+notificationEndpointPolicy *services.NotificationEndpointPolicy
+```
+
+The cached policy ensures validation and connection-time checks use the same
+allowlist. Do not cache per-request sender state.
+
+- [ ] **Step 5: Wire service and validator constructors**
+
+Change `container.NotificationService()` to pass
+`container.PhoneNotificationClients()`.
+
+Find `container.PhoneHandlerValidator()` and pass
+`container.NotificationEndpointPolicy()` as its fourth argument. Keep existing
+logger, tracer, and schedule-service arguments unchanged.
+
+- [ ] **Step 6: Run focused package tests**
+
+Run:
+
+```bash
+cd api
+go test ./pkg/entities ./pkg/services ./pkg/validators ./pkg/di -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Update API descriptions**
+
+In both phone request structs, replace the generic FCM token comment with:
+
+```go
+// FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.
+FcmToken string `json:"fcm_token" example:"https://adapter.example.com/notifications"`
+```
+
+Update handler Swagger descriptions for phone upsert and FCM-token upsert to
+state that URL-backed phones receive FCM-compatible HTTP wake-ups. Do not add or
+rename routes or JSON fields.
+
+- [ ] **Step 8: Regenerate Swagger**
+
+Run:
+
+```bash
+cd api
+swag init --requiredByDefault --parseDependency --parseInternal
+```
+
+Expected: `docs/docs.go`, `docs/swagger.json`, and `docs/swagger.yaml` update
+with the dual-purpose `fcm_token` descriptions.
+
+- [ ] **Step 9: Run the complete API test suite**
+
+Run:
+
+```bash
+cd api
+go test ./...
+```
+
+Expected: PASS.
+
+- [ ] **Step 10: Build the API**
+
+Run:
+
+```bash
+cd api
+go build -o ./tmp/main.exe .
+```
+
+Expected: build succeeds.
+
+- [ ] **Step 11: Inspect the final diff for forbidden changes**
+
+Run:
+
+```bash
+git diff --check
+git diff --stat
+git grep -n "fcm_token" -- api/pkg/entities/phone.go api/pkg/requests/phone_update_request.go api/pkg/requests/phone_fcm_token_request.go
+```
+
+Confirm:
+
+- no database field or migration was added;
+- `fcm_token` remains the persisted/API field;
+- no message content or API key is added to the HTTP callback payload;
+- scheduling and repository code are unchanged;
+
+- [ ] **Step 12: Format and commit**
+
+Run:
+
+```bash
+cd api
+go-fumpt -w pkg/validators/phone_handler_validator.go pkg/validators/phone_handler_validator_test.go pkg/di/container.go pkg/requests/phone_update_request.go pkg/requests/phone_fcm_token_request.go
+git add pkg/validators/phone_handler_validator.go pkg/validators/phone_handler_validator_test.go pkg/di/container.go pkg/requests/phone_update_request.go pkg/requests/phone_fcm_token_request.go pkg/handlers/phone_handler.go docs/docs.go docs/swagger.json docs/swagger.yaml
+git commit -m "feat(api): enable URL-backed phone gateways"
+```
+
+---
+
+### Task 7: Build the Adapter Emulator and End-to-End Scenarios
+
+**Files:**
+- Create: `tests/adapter-emulator/Dockerfile`
+- Create: `tests/adapter-emulator/go.mod`
+- Create: `tests/adapter-emulator/main.go`
+- Create: `tests/adapter-emulator/emulator.go`
+- Create: `tests/adapter-emulator/api_client.go`
+- Create: `tests/adapter-emulator/notification_handler.go`
+- Create: `tests/adapter-emulator/control_handler.go`
+- Create: `tests/adapter_integration_test.go`
+- Create: `tests/generate-adapter-certificates.sh`
+- Modify: `tests/docker-compose.yml`
+- Modify: `tests/.env.test`
+- Modify: `tests/helpers_test.go`
+- Modify: `tests/README.md`
+- Modify: `.github/workflows/api.yml`
+- Modify: `.gitignore`
+
+**Interfaces:**
+- Emulator callback: `POST https://adapter-emulator:9091/notifications/{gatewayID}`
+- Emulator control:
+
+```text
+PUT http://localhost:9092/test/gateways/{gatewayID}
+POST http://localhost:9092/test/gateways/{gatewayID}/incoming
+GET http://localhost:9092/test/gateways/{gatewayID}/notifications
+GET http://localhost:9092/health
+```
+
+- Gateway registration:
+
+```go
+type gatewayRegistration struct {
+ PhoneNumber string `json:"phone_number"`
+ PhoneAPIKey string `json:"phone_api_key"`
+}
+```
+
+- Incoming control payload:
+
+```go
+type incomingMessageRequest struct {
+ Contact string `json:"contact"`
+ Content string `json:"content"`
+ Encrypted bool `json:"encrypted"`
+}
+```
+
+- Callback record:
+
+```go
+type notificationRecord struct {
+ GatewayID string `json:"gateway_id"`
+ Data map[string]string `json:"data"`
+ MessageID string `json:"message_id,omitempty"`
+ Kind string `json:"kind"`
+ Processed bool `json:"processed"`
+ Error string `json:"error,omitempty"`
+}
+```
+
+- [ ] **Step 1: Generate throwaway TLS material**
+
+Create `tests/generate-adapter-certificates.sh`:
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+output_dir="${1:-certs}"
+mkdir -p "$output_dir"
+
+openssl req -x509 -newkey rsa:2048 -nodes \
+ -keyout "$output_dir/ca-key.pem" \
+ -out "$output_dir/ca.pem" \
+ -days 2 \
+ -subj "/CN=httpSMS integration adapter CA"
+
+openssl req -newkey rsa:2048 -nodes \
+ -keyout "$output_dir/server-key.pem" \
+ -out "$output_dir/server.csr" \
+ -subj "/CN=adapter-emulator"
+
+cat >"$output_dir/server.ext" <<'EOF'
+subjectAltName=DNS:adapter-emulator
+extendedKeyUsage=serverAuth
+EOF
+
+openssl x509 -req \
+ -in "$output_dir/server.csr" \
+ -CA "$output_dir/ca.pem" \
+ -CAkey "$output_dir/ca-key.pem" \
+ -CAcreateserial \
+ -out "$output_dir/server.pem" \
+ -days 2 \
+ -extfile "$output_dir/server.ext"
+```
+
+Add `tests/certs/` to `.gitignore`. Do not commit generated keys or
+certificates.
+
+- [ ] **Step 2: Scaffold the isolated emulator module**
+
+Create `tests/adapter-emulator/go.mod`:
+
+```go
+module github.com/NdoleStudio/httpsms/tests/adapter-emulator
+
+go 1.25.0
+```
+
+Use only the standard library. Run:
+
+```bash
+cd tests/adapter-emulator
+go mod tidy
+```
+
+Expected: the command succeeds without adding dependencies.
+
+- [ ] **Step 3: Implement emulator state and deduplication**
+
+In `tests/adapter-emulator/emulator.go`, implement:
+
+```go
+type gateway struct {
+ PhoneNumber string
+ PhoneAPIKey string
+}
+
+type emulator struct {
+ apiBaseURL string
+ client *http.Client
+ mu sync.RWMutex
+ gateways map[string]gateway
+ records []*notificationRecord
+}
+
+func newEmulator(apiBaseURL string, client *http.Client) *emulator {
+ return &emulator{
+ apiBaseURL: strings.TrimRight(apiBaseURL, "/"),
+ client: client,
+ gateways: make(map[string]gateway),
+ }
+}
+```
+
+Add locked methods to register a gateway, load a gateway, record each callback,
+mark it processed/failed, and list copied records for one gateway.
+
+- [ ] **Step 4: Implement existing phone API calls**
+
+In `tests/adapter-emulator/api_client.go`, implement:
+
+```go
+func (emulator *emulator) fetchOutstanding(
+ ctx context.Context,
+ gateway gateway,
+ messageID string,
+) (map[string]any, error)
+
+func (emulator *emulator) fireMessageEvent(
+ ctx context.Context,
+ gateway gateway,
+ messageID string,
+ eventName string,
+) error
+
+func (emulator *emulator) receiveMessage(
+ ctx context.Context,
+ gateway gateway,
+ request incomingMessageRequest,
+) (map[string]any, error)
+
+func (emulator *emulator) storeHeartbeat(
+ ctx context.Context,
+ gateway gateway,
+) error
+```
+
+Use the existing routes:
+
+```text
+GET /v1/messages/outstanding?message_id={messageID}
+POST /v1/messages/{messageID}/events
+POST /v1/messages/receive
+POST /v1/heartbeats
+```
+
+Every request sets `x-api-key`. Message events use `SENT` then `DELIVERED` with
+UTC RFC3339 timestamps. Incoming messages use the gateway phone number as `to`,
+the control request contact as `from`, `SIM1`, and the requested encryption
+flag. Return contextual `fmt.Errorf` errors because the emulator module
+intentionally does not depend on the production API module.
+
+- [ ] **Step 5: Implement FCM-compatible callback handling**
+
+In `tests/adapter-emulator/notification_handler.go`, decode:
+
+```go
+type callbackEnvelope struct {
+ Message struct {
+ Token string `json:"token"`
+ Data map[string]string `json:"data"`
+ } `json:"message"`
+}
+```
+
+The handler must:
+
+1. load `gatewayID` from the route;
+2. return `404` for unknown gateways;
+3. return `400` for unsupported data;
+4. record every callback;
+5. for `KEY_MESSAGE_ID`, fetch outstanding, fire `SENT`, fire `DELIVERED`, and
+ mark the record processed with kind `message`;
+6. for `KEY_HEARTBEAT_ID`, store a heartbeat and mark the record processed with
+ kind `heartbeat`;
+7. return `500` and retain the error string in the record when processing
+ fails, allowing the API sender's retry behavior to be exercised.
+
+Processing may be synchronous because the API accepts any `2xx` and the
+integration stack controls response time.
+
+- [ ] **Step 6: Implement control and server endpoints**
+
+In `tests/adapter-emulator/control_handler.go`, implement registration,
+incoming-message, record listing, and health handlers using `http.ServeMux`.
+
+In `main.go`, read:
+
+```text
+API_BASE_URL=http://api:8000
+ADAPTER_TLS_CERT=/certs/server.pem
+ADAPTER_TLS_KEY=/certs/server-key.pem
+```
+
+Start:
+
+- HTTPS callback server on `:9091`;
+- HTTP control server on `:9092`.
+
+Use `http.Server` with finite `ReadHeaderTimeout`, `ReadTimeout`,
+`WriteTimeout`, and `IdleTimeout`. Shut both servers down on SIGINT/SIGTERM.
+
+- [ ] **Step 7: Add the emulator container**
+
+Create `tests/adapter-emulator/Dockerfile`:
+
+```dockerfile
+FROM golang:1.25-alpine AS build
+WORKDIR /src
+COPY go.mod ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=0 go build -o /out/adapter-emulator .
+
+FROM alpine:3.22
+RUN adduser -D -u 10001 app
+USER app
+COPY --from=build /out/adapter-emulator /usr/local/bin/adapter-emulator
+ENTRYPOINT ["adapter-emulator"]
+```
+
+In `tests/docker-compose.yml`, add `adapter-emulator`:
+
+```yaml
+adapter-emulator:
+ build:
+ context: ./adapter-emulator
+ ports:
+ - "9092:9092"
+ environment:
+ API_BASE_URL: http://api:8000
+ ADAPTER_TLS_CERT: /certs/server.pem
+ ADAPTER_TLS_KEY: /certs/server-key.pem
+ volumes:
+ - ./certs:/certs:ro
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://localhost:9092/health"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+```
+
+Make `api` depend on the healthy emulator. Mount `./certs/ca.pem` into the API
+container and set:
+
+```yaml
+SSL_CERT_FILE: /adapter-certs/ca.pem
+```
+
+Do not add a private-host allowlist; Docker DNS is used through the standard Go
+HTTP transport. Keep `SSL_CERT_FILE` so the emulator certificate remains
+trusted.
+
+- [ ] **Step 8: Add adapter test helpers**
+
+In `tests/helpers_test.go`, add `adapterControlURL`,
+`systemAPIKey`, and:
+
+```go
+type adapterTestPhone struct {
+ testPhone
+ PhoneID string
+ GatewayID string
+}
+
+func setupAdapterPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) adapterTestPhone
+func dispatchInternalEvent(ctx context.Context, t *testing.T, event map[string]any)
+func waitForAdapterMessageRecords(t *testing.T, gatewayID string, messageID string, timeout time.Duration) []notificationRecord
+func waitForAdapterHeartbeatRecord(t *testing.T, gatewayID string, timeout time.Duration) notificationRecord
+func triggerAdapterIncoming(ctx context.Context, t *testing.T, phone adapterTestPhone, contact string, content string) string
+```
+
+`setupAdapterPhone` must:
+
+1. generate a gateway UUID and phone number;
+2. create a phone API key;
+3. register the gateway with the emulator control API;
+4. use callback URL
+ `https://adapter-emulator:9091/notifications/{gatewayID}`;
+5. upsert the phone through the user API and capture its phone ID;
+6. bind the same callback through the phone API-key route;
+7. wait for phone authorization using the existing helper.
+
+`dispatchInternalEvent` posts a valid CloudEvent JSON body to `/v1/events` with
+`x-api-key: system-user-api-key`.
+
+- [ ] **Step 9: Write the outgoing adapter integration test**
+
+Create `tests/adapter_integration_test.go`:
+
+```go
+func TestAdapterGatewayOutgoingMessage(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+ content := "Adapter outgoing " + randomEncryptionKey()
+
+ response, httpResponse, err := newAPIClient().Messages.Send(ctx, &httpsms.MessageSendParams{
+ From: phone.PhoneNumber,
+ To: contact,
+ Content: content,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, httpResponse.HTTPResponse.StatusCode)
+
+ messageID := response.Data.ID.String()
+ message := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second)
+
+ assert.Equal(t, phone.PhoneNumber, message.Owner)
+ assert.Equal(t, contact, message.Contact)
+ assert.Equal(t, content, message.Content)
+ records := waitForAdapterMessageRecords(t, phone.GatewayID, messageID, 30*time.Second)
+ require.Len(t, records, 1)
+ assert.Equal(t, "message", records[0].Kind)
+ assert.True(t, records[0].Processed)
+ assert.Equal(t, messageID, records[0].Data["KEY_MESSAGE_ID"])
+}
+```
+
+The record-list helper queries by message ID. Assert one processed record for
+the successful callback flow.
+
+- [ ] **Step 10: Write the incoming adapter integration test**
+
+Add:
+
+```go
+func TestAdapterGatewayIncomingMessage(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+ content := "Adapter incoming " + randomEncryptionKey()
+
+ messageID := triggerAdapterIncoming(ctx, t, phone, contact, content)
+ message := pollMessageStatus(ctx, t, messageID, "received", 15*time.Second)
+
+ assert.Equal(t, phone.PhoneNumber, message.Owner)
+ assert.Equal(t, contact, message.Contact)
+ assert.Equal(t, content, message.Content)
+ assert.Equal(t, "received", message.Status)
+}
+```
+
+This test must call the emulator control endpoint; it must not post
+`/v1/messages/receive` directly from the test runner.
+
+- [ ] **Step 11: Write the heartbeat callback integration test**
+
+Add:
+
+```go
+func TestAdapterGatewayHeartbeatWakeUp(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ monitorID := uuid.NewString()
+
+ dispatchInternalEvent(ctx, t, map[string]any{
+ "specversion": "1.0",
+ "id": uuid.NewString(),
+ "source": "/tests/adapter-emulator",
+ "type": "phone.heartbeat.missed",
+ "time": time.Now().UTC().Format(time.RFC3339),
+ "datacontenttype": "application/json",
+ "data": map[string]any{
+ "phone_id": phone.PhoneID,
+ "user_id": "test-user-id",
+ "last_heartbeat_timestamp": time.Now().UTC().Add(-20 * time.Minute).Format(time.RFC3339),
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ "monitor_id": monitorID,
+ "owner": phone.PhoneNumber,
+ },
+ })
+
+ record := waitForAdapterHeartbeatRecord(t, phone.GatewayID, 30*time.Second)
+ assert.Equal(t, "heartbeat", record.Kind)
+ assert.NotEmpty(t, record.Data["KEY_HEARTBEAT_ID"])
+
+ heartbeats, response, err := newAPIClient().Heartbeats.Index(ctx, &httpsms.HeartbeatIndexParams{
+ Owner: phone.PhoneNumber,
+ Limit: 1,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
+ require.NotEmpty(t, heartbeats.Data)
+ assert.Equal(t, phone.PhoneNumber, heartbeats.Data[0].Owner)
+}
+```
+
+The direct internal event avoids waiting for the production 16-minute monitor
+interval while still exercising `PhoneNotificationListener`,
+`PhoneNotificationService`, the HTTP client, the emulator callback, and the
+existing heartbeat API.
+
+- [ ] **Step 12: Update local and CI commands**
+
+In `.github/workflows/api.yml`, after Firebase credential generation, add:
+
+```yaml
+- name: Generate adapter certificates
+ run: bash tests/generate-adapter-certificates.sh tests/certs
+```
+
+Update `tests/README.md` architecture, project tree, coverage checklist,
+troubleshooting logs, and setup commands. The documented local sequence must
+run both credential scripts before `docker compose up`.
+
+- [ ] **Step 13: Run the complete integration suite**
+
+Run:
+
+```bash
+cd tests
+bash generate-firebase-credentials.sh firebase-credentials.json
+bash generate-adapter-certificates.sh certs
+docker compose up -d --build --wait
+docker compose wait seed
+go test -v -timeout 300s ./...
+docker compose down -v
+```
+
+Expected: existing FCM/WireMock tests and all three adapter tests pass.
+
+- [ ] **Step 14: Inspect failure logs if a scenario times out**
+
+Run:
+
+```bash
+cd tests
+docker compose logs --tail 200 api adapter-emulator
+```
+
+The outgoing logs must show callback receipt, outstanding fetch, `SENT`, and
+`DELIVERED`. Incoming logs must show the emulator calling the receive route.
+Heartbeat logs must show `KEY_HEARTBEAT_ID` and a successful heartbeat POST.
+
+- [ ] **Step 15: Commit integration coverage**
+
+Run:
+
+```bash
+git add .gitignore .github/workflows/api.yml tests
+git commit -m "test(api): cover URL-backed phone gateways"
+```
diff --git a/docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md b/docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md
new file mode 100644
index 000000000..5d833dfb0
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md
@@ -0,0 +1,514 @@
+# URL-backed Phone Notification Adapter
+
+- Date: 2026-09-02
+- Status: Approved (design)
+- Scope: `api/` Go backend plus `tests/` integration infrastructure and API CI.
+ Web and Android clients are unchanged.
+
+## Problem
+
+httpSMS currently wakes an Android phone through Firebase Cloud Messaging when
+an outgoing message is ready. The phone then fetches the outstanding message,
+sends it, and reports sent, delivered, or failed events through the phone API.
+
+Users should be able to register a non-Android gateway, such as a WhatsApp
+adapter, as a phone number. The gateway must reuse the existing message queue,
+send schedules, per-phone backpressure, expiration, retry, incoming-message,
+and status-event flows. The only behavioral difference is how httpSMS wakes the
+gateway: when the stored `fcm_token` is a URL, httpSMS calls that URL instead of
+Firebase.
+
+Customer-controlled callback URLs create additional security and delivery
+requirements:
+
+- callbacks are wake-up hints, not proof that a message was sent;
+- callback delivery is at least once, so adapters must tolerate duplicate
+ wake-up hints;
+- transient endpoint failures need bounded retries;
+- existing Android registrations must retain their current behavior.
+
+## Decisions
+
+- Reuse the existing `Phone.FcmToken` database field and `fcm_token` API field.
+ Do not add transport or callback URL columns.
+- Determine transport through helper methods on `Phone`; callers do not inspect
+ or parse `FcmToken` directly.
+- A valid `https://` URL with a hostname selects HTTP delivery. Non-URL tokens
+ select Firebase. URL-like but malformed or unsupported values are rejected.
+- Inject phone notification clients as a map keyed by
+ `entities.NotificationTransport`; domain events continue to use the existing
+ `EventDispatcher`.
+- Send both outstanding-message and heartbeat notifications to URL-backed
+ phones.
+- POST an FCM-compatible JSON envelope to adapter endpoints.
+- Treat any `2xx` response as successful wake-up acceptance and ignore its
+ body.
+- Make up to three total HTTP attempts with `retry-go/v5` and a five-second
+ timeout per attempt.
+- Retry network failures, HTTP `408`, HTTP `429`, and `5xx` responses. Other
+ non-`2xx` responses fail immediately.
+- After callback retries are exhausted, use the current notification failure
+ path and mark the message failed.
+- Do not sign or authenticate callback requests. The payload contains no
+ message content or API credentials.
+- Use a standard OpenTelemetry-wrapped `http.Client` without endpoint DNS/IP
+ filtering, custom dialing, or a private-host allowlist.
+- Preserve all existing phone API-key authorization and message-processing
+ behavior.
+
+## Existing Flow
+
+The current backend already isolates scheduling from phone wake-up delivery:
+
+1. `MessageService.SendMessage` creates the outgoing message event using the
+ phone's configured send attempts, SIM, and rate settings.
+2. `PhoneNotificationService.Schedule` persists a `PhoneNotification` and uses
+ `PhoneNotificationRepository.Schedule` or `ScheduleExact` to apply
+ per-minute limits and send schedules.
+3. `message.notification.send` invokes `PhoneNotificationService.Send`.
+4. The service sends an FCM data notification containing `KEY_MESSAGE_ID`.
+5. A successful notification increments the message send-attempt count and
+ schedules the existing expiration check.
+6. The Android phone fetches
+ `GET /v1/messages/outstanding?message_id=` using a phone API key scoped
+ to its number.
+7. The phone reports sent, delivered, or failed events and submits inbound
+ messages through the existing phone API routes.
+
+The adapter feature changes step 4 only. The same dispatcher also applies to
+the heartbeat notification currently sent by `SendHeartbeatFCM`.
+
+## Design
+
+### 1. Phone transport helpers
+
+Add helpers to `api/pkg/entities/phone.go` that classify and expose the
+notification destination while continuing to store only `FcmToken`.
+
+The helpers provide three outcomes:
+
+- **Firebase:** the token has no URL syntax and is passed unchanged to Firebase.
+- **HTTP:** the token is an absolute, syntactically valid `https://` URL.
+- **Invalid:** the value is URL-like but malformed, uses another scheme, or has
+ no hostname.
+
+Use these entity-level types and methods:
+
+```go
+type NotificationTransport string
+
+const (
+ NotificationTransportFCM NotificationTransport = "fcm"
+ NotificationTransportHTTP NotificationTransport = "http"
+)
+
+func (phone *Phone) NotificationTransport() (NotificationTransport, error)
+func (phone *Phone) NotificationURL() (*url.URL, error)
+```
+
+`NotificationTransport` returns an error for a missing token or a URL-like
+invalid token. `NotificationURL` succeeds only for the HTTP transport.
+`PhoneNotificationService`, validation, and tests must not duplicate
+string-prefix checks.
+
+A token is considered URL-like when it declares a URI scheme. This prevents an
+invalid `http://`, `ftp://`, or malformed HTTPS endpoint from falling through
+to Firebase as if it were an FCM token. Ordinary FCM tokens remain opaque.
+
+### 2. Transport client map
+
+Use the existing `FCMClient` contract for every phone notification transport:
+
+```go
+type FCMClient interface {
+ Send(context.Context, *messaging.Message) (string, error)
+}
+```
+
+`PhoneNotificationService` receives
+`map[entities.NotificationTransport]FCMClient` and constructs
+`messaging.Message` directly. A focused private helper:
+
+1. rejects a nil message with a stacktrace error;
+2. determines the phone transport once;
+3. looks up the matching client in the map;
+4. trims the configured token into `message.Token`;
+5. passes the same message pointer to the client;
+6. returns the selected transport with the result or error for existing
+ Firebase-versus-HTTP failure guidance.
+
+The result string is used only by existing notification event bookkeeping.
+Firebase keeps the message name returned by the SDK. HTTP delivery uses a
+generated identifier that does not expose the callback URL.
+
+### 3. Firebase client
+
+The Firebase map entry is the existing production or emulator `FCMClient`
+directly. There is no wrapper sender and no separate dispatcher class.
+
+The production Firebase client and emulator client remain available. Android
+tokens follow the same SDK path, payload keys, priorities, TTL values, success
+events, and failure events as before.
+
+### 4. HTTP sender and request contract
+
+The HTTP sender posts `Content-Type: application/json` with an FCM-compatible
+envelope:
+
+```json
+{
+ "message": {
+ "token": "https://adapter.example.com/notifications",
+ "data": {
+ "KEY_MESSAGE_ID": "32343a19-da5e-4b1b-a767-3298a73703cb"
+ },
+ "android": {
+ "priority": "normal",
+ "ttl": "600s"
+ }
+ }
+}
+```
+
+Heartbeat notifications use the same shape with:
+
+```json
+{
+ "data": {
+ "KEY_HEARTBEAT_ID": "2026-09-02T19:22:20Z"
+ },
+ "android": {
+ "priority": "high"
+ }
+}
+```
+
+Adapters should depend on `message.data`; the Android object exists for payload
+compatibility and communicates priority and expiration hints.
+
+`HTTPNotificationSender` obtains the destination from `message.Token` and
+marshals `map[string]any{"message": message}`. Firebase's
+`messaging.Message` and `messaging.AndroidConfig.MarshalJSON` own the FCM JSON
+shape and protobuf duration formatting, including a ten-minute TTL as `600s`.
+
+Every request includes:
+
+```text
+Content-Type: application/json
+```
+
+`HTTPNotificationSender` returns the stable result `http/success` after any
+successful callback response. The persisted `PhoneNotification.ID` remains in
+the existing repository and event flow but is not sent to the callback.
+
+The response contract is deliberately small:
+
+- any `2xx` status means the adapter accepted the wake-up;
+- response headers and body do not affect message state;
+- the sender reads at most a small bounded amount needed to safely reuse or
+ close the connection, then discards the body.
+
+The callback is not sent the message content, phone API key, user ID, or
+credentials.
+
+### 5. HTTP retry and state semantics
+
+Each HTTP delivery makes no more than three total attempts. Each attempt has a
+five-second context timeout and uses short exponential backoff.
+
+Retryable failures are:
+
+- connection, DNS, TLS, and timeout failures;
+- HTTP `408 Request Timeout`;
+- HTTP `429 Too Many Requests`;
+- HTTP `5xx`.
+
+Other non-`2xx` statuses are terminal for that notification and are not
+retried. Ignore `Retry-After`; use the sender's bounded exponential backoff so
+a customer response cannot extend the listener into an unbounded worker.
+
+When a callback returns `2xx`, `PhoneNotificationService` uses its existing
+success path:
+
+1. dispatch `message.notification.sent`;
+2. set the `PhoneNotification` status to sent;
+3. increment the message send-attempt count;
+4. schedule the configured message expiration check.
+
+This means callback success is only proof that the adapter was notified. The
+message remains pending/scheduled until the adapter fetches it, at which point
+the existing `message.phone.sending` path runs.
+
+When callback delivery reaches a terminal failure or exhausts retries,
+`PhoneNotificationService` uses its existing failed-notification path:
+
+1. dispatch `message.notification.failed`;
+2. set the `PhoneNotification` status to failed;
+3. store a failed message event through the existing message listener.
+
+The HTTP-specific error message tells the user that the configured adapter
+endpoint could not be notified. It must not reuse the current Android
+reinstallation guidance.
+
+### 6. At-least-once delivery
+
+HTTP wake-up delivery is at least once. A request may reach the adapter even if
+httpSMS observes a timeout or connection failure while receiving the response.
+The retry can therefore deliver the same wake-up payload again.
+
+Adapters must:
+
+- treat callbacks as hints to fetch work, not as message content;
+- tolerate repeated callbacks and use the outstanding message state and
+ message ID to avoid sending external messages twice;
+- retain their own provider-level idempotency and reconciliation where the
+ external channel supports it.
+
+httpSMS does not add a new acknowledgement endpoint. Fetching the outstanding
+message and posting existing message events remain the source of truth.
+
+### 7. Adapter API flow
+
+A URL-backed adapter uses the existing public API in the same way as the
+Android gateway:
+
+1. A user creates or updates a phone with an E.164 number and sets `fcm_token`
+ to the adapter's HTTPS URL.
+2. The user creates a phone API key assigned to that phone/number and configures
+ the adapter with it.
+3. httpSMS schedules outgoing messages with the existing rate limit and send
+ schedule.
+4. When a message becomes due, httpSMS POSTs the callback containing
+ `KEY_MESSAGE_ID`.
+5. The adapter fetches the message through `/v1/messages/outstanding` using its
+ phone API key.
+6. The adapter sends the message through WhatsApp or another external channel.
+7. The adapter posts the existing sent, delivered, or failed message events.
+8. The adapter posts inbound messages through `/v1/messages/receive`.
+9. For `KEY_HEARTBEAT_ID`, the adapter performs the same heartbeat callback flow
+ expected from the Android application.
+
+The phone API key's existing phone-number scope prevents an adapter from
+fetching messages belonging to another number. The adapter must not use a
+general user API key for gateway operations.
+
+Encrypted message content remains unchanged. If a user enables encryption, the
+adapter is responsible for implementing the same compatible encryption and
+decryption behavior expected of the Android gateway.
+
+### 8. HTTP client and retry ownership
+
+Phone registration validates only URL syntax, the HTTPS scheme, and the
+presence of a hostname. URL user information remains valid. The feature does
+not perform endpoint DNS/IP classification, custom dialing, or private-host
+allowlisting.
+
+`Container.NotificationHTTPClient` is a standard `http.Client` using the
+existing webhook-style `go-otelroundtripper` pattern without transport-level
+retries or custom URL redaction. The client preserves Go's default transport
+and redirect behavior.
+
+`HTTPNotificationSender` creates one reusable `retry-go/v5` retrier during
+initialization. The retrier owns exactly three total attempts and exponential
+backoff, but does not bind a caller context because it is reused across sends.
+Each delivery checks its caller context, creates a fresh request and body, and
+applies a five-second child context per attempt. Payload encoding, request
+creation, one-attempt delivery, and retry configuration remain focused
+operations.
+
+### 9. Validation and API compatibility
+
+Keep these public fields and routes unchanged:
+
+- `Phone.FcmToken` / JSON `fcm_token`;
+- `PUT /v1/phones`;
+- `PUT /v1/phones/fcm-token`;
+- all outstanding-message, event, receive-message, and heartbeat routes.
+
+Extend phone validation only when `fcm_token` is URL-like:
+
+- enforce valid HTTPS syntax and require a hostname;
+- preserve the existing maximum token length;
+- return field-level `fcm_token` validation errors for malformed or non-HTTPS
+ URL-like values.
+
+Opaque FCM token validation remains unchanged. Existing stored Android tokens
+require no migration.
+
+Update request and Swagger descriptions to explain that `fcm_token` accepts
+either an FCM registration token or an HTTPS adapter callback URL.
+Regenerate Swagger documentation after implementation.
+
+### 10. Observability
+
+Use the existing request, database, and OpenTelemetry logging behavior without
+special redaction for notification tokens or callback URLs. Rely on the
+existing OpenTelemetry HTTP round-tripper for outbound request spans and
+metrics; do not add sender-specific attempt spans or metrics.
+
+## Components and Expected Files
+
+Implementation is expected to touch:
+
+- `api/pkg/entities/phone.go` for transport and URL helpers;
+- `api/pkg/validators/phone_handler_validator.go` for URL-token validation;
+- `api/pkg/services/phone_notification_service.go` for transport-client lookup,
+ message construction, and existing state transitions;
+- `api/pkg/services/fcm_client.go` for the common transport-client contract and
+ Firebase implementation;
+- `api/pkg/services/http_notification_sender.go` for HTTP payload encoding and
+ delivery;
+- `api/pkg/services/emulator_fcm_client.go` only as needed to preserve the
+ emulator behind the adapted interface;
+- `api/pkg/di/container.go` for dispatcher, OpenTelemetry HTTP client, and
+ sender construction;
+- phone request annotations and generated Swagger files.
+- `tests/adapter-emulator/` for an HTTPS gateway emulator that consumes
+ callbacks and exercises existing phone API routes;
+- `tests/adapter_integration_test.go`, `tests/docker-compose.yml`, test
+ certificate generation, CI setup, and `tests/README.md` for end-to-end
+ coverage.
+
+The implementation must not move scheduling, message expiration, message event
+handling, or phone API-key authorization into the new transport code.
+
+## Testing
+
+### Phone helper tests
+
+Cover:
+
+- ordinary FCM tokens selecting Firebase;
+- valid HTTPS URLs selecting HTTP;
+- empty and nil tokens;
+- `http`, `ftp`, missing host, and malformed URLs being invalid;
+- URL-like invalid values never falling through to Firebase.
+
+### HTTP client wiring tests
+
+Cover the standard OpenTelemetry round-tripper wrapping
+`http.DefaultTransport` without retryable HTTP transport attempts. URL
+validation must not perform DNS or address checks.
+
+### HTTP sender tests
+
+Use an HTTP test server or controlled transport to cover:
+
+- FCM-compatible message and heartbeat JSON;
+- message priority and TTL mapping;
+- stable `http/success` result for successful delivery;
+- any `2xx` response succeeding with the body ignored;
+- retrying network errors, `408`, `429`, and `5xx`;
+- not retrying other `4xx` responses;
+- three-attempt maximum and five-second per-attempt timeout;
+- bounded response-body handling;
+- errors and logs not exposing the complete URL.
+
+### Dispatcher and service tests
+
+Cover:
+
+- Firebase tokens using the Firebase sender;
+- URL tokens using the HTTP sender;
+- outgoing messages and heartbeats using the same dispatcher;
+- HTTP success using the existing sent-notification path;
+- HTTP terminal failure using the existing failed-notification path;
+- message send-attempt count and expiration scheduling remaining unchanged;
+- scheduling, exact-send time, per-minute limits, and send schedules remaining
+ independent of transport.
+
+### Adapter integration emulator
+
+Add a dedicated Go service under `tests/adapter-emulator/`. It exposes:
+
+- an HTTPS callback listener used by the API;
+- an HTTP-only test control listener exposed to the host test runner;
+- an in-memory gateway registry mapping a unique callback path to a phone
+ number and phone API key;
+- callback records for integration assertions.
+
+For `KEY_MESSAGE_ID`, the emulator:
+
+1. fetches `/v1/messages/outstanding` using the registered phone API key;
+2. posts the existing `SENT` event;
+3. posts the existing `DELIVERED` event;
+4. records the fetched message and final adapter action for test assertions.
+
+For `KEY_HEARTBEAT_ID`, the emulator posts `/v1/heartbeats` for the registered
+phone and records the heartbeat wake-up. A control endpoint also instructs the
+emulator to submit an incoming message through `/v1/messages/receive`.
+
+The integration stack generates a throwaway CA and server certificate whose SAN
+contains `adapter-emulator`, mounts the server certificate into the emulator,
+and makes the CA available to the API's Go trust store through `SSL_CERT_FILE`.
+Docker DNS resolves the private emulator hostname through the standard Go HTTP
+transport; no private-host allowlist is configured.
+
+Add end-to-end tests for:
+
+- **Outgoing:** URL-backed phone callback -> outstanding fetch -> sent event ->
+ delivered event -> final delivered API status.
+- **Incoming:** emulator control request -> existing receive-message API ->
+ final received API status and matching owner/contact/content.
+- **Heartbeat:** internal `phone.heartbeat.missed` CloudEvent -> URL callback ->
+ emulator heartbeat POST -> heartbeat visible through the user API.
+- Callback payload keys, callback processing, and phone API-key scoping.
+
+Run:
+
+```bash
+cd api
+go test ./...
+```
+
+After annotation changes, regenerate Swagger:
+
+```bash
+cd api
+swag init --requiredByDefault --parseDependency --parseInternal
+```
+
+Run the Docker integration suite:
+
+```bash
+cd tests
+bash generate-firebase-credentials.sh
+bash generate-adapter-certificates.sh
+docker compose up -d --build --wait
+docker compose wait seed
+go test -v -timeout 300s ./...
+docker compose down -v
+```
+
+## Rollout
+
+The feature is backward compatible and requires no data migration. Deploy the
+backend before configuring URL tokens.
+
+Operational monitoring should distinguish Firebase and HTTP notification
+delivery. Initial rollout should watch:
+
+- HTTP callback success and retry rates;
+- terminal failures by status class;
+- callback latency;
+- transport failures;
+- message expiration after a successful HTTP wake-up;
+- repeated callback processing observed by test adapters.
+
+Rollback is code-only: existing Android tokens continue to be valid, while
+URL-backed phones stop receiving wake-ups if the feature is rolled back.
+
+## Out of Scope
+
+- WhatsApp provider integration or any adapter implementation.
+- Provider credentials, sessions, QR-code login, templates, or media mapping.
+- New polling/list-outstanding APIs.
+- A new adapter acknowledgement endpoint.
+- Signed callbacks, HMAC, JWT, or mutual TLS.
+- Separate transport or callback URL database columns.
+- Changes to message scheduling, backpressure, expiration, or retry-count
+ algorithms.
+- Web UI for configuring adapters.
+- Android application changes.
+- General-purpose outbound webhook refactoring.
+- Destination-specific DNS/IP filtering or allowlists.
diff --git a/tests/README.md b/tests/README.md
index df91dee24..35c144b68 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,205 +1,225 @@
# 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.
+End-to-end tests for the httpSMS API. The suite runs the API and its data
+stores in Docker, keeps the existing Firebase/WireMock coverage, and adds a
+standard-library-only HTTPS adapter emulator for URL-backed phone gateways.
## Architecture
-```
-┌──────────────┐ HTTP ┌──────────────┐
-│ Test Runner │─────────────▶│ API (Go) │
-│ (Go test) │ │ Port 8000 │
-└──────────────┘ └──────┬───────┘
- │
- FCM Push │ Events
- (HTTP) │ (HTTP)
- ▼
- ┌──────────────┐
- │ Emulator │
- │ (Fiber v3) │
- │ Port 9090 │
- └──────────────┘
- │
- ┌──────┴───────┐
- │ CockroachDB │ │ Redis │
- │ Port 26257 │ │ Port 6379 │
- └──────────────┘ └─────────────┘
+```text
+ ┌──────────────────┐
+ │ API (Go) │
+ │ Port 8000 │
+ └───────┬──────────┘
+ │
+ FCM HTTP │ HTTPS callbacks
+ ┌──────────────────┐ │ ┌──────────────────────┐
+ │ WireMock │◀──────┘ │ Adapter emulator │
+ │ Port 8080 │ │ HTTPS callback :9091│
+ └──────────────────┘ │ HTTP control :9092│
+ └──────────┬───────────┘
+ │ phone API calls
+ └──────────▶ API
+
+┌──────────────────┐ HTTP ┌────────────────────────────┐
+│ Test runner │───────────────────▶│ API, WireMock, and adapter │
+│ Go test on host │ │ control endpoints │
+└──────────────────┘ └────────────────────────────┘
+
+Data stores: CockroachDB, Redis, and MongoDB.
```
### Components
-| 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
-
-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
+| Component | Description |
+| --- | --- |
+| **API** | The httpSMS Go API server |
+| **WireMock** | Existing fake Firebase and webhook endpoints |
+| **Adapter emulator** | HTTPS URL-backed phone gateway with an HTTP-only host control API |
+| **CockroachDB** | Relational database for API data |
+| **Redis** | Cache and local event queue backend |
+| **MongoDB** | Heartbeat and contact backend |
+| **Seed** | One-shot container that inserts integration users and API keys |
+| **Test runner** | Go tests running on the host |
+
+### Gateway Flows
+
+1. **Existing FCM flow:** the API sends Firebase-compatible requests to
+ WireMock. Existing tests fetch outstanding messages and submit phone events
+ without changing their transport.
+2. **URL-backed outgoing flow:** the API posts an FCM-compatible envelope to
+ `https://adapter-emulator:9091/notifications/{gatewayID}`. The adapter uses
+ its registered phone API key to fetch the outstanding message and post
+ `SENT` followed by `DELIVERED`.
+3. **URL-backed incoming flow:** the test calls the adapter control API on
+ host port `9092`; the adapter posts `/v1/messages/receive` as the registered
+ phone.
+4. **Heartbeat wake-up:** the test dispatches `phone.heartbeat.missed` through
+ `/v1/events`. The API sends an HTTPS callback containing
+ `KEY_HEARTBEAT_ID`, and the adapter posts `/v1/heartbeats`.
+
+The HTTPS endpoint uses a two-day throwaway CA and server certificate with the
+DNS SAN `adapter-emulator`. The API container trusts only that generated CA via
+`SSL_CERT_FILE`; HTTPS verification is never bypassed. The notification sender
+uses the standard OpenTelemetry-instrumented Go HTTP transport.
## 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
-- [x] **Message thread unread count E2E** — Incoming SMS and missed calls increment the unread count, the existing thread update endpoint clears it, and outbound activity preserves the count
-- [x] **Unarchive Thread on Receive E2E** — Archived thread returns to the inbox on inbound message when the phone's `unarchive_thread` setting is enabled, and stays archived when disabled
-- [x] **Contacts E2E** — JSON CRUD, search and pagination totals, CSV import normalization, and contact details attached to message threads
+- [x] Existing encrypted send/receive phone scenarios through WireMock
+- [x] Existing rate-limit, webhook, contacts, bulk, and thread scenarios
+- [x] URL-backed outgoing message reaches `delivered`
+- [x] URL-backed incoming message reaches `received`
+- [x] URL-backed heartbeat callback stores a heartbeat
+- [x] Adapter callback notification IDs are deduplicated in memory
+- [x] HTTPS certificate trust is exercised
## 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)
+- [Go 1.25+](https://go.dev/dl/)
+- [jq](https://jqlang.github.io/jq/download/)
+- [OpenSSL](https://www.openssl.org/)
+
+On Windows, the scripts can be run with Git Bash, for example
+`C:\Program Files\Git\bin\bash.exe`.
## Running Locally
-### 1. Generate Firebase Credentials
+### 1. Generate throwaway credentials and certificates
-The integration tests use a fake Firebase service account. Generate it with:
+Run both scripts before starting Docker:
```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
+bash generate-firebase-credentials.sh firebase-credentials.json
+bash generate-adapter-certificates.sh certs
export FIREBASE_CREDENTIALS=$(jq -c . firebase-credentials.json)
```
-### 3. Start the Stack
-
-```bash
-docker compose up -d --build --wait
-```
+The generated Firebase credential and the complete `certs/` directory are
+ignored by Git.
-This starts CockroachDB, Redis, the API, and the emulator. The `--wait` flag blocks until all health checks pass.
-
-### 4. Wait for Seeding
+### 2. Start the stack and wait for seeding
```bash
+docker compose up -d --build --wait
docker compose wait seed
sleep 2
```
-The seed container inserts test users, phones, and API keys into CockroachDB after the API has run its GORM migrations.
-
-### 5. Run Tests
+### 3. Run the complete suite
```bash
-go test -v -timeout 120s ./...
+go test -v -timeout 300s ./...
```
-### 6. Tear Down
+### 4. Tear down
```bash
docker compose down -v
```
-The `-v` flag removes volumes (database data) for a clean slate next run.
-
-### One-Liner
+### One-liner
```bash
cd tests && \
- bash generate-firebase-credentials.sh && \
+ bash generate-firebase-credentials.sh firebase-credentials.json && \
+ bash generate-adapter-certificates.sh certs && \
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 ./... ; \
+ go test -v -timeout 300s ./... ; \
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
+`.github/workflows/api.yml` generates both the fake Firebase credential and
+the adapter CA/server certificate before building the Compose stack. The
+workflow runs API handler integration tests and this complete host-side suite,
+collects service logs on failure, and always tears the stack down.
## 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` |
+| Entity | Value |
+| --- | --- |
+| User API key | `test-user-api-key` |
+| System API key | `system-user-api-key` |
+| User ID | `test-user-id` |
+| System user ID | `system-user-id` |
-See [`seed.sql`](./seed.sql) for the complete seed data.
+Adapter tests create a unique gateway UUID, phone number, phone API key, and
+callback path per test. See [`seed.sql`](./seed.sql) for shared seed data.
## Project Structure
-```
+```text
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)
+├── adapter-emulator/
+│ ├── Dockerfile
+│ ├── go.mod
+│ ├── main.go
+│ ├── emulator.go
+│ ├── api_client.go
+│ ├── notification_handler.go
+│ ├── control_handler.go
+│ └── emulator_test.go
+├── wiremock/
+│ └── mappings/
+├── adapter_integration_test.go
+├── integration_test.go
+├── helpers_test.go
+├── docker-compose.yml
+├── .env.test
+├── seed.sql
+├── generate-firebase-credentials.sh
+├── generate-adapter-certificates.sh
+├── go.mod
+└── go.sum
```
## Troubleshooting
-### API fails to start
+### API or adapter fails to start
+
+```bash
+docker compose logs --tail 200 api adapter-emulator
+```
+
+Confirm `tests/certs/ca.pem`, `server.pem`, and `server-key.pem` exist. TLS
+errors should be fixed by regenerating certificates; do not disable HTTPS
+verification.
-Check the API logs:
+### URL-backed outgoing message times out
```bash
-docker compose logs api
+docker compose logs --tail 200 api adapter-emulator
```
-Common issues:
+Adapter logs should show callback receipt, the outstanding-message fetch,
+`SENT`, and `DELIVERED`. Confirm `SSL_CERT_FILE=/adapter-certs/ca.pem` is
+present in the API container.
-- `FIREBASE_CREDENTIALS` env var not set or malformed
-- CockroachDB not ready (increase `start_period` in healthcheck)
+### URL-backed incoming message times out
-### Tests timeout waiting for `delivered` status
+Adapter logs should show the control request followed by a call to
+`/v1/messages/receive`. The gateway registration contains the per-test phone
+number and phone API key.
-Check the emulator logs:
+### Heartbeat callback times out
-```bash
-docker compose logs emulator
-```
+API logs should show `phone.heartbeat.missed`. Adapter logs should show
+`KEY_HEARTBEAT_ID` followed by a successful heartbeat POST.
-The emulator should show:
+### Existing FCM scenario times out
-1. `[FCM]` — Receiving the push notification
-2. `[EVENTS]` — Fetching outstanding messages and firing events
+```bash
+docker compose logs --tail 200 api wiremock
+```
-If no `[FCM]` entries appear, the API isn't reaching the emulator (check `FCM_ENDPOINT` in `.env.test`).
+Keep `FCM_ENDPOINT=http://wiremock:8080`; the adapter service does not replace
+or weaken the WireMock phone tests.
### Seed container fails
@@ -207,11 +227,5 @@ If no `[FCM]` entries appear, the API isn't reaching the emulator (check `FCM_EN
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
+If a relation does not exist, inspect API migration/startup logs before
+increasing health-check timing.
diff --git a/tests/adapter-emulator/Dockerfile b/tests/adapter-emulator/Dockerfile
new file mode 100644
index 000000000..d752fcaa6
--- /dev/null
+++ b/tests/adapter-emulator/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.25-alpine AS build
+WORKDIR /src
+COPY go.mod ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=0 go build -o /out/adapter-emulator .
+
+FROM alpine:3.22
+RUN adduser -D -u 10001 app
+USER app
+COPY --from=build /out/adapter-emulator /usr/local/bin/adapter-emulator
+ENTRYPOINT ["adapter-emulator"]
diff --git a/tests/adapter-emulator/api_client.go b/tests/adapter-emulator/api_client.go
new file mode 100644
index 000000000..17ca2031a
--- /dev/null
+++ b/tests/adapter-emulator/api_client.go
@@ -0,0 +1,155 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+const maxAPIErrorBodyBytes = 4 * 1024
+
+func (instance *emulator) fetchOutstanding(
+ ctx context.Context,
+ registeredGateway gateway,
+ messageID string,
+) (map[string]any, error) {
+ endpoint, err := url.Parse(instance.apiBaseURL + "/v1/messages/outstanding")
+ if err != nil {
+ return nil, fmt.Errorf("build outstanding message URL: %w", err)
+ }
+ query := endpoint.Query()
+ query.Set("message_id", messageID)
+ endpoint.RawQuery = query.Encode()
+
+ log.Printf("[ADAPTER] fetching outstanding message %s", messageID)
+ var response struct {
+ Data map[string]any `json:"data"`
+ }
+ if err := instance.doAPIRequest(ctx, registeredGateway, http.MethodGet, endpoint.String(), nil, &response); err != nil {
+ return nil, fmt.Errorf("fetch outstanding message %s: %w", messageID, err)
+ }
+ return response.Data, nil
+}
+
+func (instance *emulator) fireMessageEvent(
+ ctx context.Context,
+ registeredGateway gateway,
+ messageID string,
+ eventName string,
+) error {
+ payload := map[string]any{
+ "event_name": eventName,
+ "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
+ }
+ endpoint := fmt.Sprintf("%s/v1/messages/%s/events", instance.apiBaseURL, url.PathEscape(messageID))
+ log.Printf("[ADAPTER] posting %s for message %s", eventName, messageID)
+ if err := instance.doAPIRequest(ctx, registeredGateway, http.MethodPost, endpoint, payload, nil); err != nil {
+ return fmt.Errorf("post %s event for message %s: %w", eventName, messageID, err)
+ }
+ return nil
+}
+
+func (instance *emulator) receiveMessage(
+ ctx context.Context,
+ registeredGateway gateway,
+ request incomingMessageRequest,
+) (map[string]any, error) {
+ payload := map[string]any{
+ "from": request.Contact,
+ "to": registeredGateway.PhoneNumber,
+ "content": request.Content,
+ "encrypted": request.Encrypted,
+ "sim": "SIM1",
+ "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
+ }
+ log.Printf("[ADAPTER] posting incoming message for gateway phone %s", registeredGateway.PhoneNumber)
+
+ var response struct {
+ Data map[string]any `json:"data"`
+ }
+ if err := instance.doAPIRequest(
+ ctx,
+ registeredGateway,
+ http.MethodPost,
+ instance.apiBaseURL+"/v1/messages/receive",
+ payload,
+ &response,
+ ); err != nil {
+ return nil, fmt.Errorf("post incoming message: %w", err)
+ }
+ return response.Data, nil
+}
+
+func (instance *emulator) storeHeartbeat(ctx context.Context, registeredGateway gateway) error {
+ payload := map[string]any{
+ "phone_numbers": []string{registeredGateway.PhoneNumber},
+ "charging": true,
+ }
+ log.Printf("[ADAPTER] posting heartbeat for %s", registeredGateway.PhoneNumber)
+ if err := instance.doAPIRequest(
+ ctx,
+ registeredGateway,
+ http.MethodPost,
+ instance.apiBaseURL+"/v1/heartbeats",
+ payload,
+ nil,
+ ); err != nil {
+ return fmt.Errorf("post heartbeat for %s: %w", registeredGateway.PhoneNumber, err)
+ }
+ return nil
+}
+
+func (instance *emulator) doAPIRequest(
+ ctx context.Context,
+ registeredGateway gateway,
+ method string,
+ endpoint string,
+ payload any,
+ result any,
+) error {
+ var body io.Reader
+ if payload != nil {
+ encoded, err := json.Marshal(payload)
+ if err != nil {
+ return fmt.Errorf("encode request body: %w", err)
+ }
+ body = bytes.NewReader(encoded)
+ }
+
+ request, err := http.NewRequestWithContext(ctx, method, endpoint, body)
+ if err != nil {
+ return fmt.Errorf("create request: %w", err)
+ }
+ request.Header.Set("x-api-key", registeredGateway.PhoneAPIKey)
+ if payload != nil {
+ request.Header.Set("Content-Type", "application/json")
+ }
+
+ response, err := instance.client.Do(request)
+ if err != nil {
+ return fmt.Errorf("execute request: %w", err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
+ errorBody, readErr := io.ReadAll(io.LimitReader(response.Body, maxAPIErrorBodyBytes))
+ if readErr != nil {
+ return fmt.Errorf("unexpected status %d and read error body: %w", response.StatusCode, readErr)
+ }
+ return fmt.Errorf("unexpected status %d: %s", response.StatusCode, string(errorBody))
+ }
+ if result == nil || response.StatusCode == http.StatusNoContent {
+ _, _ = io.Copy(io.Discard, response.Body)
+ return nil
+ }
+ if err := json.NewDecoder(response.Body).Decode(result); err != nil {
+ return fmt.Errorf("decode response body: %w", err)
+ }
+ return nil
+}
diff --git a/tests/adapter-emulator/control_handler.go b/tests/adapter-emulator/control_handler.go
new file mode 100644
index 000000000..cfe066891
--- /dev/null
+++ b/tests/adapter-emulator/control_handler.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+const maxControlBodyBytes = 1024 * 1024
+
+type gatewayRegistration struct {
+ PhoneNumber string `json:"phone_number"`
+ PhoneAPIKey string `json:"phone_api_key"`
+}
+
+type incomingMessageRequest struct {
+ Contact string `json:"contact"`
+ Content string `json:"content"`
+ Encrypted bool `json:"encrypted"`
+}
+
+func (instance *emulator) controlHandler() http.Handler {
+ mux := http.NewServeMux()
+ mux.HandleFunc("PUT /test/gateways/{gatewayID}", instance.handleGatewayRegistration)
+ mux.HandleFunc("POST /test/gateways/{gatewayID}/incoming", instance.handleIncomingMessage)
+ mux.HandleFunc("GET /test/gateways/{gatewayID}/notifications", instance.handleNotificationRecords)
+ mux.HandleFunc("GET /health", instance.handleHealth)
+ return mux
+}
+
+func (instance *emulator) handleGatewayRegistration(writer http.ResponseWriter, request *http.Request) {
+ var registration gatewayRegistration
+ if err := decodeControlJSON(writer, request, ®istration); err != nil {
+ writeControlError(writer, http.StatusBadRequest, err)
+ return
+ }
+ registration.PhoneNumber = strings.TrimSpace(registration.PhoneNumber)
+ registration.PhoneAPIKey = strings.TrimSpace(registration.PhoneAPIKey)
+ if registration.PhoneNumber == "" || registration.PhoneAPIKey == "" {
+ writeControlError(writer, http.StatusBadRequest, errors.New("phone_number and phone_api_key are required"))
+ return
+ }
+
+ instance.registerGateway(request.PathValue("gatewayID"), registration)
+ writer.WriteHeader(http.StatusNoContent)
+}
+
+func (instance *emulator) handleIncomingMessage(writer http.ResponseWriter, request *http.Request) {
+ registeredGateway, ok := instance.loadGateway(request.PathValue("gatewayID"))
+ if !ok {
+ writeControlError(writer, http.StatusNotFound, errors.New("unknown gateway"))
+ return
+ }
+
+ var incoming incomingMessageRequest
+ if err := decodeControlJSON(writer, request, &incoming); err != nil {
+ writeControlError(writer, http.StatusBadRequest, err)
+ return
+ }
+ incoming.Contact = strings.TrimSpace(incoming.Contact)
+ if incoming.Contact == "" {
+ writeControlError(writer, http.StatusBadRequest, errors.New("contact is required"))
+ return
+ }
+
+ message, err := instance.receiveMessage(request.Context(), registeredGateway, incoming)
+ if err != nil {
+ writeControlError(writer, http.StatusBadGateway, err)
+ return
+ }
+ writeControlJSON(writer, http.StatusOK, map[string]any{"data": message})
+}
+
+func (instance *emulator) handleNotificationRecords(writer http.ResponseWriter, request *http.Request) {
+ gatewayID := request.PathValue("gatewayID")
+ if _, ok := instance.loadGateway(gatewayID); !ok {
+ writeControlError(writer, http.StatusNotFound, errors.New("unknown gateway"))
+ return
+ }
+ records := instance.listGatewayRecords(gatewayID)
+ if messageID := strings.TrimSpace(request.URL.Query().Get("message_id")); messageID != "" {
+ filtered := make([]notificationRecord, 0, len(records))
+ for _, record := range records {
+ if record.MessageID == messageID {
+ filtered = append(filtered, record)
+ }
+ }
+ records = filtered
+ }
+ writeControlJSON(writer, http.StatusOK, map[string]any{
+ "data": records,
+ })
+}
+
+func (*emulator) handleHealth(writer http.ResponseWriter, _ *http.Request) {
+ writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ writer.WriteHeader(http.StatusOK)
+ _, _ = writer.Write([]byte("ok\n"))
+}
+
+func decodeControlJSON(writer http.ResponseWriter, request *http.Request, result any) error {
+ request.Body = http.MaxBytesReader(writer, request.Body, maxControlBodyBytes)
+ decoder := json.NewDecoder(request.Body)
+ if err := decoder.Decode(result); err != nil {
+ return fmt.Errorf("decode request body: %w", err)
+ }
+ if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
+ return errors.New("request body must contain one JSON value")
+ }
+ return nil
+}
+
+func writeControlError(writer http.ResponseWriter, status int, err error) {
+ writeControlJSON(writer, status, map[string]any{"error": err.Error()})
+}
+
+func writeControlJSON(writer http.ResponseWriter, status int, payload any) {
+ writer.Header().Set("Content-Type", "application/json")
+ writer.WriteHeader(status)
+ _ = json.NewEncoder(writer).Encode(payload)
+}
diff --git a/tests/adapter-emulator/emulator.go b/tests/adapter-emulator/emulator.go
new file mode 100644
index 000000000..13d0568d7
--- /dev/null
+++ b/tests/adapter-emulator/emulator.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+ "net/http"
+ "strings"
+ "sync"
+)
+
+type gateway struct {
+ PhoneNumber string
+ PhoneAPIKey string
+}
+
+type notificationRecord struct {
+ GatewayID string `json:"gateway_id"`
+ Data map[string]string `json:"data"`
+ MessageID string `json:"message_id,omitempty"`
+ Kind string `json:"kind"`
+ Processed bool `json:"processed"`
+ Error string `json:"error,omitempty"`
+}
+
+type emulator struct {
+ apiBaseURL string
+ client *http.Client
+ mu sync.RWMutex
+ gateways map[string]gateway
+ records []*notificationRecord
+}
+
+func newEmulator(apiBaseURL string, client *http.Client) *emulator {
+ return &emulator{
+ apiBaseURL: strings.TrimRight(apiBaseURL, "/"),
+ client: client,
+ gateways: make(map[string]gateway),
+ }
+}
+
+func (instance *emulator) registerGateway(gatewayID string, registration gatewayRegistration) {
+ instance.mu.Lock()
+ defer instance.mu.Unlock()
+
+ instance.gateways[gatewayID] = gateway{
+ PhoneNumber: registration.PhoneNumber,
+ PhoneAPIKey: registration.PhoneAPIKey,
+ }
+}
+
+func (instance *emulator) loadGateway(gatewayID string) (gateway, bool) {
+ instance.mu.RLock()
+ defer instance.mu.RUnlock()
+
+ registeredGateway, ok := instance.gateways[gatewayID]
+ return registeredGateway, ok
+}
+
+func (instance *emulator) recordNotification(
+ gatewayID string,
+ data map[string]string,
+ kind string,
+ messageID string,
+) *notificationRecord {
+ instance.mu.Lock()
+ defer instance.mu.Unlock()
+
+ record := ¬ificationRecord{
+ GatewayID: gatewayID,
+ Data: copyStringMap(data),
+ MessageID: messageID,
+ Kind: kind,
+ }
+ instance.records = append(instance.records, record)
+
+ return record
+}
+
+func (instance *emulator) markNotificationProcessed(record *notificationRecord) {
+ instance.mu.Lock()
+ defer instance.mu.Unlock()
+
+ record.Processed = true
+ record.Error = ""
+}
+
+func (instance *emulator) markNotificationFailed(record *notificationRecord, err error) {
+ instance.mu.Lock()
+ defer instance.mu.Unlock()
+
+ record.Processed = false
+ record.Error = err.Error()
+}
+
+func (instance *emulator) listGatewayRecords(gatewayID string) []notificationRecord {
+ instance.mu.RLock()
+ defer instance.mu.RUnlock()
+
+ records := make([]notificationRecord, 0)
+ for _, record := range instance.records {
+ if record.GatewayID == gatewayID {
+ records = append(records, *copyNotificationRecord(record))
+ }
+ }
+
+ return records
+}
+
+func copyNotificationRecord(record *notificationRecord) *notificationRecord {
+ copied := *record
+ copied.Data = copyStringMap(record.Data)
+ return &copied
+}
+
+func copyStringMap(values map[string]string) map[string]string {
+ copied := make(map[string]string, len(values))
+ for key, value := range values {
+ copied[key] = value
+ }
+ return copied
+}
diff --git a/tests/adapter-emulator/emulator_test.go b/tests/adapter-emulator/emulator_test.go
new file mode 100644
index 000000000..b8be3c2b0
--- /dev/null
+++ b/tests/adapter-emulator/emulator_test.go
@@ -0,0 +1,448 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "strings"
+ "sync"
+ "testing"
+)
+
+func TestRecordNotificationCopiesRecords(t *testing.T) {
+ t.Parallel()
+
+ instance := newEmulator("http://api.example", http.DefaultClient)
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+
+ record := instance.recordNotification(
+ "gateway-1",
+ map[string]string{"KEY_MESSAGE_ID": "message-1"},
+ "message",
+ "message-1",
+ )
+ instance.markNotificationProcessed(record)
+
+ records := instance.listGatewayRecords("gateway-1")
+ if len(records) != 1 {
+ t.Fatalf("record count = %d, want 1", len(records))
+ }
+ if !records[0].Processed {
+ t.Fatal("processed state was not retained")
+ }
+
+ records[0].Data["KEY_MESSAGE_ID"] = "mutated"
+ fresh := instance.listGatewayRecords("gateway-1")
+ if fresh[0].Data["KEY_MESSAGE_ID"] != "message-1" {
+ t.Fatalf("record list returned mutable state: %#v", fresh[0])
+ }
+}
+
+func TestNotificationHandlerProcessesMessage(t *testing.T) {
+ t.Parallel()
+
+ var mu sync.Mutex
+ var outstandingCalls int
+ var events []string
+ api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ if request.Header.Get("x-api-key") != "phone-key" {
+ t.Errorf("x-api-key = %q, want phone-key", request.Header.Get("x-api-key"))
+ }
+
+ switch {
+ case request.Method == http.MethodGet && request.URL.Path == "/v1/messages/outstanding":
+ mu.Lock()
+ outstandingCalls++
+ mu.Unlock()
+ if request.URL.Query().Get("message_id") != "message-1" {
+ t.Errorf("message_id = %q, want message-1", request.URL.Query().Get("message_id"))
+ }
+ writeJSON(writer, http.StatusOK, map[string]any{
+ "data": map[string]any{"id": "message-1"},
+ })
+ case request.Method == http.MethodPost && request.URL.Path == "/v1/messages/message-1/events":
+ var payload struct {
+ EventName string `json:"event_name"`
+ Timestamp string `json:"timestamp"`
+ }
+ if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
+ t.Errorf("decode event: %v", err)
+ }
+ if payload.Timestamp == "" {
+ t.Error("event timestamp is empty")
+ }
+ mu.Lock()
+ events = append(events, payload.EventName)
+ mu.Unlock()
+ writeJSON(writer, http.StatusOK, map[string]any{"data": map[string]any{"id": "message-1"}})
+ default:
+ http.NotFound(writer, request)
+ }
+ }))
+ defer api.Close()
+
+ instance := newEmulator(api.URL, api.Client())
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+
+ body := callbackBody(t, map[string]string{"KEY_MESSAGE_ID": "message-1"})
+ request := httptest.NewRequest(
+ http.MethodPost,
+ "/notifications/gateway-1",
+ bytes.NewReader(body),
+ )
+ response := httptest.NewRecorder()
+ instance.notificationHandler().ServeHTTP(response, request)
+ if response.Code != http.StatusNoContent {
+ t.Fatalf("callback status = %d, want 204: %s", response.Code, response.Body.String())
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+ if outstandingCalls != 1 {
+ t.Fatalf("outstanding calls = %d, want 1", outstandingCalls)
+ }
+ if !reflect.DeepEqual(events, []string{"SENT", "DELIVERED"}) {
+ t.Fatalf("events = %#v, want SENT then DELIVERED", events)
+ }
+
+ records := instance.listGatewayRecords("gateway-1")
+ if len(records) != 1 {
+ t.Fatalf("record count = %d, want 1", len(records))
+ }
+ record := records[0]
+ if record.Kind != "message" || record.MessageID != "message-1" || !record.Processed {
+ t.Fatalf("unexpected message record: %#v", record)
+ }
+}
+
+func TestNotificationHandlerStoresHeartbeat(t *testing.T) {
+ t.Parallel()
+
+ var heartbeatPayload map[string]any
+ api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ if request.Method != http.MethodPost || request.URL.Path != "/v1/heartbeats" {
+ http.NotFound(writer, request)
+ return
+ }
+ if request.Header.Get("x-api-key") != "phone-key" {
+ t.Errorf("x-api-key = %q, want phone-key", request.Header.Get("x-api-key"))
+ }
+ if err := json.NewDecoder(request.Body).Decode(&heartbeatPayload); err != nil {
+ t.Errorf("decode heartbeat: %v", err)
+ }
+ writeJSON(writer, http.StatusCreated, map[string]any{"data": []any{}})
+ }))
+ defer api.Close()
+
+ instance := newEmulator(api.URL, api.Client())
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+
+ request := httptest.NewRequest(
+ http.MethodPost,
+ "/notifications/gateway-1",
+ bytes.NewReader(callbackBody(t, map[string]string{"KEY_HEARTBEAT_ID": "heartbeat-1"})),
+ )
+ response := httptest.NewRecorder()
+ instance.notificationHandler().ServeHTTP(response, request)
+ if response.Code != http.StatusNoContent {
+ t.Fatalf("callback status = %d, want 204: %s", response.Code, response.Body.String())
+ }
+
+ if !reflect.DeepEqual(heartbeatPayload["phone_numbers"], []any{"+18005550199"}) {
+ t.Fatalf("phone_numbers = %#v, want gateway phone", heartbeatPayload["phone_numbers"])
+ }
+ if heartbeatPayload["charging"] != true {
+ t.Fatalf("charging = %#v, want true", heartbeatPayload["charging"])
+ }
+
+ records := instance.listGatewayRecords("gateway-1")
+ if len(records) != 1 || records[0].Kind != "heartbeat" || !records[0].Processed {
+ t.Fatalf("unexpected heartbeat records: %#v", records)
+ }
+}
+
+func TestNotificationHandlerRetainsProcessingFailure(t *testing.T) {
+ t.Parallel()
+
+ api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ http.Error(writer, "outstanding unavailable", http.StatusServiceUnavailable)
+ }))
+ defer api.Close()
+
+ instance := newEmulator(api.URL, api.Client())
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+
+ request := httptest.NewRequest(
+ http.MethodPost,
+ "/notifications/gateway-1",
+ bytes.NewReader(callbackBody(t, map[string]string{"KEY_MESSAGE_ID": "message-1"})),
+ )
+ response := httptest.NewRecorder()
+ instance.notificationHandler().ServeHTTP(response, request)
+ if response.Code != http.StatusInternalServerError {
+ t.Fatalf("callback status = %d, want 500: %s", response.Code, response.Body.String())
+ }
+
+ records := instance.listGatewayRecords("gateway-1")
+ if len(records) != 1 {
+ t.Fatalf("record count = %d, want 1", len(records))
+ }
+ if records[0].Processed {
+ t.Fatal("failed notification was marked processed")
+ }
+ if !strings.Contains(records[0].Error, "fetch outstanding message") {
+ t.Fatalf("record error = %q, want fetch context", records[0].Error)
+ }
+}
+
+func TestNotificationHandlerProcessesRetryAfterFailure(t *testing.T) {
+ t.Parallel()
+
+ var mu sync.Mutex
+ var outstandingCalls int
+ var events []string
+ api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ switch {
+ case request.Method == http.MethodGet && request.URL.Path == "/v1/messages/outstanding":
+ mu.Lock()
+ outstandingCalls++
+ firstCall := outstandingCalls == 1
+ mu.Unlock()
+ if firstCall {
+ http.Error(writer, "outstanding unavailable", http.StatusServiceUnavailable)
+ return
+ }
+ writeJSON(writer, http.StatusOK, map[string]any{
+ "data": map[string]any{"id": "message-1"},
+ })
+ case request.Method == http.MethodPost && request.URL.Path == "/v1/messages/message-1/events":
+ var payload struct {
+ EventName string `json:"event_name"`
+ }
+ if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
+ t.Errorf("decode event: %v", err)
+ }
+ mu.Lock()
+ events = append(events, payload.EventName)
+ mu.Unlock()
+ writeJSON(writer, http.StatusOK, map[string]any{"data": map[string]any{"id": "message-1"}})
+ default:
+ http.NotFound(writer, request)
+ }
+ }))
+ defer api.Close()
+
+ instance := newEmulator(api.URL, api.Client())
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+ handler := instance.notificationHandler()
+ body := callbackBody(t, map[string]string{"KEY_MESSAGE_ID": "message-1"})
+
+ firstRequest := httptest.NewRequest(http.MethodPost, "/notifications/gateway-1", bytes.NewReader(body))
+ firstResponse := httptest.NewRecorder()
+ handler.ServeHTTP(firstResponse, firstRequest)
+ if firstResponse.Code != http.StatusInternalServerError {
+ t.Fatalf("first callback status = %d, want 500: %s", firstResponse.Code, firstResponse.Body.String())
+ }
+ secondRequest := httptest.NewRequest(http.MethodPost, "/notifications/gateway-1", bytes.NewReader(body))
+ secondResponse := httptest.NewRecorder()
+ handler.ServeHTTP(secondResponse, secondRequest)
+ if secondResponse.Code != http.StatusNoContent {
+ t.Fatalf("retry callback status = %d, want 204: %s", secondResponse.Code, secondResponse.Body.String())
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+ if outstandingCalls != 2 {
+ t.Fatalf("outstanding calls = %d, want 2", outstandingCalls)
+ }
+ if !reflect.DeepEqual(events, []string{"SENT", "DELIVERED"}) {
+ t.Fatalf("events = %#v, want SENT then DELIVERED", events)
+ }
+
+ records := instance.listGatewayRecords("gateway-1")
+ if len(records) != 2 {
+ t.Fatalf("record count = %d, want 2", len(records))
+ }
+ if records[0].Processed || records[0].Error == "" {
+ t.Fatalf("unexpected failed record: %#v", records[0])
+ }
+ if !records[1].Processed || records[1].Error != "" {
+ t.Fatalf("unexpected successful retry record: %#v", records[1])
+ }
+}
+
+func TestControlHandlerRegistersGatewayAndReceivesIncomingMessage(t *testing.T) {
+ t.Parallel()
+
+ var receivePayload map[string]any
+ api := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ if request.Method != http.MethodPost || request.URL.Path != "/v1/messages/receive" {
+ http.NotFound(writer, request)
+ return
+ }
+ if request.Header.Get("x-api-key") != "phone-key" {
+ t.Errorf("x-api-key = %q, want phone-key", request.Header.Get("x-api-key"))
+ }
+ if err := json.NewDecoder(request.Body).Decode(&receivePayload); err != nil {
+ t.Errorf("decode receive payload: %v", err)
+ }
+ writeJSON(writer, http.StatusOK, map[string]any{
+ "data": map[string]any{"id": "message-1"},
+ })
+ }))
+ defer api.Close()
+
+ instance := newEmulator(api.URL, api.Client())
+ handler := instance.controlHandler()
+
+ registration := performJSONRequest(t, handler, http.MethodPut, "/test/gateways/gateway-1", map[string]any{
+ "phone_number": "+18005550199",
+ "phone_api_key": "phone-key",
+ })
+ if registration.Code != http.StatusNoContent {
+ t.Fatalf("registration status = %d, want 204: %s", registration.Code, registration.Body.String())
+ }
+
+ incoming := performJSONRequest(t, handler, http.MethodPost, "/test/gateways/gateway-1/incoming", map[string]any{
+ "contact": "+18005550100",
+ "content": "hello",
+ "encrypted": true,
+ })
+ if incoming.Code != http.StatusOK {
+ t.Fatalf("incoming status = %d, want 200: %s", incoming.Code, incoming.Body.String())
+ }
+
+ if receivePayload["to"] != "+18005550199" ||
+ receivePayload["from"] != "+18005550100" ||
+ receivePayload["content"] != "hello" ||
+ receivePayload["encrypted"] != true ||
+ receivePayload["sim"] != "SIM1" ||
+ receivePayload["timestamp"] == "" {
+ t.Fatalf("unexpected receive payload: %#v", receivePayload)
+ }
+
+ var incomingResponse struct {
+ Data map[string]any `json:"data"`
+ }
+ if err := json.NewDecoder(incoming.Body).Decode(&incomingResponse); err != nil {
+ t.Fatalf("decode incoming response: %v", err)
+ }
+ if incomingResponse.Data["id"] != "message-1" {
+ t.Fatalf("incoming message id = %#v, want message-1", incomingResponse.Data["id"])
+ }
+
+ health := httptest.NewRecorder()
+ handler.ServeHTTP(health, httptest.NewRequest(http.MethodGet, "/health", nil))
+ if health.Code != http.StatusOK {
+ t.Fatalf("health status = %d, want 200", health.Code)
+ }
+
+ records := httptest.NewRecorder()
+ handler.ServeHTTP(records, httptest.NewRequest(http.MethodGet, "/test/gateways/gateway-1/notifications", nil))
+ if records.Code != http.StatusOK {
+ t.Fatalf("records status = %d, want 200", records.Code)
+ }
+}
+
+func TestControlHandlerFiltersNotificationRecordsByMessageID(t *testing.T) {
+ t.Parallel()
+
+ instance := newEmulator("http://api.example", http.DefaultClient)
+ instance.registerGateway("gateway-1", gatewayRegistration{
+ PhoneNumber: "+18005550199",
+ PhoneAPIKey: "phone-key",
+ })
+ instance.recordNotification(
+ "gateway-1",
+ map[string]string{"KEY_MESSAGE_ID": "message-1"},
+ "message",
+ "message-1",
+ )
+ instance.recordNotification(
+ "gateway-1",
+ map[string]string{"KEY_MESSAGE_ID": "message-2"},
+ "message",
+ "message-2",
+ )
+
+ response := httptest.NewRecorder()
+ instance.controlHandler().ServeHTTP(
+ response,
+ httptest.NewRequest(
+ http.MethodGet,
+ "/test/gateways/gateway-1/notifications?message_id=message-2",
+ nil,
+ ),
+ )
+ if response.Code != http.StatusOK {
+ t.Fatalf("records status = %d, want 200", response.Code)
+ }
+
+ var payload struct {
+ Data []notificationRecord `json:"data"`
+ }
+ if err := json.NewDecoder(response.Body).Decode(&payload); err != nil {
+ t.Fatalf("decode records: %v", err)
+ }
+ if len(payload.Data) != 1 || payload.Data[0].MessageID != "message-2" {
+ t.Fatalf("filtered records = %#v, want message-2 only", payload.Data)
+ }
+}
+
+func callbackBody(t *testing.T, data map[string]string) []byte {
+ t.Helper()
+
+ body, err := json.Marshal(map[string]any{
+ "message": map[string]any{
+ "token": "https://adapter-emulator:9091/notifications/gateway-1",
+ "data": data,
+ },
+ })
+ if err != nil {
+ t.Fatalf("marshal callback: %v", err)
+ }
+ return body
+}
+
+func performJSONRequest(
+ t *testing.T,
+ handler http.Handler,
+ method string,
+ target string,
+ payload map[string]any,
+) *httptest.ResponseRecorder {
+ t.Helper()
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ t.Fatalf("marshal request: %v", err)
+ }
+ request := httptest.NewRequest(method, target, bytes.NewReader(body))
+ request.Header.Set("Content-Type", "application/json")
+ response := httptest.NewRecorder()
+ handler.ServeHTTP(response, request)
+ return response
+}
+
+func writeJSON(writer http.ResponseWriter, status int, payload any) {
+ writer.Header().Set("Content-Type", "application/json")
+ writer.WriteHeader(status)
+ _ = json.NewEncoder(writer).Encode(payload)
+}
diff --git a/tests/adapter-emulator/go.mod b/tests/adapter-emulator/go.mod
new file mode 100644
index 000000000..399833be5
--- /dev/null
+++ b/tests/adapter-emulator/go.mod
@@ -0,0 +1,3 @@
+module github.com/NdoleStudio/httpsms/tests/adapter-emulator
+
+go 1.25.0
diff --git a/tests/adapter-emulator/main.go b/tests/adapter-emulator/main.go
new file mode 100644
index 000000000..c8d93f50a
--- /dev/null
+++ b/tests/adapter-emulator/main.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+const (
+ callbackAddress = ":9091"
+ controlAddress = ":9092"
+)
+
+func main() {
+ if err := run(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func run() error {
+ apiBaseURL := environmentOrDefault("API_BASE_URL", "http://api:8000")
+ tlsCertificate := environmentOrDefault("ADAPTER_TLS_CERT", "/certs/server.pem")
+ tlsKey := environmentOrDefault("ADAPTER_TLS_KEY", "/certs/server-key.pem")
+
+ instance := newEmulator(apiBaseURL, &http.Client{Timeout: 15 * time.Second})
+ callbackServer := newHTTPServer(callbackAddress, instance.notificationHandler())
+ callbackServer.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
+ controlServer := newHTTPServer(controlAddress, instance.controlHandler())
+
+ serverErrors := make(chan error, 2)
+ go func() {
+ log.Printf("[ADAPTER] HTTPS callback server listening on %s", callbackAddress)
+ serverErrors <- callbackServer.ListenAndServeTLS(tlsCertificate, tlsKey)
+ }()
+ go func() {
+ log.Printf("[ADAPTER] HTTP control server listening on %s", controlAddress)
+ serverErrors <- controlServer.ListenAndServe()
+ }()
+
+ signals := make(chan os.Signal, 1)
+ signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
+ defer signal.Stop(signals)
+
+ var serveErr error
+ select {
+ case <-signals:
+ log.Printf("[ADAPTER] shutdown signal received")
+ case err := <-serverErrors:
+ if !errors.Is(err, http.ErrServerClosed) {
+ serveErr = err
+ }
+ }
+
+ shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ shutdownErr := errors.Join(
+ callbackServer.Shutdown(shutdownContext),
+ controlServer.Shutdown(shutdownContext),
+ )
+ return errors.Join(serveErr, shutdownErr)
+}
+
+func newHTTPServer(address string, handler http.Handler) *http.Server {
+ return &http.Server{
+ Addr: address,
+ Handler: handler,
+ ReadHeaderTimeout: 5 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 30 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+}
+
+func environmentOrDefault(name string, fallback string) string {
+ if value := os.Getenv(name); value != "" {
+ return value
+ }
+ return fallback
+}
diff --git a/tests/adapter-emulator/notification_handler.go b/tests/adapter-emulator/notification_handler.go
new file mode 100644
index 000000000..e4108eea7
--- /dev/null
+++ b/tests/adapter-emulator/notification_handler.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+)
+
+const maxCallbackBodyBytes = 1024 * 1024
+
+type callbackEnvelope struct {
+ Message struct {
+ Token string `json:"token"`
+ Data map[string]string `json:"data"`
+ } `json:"message"`
+}
+
+func (instance *emulator) notificationHandler() http.Handler {
+ mux := http.NewServeMux()
+ mux.HandleFunc("POST /notifications/{gatewayID}", instance.handleNotification)
+ return mux
+}
+
+func (instance *emulator) handleNotification(writer http.ResponseWriter, request *http.Request) {
+ gatewayID := request.PathValue("gatewayID")
+ registeredGateway, ok := instance.loadGateway(gatewayID)
+ if !ok {
+ http.Error(writer, "unknown gateway", http.StatusNotFound)
+ return
+ }
+
+ request.Body = http.MaxBytesReader(writer, request.Body, maxCallbackBodyBytes)
+ var envelope callbackEnvelope
+ if err := json.NewDecoder(request.Body).Decode(&envelope); err != nil {
+ http.Error(writer, "invalid callback payload", http.StatusBadRequest)
+ return
+ }
+
+ kind, messageID, validationErr := notificationKind(envelope.Message.Data)
+ record := instance.recordNotification(
+ gatewayID,
+ envelope.Message.Data,
+ kind,
+ messageID,
+ )
+ log.Printf(
+ "[ADAPTER] callback gateway=%s data=%v",
+ gatewayID,
+ envelope.Message.Data,
+ )
+ if validationErr != nil {
+ instance.markNotificationFailed(record, validationErr)
+ http.Error(writer, validationErr.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var processingErr error
+ switch kind {
+ case "message":
+ _, processingErr = instance.fetchOutstanding(request.Context(), registeredGateway, messageID)
+ if processingErr == nil {
+ processingErr = instance.fireMessageEvent(request.Context(), registeredGateway, messageID, "SENT")
+ }
+ if processingErr == nil {
+ processingErr = instance.fireMessageEvent(request.Context(), registeredGateway, messageID, "DELIVERED")
+ }
+ case "heartbeat":
+ processingErr = instance.storeHeartbeat(request.Context(), registeredGateway)
+ }
+ if processingErr != nil {
+ instance.markNotificationFailed(record, processingErr)
+ log.Printf("[ADAPTER] notification failed: %v", processingErr)
+ http.Error(writer, "notification processing failed", http.StatusInternalServerError)
+ return
+ }
+
+ instance.markNotificationProcessed(record)
+ log.Printf("[ADAPTER] notification processed as %s", kind)
+ writer.WriteHeader(http.StatusNoContent)
+}
+
+func notificationKind(data map[string]string) (kind string, messageID string, err error) {
+ messageID = strings.TrimSpace(data["KEY_MESSAGE_ID"])
+ heartbeatID := strings.TrimSpace(data["KEY_HEARTBEAT_ID"])
+
+ switch {
+ case messageID != "" && heartbeatID == "":
+ return "message", messageID, nil
+ case heartbeatID != "" && messageID == "":
+ return "heartbeat", "", nil
+ default:
+ return "", "", fmt.Errorf("unsupported notification data")
+ }
+}
diff --git a/tests/adapter_integration_test.go b/tests/adapter_integration_test.go
new file mode 100644
index 000000000..6dea41f1b
--- /dev/null
+++ b/tests/adapter_integration_test.go
@@ -0,0 +1,91 @@
+package tests
+
+import (
+ "context"
+ "net/http"
+ "testing"
+ "time"
+
+ httpsms "github.com/NdoleStudio/httpsms-go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAdapterGatewayOutgoingMessage(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+ content := "Adapter outgoing " + randomEncryptionKey()
+
+ response, httpResponse, err := newAPIClient().Messages.Send(ctx, &httpsms.MessageSendParams{
+ From: phone.PhoneNumber,
+ To: contact,
+ Content: content,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, httpResponse.HTTPResponse.StatusCode)
+
+ messageID := response.Data.ID.String()
+ message := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second)
+
+ assert.Equal(t, phone.PhoneNumber, message.Owner)
+ assert.Equal(t, contact, message.Contact)
+ assert.Equal(t, content, message.Content)
+ records := waitForAdapterMessageRecords(t, phone.GatewayID, messageID, 30*time.Second)
+ require.Len(t, records, 1)
+ assert.Equal(t, "message", records[0].Kind)
+ assert.True(t, records[0].Processed)
+ assert.Equal(t, messageID, records[0].Data["KEY_MESSAGE_ID"])
+}
+
+func TestAdapterGatewayIncomingMessage(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ contact := randomPhoneNumber()
+ content := "Adapter incoming " + randomEncryptionKey()
+
+ messageID := triggerAdapterIncoming(ctx, t, phone, contact, content)
+ message := pollMessageStatus(ctx, t, messageID, "received", 15*time.Second)
+
+ assert.Equal(t, phone.PhoneNumber, message.Owner)
+ assert.Equal(t, contact, message.Contact)
+ assert.Equal(t, content, message.Content)
+ assert.Equal(t, "received", message.Status)
+}
+
+func TestAdapterGatewayHeartbeatWakeUp(t *testing.T) {
+ ctx := context.Background()
+ phone := setupAdapterPhone(ctx, t, 60)
+ monitorID := uuid.NewString()
+
+ dispatchInternalEvent(ctx, t, map[string]any{
+ "specversion": "1.0",
+ "id": uuid.NewString(),
+ "source": "/tests/adapter-emulator",
+ "type": "phone.heartbeat.missed",
+ "time": time.Now().UTC().Format(time.RFC3339),
+ "datacontenttype": "application/json",
+ "data": map[string]any{
+ "phone_id": phone.PhoneID,
+ "user_id": "test-user-id",
+ "last_heartbeat_timestamp": time.Now().UTC().Add(-20 * time.Minute).Format(time.RFC3339),
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ "monitor_id": monitorID,
+ "owner": phone.PhoneNumber,
+ },
+ })
+
+ record := waitForAdapterHeartbeatRecord(t, phone.GatewayID, 30*time.Second)
+ assert.Equal(t, "heartbeat", record.Kind)
+ assert.NotEmpty(t, record.Data["KEY_HEARTBEAT_ID"])
+
+ heartbeats, response, err := newAPIClient().Heartbeats.Index(ctx, &httpsms.HeartbeatIndexParams{
+ Owner: phone.PhoneNumber,
+ Limit: 1,
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
+ require.NotEmpty(t, heartbeats.Data)
+ assert.Equal(t, phone.PhoneNumber, heartbeats.Data[0].Owner)
+}
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
index 3d82d47c1..ca03a3130 100644
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -76,6 +76,23 @@ services:
timeout: 5s
retries: 10
+ adapter-emulator:
+ build:
+ context: ./adapter-emulator
+ ports:
+ - "9092:9092"
+ environment:
+ API_BASE_URL: http://api:8000
+ ADAPTER_TLS_CERT: /certs/server.pem
+ ADAPTER_TLS_KEY: /certs/server-key.pem
+ volumes:
+ - ./certs:/certs:ro
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://localhost:9092/health"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
api:
build:
context: ../api
@@ -90,10 +107,15 @@ services:
condition: service_healthy
mongodb:
condition: service_healthy
+ adapter-emulator:
+ condition: service_healthy
env_file:
- .env.test
environment:
FIREBASE_CREDENTIALS: "${FIREBASE_CREDENTIALS}"
+ SSL_CERT_FILE: /adapter-certs/ca.pem
+ volumes:
+ - ./certs/ca.pem:/adapter-certs/ca.pem:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 5s
diff --git a/tests/generate-adapter-certificates.sh b/tests/generate-adapter-certificates.sh
new file mode 100644
index 000000000..c42eb805c
--- /dev/null
+++ b/tests/generate-adapter-certificates.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export MSYS2_ARG_CONV_EXCL="/CN="
+
+output_dir="${1:-certs}"
+mkdir -p "$output_dir"
+
+openssl req -x509 -newkey rsa:2048 -nodes \
+ -keyout "$output_dir/ca-key.pem" \
+ -out "$output_dir/ca.pem" \
+ -days 2 \
+ -subj "/CN=httpSMS integration adapter CA"
+
+openssl req -newkey rsa:2048 -nodes \
+ -keyout "$output_dir/server-key.pem" \
+ -out "$output_dir/server.csr" \
+ -subj "/CN=adapter-emulator"
+
+cat >"$output_dir/server.ext" <<'EOF'
+subjectAltName=DNS:adapter-emulator
+extendedKeyUsage=serverAuth
+EOF
+
+openssl x509 -req \
+ -in "$output_dir/server.csr" \
+ -CA "$output_dir/ca.pem" \
+ -CAkey "$output_dir/ca-key.pem" \
+ -CAcreateserial \
+ -out "$output_dir/server.pem" \
+ -days 2 \
+ -extfile "$output_dir/server.ext"
+
+# The emulator runs as an unprivileged container user and must read this
+# bind-mounted throwaway key.
+chmod 0644 "$output_dir/server-key.pem"
diff --git a/tests/helpers_test.go b/tests/helpers_test.go
index dfbf28856..a0c84c412 100644
--- a/tests/helpers_test.go
+++ b/tests/helpers_test.go
@@ -10,6 +10,7 @@ import (
"math/big"
"mime/multipart"
"net/http"
+ "net/url"
"strings"
"testing"
"time"
@@ -26,7 +27,9 @@ const (
apiBaseURL = "http://localhost:8000"
wiremockURL = "http://localhost:8080"
wiremockWebhookURL = "http://wiremock.local:8080" // reachable from API container, passes URL validation (needs a dot)
+ adapterControlURL = "http://localhost:9092"
userAPIKey = "test-user-api-key"
+ systemAPIKey = "system-user-api-key"
)
type testPhone struct {
@@ -35,6 +38,21 @@ type testPhone struct {
FcmToken string
}
+type adapterTestPhone struct {
+ testPhone
+ PhoneID string
+ GatewayID string
+}
+
+type notificationRecord struct {
+ GatewayID string `json:"gateway_id"`
+ Data map[string]string `json:"data"`
+ MessageID string `json:"message_id,omitempty"`
+ Kind string `json:"kind"`
+ Processed bool `json:"processed"`
+ Error string `json:"error,omitempty"`
+}
+
func newAPIClient() *httpsms.Client {
return httpsms.New(
httpsms.WithBaseURL(apiBaseURL),
@@ -119,6 +137,233 @@ func setupPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) testP
}
}
+func setupAdapterPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) adapterTestPhone {
+ t.Helper()
+
+ gatewayID := uuid.NewString()
+ phoneNumber := randomPhoneNumber()
+ client := newAPIClient()
+
+ apiKeyResponse, response, err := client.PhoneAPIKeys.Store(ctx, &httpsms.PhoneAPIKeyStoreParams{
+ Name: "adapter-test-key-" + uuid.NewString(),
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode, "phone api key store failed")
+
+ phoneAPIKey := apiKeyResponse.Data.APIKey
+ require.NotEmpty(t, phoneAPIKey)
+
+ registrationBody, err := json.Marshal(map[string]any{
+ "phone_number": phoneNumber,
+ "phone_api_key": phoneAPIKey,
+ })
+ require.NoError(t, err)
+ registrationRequest, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodPut,
+ fmt.Sprintf("%s/test/gateways/%s", adapterControlURL, gatewayID),
+ bytes.NewReader(registrationBody),
+ )
+ require.NoError(t, err)
+ registrationRequest.Header.Set("Content-Type", "application/json")
+ registrationResponse, err := http.DefaultClient.Do(registrationRequest)
+ require.NoError(t, err)
+ registrationResponseBody, err := io.ReadAll(registrationResponse.Body)
+ registrationResponse.Body.Close()
+ require.NoError(t, err)
+ require.Equal(
+ t,
+ http.StatusNoContent,
+ registrationResponse.StatusCode,
+ "adapter gateway registration failed: %s",
+ string(registrationResponseBody),
+ )
+
+ callbackURL := fmt.Sprintf("https://adapter-emulator:9091/notifications/%s", gatewayID)
+ phoneResponse, response, err := client.Phones.Upsert(ctx, &httpsms.PhoneUpsertParams{
+ PhoneNumber: phoneNumber,
+ FcmToken: callbackURL,
+ MessagesPerMinute: messagesPerMinute,
+ MaxSendAttempts: 2,
+ MessageExpirationSeconds: 600,
+ SIM: "SIM1",
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode, "phone upsert failed")
+ require.NotEmpty(t, phoneResponse.Data.ID)
+
+ phoneClient := newPhoneClient(phoneAPIKey)
+ _, response, err = phoneClient.Phones.UpsertFCMToken(ctx, &httpsms.PhoneFCMTokenParams{
+ PhoneNumber: phoneNumber,
+ FcmToken: callbackURL,
+ SIM: "SIM1",
+ })
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode, "adapter callback bind failed")
+
+ waitForPhoneAuthorization(ctx, t, phoneAPIKey, phoneNumber, 20*time.Second)
+
+ return adapterTestPhone{
+ testPhone: testPhone{
+ PhoneNumber: phoneNumber,
+ PhoneAPIKey: phoneAPIKey,
+ FcmToken: callbackURL,
+ },
+ PhoneID: phoneResponse.Data.ID,
+ GatewayID: gatewayID,
+ }
+}
+
+func dispatchInternalEvent(ctx context.Context, t *testing.T, event map[string]any) {
+ t.Helper()
+
+ body, err := json.Marshal(event)
+ require.NoError(t, err)
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, apiBaseURL+"/v1/events", bytes.NewReader(body))
+ require.NoError(t, err)
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("x-api-key", systemAPIKey)
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+ defer response.Body.Close()
+
+ responseBody, err := io.ReadAll(response.Body)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusNoContent, response.StatusCode, "event dispatch failed: %s", string(responseBody))
+}
+
+func waitForAdapterMessageRecords(
+ t *testing.T,
+ gatewayID string,
+ messageID string,
+ timeout time.Duration,
+) []notificationRecord {
+ t.Helper()
+
+ deadline := time.Now().Add(timeout)
+ var records []notificationRecord
+ var lastErr error
+ for time.Now().Before(deadline) {
+ records, lastErr = fetchAdapterNotificationRecords(gatewayID, messageID)
+ if lastErr == nil && len(records) > 0 && adapterRecordsProcessed(records) {
+ return records
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ require.NoError(t, lastErr)
+ require.NotEmpty(t, records, "adapter message record for %s was not available within %v", messageID, timeout)
+ require.True(t, adapterRecordsProcessed(records), "adapter message records were not processed: %#v", records)
+ return records
+}
+
+func waitForAdapterHeartbeatRecord(
+ t *testing.T,
+ gatewayID string,
+ timeout time.Duration,
+) notificationRecord {
+ t.Helper()
+
+ deadline := time.Now().Add(timeout)
+ var lastErr error
+ for time.Now().Before(deadline) {
+ records, err := fetchAdapterNotificationRecords(gatewayID, "")
+ lastErr = err
+ if err == nil {
+ for _, record := range records {
+ if record.Kind == "heartbeat" && record.Processed {
+ return record
+ }
+ }
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ require.NoError(t, lastErr)
+ t.Fatalf("processed adapter heartbeat record was not available within %v", timeout)
+ return notificationRecord{}
+}
+
+func triggerAdapterIncoming(
+ ctx context.Context,
+ t *testing.T,
+ phone adapterTestPhone,
+ contact string,
+ content string,
+) string {
+ t.Helper()
+
+ body, err := json.Marshal(map[string]any{
+ "contact": contact,
+ "content": content,
+ "encrypted": false,
+ })
+ require.NoError(t, err)
+ request, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodPost,
+ fmt.Sprintf("%s/test/gateways/%s/incoming", adapterControlURL, phone.GatewayID),
+ bytes.NewReader(body),
+ )
+ require.NoError(t, err)
+ request.Header.Set("Content-Type", "application/json")
+
+ response, err := http.DefaultClient.Do(request)
+ require.NoError(t, err)
+ defer response.Body.Close()
+ responseBody, err := io.ReadAll(response.Body)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, response.StatusCode, "adapter incoming trigger failed: %s", string(responseBody))
+
+ var result struct {
+ Data struct {
+ ID string `json:"id"`
+ } `json:"data"`
+ }
+ require.NoError(t, json.Unmarshal(responseBody, &result))
+ require.NotEmpty(t, result.Data.ID)
+ return result.Data.ID
+}
+
+func fetchAdapterNotificationRecords(gatewayID string, messageID string) ([]notificationRecord, error) {
+ endpoint := fmt.Sprintf("%s/test/gateways/%s/notifications", adapterControlURL, gatewayID)
+ if messageID != "" {
+ endpoint += "?message_id=" + url.QueryEscape(messageID)
+ }
+
+ response, err := (&http.Client{Timeout: 5 * time.Second}).Get(endpoint)
+ if err != nil {
+ return nil, fmt.Errorf("fetch adapter notification records: %w", err)
+ }
+ defer response.Body.Close()
+
+ responseBody, err := io.ReadAll(response.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read adapter notification records: %w", err)
+ }
+ if response.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("fetch adapter notification records: status %d: %s", response.StatusCode, string(responseBody))
+ }
+
+ var result struct {
+ Data []notificationRecord `json:"data"`
+ }
+ if err := json.Unmarshal(responseBody, &result); err != nil {
+ return nil, fmt.Errorf("decode adapter notification records: %w", err)
+ }
+ return result.Data, nil
+}
+
+func adapterRecordsProcessed(records []notificationRecord) bool {
+ for _, record := range records {
+ if !record.Processed {
+ return false
+ }
+ }
+ return true
+}
+
func waitForPhoneAuthorization(
ctx context.Context,
t *testing.T,
From 42b1dd7aa2445166bdeaf0eae9045915f02a9d8a Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Fri, 4 Sep 2026 08:16:27 +0300
Subject: [PATCH 56/88] chore: remove superpoers docs
---
.../2026-05-26-nuxt4-vuetify4-migration.md | 3300 -----------------
.../2026-07-18-message-thread-archive-ui.md | 241 --
.../plans/2026-07-18-read-receipts.md | 1760 ---------
.../2026-07-18-unarchive-thread-on-receive.md | 848 -----
...-07-18-webhook-email-payload-formatting.md | 593 ---
.../plans/2026-07-19-contacts-feature.md | 3025 ---------------
.../plans/2026-07-23-signup-name.md | 167 -
...2-url-backed-phone-notification-adapter.md | 1863 ----------
...6-05-26-nuxt4-vuetify4-migration-design.md | 385 --
...2026-06-24-login-last-used-badge-design.md | 75 -
...26-07-05-affiliates-landing-page-design.md | 109 -
.../2026-07-13-getting-started-page-design.md | 88 -
...-07-18-message-thread-archive-ui-design.md | 37 -
.../specs/2026-07-18-read-receipts-design.md | 286 --
...7-18-unarchive-thread-on-receive-design.md | 118 -
...webhook-email-payload-formatting-design.md | 116 -
.../2026-07-19-contacts-feature-design.md | 252 --
...26-07-19-email-number-formatting-design.md | 30 -
.../specs/2026-07-23-signup-name-design.md | 33 -
.../2026-08-21-contact-phone-input-design.md | 29 -
...2026-08-21-contacts-table-header-design.md | 29 -
...acked-phone-notification-adapter-design.md | 514 ---
22 files changed, 13898 deletions(-)
delete mode 100644 docs/superpowers/plans/2026-05-26-nuxt4-vuetify4-migration.md
delete mode 100644 docs/superpowers/plans/2026-07-18-message-thread-archive-ui.md
delete mode 100644 docs/superpowers/plans/2026-07-18-read-receipts.md
delete mode 100644 docs/superpowers/plans/2026-07-18-unarchive-thread-on-receive.md
delete mode 100644 docs/superpowers/plans/2026-07-18-webhook-email-payload-formatting.md
delete mode 100644 docs/superpowers/plans/2026-07-19-contacts-feature.md
delete mode 100644 docs/superpowers/plans/2026-07-23-signup-name.md
delete mode 100644 docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
delete mode 100644 docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md
delete mode 100644 docs/superpowers/specs/2026-06-24-login-last-used-badge-design.md
delete mode 100644 docs/superpowers/specs/2026-07-05-affiliates-landing-page-design.md
delete mode 100644 docs/superpowers/specs/2026-07-13-getting-started-page-design.md
delete mode 100644 docs/superpowers/specs/2026-07-18-message-thread-archive-ui-design.md
delete mode 100644 docs/superpowers/specs/2026-07-18-read-receipts-design.md
delete mode 100644 docs/superpowers/specs/2026-07-18-unarchive-thread-on-receive-design.md
delete mode 100644 docs/superpowers/specs/2026-07-18-webhook-email-payload-formatting-design.md
delete mode 100644 docs/superpowers/specs/2026-07-19-contacts-feature-design.md
delete mode 100644 docs/superpowers/specs/2026-07-19-email-number-formatting-design.md
delete mode 100644 docs/superpowers/specs/2026-07-23-signup-name-design.md
delete mode 100644 docs/superpowers/specs/2026-08-21-contact-phone-input-design.md
delete mode 100644 docs/superpowers/specs/2026-08-21-contacts-table-header-design.md
delete mode 100644 docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md
diff --git a/docs/superpowers/plans/2026-05-26-nuxt4-vuetify4-migration.md b/docs/superpowers/plans/2026-05-26-nuxt4-vuetify4-migration.md
deleted file mode 100644
index d5853283d..000000000
--- a/docs/superpowers/plans/2026-05-26-nuxt4-vuetify4-migration.md
+++ /dev/null
@@ -1,3300 +0,0 @@
-# Nuxt 4 + Vuetify 4 Migration 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:** Migrate the httpSMS frontend from Nuxt 2 + Vue 2 + Vuetify 2 to Nuxt 4 + Vue 3 + Vuetify 4 with full TypeScript, Pinia state management, and `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-- [ ] **Step 2: Create website layout**
-
-```vue
-
-
-
-
-
-
-
-
-
-
-
-
- httpSMS
-
-
-
-
- Pricing
-
-
- Blog
-
-
- Login
-
-
- Get Started
- For Free
-
-
- Dashboard
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- httpSMS
-
-
- Made With in Tallinn
-
-
-
-
-
-
-
-
-
-
-
- Resources
-
-
-
- Developers
-
-
-
- Legal
-
-
-
-
-
-
-
-```
-
-- [ ] **Step 3: Create error layout**
-
-```vue
-
-
-
-
-
-
-
- {{ error.statusCode }}
- {{ error.message }}
- Go Home
-
-
-
-
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "feat(web): port layouts to Nuxt 4 with Vuetify 4"
-```
-
----
-
-## Task 13: Port Components — Toast, LoadingDashboard, LoadingButton, BackButton
-
-**Files:**
-- Create: `web/app/components/Toast.vue`
-- Create: `web/app/components/LoadingDashboard.vue`
-- Create: `web/app/components/LoadingButton.vue`
-- Create: `web/app/components/BackButton.vue`
-
-**IMPORTANT:** Call `vuetify-mcp-get_component_api_by_version` for v-snackbar, v-btn, v-progress-circular, v-icon, v-container, v-row, v-col before writing these.
-
-- [ ] **Step 1: Create Toast.vue**
-
-```vue
-
-
-
-
-
-
-
- {{ notificationsStore.notification.message }}
-
-
- Close
-
-
-
-
-```
-
-- [ ] **Step 2: Create LoadingDashboard.vue**
-
-```vue
-
-
-
-
-
-
-
-
-
-
- Loading the httpSMS dashboard
-
-
-
-
-
-
-
-```
-
-- [ ] **Step 3: Create LoadingButton.vue**
-
-```vue
-
-
-
-
-
-
-
-
-
-
-```
-
-- [ ] **Step 4: Create BackButton.vue**
-
-```vue
-
-
-
-
-
-
- Go Back
-
-
-```
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add -A
-git commit -m "feat(web): port Toast, LoadingDashboard, LoadingButton, BackButton components"
-```
-
----
-
-## Task 14: Port Components — CopyButton, FixedHeader, BlogAuthorBio, BlogInfo, NuxtLogo
-
-**Files:**
-- Create: `web/app/components/CopyButton.vue`
-- Create: `web/app/components/FixedHeader.vue`
-- Create: `web/app/components/BlogAuthorBio.vue`
-- Create: `web/app/components/BlogInfo.vue`
-- Create: `web/app/components/NuxtLogo.vue`
-
-**IMPORTANT:** Call `vuetify-mcp-get_component_api_by_version` for each Vuetify component used.
-
-- [ ] **Step 1: Create CopyButton.vue**
-
-```vue
-
-
-
-
-
-
- {{ copyText }}
-
-
-```
-
-- [ ] **Step 2: Create FixedHeader.vue**
-
-```vue
-
-
-
-
-
-
-
-
-
-
- HTTP SMS
-
-
-
- Get Started
- For Free
-
-
-
-
-
-
-```
-
-- [ ] **Step 3: Create BlogAuthorBio.vue**
-
-```vue
-
-
-
-
-
-
-```
-
-- [ ] **Step 4: Create BlogInfo.vue**
-
-```vue
-
-
-
-
-
-
-
- httpSMS
-
-
- httpSMS is an
- open source
- application that converts your android phone into an SMS gateway so you
- can send and receive SMS messages using a simple HTTP API.
-
-
-
- Documentation
-
-
-
-```
-
-- [ ] **Step 5: Create NuxtLogo.vue (minimal placeholder)**
-
-```vue
-
-
-
-
-```
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add -A
-git commit -m "feat(web): port CopyButton, FixedHeader, BlogAuthorBio, BlogInfo, NuxtLogo"
-```
-
----
-
-## Task 15: Port Components — FirebaseAuth, MessageThread, MessageThreadHeader
-
-**Files:**
-- Create: `web/app/components/FirebaseAuth.vue`
-- Create: `web/app/components/MessageThread.vue`
-- Create: `web/app/components/MessageThreadHeader.vue`
-
-**IMPORTANT:** Call `vuetify-mcp-get_component_api_by_version` for v-select, v-list, v-list-item, v-menu, v-tooltip, v-sheet, v-progress-linear before writing.
-
-- [ ] **Step 1: Create FirebaseAuth.vue**
-
-This component uses FirebaseUI which must be loaded client-side only. In Nuxt 4, wrap with `` or use `.client.vue` suffix.
-
-```vue
-
-
-
-
-
-
-```
-
-- [ ] **Step 2: Create MessageThread.vue**
-
-Port from the existing component. This is a larger component — read the full source from the backup branch and rewrite using `
-
-
-
-
-
- Archived Messages
-
-
-
-
-
- Start sending messages
-
-
-
-
- New Message
-
-
-
-
-
-
-```
-
-Note: The full MessageThread.vue content should be ported from the backup branch. The pattern above shows the migration approach.
-
-- [ ] **Step 3: Create MessageThreadHeader.vue**
-
-```vue
-
-
-
-
-
-
-
-
-
-
-
- {{ phoneCountry(phonesStore.owner) }}
-
-
-
-
-
-
-
-
- Last Heartbeat
- {{ humanizeTime(phonesStore.heartbeat.timestamp) }} ago
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ threadsStore.archivedThreads ? 'Unarchived' : 'Archived' }}
-
-
-
-
- New Message
-
-
-
- Bulk Messages
-
-
-
- Search Messages
-
-
-
- Settings
-
-
-
- Phone API Keys
-
-
-
- Install App
-
-
-
- Usage & Billing
-
-
-
- Logout
-
-
-
-
-
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "feat(web): port FirebaseAuth, MessageThread, MessageThreadHeader components"
-```
-
----
-
-## Task 16: Port Pages — Login & Index (Homepage)
-
-**Files:**
-- Create: `web/app/pages/login.vue`
-- Create: `web/app/pages/index.vue`
-
-**IMPORTANT:** Call `vuetify-mcp-get_component_api_by_version` for all Vuetify components used. Port from the backup branch source, converting class-based syntax to `&"}`)
- html := string(rich)
-
- assert.Contains(t, plain, `
-
-
-
-
-
-
-
- Contacts
-
-
-
-
-
-
-
-
-
Contacts
-
- {{ contactsStore.total }}
- {{ contactsStore.total === 1 ? 'contact' : 'contacts' }}
-
-
-
-
-
- Import CSV
-
-
- Add Contact
-
-
-
-
-
-
-
-
-
- | Name |
- Emails |
- Phone Numbers |
- Actions |
-
-
-
-
- | {{ contact.name }} |
-
- {{ (contact.emails ?? []).join(', ') }}
- |
-
- {{
- (contact.phone_numbers ?? [])
- .map((number) => formatPhoneNumber(number))
- .join(', ')
- }}
- |
-
-
-
-
-
-
-
- |
-
-
- |
- No contacts found.
- |
-
-
-
-
-
-
-
-
-
-
-
- {{ dialogTitle }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Close
-
-
- Save
-
-
-
-
-
-
-
-
-
- Delete Contact
-
-
-
-
-
-
- Are you sure you want to delete this contact? This action cannot be
- undone.
-
-
-
-
- Close
-
-
- Delete
-
-
-
-
-
-
-
-
-
- Import Contacts from CSV
-
-
-
-
-
-
-
- Download our
- CSV template, fill it in and upload it here. Columns: Name, Emails,
- PhoneNumbers (multiple values separated by ";").
-
-
-
-
-
-
- Close
-
-
- Import
-
-
-
-
-
-
-```
-
-- [ ] **Step 2: Lint the page**
-
-Run: `cd web && pnpm lint:js && pnpm lint:prettier`
-Expected: no errors for `app/pages/contacts/index.vue`. If Prettier reports formatting, run `pnpm lintfix` and re-check.
-
-- [ ] **Step 3: Build the site to confirm it compiles**
-
-Run: `cd web && pnpm generate`
-Expected: build completes without type errors (the `/contacts` route is emitted).
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add web/app/pages/contacts/index.vue
-git commit -m "feat(web): add contacts page with add/edit/delete/import dialogs"
-```
-
----
-
-### Task 13: Show contact names on threads + Contacts nav link
-
-**Files:**
-- Modify: `web/app/stores/threads.ts` (send `contacts: true`)
-- Modify: `web/app/components/MessageThread.vue` (list title + avatar initial)
-- Modify: `web/app/pages/threads/[id]/index.vue` (toolbar title)
-- Modify: `web/app/components/MessageThreadHeader.vue` (Contacts nav item + icon import)
-
-**Interfaces:**
-- Consumes: `EntitiesMessageThread.contact_details?: EntitiesContact` (Task 10), the `/contacts` route (Task 12).
-- Produces: no new exports — UI wiring only.
-
-- [ ] **Step 1: Request contact resolution when loading threads**
-
-In `web/app/stores/threads.ts`, add `contacts: true` to the `loadThreads` request params:
-
-```ts
- const response = await apiFetch<{ data: EntitiesMessageThread[] }>(
- '/v1/message-threads',
- {
- params: {
- owner: phonesStore.owner ?? phonesStore.phones[0]?.phone_number,
- limit: 100,
- is_archived: archivedThreads.value,
- contacts: true,
- },
- },
- )
-```
-
-- [ ] **Step 2: Show the contact name in the threads list**
-
-In `web/app/components/MessageThread.vue`, replace the list-item title expression:
-
-```vue
- {{
- thread.contact_details?.name ?? formatPhoneNumber(thread.contact)
- }}
-```
-
-And update the avatar initial to prefer the contact name (replace the existing `` avatar content):
-
-```vue
-
-
- {{ mdiAccount }}
- {{
- (thread.contact_details?.name ?? thread.contact).substring(0, 1)
- }}
-
-
-```
-
-- [ ] **Step 3: Show the contact name in the thread header**
-
-In `web/app/pages/threads/[id]/index.vue`, replace the toolbar title expression (around line 271):
-
-```vue
-
- {{
- threadsStore.currentThread.contact_details?.name ??
- formatPhoneNumber(threadsStore.currentThread.contact)
- }}
-
-```
-
-- [ ] **Step 4: Add the Contacts navigation item**
-
-In `web/app/components/MessageThreadHeader.vue`, add `mdiAccountMultiple` to the `@mdi/js` import block:
-
-```ts
- mdiCommentTextMultipleOutline,
- mdiAccountMultiple,
- mdiCircle,
-```
-
-Add a nav item right after the "Search Messages" `v-list-item` (before the Settings item):
-
-```vue
-
-
- Contacts
-
-```
-
-- [ ] **Step 5: Lint and build**
-
-Run: `cd web && pnpm lint:js && pnpm generate`
-Expected: no errors; the build succeeds.
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add web/app/stores/threads.ts web/app/components/MessageThread.vue web/app/pages/threads/[id]/index.vue web/app/components/MessageThreadHeader.vue
-git commit -m "feat(web): display contact names on threads and add Contacts nav link"
-```
-
----
-
-## Final Verification
-
-- [ ] **API full test + build**
-
-```bash
-cd api && go test -vet=off ./... && go build ./...
-```
-Expected: all packages pass; build succeeds.
-
-- [ ] **Web lint + build**
-
-```bash
-cd web && pnpm lint && pnpm generate
-```
-Expected: lint clean; static build succeeds.
-
-- [ ] **Manual smoke test (optional, with Docker stack)**
-
-```bash
-docker compose up --build
-```
-Then: create a contact via `POST /v1/contacts`, load `/threads` and confirm the contact name shows for a matching phone number; open `/contacts` and exercise add/edit/delete/import.
diff --git a/docs/superpowers/plans/2026-07-23-signup-name.md b/docs/superpowers/plans/2026-07-23-signup-name.md
deleted file mode 100644
index 1bebf08ad..000000000
--- a/docs/superpowers/plans/2026-07-23-signup-name.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# Signup Name 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:** Require a name during Firebase email/password signup, persist it to the Firebase profile, and clear stale form errors when switching authentication modes.
-
-**Architecture:** Keep the change inside the existing `FirebaseAuth.vue` component. The component will conditionally render and validate the name, call Firebase `updateProfile` immediately after account creation, and use a dedicated mode-toggle handler to clear errors.
-
-**Tech Stack:** Nuxt 4, Vue 3 Composition API, Vuetify 4, TypeScript, Firebase Authentication
-
-## Global Constraints
-
-- The visible field label must be `Name`.
-- The name is required only for email/password signup.
-- Persist the trimmed name as Firebase `displayName`.
-- Do not add a new test framework; this web project currently has no configured tests.
-
----
-
-### Task 1: Add signup name collection and profile persistence
-
-**Files:**
-- Modify: `web/app/components/FirebaseAuth.vue`
-
-**Interfaces:**
-- Consumes: Firebase `createUserWithEmailAndPassword(auth, email, password)` and `updateProfile(user, { displayName })`
-- Produces: `toggleAuthMode(): void`, required signup `name` state, and Firebase users with a populated `displayName`
-
-- [ ] **Step 1: Confirm the current web test command**
-
-Run:
-
-```bash
-cd web
-pnpm test
-```
-
-Expected: PASS with `No tests configured yet`. Do not introduce a test framework for this focused change.
-
-- [ ] **Step 2: Add Firebase profile support and name state**
-
-In `web/app/components/FirebaseAuth.vue`, add `updateProfile` to the existing
-`firebase/auth` import:
-
-```ts
-import {
- getAuth,
- signInWithPopup,
- GoogleAuthProvider,
- GithubAuthProvider,
- signInWithEmailAndPassword,
- createUserWithEmailAndPassword,
- sendPasswordResetEmail,
- updateProfile,
-} from 'firebase/auth'
-```
-
-Add the name state beside the existing email and password state:
-
-```ts
-const name = ref('')
-const email = ref('')
-const password = ref('')
-```
-
-- [ ] **Step 3: Require the name during signup validation**
-
-At the start of `validateLoginForm`, after `let valid = true`, add:
-
-```ts
-if (isSignUp.value && !name.value.trim()) {
- errorMessages.value.add('name', 'Please provide your name')
- valid = false
-}
-```
-
-This preserves sign-in validation while requiring a non-whitespace name only
-in signup mode.
-
-- [ ] **Step 4: Persist the name after Firebase account creation**
-
-Replace the signup branch in `submitEmail` with:
-
-```ts
-if (isSignUp.value) {
- result = await createUserWithEmailAndPassword(
- auth,
- email.value.trim(),
- password.value,
- )
- await updateProfile(result.user, {
- displayName: name.value.trim(),
- })
-} else {
- result = await signInWithEmailAndPassword(
- auth,
- email.value.trim(),
- password.value,
- )
-}
-```
-
-Keep the existing `onSuccess(result.user, 'email')` call after the branch so
-the auth store receives the updated Firebase user before redirecting.
-
-- [ ] **Step 5: Clear errors when switching sign-in and signup modes**
-
-Add this function beside the existing form navigation helpers:
-
-```ts
-function toggleAuthMode() {
- clearErrors()
- isSignUp.value = !isSignUp.value
-}
-```
-
-Change the mode-toggle button handler from:
-
-```vue
-@click="isSignUp = !isSignUp"
-```
-
-to:
-
-```vue
-@click="toggleAuthMode"
-```
-
-- [ ] **Step 6: Render the signup-only Name field**
-
-Before the email field in the sign-in/sign-up form, add:
-
-```vue
-
-```
-
-- [ ] **Step 7: Run focused web validation**
-
-Run:
-
-```bash
-cd web
-pnpm exec eslint app/components/FirebaseAuth.vue
-pnpm exec prettier --check app/components/FirebaseAuth.vue
-pnpm exec stylelint app/components/FirebaseAuth.vue
-```
-
-Expected: all three commands exit successfully with no lint, formatting, or
-style errors.
-
-- [ ] **Step 8: Commit the implementation**
-
-```bash
-git add web/app/components/FirebaseAuth.vue
-git commit -m "feat(web): collect name during signup"
-```
diff --git a/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md b/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
deleted file mode 100644
index 6b81ee359..000000000
--- a/docs/superpowers/plans/2026-09-02-url-backed-phone-notification-adapter.md
+++ /dev/null
@@ -1,1863 +0,0 @@
-# URL-backed Phone Notification Adapter 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 a phone whose existing `fcm_token` is an HTTPS URL to receive message and heartbeat wake-ups over HTTP while preserving the current scheduling, backpressure, outstanding-message, and status-event flows.
-
-**Architecture:** Add transport helpers to `entities.Phone`, inject a map of existing `FCMClient` implementations keyed by transport, and let `PhoneNotificationService` select the client through map lookup. The HTTP path uses a standard OpenTelemetry-wrapped `http.Client`, application retries with `github.com/avast/retry-go/v5`, FCM-compatible JSON, and the existing notification success/failure state transitions.
-
-**Tech Stack:** Go 1.25.8, Fiber v3, Firebase Admin Messaging, OpenTelemetry, `net/http`, `retry-go/v5`, Testify, Docker Compose.
-
-**Spec:** `docs/superpowers/specs/2026-09-02-url-backed-phone-notification-adapter-design.md`
-
-## Final Accepted Architecture
-
-This section supersedes later historical task text and code samples that
-introduce an endpoint policy, DNS/IP filtering, custom dialing, or a
-private-host allowlist.
-
-- `Phone.NotificationTransport` performs only URL syntax, HTTPS scheme, and
- hostname classification; URL user information remains valid.
-- `Container.NotificationHTTPClient` uses the existing
- `go-otelroundtripper` pattern with the `phone_notification_http` name and
- the default transport, matching the webhook client style without custom URL
- redaction.
-- `HTTPNotificationSender` creates one reusable `*retry.Retrier` during
- initialization. It owns exactly three application attempts with exponential
- backoff, a fresh request/body, and a five-second child context per attempt;
- caller contexts are enforced per operation rather than stored on the
- reusable retrier.
-- Payload encoding, request creation, one-attempt delivery, and retry
- configuration are separate focused methods. OpenTelemetry HTTP telemetry is
- owned by the injected round-tripper, with no sender-specific attempt metrics
- or spans.
-- No endpoint DNS/IP/SSRF policy, custom notification transport, or private-host
- allowlist is part of the final implementation.
-- `PhoneNotificationService` receives
- `map[entities.NotificationTransport]FCMClient`, determines transport once,
- selects the client by map lookup, sets the trimmed destination in
- `message.Token`, and passes the same pointer to `FCMClient.Send`. There is no
- extra sender interface, Firebase wrapper, or dispatcher class.
-- `HTTPNotificationSender` implements `FCMClient` exactly and returns
- `http/success` after a successful callback response.
-- Domain events continue to use the existing `EventDispatcher` directly.
-
-## Global Constraints
-
-- Reuse the existing `Phone.FcmToken` database field and `fcm_token` API field; add no transport or endpoint columns.
-- A valid `https://` URL with a hostname selects HTTP; an opaque non-URL token selects Firebase.
-- URL-like malformed or unsupported tokens are invalid and must never fall through to Firebase.
-- Send both outstanding-message and heartbeat notifications through the selected transport.
-- HTTP callback requests are unsigned and contain no message content, user API key, phone API key, or other credentials.
-- HTTPS callback URLs may include standard URL user information.
-- Accept any HTTP `2xx`; ignore response content.
-- Make at most three HTTP attempts with a five-second timeout per attempt.
-- Retry network failures, `408`, `429`, and `5xx`; do not retry other non-`2xx` responses.
-- Use the standard OpenTelemetry-wrapped HTTP transport without destination
- DNS/IP filtering, custom dialing, or a private-host allowlist.
-- Preserve existing schedules, per-minute backpressure, message expiration, send-attempt counting, outstanding-message fetching, and message event routes.
-- Use `stacktrace.Propagate` or `stacktrace.Propagatef` for returned errors.
-- Use GORM query builders with context propagation; this feature requires no database query changes or migration.
-- Format Go code with `go-fumpt` through the repository's existing tooling.
-
----
-
-## File Structure
-
-### Create
-
-- `api/pkg/entities/phone_test.go` - table-driven transport classification tests.
-- `api/pkg/services/notification_endpoint_policy.go` - public HTTPS URL validation, reserved-IP rejection, and validated dialing.
-- `api/pkg/services/notification_endpoint_policy_test.go` - deterministic resolver/dialer tests, including DNS rebinding protection.
-- `api/pkg/services/fcm_client.go` - common phone notification client contract and Firebase client.
-- `api/pkg/services/phone_notification_service.go` - message construction and transport-client lookup.
-- `api/pkg/services/http_notification_sender.go` - HTTP request encoding, retry classification, and timeout.
-- `api/pkg/services/http_notification_sender_test.go` - payload, retry, and response tests.
-- `api/pkg/services/phone_notification_service_test.go` - message and heartbeat integration tests with hand-written fakes.
-- `api/pkg/validators/phone_handler_validator_test.go` - URL token validation tests for both phone update routes.
-- `tests/adapter-emulator/Dockerfile` - container image for the HTTPS adapter emulator.
-- `tests/adapter-emulator/go.mod` - isolated emulator module.
-- `tests/adapter-emulator/main.go` - HTTPS callback and HTTP control server startup.
-- `tests/adapter-emulator/emulator.go` - gateway registry, deduplication, and callback records.
-- `tests/adapter-emulator/api_client.go` - existing httpSMS phone API calls.
-- `tests/adapter-emulator/notification_handler.go` - FCM-envelope message and heartbeat handling.
-- `tests/adapter-emulator/control_handler.go` - test registration, incoming-message, and record endpoints.
-- `tests/adapter_integration_test.go` - outgoing, incoming, and heartbeat end-to-end tests.
-- `tests/generate-adapter-certificates.sh` - throwaway CA and server-certificate generation.
-
-### Modify
-
-- `api/pkg/entities/phone.go` - add `NotificationTransport`, `NotificationTransport()`, and `NotificationURL()`.
-- `api/pkg/services/fcm_client.go` - keep the SDK wrapper; document its role as the low-level Firebase client.
-- `api/pkg/services/phone_notification_service.go` - replace direct Firebase messages with neutral notifications and transport-aware failure text.
-- `api/pkg/validators/phone_handler_validator.go` - inject and apply the endpoint policy for URL-like tokens.
-- `api/pkg/di/container.go` - construct the HTTP client, transport-client map,
- and updated service dependencies.
-- `api/pkg/requests/phone_update_request.go` - document dual-purpose `fcm_token`.
-- `api/pkg/requests/phone_fcm_token_request.go` - document dual-purpose `fcm_token`.
-- `api/pkg/entities/phone_notification.go` - update FCM-specific comments to transport-neutral wording.
-- `api/docs/docs.go` - regenerate with `swag`.
-- `api/docs/swagger.json` - regenerate with `swag`.
-- `api/docs/swagger.yaml` - regenerate with `swag`.
-- `tests/docker-compose.yml` - run the emulator and mount TLS material.
-- `tests/.env.test` - allowlist the emulator hostname only in local mode.
-- `tests/helpers_test.go` - adapter setup, control client, and internal-event helpers.
-- `tests/README.md` - document emulator architecture and commands.
-- `.github/workflows/api.yml` - generate adapter certificates before Docker startup.
-- `.gitignore` - ignore generated adapter certificates.
-
----
-
-### Task 1: Classify the Existing Token Field
-
-**Files:**
-- Modify: `api/pkg/entities/phone.go`
-- Create: `api/pkg/entities/phone_test.go`
-
-**Interfaces:**
-- Consumes: `Phone.FcmToken *string`
-- Produces:
-
-```go
-type NotificationTransport string
-
-const (
- NotificationTransportFCM NotificationTransport = "fcm"
- NotificationTransportHTTP NotificationTransport = "http"
-)
-
-func (phone *Phone) NotificationTransport() (NotificationTransport, error)
-func (phone *Phone) NotificationURL() (*url.URL, error)
-```
-
-- [ ] **Step 1: Write failing transport-classification tests**
-
-Create `api/pkg/entities/phone_test.go`:
-
-```go
-package entities
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func stringPointer(value string) *string {
- return &value
-}
-
-func TestPhoneNotificationTransport(t *testing.T) {
- tests := []struct {
- name string
- token *string
- transport NotificationTransport
- hasError bool
- }{
- {name: "firebase token", token: stringPointer("fcm-token:value"), transport: NotificationTransportFCM},
- {name: "public https url", token: stringPointer("https://adapter.example.com/notify"), transport: NotificationTransportHTTP},
- {name: "missing token", token: nil, hasError: true},
- {name: "empty token", token: stringPointer(" "), hasError: true},
- {name: "http url", token: stringPointer("http://adapter.example.com/notify"), hasError: true},
- {name: "ftp url", token: stringPointer("ftp://adapter.example.com/notify"), hasError: true},
- {name: "missing host", token: stringPointer("https:///notify"), hasError: true},
- {name: "embedded credentials", token: stringPointer("https://user:pass@adapter.example.com/notify"), hasError: true},
- {name: "malformed url", token: stringPointer("https://[::1"), hasError: true},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- phone := &Phone{FcmToken: test.token}
-
- transport, err := phone.NotificationTransport()
-
- if test.hasError {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, test.transport, transport)
- })
- }
-}
-
-func TestPhoneNotificationURL(t *testing.T) {
- phone := &Phone{FcmToken: stringPointer("https://adapter.example.com/notify?tenant=42")}
-
- endpoint, err := phone.NotificationURL()
-
- require.NoError(t, err)
- assert.Equal(t, "https", endpoint.Scheme)
- assert.Equal(t, "adapter.example.com", endpoint.Hostname())
- assert.Equal(t, "/notify", endpoint.Path)
- assert.Equal(t, "tenant=42", endpoint.RawQuery)
-}
-
-func TestPhoneNotificationURLRejectsFCMToken(t *testing.T) {
- phone := &Phone{FcmToken: stringPointer("fcm-token:value")}
-
- _, err := phone.NotificationURL()
-
- require.Error(t, err)
-}
-```
-
-- [ ] **Step 2: Run the entity tests and confirm the new API is missing**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/entities -run 'TestPhoneNotification' -count=1
-```
-
-Expected: compilation fails because `NotificationTransport`,
-`NotificationTransportFCM`, `NotificationTransportHTTP`,
-`Phone.NotificationTransport`, and `Phone.NotificationURL` do not exist.
-
-- [ ] **Step 3: Implement token classification once on `Phone`**
-
-Add imports for `fmt`, `net/url`, and `strings` in
-`api/pkg/entities/phone.go`, then add:
-
-```go
-// NotificationTransport identifies how a phone receives wake-up notifications.
-type NotificationTransport string
-
-const (
- // NotificationTransportFCM sends notifications through Firebase.
- NotificationTransportFCM NotificationTransport = "fcm"
- // NotificationTransportHTTP sends notifications to a public HTTPS endpoint.
- NotificationTransportHTTP NotificationTransport = "http"
-)
-
-// NotificationTransport returns the transport encoded by FcmToken.
-func (phone *Phone) NotificationTransport() (NotificationTransport, error) {
- if phone.FcmToken == nil || strings.TrimSpace(*phone.FcmToken) == "" {
- return "", fmt.Errorf("phone has no notification token")
- }
-
- token := strings.TrimSpace(*phone.FcmToken)
- endpoint, err := url.Parse(token)
- if err != nil {
- if strings.Contains(token, "://") {
- return "", fmt.Errorf("invalid notification URL: %w", err)
- }
- return NotificationTransportFCM, nil
- }
-
- if endpoint.Scheme == "" {
- return NotificationTransportFCM, nil
- }
- if endpoint.Scheme != "https" {
- return "", fmt.Errorf("notification URL must use https")
- }
- if endpoint.Hostname() == "" {
- return "", fmt.Errorf("notification URL must include a hostname")
- }
- return NotificationTransportHTTP, nil
-}
-
-// NotificationURL returns the parsed endpoint for an HTTP notification token.
-func (phone *Phone) NotificationURL() (*url.URL, error) {
- transport, err := phone.NotificationTransport()
- if err != nil {
- return nil, err
- }
- if transport != NotificationTransportHTTP {
- return nil, fmt.Errorf("phone notification transport is [%s], not HTTP", transport)
- }
-
- endpoint, err := url.Parse(strings.TrimSpace(*phone.FcmToken))
- if err != nil {
- return nil, fmt.Errorf("cannot parse notification URL: %w", err)
- }
- return endpoint, nil
-}
-```
-
-Before finishing this step, replace the plain `fmt.Errorf` wrappers with
-`stacktrace.Propagatef` or `stacktrace.NewErrorf` to match repository error
-conventions. Preserve the exact public method signatures above.
-
-- [ ] **Step 4: Run the focused entity tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/entities -run 'TestPhoneNotification' -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 5: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/entities/phone.go pkg/entities/phone_test.go
-git add pkg/entities/phone.go pkg/entities/phone_test.go
-git commit -m "feat(api): classify phone notification tokens"
-```
-
----
-
-### Task 2: Enforce Public HTTPS Endpoint Policy
-
-**Files:**
-- Create: `api/pkg/services/notification_endpoint_policy.go`
-- Create: `api/pkg/services/notification_endpoint_policy_test.go`
-
-**Interfaces:**
-- Consumes: parsed HTTPS endpoints from `Phone.NotificationURL()`
-- Produces:
-
-```go
-type HostResolver interface {
- LookupNetIP(ctx context.Context, network string, host string) ([]netip.Addr, error)
-}
-
-type NotificationEndpointPolicy struct {
- resolver HostResolver
- allowedPrivateHosts map[string]struct{}
-}
-
-func NewNotificationEndpointPolicy(resolver HostResolver, allowedPrivateHosts []string) *NotificationEndpointPolicy
-func (policy *NotificationEndpointPolicy) Validate(ctx context.Context, endpoint *url.URL) ([]netip.Addr, error)
-func (policy *NotificationEndpointPolicy) DialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error)
-```
-
-- [ ] **Step 1: Write failing policy tests with a deterministic resolver**
-
-Create `api/pkg/services/notification_endpoint_policy_test.go` with:
-
-```go
-package services
-
-import (
- "context"
- "net/netip"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type staticHostResolver struct {
- addresses map[string][]netip.Addr
- err error
-}
-
-func (resolver *staticHostResolver) LookupNetIP(_ context.Context, _ string, host string) ([]netip.Addr, error) {
- if resolver.err != nil {
- return nil, resolver.err
- }
- return resolver.addresses[host], nil
-}
-
-func TestNotificationEndpointPolicyValidate(t *testing.T) {
- tests := []struct {
- name string
- rawURL string
- addresses []netip.Addr
- hasError bool
- }{
- {name: "public IPv4", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}},
- {name: "public IPv6", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("2606:4700:4700::1111")}},
- {name: "loopback", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, hasError: true},
- {name: "private", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("10.0.0.5")}, hasError: true},
- {name: "link local", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("169.254.169.254")}, hasError: true},
- {name: "carrier grade NAT", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, hasError: true},
- {name: "documentation range", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, hasError: true},
- {name: "unique local IPv6", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("fd00::1")}, hasError: true},
- {name: "mixed public and private", rawURL: "https://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("10.0.0.5")}, hasError: true},
- {name: "embedded credentials", rawURL: "https://user:pass@adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, hasError: true},
- {name: "insecure scheme", rawURL: "http://adapter.example.com/notify", addresses: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, hasError: true},
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- endpoint, err := url.Parse(test.rawURL)
- require.NoError(t, err)
- policy := NewNotificationEndpointPolicy(&staticHostResolver{
- addresses: map[string][]netip.Addr{endpoint.Hostname(): test.addresses},
- }, nil)
-
- addresses, err := policy.Validate(context.Background(), endpoint)
-
- if test.hasError {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, test.addresses, addresses)
- })
- }
-}
-```
-
-Add a connection-time test whose resolver returns a public address during the
-first `Validate` call and `127.0.0.1` during `DialContext`. Assert that the
-recording dialer is never invoked. Implement the recording dialer as a local
-function injected through an unexported `dialValidated` helper so the test does
-not make a real network connection.
-
-Add an exact-host allowlist test:
-
-```go
-func TestNotificationEndpointPolicyAllowsPrivateAddressForExactLocalHost(t *testing.T) {
- endpoint, err := url.Parse("https://adapter-emulator:9091/notifications/gateway-1")
- require.NoError(t, err)
- policy := NewNotificationEndpointPolicy(&staticHostResolver{
- addresses: map[string][]netip.Addr{
- "adapter-emulator": {netip.MustParseAddr("172.20.0.8")},
- },
- }, []string{"adapter-emulator"})
-
- addresses, err := policy.Validate(context.Background(), endpoint)
-
- require.NoError(t, err)
- assert.Equal(t, []netip.Addr{netip.MustParseAddr("172.20.0.8")}, addresses)
-}
-```
-
-Also assert that `adapter-emulator.example.com`, a private IP-literal URL, and
-any non-allowlisted private hostname remain rejected.
-
-- [ ] **Step 2: Run the policy tests and confirm the types are missing**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestNotificationEndpointPolicy' -count=1
-```
-
-Expected: compilation fails because `NotificationEndpointPolicy` and its
-constructor do not exist.
-
-- [ ] **Step 3: Implement reserved-range checks**
-
-Create `api/pkg/services/notification_endpoint_policy.go`. Define the resolver
-interface above and these blocked prefixes:
-
-```go
-var blockedNotificationPrefixes = []netip.Prefix{
- netip.MustParsePrefix("0.0.0.0/8"),
- netip.MustParsePrefix("10.0.0.0/8"),
- netip.MustParsePrefix("100.64.0.0/10"),
- netip.MustParsePrefix("127.0.0.0/8"),
- netip.MustParsePrefix("169.254.0.0/16"),
- netip.MustParsePrefix("172.16.0.0/12"),
- netip.MustParsePrefix("192.0.0.0/24"),
- netip.MustParsePrefix("192.0.2.0/24"),
- netip.MustParsePrefix("192.168.0.0/16"),
- netip.MustParsePrefix("198.18.0.0/15"),
- netip.MustParsePrefix("198.51.100.0/24"),
- netip.MustParsePrefix("203.0.113.0/24"),
- netip.MustParsePrefix("224.0.0.0/4"),
- netip.MustParsePrefix("240.0.0.0/4"),
- netip.MustParsePrefix("::/128"),
- netip.MustParsePrefix("::1/128"),
- netip.MustParsePrefix("100::/64"),
- netip.MustParsePrefix("2001:db8::/32"),
- netip.MustParsePrefix("fc00::/7"),
- netip.MustParsePrefix("fe80::/10"),
- netip.MustParsePrefix("ff00::/8"),
-}
-```
-
-Implement:
-
-```go
-func isPublicNotificationAddress(address netip.Addr) bool {
- address = address.Unmap()
- if !address.IsValid() || !address.IsGlobalUnicast() {
- return false
- }
- for _, prefix := range blockedNotificationPrefixes {
- if prefix.Contains(address) {
- return false
- }
- }
- return true
-}
-```
-
-`Validate` must verify HTTPS, hostname presence, at least one DNS result, and
-every resolved address passing
-`isPublicNotificationAddress`. Private addresses are accepted only when the
-lowercased hostname exactly matches `allowedPrivateHosts`; never wildcard or
-suffix-match. Wrap resolver and validation errors with stacktrace context
-without including the raw URL.
-
-- [ ] **Step 4: Implement validated dialing**
-
-Add:
-
-```go
-func (policy *NotificationEndpointPolicy) DialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
- return func(ctx context.Context, network string, address string) (net.Conn, error) {
- host, port, err := net.SplitHostPort(address)
- if err != nil {
- return nil, stacktrace.Propagatef(err, "cannot split notification endpoint address")
- }
-
- endpoint := &url.URL{Scheme: "https", Host: net.JoinHostPort(host, port)}
- addresses, err := policy.Validate(ctx, endpoint)
- if err != nil {
- return nil, stacktrace.Propagatef(err, "notification endpoint is not public")
- }
-
- var lastErr error
- for _, resolved := range addresses {
- connection, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(resolved.String(), port))
- if dialErr == nil {
- return connection, nil
- }
- lastErr = dialErr
- }
- return nil, stacktrace.Propagatef(lastErr, "cannot connect to notification endpoint")
- }
-}
-```
-
-Factor the final connection loop through an unexported function variable or
-method that accepts a dial function, allowing the DNS-rebinding test to assert
-the selected address without opening a socket. Do not perform a normal second
-hostname dial after validation.
-
-- [ ] **Step 5: Run focused tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestNotificationEndpointPolicy' -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 6: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/services/notification_endpoint_policy.go pkg/services/notification_endpoint_policy_test.go
-git add pkg/services/notification_endpoint_policy.go pkg/services/notification_endpoint_policy_test.go
-git commit -m "feat(api): validate adapter endpoints"
-```
-
----
-
-### Task 3: Add the Transport Client Map
-
-**Files:**
-- Modify: `api/pkg/services/fcm_client.go`
-- Modify: `api/pkg/services/phone_notification_service.go`
-- Modify: `api/pkg/services/phone_notification_service_test.go`
-
-**Interfaces:**
-- Consumes:
-
-```go
-func (phone *entities.Phone) NotificationTransport() (entities.NotificationTransport, error)
-```
-
-- Produces:
-
-```go
-type FCMClient interface {
- Send(context.Context, *messaging.Message) (string, error)
-}
-```
-
-- [ ] **Step 1: Write failing transport-client map tests**
-
-Create a recording client in `phone_notification_service_test.go`:
-
-```go
-type recordingPhoneNotificationClient struct {
- message *messaging.Message
- result string
- err error
- calls int
-}
-
-func (client *recordingPhoneNotificationClient) Send(_ context.Context, message *messaging.Message) (string, error) {
- client.calls++
- client.message = message
- return client.result, client.err
-}
-```
-
-Add tests for map-based FCM and HTTP selection, the same message pointer,
-trimmed `message.Token`, absent transport entries, invalid tokens, and nil
-messages.
-
-- [ ] **Step 2: Run the service tests and confirm the map dependency is missing**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
-```
-
-Expected: compilation fails because the service does not accept the client map.
-
-- [ ] **Step 3: Implement focused map-based delivery**
-
-Store `map[entities.NotificationTransport]FCMClient` on
-`PhoneNotificationService` and implement:
-
-```go
-func (service *PhoneNotificationService) sendPhoneNotification(
- ctx context.Context,
- phone *entities.Phone,
- message *messaging.Message,
-) (string, entities.NotificationTransport, error) {
- // Validate message, classify once, map lookup, set trimmed token, send.
-}
-```
-
-Update comments in `api/pkg/services/fcm_client.go` to describe `FCMClient` as
-the common phone notification transport boundary. Use the Firebase client
-directly; do not add a wrapper or dispatcher.
-
-- [ ] **Step 4: Run focused tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 5: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/services/fcm_client.go pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go
-git add pkg/services/fcm_client.go pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go
-git commit -m "refactor(api): dispatch gateway notifications"
-```
-
----
-
-### Task 4: Implement the SSRF-safe HTTP Sender
-
-**Files:**
-- Create: `api/pkg/services/http_notification_sender.go`
-- Create: `api/pkg/services/http_notification_sender_test.go`
-
-**Interfaces:**
-- Consumes:
-
-```go
-type FCMClient interface {
- Send(context.Context, *messaging.Message) (string, error)
-}
-```
-
-- Produces:
-
-```go
-type HTTPNotificationSender struct {
- logger telemetry.Logger
- client *http.Client
- retrier *retry.Retrier
- timeout time.Duration
-}
-
-func NewHTTPNotificationSender(
- logger telemetry.Logger,
- client *http.Client,
-) *HTTPNotificationSender
-
-func (sender *HTTPNotificationSender) Send(
- ctx context.Context,
- message *messaging.Message,
-) (string, error)
-```
-
-- [ ] **Step 1: Write failing payload and success tests**
-
-Create `api/pkg/services/http_notification_sender_test.go`. Use a custom
-`roundTripFunc`:
-
-```go
-type roundTripFunc func(*http.Request) (*http.Response, error)
-
-func (roundTrip roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
- return roundTrip(request)
-}
-```
-
-Construct the sender in tests with a reusable retrier configured through
-`newHTTPNotificationRetrier(0)` so retry delay is zero from initialization.
-Use a public address from the test resolver, and a client whose custom
-RoundTripper does not dial.
-
-Assert the request:
-
-```go
-assert.Equal(t, http.MethodPost, request.Method)
-assert.Equal(t, "application/json", request.Header.Get("Content-Type"))
-```
-
-Keep a test-only decoding struct and assert this structure:
-
-```go
-type httpNotificationRequest struct {
- Message struct {
- Token string `json:"token"`
- Data map[string]string `json:"data"`
- Android struct {
- Priority string `json:"priority"`
- TTL string `json:"ttl,omitempty"`
- } `json:"android"`
- } `json:"message"`
-}
-```
-
-Return `204 No Content` and assert `Send` succeeds with result `http/success`.
-
-- [ ] **Step 2: Write failing retry-classification tests**
-
-Add table-driven tests for:
-
-- network error then `202`: two calls, success;
-- `408` then `200`: two calls, success;
-- `429` then `204`: two calls, success;
-- `500`, `502`, then `204`: three calls, success;
-- `400`: one call, error;
-- three `503` responses: three calls, error;
-- redirects use Go's default client behavior; do not add terminal redirect
- handling;
-- response with a body larger than the discard limit: success without reading
- unbounded content.
-
-Assert retry attempts receive fresh requests and bodies.
-
-- [ ] **Step 3: Run the HTTP sender tests and confirm the type is missing**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestHTTPNotificationSender' -count=1
-```
-
-Expected: compilation fails because `HTTPNotificationSender` does not exist.
-
-- [ ] **Step 4: Implement the FCM-compatible HTTP payload**
-
-Create `api/pkg/services/http_notification_sender.go` and build the one-use
-payload directly with `map[string]any`:
-
-```go
-payload := map[string]any{
- "message": message,
-}
-```
-
-Obtain the destination from `message.Token`. Rely on
-`messaging.Message.MarshalJSON` and `messaging.AndroidConfig.MarshalJSON` for
-the FCM shape and protobuf TTL, including `10*time.Minute` as `"600s"`. Build a
-new request body for every attempt so retries never reuse a consumed reader.
-Reject nil messages explicitly with a stacktrace-consistent error.
-
-- [ ] **Step 5: Implement bounded retries**
-
-Implement these helpers:
-
-```go
-func isRetryableNotificationStatus(statusCode int) bool {
- return statusCode == http.StatusRequestTimeout ||
- statusCode == http.StatusTooManyRequests ||
- statusCode >= http.StatusInternalServerError
-}
-
-func notificationRetryDelay(attempt uint) time.Duration {
- return time.Duration(1<<(attempt-1)) * 250 * time.Millisecond
-}
-```
-
-Create one reusable retrier during sender initialization:
-
-```go
-func newHTTPNotificationRetrier(delay time.Duration) *retry.Retrier {
- return retry.New(
- retry.Attempts(3),
- retry.Delay(delay),
- retry.DelayType(retry.BackOffDelay),
- retry.LastErrorOnly(true),
- retry.RetryIf(isRetryableNotificationError),
- )
-}
-```
-
-Do not configure this retrier with `retry.Context`: each `Send` has a different
-caller context. Instead, check caller cancellation in the operation and return
-a terminal error, while deriving each attempt's five-second context from that
-caller.
-
-Focused sender methods must:
-
-1. parse `message.Token`;
-2. marshal the request body once, then create a fresh reader per request;
-3. create a child context with the configured timeout per attempt;
-4. set `Content-Type`;
-5. call `client.Do`;
-6. close each response body after copying at most 4 KiB to `io.Discard`;
-7. return `http/success` for any `2xx`;
-8. retry only network errors, `408`, `429`, and `5xx` while attempts remain;
-9. treat request construction, caller cancellation, and other statuses as
- terminal;
-10. return a stacktrace-wrapped error.
-
-Do not use the container's retrying HTTP client; retries belong in this sender
-so status classification and attempt count are explicit.
-
-- [ ] **Step 6: Add heartbeat tests**
-
-Add a heartbeat payload test with `KEY_HEARTBEAT_ID`, high priority, and nil
-TTL; assert the `ttl` field is omitted.
-Use the standard HTTP telemetry and logging path without feature-specific URL
-redaction.
-
-- [ ] **Step 7: Run focused tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestHTTPNotificationSender' -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 8: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/services/http_notification_sender.go pkg/services/http_notification_sender_test.go
-git add pkg/services/http_notification_sender.go pkg/services/http_notification_sender_test.go
-git commit -m "feat(api): send notifications to adapters"
-```
-
----
-
-### Task 5: Integrate Message and Heartbeat Notifications
-
-**Files:**
-- Modify: `api/pkg/services/phone_notification_service.go`
-- Create: `api/pkg/services/phone_notification_service_test.go`
-- Modify: `api/pkg/entities/phone_notification.go`
-
-**Interfaces:**
-- Consumes:
-
-```go
-map[entities.NotificationTransport]FCMClient
-```
-
-- Produces:
-
-```go
-type NotificationEventDispatcher interface {
- Dispatch(ctx context.Context, event cloudevents.Event) error
- DispatchWithTimeout(ctx context.Context, event cloudevents.Event, timeout time.Duration) (string, error)
-}
-
-func NewNotificationService(
- logger telemetry.Logger,
- tracer telemetry.Tracer,
- phoneNotificationClients map[entities.NotificationTransport]FCMClient,
- phoneRepository repositories.PhoneRepository,
- phoneNotificationRepository repositories.PhoneNotificationRepository,
- messageSendScheduleRepository repositories.MessageSendScheduleRepository,
- dispatcher NotificationEventDispatcher,
-) *PhoneNotificationService
-```
-
-- [ ] **Step 1: Write failing service tests with hand-written fakes**
-
-Create `api/pkg/services/phone_notification_service_test.go`.
-
-Define repository fakes by embedding the interfaces and overriding only methods
-used by the tests:
-
-```go
-type phoneNotificationPhoneRepository struct {
- repositories.PhoneRepository
- phone *entities.Phone
- err error
-}
-
-func (repository *phoneNotificationPhoneRepository) LoadByID(
- _ context.Context,
- _ entities.UserID,
- _ uuid.UUID,
-) (*entities.Phone, error) {
- return repository.phone, repository.err
-}
-
-type phoneNotificationRepository struct {
- repositories.PhoneNotificationRepository
- notificationID uuid.UUID
- status entities.PhoneNotificationStatus
-}
-
-func (repository *phoneNotificationRepository) UpdateStatus(
- _ context.Context,
- notificationID uuid.UUID,
- status entities.PhoneNotificationStatus,
-) error {
- repository.notificationID = notificationID
- repository.status = status
- return nil
-}
-```
-
-Add a fake event dispatcher that records CloudEvents and returns no error. Add
-recording `FCMClient` implementations through a transport-keyed map.
-
-Test `Send` with an HTTPS token and assert:
-
-- `KEY_MESSAGE_ID` equals `params.MessageID.String()`;
-- priority is `normal`;
-- TTL equals `phone.MessageExpirationDuration()`;
-- the same `messaging.Message` pointer reaches the HTTP client;
-- a `message.notification.sent` event is dispatched;
-- phone-notification status becomes sent.
-
-Test an HTTP sender error and assert:
-
-- `message.notification.failed` is dispatched;
-- status becomes failed;
-- the payload error says the adapter endpoint could not be notified;
-- the payload does not tell the user to reinstall Android.
-
-Test an FCM sender error and assert the existing Android reinstallation guidance
-is preserved.
-
-Test `SendHeartbeatFCM` with an HTTPS token and assert:
-
-- `KEY_HEARTBEAT_ID` parses as RFC3339;
-- priority is `high`;
-- TTL is nil;
-- heartbeat sender errors are logged and return nil, preserving current
- heartbeat behavior.
-
-Also test explicit nil-message handling and a missing transport map entry.
-
-- [ ] **Step 2: Run the service tests and confirm the constructor mismatch**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestPhoneNotificationService' -count=1
-```
-
-Expected: compilation fails because `PhoneNotificationService` still consumes
-`FCMClient` and a concrete `*EventDispatcher`.
-
-- [ ] **Step 3: Replace direct Firebase message creation**
-
-In `api/pkg/services/phone_notification_service.go`:
-
-- construct Firebase `messaging.Message` values directly;
-- replace the direct client with
- `phoneNotificationClients map[entities.NotificationTransport]FCMClient`;
-- change the constructor to the exact signature above;
-- change `eventDispatcher` to `NotificationEventDispatcher`.
-
-Add a private `sendPhoneNotification` helper that validates the message,
-classifies transport once, performs a map lookup, sets the trimmed token, calls
-the selected client, and returns the transport with the result or error.
-
-For message notifications, construct:
-
-```go
-ttl := phone.MessageExpirationDuration()
-message := &messaging.Message{
- Data: map[string]string{
- "KEY_MESSAGE_ID": params.MessageID.String(),
- },
- Android: &messaging.AndroidConfig{
- Priority: "normal",
- TTL: &ttl,
- },
-}
-```
-
-For heartbeat notifications, call:
-
-```go
-message := &messaging.Message{
- Data: map[string]string{
- "KEY_HEARTBEAT_ID": time.Now().UTC().Format(time.RFC3339),
- },
- Android: &messaging.AndroidConfig{Priority: "high"},
-}
-```
-
-- [ ] **Step 4: Add transport-aware failure text**
-
-Use the transport returned by `sendPhoneNotification`; do not classify the
-phone a second time.
-
-For HTTP use:
-
-```go
-msg := fmt.Sprintf(
- "cannot notify the configured adapter for phone [%s]. Check the adapter URL and availability.",
- phone.PhoneNumber,
-)
-```
-
-For Firebase preserve:
-
-```go
-msg := fmt.Sprintf(
- "cannot send notification to your phone [%s]. Reinstall the httpSMS app on your Android phone.",
- phone.PhoneNumber,
-)
-```
-
-Log the technical wrapped error without logging the raw token. If transport
-classification unexpectedly fails here, send that error through
-`handleNotificationFailed` with a generic notification-configuration message.
-
-- [ ] **Step 5: Update transport-specific comments**
-
-In `api/pkg/entities/phone_notification.go`, change:
-
-```go
-// PhoneNotification represents an FCM notification to a mobile phone
-```
-
-to:
-
-```go
-// PhoneNotification represents a scheduled wake-up notification for a phone gateway.
-```
-
-Update `PhoneNotificationService` and `SendHeartbeatFCM` comments so they refer
-to phone gateway notifications rather than only mobile phones. Keep the method
-name `SendHeartbeatFCM` in this change to avoid an unrelated listener rename.
-
-- [ ] **Step 6: Run focused tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/services -run 'TestPhoneNotificationService|TestHTTPNotificationSender' -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 7: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go pkg/entities/phone_notification.go
-git add pkg/services/phone_notification_service.go pkg/services/phone_notification_service_test.go pkg/entities/phone_notification.go
-git commit -m "feat(api): route phone gateway wake-ups"
-```
-
----
-
-### Task 6: Validate URL Tokens, Wire Dependencies, and Regenerate Swagger
-
-**Files:**
-- Modify: `api/pkg/validators/phone_handler_validator.go`
-- Create: `api/pkg/validators/phone_handler_validator_test.go`
-- Modify: `api/pkg/di/container.go`
-- Modify: `api/pkg/requests/phone_update_request.go`
-- Modify: `api/pkg/requests/phone_fcm_token_request.go`
-- Modify: `api/docs/docs.go`
-- Modify: `api/docs/swagger.json`
-- Modify: `api/docs/swagger.yaml`
-
-**Interfaces:**
-- Consumes:
-
-```go
-func (container *Container) FCMClient() services.FCMClient
-func NewHTTPNotificationSender(logger telemetry.Logger, client *http.Client) *HTTPNotificationSender
-```
-
-- Produces container factories:
-
-```go
-func (container *Container) NotificationHTTPClient() *http.Client
-func (container *Container) PhoneNotificationClients() map[entities.NotificationTransport]services.FCMClient
-```
-
-- Produces validator constructor:
-
-```go
-func NewPhoneHandlerValidator(
- logger telemetry.Logger,
- tracer telemetry.Tracer,
- scheduleService *services.MessageSendScheduleService,
- endpointPolicy *services.NotificationEndpointPolicy,
-) *PhoneHandlerValidator
-```
-
-- [ ] **Step 1: Write failing validator tests**
-
-Create `api/pkg/validators/phone_handler_validator_test.go`.
-
-Build the validator with a static public resolver and nil schedule service for
-requests without a schedule ID. Add tests for both `ValidateUpsert` and
-`ValidateFCMToken`:
-
-```go
-func TestPhoneHandlerValidatorAcceptsPublicHTTPSNotificationURL(t *testing.T) {
- validator := newPhoneHandlerValidatorWithAddresses(map[string][]netip.Addr{
- "adapter.example.com": {netip.MustParseAddr("8.8.8.8")},
- })
-
- errors := validator.ValidateFCMToken(context.Background(), requests.PhoneFCMToken{
- PhoneNumber: "+18005550199",
- FcmToken: "https://adapter.example.com/notify",
- SIM: entities.SIM1.String(),
- })
-
- assert.Empty(t, errors)
-}
-```
-
-Add rejection tests for `http://`, loopback resolution, private resolution,
-mixed public/private resolution, and malformed HTTPS.
-Add an opaque FCM token test to prove the resolver is not required for Firebase
-tokens.
-
-- [ ] **Step 2: Run validator tests and confirm unsafe URLs are accepted**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/validators -run 'TestPhoneHandlerValidator.*Notification' -count=1
-```
-
-Expected: tests fail because the validator only checks token length.
-
-- [ ] **Step 3: Inject and apply endpoint policy**
-
-Add `endpointPolicy *services.NotificationEndpointPolicy` to
-`PhoneHandlerValidator` and its constructor.
-
-Add:
-
-```go
-func (validator *PhoneHandlerValidator) validateNotificationToken(
- ctx context.Context,
- token string,
- result url.Values,
-) {
- token = strings.TrimSpace(token)
- if token == "" {
- return
- }
-
- phone := &entities.Phone{FcmToken: &token}
- transport, err := phone.NotificationTransport()
- if err != nil {
- result.Add("fcm_token", err.Error())
- return
- }
- if transport != entities.NotificationTransportHTTP {
- return
- }
-
- endpoint, err := phone.NotificationURL()
- if err != nil {
- result.Add("fcm_token", err.Error())
- return
- }
- if _, err = validator.endpointPolicy.Validate(ctx, endpoint); err != nil {
- result.Add("fcm_token", "fcm_token must be a public HTTPS adapter URL")
- }
-}
-```
-
-Call it after structural validation succeeds in both `ValidateUpsert` and
-`ValidateFCMToken`. Change `ValidateFCMToken` to use its context argument.
-
-- [ ] **Step 4: Add SSRF-safe container factories**
-
-In `api/pkg/di/container.go`, add:
-
-```go
-func (container *Container) NotificationEndpointPolicy() *services.NotificationEndpointPolicy {
- if container.notificationEndpointPolicy != nil {
- return container.notificationEndpointPolicy
- }
-
- allowedPrivateHosts := []string{} // Superseded: no private-host allowlist is configured.
- container.notificationEndpointPolicy = services.NewNotificationEndpointPolicy(
- net.DefaultResolver,
- allowedPrivateHosts,
- )
- return container.notificationEndpointPolicy
-}
-```
-
-Add a client factory:
-
-```go
-func (container *Container) NotificationHTTPClient() *http.Client {
- policy := container.NotificationEndpointPolicy()
- transport := &http.Transport{
- Proxy: nil,
- DialContext: policy.DialContext(&net.Dialer{
- Timeout: 5 * time.Second,
- KeepAlive: 30 * time.Second,
- }),
- ForceAttemptHTTP2: true,
- TLSClientConfig: &tls.Config{
- MinVersion: tls.VersionTLS12,
- },
- }
-
- return &http.Client{
- Transport: otelroundtripper.New(
- otelroundtripper.WithName("phone_notification_http"),
- otelroundtripper.WithParent(transport),
- otelroundtripper.WithMeter(otel.GetMeterProvider().Meter(container.projectID)),
- ),
- }
-}
-```
-
-Do not set a client-wide timeout; the sender creates the approved five-second
-context for each attempt.
-
-Add:
-
-```go
-func (container *Container) PhoneNotificationClients() map[entities.NotificationTransport]services.FCMClient {
- return map[entities.NotificationTransport]services.FCMClient{
- entities.NotificationTransportFCM: container.FCMClient(),
- entities.NotificationTransportHTTP: services.NewHTTPNotificationSender(
- container.Logger(),
- container.NotificationHTTPClient(),
- ),
- }
-}
-```
-
-Add this field to `Container`:
-
-```go
-notificationEndpointPolicy *services.NotificationEndpointPolicy
-```
-
-The cached policy ensures validation and connection-time checks use the same
-allowlist. Do not cache per-request sender state.
-
-- [ ] **Step 5: Wire service and validator constructors**
-
-Change `container.NotificationService()` to pass
-`container.PhoneNotificationClients()`.
-
-Find `container.PhoneHandlerValidator()` and pass
-`container.NotificationEndpointPolicy()` as its fourth argument. Keep existing
-logger, tracer, and schedule-service arguments unchanged.
-
-- [ ] **Step 6: Run focused package tests**
-
-Run:
-
-```bash
-cd api
-go test ./pkg/entities ./pkg/services ./pkg/validators ./pkg/di -count=1
-```
-
-Expected: PASS.
-
-- [ ] **Step 7: Update API descriptions**
-
-In both phone request structs, replace the generic FCM token comment with:
-
-```go
-// FcmToken is either a Firebase registration token or a public HTTPS adapter callback URL.
-FcmToken string `json:"fcm_token" example:"https://adapter.example.com/notifications"`
-```
-
-Update handler Swagger descriptions for phone upsert and FCM-token upsert to
-state that URL-backed phones receive FCM-compatible HTTP wake-ups. Do not add or
-rename routes or JSON fields.
-
-- [ ] **Step 8: Regenerate Swagger**
-
-Run:
-
-```bash
-cd api
-swag init --requiredByDefault --parseDependency --parseInternal
-```
-
-Expected: `docs/docs.go`, `docs/swagger.json`, and `docs/swagger.yaml` update
-with the dual-purpose `fcm_token` descriptions.
-
-- [ ] **Step 9: Run the complete API test suite**
-
-Run:
-
-```bash
-cd api
-go test ./...
-```
-
-Expected: PASS.
-
-- [ ] **Step 10: Build the API**
-
-Run:
-
-```bash
-cd api
-go build -o ./tmp/main.exe .
-```
-
-Expected: build succeeds.
-
-- [ ] **Step 11: Inspect the final diff for forbidden changes**
-
-Run:
-
-```bash
-git diff --check
-git diff --stat
-git grep -n "fcm_token" -- api/pkg/entities/phone.go api/pkg/requests/phone_update_request.go api/pkg/requests/phone_fcm_token_request.go
-```
-
-Confirm:
-
-- no database field or migration was added;
-- `fcm_token` remains the persisted/API field;
-- no message content or API key is added to the HTTP callback payload;
-- scheduling and repository code are unchanged;
-
-- [ ] **Step 12: Format and commit**
-
-Run:
-
-```bash
-cd api
-go-fumpt -w pkg/validators/phone_handler_validator.go pkg/validators/phone_handler_validator_test.go pkg/di/container.go pkg/requests/phone_update_request.go pkg/requests/phone_fcm_token_request.go
-git add pkg/validators/phone_handler_validator.go pkg/validators/phone_handler_validator_test.go pkg/di/container.go pkg/requests/phone_update_request.go pkg/requests/phone_fcm_token_request.go pkg/handlers/phone_handler.go docs/docs.go docs/swagger.json docs/swagger.yaml
-git commit -m "feat(api): enable URL-backed phone gateways"
-```
-
----
-
-### Task 7: Build the Adapter Emulator and End-to-End Scenarios
-
-**Files:**
-- Create: `tests/adapter-emulator/Dockerfile`
-- Create: `tests/adapter-emulator/go.mod`
-- Create: `tests/adapter-emulator/main.go`
-- Create: `tests/adapter-emulator/emulator.go`
-- Create: `tests/adapter-emulator/api_client.go`
-- Create: `tests/adapter-emulator/notification_handler.go`
-- Create: `tests/adapter-emulator/control_handler.go`
-- Create: `tests/adapter_integration_test.go`
-- Create: `tests/generate-adapter-certificates.sh`
-- Modify: `tests/docker-compose.yml`
-- Modify: `tests/.env.test`
-- Modify: `tests/helpers_test.go`
-- Modify: `tests/README.md`
-- Modify: `.github/workflows/api.yml`
-- Modify: `.gitignore`
-
-**Interfaces:**
-- Emulator callback: `POST https://adapter-emulator:9091/notifications/{gatewayID}`
-- Emulator control:
-
-```text
-PUT http://localhost:9092/test/gateways/{gatewayID}
-POST http://localhost:9092/test/gateways/{gatewayID}/incoming
-GET http://localhost:9092/test/gateways/{gatewayID}/notifications
-GET http://localhost:9092/health
-```
-
-- Gateway registration:
-
-```go
-type gatewayRegistration struct {
- PhoneNumber string `json:"phone_number"`
- PhoneAPIKey string `json:"phone_api_key"`
-}
-```
-
-- Incoming control payload:
-
-```go
-type incomingMessageRequest struct {
- Contact string `json:"contact"`
- Content string `json:"content"`
- Encrypted bool `json:"encrypted"`
-}
-```
-
-- Callback record:
-
-```go
-type notificationRecord struct {
- GatewayID string `json:"gateway_id"`
- Data map[string]string `json:"data"`
- MessageID string `json:"message_id,omitempty"`
- Kind string `json:"kind"`
- Processed bool `json:"processed"`
- Error string `json:"error,omitempty"`
-}
-```
-
-- [ ] **Step 1: Generate throwaway TLS material**
-
-Create `tests/generate-adapter-certificates.sh`:
-
-```bash
-#!/usr/bin/env bash
-set -euo pipefail
-
-output_dir="${1:-certs}"
-mkdir -p "$output_dir"
-
-openssl req -x509 -newkey rsa:2048 -nodes \
- -keyout "$output_dir/ca-key.pem" \
- -out "$output_dir/ca.pem" \
- -days 2 \
- -subj "/CN=httpSMS integration adapter CA"
-
-openssl req -newkey rsa:2048 -nodes \
- -keyout "$output_dir/server-key.pem" \
- -out "$output_dir/server.csr" \
- -subj "/CN=adapter-emulator"
-
-cat >"$output_dir/server.ext" <<'EOF'
-subjectAltName=DNS:adapter-emulator
-extendedKeyUsage=serverAuth
-EOF
-
-openssl x509 -req \
- -in "$output_dir/server.csr" \
- -CA "$output_dir/ca.pem" \
- -CAkey "$output_dir/ca-key.pem" \
- -CAcreateserial \
- -out "$output_dir/server.pem" \
- -days 2 \
- -extfile "$output_dir/server.ext"
-```
-
-Add `tests/certs/` to `.gitignore`. Do not commit generated keys or
-certificates.
-
-- [ ] **Step 2: Scaffold the isolated emulator module**
-
-Create `tests/adapter-emulator/go.mod`:
-
-```go
-module github.com/NdoleStudio/httpsms/tests/adapter-emulator
-
-go 1.25.0
-```
-
-Use only the standard library. Run:
-
-```bash
-cd tests/adapter-emulator
-go mod tidy
-```
-
-Expected: the command succeeds without adding dependencies.
-
-- [ ] **Step 3: Implement emulator state and deduplication**
-
-In `tests/adapter-emulator/emulator.go`, implement:
-
-```go
-type gateway struct {
- PhoneNumber string
- PhoneAPIKey string
-}
-
-type emulator struct {
- apiBaseURL string
- client *http.Client
- mu sync.RWMutex
- gateways map[string]gateway
- records []*notificationRecord
-}
-
-func newEmulator(apiBaseURL string, client *http.Client) *emulator {
- return &emulator{
- apiBaseURL: strings.TrimRight(apiBaseURL, "/"),
- client: client,
- gateways: make(map[string]gateway),
- }
-}
-```
-
-Add locked methods to register a gateway, load a gateway, record each callback,
-mark it processed/failed, and list copied records for one gateway.
-
-- [ ] **Step 4: Implement existing phone API calls**
-
-In `tests/adapter-emulator/api_client.go`, implement:
-
-```go
-func (emulator *emulator) fetchOutstanding(
- ctx context.Context,
- gateway gateway,
- messageID string,
-) (map[string]any, error)
-
-func (emulator *emulator) fireMessageEvent(
- ctx context.Context,
- gateway gateway,
- messageID string,
- eventName string,
-) error
-
-func (emulator *emulator) receiveMessage(
- ctx context.Context,
- gateway gateway,
- request incomingMessageRequest,
-) (map[string]any, error)
-
-func (emulator *emulator) storeHeartbeat(
- ctx context.Context,
- gateway gateway,
-) error
-```
-
-Use the existing routes:
-
-```text
-GET /v1/messages/outstanding?message_id={messageID}
-POST /v1/messages/{messageID}/events
-POST /v1/messages/receive
-POST /v1/heartbeats
-```
-
-Every request sets `x-api-key`. Message events use `SENT` then `DELIVERED` with
-UTC RFC3339 timestamps. Incoming messages use the gateway phone number as `to`,
-the control request contact as `from`, `SIM1`, and the requested encryption
-flag. Return contextual `fmt.Errorf` errors because the emulator module
-intentionally does not depend on the production API module.
-
-- [ ] **Step 5: Implement FCM-compatible callback handling**
-
-In `tests/adapter-emulator/notification_handler.go`, decode:
-
-```go
-type callbackEnvelope struct {
- Message struct {
- Token string `json:"token"`
- Data map[string]string `json:"data"`
- } `json:"message"`
-}
-```
-
-The handler must:
-
-1. load `gatewayID` from the route;
-2. return `404` for unknown gateways;
-3. return `400` for unsupported data;
-4. record every callback;
-5. for `KEY_MESSAGE_ID`, fetch outstanding, fire `SENT`, fire `DELIVERED`, and
- mark the record processed with kind `message`;
-6. for `KEY_HEARTBEAT_ID`, store a heartbeat and mark the record processed with
- kind `heartbeat`;
-7. return `500` and retain the error string in the record when processing
- fails, allowing the API sender's retry behavior to be exercised.
-
-Processing may be synchronous because the API accepts any `2xx` and the
-integration stack controls response time.
-
-- [ ] **Step 6: Implement control and server endpoints**
-
-In `tests/adapter-emulator/control_handler.go`, implement registration,
-incoming-message, record listing, and health handlers using `http.ServeMux`.
-
-In `main.go`, read:
-
-```text
-API_BASE_URL=http://api:8000
-ADAPTER_TLS_CERT=/certs/server.pem
-ADAPTER_TLS_KEY=/certs/server-key.pem
-```
-
-Start:
-
-- HTTPS callback server on `:9091`;
-- HTTP control server on `:9092`.
-
-Use `http.Server` with finite `ReadHeaderTimeout`, `ReadTimeout`,
-`WriteTimeout`, and `IdleTimeout`. Shut both servers down on SIGINT/SIGTERM.
-
-- [ ] **Step 7: Add the emulator container**
-
-Create `tests/adapter-emulator/Dockerfile`:
-
-```dockerfile
-FROM golang:1.25-alpine AS build
-WORKDIR /src
-COPY go.mod ./
-RUN go mod download
-COPY . .
-RUN CGO_ENABLED=0 go build -o /out/adapter-emulator .
-
-FROM alpine:3.22
-RUN adduser -D -u 10001 app
-USER app
-COPY --from=build /out/adapter-emulator /usr/local/bin/adapter-emulator
-ENTRYPOINT ["adapter-emulator"]
-```
-
-In `tests/docker-compose.yml`, add `adapter-emulator`:
-
-```yaml
-adapter-emulator:
- build:
- context: ./adapter-emulator
- ports:
- - "9092:9092"
- environment:
- API_BASE_URL: http://api:8000
- ADAPTER_TLS_CERT: /certs/server.pem
- ADAPTER_TLS_KEY: /certs/server-key.pem
- volumes:
- - ./certs:/certs:ro
- healthcheck:
- test: ["CMD", "wget", "-qO-", "http://localhost:9092/health"]
- interval: 5s
- timeout: 5s
- retries: 10
-```
-
-Make `api` depend on the healthy emulator. Mount `./certs/ca.pem` into the API
-container and set:
-
-```yaml
-SSL_CERT_FILE: /adapter-certs/ca.pem
-```
-
-Do not add a private-host allowlist; Docker DNS is used through the standard Go
-HTTP transport. Keep `SSL_CERT_FILE` so the emulator certificate remains
-trusted.
-
-- [ ] **Step 8: Add adapter test helpers**
-
-In `tests/helpers_test.go`, add `adapterControlURL`,
-`systemAPIKey`, and:
-
-```go
-type adapterTestPhone struct {
- testPhone
- PhoneID string
- GatewayID string
-}
-
-func setupAdapterPhone(ctx context.Context, t *testing.T, messagesPerMinute uint) adapterTestPhone
-func dispatchInternalEvent(ctx context.Context, t *testing.T, event map[string]any)
-func waitForAdapterMessageRecords(t *testing.T, gatewayID string, messageID string, timeout time.Duration) []notificationRecord
-func waitForAdapterHeartbeatRecord(t *testing.T, gatewayID string, timeout time.Duration) notificationRecord
-func triggerAdapterIncoming(ctx context.Context, t *testing.T, phone adapterTestPhone, contact string, content string) string
-```
-
-`setupAdapterPhone` must:
-
-1. generate a gateway UUID and phone number;
-2. create a phone API key;
-3. register the gateway with the emulator control API;
-4. use callback URL
- `https://adapter-emulator:9091/notifications/{gatewayID}`;
-5. upsert the phone through the user API and capture its phone ID;
-6. bind the same callback through the phone API-key route;
-7. wait for phone authorization using the existing helper.
-
-`dispatchInternalEvent` posts a valid CloudEvent JSON body to `/v1/events` with
-`x-api-key: system-user-api-key`.
-
-- [ ] **Step 9: Write the outgoing adapter integration test**
-
-Create `tests/adapter_integration_test.go`:
-
-```go
-func TestAdapterGatewayOutgoingMessage(t *testing.T) {
- ctx := context.Background()
- phone := setupAdapterPhone(ctx, t, 60)
- contact := randomPhoneNumber()
- content := "Adapter outgoing " + randomEncryptionKey()
-
- response, httpResponse, err := newAPIClient().Messages.Send(ctx, &httpsms.MessageSendParams{
- From: phone.PhoneNumber,
- To: contact,
- Content: content,
- })
- require.NoError(t, err)
- require.Equal(t, http.StatusOK, httpResponse.HTTPResponse.StatusCode)
-
- messageID := response.Data.ID.String()
- message := pollMessageStatus(ctx, t, messageID, "delivered", 30*time.Second)
-
- assert.Equal(t, phone.PhoneNumber, message.Owner)
- assert.Equal(t, contact, message.Contact)
- assert.Equal(t, content, message.Content)
- records := waitForAdapterMessageRecords(t, phone.GatewayID, messageID, 30*time.Second)
- require.Len(t, records, 1)
- assert.Equal(t, "message", records[0].Kind)
- assert.True(t, records[0].Processed)
- assert.Equal(t, messageID, records[0].Data["KEY_MESSAGE_ID"])
-}
-```
-
-The record-list helper queries by message ID. Assert one processed record for
-the successful callback flow.
-
-- [ ] **Step 10: Write the incoming adapter integration test**
-
-Add:
-
-```go
-func TestAdapterGatewayIncomingMessage(t *testing.T) {
- ctx := context.Background()
- phone := setupAdapterPhone(ctx, t, 60)
- contact := randomPhoneNumber()
- content := "Adapter incoming " + randomEncryptionKey()
-
- messageID := triggerAdapterIncoming(ctx, t, phone, contact, content)
- message := pollMessageStatus(ctx, t, messageID, "received", 15*time.Second)
-
- assert.Equal(t, phone.PhoneNumber, message.Owner)
- assert.Equal(t, contact, message.Contact)
- assert.Equal(t, content, message.Content)
- assert.Equal(t, "received", message.Status)
-}
-```
-
-This test must call the emulator control endpoint; it must not post
-`/v1/messages/receive` directly from the test runner.
-
-- [ ] **Step 11: Write the heartbeat callback integration test**
-
-Add:
-
-```go
-func TestAdapterGatewayHeartbeatWakeUp(t *testing.T) {
- ctx := context.Background()
- phone := setupAdapterPhone(ctx, t, 60)
- monitorID := uuid.NewString()
-
- dispatchInternalEvent(ctx, t, map[string]any{
- "specversion": "1.0",
- "id": uuid.NewString(),
- "source": "/tests/adapter-emulator",
- "type": "phone.heartbeat.missed",
- "time": time.Now().UTC().Format(time.RFC3339),
- "datacontenttype": "application/json",
- "data": map[string]any{
- "phone_id": phone.PhoneID,
- "user_id": "test-user-id",
- "last_heartbeat_timestamp": time.Now().UTC().Add(-20 * time.Minute).Format(time.RFC3339),
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- "monitor_id": monitorID,
- "owner": phone.PhoneNumber,
- },
- })
-
- record := waitForAdapterHeartbeatRecord(t, phone.GatewayID, 30*time.Second)
- assert.Equal(t, "heartbeat", record.Kind)
- assert.NotEmpty(t, record.Data["KEY_HEARTBEAT_ID"])
-
- heartbeats, response, err := newAPIClient().Heartbeats.Index(ctx, &httpsms.HeartbeatIndexParams{
- Owner: phone.PhoneNumber,
- Limit: 1,
- })
- require.NoError(t, err)
- require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
- require.NotEmpty(t, heartbeats.Data)
- assert.Equal(t, phone.PhoneNumber, heartbeats.Data[0].Owner)
-}
-```
-
-The direct internal event avoids waiting for the production 16-minute monitor
-interval while still exercising `PhoneNotificationListener`,
-`PhoneNotificationService`, the HTTP client, the emulator callback, and the
-existing heartbeat API.
-
-- [ ] **Step 12: Update local and CI commands**
-
-In `.github/workflows/api.yml`, after Firebase credential generation, add:
-
-```yaml
-- name: Generate adapter certificates
- run: bash tests/generate-adapter-certificates.sh tests/certs
-```
-
-Update `tests/README.md` architecture, project tree, coverage checklist,
-troubleshooting logs, and setup commands. The documented local sequence must
-run both credential scripts before `docker compose up`.
-
-- [ ] **Step 13: Run the complete integration suite**
-
-Run:
-
-```bash
-cd tests
-bash generate-firebase-credentials.sh firebase-credentials.json
-bash generate-adapter-certificates.sh certs
-docker compose up -d --build --wait
-docker compose wait seed
-go test -v -timeout 300s ./...
-docker compose down -v
-```
-
-Expected: existing FCM/WireMock tests and all three adapter tests pass.
-
-- [ ] **Step 14: Inspect failure logs if a scenario times out**
-
-Run:
-
-```bash
-cd tests
-docker compose logs --tail 200 api adapter-emulator
-```
-
-The outgoing logs must show callback receipt, outstanding fetch, `SENT`, and
-`DELIVERED`. Incoming logs must show the emulator calling the receive route.
-Heartbeat logs must show `KEY_HEARTBEAT_ID` and a successful heartbeat POST.
-
-- [ ] **Step 15: Commit integration coverage**
-
-Run:
-
-```bash
-git add .gitignore .github/workflows/api.yml tests
-git commit -m "test(api): cover URL-backed phone gateways"
-```
diff --git a/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md b/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md
deleted file mode 100644
index bc982b575..000000000
--- a/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md
+++ /dev/null
@@ -1,385 +0,0 @@
-# httpSMS Frontend Migration: Nuxt 2 + Vuetify 2 → Nuxt 4 + Vuetify 4
-
-## Summary
-
-Migrate the `web/` frontend from Nuxt 2 (Vue 2, Vuetify 2, Vuex, class-based components) to Nuxt 4 (Vue 3, Vuetify 4, Pinia, `
-```
-
-**After (Vue 3):**
-```vue
-
-```
-
-#### Vuetify Breakpoints: `$vuetify.breakpoint` → `useDisplay()`
-
-**Before:**
-```vue
-
-```
-
-**After:**
-```vue
-
-
-
-
-```
-
-#### State: Vuex → Pinia
-
-**Before:**
-```ts
-this.$store.dispatch('loadPhones', true)
-this.$store.getters.getAuthUser
-```
-
-**After:**
-```ts
-const phonesStore = usePhonesStore()
-await phonesStore.loadPhones(true)
-phonesStore.authUser
-```
-
-#### Firebase: `this.$fire.auth` → VueFire composables
-
-**Before:**
-```ts
-await this.$fire.auth.currentUser?.getIdToken()
-```
-
-**After:**
-```ts
-import { useCurrentUser } from 'vuefire'
-const user = useCurrentUser()
-const token = await user.value?.getIdToken()
-```
-
-#### Dynamic Routes: `_id` → `[id]`
-
-- `pages/threads/_id/index.vue` → `pages/threads/[id]/index.vue`
-- `pages/heartbeats/_id.vue` → `pages/heartbeats/[id].vue`
-
-### Vuetify 4 Breaking Changes to Address
-
-Using the Vuetify MCP for each component, the key changes are:
-
-1. **CSS Layers** — mandatory in v4; adjust any custom style overrides
-2. **Theme** — default is now "system" (we want dark, configure explicitly)
-3. **Typography** — MD2 → MD3 type scale (text-h1 → text-display-large, etc.)
-4. **Breakpoints** — reduced default sizes (restore v3 values via config)
-5. **Elevation** — 25 levels → 6 levels (MD3)
-6. **VBtn** — no default uppercase, grid → flex layout
-7. **VSnackbar** — removed multi-line prop
-8. **VSelect** — "item" slot → "internalItem"
-9. **Grid** — v-row/v-col overhauled
-10. **CSS Reset** — mostly removed, add selective resets
-
-### Vuetify MCP Usage Per Component
-
-For EVERY component/page being migrated, the implementation must:
-1. Call `vuetify-mcp-get_component_api_by_version` for each Vuetify component used
-2. Call `vuetify-mcp-get_v4_breaking_changes` filtered by relevant category
-3. Apply the correct v4 API (props, slots, events) based on MCP output
-4. Verify no deprecated props/events remain
-
-### Pinia Store Design
-
-Split the monolithic Vuex store into domain stores:
-
-| Store | Responsibility |
-|-------|---------------|
-| `auth.ts` | Firebase auth state, user profile, onAuthStateChanged |
-| `messages.ts` | Messages CRUD, search |
-| `threads.ts` | Message threads, current thread |
-| `phones.ts` | Phone list, heartbeats, polling |
-| `billing.ts` | Usage, subscription, payments |
-| `notifications.ts` | Toast/snackbar queue |
-| `app.ts` | App metadata, polling state, runtime config |
-
-### Plugin Migrations
-
-| Old Plugin | New Approach |
-|-----------|-------------|
-| `plugins/axios.ts` | `composables/useApi.ts` using `$fetch` with auth header |
-| `plugins/filters.ts` | `utils/filters.ts` (import explicitly or app.config globalProperties) |
-| `plugins/vue-glow.ts` | `plugins/vue-glow.client.ts` (client-only plugin) |
-| `plugins/chart.ts` | `plugins/chart.client.ts` (client-only plugin) |
-| `plugins/errors.ts` | `utils/errors.ts` |
-| `plugins/bag.ts` | `utils/bag.ts` |
-| `plugins/capitalize.ts` | `utils/capitalize.ts` |
-| `plugins/veutify.ts` | `plugins/vuetify.ts` (createVuetify setup) |
-
-## Migration Order (Tasks)
-
-### Phase 1: Scaffold & Configuration
-1. Initialize fresh Nuxt 4 project in `web/` (backup old code)
-2. Install dependencies (vuetify, pinia, nuxt-vuefire, sass, @mdi/js, pusher-js, etc.)
-3. Configure `nuxt.config.ts` (SSG, runtime config, modules)
-4. Set up Vuetify plugin with dark theme, restored breakpoints, MDI SVG icons
-5. Set up nuxt-vuefire with Firebase config
-6. Configure TypeScript strictly
-
-### Phase 2: Foundation
-7. Port `shared/types/` (API models — mostly copy)
-8. Port `utils/` (errors, filters, bag, capitalize)
-9. Create `composables/useApi.ts` (replace Axios plugin)
-10. Create `composables/useAuth.ts` (Firebase auth helpers)
-
-### Phase 3: State Management
-11. Create Pinia store: `stores/auth.ts`
-12. Create Pinia store: `stores/notifications.ts`
-13. Create Pinia store: `stores/app.ts`
-14. Create Pinia store: `stores/phones.ts`
-15. Create Pinia store: `stores/messages.ts`
-16. Create Pinia store: `stores/threads.ts`
-17. Create Pinia store: `stores/billing.ts`
-
-### Phase 4: Layouts & Middleware
-18. Port `middleware/auth.ts`
-19. Port `middleware/guest.ts`
-20. Port `layouts/default.vue` (with Vuetify MCP)
-21. Port `layouts/website.vue` (with Vuetify MCP)
-22. Port `layouts/error.vue` (with Vuetify MCP)
-23. Create `app.vue`
-
-### Phase 5: Components (use Vuetify MCP for each)
-24. Port `components/Toast.vue`
-25. Port `components/LoadingDashboard.vue`
-26. Port `components/LoadingButton.vue`
-27. Port `components/BackButton.vue`
-28. Port `components/CopyButton.vue`
-29. Port `components/FixedHeader.vue`
-30. Port `components/BlogAuthorBio.vue`
-31. Port `components/BlogInfo.vue`
-32. Port `components/NuxtLogo.vue`
-33. Port `components/FirebaseAuth.vue`
-34. Port `components/MessageThread.vue`
-35. Port `components/MessageThreadHeader.vue`
-
-### Phase 6: Pages (use Vuetify MCP for each)
-36. Port `pages/index.vue` (homepage)
-37. Port `pages/login.vue`
-38. Port `pages/threads/index.vue`
-39. Port `pages/threads/[id]/index.vue`
-40. Port `pages/messages/index.vue`
-41. Port `pages/search-messages/index.vue`
-42. Port `pages/bulk-messages/index.vue`
-43. Port `pages/settings/index.vue`
-44. Port `pages/billing/index.vue`
-45. Port `pages/heartbeats/[id].vue`
-46. Port `pages/phone-api-keys/index.vue`
-47. Port `pages/privacy-policy/index.vue`
-48. Port `pages/terms-and-conditions/index.vue`
-49. Port `pages/blog/index.vue`
-50. Port `pages/blog/how-to-send-sms-messages-from-excel.vue`
-51. Port `pages/blog/grant-send-and-read-sms-permissions-on-android.vue`
-52. Port `pages/blog/forward-incoming-sms-from-phone-to-webhook.vue`
-53. Port `pages/blog/end-to-end-encryption-to-sms-messages.vue`
-54. Port `pages/blog/send-bulk-sms-from-csv-file-with-no-code.vue`
-55. Port `pages/blog/send-sms-from-android-phone-with-python.vue`
-56. Port `pages/blog/send-sms-when-new-row-is-added-to-google-sheets-using-zapier.vue`
-
-### Phase 7: Final Setup
-57. Port static assets (`public/`)
-58. Port environment files (`.env`, `.env.production`)
-59. Update Dockerfile and nginx.conf
-60. Update sitemap configuration
-61. Configure highlight.js (nuxt-highlightjs or manual)
-
-### Phase 8: Verification (EVERY component and page)
-62. Verify `app.vue` renders
-63. Verify `layouts/default.vue` renders correctly
-64. Verify `layouts/website.vue` renders correctly
-65. Verify `layouts/error.vue` renders correctly
-66. Verify `components/Toast.vue` renders correctly
-67. Verify `components/LoadingDashboard.vue` renders correctly
-68. Verify `components/LoadingButton.vue` renders correctly
-69. Verify `components/BackButton.vue` renders correctly
-70. Verify `components/CopyButton.vue` renders correctly
-71. Verify `components/FixedHeader.vue` renders correctly
-72. Verify `components/BlogAuthorBio.vue` renders correctly
-73. Verify `components/BlogInfo.vue` renders correctly
-74. Verify `components/NuxtLogo.vue` renders correctly
-75. Verify `components/FirebaseAuth.vue` renders correctly
-76. Verify `components/MessageThread.vue` renders correctly
-77. Verify `components/MessageThreadHeader.vue` renders correctly
-78. Verify `pages/index.vue` renders correctly
-79. Verify `pages/login.vue` renders correctly
-80. Verify `pages/threads/index.vue` renders correctly
-81. Verify `pages/threads/[id]/index.vue` renders correctly
-82. Verify `pages/messages/index.vue` renders correctly
-83. Verify `pages/search-messages/index.vue` renders correctly
-84. Verify `pages/bulk-messages/index.vue` renders correctly
-85. Verify `pages/settings/index.vue` renders correctly
-86. Verify `pages/billing/index.vue` renders correctly
-87. Verify `pages/heartbeats/[id].vue` renders correctly
-88. Verify `pages/phone-api-keys/index.vue` renders correctly
-89. Verify `pages/privacy-policy/index.vue` renders correctly
-90. Verify `pages/terms-and-conditions/index.vue` renders correctly
-91. Verify `pages/blog/index.vue` renders correctly
-92. Verify all blog subpages render correctly
-93. Run `pnpm build` (static generation) successfully
-94. Verify no TypeScript errors (`pnpm typecheck`)
-95. Verify lint passes (`pnpm lint`)
-
-## Verification Strategy
-
-Each verification task in Phase 8 means:
-1. Start the dev server (`pnpm dev`)
-2. Navigate to the page/route in question
-3. Confirm no console errors, no hydration mismatches
-4. Confirm visual layout matches intent (Vuetify components render, dark theme active, responsive breakpoints work)
-5. For interactive components (forms, modals, auth), confirm basic interactions work
-
-The build verification (`pnpm build`) confirms all pages can be statically generated without errors.
-
-## Risk Mitigations
-
-- **Backup old code**: Keep old `web/` contents in a branch before starting
-- **Incremental porting**: Each file is ported and verified before moving to the next
-- **Vuetify MCP**: Use for every Vuetify component to catch breaking changes
-- **Restored breakpoints**: Keep v2/v3 breakpoint values to minimize layout drift
-- **CSS Reset compatibility**: Add selective reset CSS to maintain existing spacing behavior
diff --git a/docs/superpowers/specs/2026-06-24-login-last-used-badge-design.md b/docs/superpowers/specs/2026-06-24-login-last-used-badge-design.md
deleted file mode 100644
index f264a926c..000000000
--- a/docs/superpowers/specs/2026-06-24-login-last-used-badge-design.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Login "Last Used" Badge — Design
-
-## Problem
-
-The login page (`web/app/pages/login.vue` → `web/app/components/FirebaseAuth.vue`)
-offers three authentication methods: Continue with Google, Continue with GitHub,
-and Continue with email. Returning users don't get any hint about which method
-they used last, so they may pick the wrong provider and end up creating a second
-account or failing to sign in.
-
-## Goal
-
-Show a small "Last Used" badge on the button corresponding to the login method
-the user most recently used successfully on this device, so they can quickly pick
-the right one next time.
-
-## Scope
-
-Single component change: `web/app/components/FirebaseAuth.vue`. No API, store, or
-backend changes.
-
-## Approach
-
-### Storage
-
-- Persist the last successful method in `localStorage` under the key
- `httpsms_last_login_method`.
-- Value is one of `'google' | 'github' | 'email'`.
-- The value is written only **after a successful login** (inside `onSuccess`),
- never on click/attempt.
-
-### Recording the method
-
-- `onSuccess(user)` is currently shared by all three flows. Extend it to
- `onSuccess(user, method)` where `method` is `'google' | 'github' | 'email'`.
- - `signInWithGoogle` → `onSuccess(result.user, 'google')`
- - `signInWithGithub` → `onSuccess(result.user, 'github')`
- - `submitEmail` → `onSuccess(result.user, 'email')`
-- Inside `onSuccess`, write the method to `localStorage` before redirecting.
-
-### Reading the method
-
-- A reactive `lastUsedMethod = ref(null)`.
-- Populated in `onMounted` from `localStorage` (client-only, SSR-safe; the
- component is already rendered inside `` on the login page).
-
-### Display
-
-- Each of the three method buttons gets `class="position-relative"`.
-- A floating `v-chip` is rendered in the top-right corner of the matching button:
- - Vuetify `v-chip` with `label`, `size="x-small"`, `color="primary"`.
- - `class="position-absolute"` pinned to the top-right corner (slightly
- overlapping), text `Last Used`.
- - Shown via `v-if="lastUsedMethod === 'google'"` (and `'github'`, `'email'`
- respectively).
-
-### Edge cases
-
-- The email button is hidden once the inline email form opens
- (`v-if="!showEmailForm"`), so its badge hides with it automatically. No extra
- handling needed.
-- An unknown/empty stored value shows no badge anywhere.
-- `localStorage` access is guarded (wrapped in try/catch or `onMounted` only) so
- it never runs during SSR.
-
-## Testing
-
-- Manual verification: sign in with each method, confirm the badge appears on the
- correct button on the next visit to the login page.
-- Confirm no SSR/hydration errors (badge logic runs client-side only).
-
-## Out of scope
-
-- Syncing the preference across devices.
-- Remembering the specific email address used.
diff --git a/docs/superpowers/specs/2026-07-05-affiliates-landing-page-design.md b/docs/superpowers/specs/2026-07-05-affiliates-landing-page-design.md
deleted file mode 100644
index 312033520..000000000
--- a/docs/superpowers/specs/2026-07-05-affiliates-landing-page-design.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# Affiliates Landing Page — Design
-
-**Date:** 2026-07-05
-**Component:** `web/` (Nuxt 4 SPA, Vuetify 4)
-
-## Goal
-
-Create a marketing landing page at `/affiliates` that convinces people to join the
-httpSMS affiliate program, and replace the current external "Affiliates" footer link
-(`https://httpsms.lemonsqueezy.com/affiliates`) with a `NuxtLink` to this new internal page.
-
-All primary calls-to-action link to the LemonSqueezy affiliate signup:
-`https://affiliates.lemonsqueezy.com/programs/httpsms` (opens in a new tab).
-
-## Files
-
-- **New:** `web/app/pages/affiliates/index.vue` — the landing page.
-- **Edit:** `web/app/layouts/website.vue` — change the footer "Affiliates" link from the
- external `` to
- ``, keeping the existing `mdiShieldStar` warning icon and
- `text-white text-decoration-none footer-link` classes.
-
-## Conventions to follow
-
-- `'),
+ '<script>alert(1)</script>',
+ )
+ })
+
+ await t.test(
+ 'does not let literal placeholder-like text leak a link into the output',
+ () => {
+ const input = 'no links here, just text'
+ assert.equal(formatWhatsappText(input), input)
+ },
+ )
+
+ await t.test('two calls do not interfere with each other', () => {
+ const first = formatWhatsappText('visit https://a.example today')
+ const second = formatWhatsappText('visit https://b.example today')
+ assert.match(first, /href="https:\/\/a\.example"/)
+ assert.match(second, /href="https:\/\/b\.example"/)
+ assert.doesNotMatch(first, /https:\/\/b\.example/)
+ })
+})
diff --git a/web/app/utils/whatsapp-format.ts b/web/app/utils/whatsapp-format.ts
new file mode 100644
index 000000000..c1e4e9e46
--- /dev/null
+++ b/web/app/utils/whatsapp-format.ts
@@ -0,0 +1,198 @@
+/**
+ * Returns true when the given phone notification token is a URL (HTTP notification
+ * transport) instead of an FCM token.
+ */
+export function isUrlNotificationToken(
+ fcmToken?: string | null | undefined,
+): boolean {
+ if (!fcmToken) {
+ return false
+ }
+ return fcmToken.trim().toLowerCase().startsWith('https://')
+}
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
+// Character class (as a regex source string) that is treated as a valid
+// boundary around a formatting marker: whitespace, common punctuation, or
+// the start/end of the string.
+const BOUNDARY_CHARS = String.raw`\s.,!?;:'"()\[\]{}-`
+
+function withBoundaries(pattern: string): RegExp {
+ return new RegExp(
+ `(?<=^|[${BOUNDARY_CHARS}])${pattern}(?=$|[${BOUNDARY_CHARS}])`,
+ 'g',
+ )
+}
+
+const CODE_BLOCK_PATTERN = withBoundaries('```([^`]+)```')
+const INLINE_CODE_PATTERN = withBoundaries('`(\\S(?:[^`\\n]*\\S)?)`')
+const BOLD_PATTERN = withBoundaries('\\*(\\S(?:[^*\\n]*\\S)?)\\*')
+const ITALIC_PATTERN = withBoundaries('_(\\S(?:[^_\\n]*\\S)?)_')
+const STRIKE_PATTERN = withBoundaries('~(\\S(?:[^~\\n]*\\S)?)~')
+
+// Characters that are stripped off the end of a matched URL because they are
+// Sentence punctuation that is always treated as trailing text, never part
+// of a URL (balanced closing parentheses are handled separately below).
+const URL_TRAILING_PUNCTUATION = new Set([
+ ',',
+ '.',
+ ';',
+ ':',
+ '!',
+ '?',
+ "'",
+ '"',
+ ']',
+ '}',
+])
+
+// WhatsApp formatting-marker characters. A trailing one of these is only
+// stripped from a URL when it pairs with a matching opening delimiter
+// immediately before the URL (e.g. the closing `*` in `*https://x.com*`),
+// so legitimate URL characters like the trailing `_` in `.../file_name_`
+// are preserved.
+const URL_TRAILING_MARKERS = new Set(['*', '_', '~', '`'])
+
+/**
+ * Splits a raw URL match into the actual URL and any trailing characters
+ * that aren't part of it. Closing parentheses are balance-checked so URLs
+ * such as `https://example.com/page_(v2)` keep their closing parenthesis.
+ */
+function splitTrailingPunctuation(
+ raw: string,
+ precedingChar: string | undefined,
+): {
+ url: string
+ trailing: string
+} {
+ let end = raw.length
+ while (end > 0) {
+ const char = raw[end - 1] as string
+ if (char === ')') {
+ const opens = (raw.slice(0, end).match(/\(/g) ?? []).length
+ const closes = (raw.slice(0, end).match(/\)/g) ?? []).length
+ if (closes <= opens) break
+ end--
+ continue
+ }
+ if (!URL_TRAILING_PUNCTUATION.has(char)) break
+ end--
+ }
+
+ if (end > 0) {
+ const char = raw[end - 1] as string
+ if (URL_TRAILING_MARKERS.has(char) && precedingChar === char) {
+ end--
+ }
+ }
+
+ return { url: raw.slice(0, end), trailing: raw.slice(end) }
+}
+
+/**
+ * Stores HTML fragments (links, code spans) behind unique placeholder
+ * tokens so that the markdown replacements that run afterwards don't touch
+ * them. Each store instance uses a random per-call nonce so that literal
+ * text in the message can never collide with a placeholder token.
+ */
+function createPlaceholderStore() {
+ const nonce = Math.random().toString(36).slice(2)
+ const open = `\uE000${nonce}:`
+ const close = `:${nonce}\uE000`
+ const store: string[] = []
+ const pattern = new RegExp(`${open}(\\d+)${close}`, 'g')
+
+ return {
+ protect(html: string): string {
+ store.push(html)
+ return `${open}${store.length - 1}${close}`
+ },
+ restore(text: string): string {
+ return text.replace(pattern, (_, idx: string) => store[Number(idx)] ?? '')
+ },
+ }
+}
+
+type PlaceholderStore = ReturnType
+
+/**
+ * Replaces raw URLs with anchor tags, protecting them behind placeholder
+ * tokens so that markdown characters are not misinterpreted as formatting.
+ */
+function linkifyUrls(text: string, placeholders: PlaceholderStore): string {
+ return text.replace(
+ /(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi,
+ (match, _group, offset: number, full: string) => {
+ const precedingChar = offset > 0 ? full[offset - 1] : undefined
+ const { url, trailing } = splitTrailingPunctuation(match, precedingChar)
+ const href = /^www\./i.test(url) ? `https://${url}` : url
+ const anchor = `${url}`
+ return placeholders.protect(anchor) + trailing
+ },
+ )
+}
+
+/**
+ * Replaces code blocks/spans with placeholder tokens so that bold, italic
+ * and strikethrough markers inside code are rendered literally instead of
+ * being interpreted as formatting.
+ */
+function extractCodeSpans(
+ text: string,
+ placeholders: PlaceholderStore,
+): string {
+ return text
+ .replace(CODE_BLOCK_PATTERN, (_, content: string) =>
+ placeholders.protect(`${content}`),
+ )
+ .replace(INLINE_CODE_PATTERN, (_, content: string) =>
+ placeholders.protect(`${content}`),
+ )
+}
+
+/**
+ * Converts WhatsApp-style formatted text into sanitized HTML.
+ * Supported syntax (each marker pair must be surrounded by whitespace,
+ * punctuation, or the start/end of the string, and must not have internal
+ * leading/trailing whitespace, matching WhatsApp's own formatting rules):
+ * - *bold*
+ * - _italic_
+ * - ~strikethrough~
+ * - `code` / ```code block``` (code content is never re-formatted)
+ * - "> " quoted lines (the space after ">" is required)
+ * - raw URLs, turned into clickable links
+ */
+export function formatWhatsappText(text: string): string {
+ const escaped = escapeHtml(text ?? '')
+ const placeholders = createPlaceholderStore()
+
+ const withLinks = linkifyUrls(escaped, placeholders)
+
+ const withQuotes = withLinks
+ .split('\n')
+ .map((line) => {
+ const quoteMatch = line.match(/^> (.*)$/)
+ if (quoteMatch) {
+ return `${quoteMatch[1]}`
+ }
+ return line
+ })
+ .join('\n')
+
+ const withCode = extractCodeSpans(withQuotes, placeholders)
+
+ const formatted = withCode
+ .replace(BOLD_PATTERN, '$1')
+ .replace(ITALIC_PATTERN, '$1')
+ .replace(STRIKE_PATTERN, '$1')
+
+ return placeholders.restore(formatted)
+}
diff --git a/web/package.json b/web/package.json
index 5a1f62823..9d88845c8 100644
--- a/web/package.json
+++ b/web/package.json
@@ -15,7 +15,7 @@
"lint:prettier": "prettier --check .",
"lint": "pnpm lint:js && pnpm lint:style && pnpm lint:prettier",
"lintfix": "prettier --write --list-different . && pnpm lint:js --fix && pnpm lint:style --fix",
- "test": "echo 'No tests configured yet'"
+ "test": "tsx --test app/**/*.test.ts"
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --cache",
@@ -60,6 +60,7 @@
"stylelint": "^17.15.0",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^40.0.0",
+ "tsx": "^4.23.15",
"typescript": "^6.0.3",
"vuetify-nuxt-module": "^0.19.5"
}
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 36992018e..597ec35f9 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -16,7 +16,7 @@ importers:
version: 3.2.0(magicast@0.5.4)
'@pinia/nuxt':
specifier: ^0.11.3
- version: 0.11.3(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ version: 0.11.3(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
chart.js:
specifier: ^4.5.1
version: 4.5.1
@@ -43,7 +43,7 @@ importers:
version: 2.31.0
nuxt:
specifier: ^4.5.1
- version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
+ version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
@@ -61,7 +61,7 @@ importers:
version: 1.105.1
v-phone-input:
specifier: ^7.0.1
- version: 7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ version: 7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
vue:
specifier: ^3.5.34
version: 3.5.41(typescript@6.0.3)
@@ -70,10 +70,10 @@ importers:
version: 5.3.4(chart.js@4.5.1)(vue@3.5.41(typescript@6.0.3))
vue-router:
specifier: ^5.0.7
- version: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ version: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
vuetify:
specifier: ^4.0.7
- version: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ version: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
devDependencies:
'@commitlint/cli':
specifier: ^21.0.2
@@ -83,19 +83,19 @@ importers:
version: 21.2.0
'@nuxt/eslint':
specifier: ^1.16.0
- version: 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ version: 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
'@nuxtjs/seo':
specifier: ^5.3.2
- version: 5.3.10(23a11bd7378bcb74ded7db8eb3f5b08b)
+ version: 5.3.10(90367d1c4564690123e38c844c310bb8)
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
eslint:
specifier: ^10.5.0
- version: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ version: 10.8.0(jiti@2.7.0)
eslint-config-prettier:
specifier: ^10.1.8
- version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ version: 10.1.8(eslint@10.8.0(jiti@2.7.0))
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -107,19 +107,22 @@ importers:
version: 1.8.1
stylelint:
specifier: ^17.15.0
- version: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ version: 17.15.0(typescript@6.0.3)
stylelint-config-recommended-vue:
specifier: ^1.6.1
- version: 1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
+ version: 1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3))
stylelint-config-standard:
specifier: ^40.0.0
- version: 40.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
+ version: 40.0.0(stylelint@17.15.0(typescript@6.0.3))
+ tsx:
+ specifier: ^4.23.15
+ version: 4.23.15
typescript:
specifier: ^6.0.3
version: 6.0.3
vuetify-nuxt-module:
specifier: ^0.19.5
- version: 0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ version: 0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
packages:
@@ -5325,6 +5328,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ tsx@4.23.15:
+ resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
tweetnacl@1.0.3:
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
@@ -5931,20 +5939,20 @@ snapshots:
'@babel/compat-data@7.29.7': {}
- '@babel/core@7.29.7(supports-color@10.2.2)':
+ '@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
- '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -5980,41 +5988,41 @@ snapshots:
lru-cache: 5.1.1
semver: 6.3.1
- '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
'@babel/helper-annotate-as-pure': 7.29.7
- '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
+ '@babel/helper-member-expression-to-functions': 7.29.7
'@babel/helper-optimise-call-expression': 7.29.7
- '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
- '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/traverse': 7.29.8
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/helper-globals@7.29.7': {}
- '@babel/helper-member-expression-to-functions@7.29.7(supports-color@10.2.2)':
+ '@babel/helper-member-expression-to-functions@7.29.7':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)':
+ '@babel/helper-module-imports@7.29.7':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
- '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -6024,18 +6032,18 @@ snapshots:
'@babel/helper-plugin-utils@7.29.7': {}
- '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
- '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
+ '@babel/helper-member-expression-to-functions': 7.29.7
'@babel/helper-optimise-call-expression': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@10.2.2)':
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -6063,24 +6071,24 @@ snapshots:
dependencies:
'@babel/types': 8.0.4
- '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
+ '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
+ '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
'@babel/helper-annotate-as-pure': 7.29.7
- '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
'@babel/helper-plugin-utils': 7.29.7
- '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
transitivePeerDependencies:
- supports-color
@@ -6090,7 +6098,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
- '@babel/traverse@7.29.8(supports-color@10.2.2)':
+ '@babel/traverse@7.29.8':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
@@ -6098,7 +6106,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.8
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -6292,17 +6300,17 @@ snapshots:
dependencies:
postcss-selector-parser: 7.1.5
- '@dxup/nuxt@0.5.6(esbuild@0.25.12)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))':
+ '@dxup/nuxt@0.5.6(esbuild@0.25.12)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
'@dxup/unimport': 0.1.2
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@vue/compiler-dom': 3.5.41
chokidar: 5.0.0
knitwork: 1.3.0
magic-string: 1.1.0
pathe: 2.0.3
tinyglobby: 0.2.17
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@farmfe/core'
- '@rspack/core'
@@ -6521,23 +6529,23 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
- '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))':
dependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/compat@2.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
+ '@eslint/compat@2.1.0(eslint@10.8.0(jiti@2.7.0))':
dependencies:
'@eslint/core': 1.2.1
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
- '@eslint/config-array@0.23.5(supports-color@10.2.2)':
+ '@eslint/config-array@0.23.5':
dependencies:
'@eslint/object-schema': 3.0.5
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
minimatch: 10.2.6
transitivePeerDependencies:
- supports-color
@@ -6550,13 +6558,13 @@ snapshots:
dependencies:
'@eslint/core': 1.2.1
- '@eslint/config-inspector@3.2.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(srvx@0.11.22)':
+ '@eslint/config-inspector@3.2.0(eslint@10.8.0(jiti@2.7.0))(srvx@0.11.22)':
dependencies:
ansis: 4.3.1
cac: 7.0.0
chokidar: 5.0.0
devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
jiti: 2.7.0
tinyglobby: 0.2.17
transitivePeerDependencies:
@@ -6573,9 +6581,9 @@ snapshots:
mdn-data: 2.29.0
source-map-js: 1.2.1
- '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
+ '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0))':
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
'@eslint/object-schema@3.0.5': {}
@@ -7088,19 +7096,19 @@ snapshots:
'@kurkle/color@0.3.4': {}
- '@kwsites/file-exists@1.1.1(supports-color@10.2.2)':
+ '@kwsites/file-exists@1.1.1':
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@kwsites/promise-deferred@1.1.1': {}
- '@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)':
+ '@mapbox/node-pre-gyp@2.0.3':
dependencies:
consola: 3.4.2
detect-libc: 2.1.2
- https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ https-proxy-agent: 7.0.6
node-fetch: 2.7.0
nopt: 8.1.0
semver: 7.8.5
@@ -7142,7 +7150,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
- '@nuxt/cli@3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)(supports-color@10.2.2)':
+ '@nuxt/cli@3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)':
dependencies:
'@bomb.sh/tab': 0.0.19(cac@7.0.0)(citty@0.2.2)
'@clack/prompts': 1.7.0
@@ -7150,7 +7158,7 @@ snapshots:
citty: 0.2.2
confbox: 0.2.4
consola: 3.4.2
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
defu: 6.1.7
exsolve: 1.1.1
fuse.js: 7.5.0
@@ -7183,11 +7191,11 @@ snapshots:
'@nuxt/devalue@2.0.2': {}
- '@nuxt/devtools-kit@3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))':
+ '@nuxt/devtools-kit@3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
execa: 8.0.1
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- magic-string
- magicast
@@ -7195,11 +7203,11 @@ snapshots:
- rolldown
- unplugin
- '@nuxt/devtools-kit@4.0.0-alpha.7(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))':
+ '@nuxt/devtools-kit@4.0.0-alpha.7(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
tinyexec: 1.3.0
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- magic-string
- magicast
@@ -7218,11 +7226,11 @@ snapshots:
pkg-types: 2.3.1
semver: 7.8.5
- '@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
- '@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ '@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
'@nuxt/devtools-wizard': 3.4.1
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@vue/devtools-core': 8.2.1(vue@3.5.41(typescript@6.0.3))
'@vue/devtools-kit': 8.2.1
birpc: 4.0.0
@@ -7244,14 +7252,14 @@ snapshots:
perfect-debounce: 2.1.0
pkg-types: 2.3.1
semver: 7.8.5
- simple-git: 3.36.0(supports-color@10.2.2)
+ simple-git: 3.36.0
sirv: 3.0.2
structured-clone-es: 2.0.1
tinyglobby: 0.2.17
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
- vite-plugin-inspect: 11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- vite-plugin-vue-tracer: 1.4.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
+ vite-plugin-inspect: 11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ vite-plugin-vue-tracer: 1.4.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
which: 6.0.1
ws: 8.21.2
transitivePeerDependencies:
@@ -7283,30 +7291,30 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@antfu/install-pkg': 2.0.1
'@clack/prompts': 1.7.0
- '@eslint/js': 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
- eslint-config-flat-gitignore: 2.3.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint/js': 10.0.1(eslint@10.8.0(jiti@2.7.0))
+ '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0))
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)
+ eslint-config-flat-gitignore: 2.3.0(eslint@10.8.0(jiti@2.7.0))
eslint-flat-config-utils: 3.2.0
- eslint-merge-processors: 2.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint-plugin-import-lite: 0.6.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
- eslint-plugin-jsdoc: 63.3.3(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
- eslint-plugin-regexp: 3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint-plugin-unicorn: 73.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))
- eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-merge-processors: 2.0.0(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-import-lite: 0.6.0(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-jsdoc: 63.3.3(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-regexp: 3.1.1(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-unicorn: 73.0.0(eslint@10.8.0(jiti@2.7.0))
+ eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)))
+ eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))
globals: 17.9.0
local-pkg: 1.2.1
pathe: 2.0.3
- vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0))
transitivePeerDependencies:
- '@typescript-eslint/utils'
- '@vue/compiler-sfc'
@@ -7314,31 +7322,31 @@ snapshots:
- supports-color
- typescript
- '@nuxt/eslint-plugin@1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@nuxt/eslint-plugin@1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)
transitivePeerDependencies:
- supports-color
- typescript
- '@nuxt/eslint@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))':
+ '@nuxt/eslint@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
- '@eslint/config-inspector': 3.2.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(srvx@0.11.22)
- '@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- '@nuxt/eslint-config': 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@eslint/config-inspector': 3.2.0(eslint@10.8.0(jiti@2.7.0))(srvx@0.11.22)
+ '@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ '@nuxt/eslint-config': 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
chokidar: 5.0.0
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
eslint-flat-config-utils: 3.2.0
- eslint-typegen: 2.3.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-typegen: 2.3.1(eslint@10.8.0(jiti@2.7.0))
find-up: 8.0.0
get-port-please: 3.2.0
mlly: 1.8.2
pathe: 2.0.3
- unimport: 6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unimport: 6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@farmfe/core'
- '@modelcontextprotocol/client'
@@ -7389,7 +7397,7 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))':
+ '@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))':
dependencies:
c12: 3.3.4(magicast@0.5.4)
consola: 3.4.2
@@ -7409,7 +7417,7 @@ snapshots:
scule: 1.3.0
tinyglobby: 0.2.17
ufo: 1.6.4
- unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
untyped: 2.0.0
verkit: 0.3.2
transitivePeerDependencies:
@@ -7419,11 +7427,11 @@ snapshots:
- rolldown
- unplugin
- '@nuxt/nitro-server@4.5.2(939e14dbbe7d2e2e95d6de722d728255)':
+ '@nuxt/nitro-server@4.5.2(6be6cc826f211ead8f9963a4117b341a)':
dependencies:
'@nuxt/devalue': 2.0.2
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@vue/shared': 3.5.41
consola: 3.4.2
defu: 6.1.7
@@ -7433,25 +7441,25 @@ snapshots:
escape-string-regexp: 5.0.0
exsolve: 1.1.1
h3: 1.15.11
- impound: 1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ impound: 1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
klona: 2.0.6
mocked-exports: 0.1.1
- nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
nostics: 1.2.0
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
ohash: 2.0.11
pathe: 2.0.3
rou3: 0.9.1
std-env: 4.2.0
ufo: 1.6.4
- unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
+ unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
vue: 3.5.41(typescript@6.0.3)
vue-bundle-renderer: 2.3.1
vue-devtools-stub: 0.1.0
optionalDependencies:
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7514,20 +7522,20 @@ snapshots:
pkg-types: 2.3.1
std-env: 4.2.0
- '@nuxt/telemetry@2.8.0(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))))':
+ '@nuxt/telemetry@2.8.0(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
citty: 0.2.2
consola: 3.4.2
ofetch: 2.0.0-alpha.3
rc9: 3.0.1
std-env: 4.2.0
- '@nuxt/vite-builder@4.5.2(c451715f9da445cf1604dc21ee91ddbf)':
+ '@nuxt/vite-builder@4.5.2(08c1d6837a056862854e60ca899e0fc8)':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
- '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@vitejs/plugin-vue-jsx': 5.1.6(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
autoprefixer: 10.5.4(postcss@8.5.26)
consola: 3.4.2
cssnano: 8.0.4(postcss@8.5.26)
@@ -7541,7 +7549,7 @@ snapshots:
knitwork: 1.3.0
mlly: 1.8.2
mocked-exports: 0.1.1
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
pathe: 2.0.3
pkg-types: 2.3.1
@@ -7551,23 +7559,19 @@ snapshots:
std-env: 4.2.0
ufo: 1.6.4
unenv: 2.0.0-rc.24
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
- vite-node: 6.0.0(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
- vite-plugin-checker: 0.14.5(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
+ vite-node: 6.0.0(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
+ vite-plugin-checker: 0.14.5(eslint@10.8.0(jiti@2.7.0))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
vue: 3.5.41(typescript@6.0.3)
vue-bundle-renderer: 2.3.1
optionalDependencies:
- '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
rolldown: 1.2.3
rollup-plugin-visualizer: 7.0.1(rolldown@1.2.3)(rollup@4.62.4)
transitivePeerDependencies:
- '@biomejs/biome'
- - '@farmfe/core'
- - '@rspack/core'
- '@types/node'
- '@vitejs/devtools'
- - bun-types-no-globals
- esbuild
- eslint
- less
@@ -7577,7 +7581,6 @@ snapshots:
- optionator
- oxc-parser
- oxlint
- - rollup
- sass
- sass-embedded
- stylelint
@@ -7587,9 +7590,8 @@ snapshots:
- terser
- tsx
- typescript
- - unloader
+ - unplugin
- vue-tsc
- - webpack
- yaml
'@nuxtjs/google-fonts@3.2.0(magicast@0.5.4)':
@@ -7600,15 +7602,15 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxtjs/robots@6.1.3(f964271bb36a557850bb349e3da7a4bd)':
+ '@nuxtjs/robots@6.1.3(7f089dda3677d5e76f6c9e5335d8969d)':
dependencies:
'@fingerprintjs/botd': 2.0.0
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
consola: 3.4.2
defu: 6.1.7
h3: 1.15.11
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
pathe: 2.0.3
pkg-types: 2.3.1
ufo: 1.6.4
@@ -7623,18 +7625,18 @@ snapshots:
- vite
- vue
- '@nuxtjs/seo@5.3.10(23a11bd7378bcb74ded7db8eb3f5b08b)':
- dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- '@nuxtjs/robots': 6.1.3(f964271bb36a557850bb349e3da7a4bd)
- '@nuxtjs/sitemap': 8.3.3(f964271bb36a557850bb349e3da7a4bd)
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-link-checker: 5.1.3(073d05fcc8850462c89d9255517da191)
- nuxt-og-image: 6.7.6(53be939db9d25e3b03ee880813903870)
- nuxt-schema-org: 6.2.8(ac023424663d637d35a549ec04853864)
- nuxt-seo-utils: 8.3.3(c76948da82c2396f2c34923c70282261)
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ '@nuxtjs/seo@5.3.10(90367d1c4564690123e38c844c310bb8)':
+ dependencies:
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ '@nuxtjs/robots': 6.1.3(7f089dda3677d5e76f6c9e5335d8969d)
+ '@nuxtjs/sitemap': 8.3.3(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-link-checker: 5.1.3(ee45fc925654584e4a6f72bf8e386c53)
+ nuxt-og-image: 6.7.6(f726c63e583b3eb786f27b68321e6d91)
+ nuxt-schema-org: 6.2.8(2d10904abaf8d3b235f35e4fac57727b)
+ nuxt-seo-utils: 8.3.3(a89aa3147dcc17dfe4200c84f0407a4d)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7693,13 +7695,13 @@ snapshots:
- webpack
- zod
- '@nuxtjs/sitemap@8.3.3(f964271bb36a557850bb349e3da7a4bd)':
+ '@nuxtjs/sitemap@8.3.3(7f089dda3677d5e76f6c9e5335d8969d)':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
consola: 3.4.2
defu: 6.1.7
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.1
@@ -7848,9 +7850,9 @@ snapshots:
'@parcel/watcher-win32-x64': 2.6.0
optional: true
- '@pinia/nuxt@0.11.3(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))':
+ '@pinia/nuxt@0.11.3(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))':
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
pinia: 3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
transitivePeerDependencies:
- magic-string
@@ -8100,11 +8102,11 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
+ '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0))':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
'@typescript-eslint/types': 8.66.0
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@@ -8135,15 +8137,15 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
- '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.66.0
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -8151,23 +8153,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3(supports-color@10.2.2)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ debug: 4.4.3
+ eslint: 10.8.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.66.0(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
'@typescript-eslint/types': 8.66.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -8181,13 +8183,13 @@ snapshots:
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- debug: 4.4.3(supports-color@10.2.2)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ debug: 4.4.3
+ eslint: 10.8.0(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
@@ -8195,13 +8197,13 @@ snapshots:
'@typescript-eslint/types@8.66.0': {}
- '@typescript-eslint/typescript-estree@8.66.0(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
'@typescript-eslint/types': 8.66.0
'@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
minimatch: 10.2.6
semver: 7.8.5
tinyglobby: 0.2.17
@@ -8210,13 +8212,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -8226,18 +8228,18 @@ snapshots:
'@typescript-eslint/types': 8.66.0
eslint-visitor-keys: 5.0.1
- '@unhead/bundler@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))':
+ '@unhead/bundler@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
magic-string: 1.1.0
oxc-walker: 1.1.1(@oxc-project/types@0.143.0)(oxc-parser@0.142.0)(rolldown@1.2.3)
- unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
optionalDependencies:
esbuild: 0.25.12
lightningcss: 1.33.0
oxc-parser: 0.142.0
rolldown: 1.2.3
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- '@farmfe/core'
- '@oxc-project/types'
@@ -8246,15 +8248,15 @@ snapshots:
- rollup
- unloader
- '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@unhead/vue@3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
- '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
hookable: 6.1.1
- unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- '@farmfe/core'
- '@oxc-project/types'
@@ -8339,9 +8341,9 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
- '@vercel/nft@1.10.2(rollup@4.62.4)(supports-color@10.2.2)':
+ '@vercel/nft@1.10.2(rollup@4.62.4)':
dependencies:
- '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2)
+ '@mapbox/node-pre-gyp': 2.0.3
'@rollup/pluginutils': 5.4.0(rollup@4.62.4)
acorn: 8.18.0
acorn-import-attributes: 1.9.5(acorn@8.18.0)
@@ -8358,22 +8360,22 @@ snapshots:
- rollup
- supports-color
- '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
- '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
+ '@babel/core': 7.29.7
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
'@rolldown/pluginutils': 1.0.1
- '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
'@vue-macros/common@3.1.4(vue@3.5.41(typescript@6.0.3))':
@@ -8388,27 +8390,27 @@ snapshots:
'@vue/babel-helper-vue-transform-on@2.0.1': {}
- '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)':
dependencies:
- '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
+ '@babel/helper-module-imports': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@vue/babel-helper-vue-transform-on': 2.0.1
- '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
+ '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7)
'@vue/shared': 3.5.41
optionalDependencies:
- '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
transitivePeerDependencies:
- supports-color
- '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
+ '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)':
dependencies:
'@babel/code-frame': 7.29.7
- '@babel/core': 7.29.7(supports-color@10.2.2)
- '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
'@babel/parser': 7.29.8
'@vue/compiler-sfc': 3.5.41
@@ -8510,7 +8512,7 @@ snapshots:
dependencies:
upath: 2.0.1
vue: 3.5.41(typescript@6.0.3)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
'@vuetify/v0@1.2.3(vue@3.5.41(typescript@6.0.3))':
dependencies:
@@ -8783,12 +8785,12 @@ snapshots:
chownr@3.0.0: {}
- chrome-launcher@1.2.1(supports-color@10.2.2):
+ chrome-launcher@1.2.1:
dependencies:
'@types/node': 26.1.2
escape-string-regexp: 4.0.0
is-wsl: 2.2.0
- lighthouse-logger: 2.0.2(supports-color@10.2.2)
+ lighthouse-logger: 2.0.2
transitivePeerDependencies:
- supports-color
@@ -9003,11 +9005,9 @@ snapshots:
db0@0.3.4: {}
- debug@4.4.3(supports-color@10.2.2):
+ debug@4.4.3:
dependencies:
ms: 2.1.3
- optionalDependencies:
- supports-color: 10.2.2
decamelize@1.2.0: {}
@@ -9188,14 +9188,14 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-config-flat-gitignore@2.3.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-config-flat-gitignore@2.3.0(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- '@eslint/compat': 2.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ '@eslint/compat': 2.1.0(eslint@10.8.0(jiti@2.7.0))
+ eslint: 10.8.0(jiti@2.7.0)
- eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
eslint-flat-config-utils@3.2.0:
dependencies:
@@ -9209,20 +9209,20 @@ snapshots:
optionalDependencies:
unrs-resolver: 1.12.2
- eslint-merge-processors@2.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-merge-processors@2.0.0(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
- eslint-plugin-import-lite@0.6.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-plugin-import-lite@0.6.0(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
- eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
+ eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)):
dependencies:
'@typescript-eslint/types': 8.66.0
comment-parser: 1.4.7
- debug: 4.4.3(supports-color@10.2.2)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ debug: 4.4.3
+ eslint: 10.8.0(jiti@2.7.0)
eslint-import-context: 0.1.9(unrs-resolver@1.12.2)
is-glob: 4.0.3
minimatch: 10.2.6
@@ -9230,19 +9230,19 @@ snapshots:
stable-hash-x: 0.2.0
unrs-resolver: 1.12.2
optionalDependencies:
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
transitivePeerDependencies:
- supports-color
- eslint-plugin-jsdoc@63.3.3(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
+ eslint-plugin-jsdoc@63.3.3(eslint@10.8.0(jiti@2.7.0)):
dependencies:
'@es-joy/jsdoccomment': 0.91.0
'@es-joy/resolve.exports': 1.2.0
are-docs-informative: 0.0.2
comment-parser: 1.4.7
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
escape-string-regexp: 4.0.0
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
espree: 11.2.0
esquery: 1.7.0
html-entities: 2.6.0
@@ -9254,20 +9254,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-regexp@3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-plugin-regexp@3.1.1(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
comment-parser: 1.4.7
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
jsdoc-type-pratt-parser: 7.3.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
- eslint-plugin-unicorn@73.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-plugin-unicorn@73.0.0(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
'@eslint/css-tree': 4.0.5
browserslist: 4.28.7
change-case: 5.4.4
@@ -9275,7 +9275,7 @@ snapshots:
core-js-compat: 3.50.0
detect-indent: 7.0.2
entities: 4.5.0
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
find-up-simple: 1.0.1
globals: 17.9.0
indent-string: 5.0.0
@@ -9289,24 +9289,24 @@ snapshots:
strip-indent: 4.1.1
yaml: 2.9.0
- eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)):
+ eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0))):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ eslint: 10.8.0(jiti@2.7.0)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.5
semver: 7.8.5
- vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0))
xml-name-validator: 5.0.0
optionalDependencies:
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0))
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)):
dependencies:
'@vue/compiler-sfc': 3.5.41
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
eslint-scope@9.1.2:
dependencies:
@@ -9315,9 +9315,9 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
- eslint-typegen@2.3.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
+ eslint-typegen@2.3.1(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
json-schema-to-typescript-lite: 15.0.0
ohash: 2.0.11
@@ -9327,11 +9327,11 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2):
+ eslint@10.8.0(jiti@2.7.0):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.23.5(supports-color@10.2.2)
+ '@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.7.0
'@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.7.2
@@ -9341,7 +9341,7 @@ snapshots:
'@types/estree': 1.0.9
ajv: 6.15.0
cross-spawn: 7.0.6
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
@@ -9731,10 +9731,10 @@ snapshots:
http-shutdown@1.2.2: {}
- https-proxy-agent@7.0.6(supports-color@10.2.2):
+ https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -9769,10 +9769,10 @@ snapshots:
import-meta-resolve@4.2.0: {}
- importx@0.5.2(supports-color@10.2.2):
+ importx@0.5.2:
dependencies:
bundle-require: 5.1.0(esbuild@0.25.12)
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
esbuild: 0.25.12
jiti: 2.7.0
pathe: 2.0.3
@@ -9780,12 +9780,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- impound@1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ impound@1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
es-module-lexer: 2.3.1
pathe: 2.0.3
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
transitivePeerDependencies:
- '@farmfe/core'
@@ -9810,11 +9810,11 @@ snapshots:
ini@6.0.0: {}
- ioredis@5.11.1(supports-color@10.2.2):
+ ioredis@5.11.1:
dependencies:
'@ioredis/commands': 1.10.0
cluster-key-slot: 1.1.1
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
denque: 2.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
@@ -9971,9 +9971,9 @@ snapshots:
libphonenumber-js@1.13.10: {}
- lighthouse-logger@2.0.2(supports-color@10.2.2):
+ lighthouse-logger@2.0.2:
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
marky: 1.3.0
transitivePeerDependencies:
- supports-color
@@ -10200,7 +10200,7 @@ snapshots:
natural-compare@1.4.0: {}
- nitropack@2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ nitropack@2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.2
'@rollup/plugin-alias': 6.0.0(rollup@4.62.4)
@@ -10210,7 +10210,7 @@ snapshots:
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.4)
'@rollup/plugin-replace': 6.0.3(rollup@4.62.4)
'@rollup/plugin-terser': 1.0.0(rollup@4.62.4)
- '@vercel/nft': 1.10.2(rollup@4.62.4)(supports-color@10.2.2)
+ '@vercel/nft': 1.10.2(rollup@4.62.4)
archiver: 7.0.1
c12: 3.3.4(magicast@0.5.4)
chokidar: 5.0.0
@@ -10234,7 +10234,7 @@ snapshots:
h3: 1.15.11
hookable: 5.5.3
httpxy: 0.5.5
- ioredis: 5.11.1(supports-color@10.2.2)
+ ioredis: 5.11.1
jiti: 2.7.0
klona: 2.0.6
knitwork: 1.3.0
@@ -10257,7 +10257,7 @@ snapshots:
scule: 1.3.0
semver: 7.8.5
serve-placeholder: 2.0.2
- serve-static: 2.2.1(supports-color@10.2.2)
+ serve-static: 2.2.1
source-map: 0.7.6
std-env: 4.2.0
ufo: 1.6.4
@@ -10265,9 +10265,9 @@ snapshots:
uncrypto: 0.1.3
unctx: 2.5.0
unenv: 2.0.0-rc.24
- unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
untyped: 2.0.0
unwasm: 0.5.3
youch: 4.1.1
@@ -10350,18 +10350,18 @@ snapshots:
dependencies:
boolbase: 1.0.0
- nuxt-link-checker@5.1.3(073d05fcc8850462c89d9255517da191):
+ nuxt-link-checker@5.1.3(ee45fc925654584e4a6f72bf8e386c53):
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3))
consola: 3.4.2
diff: 9.0.0
fuse.js: 7.5.0
h3: 1.15.11
magic-string: 1.1.0
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.1
@@ -10369,7 +10369,7 @@ snapshots:
site-config-stack: 4.2.0(vue@3.5.41(typescript@6.0.3))
ufo: 1.6.4
ultrahtml: 1.7.0
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -10399,13 +10399,13 @@ snapshots:
- vue
- zod
- nuxt-og-image@6.7.6(53be939db9d25e3b03ee880813903870):
+ nuxt-og-image@6.7.6(f726c63e583b3eb786f27b68321e6d91):
dependencies:
'@clack/prompts': 1.7.0
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@vue/compiler-sfc': 3.5.41
- chrome-launcher: 1.2.1(supports-color@10.2.2)
+ chrome-launcher: 1.2.1
consola: 3.4.2
defu: 6.1.7
devalue: 5.9.2
@@ -10415,8 +10415,8 @@ snapshots:
magic-string: 1.1.0
magicast: 0.5.4
mocked-exports: 0.1.1
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
nypm: 0.6.9
object-identity: 0.2.3
ofetch: 1.5.1
@@ -10431,10 +10431,10 @@ snapshots:
tinyexec: 1.3.0
ufo: 1.6.4
ultrahtml: 1.7.0
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
optionalDependencies:
- nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@farmfe/core'
- '@nuxt/schema'
@@ -10451,17 +10451,17 @@ snapshots:
- vue
- webpack
- nuxt-schema-org@6.2.8(ac023424663d637d35a549ec04853864):
+ nuxt-schema-org@6.2.8(2d10904abaf8d3b235f35e4fac57727b):
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
defu: 6.1.7
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
pkg-types: 2.3.1
ufo: 1.6.4
optionalDependencies:
- '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
- unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@nuxt/schema'
- magic-string
@@ -10473,31 +10473,31 @@ snapshots:
- vite
- vue
- nuxt-seo-utils@8.3.3(c76948da82c2396f2c34923c70282261):
+ nuxt-seo-utils@8.3.3(a89aa3147dcc17dfe4200c84f0407a4d):
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
citty: 0.2.2
consola: 3.4.2
defu: 6.1.7
escape-string-regexp: 5.0.0
exsolve: 1.1.1
image-size: 2.0.2
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
pathe: 2.0.3
pkg-types: 2.3.1
scule: 1.3.0
tinyglobby: 0.2.17
ufo: 1.6.4
optionalDependencies:
- '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@unhead/bundler': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
esbuild: 0.25.12
lightningcss: 1.33.0
rolldown: 1.2.3
sharp: 0.35.4(@types/node@26.1.2)
- unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@nuxt/schema'
- '@types/node'
@@ -10509,9 +10509,9 @@ snapshots:
- vue
- zod
- nuxt-site-config-kit@4.2.0(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3)):
+ nuxt-site-config-kit@4.2.0(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3)):
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
site-config-stack: 4.2.0(vue@3.5.41(typescript@6.0.3))
std-env: 4.2.0
ufo: 1.6.4
@@ -10523,15 +10523,15 @@ snapshots:
- unplugin
- vue
- nuxt-site-config@4.2.0(f964271bb36a557850bb349e3da7a4bd):
+ nuxt-site-config@4.2.0(7f089dda3677d5e76f6c9e5335d8969d):
dependencies:
'@nuxt/devalue': 2.0.2
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
consola: 3.4.2
defu: 6.1.7
h3: 1.15.11
- nuxt-site-config-kit: 4.2.0(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3))
- nuxtseo-shared: 5.3.10(506c592eb03846313a3f5d1595fc6889)
+ nuxt-site-config-kit: 4.2.0(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3))
+ nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
pathe: 2.0.3
pkg-types: 2.3.1
site-config-stack: 4.2.0(vue@3.5.41(typescript@6.0.3))
@@ -10548,17 +10548,17 @@ snapshots:
- vite
- zod
- nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0):
+ nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0):
dependencies:
- '@dxup/nuxt': 0.5.6(esbuild@0.25.12)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)(supports-color@10.2.2)
- '@nuxt/devtools': 3.4.1(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
- '@nuxt/nitro-server': 4.5.2(939e14dbbe7d2e2e95d6de722d728255)
+ '@dxup/nuxt': 0.5.6(esbuild@0.25.12)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)
+ '@nuxt/devtools': 3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
+ '@nuxt/nitro-server': 4.5.2(6be6cc826f211ead8f9963a4117b341a)
'@nuxt/schema': 4.5.2
- '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))))
- '@nuxt/vite-builder': 4.5.2(c451715f9da445cf1604dc21ee91ddbf)
- '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))
+ '@nuxt/vite-builder': 4.5.2(08c1d6837a056862854e60ca899e0fc8)
+ '@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@vue/shared': 3.5.41
chokidar: 5.0.0
compatx: 0.2.0
@@ -10572,7 +10572,7 @@ snapshots:
fnv1a-64: 0.1.2
hookable: 6.1.1
ignore: 7.0.6
- impound: 1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ impound: 1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
jiti: 2.7.0
klona: 2.0.6
knitwork: 1.3.0
@@ -10599,17 +10599,17 @@ snapshots:
ufo: 1.6.4
ultrahtml: 1.7.0
uncrypto: 0.1.3
- unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
undici: 8.11.2
- unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- unimport: 6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unhead: 3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ unimport: 6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unrouting: 0.2.2
untyped: 2.0.0
verkit: 0.3.2
vue: 3.5.41(typescript@6.0.3)
vue-component-type-helpers: 3.3.9
- vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
optionalDependencies:
'@parcel/watcher': 2.6.0
'@types/node': 26.1.2
@@ -10690,16 +10690,16 @@ snapshots:
- xml2js
- yaml
- nuxtseo-shared@5.3.10(506c592eb03846313a3f5d1595fc6889):
+ nuxtseo-shared@5.3.10(e7e317c556a96caaa3d5be917284fb38):
dependencies:
'@clack/prompts': 1.7.0
- '@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@nuxt/schema': 4.5.2
birpc: 4.0.0
consola: 3.4.2
defu: 6.1.7
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.10)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
ofetch: 1.5.1
pathe: 2.0.3
@@ -10710,7 +10710,7 @@ snapshots:
ufo: 1.6.4
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
- nuxt-site-config: 4.2.0(f964271bb36a557850bb349e3da7a4bd)
+ nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
transitivePeerDependencies:
- magic-string
- magicast
@@ -11328,9 +11328,9 @@ snapshots:
semver@7.8.5: {}
- send@1.2.1(supports-color@10.2.2):
+ send@1.2.1:
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -11352,12 +11352,12 @@ snapshots:
dependencies:
defu: 6.1.7
- serve-static@2.2.1(supports-color@10.2.2):
+ serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
- send: 1.2.1(supports-color@10.2.2)
+ send: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -11411,13 +11411,13 @@ snapshots:
signal-exit@4.1.0: {}
- simple-git@3.36.0(supports-color@10.2.2):
+ simple-git@3.36.0:
dependencies:
- '@kwsites/file-exists': 1.1.1(supports-color@10.2.2)
+ '@kwsites/file-exists': 1.1.1
'@kwsites/promise-deferred': 1.1.1
'@simple-git/args-pathspec': 1.0.3
'@simple-git/argv-parser': 1.1.1
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -11552,29 +11552,29 @@ snapshots:
postcss: 8.5.26
postcss-selector-parser: 7.1.5
- stylelint-config-html@2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
+ stylelint-config-html@2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3)):
dependencies:
postcss-html: 1.8.1
- stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint: 17.15.0(typescript@6.0.3)
- stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
+ stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3)):
dependencies:
postcss-html: 1.8.1
semver: 7.8.5
- stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
- stylelint-config-html: 2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
- stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
+ stylelint: 17.15.0(typescript@6.0.3)
+ stylelint-config-html: 2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3))
+ stylelint-config-recommended: 18.0.0(stylelint@17.15.0(typescript@6.0.3))
- stylelint-config-recommended@18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
+ stylelint-config-recommended@18.0.0(stylelint@17.15.0(typescript@6.0.3)):
dependencies:
- stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint: 17.15.0(typescript@6.0.3)
- stylelint-config-standard@40.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
+ stylelint-config-standard@40.0.0(stylelint@17.15.0(typescript@6.0.3)):
dependencies:
- stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
- stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
+ stylelint: 17.15.0(typescript@6.0.3)
+ stylelint-config-recommended: 18.0.0(stylelint@17.15.0(typescript@6.0.3))
- stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3):
+ stylelint@17.15.0(typescript@6.0.3):
dependencies:
'@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
@@ -11587,7 +11587,7 @@ snapshots:
cosmiconfig: 9.0.2(typescript@6.0.3)
css-functions-list: 3.3.3
css-tree: 3.2.1
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
fast-glob: 3.3.3
fastest-levenshtein: 1.0.16
file-entry-cache: 11.1.5
@@ -11737,6 +11737,12 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ tsx@4.23.15:
+ dependencies:
+ esbuild: 0.28.1
+ optionalDependencies:
+ fsevents: 2.3.3
+
tweetnacl@1.0.3: {}
type-check@0.4.0:
@@ -11755,11 +11761,11 @@ snapshots:
ultrahtml@1.7.0: {}
- unconfig@0.6.1(supports-color@10.2.2):
+ unconfig@0.6.1:
dependencies:
'@antfu/utils': 8.1.1
defu: 6.1.7
- importx: 0.5.2(supports-color@10.2.2)
+ importx: 0.5.2
transitivePeerDependencies:
- supports-color
@@ -11772,12 +11778,12 @@ snapshots:
magic-string: 0.30.21
unplugin: 2.3.11
- unctx@3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))):
+ unctx@3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))):
optionalDependencies:
magic-string: 1.1.0
oxc-parser: 0.142.0
rolldown: 1.2.3
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
undici-types@8.3.0: {}
@@ -11787,12 +11793,12 @@ snapshots:
dependencies:
pathe: 2.0.3
- unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ unhead@3.3.1(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
hookable: 6.1.1
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
optionalDependencies:
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- '@farmfe/core'
- '@rspack/core'
@@ -11807,7 +11813,7 @@ snapshots:
unicorn-magic@0.4.0: {}
- unimport@6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ unimport@6.4.0(esbuild@0.25.12)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
acorn: 8.18.0
escape-string-regexp: 5.0.0
@@ -11821,7 +11827,7 @@ snapshots:
scule: 1.3.0
strip-literal: 4.0.0
tinyglobby: 0.2.17
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
optionalDependencies:
oxc-parser: 0.142.0
@@ -11836,7 +11842,7 @@ snapshots:
- vite
- webpack
- unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
acorn: 8.18.0
escape-string-regexp: 5.0.0
@@ -11850,7 +11856,7 @@ snapshots:
scule: 1.3.0
strip-literal: 4.0.0
tinyglobby: 0.2.17
- unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
optionalDependencies:
oxc-parser: 0.142.0
@@ -11877,7 +11883,7 @@ snapshots:
picomatch: 4.0.5
webpack-virtual-modules: 0.6.2
- unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.5
@@ -11886,9 +11892,9 @@ snapshots:
esbuild: 0.25.12
rolldown: 1.2.3
rollup: 4.62.4
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
- unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.5
@@ -11897,7 +11903,7 @@ snapshots:
esbuild: 0.28.1
rolldown: 1.2.3
rollup: 4.62.4
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
unrouting@0.2.2:
dependencies:
@@ -11931,7 +11937,7 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.12.2
'@unrs/resolver-binding-win32-x64-msvc': 1.12.2
- unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)):
+ unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -11943,7 +11949,7 @@ snapshots:
ufo: 1.6.4
optionalDependencies:
db0: 0.3.4
- ioredis: 5.11.1(supports-color@10.2.2)
+ ioredis: 5.11.1
untun@0.2.2: {}
@@ -11980,14 +11986,14 @@ snapshots:
util-deprecate@1.0.2: {}
- v-phone-input@7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
+ v-phone-input@7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
dependencies:
awesome-phonenumber: 7.8.0
countries-list: 3.4.1
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
flag-icons: 7.5.0
- vuetify: 4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
world-flags-sprite: 0.0.2
transitivePeerDependencies:
- '@adobe/leonardo-contrast-colors'
@@ -12005,23 +12011,23 @@ snapshots:
verkit@0.3.2: {}
- vite-dev-rpc@2.0.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ vite-dev-rpc@2.0.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
birpc: 4.0.0
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
- vite-hot-client: 2.2.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
+ vite-hot-client: 2.2.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
- vite-hot-client@2.2.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ vite-hot-client@2.2.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
- vite-node@6.0.0(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0):
+ vite-node@6.0.0(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0):
dependencies:
cac: 7.0.0
es-module-lexer: 2.3.1
obug: 2.1.4
pathe: 2.0.3
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- '@vitejs/devtools'
@@ -12036,7 +12042,7 @@ snapshots:
- tsx
- yaml
- vite-plugin-checker@0.14.5(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ vite-plugin-checker@0.14.5(eslint@10.8.0(jiti@2.7.0))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@babel/code-frame': 7.29.7
chokidar: 5.0.0
@@ -12045,15 +12051,15 @@ snapshots:
picomatch: 4.0.5
proper-lockfile: 4.1.2
tiny-invariant: 1.3.3
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)
meow: 14.1.0
optionator: 0.9.4
- stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint: 17.15.0(typescript@6.0.3)
typescript: 6.0.3
- vite-plugin-inspect@11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)):
+ vite-plugin-inspect@11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
ansis: 4.3.1
error-stack-parser-es: 1.0.5
@@ -12063,33 +12069,33 @@ snapshots:
perfect-debounce: 2.1.0
sirv: 3.0.2
unplugin-utils: 0.3.2
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
- vite-dev-rpc: 2.0.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
+ vite-dev-rpc: 2.0.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
optionalDependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
- vite-plugin-vue-tracer@1.4.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
+ vite-plugin-vue-tracer@1.4.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
dependencies:
estree-walker: 3.0.3
exsolve: 1.1.1
magic-string: 0.30.21
pathe: 2.0.3
source-map-js: 1.2.1
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
- vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8):
+ vite-plugin-vuetify@2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8):
dependencies:
'@vuetify/loader-shared': 2.1.2(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3
upath: 2.0.1
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
transitivePeerDependencies:
- supports-color
- vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0):
+ vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.5
@@ -12103,7 +12109,7 @@ snapshots:
jiti: 2.7.0
sass: 1.105.1
terser: 5.49.2
- tsx: 4.23.10
+ tsx: 4.23.15
yaml: 2.9.0
vue-bundle-renderer@2.3.1:
@@ -12119,10 +12125,10 @@ snapshots:
vue-devtools-stub@0.1.0: {}
- vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
+ vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)):
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
- eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ debug: 4.4.3
+ eslint: 10.8.0(jiti@2.7.0)
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
@@ -12131,7 +12137,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
+ vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
dependencies:
'@babel/generator': 8.0.0
'@vue-macros/common': 3.1.4(vue@3.5.41(typescript@6.0.3))
@@ -12148,14 +12154,14 @@ snapshots:
picomatch: 4.0.5
scule: 1.3.0
tinyglobby: 0.2.17
- unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
vue: 3.5.41(typescript@6.0.3)
yaml: 2.9.0
optionalDependencies:
'@vue/compiler-sfc': 3.5.41
pinia: 3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
- vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)
+ vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
transitivePeerDependencies:
- '@farmfe/core'
- '@rspack/core'
@@ -12176,9 +12182,9 @@ snapshots:
optionalDependencies:
typescript: 6.0.3
- vuetify-nuxt-module@0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
+ vuetify-nuxt-module@0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
dependencies:
- '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0)))
+ '@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
defu: 6.1.7
destr: 2.0.5
local-pkg: 1.2.1
@@ -12186,10 +12192,10 @@ snapshots:
perfect-debounce: 1.0.0
semver: 7.8.5
ufo: 1.6.4
- unconfig: 0.6.1(supports-color@10.2.2)
+ unconfig: 0.6.1
upath: 2.0.1
- vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
+ vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
transitivePeerDependencies:
- magic-string
- magicast
@@ -12202,20 +12208,20 @@ snapshots:
- vue
- webpack-plugin-vuetify
- vuetify@4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
+ vuetify@4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
dependencies:
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
- vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- vuetify@4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
+ vuetify@4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
dependencies:
'@vuetify/v0': 1.2.3(vue@3.5.41(typescript@6.0.3))
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
- vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.10)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
transitivePeerDependencies:
- '@adobe/leonardo-contrast-colors'
- '@ant-design/colors'
From 8f44d83091deac7a8c6491fb3c5fa85f1ed08a53 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2026 11:09:29 +0000
Subject: [PATCH 86/88] fix(deps): bump devalue from 5.9.2 to 5.9.4 in /web
(#1017)
Bumps [devalue](https://github.com/sveltejs/devalue) from 5.9.2 to 5.9.4.
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/devalue/compare/v5.9.2...v5.9.4)
---
updated-dependencies:
- dependency-name: devalue
dependency-version: 5.9.4
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
web/pnpm-lock.yaml | 588 +++++++++++++++++++++++----------------------
1 file changed, 298 insertions(+), 290 deletions(-)
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 597ec35f9..83847691b 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -43,7 +43,7 @@ importers:
version: 2.31.0
nuxt:
specifier: ^4.5.1
- version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ version: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))
@@ -61,7 +61,7 @@ importers:
version: 1.105.1
v-phone-input:
specifier: ^7.0.1
- version: 7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ version: 7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
vue:
specifier: ^3.5.34
version: 3.5.41(typescript@6.0.3)
@@ -73,7 +73,7 @@ importers:
version: 5.2.0(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
vuetify:
specifier: ^4.0.7
- version: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ version: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
devDependencies:
'@commitlint/cli':
specifier: ^21.0.2
@@ -83,19 +83,19 @@ importers:
version: 21.2.0
'@nuxt/eslint':
specifier: ^1.16.0
- version: 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ version: 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
'@nuxtjs/seo':
specifier: ^5.3.2
- version: 5.3.10(90367d1c4564690123e38c844c310bb8)
+ version: 5.3.10(ec03e3381d8c0e355227b79f6a6d4d05)
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
eslint:
specifier: ^10.5.0
- version: 10.8.0(jiti@2.7.0)
+ version: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-config-prettier:
specifier: ^10.1.8
- version: 10.1.8(eslint@10.8.0(jiti@2.7.0))
+ version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -107,13 +107,13 @@ importers:
version: 1.8.1
stylelint:
specifier: ^17.15.0
- version: 17.15.0(typescript@6.0.3)
+ version: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
stylelint-config-recommended-vue:
specifier: ^1.6.1
- version: 1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3))
+ version: 1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
stylelint-config-standard:
specifier: ^40.0.0
- version: 40.0.0(stylelint@17.15.0(typescript@6.0.3))
+ version: 40.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
tsx:
specifier: ^4.23.15
version: 4.23.15
@@ -122,7 +122,7 @@ importers:
version: 6.0.3
vuetify-nuxt-module:
specifier: ^0.19.5
- version: 0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ version: 0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
packages:
@@ -3063,8 +3063,8 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
- devalue@5.9.2:
- resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==}
+ devalue@5.9.4:
+ resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==}
devframe@0.8.2:
resolution: {integrity: sha512-oodQXEQnrvufA1so/E058Z3SG3bk9AlUE5+NIAsyx5ZJH9P/oX6WMxjC0bs5PPwtKbS7pBHjxja2AD8EwGE8/Q==}
@@ -5939,20 +5939,20 @@ snapshots:
'@babel/compat-data@7.29.7': {}
- '@babel/core@7.29.7':
+ '@babel/core@7.29.7(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
- '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -5988,41 +5988,41 @@ snapshots:
lru-cache: 5.1.1
semver: 6.3.1
- '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
+ '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-annotate-as-pure': 7.29.7
- '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
'@babel/helper-optimise-call-expression': 7.29.7
- '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
- '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/helper-globals@7.29.7': {}
- '@babel/helper-member-expression-to-functions@7.29.7':
+ '@babel/helper-member-expression-to-functions@7.29.7(supports-color@10.2.2)':
dependencies:
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-imports@7.29.7':
+ '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)':
dependencies:
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
- '@babel/core': 7.29.7
- '@babel/helper-module-imports': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
'@babel/helper-validator-identifier': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -6032,18 +6032,18 @@ snapshots:
'@babel/helper-plugin-utils@7.29.7': {}
- '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
+ '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
- '@babel/core': 7.29.7
- '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
'@babel/helper-optimise-call-expression': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
- '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@10.2.2)':
dependencies:
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -6071,24 +6071,24 @@ snapshots:
dependencies:
'@babel/types': 8.0.4
- '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
+ '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
dependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
+ '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
dependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)':
+ '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/helper-annotate-as-pure': 7.29.7
- '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
- '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
transitivePeerDependencies:
- supports-color
@@ -6098,7 +6098,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
- '@babel/traverse@7.29.8':
+ '@babel/traverse@7.29.8(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
@@ -6106,7 +6106,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.8
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -6529,23 +6529,23 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
- '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0))':
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
dependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/compat@2.1.0(eslint@10.8.0(jiti@2.7.0))':
+ '@eslint/compat@2.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
dependencies:
'@eslint/core': 1.2.1
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
- '@eslint/config-array@0.23.5':
+ '@eslint/config-array@0.23.5(supports-color@10.2.2)':
dependencies:
'@eslint/object-schema': 3.0.5
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 10.2.6
transitivePeerDependencies:
- supports-color
@@ -6558,13 +6558,13 @@ snapshots:
dependencies:
'@eslint/core': 1.2.1
- '@eslint/config-inspector@3.2.0(eslint@10.8.0(jiti@2.7.0))(srvx@0.11.22)':
+ '@eslint/config-inspector@3.2.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(srvx@0.11.22)':
dependencies:
ansis: 4.3.1
cac: 7.0.0
chokidar: 5.0.0
devframe: 0.8.2(cac@7.0.0)(srvx@0.11.22)
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
jiti: 2.7.0
tinyglobby: 0.2.17
transitivePeerDependencies:
@@ -6581,9 +6581,9 @@ snapshots:
mdn-data: 2.29.0
source-map-js: 1.2.1
- '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0))':
+ '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
'@eslint/object-schema@3.0.5': {}
@@ -7096,19 +7096,19 @@ snapshots:
'@kurkle/color@0.3.4': {}
- '@kwsites/file-exists@1.1.1':
+ '@kwsites/file-exists@1.1.1(supports-color@10.2.2)':
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
'@kwsites/promise-deferred@1.1.1': {}
- '@mapbox/node-pre-gyp@2.0.3':
+ '@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)':
dependencies:
consola: 3.4.2
detect-libc: 2.1.2
- https-proxy-agent: 7.0.6
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
node-fetch: 2.7.0
nopt: 8.1.0
semver: 7.8.5
@@ -7150,7 +7150,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
- '@nuxt/cli@3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)':
+ '@nuxt/cli@3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)(supports-color@10.2.2)':
dependencies:
'@bomb.sh/tab': 0.0.19(cac@7.0.0)(citty@0.2.2)
'@clack/prompts': 1.7.0
@@ -7158,7 +7158,7 @@ snapshots:
citty: 0.2.2
confbox: 0.2.4
consola: 3.4.2
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
defu: 6.1.7
exsolve: 1.1.1
fuse.js: 7.5.0
@@ -7226,7 +7226,7 @@ snapshots:
pkg-types: 2.3.1
semver: 7.8.5
- '@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@nuxt/devtools@3.4.1(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
'@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
'@nuxt/devtools-wizard': 3.4.1
@@ -7252,11 +7252,11 @@ snapshots:
perfect-debounce: 2.1.0
pkg-types: 2.3.1
semver: 7.8.5
- simple-git: 3.36.0
+ simple-git: 3.36.0(supports-color@10.2.2)
sirv: 3.0.2
structured-clone-es: 2.0.1
tinyglobby: 0.2.17
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vite-plugin-inspect: 11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
vite-plugin-vue-tracer: 1.4.0(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
@@ -7291,30 +7291,30 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@nuxt/eslint-config@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@antfu/install-pkg': 2.0.1
'@clack/prompts': 1.7.0
- '@eslint/js': 10.0.1(eslint@10.8.0(jiti@2.7.0))
- '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0))
- '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)
- eslint-config-flat-gitignore: 2.3.0(eslint@10.8.0(jiti@2.7.0))
+ '@eslint/js': 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
+ eslint-config-flat-gitignore: 2.3.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
eslint-flat-config-utils: 3.2.0
- eslint-merge-processors: 2.0.0(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-import-lite: 0.6.0(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-jsdoc: 63.3.3(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-regexp: 3.1.1(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-unicorn: 73.0.0(eslint@10.8.0(jiti@2.7.0))
- eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)))
- eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))
+ eslint-merge-processors: 2.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-plugin-import-lite: 0.6.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
+ eslint-plugin-jsdoc: 63.3.3(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
+ eslint-plugin-regexp: 3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-plugin-unicorn: 73.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint-plugin-vue: 10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))
+ eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
globals: 17.9.0
local-pkg: 1.2.1
pathe: 2.0.3
- vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0))
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
transitivePeerDependencies:
- '@typescript-eslint/utils'
- '@vue/compiler-sfc'
@@ -7322,26 +7322,26 @@ snapshots:
- supports-color
- typescript
- '@nuxt/eslint-plugin@1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@nuxt/eslint-plugin@1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
- typescript
- '@nuxt/eslint@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
+ '@nuxt/eslint@1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(srvx@0.11.22)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))':
dependencies:
- '@eslint/config-inspector': 3.2.0(eslint@10.8.0(jiti@2.7.0))(srvx@0.11.22)
+ '@eslint/config-inspector': 3.2.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(srvx@0.11.22)
'@nuxt/devtools-kit': 3.4.1(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
- '@nuxt/eslint-config': 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@nuxt/eslint-config': 1.17.0(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@nuxt/eslint-plugin': 1.17.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
chokidar: 5.0.0
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-flat-config-utils: 3.2.0
- eslint-typegen: 2.3.1(eslint@10.8.0(jiti@2.7.0))
+ eslint-typegen: 2.3.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
find-up: 8.0.0
get-port-please: 3.2.0
mlly: 1.8.2
@@ -7427,7 +7427,7 @@ snapshots:
- rolldown
- unplugin
- '@nuxt/nitro-server@4.5.2(6be6cc826f211ead8f9963a4117b341a)':
+ '@nuxt/nitro-server@4.5.2(e0438e77a78f708e769b6ace8a1a407a)':
dependencies:
'@nuxt/devalue': 2.0.2
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
@@ -7436,7 +7436,7 @@ snapshots:
consola: 3.4.2
defu: 6.1.7
destr: 2.0.5
- devalue: 5.9.2
+ devalue: 5.9.4
errx: 0.1.2
escape-string-regexp: 5.0.0
exsolve: 1.1.1
@@ -7444,9 +7444,9 @@ snapshots:
impound: 1.1.6(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
klona: 2.0.6
mocked-exports: 0.1.1
- nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
nostics: 1.2.0
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
ohash: 2.0.11
pathe: 2.0.3
@@ -7454,12 +7454,12 @@ snapshots:
std-env: 4.2.0
ufo: 1.6.4
unctx: 3.0.0(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
vue: 3.5.41(typescript@6.0.3)
vue-bundle-renderer: 2.3.1
vue-devtools-stub: 0.1.0
optionalDependencies:
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7531,11 +7531,11 @@ snapshots:
rc9: 3.0.1
std-env: 4.2.0
- '@nuxt/vite-builder@4.5.2(08c1d6837a056862854e60ca899e0fc8)':
+ '@nuxt/vite-builder@4.5.2(23c1c4894e9a84251f3eb81eaa91b13f)':
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
- '@vitejs/plugin-vue-jsx': 5.1.6(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
autoprefixer: 10.5.4(postcss@8.5.26)
consola: 3.4.2
cssnano: 8.0.4(postcss@8.5.26)
@@ -7549,7 +7549,7 @@ snapshots:
knitwork: 1.3.0
mlly: 1.8.2
mocked-exports: 0.1.1
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
pathe: 2.0.3
pkg-types: 2.3.1
@@ -7559,19 +7559,23 @@ snapshots:
std-env: 4.2.0
ufo: 1.6.4
unenv: 2.0.0-rc.24
+ unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vite-node: 6.0.0(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
- vite-plugin-checker: 0.14.5(eslint@10.8.0(jiti@2.7.0))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ vite-plugin-checker: 0.14.5(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
vue: 3.5.41(typescript@6.0.3)
vue-bundle-renderer: 2.3.1
optionalDependencies:
- '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
rolldown: 1.2.3
rollup-plugin-visualizer: 7.0.1(rolldown@1.2.3)(rollup@4.62.4)
transitivePeerDependencies:
- '@biomejs/biome'
+ - '@farmfe/core'
+ - '@rspack/core'
- '@types/node'
- '@vitejs/devtools'
+ - bun-types-no-globals
- esbuild
- eslint
- less
@@ -7581,6 +7585,7 @@ snapshots:
- optionator
- oxc-parser
- oxlint
+ - rollup
- sass
- sass-embedded
- stylelint
@@ -7590,8 +7595,9 @@ snapshots:
- terser
- tsx
- typescript
- - unplugin
+ - unloader
- vue-tsc
+ - webpack
- yaml
'@nuxtjs/google-fonts@3.2.0(magicast@0.5.4)':
@@ -7602,15 +7608,15 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxtjs/robots@6.1.3(7f089dda3677d5e76f6c9e5335d8969d)':
+ '@nuxtjs/robots@6.1.3(598adebc3889a4cdefb637234185cc59)':
dependencies:
'@fingerprintjs/botd': 2.0.0
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
consola: 3.4.2
defu: 6.1.7
h3: 1.15.11
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
pathe: 2.0.3
pkg-types: 2.3.1
ufo: 1.6.4
@@ -7625,18 +7631,18 @@ snapshots:
- vite
- vue
- '@nuxtjs/seo@5.3.10(90367d1c4564690123e38c844c310bb8)':
+ '@nuxtjs/seo@5.3.10(ec03e3381d8c0e355227b79f6a6d4d05)':
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
- '@nuxtjs/robots': 6.1.3(7f089dda3677d5e76f6c9e5335d8969d)
- '@nuxtjs/sitemap': 8.3.3(7f089dda3677d5e76f6c9e5335d8969d)
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-link-checker: 5.1.3(ee45fc925654584e4a6f72bf8e386c53)
- nuxt-og-image: 6.7.6(f726c63e583b3eb786f27b68321e6d91)
- nuxt-schema-org: 6.2.8(2d10904abaf8d3b235f35e4fac57727b)
- nuxt-seo-utils: 8.3.3(a89aa3147dcc17dfe4200c84f0407a4d)
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ '@nuxtjs/robots': 6.1.3(598adebc3889a4cdefb637234185cc59)
+ '@nuxtjs/sitemap': 8.3.3(598adebc3889a4cdefb637234185cc59)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-link-checker: 5.1.3(323961d5cc0410dce4f3db9757b0fd7b)
+ nuxt-og-image: 6.7.6(9554561a4779e4e6d5392bacf4a17faf)
+ nuxt-schema-org: 6.2.8(ad4b878d66007fdc3a824dac936e15df)
+ nuxt-seo-utils: 8.3.3(eac2e55e0932747d8b88f1b2684d2ce2)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7695,13 +7701,13 @@ snapshots:
- webpack
- zod
- '@nuxtjs/sitemap@8.3.3(7f089dda3677d5e76f6c9e5335d8969d)':
+ '@nuxtjs/sitemap@8.3.3(598adebc3889a4cdefb637234185cc59)':
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
consola: 3.4.2
defu: 6.1.7
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.1
@@ -8102,11 +8108,11 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0))':
+ '@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
'@typescript-eslint/types': 8.66.0
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@@ -8137,15 +8143,15 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
- '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.66.0
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -8153,23 +8159,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3
- eslint: 10.8.0(jiti@2.7.0)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.66.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
'@typescript-eslint/types': 8.66.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -8183,13 +8189,13 @@ snapshots:
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
- debug: 4.4.3
- eslint: 10.8.0(jiti@2.7.0)
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
@@ -8197,13 +8203,13 @@ snapshots:
'@typescript-eslint/types@8.66.0': {}
- '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.66.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3)
+ '@typescript-eslint/project-service': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
'@typescript-eslint/types': 8.66.0
'@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 10.2.6
semver: 7.8.5
tinyglobby: 0.2.17
@@ -8212,13 +8218,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
- eslint: 10.8.0(jiti@2.7.0)
+ '@typescript-eslint/typescript-estree': 8.66.0(supports-color@10.2.2)(typescript@6.0.3)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -8341,9 +8347,9 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
- '@vercel/nft@1.10.2(rollup@4.62.4)':
+ '@vercel/nft@1.10.2(rollup@4.62.4)(supports-color@10.2.2)':
dependencies:
- '@mapbox/node-pre-gyp': 2.0.3
+ '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2)
'@rollup/pluginutils': 5.4.0(rollup@4.62.4)
acorn: 8.18.0
acorn-import-attributes: 1.9.5(acorn@8.18.0)
@@ -8360,13 +8366,13 @@ snapshots:
- rollup
- supports-color
- '@vitejs/plugin-vue-jsx@5.1.6(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
+ '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))':
dependencies:
- '@babel/core': 7.29.7
- '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
- '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
+ '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@rolldown/pluginutils': 1.0.1
- '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7)
+ '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
transitivePeerDependencies:
@@ -8390,27 +8396,27 @@ snapshots:
'@vue/babel-helper-vue-transform-on@2.0.1': {}
- '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)':
+ '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
- '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
- '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
'@babel/types': 7.29.8
'@vue/babel-helper-vue-transform-on': 2.0.1
- '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7)
+ '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@vue/shared': 3.5.41
optionalDependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
- '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)':
+ '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
- '@babel/core': 7.29.7
- '@babel/helper-module-imports': 7.29.7
+ '@babel/core': 7.29.7(supports-color@10.2.2)
+ '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
'@babel/helper-plugin-utils': 7.29.7
'@babel/parser': 7.29.8
'@vue/compiler-sfc': 3.5.41
@@ -8512,7 +8518,7 @@ snapshots:
dependencies:
upath: 2.0.1
vue: 3.5.41(typescript@6.0.3)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
'@vuetify/v0@1.2.3(vue@3.5.41(typescript@6.0.3))':
dependencies:
@@ -8785,12 +8791,12 @@ snapshots:
chownr@3.0.0: {}
- chrome-launcher@1.2.1:
+ chrome-launcher@1.2.1(supports-color@10.2.2):
dependencies:
'@types/node': 26.1.2
escape-string-regexp: 4.0.0
is-wsl: 2.2.0
- lighthouse-logger: 2.0.2
+ lighthouse-logger: 2.0.2(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -9005,9 +9011,11 @@ snapshots:
db0@0.3.4: {}
- debug@4.4.3:
+ debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
decamelize@1.2.0: {}
@@ -9036,7 +9044,7 @@ snapshots:
detect-libc@2.1.2: {}
- devalue@5.9.2: {}
+ devalue@5.9.4: {}
devframe@0.8.2(cac@7.0.0)(srvx@0.11.22):
dependencies:
@@ -9188,14 +9196,14 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-config-flat-gitignore@2.3.0(eslint@10.8.0(jiti@2.7.0)):
+ eslint-config-flat-gitignore@2.3.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- '@eslint/compat': 2.1.0(eslint@10.8.0(jiti@2.7.0))
- eslint: 10.8.0(jiti@2.7.0)
+ '@eslint/compat': 2.1.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
- eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)):
+ eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-flat-config-utils@3.2.0:
dependencies:
@@ -9209,20 +9217,20 @@ snapshots:
optionalDependencies:
unrs-resolver: 1.12.2
- eslint-merge-processors@2.0.0(eslint@10.8.0(jiti@2.7.0)):
+ eslint-merge-processors@2.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
- eslint-plugin-import-lite@0.6.0(eslint@10.8.0(jiti@2.7.0)):
+ eslint-plugin-import-lite@0.6.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
- eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)):
+ eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
dependencies:
'@typescript-eslint/types': 8.66.0
comment-parser: 1.4.7
- debug: 4.4.3
- eslint: 10.8.0(jiti@2.7.0)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-import-context: 0.1.9(unrs-resolver@1.12.2)
is-glob: 4.0.3
minimatch: 10.2.6
@@ -9230,19 +9238,19 @@ snapshots:
stable-hash-x: 0.2.0
unrs-resolver: 1.12.2
optionalDependencies:
- '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
transitivePeerDependencies:
- supports-color
- eslint-plugin-jsdoc@63.3.3(eslint@10.8.0(jiti@2.7.0)):
+ eslint-plugin-jsdoc@63.3.3(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
dependencies:
'@es-joy/jsdoccomment': 0.91.0
'@es-joy/resolve.exports': 1.2.0
are-docs-informative: 0.0.2
comment-parser: 1.4.7
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
escape-string-regexp: 4.0.0
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
espree: 11.2.0
esquery: 1.7.0
html-entities: 2.6.0
@@ -9254,20 +9262,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-regexp@3.1.1(eslint@10.8.0(jiti@2.7.0)):
+ eslint-plugin-regexp@3.1.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
'@eslint-community/regexpp': 4.12.2
comment-parser: 1.4.7
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
jsdoc-type-pratt-parser: 7.3.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
- eslint-plugin-unicorn@73.0.0(eslint@10.8.0(jiti@2.7.0)):
+ eslint-plugin-unicorn@73.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
'@eslint/css-tree': 4.0.5
browserslist: 4.28.7
change-case: 5.4.4
@@ -9275,7 +9283,7 @@ snapshots:
core-js-compat: 3.50.0
detect-indent: 7.0.2
entities: 4.5.0
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
find-up-simple: 1.0.1
globals: 17.9.0
indent-string: 5.0.0
@@ -9289,24 +9297,24 @@ snapshots:
strip-indent: 4.1.1
yaml: 2.9.0
- eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0))):
+ eslint-plugin-vue@10.10.0(@stylistic/eslint-plugin@5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)))(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
- eslint: 10.8.0(jiti@2.7.0)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.5
semver: 7.8.5
- vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0))
+ vue-eslint-parser: 10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
xml-name-validator: 5.0.0
optionalDependencies:
- '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0))
- '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0))(typescript@6.0.3)
+ '@stylistic/eslint-plugin': 5.10.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)):
+ eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.41)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
'@vue/compiler-sfc': 3.5.41
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-scope@9.1.2:
dependencies:
@@ -9315,9 +9323,9 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
- eslint-typegen@2.3.1(eslint@10.8.0(jiti@2.7.0)):
+ eslint-typegen@2.3.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2)):
dependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
json-schema-to-typescript-lite: 15.0.0
ohash: 2.0.11
@@ -9327,11 +9335,11 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.8.0(jiti@2.7.0):
+ eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))
'@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.23.5
+ '@eslint/config-array': 0.23.5(supports-color@10.2.2)
'@eslint/config-helpers': 0.7.0
'@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.7.2
@@ -9341,7 +9349,7 @@ snapshots:
'@types/estree': 1.0.9
ajv: 6.15.0
cross-spawn: 7.0.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
escape-string-regexp: 4.0.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
@@ -9731,10 +9739,10 @@ snapshots:
http-shutdown@1.2.2: {}
- https-proxy-agent@7.0.6:
+ https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -9769,10 +9777,10 @@ snapshots:
import-meta-resolve@4.2.0: {}
- importx@0.5.2:
+ importx@0.5.2(supports-color@10.2.2):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.12)
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
esbuild: 0.25.12
jiti: 2.7.0
pathe: 2.0.3
@@ -9810,11 +9818,11 @@ snapshots:
ini@6.0.0: {}
- ioredis@5.11.1:
+ ioredis@5.11.1(supports-color@10.2.2):
dependencies:
'@ioredis/commands': 1.10.0
cluster-key-slot: 1.1.1
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
denque: 2.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
@@ -9971,9 +9979,9 @@ snapshots:
libphonenumber-js@1.13.10: {}
- lighthouse-logger@2.0.2:
+ lighthouse-logger@2.0.2(supports-color@10.2.2):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
marky: 1.3.0
transitivePeerDependencies:
- supports-color
@@ -10200,7 +10208,7 @@ snapshots:
natural-compare@1.4.0: {}
- nitropack@2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
+ nitropack@2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.2
'@rollup/plugin-alias': 6.0.0(rollup@4.62.4)
@@ -10210,7 +10218,7 @@ snapshots:
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.4)
'@rollup/plugin-replace': 6.0.3(rollup@4.62.4)
'@rollup/plugin-terser': 1.0.0(rollup@4.62.4)
- '@vercel/nft': 1.10.2(rollup@4.62.4)
+ '@vercel/nft': 1.10.2(rollup@4.62.4)(supports-color@10.2.2)
archiver: 7.0.1
c12: 3.3.4(magicast@0.5.4)
chokidar: 5.0.0
@@ -10234,7 +10242,7 @@ snapshots:
h3: 1.15.11
hookable: 5.5.3
httpxy: 0.5.5
- ioredis: 5.11.1
+ ioredis: 5.11.1(supports-color@10.2.2)
jiti: 2.7.0
klona: 2.0.6
knitwork: 1.3.0
@@ -10257,7 +10265,7 @@ snapshots:
scule: 1.3.0
semver: 7.8.5
serve-placeholder: 2.0.2
- serve-static: 2.2.1
+ serve-static: 2.2.1(supports-color@10.2.2)
source-map: 0.7.6
std-env: 4.2.0
ufo: 1.6.4
@@ -10267,7 +10275,7 @@ snapshots:
unenv: 2.0.0-rc.24
unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
unplugin-utils: 0.3.2
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
untyped: 2.0.0
unwasm: 0.5.3
youch: 4.1.1
@@ -10350,7 +10358,7 @@ snapshots:
dependencies:
boolbase: 1.0.0
- nuxt-link-checker@5.1.3(ee45fc925654584e4a6f72bf8e386c53):
+ nuxt-link-checker@5.1.3(323961d5cc0410dce4f3db9757b0fd7b):
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@vueuse/core': 14.4.0(vue@3.5.41(typescript@6.0.3))
@@ -10359,9 +10367,9 @@ snapshots:
fuse.js: 7.5.0
h3: 1.15.11
magic-string: 1.1.0
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
ofetch: 1.5.1
pathe: 2.0.3
pkg-types: 2.3.1
@@ -10369,7 +10377,7 @@ snapshots:
site-config-stack: 4.2.0(vue@3.5.41(typescript@6.0.3))
ufo: 1.6.4
ultrahtml: 1.7.0
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -10399,24 +10407,24 @@ snapshots:
- vue
- zod
- nuxt-og-image@6.7.6(f726c63e583b3eb786f27b68321e6d91):
+ nuxt-og-image@6.7.6(9554561a4779e4e6d5392bacf4a17faf):
dependencies:
'@clack/prompts': 1.7.0
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
'@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@vue/compiler-sfc': 3.5.41
- chrome-launcher: 1.2.1
+ chrome-launcher: 1.2.1(supports-color@10.2.2)
consola: 3.4.2
defu: 6.1.7
- devalue: 5.9.2
+ devalue: 5.9.4
exsolve: 1.1.1
fnv1a-64: 0.1.2
lightningcss: 1.33.0
magic-string: 1.1.0
magicast: 0.5.4
mocked-exports: 0.1.1
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
nypm: 0.6.9
object-identity: 0.2.3
ofetch: 1.5.1
@@ -10432,9 +10440,9 @@ snapshots:
ufo: 1.6.4
ultrahtml: 1.7.0
unplugin: 3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
- unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
optionalDependencies:
- nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
+ nitropack: 2.13.4(@parcel/watcher@2.6.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
transitivePeerDependencies:
- '@farmfe/core'
- '@nuxt/schema'
@@ -10451,12 +10459,12 @@ snapshots:
- vue
- webpack
- nuxt-schema-org@6.2.8(2d10904abaf8d3b235f35e4fac57727b):
+ nuxt-schema-org@6.2.8(ad4b878d66007fdc3a824dac936e15df):
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
defu: 6.1.7
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
pkg-types: 2.3.1
ufo: 1.6.4
optionalDependencies:
@@ -10473,7 +10481,7 @@ snapshots:
- vite
- vue
- nuxt-seo-utils@8.3.3(a89aa3147dcc17dfe4200c84f0407a4d):
+ nuxt-seo-utils@8.3.3(eac2e55e0932747d8b88f1b2684d2ce2):
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
citty: 0.2.2
@@ -10482,9 +10490,9 @@ snapshots:
escape-string-regexp: 5.0.0
exsolve: 1.1.1
image-size: 2.0.2
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
pathe: 2.0.3
pkg-types: 2.3.1
scule: 1.3.0
@@ -10523,7 +10531,7 @@ snapshots:
- unplugin
- vue
- nuxt-site-config@4.2.0(7f089dda3677d5e76f6c9e5335d8969d):
+ nuxt-site-config@4.2.0(598adebc3889a4cdefb637234185cc59):
dependencies:
'@nuxt/devalue': 2.0.2
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
@@ -10531,7 +10539,7 @@ snapshots:
defu: 6.1.7
h3: 1.15.11
nuxt-site-config-kit: 4.2.0(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3))
- nuxtseo-shared: 5.3.10(e7e317c556a96caaa3d5be917284fb38)
+ nuxtseo-shared: 5.3.10(9d8473589308f64eb416952c24f546e0)
pathe: 2.0.3
pkg-types: 2.3.1
site-config-stack: 4.2.0(vue@3.5.41(typescript@6.0.3))
@@ -10548,16 +10556,16 @@ snapshots:
- vite
- zod
- nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0):
+ nuxt@4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0):
dependencies:
'@dxup/nuxt': 0.5.6(esbuild@0.25.12)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
- '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)
- '@nuxt/devtools': 3.4.1(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
+ '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.2)(@parcel/watcher@2.6.0)(cac@7.0.0)(magicast@0.5.4)(supports-color@10.2.2)
+ '@nuxt/devtools': 3.4.1(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.1.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
- '@nuxt/nitro-server': 4.5.2(6be6cc826f211ead8f9963a4117b341a)
+ '@nuxt/nitro-server': 4.5.2(e0438e77a78f708e769b6ace8a1a407a)
'@nuxt/schema': 4.5.2
'@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))
- '@nuxt/vite-builder': 4.5.2(08c1d6837a056862854e60ca899e0fc8)
+ '@nuxt/vite-builder': 4.5.2(23c1c4894e9a84251f3eb81eaa91b13f)
'@unhead/vue': 3.3.1(@oxc-project/types@0.143.0)(esbuild@0.25.12)(lightningcss@1.33.0)(oxc-parser@0.142.0)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))
'@vue/shared': 3.5.41
chokidar: 5.0.0
@@ -10565,7 +10573,7 @@ snapshots:
consola: 3.4.2
cookie-es: 3.1.1
defu: 6.1.7
- devalue: 5.9.2
+ devalue: 5.9.4
errx: 0.1.2
escape-string-regexp: 5.0.0
exsolve: 1.1.1
@@ -10690,7 +10698,7 @@ snapshots:
- xml2js
- yaml
- nuxtseo-shared@5.3.10(e7e317c556a96caaa3d5be917284fb38):
+ nuxtseo-shared@5.3.10(9d8473589308f64eb416952c24f546e0):
dependencies:
'@clack/prompts': 1.7.0
'@nuxt/devtools-kit': 4.0.0-alpha.7(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))
@@ -10699,7 +10707,7 @@ snapshots:
birpc: 4.0.0
consola: 3.4.2
defu: 6.1.7
- nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(typescript@6.0.3))(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
+ nuxt: 4.5.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.143.0)(@parcel/watcher@2.6.0)(@types/node@26.1.2)(@vue/compiler-sfc@3.5.41)(cac@7.0.0)(db0@0.3.4)(esbuild@0.25.12)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(ioredis@5.11.1(supports-color@10.2.2))(lightningcss@1.33.0)(magicast@0.5.4)(meow@14.1.0)(optionator@0.9.4)(oxc-parser@0.142.0)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)))(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.4))(rollup@4.62.4)(sass@1.105.1)(srvx@0.11.22)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)(terser@5.49.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(yaml@2.9.0)
nypm: 0.6.9
ofetch: 1.5.1
pathe: 2.0.3
@@ -10710,7 +10718,7 @@ snapshots:
ufo: 1.6.4
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
- nuxt-site-config: 4.2.0(7f089dda3677d5e76f6c9e5335d8969d)
+ nuxt-site-config: 4.2.0(598adebc3889a4cdefb637234185cc59)
transitivePeerDependencies:
- magic-string
- magicast
@@ -11328,9 +11336,9 @@ snapshots:
semver@7.8.5: {}
- send@1.2.1:
+ send@1.2.1(supports-color@10.2.2):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -11352,12 +11360,12 @@ snapshots:
dependencies:
defu: 6.1.7
- serve-static@2.2.1:
+ serve-static@2.2.1(supports-color@10.2.2):
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
- send: 1.2.1
+ send: 1.2.1(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -11411,13 +11419,13 @@ snapshots:
signal-exit@4.1.0: {}
- simple-git@3.36.0:
+ simple-git@3.36.0(supports-color@10.2.2):
dependencies:
- '@kwsites/file-exists': 1.1.1
+ '@kwsites/file-exists': 1.1.1(supports-color@10.2.2)
'@kwsites/promise-deferred': 1.1.1
'@simple-git/args-pathspec': 1.0.3
'@simple-git/argv-parser': 1.1.1
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -11552,29 +11560,29 @@ snapshots:
postcss: 8.5.26
postcss-selector-parser: 7.1.5
- stylelint-config-html@2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3)):
+ stylelint-config-html@2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
dependencies:
postcss-html: 1.8.1
- stylelint: 17.15.0(typescript@6.0.3)
+ stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
- stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3)):
+ stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
dependencies:
postcss-html: 1.8.1
semver: 7.8.5
- stylelint: 17.15.0(typescript@6.0.3)
- stylelint-config-html: 2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(typescript@6.0.3))
- stylelint-config-recommended: 18.0.0(stylelint@17.15.0(typescript@6.0.3))
+ stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint-config-html: 2.0.0(postcss-html@1.8.1)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
+ stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
- stylelint-config-recommended@18.0.0(stylelint@17.15.0(typescript@6.0.3)):
+ stylelint-config-recommended@18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
dependencies:
- stylelint: 17.15.0(typescript@6.0.3)
+ stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
- stylelint-config-standard@40.0.0(stylelint@17.15.0(typescript@6.0.3)):
+ stylelint-config-standard@40.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3)):
dependencies:
- stylelint: 17.15.0(typescript@6.0.3)
- stylelint-config-recommended: 18.0.0(stylelint@17.15.0(typescript@6.0.3))
+ stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))
- stylelint@17.15.0(typescript@6.0.3):
+ stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3):
dependencies:
'@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
@@ -11587,7 +11595,7 @@ snapshots:
cosmiconfig: 9.0.2(typescript@6.0.3)
css-functions-list: 3.3.3
css-tree: 3.2.1
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
fast-glob: 3.3.3
fastest-levenshtein: 1.0.16
file-entry-cache: 11.1.5
@@ -11761,11 +11769,11 @@ snapshots:
ultrahtml@1.7.0: {}
- unconfig@0.6.1:
+ unconfig@0.6.1(supports-color@10.2.2):
dependencies:
'@antfu/utils': 8.1.1
defu: 6.1.7
- importx: 0.5.2
+ importx: 0.5.2(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -11937,7 +11945,7 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.12.2
'@unrs/resolver-binding-win32-x64-msvc': 1.12.2
- unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1):
+ unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -11949,7 +11957,7 @@ snapshots:
ufo: 1.6.4
optionalDependencies:
db0: 0.3.4
- ioredis: 5.11.1
+ ioredis: 5.11.1(supports-color@10.2.2)
untun@0.2.2: {}
@@ -11986,14 +11994,14 @@ snapshots:
util-deprecate@1.0.2: {}
- v-phone-input@7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
+ v-phone-input@7.0.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
dependencies:
awesome-phonenumber: 7.8.0
countries-list: 3.4.1
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
flag-icons: 7.5.0
- vuetify: 4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
world-flags-sprite: 0.0.2
transitivePeerDependencies:
- '@adobe/leonardo-contrast-colors'
@@ -12042,7 +12050,7 @@ snapshots:
- tsx
- yaml
- vite-plugin-checker@0.14.5(eslint@10.8.0(jiti@2.7.0))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
+ vite-plugin-checker@0.14.5(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.15.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
dependencies:
'@babel/code-frame': 7.29.7
chokidar: 5.0.0
@@ -12053,10 +12061,10 @@ snapshots:
tiny-invariant: 1.3.3
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
optionalDependencies:
- eslint: 10.8.0(jiti@2.7.0)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
meow: 14.1.0
optionator: 0.9.4
- stylelint: 17.15.0(typescript@6.0.3)
+ stylelint: 17.15.0(supports-color@10.2.2)(typescript@6.0.3)
typescript: 6.0.3
vite-plugin-inspect@11.4.1(@nuxt/kit@4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)):
@@ -12084,14 +12092,14 @@ snapshots:
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
- vite-plugin-vuetify@2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8):
+ vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8):
dependencies:
'@vuetify/loader-shared': 2.1.2(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
upath: 2.0.1
vite: 8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)
vue: 3.5.41(typescript@6.0.3)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
transitivePeerDependencies:
- supports-color
@@ -12125,10 +12133,10 @@ snapshots:
vue-devtools-stub@0.1.0: {}
- vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)):
+ vue-eslint-parser@10.4.1(eslint@10.8.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2):
dependencies:
- debug: 4.4.3
- eslint: 10.8.0(jiti@2.7.0)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.8.0(jiti@2.7.0)(supports-color@10.2.2)
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
@@ -12182,7 +12190,7 @@ snapshots:
optionalDependencies:
typescript: 6.0.3
- vuetify-nuxt-module@0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
+ vuetify-nuxt-module@0.19.5(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(supports-color@10.2.2)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)):
dependencies:
'@nuxt/kit': 4.5.2(magic-string@1.1.0)(magicast@0.5.4)(oxc-parser@0.142.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.25.12)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0)))
defu: 6.1.7
@@ -12192,10 +12200,10 @@ snapshots:
perfect-debounce: 1.0.0
semver: 7.8.5
ufo: 1.6.4
- unconfig: 0.6.1
+ unconfig: 0.6.1(supports-color@10.2.2)
upath: 2.0.1
- vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3))
+ vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vuetify: 4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3))
transitivePeerDependencies:
- magic-string
- magicast
@@ -12208,20 +12216,20 @@ snapshots:
- vue
- webpack-plugin-vuetify
- vuetify@4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
+ vuetify@4.1.8(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
dependencies:
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
- vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
- vuetify@4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.41(typescript@6.0.3)):
+ vuetify@4.2.2(typescript@6.0.3)(vite-plugin-vuetify@2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8))(vue@3.5.41(typescript@6.0.3)):
dependencies:
'@vuetify/v0': 1.2.3(vue@3.5.41(typescript@6.0.3))
vue: 3.5.41(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
- vite-plugin-vuetify: 2.1.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
+ vite-plugin-vuetify: 2.1.3(supports-color@10.2.2)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.25.12)(jiti@2.7.0)(sass@1.105.1)(terser@5.49.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))(vuetify@4.1.8)
transitivePeerDependencies:
- '@adobe/leonardo-contrast-colors'
- '@ant-design/colors'
From 00d61b02bd25d3851741734164d1e5deb6021c76 Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Tue, 6 Oct 2026 23:34:09 +0300
Subject: [PATCH 87/88] fix(web): support nested WhatsApp formatting and format
thread previews
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3362f283-a915-49b9-ae89-d16e3c7ecb2a
---
web/app/components/MessageThread.vue | 23 ++++++++++++++++++++++-
web/app/utils/whatsapp-format.ts | 4 +++-
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index 93c77324d..53bd401b0 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -8,6 +8,10 @@ import {
mdiAccount,
} from '@mdi/js'
import type { EntitiesMessageThread } from '~~/shared/types/api'
+import {
+ isUrlNotificationToken,
+ formatWhatsappText,
+} from '~/utils/whatsapp-format'
const threadsStore = useThreadsStore()
const phonesStore = usePhonesStore()
@@ -45,6 +49,19 @@ function onInstallApp() {
})
}
+function isWhatsappFormatted(thread: EntitiesMessageThread): boolean {
+ const phone = phonesStore.phones.find((x) => x.phone_number === thread.owner)
+ return isUrlNotificationToken(phone?.fcm_token)
+}
+
+function threadPreviewHtml(thread: EntitiesMessageThread): string {
+ // Links are unwrapped because the preview sits inside a router link.
+ return formatWhatsappText(thread.last_message_content ?? '').replace(
+ /]*>(.*?)<\/a>/g,
+ '$1',
+ )
+}
+
function threadContactName(thread: EntitiesMessageThread): string {
return thread.contact_details?.name?.trim() ?? ''
}
@@ -160,7 +177,11 @@ function threadAvatarInitial(thread: EntitiesMessageThread): string {
}"
style="max-width: 250px"
>
- {{ thread.last_message_content }}
+
+ {{ thread.last_message_content }}
diff --git a/web/app/utils/whatsapp-format.ts b/web/app/utils/whatsapp-format.ts
index c1e4e9e46..202a12355 100644
--- a/web/app/utils/whatsapp-format.ts
+++ b/web/app/utils/whatsapp-format.ts
@@ -23,7 +23,9 @@ function escapeHtml(text: string): string {
// Character class (as a regex source string) that is treated as a valid
// boundary around a formatting marker: whitespace, common punctuation, or
// the start/end of the string.
-const BOUNDARY_CHARS = String.raw`\s.,!?;:'"()\[\]{}-`
+// Other formatting markers and generated tags (<>) also count so that nested
+// formatting such as `_*text*_` works.
+const BOUNDARY_CHARS = String.raw`\s.,!?;:'"()\[\]{}<>*_~-`
function withBoundaries(pattern: string): RegExp {
return new RegExp(
From b4527e8a572eb0d12ca1b75ad5ee2f25a94808ca Mon Sep 17 00:00:00 2001
From: Acho Arnold Ewin
Date: Tue, 6 Oct 2026 23:35:20 +0300
Subject: [PATCH 88/88] chore(web): silence vue/no-v-html for sanitized thread
preview
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3362f283-a915-49b9-ae89-d16e3c7ecb2a
---
web/app/components/MessageThread.vue | 2 ++
1 file changed, 2 insertions(+)
diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue
index 53bd401b0..2a699408a 100644
--- a/web/app/components/MessageThread.vue
+++ b/web/app/components/MessageThread.vue
@@ -177,10 +177,12 @@ function threadAvatarInitial(thread: EntitiesMessageThread): string {
}"
style="max-width: 250px"
>
+
+
{{ thread.last_message_content }}