diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 8da5cc4..ebf2e2b 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -21,6 +21,7 @@ import ( "instant.dev/internal/config" "instant.dev/internal/middleware" "instant.dev/internal/models" + "instant.dev/internal/urls" ) // --- Browser OAuth flow shared helpers --- @@ -34,7 +35,9 @@ const defaultReturnTo = "https://instanode.dev/login/callback" // Hardcoded rather than reading from cfg because the registered redirect_uri // at GitHub/Google is fixed at app-registration time — varying it per // deployment would require multiple OAuth apps. -const canonicalAPIBase = "https://api.instanode.dev" +// canonicalAPIBase is preserved as an alias of urls.PublicAPIBase so any +// external reference keeps compiling. New code should use urls.PublicAPIBase. +const canonicalAPIBase = urls.PublicAPIBase // allowedReturnOrigins is the static allowlist for ?return_to= validation. // Anything not on this list collapses to defaultReturnTo. The list is diff --git a/internal/handlers/cache.go b/internal/handlers/cache.go index 4716ae4..10b4b03 100644 --- a/internal/handlers/cache.go +++ b/internal/handlers/cache.go @@ -9,7 +9,6 @@ package handlers import ( "context" "database/sql" - "fmt" "log/slog" "time" @@ -20,6 +19,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" "instant.dev/internal/provisioner" @@ -68,7 +68,7 @@ func (h *CacheHandler) provisionCache(ctx context.Context, token, tier string) ( func (h *CacheHandler) NewCache(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("redis") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "Redis provisioning is coming in Phase 3. Sign up at https://instanode.dev/start to be notified.") + "Redis provisioning is coming in Phase 3. Sign up at "+urls.StartURLPrefix+" to be notified.") } start := time.Now() @@ -95,7 +95,7 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error { // ── Dedicated requires authentication ───────────────────────────────────── if body.Dedicated { return respondError(c, fiber.StatusPaymentRequired, "auth_required", - "isolated resources require an authenticated team. Sign up at https://instanode.dev/start") + "isolated resources require an authenticated team. Sign up at "+urls.StartURLPrefix) } // ── Anonymous path ───────────────────────────────────────────────────────── @@ -117,7 +117,7 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } // Decrypt the stored connection_url to return it in plaintext. @@ -230,7 +230,7 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/handlers/db.go b/internal/handlers/db.go index eeaf667..bcc1852 100644 --- a/internal/handlers/db.go +++ b/internal/handlers/db.go @@ -19,7 +19,6 @@ package handlers import ( "context" "database/sql" - "fmt" "log/slog" "time" @@ -30,6 +29,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" dbprovider "instant.dev/internal/providers/db" @@ -79,7 +79,7 @@ func (h *DBHandler) provisionDB(ctx context.Context, token, tier string) (*dbpro func (h *DBHandler) NewDB(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("postgres") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "Postgres provisioning is coming in Phase 2. Sign up at https://instanode.dev/start to be notified.") + "Postgres provisioning is coming in Phase 2. Sign up at "+urls.StartURLPrefix+" to be notified.") } start := time.Now() @@ -106,7 +106,7 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error { // ── Dedicated requires authentication ───────────────────────────────────── if body.Dedicated { return respondError(c, fiber.StatusPaymentRequired, "auth_required", - "isolated resources require an authenticated team. Sign up at https://instanode.dev/start") + "isolated resources require an authenticated team. Sign up at "+urls.StartURLPrefix) } // ── Anonymous path ───────────────────────────────────────────────────────── @@ -129,7 +129,7 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } // Decrypt the stored connection_url to return it in plaintext. @@ -229,7 +229,7 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/handlers/deploy.go b/internal/handlers/deploy.go index 25d4c52..83d113b 100644 --- a/internal/handlers/deploy.go +++ b/internal/handlers/deploy.go @@ -33,6 +33,7 @@ import ( "github.com/redis/go-redis/v9" "instant.dev/internal/config" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/providers/compute" "instant.dev/internal/providers/compute/k8s" @@ -115,7 +116,7 @@ func (h *DeployHandler) requireTeam(c *fiber.Ctx) (*models.Team, error) { teamIDStr := middleware.GetTeamID(c) if teamIDStr == "" { return nil, respondError(c, fiber.StatusUnauthorized, "unauthorized", - "A session token is required to deploy. Sign in at https://instanode.dev/start") + "A session token is required to deploy. Sign in at "+urls.StartURLPrefix) } teamUUID, err := parseTeamID(teamIDStr) if err != nil { @@ -183,7 +184,7 @@ func (h *DeployHandler) runDeploy(d *models.Deployment, tarball []byte) { func (h *DeployHandler) New(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("deploy") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "Container deployment is coming in Phase 6. Sign up at https://instanode.dev/start to be notified.") + "Container deployment is coming in Phase 6. Sign up at "+urls.StartURLPrefix+" to be notified.") } team, err := h.requireTeam(c) diff --git a/internal/handlers/internal_url.go b/internal/handlers/internal_url.go index e7ae743..ec803c0 100644 --- a/internal/handlers/internal_url.go +++ b/internal/handlers/internal_url.go @@ -1,6 +1,10 @@ package handlers -import "net/url" +import ( + "net/url" + + "instant.dev/internal/urls" +) // proxiedInternalURL rewrites a customer-facing public URL to the cluster-internal // address of the per-protocol proxy. Workloads deployed inside the same cluster @@ -26,13 +30,13 @@ func proxiedInternalURL(publicURL, resourceType string) string { } switch resourceType { case "postgres": - parsed.Host = "instant-pg-proxy.instant.svc.cluster.local:5432" + parsed.Host = urls.InternalPGProxy case "redis": - parsed.Host = "instant-redis-proxy.instant.svc.cluster.local:6379" + parsed.Host = urls.InternalRedisProxy case "mongodb": - parsed.Host = "instant-mongo-proxy.instant.svc.cluster.local:27017" + parsed.Host = urls.InternalMongoProxy case "queue": - parsed.Host = "instant-nats-proxy.instant.svc.cluster.local:4222" + parsed.Host = urls.InternalNATSProxy default: return publicURL } diff --git a/internal/handlers/nosql.go b/internal/handlers/nosql.go index 89d515a..e401c60 100644 --- a/internal/handlers/nosql.go +++ b/internal/handlers/nosql.go @@ -8,7 +8,6 @@ package handlers import ( "context" "database/sql" - "fmt" "log/slog" "time" @@ -19,6 +18,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" "instant.dev/internal/provisioner" @@ -67,7 +67,7 @@ func (h *NoSQLHandler) provisionNoSQL(ctx context.Context, token, tier string) ( func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("mongodb") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "MongoDB provisioning is coming in Phase 4. Sign up at https://instanode.dev/start to be notified.") + "MongoDB provisioning is coming in Phase 4. Sign up at "+urls.StartURLPrefix+" to be notified.") } start := time.Now() @@ -94,7 +94,7 @@ func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error { // ── Dedicated requires authentication ───────────────────────────────────── if body.Dedicated { return respondError(c, fiber.StatusPaymentRequired, "auth_required", - "isolated resources require an authenticated team. Sign up at https://instanode.dev/start") + "isolated resources require an authenticated team. Sign up at "+urls.StartURLPrefix) } // ── Anonymous path ───────────────────────────────────────────────────────── @@ -116,7 +116,7 @@ func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } // Decrypt the stored connection_url to return it in plaintext. @@ -218,7 +218,7 @@ func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/handlers/provision_helper.go b/internal/handlers/provision_helper.go index 804d0ee..ba93dc6 100644 --- a/internal/handlers/provision_helper.go +++ b/internal/handlers/provision_helper.go @@ -31,6 +31,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/models" "instant.dev/internal/plans" + "instant.dev/internal/urls" ) // provisionHelper holds the shared dependencies used by every provisioning handler. @@ -182,7 +183,7 @@ func upgradeNote(upgradeURL string) string { if upgradeURL != "" { return fmt.Sprintf("Works for 24h free. Claim to keep — from $9/mo: %s", upgradeURL) } - return "Works for 24h free. Claim to keep — from $9/mo: https://instanode.dev/start" + return "Works for 24h free. Claim to keep — from $9/mo: " + urls.StartURLPrefix } // limitExceededNote builds the note for the rate-limit-exceeded path. @@ -196,7 +197,7 @@ func limitExceededNote(upgradeURL string, expiresAt time.Time) string { if upgradeURL != "" { return fmt.Sprintf("Returning your existing resource.%s Claim to keep — from $9/mo: %s", expiry, upgradeURL) } - return fmt.Sprintf("Returning your existing resource.%s Claim to keep — from $9/mo: https://instanode.dev/start", expiry) + return fmt.Sprintf("Returning your existing resource.%s Claim to keep — from $9/mo: %s", expiry, urls.StartURLPrefix) } // formatDuration formats a duration as "Xh Ym" or "Xm". diff --git a/internal/handlers/queue.go b/internal/handlers/queue.go index 59b7bf6..ee2d3e5 100644 --- a/internal/handlers/queue.go +++ b/internal/handlers/queue.go @@ -23,7 +23,6 @@ package handlers import ( "context" "database/sql" - "fmt" "log/slog" "time" @@ -34,6 +33,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" "instant.dev/internal/provisioner" @@ -83,7 +83,7 @@ func (h *QueueHandler) provisionQueue(ctx context.Context, token, tier string) ( func (h *QueueHandler) NewQueue(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("queue") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "NATS JetStream provisioning is coming in Phase 4. Sign up at https://instanode.dev/start to be notified.") + "NATS JetStream provisioning is coming in Phase 4. Sign up at "+urls.StartURLPrefix+" to be notified.") } start := time.Now() @@ -110,7 +110,7 @@ func (h *QueueHandler) NewQueue(c *fiber.Ctx) error { // ── Dedicated requires authentication ───────────────────────────────────── if body.Dedicated { return respondError(c, fiber.StatusPaymentRequired, "auth_required", - "isolated resources require an authenticated team. Sign up at https://instanode.dev/start") + "isolated resources require an authenticated team. Sign up at "+urls.StartURLPrefix) } // ── Anonymous path ───────────────────────────────────────────────────────── @@ -132,7 +132,7 @@ func (h *QueueHandler) NewQueue(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } // Decrypt the stored connection_url to return it in plaintext. @@ -227,7 +227,7 @@ func (h *QueueHandler) NewQueue(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/handlers/stack.go b/internal/handlers/stack.go index 3e874da..474f052 100644 --- a/internal/handlers/stack.go +++ b/internal/handlers/stack.go @@ -40,6 +40,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/manifest" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" compute "instant.dev/internal/providers/compute" @@ -84,7 +85,7 @@ func (h *StackHandler) requireStackTeam(c *fiber.Ctx) (*models.Team, error) { teamIDStr := middleware.GetTeamID(c) if teamIDStr == "" { return nil, respondError(c, fiber.StatusUnauthorized, "unauthorized", - "A session token is required for this action. Sign in at https://instanode.dev/start") + "A session token is required for this action. Sign in at "+urls.StartURLPrefix) } teamUUID, err := parseTeamID(teamIDStr) if err != nil { @@ -355,7 +356,7 @@ func (h *StackHandler) New(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "ok": false, "error": "rate_limit_exceeded", - "message": "Anonymous deploy limit reached. Upgrade at https://instanode.dev/start", + "message": "Anonymous deploy limit reached. Upgrade at "+urls.StartURLPrefix, }) } } @@ -646,7 +647,7 @@ func (h *StackHandler) New(c *fiber.Ctx) error { // Step 9: Return 202. noteMsg := "Stack is building. Poll GET /stacks/" + slug + " for status." if anon { - noteMsg += " Anonymous stacks expire in 24h. Upgrade at https://instanode.dev/start" + noteMsg += " Anonymous stacks expire in 24h. Upgrade at "+urls.StartURLPrefix+"" } if len(warnings) > 0 { noteMsg = fmt.Sprintf("%d warning(s) from manifest parsing. %s", len(warnings), noteMsg) diff --git a/internal/handlers/storage.go b/internal/handlers/storage.go index 57b1727..c15a972 100644 --- a/internal/handlers/storage.go +++ b/internal/handlers/storage.go @@ -38,6 +38,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" storageprovider "instant.dev/internal/providers/storage" @@ -123,7 +124,7 @@ func (h *StorageHandler) NewStorage(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } metrics.FingerprintAbuseBlocked.Inc() @@ -212,7 +213,7 @@ func (h *StorageHandler) NewStorage(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/handlers/webhook.go b/internal/handlers/webhook.go index 333776d..d229b90 100644 --- a/internal/handlers/webhook.go +++ b/internal/handlers/webhook.go @@ -34,6 +34,7 @@ import ( "instant.dev/internal/crypto" "instant.dev/internal/metrics" "instant.dev/internal/middleware" + "instant.dev/internal/urls" "instant.dev/internal/models" "instant.dev/internal/plans" ) @@ -110,7 +111,7 @@ func webhookAnonLimits() fiber.Map { func (h *WebhookHandler) NewWebhook(c *fiber.Ctx) error { if !h.cfg.IsServiceEnabled("webhook") { return respondError(c, fiber.StatusServiceUnavailable, "service_disabled", - "Webhook provisioning is coming soon. Sign up at https://instanode.dev/start to be notified.") + "Webhook provisioning is coming soon. Sign up at "+urls.StartURLPrefix+" to be notified.") } start := time.Now() @@ -153,7 +154,7 @@ func (h *WebhookHandler) NewWebhook(c *fiber.Ctx) error { } upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } metrics.FingerprintAbuseBlocked.Inc() @@ -222,7 +223,7 @@ func (h *WebhookHandler) NewWebhook(c *fiber.Ctx) error { upgradeURL := "" if jwtToken != "" { - upgradeURL = fmt.Sprintf("https://instanode.dev/start?t=%s", jwtToken) + upgradeURL = urls.UpgradeStartURL(jwtToken) c.Set("X-Instant-Upgrade", upgradeURL) } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 1dbef3e..161e672 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -9,6 +9,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v4" "instant.dev/internal/config" + "instant.dev/internal/urls" ) const ( @@ -27,8 +28,9 @@ const ( ) // defaultCanonicalResourceURL is the audience used when neither API_PUBLIC_URL -// nor the live request host is available. -const defaultCanonicalResourceURL = "https://api.instanode.dev" +// nor the live request host is available. Aliased to urls.PublicAPIBase to +// keep the literal "https://api.instanode.dev" in exactly one place. +const defaultCanonicalResourceURL = urls.PublicAPIBase // confirmation captures the OAuth 2.0 PoP "cnf" claim shape (RFC 7800). // Currently only the JWK thumbprint variant ("jkt") used by DPoP is consumed. diff --git a/internal/urls/urls.go b/internal/urls/urls.go new file mode 100644 index 0000000..f92b682 --- /dev/null +++ b/internal/urls/urls.go @@ -0,0 +1,70 @@ +// Package urls centralises every public hostname, cluster-internal FQDN, and +// onboarding URL the platform produces. The previous status quo had each +// string scattered across handler/middleware/template code — the last domain +// rename (instant.dev → instanode.dev) needed a 28-site sed sweep and still +// missed places. +// +// Rules of the road: +// +// 1. Anywhere a Go file would write "instanode.dev" or "instant-pg-proxy.svc" +// as a string literal, import this package instead. +// 2. Operator-facing config (env vars, configmaps) is NOT in scope here — +// those still flow through config.Config. This package is for code-only +// constants that don't make sense as runtime config. +// 3. Email templates and marketing copy live elsewhere; this package is for +// programmatic URLs the API itself produces. +// 4. Test files SHOULD continue to use string literals — tests asserting +// "got 'instanode.dev'" should not import this package, or the test +// tautologically passes whenever the constant changes. +package urls + +// Public hostnames returned to customers and referenced in URL strings the +// API itself produces. +const ( + // PublicAPIBase is the canonical resource URL of the agent-facing API. + // Used as the default JWT audience and in URL construction for any + // response that needs to point a caller back at us. + PublicAPIBase = "https://api.instanode.dev" + + // PublicMarketingBase is the customer-facing marketing site. /start lives + // here and is the entry point for the claim flow. + PublicMarketingBase = "https://instanode.dev" + + // StartURLPrefix is the bare path that anonymous resources point users at + // to claim — append "?t=" to produce the upgrade URL. + StartURLPrefix = PublicMarketingBase + "/start" + + // DeploymentWildcard is the suffix every /deploy/new and /stacks/new + // service URL gets prefixed by its app-id slug. + DeploymentWildcard = "deployment.instanode.dev" + + // StoragePublicHost is the customer-facing S3 endpoint hostname. + StoragePublicHost = "s3.instanode.dev" +) + +// Cluster-internal FQDNs for the per-protocol proxies. These are written into +// "internal_url" response fields and used by /deploy /stacks pipelines when +// a workload needs to reach a provisioned resource without going through the +// public LoadBalancer (DOKS doesn't hairpin reliably). See friction PR #2. +const ( + InternalPGProxy = "instant-pg-proxy.instant.svc.cluster.local:5432" + InternalRedisProxy = "instant-redis-proxy.instant.svc.cluster.local:6379" + InternalMongoProxy = "instant-mongo-proxy.instant.svc.cluster.local:27017" + InternalNATSProxy = "instant-nats-proxy.instant.svc.cluster.local:4222" + + // InternalMinIO is the in-cluster MinIO endpoint used by the kaniko build + // context delivery (presigned URL fetched by init-container). Customers + // use StoragePublicHost above. + InternalMinIO = "minio.instant-data.svc.cluster.local:9000" +) + +// UpgradeStartURL builds the URL we hand to anonymous users so they can claim +// their resources. token is the onboarding JWT (single-use, 7d TTL). Returning +// a single canonical builder avoids the previous pattern of fmt.Sprintf with +// inline string literals in every handler. +func UpgradeStartURL(token string) string { + if token == "" { + return StartURLPrefix + } + return StartURLPrefix + "?t=" + token +} diff --git a/internal/urls/urls_test.go b/internal/urls/urls_test.go new file mode 100644 index 0000000..256df58 --- /dev/null +++ b/internal/urls/urls_test.go @@ -0,0 +1,70 @@ +package urls + +import ( + "strings" + "testing" +) + +// These tests guard the constants from accidental edits — the whole point of +// extracting them was to make a domain rename a one-file diff. If someone +// edits a value here they should also fix the test, which surfaces the +// change in code review. + +func TestPublicHostnames_MatchExpectedShape(t *testing.T) { + cases := []struct { + name, got, contains string + }{ + {"PublicAPIBase has scheme + api subdomain", PublicAPIBase, "https://api.instanode.dev"}, + {"PublicMarketingBase has scheme + apex", PublicMarketingBase, "https://instanode.dev"}, + {"StartURLPrefix is marketing + /start", StartURLPrefix, "https://instanode.dev/start"}, + {"DeploymentWildcard is bare host", DeploymentWildcard, "deployment.instanode.dev"}, + {"StoragePublicHost is bare host", StoragePublicHost, "s3.instanode.dev"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if c.got != c.contains { + t.Errorf("%s = %q; want %q", c.name, c.got, c.contains) + } + // All public hostnames must point at instanode.dev — block the + // old instant.dev domain from sneaking back in via a typo. + if strings.Contains(c.got, "instant.dev") { + t.Errorf("%s leaks old domain instant.dev: %q", c.name, c.got) + } + }) + } +} + +func TestInternalProxyHostnames_CorrectPortsAndService(t *testing.T) { + cases := []struct { + name, got, suffix, port string + }{ + {"pg-proxy", InternalPGProxy, ".svc.cluster.local:5432", "5432"}, + {"redis-proxy", InternalRedisProxy, ".svc.cluster.local:6379", "6379"}, + {"mongo-proxy", InternalMongoProxy, ".svc.cluster.local:27017", "27017"}, + {"nats-proxy", InternalNATSProxy, ".svc.cluster.local:4222", "4222"}, + {"minio", InternalMinIO, ".svc.cluster.local:9000", "9000"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if !strings.HasSuffix(c.got, c.suffix) { + t.Errorf("%s = %q; must end with %q (k8s cluster-local FQDN + standard port)", c.name, c.got, c.suffix) + } + }) + } +} + +func TestUpgradeStartURL_Composition(t *testing.T) { + cases := []struct { + name, token, want string + }{ + {"with token", "ey.abc.def", "https://instanode.dev/start?t=ey.abc.def"}, + {"empty token returns bare /start", "", "https://instanode.dev/start"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := UpgradeStartURL(c.token); got != c.want { + t.Errorf("UpgradeStartURL(%q) = %q; want %q", c.token, got, c.want) + } + }) + } +}