Callbacks service docs updated #598

Merged
tech merged 1 commits from callbacks-596 into main 2026-03-02 15:28:27 +00:00
77 changed files with 803 additions and 764 deletions

View File

@@ -0,0 +1,33 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type callbackWriteResponse struct {
AccessToken TokenData `json:"accessToken"`
Callbacks []model.Callback `json:"callbacks"`
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"`
}
func Callback(
logger mlogger.Logger,
callback *model.Callback,
accessToken *TokenData,
generatedSecret string,
created bool,
) http.HandlerFunc {
resp := callbackWriteResponse{
AccessToken: *accessToken,
Callbacks: []model.Callback{*callback},
GeneratedSigningSecret: generatedSecret,
}
if created {
return response.Created(logger, resp)
}
return response.Ok(logger, resp)
}

View File

@@ -42,9 +42,7 @@ func PrepareRefreshToken(
}
token := &model.RefreshToken{
AccountBoundBase: model.AccountBoundBase{
AccountRef: account.GetID(),
},
AccountRef: account.GetID(),
ClientRefreshToken: model.ClientRefreshToken{
SessionIdentifier: *session,
RefreshToken: refreshToken,

View File

@@ -0,0 +1,47 @@
package callbacksimp
import (
"context"
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
var callback model.Callback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
mutation, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, "", true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
if err := a.DB.Create(ctx, *account.GetID(), organizationRef, &callback); err != nil {
return nil, err
}
if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil {
return nil, err
}
return nil, nil
}); err != nil {
a.Logger.Warn("Failed to create callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&callback, accessToken, mutation.Generated, true)
}

View File

@@ -2,7 +2,7 @@ package callbacksimp
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
@@ -16,68 +16,10 @@ import (
"go.uber.org/zap"
)
type callbackWriteResponse struct {
AccessToken sresponse.TokenData `json:"accessToken"`
Callbacks []model.Callback `json:"callbacks"`
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"`
}
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
var callback model.Callback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.DB.Create(r.Context(), *account.GetID(), organizationRef, &callback); err != nil {
a.Logger.Warn("Failed to create callback", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&callback, accessToken, generatedSecret, true)
}
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
var input model.Callback
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
callbackRef := *input.GetID()
if callbackRef.IsZero() {
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback id is required", "id"))
}
var existing model.Callback
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
mergeCallbackMutable(&existing, &input)
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.DB.Update(r.Context(), *account.GetID(), &existing); err != nil {
a.Logger.Warn("Failed to update callback", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&existing, accessToken, generatedSecret, false)
type signingSecretMutation struct {
SetSecretRef string
Clear bool
Generated string
}
func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
@@ -102,10 +44,8 @@ func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, acc
a.Logger.Warn("Failed to rotate callback signing secret", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
callback.RetryPolicy.SecretRef = secretRef
if err := a.DB.Update(r.Context(), *account.GetID(), &callback); err != nil {
a.Logger.Warn("Failed to persist rotated callback secret reference", zap.Error(err))
if err := a.db.SetSigningSecretRef(r.Context(), *account.GetID(), callbackRef, secretRef); err != nil {
a.Logger.Warn("Failed to persist rotated callback signing secret reference", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
@@ -116,29 +56,25 @@ func (a *CallbacksAPI) normalizeAndPrepare(
ctx context.Context,
callback *model.Callback,
organizationRef bson.ObjectID,
existingSecretRef string,
allowSecretGeneration bool,
) (string, error) {
) (signingSecretMutation, error) {
if callback == nil {
return "", merrors.InvalidArgument("callback payload is required")
return signingSecretMutation{}, merrors.InvalidArgument("callback payload is required")
}
if organizationRef.IsZero() {
return "", merrors.InvalidArgument("organization reference is required", "organizationRef")
return signingSecretMutation{}, merrors.InvalidArgument("organization reference is required", "organizationRef")
}
callback.SetOrganizationRef(organizationRef)
callback.Name = strings.TrimSpace(callback.Name)
callback.Description = trimDescription(callback.Description)
callback.ClientID = strings.TrimSpace(callback.ClientID)
if callback.ClientID == "" {
callback.ClientID = organizationRef.Hex()
}
callback.URL = strings.TrimSpace(callback.URL)
if callback.URL == "" {
return "", merrors.InvalidArgument("url is required", "url")
return signingSecretMutation{}, merrors.InvalidArgument("url is required", "url")
}
if err := validateCallbackURL(callback.URL); err != nil {
return "", err
return signingSecretMutation{}, err
}
if callback.Name == "" {
callback.Name = callback.URL
@@ -146,15 +82,15 @@ func (a *CallbacksAPI) normalizeAndPrepare(
status, err := normalizeStatus(callback.Status, a.config.DefaultStatus)
if err != nil {
return "", err
return signingSecretMutation{}, err
}
callback.Status = status
callback.EventTypes = normalizeEventTypes(callback.EventTypes, a.config.DefaultEventTypes)
callback.RetryPolicy.MinDelayMS = defaultInt(callback.RetryPolicy.MinDelayMS, defaultRetryMinDelayMS)
callback.RetryPolicy.MaxDelayMS = defaultInt(callback.RetryPolicy.MaxDelayMS, defaultRetryMaxDelayMS)
if callback.RetryPolicy.MaxDelayMS < callback.RetryPolicy.MinDelayMS {
callback.RetryPolicy.MaxDelayMS = callback.RetryPolicy.MinDelayMS
callback.RetryPolicy.Backoff.MinDelayMS = defaultInt(callback.RetryPolicy.Backoff.MinDelayMS, defaultRetryMinDelayMS)
callback.RetryPolicy.Backoff.MaxDelayMS = defaultInt(callback.RetryPolicy.Backoff.MaxDelayMS, defaultRetryMaxDelayMS)
if callback.RetryPolicy.Backoff.MaxDelayMS < callback.RetryPolicy.Backoff.MinDelayMS {
callback.RetryPolicy.Backoff.MaxDelayMS = callback.RetryPolicy.Backoff.MinDelayMS
}
callback.RetryPolicy.MaxAttempts = defaultInt(callback.RetryPolicy.MaxAttempts, defaultRetryMaxAttempts)
callback.RetryPolicy.RequestTimeoutMS = defaultInt(callback.RetryPolicy.RequestTimeoutMS, defaultRetryRequestTimeoutMS)
@@ -162,36 +98,55 @@ func (a *CallbacksAPI) normalizeAndPrepare(
mode, err := normalizeSigningMode(callback.RetryPolicy.SigningMode)
if err != nil {
return "", err
return signingSecretMutation{}, err
}
callback.RetryPolicy.SigningMode = mode
callback.RetryPolicy.SecretRef = strings.TrimSpace(callback.RetryPolicy.SecretRef)
existingSecretRef = strings.TrimSpace(existingSecretRef)
switch callback.RetryPolicy.SigningMode {
case model.CallbackSigningModeNone:
callback.RetryPolicy.SecretRef = ""
return "", nil
return signingSecretMutation{Clear: existingSecretRef != ""}, nil
case model.CallbackSigningModeHMACSHA256:
if callback.RetryPolicy.SecretRef != "" {
return "", nil
if existingSecretRef != "" {
return signingSecretMutation{SetSecretRef: existingSecretRef}, nil
}
if !allowSecretGeneration {
return "", merrors.InvalidArgument("secretRef is required for hmac_sha256 callbacks", "retryPolicy.secretRef")
return signingSecretMutation{}, merrors.InvalidArgument("signing secret is required for hmac_sha256 callbacks", "retryPolicy.signingMode")
}
if callback.GetID().IsZero() {
callback.SetID(bson.NewObjectID())
}
secretRef, generatedSecret, err := a.secrets.Provision(ctx, organizationRef, *callback.GetID())
if err != nil {
return "", err
return signingSecretMutation{}, err
}
callback.RetryPolicy.SecretRef = secretRef
return generatedSecret, nil
return signingSecretMutation{SetSecretRef: secretRef, Generated: generatedSecret}, nil
default:
return "", merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode")
return signingSecretMutation{}, merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode")
}
}
func (a *CallbacksAPI) applySigningSecretMutation(
ctx context.Context,
accountRef,
callbackRef bson.ObjectID,
mutation signingSecretMutation,
) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
if strings.TrimSpace(mutation.SetSecretRef) != "" {
return a.db.SetSigningSecretRef(ctx, accountRef, callbackRef, mutation.SetSecretRef)
}
if mutation.Clear {
err := a.db.ClearSigningSecretRef(ctx, accountRef, callbackRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
return err
}
}
return nil
}
func (a *CallbacksAPI) callbackResponse(
callback *model.Callback,
accessToken *sresponse.TokenData,
@@ -202,15 +157,7 @@ func (a *CallbacksAPI) callbackResponse(
return response.Internal(a.Logger, a.Name(), merrors.Internal("failed to build callback response"))
}
resp := callbackWriteResponse{
AccessToken: *accessToken,
Callbacks: []model.Callback{*callback},
GeneratedSigningSecret: generatedSecret,
}
if created {
return response.Created(a.Logger, resp)
}
return response.Ok(a.Logger, resp)
return sresponse.Callback(a.Logger, callback, accessToken, generatedSecret, created)
}
func normalizeStatus(raw, fallback model.CallbackStatus) (model.CallbackStatus, error) {
@@ -286,16 +233,17 @@ func normalizeHeaders(headers map[string]string) map[string]string {
}
func mergeCallbackMutable(dst, src *model.Callback) {
dst.OrganizationRef = src.OrganizationRef
dst.Describable = src.Describable
dst.ClientID = src.ClientID
dst.Status = src.Status
dst.URL = src.URL
dst.EventTypes = append([]string(nil), src.EventTypes...)
dst.RetryPolicy = model.CallbackRetryPolicy{
MinDelayMS: src.RetryPolicy.MinDelayMS,
MaxDelayMS: src.RetryPolicy.MaxDelayMS,
Backoff: model.CallbackBackoff{
MinDelayMS: src.RetryPolicy.Backoff.MinDelayMS,
MaxDelayMS: src.RetryPolicy.Backoff.MaxDelayMS,
},
SigningMode: src.RetryPolicy.SigningMode,
SecretRef: src.RetryPolicy.SecretRef,
Headers: normalizeHeaders(src.RetryPolicy.Headers),
MaxAttempts: src.RetryPolicy.MaxAttempts,
RequestTimeoutMS: src.RetryPolicy.RequestTimeoutMS,

View File

@@ -81,7 +81,7 @@ func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signin
}
if isVaultConfigEmpty(cfg.Vault) {
manager.logger.Warn("Callbacks Vault config is not set; secret generation requires explicit secretRef in payloads")
manager.logger.Warn("Callbacks Vault config is not set; hmac signing secret generation is disabled")
ensureSigningSecretMetrics()
return manager, nil
}

View File

@@ -5,6 +5,7 @@ import (
api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/callbacks"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
@@ -16,6 +17,7 @@ import (
type CallbacksAPI struct {
papitemplate.ProtectedAPI[model.Callback]
db callbacks.DB
tf transaction.Factory
secrets signingSecretManager
config callbacksConfig
}
@@ -35,6 +37,7 @@ func CreateAPI(apiCtx eapi.API) (*CallbacksAPI, error) {
res := &CallbacksAPI{
config: newCallbacksConfig(apiCtx.Config().Callbacks),
tf: apiCtx.DBFactory().TransactionFactory(),
}
p, err := papitemplate.CreateAPI(apiCtx, dbFactory, mservice.Organizations, mservice.Callbacks)

View File

@@ -0,0 +1,59 @@
package callbacksimp
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
var input model.Callback
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
callbackRef := *input.GetID()
if callbackRef.IsZero() {
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback ref is required", "id"))
}
var existing model.Callback
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
existingSecretRef, err := a.db.GetSigningSecretRef(r.Context(), *account.GetID(), callbackRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
a.Logger.Warn("Failed to fetch callback signing secret metadata", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
mergeCallbackMutable(&existing, &input)
mutation, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, existingSecretRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
if err := a.DB.Update(ctx, *account.GetID(), &existing); err != nil {
return nil, err
}
if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil {
return nil, err
}
return nil, nil
}); err != nil {
a.Logger.Warn("Failed to update callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&existing, accessToken, mutation.Generated, false)
}

View File

@@ -10,6 +10,7 @@ import (
"sync"
"time"
"github.com/tech/sendico/edge/callbacks/internal/model"
"github.com/tech/sendico/edge/callbacks/internal/signing"
"github.com/tech/sendico/edge/callbacks/internal/storage"
"github.com/tech/sendico/pkg/merrors"
@@ -154,7 +155,7 @@ func (s *service) runWorker(ctx context.Context, workerID string) {
}
}
func (s *service) handleTask(ctx context.Context, workerID string, task *storage.Task) {
func (s *service) handleTask(ctx context.Context, workerID string, task *model.Task) {
started := time.Now()
statusCode := 0
result := "failed"

View File

@@ -4,16 +4,18 @@ import (
"context"
"encoding/json"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Envelope is the canonical incoming event envelope.
type Envelope struct {
EventID string `json:"event_id"`
Type string `json:"type"`
ClientID string `json:"client_id"`
OccurredAt time.Time `json:"occurred_at"`
PublishedAt time.Time `json:"published_at,omitempty"`
Data json.RawMessage `json:"data"`
EventID string `json:"event_id"`
Type string `json:"type"`
OrganizationRef bson.ObjectID `json:"organization_ref"`
OccurredAt time.Time `json:"occurred_at"`
PublishedAt time.Time `json:"published_at,omitempty"`
Data json.RawMessage `json:"data"`
}
// Service parses incoming messages and builds outbound payload bytes.
@@ -24,10 +26,10 @@ type Service interface {
// Payload is the stable outbound JSON body.
type Payload struct {
EventID string `json:"event_id"`
Type string `json:"type"`
ClientID string `json:"client_id"`
OccurredAt string `json:"occurred_at"`
PublishedAt string `json:"published_at,omitempty"`
Data json.RawMessage `json:"data"`
EventID string `json:"event_id"`
Type string `json:"type"`
OrganizationRef bson.ObjectID `json:"organization_ref"`
OccurredAt string `json:"occurred_at"`
PublishedAt string `json:"published_at,omitempty"`
Data json.RawMessage `json:"data"`
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -39,8 +40,8 @@ func (s *parserService) Parse(data []byte) (*Envelope, error) {
if strings.TrimSpace(envelope.Type) == "" {
return nil, merrors.InvalidArgument("type is required", "type")
}
if strings.TrimSpace(envelope.ClientID) == "" {
return nil, merrors.InvalidArgument("client_id is required", "client_id")
if envelope.OrganizationRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("organization_ref is required", "organization_ref")
}
if envelope.OccurredAt.IsZero() {
return nil, merrors.InvalidArgument("occurred_at is required", "occurred_at")
@@ -51,7 +52,6 @@ func (s *parserService) Parse(data []byte) (*Envelope, error) {
envelope.EventID = strings.TrimSpace(envelope.EventID)
envelope.Type = strings.TrimSpace(envelope.Type)
envelope.ClientID = strings.TrimSpace(envelope.ClientID)
envelope.OccurredAt = envelope.OccurredAt.UTC()
if !envelope.PublishedAt.IsZero() {
envelope.PublishedAt = envelope.PublishedAt.UTC()
@@ -66,11 +66,11 @@ func (s *parserService) BuildPayload(_ context.Context, envelope *Envelope) ([]b
}
payload := Payload{
EventID: envelope.EventID,
Type: envelope.Type,
ClientID: envelope.ClientID,
OccurredAt: envelope.OccurredAt.UTC().Format(time.RFC3339Nano),
Data: envelope.Data,
EventID: envelope.EventID,
Type: envelope.Type,
OrganizationRef: envelope.OrganizationRef,
OccurredAt: envelope.OccurredAt.UTC().Format(time.RFC3339Nano),
Data: envelope.Data,
}
if !envelope.PublishedAt.IsZero() {
payload.PublishedAt = envelope.PublishedAt.UTC().Format(time.RFC3339Nano)

View File

@@ -17,6 +17,7 @@ import (
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -178,7 +179,7 @@ func (s *service) handlePaymentStatusUpdated(ctx context.Context, msg *model.Pay
result = ingestResultEmptyPayload
return nil
}
if strings.TrimSpace(msg.EventID) == "" || strings.TrimSpace(msg.ClientID) == "" || msg.OccurredAt.IsZero() {
if strings.TrimSpace(msg.EventID) == "" || msg.Data.OrganizationRef == bson.NilObjectID || msg.OccurredAt.IsZero() {
result = ingestResultInvalidEvent
return nil
}
@@ -195,15 +196,15 @@ func (s *service) handlePaymentStatusUpdated(ctx context.Context, msg *model.Pay
}
parsed := &events.Envelope{
EventID: strings.TrimSpace(msg.EventID),
Type: eventType,
ClientID: strings.TrimSpace(msg.ClientID),
OccurredAt: msg.OccurredAt.UTC(),
PublishedAt: msg.PublishedAt.UTC(),
Data: data,
EventID: strings.TrimSpace(msg.EventID),
Type: eventType,
OrganizationRef: msg.Data.OrganizationRef,
OccurredAt: msg.OccurredAt.UTC(),
PublishedAt: msg.PublishedAt.UTC(),
Data: data,
}
inserted, err := s.deps.InboxRepo.TryInsert(ctx, parsed.EventID, parsed.ClientID, parsed.Type, time.Now().UTC())
inserted, err := s.deps.InboxRepo.TryInsert(ctx, parsed.EventID, parsed.Type, parsed.OrganizationRef, time.Now().UTC())
if err != nil {
result = ingestResultInboxError
return err
@@ -213,7 +214,7 @@ func (s *service) handlePaymentStatusUpdated(ctx context.Context, msg *model.Pay
return nil
}
endpoints, err := s.deps.Resolver.Resolve(ctx, parsed.ClientID, parsed.Type)
endpoints, err := s.deps.Resolver.Resolve(ctx, parsed.Type, parsed.OrganizationRef)
if err != nil {
result = ingestResultResolveError
return err

View File

@@ -0,0 +1,8 @@
package model
import pmodel "github.com/tech/sendico/pkg/model"
type CallbackInternal struct {
pmodel.Callback `bson:",inline" json:",inline"`
SecretRef string `bson:"secretRef"`
}

View File

@@ -0,0 +1,22 @@
package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
pmodel "github.com/tech/sendico/pkg/model"
)
// Endpoint describes one target callback endpoint.
type Endpoint struct {
storable.Base
pmodel.OrganizationBoundBase
URL string
SigningMode string
SecretRef string
Headers map[string]string
MaxAttempts int
MinDelay time.Duration
MaxDelay time.Duration
RequestTimeout time.Duration
}

View File

@@ -0,0 +1,37 @@
package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"go.mongodb.org/mongo-driver/v2/bson"
)
// TaskStatus tracks delivery task lifecycle.
type TaskStatus string
const (
TaskStatusPending TaskStatus = "PENDING"
TaskStatusRetry TaskStatus = "RETRY"
TaskStatusDelivered TaskStatus = "DELIVERED"
TaskStatusFailed TaskStatus = "FAILED"
)
// Task is one callback delivery job.
type Task struct {
storable.Base
EventID string
EndpointRef bson.ObjectID
EndpointURL string
SigningMode string
SecretRef string
Headers map[string]string
Payload []byte
Attempt int
MaxAttempts int
MinDelay time.Duration
MaxDelay time.Duration
RequestTimeout time.Duration
Status TaskStatus
NextAttemptAt time.Time
}

View File

@@ -4,54 +4,12 @@ import (
"context"
"time"
"github.com/tech/sendico/edge/callbacks/internal/model"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
)
// TaskStatus tracks delivery task lifecycle.
type TaskStatus string
const (
TaskStatusPending TaskStatus = "PENDING"
TaskStatusRetry TaskStatus = "RETRY"
TaskStatusDelivered TaskStatus = "DELIVERED"
TaskStatusFailed TaskStatus = "FAILED"
)
// Endpoint describes one target callback endpoint.
type Endpoint struct {
ID bson.ObjectID
ClientID string
URL string
SigningMode string
SecretRef string
Headers map[string]string
MaxAttempts int
MinDelay time.Duration
MaxDelay time.Duration
RequestTimeout time.Duration
}
// Task is one callback delivery job.
type Task struct {
ID bson.ObjectID
EventID string
EndpointID bson.ObjectID
EndpointURL string
SigningMode string
SecretRef string
Headers map[string]string
Payload []byte
Attempt int
MaxAttempts int
MinDelay time.Duration
MaxDelay time.Duration
RequestTimeout time.Duration
Status TaskStatus
NextAttemptAt time.Time
}
// TaskDefaults are applied when creating tasks.
type TaskDefaults struct {
MaxAttempts int
@@ -69,18 +27,18 @@ type Options struct {
// InboxRepo controls event dedupe state.
type InboxRepo interface {
TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error)
TryInsert(ctx context.Context, eventID, ceventType string, organizationRef bson.ObjectID, at time.Time) (bool, error)
}
// EndpointRepo resolves endpoints for events.
type EndpointRepo interface {
FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error)
FindActive(ctx context.Context, eventType string, organizationRef bson.ObjectID) ([]model.Endpoint, error)
}
// TaskRepo manages callback tasks.
type TaskRepo interface {
UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error
LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error)
UpsertTasks(ctx context.Context, eventID string, endpoints []model.Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error
LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*model.Task, error)
MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error
MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt int, nextAttemptAt time.Time, lastError string, httpCode int, at time.Time) error
MarkFailed(ctx context.Context, taskID bson.ObjectID, attempt int, lastError string, httpCode int, at time.Time) error

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/tech/sendico/edge/callbacks/internal/model"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
@@ -13,6 +14,7 @@ import (
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
mutil "github.com/tech/sendico/pkg/mutil/db"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -39,10 +41,10 @@ type mongoRepository struct {
}
type inboxDoc struct {
storable.Base `bson:",inline"`
EventID string `bson:"event_id"`
ClientID string `bson:"client_id"`
EventType string `bson:"event_type"`
storable.Base `bson:",inline"`
pmodel.OrganizationBoundBase `bson:",inline"`
EventID string `bson:"event_id"`
EventType string `bson:"event_type"`
}
func (d *inboxDoc) Collection() string {
@@ -64,12 +66,12 @@ type deliveryPolicy struct {
}
type endpointDoc struct {
storable.Base `bson:",inline"`
deliveryPolicy `bson:"retry_policy"`
ClientID string `bson:"client_id"`
Status string `bson:"status"`
URL string `bson:"url"`
EventTypes []string `bson:"event_types"`
storable.Base `bson:",inline"`
pmodel.OrganizationBoundBase `bson:",inline"`
deliveryPolicy `bson:"retry_policy"`
Status string `bson:"status"`
URL string `bson:"url"`
EventTypes []string `bson:"event_types"`
}
func (d *endpointDoc) Collection() string {
@@ -79,18 +81,18 @@ func (d *endpointDoc) Collection() string {
type taskDoc struct {
storable.Base `bson:",inline"`
deliveryPolicy `bson:"retry_policy"`
EventID string `bson:"event_id"`
EndpointID bson.ObjectID `bson:"endpoint_id"`
EndpointURL string `bson:"endpoint_url"`
Payload []byte `bson:"payload"`
Status TaskStatus `bson:"status"`
Attempt int `bson:"attempt"`
LastError string `bson:"last_error,omitempty"`
LastHTTPCode int `bson:"last_http_code,omitempty"`
NextAttemptAt time.Time `bson:"next_attempt_at"`
LockedUntil *time.Time `bson:"locked_until,omitempty"`
WorkerID string `bson:"worker_id,omitempty"`
DeliveredAt *time.Time `bson:"delivered_at,omitempty"`
EventID string `bson:"event_id"`
EndpointRef bson.ObjectID `bson:"endpoint_ref"`
EndpointURL string `bson:"endpoint_url"`
Payload []byte `bson:"payload"`
Status model.TaskStatus `bson:"status"`
Attempt int `bson:"attempt"`
LastError string `bson:"last_error,omitempty"`
LastHTTPCode int `bson:"last_http_code,omitempty"`
NextAttemptAt time.Time `bson:"next_attempt_at"`
LockedUntil *time.Time `bson:"locked_until,omitempty"`
WorkerID string `bson:"worker_id,omitempty"`
DeliveredAt *time.Time `bson:"delivered_at,omitempty"`
}
func (d *taskDoc) Collection() string {
@@ -152,7 +154,7 @@ func (m *mongoRepository) ensureIndexes() error {
Unique: true,
Keys: []ri.Key{
{Field: "event_id", Sort: ri.Asc},
{Field: "endpoint_id", Sort: ri.Asc},
{Field: "endpoint_ref", Sort: ri.Asc},
},
},
{
@@ -172,7 +174,7 @@ func (m *mongoRepository) ensureIndexes() error {
if err := m.endpointsRepo.CreateIndex(&ri.Definition{
Name: "idx_client_event",
Keys: []ri.Key{
{Field: "client_id", Sort: ri.Asc},
{Field: "organization_ref", Sort: ri.Asc},
{Field: "status", Sort: ri.Asc},
{Field: "event_types", Sort: ri.Asc},
},
@@ -188,11 +190,11 @@ type inboxStore struct {
repo repository.Repository
}
func (r *inboxStore) TryInsert(ctx context.Context, eventID, clientID, eventType string, at time.Time) (bool, error) {
func (r *inboxStore) TryInsert(ctx context.Context, eventID, eventType string, organizationRef bson.ObjectID, at time.Time) (bool, error) {
doc := &inboxDoc{
EventID: strings.TrimSpace(eventID),
ClientID: strings.TrimSpace(clientID),
EventType: strings.TrimSpace(eventType),
OrganizationBoundBase: pmodel.OrganizationBoundBase{OrganizationRef: organizationRef},
EventID: strings.TrimSpace(eventID),
EventType: strings.TrimSpace(eventType),
}
filter := repository.Filter("event_id", doc.EventID)
@@ -212,21 +214,20 @@ type endpointStore struct {
repo repository.Repository
}
func (r *endpointStore) FindActiveByClientAndType(ctx context.Context, clientID, eventType string) ([]Endpoint, error) {
clientID = strings.TrimSpace(clientID)
func (r *endpointStore) FindActive(ctx context.Context, eventType string, organizationRef bson.ObjectID) ([]model.Endpoint, error) {
eventType = strings.TrimSpace(eventType)
if clientID == "" {
return nil, merrors.InvalidArgument("client_id is required", "client_id")
if organizationRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("organization_ref is required", "organization_ref")
}
if eventType == "" {
return nil, merrors.InvalidArgument("event type is required", "event_type")
}
query := repository.Query().
Filter(repository.Field("client_id"), clientID).
Filter(repository.OrgField(), organizationRef).
In(repository.Field("status"), "active", "enabled")
out := make([]Endpoint, 0)
out := make([]model.Endpoint, 0)
err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
doc := &endpointDoc{}
if err := cur.Decode(doc); err != nil {
@@ -238,17 +239,17 @@ func (r *endpointStore) FindActiveByClientAndType(ctx context.Context, clientID,
if !supportsEventType(doc.EventTypes, eventType) {
return nil
}
out = append(out, Endpoint{
ID: doc.ID,
ClientID: doc.ClientID,
URL: strings.TrimSpace(doc.URL),
SigningMode: strings.TrimSpace(doc.SigningMode),
SecretRef: strings.TrimSpace(doc.SecretRef),
Headers: cloneHeaders(doc.Headers),
MaxAttempts: doc.MaxAttempts,
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
out = append(out, model.Endpoint{
Base: doc.Base,
OrganizationBoundBase: doc.OrganizationBoundBase,
URL: strings.TrimSpace(doc.URL),
SigningMode: strings.TrimSpace(doc.SigningMode),
SecretRef: strings.TrimSpace(doc.SecretRef),
Headers: cloneHeaders(doc.Headers),
MaxAttempts: doc.MaxAttempts,
MinDelay: time.Duration(doc.MinDelayMS) * time.Millisecond,
MaxDelay: time.Duration(doc.MaxDelayMS) * time.Millisecond,
RequestTimeout: time.Duration(doc.RequestTimeoutMS) * time.Millisecond,
})
return nil
})
@@ -281,7 +282,7 @@ type taskStore struct {
repo repository.Repository
}
func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints []Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error {
func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints []model.Endpoint, payload []byte, defaults TaskDefaults, at time.Time) error {
eventID = strings.TrimSpace(eventID)
if eventID == "" {
return merrors.InvalidArgument("event id is required", "event_id")
@@ -292,7 +293,7 @@ func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints [
now := at.UTC()
for _, endpoint := range endpoints {
if endpoint.ID == bson.NilObjectID {
if endpoint.GetID() == nil || *endpoint.GetID() == bson.NilObjectID {
continue
}
@@ -327,13 +328,13 @@ func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints [
doc := &taskDoc{}
doc.EventID = eventID
doc.EndpointID = endpoint.ID
doc.EndpointRef = *endpoint.GetID()
doc.EndpointURL = strings.TrimSpace(endpoint.URL)
doc.SigningMode = strings.TrimSpace(endpoint.SigningMode)
doc.SecretRef = strings.TrimSpace(endpoint.SecretRef)
doc.Headers = cloneHeaders(endpoint.Headers)
doc.Payload = append([]byte(nil), payload...)
doc.Status = TaskStatusPending
doc.Status = model.TaskStatusPending
doc.Attempt = 0
doc.MaxAttempts = maxAttempts
doc.MinDelayMS = int(minDelay / time.Millisecond)
@@ -341,7 +342,7 @@ func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints [
doc.RequestTimeoutMS = int(requestTimeout / time.Millisecond)
doc.NextAttemptAt = now
filter := repository.Filter("event_id", eventID).And(repository.Filter("endpoint_id", endpoint.ID))
filter := repository.Filter("event_id", eventID).And(repository.Filter("endpoint_ref", endpoint.ID))
if err := r.repo.Insert(ctx, doc, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
continue
@@ -353,7 +354,7 @@ func (r *taskStore) UpsertTasks(ctx context.Context, eventID string, endpoints [
return nil
}
func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*Task, error) {
func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID string, lockTTL time.Duration) (*model.Task, error) {
workerID = strings.TrimSpace(workerID)
if workerID == "" {
return nil, merrors.InvalidArgument("worker id is required", "worker_id")
@@ -368,7 +369,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
)
query := repository.Query().
In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)).
In(repository.Field("status"), string(model.TaskStatusPending), string(model.TaskStatusRetry)).
Comparison(repository.Field("next_attempt_at"), builder.Lte, now).
And(lockFilter).
Sort(repository.Field("next_attempt_at"), true).
@@ -390,7 +391,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
Set(repository.Field("worker_id"), workerID)
conditional := repository.IDFilter(candidate.ID).And(
repository.Query().In(repository.Field("status"), string(TaskStatusPending), string(TaskStatusRetry)),
repository.Query().In(repository.Field("status"), string(model.TaskStatusPending), string(model.TaskStatusRetry)),
repository.Query().Comparison(repository.Field("next_attempt_at"), builder.Lte, now),
lockFilter,
)
@@ -427,7 +428,7 @@ func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, htt
}
patch := repository.Patch().
Set(repository.Field("status"), TaskStatusDelivered).
Set(repository.Field("status"), model.TaskStatusDelivered).
Set(repository.Field("last_http_code"), httpCode).
Set(repository.Field("delivered_at"), time.Now()).
Set(repository.Field("locked_until"), nil).
@@ -446,7 +447,7 @@ func (r *taskStore) MarkRetry(ctx context.Context, taskID bson.ObjectID, attempt
}
patch := repository.Patch().
Set(repository.Field("status"), TaskStatusRetry).
Set(repository.Field("status"), model.TaskStatusRetry).
Set(repository.Field("attempt"), attempt).
Set(repository.Field("next_attempt_at"), nextAttemptAt.UTC()).
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
@@ -466,7 +467,7 @@ func (r *taskStore) MarkFailed(ctx context.Context, taskID bson.ObjectID, attemp
}
patch := repository.Patch().
Set(repository.Field("status"), TaskStatusFailed).
Set(repository.Field("status"), model.TaskStatusFailed).
Set(repository.Field("attempt"), attempt).
Set(repository.Field("last_error"), strings.TrimSpace(lastError)).
Set(repository.Field("last_http_code"), httpCode).
@@ -479,14 +480,14 @@ func (r *taskStore) MarkFailed(ctx context.Context, taskID bson.ObjectID, attemp
return nil
}
func mapTaskDoc(doc *taskDoc) *Task {
func mapTaskDoc(doc *taskDoc) *model.Task {
if doc == nil {
return nil
}
return &Task{
ID: doc.ID,
return &model.Task{
Base: doc.Base,
EventID: doc.EventID,
EndpointID: doc.EndpointID,
EndpointRef: doc.EndpointRef,
EndpointURL: doc.EndpointURL,
SigningMode: doc.SigningMode,
SecretRef: doc.SecretRef,

View File

@@ -3,12 +3,14 @@ package subscriptions
import (
"context"
"github.com/tech/sendico/edge/callbacks/internal/model"
"github.com/tech/sendico/edge/callbacks/internal/storage"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Resolver resolves active webhook endpoints for an event.
type Resolver interface {
Resolve(ctx context.Context, clientID, eventType string) ([]storage.Endpoint, error)
Resolve(ctx context.Context, eventType string, organizationRef bson.ObjectID) ([]model.Endpoint, error)
}
// Dependencies defines subscriptions resolver dependencies.

View File

@@ -4,8 +4,10 @@ import (
"context"
"strings"
"github.com/tech/sendico/edge/callbacks/internal/model"
"github.com/tech/sendico/edge/callbacks/internal/storage"
"github.com/tech/sendico/pkg/merrors"
"go.mongodb.org/mongo-driver/v2/bson"
)
type service struct {
@@ -21,15 +23,15 @@ func New(deps Dependencies) (Resolver, error) {
return &service{repo: deps.EndpointRepo}, nil
}
func (s *service) Resolve(ctx context.Context, clientID, eventType string) ([]storage.Endpoint, error) {
if strings.TrimSpace(clientID) == "" {
func (s *service) Resolve(ctx context.Context, eventType string, organizationRef bson.ObjectID) ([]model.Endpoint, error) {
if organizationRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("subscriptions: client id is required", "clientID")
}
if strings.TrimSpace(eventType) == "" {
return nil, merrors.InvalidArgument("subscriptions: event type is required", "eventType")
}
endpoints, err := s.repo.FindActiveByClientAndType(ctx, clientID, eventType)
endpoints, err := s.repo.FindActive(ctx, eventType, organizationRef)
if err != nil {
return nil, err
}

View File

@@ -23,7 +23,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b h1:zoMPWbn27kscAQflTWzSHhm9fuex5SXSpyMlhCFPfxk=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -26,7 +26,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b h1:zoMPWbn27kscAQflTWzSHhm9fuex5SXSpyMlhCFPfxk=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -81,11 +81,10 @@ func (p *brokerPaymentStatusPublisher) Publish(_ context.Context, in paymentStat
message := &model.PaymentStatusUpdated{
EventID: buildPaymentStatusEventID(paymentRef, payment.Version, in.CurrentState),
Type: model.PaymentStatusUpdatedType,
ClientID: payment.OrganizationRef.Hex(),
OccurredAt: occurredAt,
PublishedAt: time.Now().UTC(),
Data: model.PaymentStatusUpdatedData{
OrganizationRef: payment.OrganizationRef.Hex(),
OrganizationRef: payment.OrganizationRef,
PaymentRef: paymentRef,
QuotationRef: strings.TrimSpace(payment.QuotationRef),
ClientPaymentRef: strings.TrimSpace(payment.ClientPaymentRef),

View File

@@ -12,4 +12,7 @@ type DB interface {
auth.ProtectedDB[*model.Callback]
SetArchived(ctx context.Context, accountRef, organizationRef, callbackRef bson.ObjectID, archived, cascade bool) error
List(ctx context.Context, accountRef, organizationRef, _ bson.ObjectID, cursor *model.ViewCursor) ([]model.Callback, error)
GetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) (string, error)
SetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID, secretRef string) error
ClearSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) error
}

View File

@@ -0,0 +1,8 @@
package callbacksdb
import "github.com/tech/sendico/pkg/model"
type callbackInternal struct {
model.Callback `bson:",inline" json:",inline"`
SecretRef string `bson:"secret_ref" json:"-"`
}

View File

@@ -2,14 +2,12 @@ package callbacksdb
import (
"context"
"errors"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/callbacks"
"github.com/tech/sendico/pkg/db/policy"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
@@ -29,10 +27,6 @@ func Create(
pdb policy.DB,
db *mongo.Database,
) (*CallbacksDB, error) {
if err := ensureBuiltInPolicy(ctx, logger, pdb); err != nil {
return nil, err
}
p, err := auth.CreateDBImp[*model.Callback](ctx, logger, pdb, enforcer, mservice.Callbacks, db)
if err != nil {
return nil, err
@@ -43,7 +37,7 @@ func Create(
Name: "uq_callbacks_client_url",
Keys: []ri.Key{
{Field: storable.OrganizationRefField, Sort: ri.Asc},
{Field: "client_id", Sort: ri.Asc},
{Field: "organization_ref", Sort: ri.Asc},
{Field: "url", Sort: ri.Asc},
},
Unique: true,
@@ -82,31 +76,4 @@ func Create(
}, nil
}
func ensureBuiltInPolicy(ctx context.Context, logger mlogger.Logger, pdb policy.DB) error {
var existing model.PolicyDescription
if err := pdb.GetBuiltInPolicy(ctx, mservice.Callbacks, &existing); err == nil {
return nil
} else if !errors.Is(err, merrors.ErrNoData) {
return err
}
description := "Callbacks subscription management"
resourceTypes := []mservice.Type{mservice.Callbacks}
policyDescription := &model.PolicyDescription{
Describable: model.Describable{
Name: "Callbacks",
Description: &description,
},
ResourceTypes: &resourceTypes,
}
if err := pdb.Create(ctx, policyDescription); err != nil && !errors.Is(err, merrors.ErrDataConflict) {
if logger != nil {
logger.Warn("Failed to create built-in callbacks policy", zap.Error(err))
}
return err
}
return pdb.GetBuiltInPolicy(ctx, mservice.Callbacks, &existing)
}
var _ callbacks.DB = (*CallbacksDB)(nil)

View File

@@ -0,0 +1,60 @@
package callbacksdb
import (
"context"
"strings"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
func (db *CallbacksDB) GetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) (string, error) {
if callbackRef.IsZero() {
return "", merrors.InvalidArgument("callback reference is required", "callbackRef")
}
// Enforce read permissions through the public callback object first.
var callback model.Callback
if err := db.Get(ctx, accountRef, callbackRef, &callback); err != nil {
return "", err
}
internal := &callbackInternal{}
if err := db.DBImp.Repository.Get(ctx, callbackRef, internal); err != nil {
return "", err
}
return strings.TrimSpace(internal.SecretRef), nil
}
func (db *CallbacksDB) SetSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID, secretRef string) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
value := strings.TrimSpace(secretRef)
if value == "" {
return merrors.InvalidArgument("secret reference is required", "secretRef")
}
return db.Patch(
ctx,
accountRef,
callbackRef,
repository.Patch().Set(repository.Field("secretRef"), value),
)
}
func (db *CallbacksDB) ClearSigningSecretRef(ctx context.Context, accountRef, callbackRef bson.ObjectID) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
return db.Patch(
ctx,
accountRef,
callbackRef,
repository.Patch().Unset(repository.Field("secretRef")),
)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
mutil "github.com/tech/sendico/pkg/mutil/db"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
@@ -54,7 +55,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("scope_filter", scopeFilter.BuildQuery()),
)
// 1) Fast path for magic-link tokens: hash is deterministic and globally unique.
@@ -69,7 +69,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("magic_filter", magicFilter.BuildQuery()),
)
var direct model.VerificationToken
err := db.DBImp.FindOne(ctx, magicFilter, &direct)
@@ -152,7 +151,7 @@ func (db *verificationDB) Consume(
db.Logger.Debug("Verification consume OTP candidate evaluated",
zap.Int("candidate_index", i),
zap.Int("candidate_total", len(tokens)),
zap.String("candidate_id", t.ID.Hex()),
mzap.ObjRef("candidate_ref", t.ID),
zap.Bool("candidate_has_salt", t.Salt != nil),
zap.Bool("candidate_used", t.UsedAt != nil),
zap.Time("candidate_expires_at", t.ExpiresAt),
@@ -181,7 +180,6 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.Any("active_filter", activeFilter.BuildQuery()),
)
incremented, patchErr := db.DBImp.PatchMany(
@@ -271,8 +269,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
zap.Any("consume_filter", consumeFilter.BuildQuery()),
mzap.StorableRef(token),
)
updated, err := db.DBImp.PatchMany(
@@ -285,7 +282,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(err),
)
return nil, err
@@ -294,7 +291,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("updated_count", updated),
)
@@ -322,7 +319,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(incrementErr),
)
} else {
@@ -330,7 +327,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("updated_count", incremented),
zap.String("transaction_note", "this update occurs inside transaction and may roll back depending on returned error"),
)
@@ -343,7 +340,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Error(err),
)
return nil, merrors.Internal("failed to re-check token state")
@@ -353,7 +350,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
}, tokenStateFields(&fresh)...,
)...,
)
@@ -363,7 +360,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
)
return nil, verification.ErorrTokenAlreadyUsed()
}
@@ -372,7 +369,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Time("now_utc", now),
zap.Time("token_expires_at", fresh.ExpiresAt),
)
@@ -383,7 +380,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
zap.Int("token_attempts", fresh.Attempts),
zap.Int("token_max_retries", *fresh.MaxRetries),
)
@@ -394,7 +391,7 @@ func (db *verificationDB) Consume(
zap.String("purpose", string(purpose)),
zap.Bool("account_scoped", accountScoped),
zap.String("account_ref", accountRefHex),
zap.String("token_id", token.ID.Hex()),
mzap.StorableRef(token),
)
return nil, verification.ErorrTokenNotFound()
},
@@ -457,8 +454,8 @@ func tokenIsDigitsOnly(value string) bool {
func tokenStateFields(token *model.VerificationToken) []zap.Field {
fields := []zap.Field{
zap.String("token_id", token.ID.Hex()),
zap.String("token_account_ref", token.AccountRef.Hex()),
mzap.StorableRef(token),
mzap.ObjRef("token_account_ref", token.AccountRef),
zap.String("token_purpose", string(token.Purpose)),
zap.Bool("token_has_target", strings.TrimSpace(token.Target) != ""),
zap.Bool("token_has_idempotency_key", token.IdempotencyKey != nil),

View File

@@ -29,14 +29,6 @@ func syntheticIdempotencyKey() string {
return "auto:" + bson.NewObjectID().Hex()
}
func verificationContextFilter(request *verification.Request) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),
repository.Filter("purpose", request.Purpose),
repository.Filter("target", request.Target),
)
}
func activeContextFilter(request *verification.Request, now time.Time) builder.Query {
return repository.Query().And(
repository.Filter("accountRef", request.AccountRef),

View File

@@ -28,7 +28,7 @@ require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect

View File

@@ -6,8 +6,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b h1:zoMPWbn27kscAQflTWzSHhm9fuex5SXSpyMlhCFPfxk=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260302101851-b43f15d4ea3b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

View File

@@ -16,11 +16,14 @@ const (
CallbackSigningModeHMACSHA256 CallbackSigningMode = "hmac_sha256"
)
type CallbackBackoff struct {
MinDelayMS int `bson:"min_ms" json:"minDelayMs"`
MaxDelayMS int `bson:"max_ms" json:"maxDelayMs"`
}
type CallbackRetryPolicy struct {
MinDelayMS int `bson:"min_ms" json:"minDelayMs"`
MaxDelayMS int `bson:"max_ms" json:"maxDelayMs"`
Backoff CallbackBackoff `bson:"backoff" json:"backoff"`
SigningMode CallbackSigningMode `bson:"signing_mode" json:"signingMode"`
SecretRef string `bson:"secret_ref,omitempty" json:"secretRef,omitempty"`
Headers map[string]string `bson:"headers,omitempty" json:"headers,omitempty"`
MaxAttempts int `bson:"max_attempts" json:"maxAttempts"`
RequestTimeoutMS int `bson:"request_timeout_ms" json:"requestTimeoutMs"`
@@ -29,7 +32,6 @@ type CallbackRetryPolicy struct {
type Callback struct {
PermissionBound `bson:",inline" json:",inline"`
Describable `bson:",inline" json:",inline"`
ClientID string `bson:"client_id" json:"clientId"`
Status CallbackStatus `bson:"status" json:"status"`
URL string `bson:"url" json:"url"`
EventTypes []string `bson:"event_types" json:"eventTypes"`

View File

@@ -1,6 +1,10 @@
package model
import "time"
import (
"time"
"go.mongodb.org/mongo-driver/v2/bson"
)
const (
PaymentStatusUpdatedType = "payment.status.updated"
@@ -9,21 +13,20 @@ const (
type PaymentStatusUpdated struct {
EventID string `json:"event_id,omitempty"`
Type string `json:"type,omitempty"`
ClientID string `json:"client_id,omitempty"`
OccurredAt time.Time `json:"occurred_at,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"`
Data PaymentStatusUpdatedData `json:"data"`
}
type PaymentStatusUpdatedData struct {
OrganizationRef string `json:"organization_ref,omitempty"`
PaymentRef string `json:"payment_ref,omitempty"`
QuotationRef string `json:"quotation_ref,omitempty"`
ClientPaymentRef string `json:"client_payment_ref,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
State string `json:"state,omitempty"`
PreviousState string `json:"previous_state,omitempty"`
Version uint64 `json:"version,omitempty"`
IsTerminal bool `json:"is_terminal"`
Event string `json:"event,omitempty"`
OrganizationRef bson.ObjectID `json:"organization_ref,omitempty"`
PaymentRef string `json:"payment_ref,omitempty"`
QuotationRef string `json:"quotation_ref,omitempty"`
ClientPaymentRef string `json:"client_payment_ref,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
State string `json:"state,omitempty"`
PreviousState string `json:"previous_state,omitempty"`
Version uint64 `json:"version,omitempty"`
IsTerminal bool `json:"is_terminal"`
Event string `json:"event,omitempty"`
}

View File

@@ -3,7 +3,9 @@ package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
)
type ClientRefreshToken struct {
@@ -12,13 +14,14 @@ type ClientRefreshToken struct {
}
type RefreshToken struct {
AccountBoundBase `bson:",inline" json:",inline"`
storable.Base `bson:",inline" json:",inline"`
ClientRefreshToken `bson:",inline" json:",inline"`
ExpiresAt time.Time `bson:"expiresAt"`
IsRevoked bool `bson:"isRevoked"`
LastUsedAt time.Time `bson:"lastUsedAt,omitempty"`
UserAgent string `bson:"userAgent"`
IPAddress string `bson:"ipAddress"`
AccountRef *bson.ObjectID `bson:"accountRef,omitempty" json:"accountRef,omitempty"`
ExpiresAt time.Time `bson:"expiresAt"`
IsRevoked bool `bson:"isRevoked"`
LastUsedAt time.Time `bson:"lastUsedAt,omitempty"`
UserAgent string `bson:"userAgent"`
IPAddress string `bson:"ipAddress"`
}
func (*RefreshToken) Collection() string {

View File

@@ -1,388 +0,0 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0lCi7JKrS4qQ=
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI=
github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/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/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.8 h1:BDP3+U3Y8K0vTrpqDJIRaXNhb/bKyoVeg6tIJsW5EhM=
go.mongodb.org/mongo-driver v1.17.8/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.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/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
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.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
moul.io/chizap v1.0.3 h1:mliXvvuS5HVo3QP8qPXczWtRM5dQ9UmK3bBVIkZo6ek=
moul.io/chizap v1.0.3/go.mod h1:pq4R9kGLwz4XjBc4hodQYuoE7Yc9RUabLBFyyi2uErk=

View File

@@ -26,8 +26,8 @@ WORKDIR /src
COPY --from=builder /src/api/proto ./api/proto
COPY --from=builder /src/api/pkg ./api/pkg
# Copy vault-aware entrypoint wrapper
COPY api/edge/callbacks/entrypoint.sh /app/entrypoint.sh
# Copy dev-specific entrypoint script
COPY ci/dev/entrypoints/callbacks.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Source code will be mounted at runtime

View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
# Load Vault token from file if VAULT_TOKEN_FILE is set
if [ -n "${VAULT_TOKEN_FILE:-}" ] && [ -f "${VAULT_TOKEN_FILE}" ]; then
token="$(cat "${VAULT_TOKEN_FILE}" 2>/dev/null | tr -d '[:space:]')"
if [ -n "${token}" ]; then
export VAULT_TOKEN="${token}"
fi
fi
if [ -z "${VAULT_TOKEN:-}" ]; then
echo "[entrypoint] VAULT_TOKEN is not set; expected Vault Agent sink to write a token to ${VAULT_TOKEN_FILE:-/run/vault/token}" >&2
fi
# Execute the command passed as arguments (e.g., air)
exec "$@"

View File

@@ -89,8 +89,6 @@ paths:
$ref: ./api/payments/quote.yaml
/payments/multiquote/{organizations_ref}:
$ref: ./api/payments/multiquote.yaml
/payments/immediate/{organizations_ref}:
$ref: ./api/payments/immediate.yaml
/payments/by-quote/{organizations_ref}:
$ref: ./api/payments/by_quote.yaml
/payments/by-multiquote/{organizations_ref}:

View File

@@ -1,12 +1,14 @@
components:
requestBodies:
LoginBody:
description: JSON credentials payload for standard login.
required: true
content:
application/json:
schema:
$ref: ../request/auth.yaml#/components/schemas/LoginRequest
ApiLoginBody:
description: JSON credentials payload for API client login.
required: true
content:
application/json:
@@ -14,6 +16,7 @@ components:
$ref: ../request/auth.yaml#/components/schemas/ApiLoginRequest
RefreshTokenBody:
description: JSON payload containing session and refresh token data.
required: true
content:
application/json:

View File

@@ -11,6 +11,7 @@ components:
description: Client identifier bound to refresh token lifecycle and client policy checks.
deviceId:
type: string
description: Device/server identifier associated with the client session.
login:
$ref: ../../../models/auth/login_data.yaml#/components/schemas/LoginData

View File

@@ -1,6 +1,7 @@
components:
schemas:
AccountAuthData:
description: Authentication response containing account profile and access token.
type: object
additionalProperties: false
required:
@@ -8,11 +9,14 @@ components:
- account
properties:
accessToken:
description: Access token used for authenticated requests.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
account:
description: Authenticated account data.
$ref: ../../../models/account/account.yaml#/components/schemas/AccountData
LoginData:
description: Full login response including access and refresh tokens.
type: object
additionalProperties: false
required:
@@ -21,13 +25,17 @@ components:
- refreshToken
properties:
accessToken:
description: Access token used for authenticated requests.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
account:
description: Authenticated account data.
$ref: ../../../models/account/account.yaml#/components/schemas/AccountData
refreshToken:
description: Refresh token used to obtain a new access token.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
PendingLoginData:
description: Pending login response requiring additional verification.
type: object
additionalProperties: false
required:
@@ -36,14 +44,19 @@ components:
- target
properties:
account:
description: Interim authentication payload prepared for verification completion.
type: object
additionalProperties: false
properties:
accessToken:
description: Temporary access token issued before verification is completed.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
account:
description: Account data associated with the pending login flow.
$ref: ../../../models/account/account.yaml#/components/schemas/AccountData
pendingToken:
description: Token proving the pending verification session.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
target:
description: Verification target destination (for example email or phone).
type: string

View File

@@ -1,9 +1,9 @@
components:
requestBodies:
CallbackBody:
description: JSON body containing callback endpoint configuration.
required: true
content:
application/json:
schema:
$ref: ../request/callback.yaml#/components/schemas/CallbackRequest

View File

@@ -1,5 +1,5 @@
components:
schemas:
CallbackRequest:
$ref: ../../../models/callback/callback.yaml#/components/schemas/Callback
description: Request payload used to create or update a callback configuration.
$ref: ../../../models/callback/callback.yaml#/components/schemas/CallbackContent

View File

@@ -1,6 +1,7 @@
components:
schemas:
CallbacksAuthData:
description: Authenticated response payload containing callback configurations.
type: object
additionalProperties: false
required:
@@ -8,12 +9,14 @@ components:
- callbacks
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
callbacks:
description: Collection of callbacks configured for the authenticated scope.
type: array
items:
$ref: ../../../models/callback/callback.yaml#/components/schemas/Callback
generatedSigningSecret:
description: Newly generated signing secret when secret rotation is requested.
type: string
nullable: true

View File

@@ -1,7 +1,7 @@
post:
tags: [Callbacks]
summary: Rotate callback signing secret
description: Generates and stores a new HMAC secret for the callback in Vault and returns it once.
description: Generates and stores a new HMAC secret for the callback and returns it once.
operationId: callbacksRotateSecret
security:
- bearerAuth: []

View File

@@ -1,6 +1,7 @@
components:
schemas:
OrganizationsAuthData:
description: Authenticated response payload containing organizations.
type: object
additionalProperties: false
required:
@@ -8,8 +9,10 @@ components:
- organizations
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
organizations:
description: Collection of organizations available to the authenticated account.
type: array
items:
$ref: ../../../models/organization/organization.yaml#/components/schemas/Organization

View File

@@ -4,7 +4,7 @@ components:
name: callbacks_ref
in: path
required: true
description: Callback subscription reference (Mongo ObjectId, 24 hex chars).
description: Callback subscription reference, 24 hex chars.
schema:
$ref: ../../models/objectid.yaml#/components/schemas/ObjectId

View File

@@ -4,6 +4,6 @@ components:
name: org_ref
in: path
required: true
description: Organization reference in BFF route format (Mongo ObjectId, 24 hex chars).
description: Organization reference in BFF route format, 24 hex chars.
schema:
$ref: ../../models/objectid.yaml#/components/schemas/ObjectId

View File

@@ -4,6 +4,6 @@ components:
name: payment_methods_ref
in: path
required: true
description: Payment method reference (Mongo ObjectId, 24 hex chars).
description: Payment method reference, 24 hex chars.
schema:
$ref: ../../models/objectid.yaml#/components/schemas/ObjectId

View File

@@ -4,6 +4,6 @@ components:
name: recipients_ref
in: path
required: true
description: Recipient reference (Mongo ObjectId, 24 hex chars).
description: Recipient reference, 24 hex chars.
schema:
$ref: ../../models/objectid.yaml#/components/schemas/ObjectId

View File

@@ -1,6 +1,7 @@
components:
requestBodies:
PaymentMethodBody:
description: JSON body containing a payment method definition.
required: true
content:
application/json:

View File

@@ -1,4 +1,5 @@
components:
schemas:
PaymentMethodRequest:
description: Request payload used to create or update a payment method.
$ref: ../../../models/payment_method/payment_method.yaml#/components/schemas/PaymentMethod

View File

@@ -1,6 +1,7 @@
components:
schemas:
Payment MethodsAuthData:
description: Authenticated response payload containing payment methods.
type: object
additionalProperties: false
required:
@@ -8,8 +9,10 @@ components:
- payment_methods
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
payment_methods:
description: Collection of payment methods available to the authenticated account.
type: array
items:
$ref: ../../../models/payment_method/payment_method.yaml#/components/schemas/PaymentMethod

View File

@@ -1,6 +1,7 @@
components:
requestBodies:
QuotePaymentBody:
description: JSON payload used to request a quote for one payment intent.
required: true
content:
application/json:
@@ -8,6 +9,7 @@ components:
$ref: ../request/payment.yaml#/components/schemas/QuotePaymentRequest
QuotePaymentsBody:
description: JSON payload used to request quotes for multiple payment intents.
required: true
content:
application/json:
@@ -15,6 +17,7 @@ components:
$ref: ../request/payment.yaml#/components/schemas/QuotePaymentsRequest
InitiatePaymentBody:
description: JSON payload used to initiate a single payment.
required: true
content:
application/json:
@@ -22,6 +25,7 @@ components:
$ref: ../request/payment.yaml#/components/schemas/InitiatePaymentRequest
InitiatePaymentsBody:
description: JSON payload used to initiate multiple payments from a quote reference.
required: true
content:
application/json:

View File

@@ -1,33 +1,41 @@
components:
schemas:
PaymentBase:
description: Base fields shared by payment initiation request payloads.
type: object
additionalProperties: false
required:
- idempotencyKey
properties:
idempotencyKey:
description: Client-supplied key used to safely deduplicate requests.
type: string
metadata:
description: Optional request metadata forwarded through payment processing.
type: object
additionalProperties:
type: string
QuotePaymentRequest:
description: Request payload to quote a single payment intent.
type: object
additionalProperties: false
required:
- intent
properties:
idempotencyKey:
description: Idempotency key used when persisting quote context.
type: string
metadata:
description: Optional metadata associated with the quote request.
type: object
additionalProperties:
type: string
intent:
description: Payment intent to be priced.
$ref: ../../../models/payment/payment.yaml#/components/schemas/PaymentIntent
previewOnly:
description: If true, returns a preview quote without requiring idempotency.
type: boolean
allOf:
- if:
@@ -45,23 +53,28 @@ components:
- idempotencyKey
QuotePaymentsRequest:
description: Request payload to quote multiple payment intents in a single call.
type: object
additionalProperties: false
required:
- intents
properties:
idempotencyKey:
description: Idempotency key used when persisting batch quote context.
type: string
metadata:
description: Optional metadata associated with the quote request.
type: object
additionalProperties:
type: string
intents:
description: List of payment intents to be priced.
type: array
minItems: 1
items:
$ref: ../../../models/payment/payment.yaml#/components/schemas/PaymentIntent
previewOnly:
description: If true, returns preview quotes without requiring idempotency.
type: boolean
allOf:
- if:
@@ -79,6 +92,7 @@ components:
- idempotencyKey
InitiatePaymentRequest:
description: Request payload to initiate a single payment.
allOf:
- $ref: ./payment.yaml#/components/schemas/PaymentBase
- type: object
@@ -88,11 +102,14 @@ components:
- required: [quoteRef]
properties:
intent:
description: Payment intent to execute directly.
$ref: ../../../models/payment/payment.yaml#/components/schemas/PaymentIntent
quoteRef:
description: Reference to a previously generated quote to execute.
type: string
InitiatePaymentsRequest:
description: Request payload to initiate multiple payments from a multi-quote reference.
allOf:
- $ref: ./payment.yaml#/components/schemas/PaymentBase
- type: object
@@ -101,4 +118,5 @@ components:
- quoteRef
properties:
quoteRef:
description: Reference to a previously generated multi-quote.
type: string

View File

@@ -1,6 +1,7 @@
components:
schemas:
PaymentQuoteData:
description: Response payload for a single payment quote.
type: object
additionalProperties: false
required:
@@ -8,13 +9,17 @@ components:
- quote
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
idempotencyKey:
description: Idempotency key associated with the quote response.
type: string
quote:
description: Generated quote data for the requested payment intent.
$ref: ../../../models/payment/payment.yaml#/components/schemas/PaymentQuote
PaymentQuotesData:
description: Response payload for a batch quote request.
type: object
additionalProperties: false
required:
@@ -22,21 +27,27 @@ components:
- quote
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
quote:
description: Batch quote summary and quoted items.
type: object
additionalProperties: false
properties:
idempotencyKey:
description: Idempotency key associated with the batch quote response.
type: string
quoteRef:
description: Reference to the generated batch quote.
type: string
items:
description: Collection of quotes for each requested payment intent.
type: array
items:
$ref: ../../../models/payment/payment.yaml#/components/schemas/PaymentQuote
PaymentsData:
description: Response payload containing a list of payments.
type: object
additionalProperties: false
required:
@@ -44,15 +55,19 @@ components:
- payments
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
payments:
description: Collection of payment records.
type: array
items:
$ref: ../../../models/payment/payment.yaml#/components/schemas/Payment
page:
description: Pagination cursor metadata for payment listing endpoints.
$ref: ../../../models/common/pagination.yaml#/components/schemas/CursorPageResponse
PaymentData:
description: Response payload containing a single payment record.
type: object
additionalProperties: false
required:
@@ -60,6 +75,8 @@ components:
- payment
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
payment:
description: Requested payment record.
$ref: ../../../models/payment/payment.yaml#/components/schemas/Payment

View File

@@ -1,6 +1,7 @@
components:
requestBodies:
RecipientBody:
description: JSON body containing recipient details.
required: true
content:
application/json:

View File

@@ -1,4 +1,5 @@
components:
schemas:
RecipientRequest:
description: Request payload used to create or update a recipient.
$ref: ../../../models/recipient/recipient.yaml#/components/schemas/Recipient

View File

@@ -1,6 +1,7 @@
components:
schemas:
RecipientsAuthData:
description: Authenticated response payload containing recipients.
type: object
additionalProperties: false
required:
@@ -8,8 +9,10 @@ components:
- recipients
properties:
accessToken:
description: Refreshed access token to be used in subsequent API calls.
$ref: ../../../models/auth/token_data.yaml#/components/schemas/TokenData
recipients:
description: Collection of recipients available to the authenticated account.
type: array
items:
$ref: ../../../models/recipient/recipient.yaml#/components/schemas/Recipient

View File

@@ -1,6 +1,7 @@
components:
schemas:
ApiError:
description: Standard API error envelope with optional debugging details.
type: object
additionalProperties: false
required:
@@ -9,11 +10,15 @@ components:
- source
properties:
code:
description: Application-specific numeric error code.
type: integer
format: int32
error:
description: Human-readable error summary.
type: string
source:
description: Component or subsystem that produced the error.
type: string
details:
description: Optional detailed message providing additional failure context.
type: string

View File

@@ -1,6 +1,7 @@
components:
requestBodies:
VerificationCodeBody:
description: JSON payload used to request a new verification code.
required: true
content:
application/json:
@@ -8,6 +9,7 @@ components:
$ref: ../request/verification.yaml#/components/schemas/VerificationCodeRequest
VerifyCodeBody:
description: JSON payload containing a verification code to validate.
required: true
content:
application/json:

View File

@@ -1,6 +1,7 @@
components:
schemas:
VerificationResponseData:
description: Response data returned after requesting a verification code.
type: object
additionalProperties: false
required:
@@ -10,17 +11,22 @@ components:
- target
properties:
idempotencyKey:
description: Idempotency key associated with the verification request.
type: string
ttl_seconds:
description: Verification code validity period in seconds.
type: integer
format: int32
cooldown_seconds:
description: Cooldown period before another code can be requested.
type: integer
format: int32
target:
description: Destination where the verification code was sent.
type: string
VerifyResultData:
description: Result payload returned after code verification succeeds.
oneOf:
- $ref: ../../accounts/response/auth.yaml#/components/schemas/LoginData
- $ref: ../../response/response.yaml#/components/schemas/SuccessResultData

View File

@@ -1,12 +1,15 @@
components:
schemas:
AccountData:
description: Extended account payload including visibility-specific metadata.
allOf:
- $ref: ./account_public.yaml#/components/schemas/AccountPublic
- type: object
description: Account attributes that are not part of the public profile.
additionalProperties: false
required:
- isAnonymous
properties:
isAnonymous:
description: Indicates whether the account is marked as anonymous.
type: boolean

View File

@@ -1,10 +1,12 @@
components:
schemas:
AccountPublic:
description: Public account profile data exposed in API responses.
allOf:
- $ref: ../storable.yaml#/components/schemas/Storable
- $ref: ../common/describable.yaml#/components/schemas/Describable
- type: object
description: Account-specific public attributes.
additionalProperties: false
required:
- login
@@ -13,14 +15,19 @@ components:
- isArchived
properties:
login:
description: Account login identifier represented as an email address.
type: string
format: email
locale:
description: Preferred locale used for account-facing content.
type: string
lastName:
description: Account holder last name.
type: string
avatarUrl:
description: Optional URL of the account avatar image.
type: string
nullable: true
isArchived:
description: Indicates whether the account is archived and inactive.
type: boolean

View File

@@ -1,12 +1,15 @@
components:
schemas:
ClientRefreshToken:
description: Refresh token bound to a specific client and device session.
allOf:
- $ref: ./session_identifier.yaml#/components/schemas/SessionIdentifier
- type: object
description: Refresh-token payload associated with the session identifier.
additionalProperties: false
required:
- token
properties:
token:
description: Long-lived token used to obtain a new access token.
type: string

View File

@@ -1,6 +1,7 @@
components:
schemas:
LoginData:
description: Credentials and optional locale used during user sign-in.
type: object
additionalProperties: false
required:
@@ -8,10 +9,13 @@ components:
- password
properties:
login:
description: User login identifier, represented as an email address.
type: string
format: email
password:
description: User password submitted for authentication.
type: string
format: password
locale:
description: Optional locale preference provided at login time.
type: string

View File

@@ -1,6 +1,7 @@
components:
schemas:
SessionIdentifier:
description: Identifies an authentication session by client and device.
type: object
additionalProperties: false
required:
@@ -8,6 +9,8 @@ components:
- deviceId
properties:
clientId:
description: Client application identifier associated with the session.
type: string
deviceId:
description: Device/server identifier associated with the client session.
type: string

View File

@@ -1,6 +1,7 @@
components:
schemas:
TokenData:
description: Authentication token payload with expiration metadata.
type: object
additionalProperties: false
required:
@@ -8,7 +9,9 @@ components:
- expiration
properties:
token:
description: Issued authentication token value.
type: string
expiration:
description: RFC 3339 timestamp when the token expires.
type: string
format: date-time

View File

@@ -1,67 +1,89 @@
components:
schemas:
CallbackRetryPolicy:
CallbackBackoff:
description: Backoff window used between callback retry attempts.
type: object
additionalProperties: false
required:
- minDelayMs
- maxDelayMs
properties:
minDelayMs:
description: Minimum delay in milliseconds before scheduling a retry attempt.
type: integer
minimum: 1
maxDelayMs:
description: Maximum delay in milliseconds before scheduling a retry attempt.
type: integer
minimum: 1
CallbackRetryPolicy:
description: Retry and delivery behavior for callback requests.
type: object
additionalProperties: false
required:
- backoff
- signingMode
- maxAttempts
- requestTimeoutMs
properties:
minDelayMs:
type: integer
minimum: 1
maxDelayMs:
type: integer
minimum: 1
backoff:
description: Delay boundaries used to compute retry backoff between attempts.
$ref: '#/components/schemas/CallbackBackoff'
signingMode:
description: Request-signing strategy applied when dispatching callback payloads.
type: string
enum:
- none
- hmac_sha256
secretRef:
type: string
nullable: true
headers:
description: Additional HTTP headers included with every callback request.
type: object
additionalProperties:
type: string
maxAttempts:
description: Maximum number of delivery attempts before marking dispatch as failed.
type: integer
minimum: 1
requestTimeoutMs:
description: Per-request timeout in milliseconds for callback HTTP calls.
type: integer
minimum: 1
Callback:
CallbackContent:
description: Callback endpoint configuration and delivery parameters.
allOf:
- $ref: ../permission_bound.yaml#/components/schemas/PermissionBound
- $ref: ../common/describable.yaml#/components/schemas/Describable
- type: object
additionalProperties: false
required:
- clientId
- status
- url
- eventTypes
- retryPolicy
properties:
clientId:
type: string
status:
description: Operational state controlling whether callback delivery is enabled.
type: string
enum:
- active
- disabled
url:
description: Absolute HTTPS endpoint URL that receives callback events.
type: string
format: uri
eventTypes:
description: Event type names that trigger callback delivery to this endpoint.
type: array
items:
type: string
retryPolicy:
description: Retry and timeout parameters used when dispatching callback events.
$ref: '#/components/schemas/CallbackRetryPolicy'
Callback:
description: Stored callback configuration bound to an organization permission scope.
allOf:
- $ref: ../permission_bound.yaml#/components/schemas/PermissionBound
- $ref: ../common/describable.yaml#/components/schemas/Describable
- $ref: '#/components/schemas/CallbackContent'

View File

@@ -1,13 +1,16 @@
components:
schemas:
Describable:
description: Common naming metadata for user-defined entities.
type: object
additionalProperties: false
required:
- name
properties:
name:
description: Human-readable name of the entity.
type: string
description:
description: Optional free-form description of the entity.
type: string
nullable: true

View File

@@ -1,6 +1,7 @@
components:
schemas:
Money:
description: Monetary value represented by a decimal amount and ISO currency code.
type: object
additionalProperties: false
required:
@@ -12,5 +13,6 @@ components:
description: Decimal string amount.
example: '125.50'
currency:
description: ISO 4217 currency code for the provided amount.
type: string
example: USD

View File

@@ -1,8 +1,10 @@
components:
schemas:
CursorPageResponse:
description: Cursor-based pagination metadata returned with list responses.
type: object
additionalProperties: false
properties:
next_cursor:
description: Opaque cursor to request the next page; omitted when no more items exist.
type: string

View File

@@ -1,7 +1,9 @@
components:
schemas:
ObjectId:
description: Object identifier represented as a 24-character hexadecimal string.
type: string
format: objectid
pattern: '^[a-fA-F0-9]{24}$'
examples:
- 64f85f5f4c7dbf7cfb8f3f10

View File

@@ -7,11 +7,8 @@ components:
- type: object
additionalProperties: false
required:
- tenantRef
- timeZone
properties:
tenantRef:
$ref: ../objectid.yaml#/components/schemas/ObjectId
timeZone:
type: string
logoUrl:

View File

@@ -1,39 +1,50 @@
components:
schemas:
Asset:
description: Blockchain asset identifier used for on-chain transfers.
type: object
additionalProperties: false
properties:
chain:
description: Chain/network where the asset exists.
$ref: ../../external/chain_network.yaml#/components/schemas/ChainNetwork
token_symbol:
description: Symbol of the token on the selected chain.
type: string
contract_address:
description: Smart-contract address for tokenized assets.
type: string
LedgerEndpoint:
description: Internal ledger-based endpoint used as payment source or destination.
type: object
additionalProperties: false
required:
- ledger_account_ref
properties:
ledger_account_ref:
description: Reference of the primary ledger account.
type: string
contra_ledger_account_ref:
description: Optional contra ledger account reference for balancing entries.
type: string
ManagedWalletEndpoint:
description: Endpoint referencing a managed wallet within the platform.
type: object
additionalProperties: false
required:
- managed_wallet_ref
properties:
managed_wallet_ref:
description: Reference of the managed wallet.
type: string
asset:
description: Asset details for the managed wallet transfer.
$ref: ./payment.yaml#/components/schemas/Asset
ExternalChainEndpoint:
description: Endpoint representing an external blockchain address.
type: object
additionalProperties: false
required:
@@ -41,56 +52,72 @@ components:
- address
properties:
asset:
description: Asset to be transferred to or from the external address.
$ref: ./payment.yaml#/components/schemas/Asset
address:
description: Target blockchain address.
type: string
memo:
description: Optional destination memo or tag required by some chains.
type: string
CardEndpoint:
description: Raw card details endpoint used for card-based payment flows.
type: object
additionalProperties: false
required:
- pan
properties:
pan:
description: Primary account number of the card.
type: string
firstName:
description: Cardholder first name.
type: string
lastName:
description: Cardholder last name.
type: string
exp_month:
description: Card expiration month.
type: integer
format: int32
minimum: 0
exp_year:
description: Card expiration year.
type: integer
format: int32
minimum: 0
country:
description: Card issuing country code.
type: string
CardTokenEndpoint:
description: Tokenized card endpoint used instead of raw PAN details.
type: object
additionalProperties: false
required:
- token
properties:
token:
description: Provider-issued card token.
type: string
masked_pan:
description: Masked PAN representation for display and auditing.
type: string
WalletEndpoint:
description: Generic wallet endpoint identified by wallet ID.
type: object
additionalProperties: false
required:
- walletId
properties:
walletId:
description: Unique identifier of the wallet.
type: string
Endpoint:
description: Polymorphic payment endpoint definition.
type: object
additionalProperties: false
required:
@@ -98,8 +125,10 @@ components:
- data
properties:
type:
description: Endpoint type determining the shape of the `data` field.
$ref: ../../external/endpoint_type.yaml#/components/schemas/EndpointType
data:
description: Endpoint payload corresponding to the selected endpoint type.
oneOf:
- $ref: ./payment.yaml#/components/schemas/LedgerEndpoint
- $ref: ./payment.yaml#/components/schemas/ManagedWalletEndpoint
@@ -108,36 +137,49 @@ components:
- $ref: ./payment.yaml#/components/schemas/CardTokenEndpoint
- $ref: ./payment.yaml#/components/schemas/WalletEndpoint
metadata:
description: Optional key-value metadata attached to the endpoint.
type: object
additionalProperties:
type: string
Customer:
description: Customer identity and address attributes for compliance and routing.
type: object
additionalProperties: false
properties:
id:
description: External or internal customer identifier.
type: string
first_name:
description: Customer first name.
type: string
middle_name:
description: Customer middle name.
type: string
last_name:
description: Customer last name.
type: string
ip:
description: Source IP address associated with the payment request.
type: string
zip:
description: Postal or ZIP code.
type: string
country:
description: Country code of customer residence or billing address.
type: string
state:
description: State or region.
type: string
city:
description: City name.
type: string
address:
description: Street address line.
type: string
CurrencyPair:
description: Foreign-exchange currency pair.
type: object
additionalProperties: false
required:
@@ -145,11 +187,14 @@ components:
- quote
properties:
base:
description: Base currency code.
type: string
quote:
description: Quote currency code.
type: string
FxIntent:
description: FX execution preferences associated with a payment.
type: object
additionalProperties: false
required:
@@ -157,21 +202,28 @@ components:
- side
properties:
pair:
description: Currency pair for FX conversion.
$ref: ./payment.yaml#/components/schemas/CurrencyPair
side:
description: Side of the FX trade intent.
$ref: ../../external/fx_side.yaml#/components/schemas/FxSide
firm:
description: Whether a firm quote is required.
type: boolean
ttl_ms:
description: Desired quote time-to-live in milliseconds.
type: integer
format: int64
preferred_provider:
description: Preferred FX liquidity provider identifier.
type: string
max_age_ms:
description: Maximum accepted quote age in milliseconds.
type: integer
format: int32
PaymentIntent:
description: Requested payment execution parameters before processing.
type: object
additionalProperties: false
required:
@@ -181,160 +233,219 @@ components:
- amount
properties:
kind:
description: High-level payment kind that drives orchestration behavior.
$ref: ../../external/payment_kind.yaml#/components/schemas/PaymentKind
source:
description: Source endpoint from which funds are debited.
$ref: ./payment.yaml#/components/schemas/Endpoint
destination:
description: Destination endpoint to which funds are credited.
$ref: ./payment.yaml#/components/schemas/Endpoint
amount:
description: Requested payment amount.
$ref: ../common/money.yaml#/components/schemas/Money
fx:
description: Optional FX preferences when conversion is needed.
$ref: ./payment.yaml#/components/schemas/FxIntent
settlement_mode:
description: Settlement strategy for the transfer.
$ref: ../../external/settlement_mode.yaml#/components/schemas/SettlementMode
fee_treatment:
description: How fees should be applied to source/destination amounts.
type: string
enum:
- unspecified
- add_to_source
- deduct_from_destination
attributes:
description: Additional key-value attributes used by downstream processors.
type: object
additionalProperties:
type: string
customer:
description: Optional customer information attached to the payment intent.
$ref: ./payment.yaml#/components/schemas/Customer
FeeLine:
description: Single fee component included in a payment quote.
type: object
additionalProperties: false
properties:
ledgerAccountRef:
description: Ledger account reference receiving or charging this fee.
type: string
amount:
description: Monetary amount of the fee line.
$ref: ../common/money.yaml#/components/schemas/Money
lineType:
description: Fee line classification.
type: string
side:
description: Indicates whether the fee applies on debit or credit side.
type: string
meta:
description: Optional metadata for fee calculation traceability.
type: object
additionalProperties:
type: string
QuoteFees:
description: Collection of fee lines for a quoted payment.
type: object
additionalProperties: false
properties:
lines:
description: List of individual quoted fee lines.
type: array
items:
$ref: ./payment.yaml#/components/schemas/FeeLine
QuoteAmounts:
description: Amount breakdown returned with a payment quote.
type: object
additionalProperties: false
properties:
sourcePrincipal:
description: Principal amount debited from the source before fees.
$ref: ../common/money.yaml#/components/schemas/Money
sourceDebitTotal:
description: Total amount debited from the source including adjustments.
$ref: ../common/money.yaml#/components/schemas/Money
destinationSettlement:
description: Net amount expected to settle at the destination.
$ref: ../common/money.yaml#/components/schemas/Money
FxQuote:
description: FX quote details used to price converted payment amounts.
type: object
additionalProperties: false
properties:
quoteRef:
description: Identifier of the FX quote.
type: string
baseCurrency:
description: Base currency of the quote pair.
type: string
quoteCurrency:
description: Quote currency of the quote pair.
type: string
side:
description: FX side used for pricing.
type: string
price:
description: Quoted FX price as a decimal string.
type: string
baseAmount:
description: Base currency amount covered by this quote.
$ref: ../common/money.yaml#/components/schemas/Money
quoteAmount:
description: Quote currency amount corresponding to the base amount.
$ref: ../common/money.yaml#/components/schemas/Money
expiresAtUnixMs:
description: Quote expiration timestamp in Unix milliseconds.
type: integer
format: int64
pricedAtUnixMs:
description: Quote pricing timestamp in Unix milliseconds.
type: integer
format: int64
provider:
description: Provider that supplied the FX quote.
type: string
rateRef:
description: Provider-specific rate reference identifier.
type: string
firm:
description: Indicates whether the FX quote is firm.
type: boolean
PaymentQuote:
description: Pricing result for a specific payment intent.
type: object
additionalProperties: false
properties:
quoteRef:
description: Unique identifier of the payment quote.
type: string
intentRef:
description: Reference to the payment intent used for quoting.
type: string
amounts:
description: Amount breakdown for source and destination values.
$ref: ./payment.yaml#/components/schemas/QuoteAmounts
fees:
description: Fee breakdown included in the quote.
$ref: ./payment.yaml#/components/schemas/QuoteFees
fxQuote:
description: Associated FX quote when conversion is part of the payment.
$ref: ./payment.yaml#/components/schemas/FxQuote
PaymentOperation:
description: Execution step record captured during payment processing.
type: object
additionalProperties: false
properties:
stepRef:
description: Identifier of the orchestration step.
type: string
code:
description: Operation code representing the executed action.
type: string
state:
description: Current state of the operation.
type: string
label:
description: Human-readable operation label.
type: string
failureCode:
description: Machine-readable failure code when operation fails.
type: string
failureReason:
description: Human-readable failure reason when operation fails.
type: string
startedAt:
description: RFC 3339 timestamp when execution of the operation started.
type: string
format: date-time
completedAt:
description: RFC 3339 timestamp when execution of the operation completed.
type: string
format: date-time
Payment:
description: Persisted payment aggregate with status, quote, and operation history.
type: object
additionalProperties: false
properties:
paymentRef:
description: Unique payment reference identifier.
type: string
idempotencyKey:
description: Idempotency key used to safely deduplicate create requests.
type: string
state:
description: Current lifecycle state of the payment.
$ref: ../../external/payment_state.yaml#/components/schemas/PaymentState
failureCode:
description: Failure code set when the payment cannot be completed.
type: string
failureReason:
description: Human-readable explanation of the failure.
type: string
operations:
description: Chronological list of operations executed for this payment.
type: array
items:
$ref: ./payment.yaml#/components/schemas/PaymentOperation
lastQuote:
description: Most recent payment quote used or generated for the payment.
$ref: ./payment.yaml#/components/schemas/PaymentQuote
createdAt:
description: RFC 3339 timestamp when the payment was created.
type: string
format: date-time
meta:
description: Additional metadata captured for the payment.
type: object
additionalProperties:
type: string

View File

@@ -1,9 +1,11 @@
components:
schemas:
PermissionBound:
description: Association between an organization and a permission, including archival state.
allOf:
- $ref: ./storable.yaml#/components/schemas/Storable
- type: object
description: Permission binding details scoped to an organization.
additionalProperties: false
required:
- organizationRef
@@ -11,8 +13,11 @@ components:
- isArchived
properties:
organizationRef:
description: Identifier of the organization that owns this permission binding.
$ref: ./objectid.yaml#/components/schemas/ObjectId
permissionRef:
description: Identifier of the permission granted to the organization.
$ref: ./objectid.yaml#/components/schemas/ObjectId
isArchived:
description: Indicates whether this object is archived and inactive.
type: boolean

View File

@@ -1,6 +1,7 @@
components:
schemas:
Storable:
description: Common persistence metadata included on stored entities.
type: object
additionalProperties: false
required:
@@ -9,10 +10,13 @@ components:
- updatedAt
properties:
id:
description: Unique identifier assigned to the stored entity.
$ref: ./objectid.yaml#/components/schemas/ObjectId
createdAt:
description: RFC 3339 timestamp indicating when the entity was created.
type: string
format: date-time
updatedAt:
description: RFC 3339 timestamp indicating the most recent entity update.
type: string
format: date-time