Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions internal/email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,11 @@ func clampAttemptCount(n int) int {
// TEXT column on the table, so adding a new kind never needs a migration —
// the only invariant is "operators can filter by kind in the dashboard".
const (
EmailSendKindPaymentFailed = "payment_failed"
EmailSendKindPaymentReceipt = "receipt"
EmailSendKindTeamInvite = "team_invite"
EmailSendKindDeletionConfirm = "deletion_confirm"
EmailSendKindMagicLink = "magic_link"
EmailSendKindPaymentFailed = "payment_failed"
EmailSendKindPaymentReceipt = "receipt"
EmailSendKindTeamInvite = "team_invite"
EmailSendKindDeletionConfirm = "deletion_confirm"
EmailSendKindMagicLink = "magic_link"
)

// SendPaymentFailed sends a payment failure notification email.
Expand Down Expand Up @@ -728,7 +728,7 @@ Receipt

View your billing details: https://instanode.dev/app/billing

Need help? Reply to this email or contact support@instanode.dev.
Need help? Reply to this email or contact contact@instanode.dev.

— The instanode.dev team
`, headline, leadPlain, receipt.Plan, amountPlain, receipt.Period)
Expand All @@ -752,7 +752,7 @@ Need help? Reply to this email or contact support@instanode.dev.
</p>
<p style="margin-top:24px;color:#666;font-size:13px;">
Need help? Reply to this email or contact
<a href="mailto:support@instanode.dev" style="color:#444;">support@instanode.dev</a>.
<a href="mailto:contact@instanode.dev" style="color:#444;">contact@instanode.dev</a>.
</p>
<p style="margin-top:40px;color:#666;font-size:13px;">— The instanode.dev team</p>
</body>
Expand Down
2 changes: 1 addition & 1 deletion internal/handlers/agent_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ const AgentActionBindingLookupFailed = "Tell the user the platform couldn't reso
// the middleware can't import handlers (cycle), so both sides keep their
// own copy. The contract test asserts only one of the two copies; touching
// either without the other is the regression we want CI to catch.
const AgentActionAdminRequired = "Tell the user this endpoint requires platform-admin access. Ask support@instanode.dev via https://instanode.dev/support if you think this is wrong."
const AgentActionAdminRequired = "Tell the user this endpoint requires platform-admin access. Ask contact@instanode.dev via https://instanode.dev/support if you think this is wrong."

// newAgentActionAdminTierChanged is returned in the success response of
// POST /api/v1/admin/customers/:team_id/tier so the calling agent has
Expand Down
14 changes: 7 additions & 7 deletions internal/handlers/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ func (h *BillingHandler) CreateCheckoutAPI(c *fiber.Ctx) error {
// "contact sales / not yet available" message instead of telling
// the user they made a typo.
return respondError(c, fiber.StatusBadRequest, "tier_not_yet_available",
"The Team plan is not yet available for self-serve checkout — contact support@instanode.dev.")
"The Team plan is not yet available for self-serve checkout — contact contact@instanode.dev.")
default:
return respondError(c, fiber.StatusBadRequest, "invalid_plan", "plan must be 'hobby', 'hobby_plus', or 'pro'")
}
Expand Down Expand Up @@ -3141,7 +3141,7 @@ func resolveTeamFromNotes(ctx context.Context, h *BillingHandler, sub rzpSubscri
// dashboard, executed by support staff, which fires the subscription.cancelled
// webhook → handleSubscriptionCancelled in RazorpayWebhook (unchanged).
//
// The dashboard surfaces cancellation as a mailto:support@instanode.dev link,
// The dashboard surfaces cancellation as a mailto:contact@instanode.dev link,
// not as a button that calls this API.
//
// If a future internal flow (RTBF / team deletion) needs to cancel a
Expand Down Expand Up @@ -3461,7 +3461,7 @@ func (h *BillingHandler) ChangePlanAPI(c *fiber.Ctx) error {
// OK — fall through.
case "yearly":
return respondError(c, fiber.StatusBadRequest, "yearly_change_plan_unsupported",
"Changing to a yearly plan via /change-plan is not yet supported. Cancel and use POST /api/v1/billing/checkout with plan_frequency='yearly', or contact support@instanode.dev.")
"Changing to a yearly plan via /change-plan is not yet supported. Cancel and use POST /api/v1/billing/checkout with plan_frequency='yearly', or contact contact@instanode.dev.")
default:
return respondError(c, fiber.StatusBadRequest, "invalid_frequency",
"plan_frequency must be 'monthly' or 'yearly'")
Expand Down Expand Up @@ -3489,7 +3489,7 @@ func (h *BillingHandler) ChangePlanAPI(c *fiber.Ctx) error {
// happens to be set in this environment.
if target == "team" {
return respondError(c, fiber.StatusBadRequest, "tier_not_yet_available",
"The Team plan is not yet available for self-serve plan changes — contact support@instanode.dev.")
"The Team plan is not yet available for self-serve plan changes — contact contact@instanode.dev.")
}
planIDs := h.razorpayPlanIDs()
if _, ok := planIDs[target]; !ok {
Expand All @@ -3505,9 +3505,9 @@ func (h *BillingHandler) ChangePlanAPI(c *fiber.Ctx) error {
targetRank := plans.Rank(target)
if currentRank >= 0 && targetRank >= 0 && targetRank <= currentRank {
return respondErrorWithAgentAction(c, fiber.StatusBadRequest, "downgrade_not_self_serve",
"Plan downgrades are handled by support, not self-serve. Email support@instanode.dev to change to a lower tier.",
"Tell the user that downgrading to a lower plan is support-assisted. Have them email support@instanode.dev with their team and the target plan.",
"mailto:support@instanode.dev")
"Plan downgrades are handled by support, not self-serve. Email contact@instanode.dev to change to a lower tier.",
"Tell the user that downgrading to a lower plan is support-assisted. Have them email contact@instanode.dev with their team and the target plan.",
"mailto:contact@instanode.dev")
}
// (Target=team is rejected above with tier_not_yet_available — the
// 2026-06-04 CEO re-gate. Only hobby/hobby_plus/pro upgrades reach here.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func TestBillingBlock_NoSelfServeCancelOrDowngradeRoute(t *testing.T) {
// paying team requesting a LOWER or EQUAL tier via the in-app change-plan path
// is rejected with downgrade_not_self_serve and routed to support, NOT
// silently dropped. Verified against billing.go:ChangePlanAPI (it returns 400
// downgrade_not_self_serve + a mailto:support@instanode.dev agent_action for
// downgrade_not_self_serve + a mailto:contact@instanode.dev agent_action for
// any target whose rank ≤ the current tier's rank).
func TestBillingBlock_ChangePlanRejectsDowngrade(t *testing.T) {
if billingBlockSkipNoDB(t) {
Expand Down
4 changes: 2 additions & 2 deletions internal/handlers/error_envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func TestErrorEnvelope_500_NoRetryAfter(t *testing.T) {
assert.NotEmpty(t, body["agent_action"],
"5xx must always carry an agent_action — registry entry preferred, fallback as floor")
if action, ok := body["agent_action"].(string); ok {
assert.Contains(t, action, "support@instanode.dev",
assert.Contains(t, action, "contact@instanode.dev",
"every 5xx agent_action — whether registry or fallback — names the support contact")
}
}
Expand Down Expand Up @@ -336,7 +336,7 @@ func TestErrorEnvelope_ContactSupportContract(t *testing.T) {
assert.Contains(t, s, "request_id",
"AgentActionContactSupport must name request_id so the user knows what to quote")
// 3. Exact next action.
assert.Contains(t, s, "support@instanode.dev",
assert.Contains(t, s, "contact@instanode.dev",
"AgentActionContactSupport must name the support email — that's the action")
// 4. Full https URL.
assert.Contains(t, s, "https://instanode.dev/",
Expand Down
38 changes: 19 additions & 19 deletions internal/handlers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type errorCodeMeta struct {
// Used by respondError when status >= 500 and the code is not in the
// registry. Keeps the agent_action field populated even for plumbing
// errors so the calling agent always has something concrete to relay.
const AgentActionContactSupport = "Tell the user something on our side went wrong. Email support@instanode.dev with this request_id and a brief description — see https://instanode.dev/support."
const AgentActionContactSupport = "Tell the user something on our side went wrong. Email contact@instanode.dev with this request_id and a brief description — see https://instanode.dev/support."

// codeToAgentAction maps respondError `code` values to the sentence the
// agent should surface and, where relevant, the upgrade URL. Codes absent
Expand Down Expand Up @@ -152,7 +152,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
// right "not yet available / contact sales" message. Refs: memory
// `project_team_plan_not_rolled_out_no_payment`.
"tier_not_yet_available": {
AgentAction: "Tell the user the Team plan is not yet available for self-serve purchase. They should contact support@instanode.dev — see https://instanode.dev/pricing.",
AgentAction: "Tell the user the Team plan is not yet available for self-serve purchase. They should contact contact@instanode.dev — see https://instanode.dev/pricing.",
UpgradeURL: "https://instanode.dev/pricing",
},
"events_query_failed": {
Expand Down Expand Up @@ -288,7 +288,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user this invitation has already been accepted — they're on the team. Have them open https://instanode.dev/app to see their resources.",
},
"already_claimed": {
AgentAction: "Tell the user these resources were already claimed by another account. If they believe this is wrong, have them email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user these resources were already claimed by another account. If they believe this is wrong, have them email contact@instanode.dev — see https://instanode.dev/support.",
},

// ── Expired / gone ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -663,7 +663,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user their session belongs to a different team than the storage token. Re-authenticate as the token's owning team — see https://instanode.dev/docs/auth.",
},
"env_load_failed": {
AgentAction: "Tell the user the persisted environment variables could not be loaded for this stack. Retry the redeploy in 30 seconds — see https://instanode.dev/status. If it keeps failing, email support@instanode.dev with the request_id.",
AgentAction: "Tell the user the persisted environment variables could not be loaded for this stack. Retry the redeploy in 30 seconds — see https://instanode.dev/status. If it keeps failing, email contact@instanode.dev with the request_id.",
},
"invalid_service": {
AgentAction: "Tell the user the service value is unknown. Use one of: postgres, redis, mongodb, queue, storage, webhook, vector — see https://instanode.dev/docs.",
Expand Down Expand Up @@ -876,7 +876,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user billing is not configured on this deployment. Operators must set RAZORPAY_KEY_ID / SECRET — see https://instanode.dev/docs/billing.",
},
"downgrade_not_self_serve": {
AgentAction: "Tell the user downgrades and cancellations are not self-serve. Email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user downgrades and cancellations are not self-serve. Email contact@instanode.dev — see https://instanode.dev/support.",
},
"yearly_change_plan_unsupported": {
AgentAction: "Tell the user yearly subscriptions can't switch plans inline. Cancel the current subscription, then start the new plan at https://instanode.dev/pricing.",
Expand All @@ -888,7 +888,7 @@ var codeToAgentAction = map[string]errorCodeMeta{

// ── Razorpay codes (kept as raw passthrough) ───────────────────────────
"razorpay_error": {
AgentAction: "Tell the user Razorpay returned an error completing the payment. Check the error message and retry, or contact support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user Razorpay returned an error completing the payment. Check the error message and retry, or contact contact@instanode.dev — see https://instanode.dev/support.",
},

// ── Validation 4xx: signature / state ──────────────────────────────────
Expand Down Expand Up @@ -939,7 +939,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user the platform database hit a transient error. Retry in 30 seconds with exponential backoff — see https://instanode.dev/status if it persists.",
},
"internal_error": {
AgentAction: "Tell the user something on our side went wrong. Email support@instanode.dev with this request_id, or check https://instanode.dev/status.",
AgentAction: "Tell the user something on our side went wrong. Email contact@instanode.dev with this request_id, or check https://instanode.dev/status.",
},
"lookup_failed": {
AgentAction: "Tell the user a lookup on the platform backend timed out. Retry in 30 seconds — see https://instanode.dev/status.",
Expand Down Expand Up @@ -988,7 +988,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user creating the restore failed. Retry in 60 seconds — see https://instanode.dev/status.",
},
"restore_failed": {
AgentAction: "Tell the user the restore did not complete. Retry in 60 seconds; if it persists email support@instanode.dev — see https://instanode.dev/status.",
AgentAction: "Tell the user the restore did not complete. Retry in 60 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/status.",
},
"deletion_request_failed": {
AgentAction: "Tell the user the team-deletion request failed to persist. Retry in 30 seconds — see https://instanode.dev/status.",
Expand All @@ -1000,10 +1000,10 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user recording the promote rejection failed. Retry the rejection in 30 seconds — see https://instanode.dev/status.",
},
"execute_failed": {
AgentAction: "Tell the user executing the action failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user executing the action failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"summary_failed": {
AgentAction: "Tell the user computing the summary failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user computing the summary failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"status_failed": {
AgentAction: "Tell the user reading the status failed. Retry in 30 seconds — see https://instanode.dev/status.",
Expand All @@ -1012,13 +1012,13 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user reading the resource status failed. Retry in 30 seconds — see https://instanode.dev/status.",
},
"tier_failed": {
AgentAction: "Tell the user updating the tier failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user updating the tier failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"upgrade_failed": {
AgentAction: "Tell the user the tier upgrade could not be applied right now. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user the tier upgrade could not be applied right now. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"revocation_failed": {
AgentAction: "Tell the user revoking the session failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user revoking the session failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"role_lookup_failed": {
AgentAction: "Tell the user a team-role lookup failed. Retry in 30 seconds — see https://instanode.dev/status.",
Expand All @@ -1027,13 +1027,13 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user a team lookup failed. Retry in 30 seconds — see https://instanode.dev/status.",
},
"team_creation_failed": {
AgentAction: "Tell the user creating the team failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user creating the team failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"team_has_no_users": {
AgentAction: "Tell the user this team has no users yet — add an owner before issuing operations against it. See https://instanode.dev/docs/team.",
},
"user_creation_failed": {
AgentAction: "Tell the user creating the user account failed. Retry in 30 seconds; if it persists email support@instanode.dev — see https://instanode.dev/support.",
AgentAction: "Tell the user creating the user account failed. Retry in 30 seconds; if it persists email contact@instanode.dev — see https://instanode.dev/support.",
},
"user_upsert_failed": {
AgentAction: "Tell the user upserting the user record failed. Retry in 30 seconds — see https://instanode.dev/status.",
Expand Down Expand Up @@ -1061,10 +1061,10 @@ var codeToAgentAction = map[string]errorCodeMeta{
},
// (deletion_token_invalid covered in the deletion-confirmed section above)
"encryption_failed": {
AgentAction: "Tell the user the encryption step failed. Retry in 30 seconds; if it persists email support@instanode.dev with this request_id — see https://instanode.dev/support.",
AgentAction: "Tell the user the encryption step failed. Retry in 30 seconds; if it persists email contact@instanode.dev with this request_id — see https://instanode.dev/support.",
},
"decrypt_failed": {
AgentAction: "Tell the user decrypting the stored credential failed. Retry in 30 seconds; if it persists email support@instanode.dev with this request_id — see https://instanode.dev/support.",
AgentAction: "Tell the user decrypting the stored credential failed. Retry in 30 seconds; if it persists email contact@instanode.dev with this request_id — see https://instanode.dev/support.",
},
"encryption_unavailable": {
AgentAction: "Tell the user the encryption backend is temporarily unavailable. Retry in 60 seconds — see https://instanode.dev/status.",
Expand Down Expand Up @@ -1127,7 +1127,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user the quota check failed. Retry in 30 seconds — see https://instanode.dev/status.",
},
"billing_persistence_failed": {
AgentAction: "Tell the user persisting the billing change failed. Retry the action in 30 seconds; if it persists email support@instanode.dev with this request_id — see https://instanode.dev/support.",
AgentAction: "Tell the user persisting the billing change failed. Retry the action in 30 seconds; if it persists email contact@instanode.dev with this request_id — see https://instanode.dev/support.",
},

// ── 429 rate-limited (canonical) ───────────────────────────────────────
Expand Down Expand Up @@ -1186,7 +1186,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
AgentAction: "Tell the user marking the deletion as confirmed failed. Retry in 30 seconds — see https://instanode.dev/status.",
},
"subscription_cancel_failed": {
AgentAction: "Tell the user cancelling the Razorpay subscription failed. The team-delete is paused; email support@instanode.dev so an operator can reconcile — see https://instanode.dev/support.",
AgentAction: "Tell the user cancelling the Razorpay subscription failed. The team-delete is paused; email contact@instanode.dev so an operator can reconcile — see https://instanode.dev/support.",
},

// ── Auth content-type gate (AUTH-163, CSRF). Per-IP rate-limit (AUTH-097/107)
Expand Down
Loading
Loading