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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ node_modules

# Internal Claude Code skills (per-repo)
.claude/

# Local Redis dump artifact — never commit
dump.rdb
10 changes: 10 additions & 0 deletions e2e/reliability_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ var auditConsumerSpec = map[string]auditConsumerExpectation{
// signature-passed-but-team-unknown signal. No customer email: the
// affected "customer" either does not exist or was deleted.
"razorpay.webhook.team_not_found": {IntentionallyNoConsumer: true},

// CI-only ephemeral-test-account surface (guarded; inert by default).
// Both fire from the internal POST/DELETE /internal/e2e/account routes
// and are operator-internal observability signals — a spike in created
// (vs reaped) means CI is leaking test accounts. NEVER customer-facing:
// the team is always is_test_cohort and the synthetic email is not PII.
// Counterparts to the other operator-only kinds above — audit rows are
// dashboard signals, not customer notifications.
"e2e.account.created": {IntentionallyNoConsumer: true},
"e2e.account.reaped": {IntentionallyNoConsumer: true},
}

// ─── Test 1: every constant has a spec entry ──────────────────────────────────
Expand Down
26 changes: 26 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,29 @@ type Config struct {
// WORKER_INTERNAL_JWT_SECRET in BOTH the api and the worker
// (same value, generated via `openssl rand -hex 32`).
WorkerInternalJWTSecret string

// E2EAccountToken is the shared secret that guards the CI-only
// ephemeral-test-account surface (POST/DELETE /internal/e2e/account).
// CI mints real test-cohort accounts against PRODUCTION to run
// integration tests, then reaps them — that is the only thing this
// token authorizes.
//
// INERT BY DEFAULT (flag-protection): when this is empty, BOTH e2e
// routes return 404 for every request, hiding the endpoint's
// existence entirely. The endpoint cannot mint or reap a single
// account until an operator sets E2E_ACCOUNT_TOKEN — so the surface
// ships safe-by-default and is only "armed" in the environments
// (CI/prod) where the secret is wired. The caller authenticates by
// sending the exact value in the X-E2E-Token request header; the
// handler does a crypto/subtle constant-time compare and 404s on any
// mismatch (never 401/403 — a distinguishable status would leak that
// the route exists).
//
// Distinct secret from JWTSecret and WorkerInternalJWTSecret: this
// one authorizes account *creation/destruction*, a strictly more
// dangerous capability than session-signing, so it gets its own key
// and its own k8s Secret entry (generate via `openssl rand -hex 32`).
E2EAccountToken string
}

// ErrMissingConfig is returned when a required env var is absent.
Expand Down Expand Up @@ -427,6 +450,9 @@ func Load() *Config {
cfg.SendGridWebhookKey = os.Getenv("SENDGRID_WEBHOOK_PUBLIC_KEY")

cfg.WorkerInternalJWTSecret = strings.TrimSpace(os.Getenv("WORKER_INTERNAL_JWT_SECRET"))
// E2E_ACCOUNT_TOKEN: empty = the /internal/e2e/* surface is inert
// (every call 404s). See Config.E2EAccountToken for the full posture.
cfg.E2EAccountToken = strings.TrimSpace(os.Getenv("E2E_ACCOUNT_TOKEN"))
cfg.DeployDomain = getenv("DEPLOY_DOMAIN", "instant.dev")
cfg.ComputeProvider = getenv("COMPUTE_PROVIDER", "noop")
cfg.KubeNamespaceApps = getenv("KUBE_NAMESPACE_APPS", "instant-apps")
Expand Down
14 changes: 14 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func allKeys() []string {
"BREVO_WEBHOOK_SECRET", "SES_SNS_SUBSCRIPTION_ARN",
"SENDGRID_WEBHOOK_PUBLIC_KEY",
"WORKER_INTERNAL_JWT_SECRET", "ADMIN_PATH_PREFIX",
"E2E_ACCOUNT_TOKEN",
}
}

Expand Down Expand Up @@ -196,6 +197,19 @@ func TestConfig_IsServiceEnabled(t *testing.T) {
}
}

func TestLoad_E2EAccountToken(t *testing.T) {
// Unset → empty (inert-by-default: the /internal/e2e/* surface 404s).
applyBaselineEnv(t, nil)
if got := Load().E2EAccountToken; got != "" {
t.Errorf("E2EAccountToken default: want empty (inert), got %q", got)
}
// Set (with surrounding whitespace) → trimmed value.
applyBaselineEnv(t, map[string]string{"E2E_ACCOUNT_TOKEN": " secret-token "})
if got := Load().E2EAccountToken; got != "secret-token" {
t.Errorf("E2EAccountToken: want trimmed 'secret-token', got %q", got)
}
}

func TestLoad_HappyPath_AppliesDefaults(t *testing.T) {
applyBaselineEnv(t, nil)
cfg := Load()
Expand Down
15 changes: 15 additions & 0 deletions internal/handlers/error_envelope_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ var coverageAllowlist = map[string]string{
// real handler call sites. Filtered by the test (see emitCode).
"code": "regex artefact — not a real emit",
"x": "regex artefact — not a real emit",

// CI-only ephemeral-test-account surface (POST/DELETE /internal/e2e/account).
// These codes are emitted only on the operator/CI-guarded endpoint, which is
// inert by default (404 unless E2E_ACCOUNT_TOKEN is set) and driven by the
// machine-to-machine E2E harness — never a customer agent. A customer-style
// "Tell the user … https://instanode.dev/…" agent_action would be wrong for
// a CI caller, so they intentionally carry no codeToAgentAction entry: the
// 503 arms fall back to AgentActionContactSupport, the 4xx arms to an empty
// agent_action with a self-explanatory message.
"not_test_cohort": "CI-only /internal/e2e/account reap-safety 403 (machine-to-machine; not customer-facing)",
"team_create_failed": "CI-only /internal/e2e/account mint 503 (machine-to-machine; not customer-facing)",
"user_create_failed": "CI-only /internal/e2e/account mint 503 (machine-to-machine; not customer-facing)",
"tier_not_allowed": "CI-only /internal/e2e/account gated-tier 400 (machine-to-machine; not customer-facing)",
"tier_set_failed": "CI-only /internal/e2e/account mint 503 (machine-to-machine; not customer-facing)",
"rand_failed": "CI-only /internal/e2e/account mint 503 (machine-to-machine; not customer-facing)",
}

// TestErrorCode_HasAgentAction is the registry-iterating coverage gate.
Expand Down
12 changes: 12 additions & 0 deletions internal/handlers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,18 @@ var codeToAgentAction = map[string]errorCodeMeta{
"reauth_required": {
AgentAction: "Tell the user this action requires a fresh session (admin-scope PAT mints need re-auth). Sign in again at https://instanode.dev/login — see https://instanode.dev/docs/auth.",
},

// NOTE: the CI-only ephemeral-test-account error codes (not_test_cohort,
// team_create_failed, user_create_failed, tier_not_allowed, tier_set_failed,
// rand_failed) are deliberately NOT registered here. codeToAgentAction holds
// CUSTOMER-facing agent guidance — the contract test (TestAgentActionContract)
// requires every entry to start "Tell the user …" and carry a customer
// recovery URL. The /internal/e2e/account surface is operator/CI-only and
// inert by default (404 unless E2E_ACCOUNT_TOKEN is set), so its codes never
// reach a customer agent: the 503 arms already get the generic
// AgentActionContactSupport via respondError's status>=500 fallback, and the
// 4xx arms (400/403/429) carry a self-explanatory message with no
// agent_action — correct for a machine-to-machine CI caller.
}

// ErrorResponse is the canonical JSON shape for every 4xx/5xx response.
Expand Down
Loading
Loading