api

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 26, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultTimeout = 10 * time.Second
	MaxRetries     = 3
	KeyPrefixLive  = "ara_live_"
	KeyPrefixTest  = "ara_test_"

	// DefaultMCPDryRunURL and MCPDryRunKey produce a Client that can be
	// constructed without credentials, suitable only for inspecting tool
	// registration via `arara mcp --dry-run`. The key is deliberately invalid
	// so it cannot be used against a live API.
	DefaultMCPDryRunURL = "https://invalid.local"
	MCPDryRunKey        = "ara_test_DRY_RUN_INSPECT_ONLY"
)

Variables

View Source
var ErrCaptureNotSupported = errors.New("backend does not support webhook capture listeners (RFC 0003)")

ErrCaptureNotSupported is returned when the backend doesn't yet expose the capture-listener endpoints. The CLI uses it to fall back to passive SSE mode without breaking the user flow.

View Source
var ErrOAuthNotSupported = errors.New("OAuth device flow endpoint not found on this backend (older deploy?). Run 'arara login --key <api-key>' as fallback")

Functions

func IsAuthError

func IsAuthError(err error) bool

func IsNotFoundError

func IsNotFoundError(err error) bool

func NormalizeReceiver

func NormalizeReceiver(receiver string) string

func RedactJSONForLog

func RedactJSONForLog(raw []byte) []byte

RedactJSONForLog returns a copy of raw with sensitive field values replaced by "<REDACTED>". When raw is not valid JSON it returns a placeholder noting the byte length, so opaque token formats never reach stderr by accident.

The redaction walks nested objects and arrays. Empty input returns an empty slice so verbose log lines render cleanly without extra branches at the call site.

Types

type APIError

type APIError struct {
	StatusCode int
	Code       string `json:"code"`
	Message    string `json:"message"`
	RawBody    string
	RetryAfter time.Duration
}

func ParseErrorResponse

func ParseErrorResponse(statusCode int, body []byte) *APIError

func (*APIError) Error

func (apiError *APIError) Error() string

func (*APIError) FriendlyMessage

func (apiError *APIError) FriendlyMessage() string

type APIKeyInfo

type APIKeyInfo struct {
	ID         string `json:"id"`
	Prefix     string `json:"prefix"`
	LastFour   string `json:"lastFour"`
	Mode       string `json:"mode"`
	CreatedAt  string `json:"createdAt"`
	LastUsedAt string `json:"lastUsedAt,omitempty"`
}

type CampaignContact

type CampaignContact struct {
	To        string   `json:"to"`
	Variables []string `json:"variables,omitempty"`
}

type CampaignEstimateResponse

type CampaignEstimateResponse struct {
	TemplateCost     float64 `json:"templateCost"`
	AraraFee         float64 `json:"araraFee"`
	UnitPrice        float64 `json:"unitPrice"`
	TotalCost        float64 `json:"totalCost"`
	TemplateCategory string  `json:"templateCategory"`
	RecipientCount   int     `json:"recipientCount"`
}

type CampaignResponse

type CampaignResponse struct {
	ID             string  `json:"id"`
	Name           string  `json:"name"`
	Status         string  `json:"status"`
	TemplateName   string  `json:"templateName,omitempty"`
	TemplateBody   string  `json:"templateBody,omitempty"`
	TotalMessages  int     `json:"totalMessages"`
	SentCount      int     `json:"sentCount,omitempty"`
	DeliveredCount int     `json:"deliveredCount,omitempty"`
	ReadCount      int     `json:"readCount,omitempty"`
	ClickedCount   int     `json:"clickedCount,omitempty"`
	ConvertedCount int     `json:"convertedCount,omitempty"`
	ConvertedValue float64 `json:"convertedValue,omitempty"`
	TotalCost      float64 `json:"totalCost"`
	ScheduledAt    string  `json:"scheduledAt,omitempty"`
	StartedAt      string  `json:"startedAt,omitempty"`
	FinishedAt     string  `json:"finishedAt,omitempty"`
	CreatedAt      string  `json:"createdAt,omitempty"`
}

CampaignResponse is the unified shape used for create/list/get. Fields are a superset of what the three endpoints actually populate:

  • POST /v1/campaigns returns the small CampaignResponse DTO (id, name, status, totalMessages, totalCost) — extra fields stay zero.
  • GET /v1/campaigns returns the rich CampaignListItem (adds templateName, sentCount, createdAt) inside a Spring page wrapper — see ListCampaigns for the unwrap step.
  • GET /v1/campaigns/{id} returns CampaignDetailResponse with the full funnel (delivered/read/clicked/converted) plus timestamps.

Keeping a single Go struct avoids forcing every caller to choose between three near-identical types. Callers should treat any field they don't expect for their endpoint as "zero, ignore".

func (*CampaignResponse) FailedCount

func (campaign *CampaignResponse) FailedCount() int

FailedCount reports how many messages of the campaign never reached the "sent" state. The backend doesn't expose failedCount directly — it's inferred from totalMessages minus sentCount. Returns 0 if the CampaignResponse came from an endpoint that doesn't populate sentCount (e.g. POST /v1/campaigns), so display code should guard with a check like `if campaign.SentCount > 0 || campaign.TotalMessages == 0`.

type CaptureConflictError

type CaptureConflictError struct {
	ListenerID string
	Owner      string
	StartedAt  string
	ExpiresAt  string
	Message    string
}

CaptureConflictError is returned when another CLI session is already holding the org's capture slot. The CLI displays the conflict details (owner, expiresAt) so the user can decide whether to wait or kill the other session.

func (*CaptureConflictError) Error

func (err *CaptureConflictError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(baseURL string, apiKey string) *Client

func NewClientFromConfig

func NewClientFromConfig() (*Client, error)

func (*Client) BaseURL

func (client *Client) BaseURL() string

func (*Client) CreateAPIKey

func (client *Client) CreateAPIKey(mode, name string) (*GeneratedAPIKey, error)

func (*Client) CreateCampaign

func (client *Client) CreateCampaign(request CreateCampaignRequest, idempotencyKey string) (*CampaignResponse, error)

func (*Client) CreateWebhookListener

func (client *Client) CreateWebhookListener(req CreateWebhookListenerRequest) (*CreateWebhookListenerResponse, error)

CreateWebhookListener registers an ephemeral capture session. Returns ErrCaptureNotSupported on 404 (graceful fallback) and CaptureConflictError on 409 (another session holds the slot).

func (*Client) Delete

func (client *Client) Delete(path string) error

func (*Client) DeleteTemplate

func (client *Client) DeleteTemplate(templateID string) error

func (*Client) Do

func (client *Client) Do(method string, path string, body any, result any) error

func (*Client) DoWithHeaders

func (client *Client) DoWithHeaders(method string, path string, body any, result any, headers map[string]string) error

func (*Client) EstimateCampaign

func (client *Client) EstimateCampaign(templateName string, recipientCount int) (*CampaignEstimateResponse, error)

func (*Client) Get

func (client *Client) Get(path string, result any) error

func (*Client) GetCampaign

func (client *Client) GetCampaign(campaignID string) (*CampaignResponse, error)

func (*Client) GetContact

func (client *Client) GetContact(phone string) (*ContactResponse, error)

func (*Client) GetContactStats

func (client *Client) GetContactStats() (map[string]int64, error)

func (*Client) GetMessageStatus

func (client *Client) GetMessageStatus(messageID string) (*MessageResponse, error)

func (*Client) GetMessages

func (client *Client) GetMessages(mode string, page int, size int) (map[string]any, error)

func (*Client) GetMetrics

func (client *Client) GetMetrics(mode string) (map[string]any, error)

func (*Client) GetTemplateStatus

func (client *Client) GetTemplateStatus(templateID string) (*TemplateStatusResponse, error)

func (*Client) GetWalletBalance

func (client *Client) GetWalletBalance(mode string) (map[string]any, error)

func (*Client) HeartbeatWebhookListener

func (client *Client) HeartbeatWebhookListener(listenerID string) error

HeartbeatWebhookListener extends the listener's TTL. Returns nil on 204 success. On 410 (already expired), returns a sentinel so the CLI can surface "listener expired, re-run with --capture to grab a fresh slot".

func (*Client) ImportContacts

func (client *Client) ImportContacts(contacts []ContactRequest) (string, error)

func (*Client) IsLiveMode

func (client *Client) IsLiveMode() bool

func (*Client) ListAPIKeys

func (client *Client) ListAPIKeys() ([]APIKeyInfo, error)

func (*Client) ListCampaigns

func (client *Client) ListCampaigns() ([]CampaignResponse, error)

func (*Client) ListContacts

func (client *Client) ListContacts(query string, page int, size int) (*ContactsListResponse, error)

func (*Client) ListNumbers

func (client *Client) ListNumbers() ([]PhoneNumberResponse, error)

func (*Client) ListOrganizations

func (client *Client) ListOrganizations() ([]Organization, error)

ListOrganizations fetches every organization accessible to the current authenticated user. Used by `arara org list` and `arara org use`.

func (*Client) ListTemplates

func (client *Client) ListTemplates() ([]Template, error)

func (*Client) Mode

func (client *Client) Mode() string

func (*Client) PollDeviceToken

func (client *Client) PollDeviceToken(options PollOptions) (*TokenResponse, error)

func (*Client) Post

func (client *Client) Post(path string, body any, result any) error

func (*Client) RefreshDeviceToken

func (client *Client) RefreshDeviceToken(clientID, refreshToken string) (*TokenResponse, error)

func (*Client) ReleaseWebhookListener

func (client *Client) ReleaseWebhookListener(listenerID string) error

ReleaseWebhookListener tells the backend the CLI is done. Idempotent on the backend side. Best-effort from the CLI's perspective: failure here just means the listener will expire naturally on its TTL.

func (*Client) RequestDeviceCode

func (client *Client) RequestDeviceCode(clientID, scope string) (*DeviceCodeResponse, error)

func (*Client) RevokeAPIKey

func (client *Client) RevokeAPIKey(keyID string) error

func (*Client) SendMessage

func (client *Client) SendMessage(request SendMessageRequest) (*MessageResponse, error)

func (*Client) SetVerbose

func (client *Client) SetVerbose(enabled bool)

func (*Client) StreamEvents

func (client *Client) StreamEvents(ctx context.Context) (<-chan SSEEvent, error)

StreamEvents connects to the SSE endpoint and returns a channel of events. The channel is closed when the context is cancelled or the connection is permanently lost. Temporary disconnections are retried with exponential backoff.

type ContactRequest

type ContactRequest struct {
	Name       string         `json:"name"`
	Phone      string         `json:"phone"`
	Email      string         `json:"email,omitempty"`
	Attributes map[string]any `json:"attributes,omitempty"`
}

type ContactResponse

type ContactResponse struct {
	ID         string         `json:"id"`
	Name       string         `json:"name"`
	Phone      string         `json:"phone"`
	Email      string         `json:"email,omitempty"`
	Attributes map[string]any `json:"attributes,omitempty"`
	CreatedAt  string         `json:"createdAt"`
}

type ContactsListResponse

type ContactsListResponse struct {
	Contacts   []ContactResponse `json:"contacts"`
	Total      int64             `json:"total"`
	Page       int               `json:"page"`
	Size       int               `json:"size"`
	TotalPages int               `json:"totalPages"`
}

type CreateCampaignRequest

type CreateCampaignRequest struct {
	Name         string            `json:"name"`
	TemplateName string            `json:"templateName"`
	Contacts     []CampaignContact `json:"contacts"`
}

type CreateWebhookListenerRequest

type CreateWebhookListenerRequest struct {
	Owner      string `json:"owner,omitempty"`
	TTLSeconds int    `json:"ttlSeconds,omitempty"`
}

CreateWebhookListenerRequest is the body of POST /v1/cli/webhook-listeners. Both fields are optional — the backend supplies sensible defaults when they're empty.

type CreateWebhookListenerResponse

type CreateWebhookListenerResponse struct {
	ID                       string `json:"id"`
	Secret                   string `json:"secret"`
	OrganizationID           string `json:"organizationId"`
	ExpiresAt                string `json:"expiresAt"`
	HeartbeatIntervalSeconds int    `json:"heartbeatIntervalSeconds"`
}

CreateWebhookListenerResponse is the 201 body. Secret is shown ONCE and must not be persisted to disk by the CLI — it dies with the process.

type DeviceCodeRequest

type DeviceCodeRequest struct {
	ClientID string `json:"clientId"`
	Scope    string `json:"scope,omitempty"`
}

type DeviceCodeResponse

type DeviceCodeResponse struct {
	DeviceCode              string `json:"deviceCode"`
	UserCode                string `json:"userCode"`
	VerificationURI         string `json:"verificationUri"`
	VerificationURIComplete string `json:"verificationUriComplete,omitempty"`
	ExpiresIn               int    `json:"expiresIn"`
	Interval                int    `json:"interval"`
}

type DeviceTokenRequest

type DeviceTokenRequest struct {
	ClientID   string `json:"clientId"`
	DeviceCode string `json:"deviceCode"`
}

type DryRunPreview

type DryRunPreview struct {
	Method        string                    `json:"method"`
	Path          string                    `json:"path"`
	Payload       SendMessageRequest        `json:"payload"`
	EstimatedCost *CampaignEstimateResponse `json:"estimatedCost,omitempty"`
	EstimateError string                    `json:"estimateError,omitempty"`
}

type GeneratedAPIKey

type GeneratedAPIKey struct {
	PlainTextKey string `json:"plainTextKey"`
}

type MessageResponse

type MessageResponse struct {
	ID           string  `json:"id"`
	Receiver     string  `json:"receiver"`
	TemplateName string  `json:"templateName,omitempty"`
	Body         string  `json:"body,omitempty"`
	Status       string  `json:"status"`
	MessageType  string  `json:"messageType,omitempty"`
	Mode         string  `json:"mode,omitempty"`
	Sender       string  `json:"sender,omitempty"`
	Cost         float64 `json:"cost,omitempty"`
	CreatedAt    string  `json:"createdAt,omitempty"`
}

func (*MessageResponse) UnmarshalJSON

func (response *MessageResponse) UnmarshalJSON(data []byte) error

UnmarshalJSON tolerates `"id": null` from the backend. The Kotlin DTO declares `id: String?`, so although today every response has the id populated, the contract permits null. Without this custom unmarshaller, `json.Unmarshal` fails on null with "cannot unmarshal null into string" — we'd silently turn a successful send into an error. Mapping null → "" lets the CLI surface a "no id returned" message to the user instead.

type Organization

type Organization struct {
	ID   string `json:"id"`
	Slug string `json:"slug"`
	Name string `json:"name"`
	Role string `json:"role"`
	Mode string `json:"mode"`
}

Organization is a single org the authenticated user belongs to. Returned by GET /auth/me/organizations. Backend reads the user_organizations M2M table and unions in the user's primary org, so a single user can show up in N orgs with different roles per org. The CLI uses this to drive `arara org list` / `arara org use`.

type PhoneNumberResponse

type PhoneNumberResponse struct {
	ID          string `json:"id"`
	PhoneNumber string `json:"phoneNumber"`
	Name        string `json:"name"`
}

type PollOptions

type PollOptions struct {
	ClientID   string
	DeviceCode string
	Interval   time.Duration
	ExpiresIn  time.Duration
	Sleep      func(time.Duration)
	Now        func() time.Time
	OnSlowDown func(newInterval time.Duration)
}

type RefreshTokenRequest

type RefreshTokenRequest struct {
	ClientID     string `json:"clientId"`
	RefreshToken string `json:"refreshToken"`
}

type SSEEvent

type SSEEvent struct {
	Event     string
	Data      string
	Signature string
}

SSEEvent represents a single Server-Sent Event received from the stream.

When the backend wraps an event in an envelope (RFC 0003), Data carries only the inner payload JSON (the part the customer's webhook handler would normally receive) and Signature carries the pre-computed `sha256=<hex>` HMAC. Old-format events leave Signature empty and Data holds the raw `data:` line as-is.

type SendMessageRequest

type SendMessageRequest struct {
	Receiver          string   `json:"receiver"`
	TemplateName      string   `json:"templateName,omitempty"`
	TemplateVariables []string `json:"variables,omitempty"`
	Body              string   `json:"body,omitempty"`
	ScheduledAt       string   `json:"scheduledAt,omitempty"`
}

type Template

type Template struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	Category       string `json:"category"`
	Language       string `json:"language"`
	Body           string `json:"body"`
	Header         string `json:"header,omitempty"`
	Footer         string `json:"footer,omitempty"`
	ProviderStatus string `json:"providerStatus,omitempty"`
	CreatedAt      string `json:"createdAt,omitempty"`
}

type TemplateStatusResponse

type TemplateStatusResponse struct {
	Status          string `json:"status"`
	RejectionReason string `json:"rejectionReason,omitempty"`
	Category        string `json:"category"`
}

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken,omitempty"`
	TokenType    string `json:"tokenType,omitempty"`
	ExpiresIn    int    `json:"expiresIn,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL