From 44ffb43c8cdaf579e76e1d61ce1867e1061e65ee Mon Sep 17 00:00:00 2001 From: stack72 Date: Mon, 27 Apr 2026 12:19:07 +0200 Subject: [PATCH] feat(domain): migrate default swamp-club URL to swamp-club.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches the CLI's default registry/auth/telemetry endpoints from the legacy swamp.club domain to swamp-club.com. Existing auth.json files carrying https://swamp.club are transparently rewritten on load (and persisted) so users keep their session without re-running auth login. The SWAMP_CLUB_URL env var and DEFAULT_SWAMP_CLUB_URL constant keep their names — only the URL value changed. Consolidates eight previously-duplicated DEFAULT_SERVER_URL constants in src/cli and src/libswamp into a single import of DEFAULT_SWAMP_CLUB_URL from src/domain/auth/auth_credentials.ts so future domain changes land in one place. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/auto-response.yml | 10 ++-- BUG_BOUNTY.md | 12 ++-- README.md | 2 +- TRADEMARKS.md | 1 + design/extension.md | 2 +- design/libswamp.md | 4 +- extensions/models/README.md | 6 +- extensions/models/_lib/schemas.ts | 2 +- extensions/models/_lib/swamp_club.ts | 11 +++- integration/issue_extension_test.ts | 2 +- .../extension_report_dispatcher_test.ts | 2 +- src/cli/commands/extension_search.ts | 7 +-- src/cli/commands/extension_update.ts | 5 +- src/cli/commands/issue_submit.ts | 2 +- src/cli/commands/open.ts | 4 +- src/cli/mod.ts | 5 +- src/cli/mod_test.ts | 13 +++-- src/cli/resolve_datastore.ts | 3 +- src/domain/auth/auth_credentials.ts | 8 ++- src/domain/extensions/repository_url.ts | 5 +- src/domain/extensions/repository_url_test.ts | 2 +- .../http/callback_server_test.ts | 15 +++-- .../persistence/auth_repository.ts | 12 +++- .../persistence/auth_repository_test.ts | 58 ++++++++++++++++++- src/libswamp/auth/login_test.ts | 6 +- src/libswamp/auth/whoami_test.ts | 10 ++-- src/libswamp/extensions/pull.ts | 6 +- src/libswamp/extensions/push.ts | 5 +- .../extensions/push_pull_roundtrip_test.ts | 2 +- src/libswamp/extensions/push_test.ts | 4 +- src/libswamp/extensions/unyank.ts | 4 +- src/libswamp/extensions/update.ts | 4 +- src/libswamp/extensions/yank.ts | 4 +- src/libswamp/issues/create_test.ts | 41 +++++++++---- src/presentation/renderers/issue_create.ts | 4 +- .../renderers/issue_create_test.ts | 4 +- 36 files changed, 196 insertions(+), 91 deletions(-) diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index 733a6c4b..be80d66e 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -10,7 +10,7 @@ permissions: jobs: automove: - name: Move issue to swamp.club lab + name: Move issue to swamp-club.com lab runs-on: ubuntu-latest steps: - name: Move to lab and close GitHub issue @@ -41,7 +41,7 @@ jobs: // Create the lab issue via the public submission endpoint. The // `source: "swamp"` tag attributes it to this repo in the Lab UI. const createRes = await fetch( - "https://swamp.club/api/v1/lab/issues", + "https://swamp-club.com/api/v1/lab/issues", { method: "POST", headers: { @@ -61,7 +61,7 @@ jobs: if (!createRes.ok) { const text = await createRes.text().catch(() => ""); core.setFailed( - `swamp.club create failed: ${createRes.status} ${text}`, + `swamp-club.com create failed: ${createRes.status} ${text}`, ); return; } @@ -79,7 +79,7 @@ jobs: labIssueNumber <= 0 ) { core.setFailed( - `swamp.club create returned no lab issue number: ${ + `swamp-club.com create returned no lab issue number: ${ JSON.stringify(data) }`, ); @@ -87,7 +87,7 @@ jobs: } // We have a confirmed lab issue number — safe to comment and close. - const labUrl = `https://swamp.club/lab/${labIssueNumber}`; + const labUrl = `https://swamp-club.com/lab/${labIssueNumber}`; // Post the auto-responder comment linking to the lab issue. await github.rest.issues.createComment({ diff --git a/BUG_BOUNTY.md b/BUG_BOUNTY.md index 6adcc68a..eeee5472 100644 --- a/BUG_BOUNTY.md +++ b/BUG_BOUNTY.md @@ -261,9 +261,9 @@ Examples include: ### Eligible Assets -| Identifier | Asset Type | Instruction | Eligible for Bounty | Eligible for Submission | Availability Requirement | Confidentiality Requirement | Integrity Requirement | Max Severity | System Tags | Created At | Updated At | -| -------------------- | ---------- | ----------- | ------------------- | ----------------------- | ------------------------ | --------------------------- | --------------------- | ------------ | ----------- | ----------------------- | ----------------------- | -| telemetry.swamp.club | API | | true | true | | | | critical | production | 2026-11-02 13:45:00 UTC | 2026-11-02 13:45:00 UTC | -| swamp.club | WEB APP | | true | true | | | | critical | production | 2026-11-02 13:45:00 UTC | 2026-11-02 13:45:00 UTC | -| systeminit.com | WEB APP | | true | true | | | | medium | production | 2024-11-28 13:45:00 UTC | 2024-11-28 13:45:00 UTC | -| swamp CLI (stable) | OTHER | | true | true | | | | high | production | 2026-02-12 00:00:00 UTC | 2026-02-12 00:00:00 UTC | +| Identifier | Asset Type | Instruction | Eligible for Bounty | Eligible for Submission | Availability Requirement | Confidentiality Requirement | Integrity Requirement | Max Severity | System Tags | Created At | Updated At | +| ------------------------ | ---------- | ----------- | ------------------- | ----------------------- | ------------------------ | --------------------------- | --------------------- | ------------ | ----------- | ----------------------- | ----------------------- | +| telemetry.swamp-club.com | API | | true | true | | | | critical | production | 2026-11-02 13:45:00 UTC | 2026-11-02 13:45:00 UTC | +| swamp-club.com | WEB APP | | true | true | | | | critical | production | 2026-11-02 13:45:00 UTC | 2026-11-02 13:45:00 UTC | +| systeminit.com | WEB APP | | true | true | | | | medium | production | 2024-11-28 13:45:00 UTC | 2024-11-28 13:45:00 UTC | +| swamp CLI (stable) | OTHER | | true | true | | | | high | production | 2026-02-12 00:00:00 UTC | 2026-02-12 00:00:00 UTC | diff --git a/README.md b/README.md index 3cbaf79d..17edc95d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Come join the [swamp party on discord](https://discord.gg/swamp-club). ## Getting Started ```bash -curl -fsSL https://swamp.club/install.sh | sh +curl -fsSL https://swamp-club.com/install.sh | sh ``` ### Quick Start diff --git a/TRADEMARKS.md b/TRADEMARKS.md index 9e501c64..762e5f5c 100644 --- a/TRADEMARKS.md +++ b/TRADEMARKS.md @@ -55,6 +55,7 @@ This Policy covers: | swamp | software platform | | Swamp Club | software platform | | swamp.club | software platform | +| swamp-club.com | software platform | Some Marks may not be registered, but registration does not equal ownership of trademarks. This Policy covers our Marks whether they are registered or not. diff --git a/design/extension.md b/design/extension.md index b4d4b1a7..a92d517e 100644 --- a/design/extension.md +++ b/design/extension.md @@ -529,7 +529,7 @@ support are marked as "unverified" but still allowed. ## Registry -Extensions are distributed through the swamp registry at `https://swamp.club`. +Extensions are distributed through the swamp registry at `https://swamp-club.com`. ### Authentication diff --git a/design/libswamp.md b/design/libswamp.md index f40464e4..99dff163 100644 --- a/design/libswamp.md +++ b/design/libswamp.md @@ -702,11 +702,11 @@ Deno.test("whoami yields identity on success", async () => { assertEquals(events, [ { kind: "loading_credentials" }, - { kind: "contacting_server", serverUrl: "https://swamp.club" }, + { kind: "contacting_server", serverUrl: "https://swamp-club.com" }, { kind: "completed", identity: { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", id: "user-1", username: "adam", email: "adam@example.com", diff --git a/extensions/models/README.md b/extensions/models/README.md index 11947007..bca13235 100644 --- a/extensions/models/README.md +++ b/extensions/models/README.md @@ -170,7 +170,7 @@ match the classification. Each lab issue has a sequential, human-friendly number (`#1`, `#2`, ...) used in every lab URL — both the dashboard and the API. You can find an issue at -`https://swamp.club/lab/`. +`https://swamp-club.com/lab/`. Classification types (`triage`) match swamp-club's issue types: @@ -195,7 +195,7 @@ Status transitions in swamp-club: ### Setup The model requires a swamp-club API key. The URL defaults to -`https://swamp.club`. +`https://swamp-club.com`. **Option 1: Environment variable (recommended)** @@ -204,7 +204,7 @@ export SWAMP_API_KEY=swamp_your_key_here ``` The model reads `SWAMP_API_KEY` automatically and connects to -`https://swamp.club`. +`https://swamp-club.com`. **Option 2: Point at a local dev server** diff --git a/extensions/models/_lib/schemas.ts b/extensions/models/_lib/schemas.ts index 754c5190..57b54741 100644 --- a/extensions/models/_lib/schemas.ts +++ b/extensions/models/_lib/schemas.ts @@ -25,7 +25,7 @@ export const GlobalArgsSchema = z.object({ "Swamp Club lab issue number (the issue must already exist in swamp-club)", ), swampClubUrl: z.string().optional().describe( - "Swamp Club API base URL (defaults to https://swamp.club)", + "Swamp Club API base URL (defaults to https://swamp-club.com)", ), swampClubApiKey: z.string().optional().describe( "Swamp Club API key (defaults to SWAMP_API_KEY env var)", diff --git a/extensions/models/_lib/swamp_club.ts b/extensions/models/_lib/swamp_club.ts index 23d98552..b45232fb 100644 --- a/extensions/models/_lib/swamp_club.ts +++ b/extensions/models/_lib/swamp_club.ts @@ -301,8 +301,15 @@ export async function loadAuthFile(): Promise< username?: string; }; if (creds.apiKey) { + // Read-only domain migration: rewrite the legacy swamp.club URL to + // the new domain so callers transparently hit the right host. The + // CLI's AuthRepository persists the rewrite; here we don't own the + // file, so we just translate at the read site. + const serverUrl = creds.serverUrl === "https://swamp.club" + ? "https://swamp-club.com" + : creds.serverUrl ?? "https://swamp-club.com"; return { - serverUrl: creds.serverUrl ?? "https://swamp.club", + serverUrl, apiKey: creds.apiKey, username: creds.username || undefined, }; @@ -339,7 +346,7 @@ export async function createSwampClubClient( } } - url = url ?? "https://swamp.club"; + url = url ?? "https://swamp-club.com"; if (!apiKey) { logger?.warning( diff --git a/integration/issue_extension_test.ts b/integration/issue_extension_test.ts index 9321dfed..bae6fd9f 100644 --- a/integration/issue_extension_test.ts +++ b/integration/issue_extension_test.ts @@ -174,7 +174,7 @@ models: assertEquals(parsed.reason, "no-repository"); assertStringIncludes( parsed.guidance, - "swamp.club/extensions/%40adam%2Fcfgmgmt", + "swamp-club.com/extensions/%40adam%2Fcfgmgmt", ); } finally { await Deno.remove(repo, { recursive: true }); diff --git a/src/cli/commands/extension_report_dispatcher_test.ts b/src/cli/commands/extension_report_dispatcher_test.ts index b8589f04..d9fa8c5f 100644 --- a/src/cli/commands/extension_report_dispatcher_test.ts +++ b/src/cli/commands/extension_report_dispatcher_test.ts @@ -155,7 +155,7 @@ Deno.test("resolveExtensionTarget: no-repository refusal when manifest has no re assertStringIncludes(t.guidance, "does not declare a repository"); assertStringIncludes( t.guidance, - "swamp.club/extensions/%40adam%2Fcfgmgmt", + "swamp-club.com/extensions/%40adam%2Fcfgmgmt", ); } finally { await Deno.remove(repo, { recursive: true }); diff --git a/src/cli/commands/extension_search.ts b/src/cli/commands/extension_search.ts index 24cec6cc..cbc93f76 100644 --- a/src/cli/commands/extension_search.ts +++ b/src/cli/commands/extension_search.ts @@ -44,15 +44,14 @@ import { } from "../../libswamp/mod.ts"; import { createExtensionSearchRenderer } from "../../presentation/renderers/extension_search.tsx"; import { resolveSkillsDir } from "../../domain/repo/skill_dirs.ts"; - -const DEFAULT_SERVER_URL = "https://swamp.club"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; /** * Resolves the registry server URL. - * Priority: SWAMP_CLUB_URL env var > default "https://swamp.club" + * Priority: SWAMP_CLUB_URL env var > DEFAULT_SWAMP_CLUB_URL */ function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } // deno-lint-ignore no-explicit-any diff --git a/src/cli/commands/extension_update.ts b/src/cli/commands/extension_update.ts index 3414acfb..dad12148 100644 --- a/src/cli/commands/extension_update.ts +++ b/src/cli/commands/extension_update.ts @@ -44,14 +44,13 @@ import { } from "../../libswamp/mod.ts"; import { createExtensionUpdateRenderer } from "../../presentation/renderers/extension_update.ts"; import { resolveSkillsDir } from "../../domain/repo/skill_dirs.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; // deno-lint-ignore no-explicit-any type AnyOptions = any; -const DEFAULT_SERVER_URL = "https://swamp.club"; - function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } export const extensionUpdateCommand = new Command() diff --git a/src/cli/commands/issue_submit.ts b/src/cli/commands/issue_submit.ts index e4998a1f..efca1544 100644 --- a/src/cli/commands/issue_submit.ts +++ b/src/cli/commands/issue_submit.ts @@ -129,7 +129,7 @@ async function promptLoginOrEmail(): Promise<"login" | "email"> { await Deno.stdout.write( encoder.encode( - "\nYou're not logged in to swamp.club.\n\n" + + "\nYou're not logged in to swamp-club.com.\n\n" + " 1. Log in first (then retry this command)\n" + " 2. Send via email\n\n" + "Choose [1/2]: ", diff --git a/src/cli/commands/open.ts b/src/cli/commands/open.ts index 07ed22d0..f6a13180 100644 --- a/src/cli/commands/open.ts +++ b/src/cli/commands/open.ts @@ -57,12 +57,12 @@ import { } from "../mod.ts"; import { resolveSkillsDir } from "../../domain/repo/skill_dirs.ts"; import { VERSION } from "./version.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; // deno-lint-ignore no-explicit-any type AnyOptions = any; const logger = getSwampLogger(["open"]); -const DEFAULT_SERVER_URL = "https://swamp.club"; function forceExtensionCatalogRescan(repoDir: string): void { try { @@ -155,7 +155,7 @@ export const openCommand = new Command() ]); const extClient = new ExtensionApiClient( - Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL, + Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL, ); const state: OpenServerState = { diff --git a/src/cli/mod.ts b/src/cli/mod.ts index debd1b8e..9f825257 100644 --- a/src/cli/mod.ts +++ b/src/cli/mod.ts @@ -83,6 +83,7 @@ import { import { RepoPath } from "../domain/repo/repo_path.ts"; import { ExtensionAutoResolver } from "../domain/extensions/extension_auto_resolver.ts"; import { ExtensionApiClient } from "../infrastructure/http/extension_api_client.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../domain/auth/auth_credentials.ts"; import { setAutoResolver } from "./auto_resolver_context.ts"; import { createAutoResolveInstallerAdapter, @@ -320,7 +321,7 @@ export function configureExtensionAutoResolver( setAutoResolver(null); return; } - const serverUrl = Deno.env.get("SWAMP_CLUB_URL") ?? "https://swamp.club"; + const serverUrl = Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; const extensionClient = new ExtensionApiClient(serverUrl); const modelsDir = resolveModelsDir(marker); const denoRuntime = new EmbeddedDenoRuntime(); @@ -717,7 +718,7 @@ async function checkForMissingPulledExtensions( } /** Default telemetry endpoint */ -const DEFAULT_TELEMETRY_ENDPOINT = "https://telemetry.swamp.club"; +const DEFAULT_TELEMETRY_ENDPOINT = "https://telemetry.swamp-club.com"; /** Telemetry endpoint used when auth serverUrl is a localhost address */ const LOCALHOST_TELEMETRY_ENDPOINT = "http://localhost:8080"; diff --git a/src/cli/mod_test.ts b/src/cli/mod_test.ts index 63515174..f93edddf 100644 --- a/src/cli/mod_test.ts +++ b/src/cli/mod_test.ts @@ -425,8 +425,8 @@ Deno.test("isLocalhostUrl returns true for http://[::1]:3000", () => { assertEquals(isLocalhostUrl("http://[::1]:3000"), true); }); -Deno.test("isLocalhostUrl returns false for https://swamp.club", () => { - assertEquals(isLocalhostUrl("https://swamp.club"), false); +Deno.test("isLocalhostUrl returns false for https://swamp-club.com", () => { + assertEquals(isLocalhostUrl("https://swamp-club.com"), false); }); Deno.test("isLocalhostUrl returns false for https://example.com", () => { @@ -457,13 +457,16 @@ Deno.test("resolveTelemetryEndpoint returns localhost endpoint when auth serverU }); Deno.test("resolveTelemetryEndpoint returns default when auth serverUrl is remote", () => { - const result = resolveTelemetryEndpoint(undefined, "https://swamp.club"); - assertEquals(result, "https://telemetry.swamp.club"); + const result = resolveTelemetryEndpoint( + undefined, + "https://swamp-club.com", + ); + assertEquals(result, "https://telemetry.swamp-club.com"); }); Deno.test("resolveTelemetryEndpoint returns default when auth serverUrl is null", () => { const result = resolveTelemetryEndpoint(undefined, null); - assertEquals(result, "https://telemetry.swamp.club"); + assertEquals(result, "https://telemetry.swamp-club.com"); }); // --- isUpdateCheckDisabledByEnv tests --- diff --git a/src/cli/resolve_datastore.ts b/src/cli/resolve_datastore.ts index e5f3c1aa..655d0573 100644 --- a/src/cli/resolve_datastore.ts +++ b/src/cli/resolve_datastore.ts @@ -45,6 +45,7 @@ import { maybeAutoUpdateDatastoreExtension } from "../libswamp/extensions/datast import { FileExtensionUpdateCheckRepository } from "../infrastructure/persistence/extension_update_check_repository.ts"; import { readUpstreamExtensions } from "../infrastructure/persistence/upstream_extensions.ts"; import { ExtensionApiClient } from "../infrastructure/http/extension_api_client.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../domain/auth/auth_credentials.ts"; import { detectLocalEditsForExtension, enumeratePulledExtensionDirs, @@ -93,7 +94,7 @@ async function maybeAutoUpdateSwampDatastore( type, path: lockfilePath, }); - const serverUrl = Deno.env.get("SWAMP_CLUB_URL") ?? "https://swamp.club"; + const serverUrl = Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; const extensionClient = new ExtensionApiClient(serverUrl); const cacheRepository = new FileExtensionUpdateCheckRepository(swampDir); diff --git a/src/domain/auth/auth_credentials.ts b/src/domain/auth/auth_credentials.ts index abf84a6b..fbd89978 100644 --- a/src/domain/auth/auth_credentials.ts +++ b/src/domain/auth/auth_credentials.ts @@ -18,11 +18,15 @@ // along with Swamp. If not, see . /** Default swamp-club server URL, used when no override is configured. */ -export const DEFAULT_SWAMP_CLUB_URL = "https://swamp.club"; +export const DEFAULT_SWAMP_CLUB_URL = "https://swamp-club.com"; + +/** Legacy default swamp-club URL, retained so stored credentials carrying the + * old domain can be transparently migrated on load. */ +export const LEGACY_SWAMP_CLUB_URL = "https://swamp.club"; /** Stored authentication credentials for swamp-club API access. */ export interface AuthCredentials { - /** The swamp-club server URL (e.g., "https://swamp.club") */ + /** The swamp-club server URL (e.g., "https://swamp-club.com") */ serverUrl: string; /** The API key prefixed with "swamp_" */ apiKey: string; diff --git a/src/domain/extensions/repository_url.ts b/src/domain/extensions/repository_url.ts index eaa906a2..db60d206 100644 --- a/src/domain/extensions/repository_url.ts +++ b/src/domain/extensions/repository_url.ts @@ -18,6 +18,7 @@ // along with Swamp. If not, see . import { UserError } from "../errors.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../auth/auth_credentials.ts"; /** Supported third-party repository providers. */ export type RepositoryProvider = "github" | "gitlab" | "other"; @@ -129,7 +130,9 @@ export function buildGitlabNewIssueUrl( * swamp-club/routes/extensions/[...name].tsx. */ export function swampClubExtensionUrl(extensionName: string): string { - return `https://swamp.club/extensions/${encodeURIComponent(extensionName)}`; + return `${DEFAULT_SWAMP_CLUB_URL}/extensions/${ + encodeURIComponent(extensionName) + }`; } /** diff --git a/src/domain/extensions/repository_url_test.ts b/src/domain/extensions/repository_url_test.ts index c7d8dcc7..b4e39dfd 100644 --- a/src/domain/extensions/repository_url_test.ts +++ b/src/domain/extensions/repository_url_test.ts @@ -168,7 +168,7 @@ Deno.test("buildGitlabNewIssueUrl: strips trailing slash from repo URL", () => { Deno.test("swampClubExtensionUrl: percent-encodes the scoped name", () => { assertEquals( swampClubExtensionUrl("@adam/cfgmgmt"), - "https://swamp.club/extensions/%40adam%2Fcfgmgmt", + "https://swamp-club.com/extensions/%40adam%2Fcfgmgmt", ); }); diff --git a/src/infrastructure/http/callback_server_test.ts b/src/infrastructure/http/callback_server_test.ts index 03e7dc6e..6bdac1b4 100644 --- a/src/infrastructure/http/callback_server_test.ts +++ b/src/infrastructure/http/callback_server_test.ts @@ -22,7 +22,7 @@ import { startCallbackServer } from "./callback_server.ts"; Deno.test("callback server - resolves token on valid callback", async () => { const state = crypto.randomUUID(); - const serverUrl = "https://swamp.club"; + const serverUrl = "https://swamp-club.com"; const server = startCallbackServer(state, serverUrl); assertExists(server.port); @@ -33,7 +33,10 @@ Deno.test("callback server - resolves token on valid callback", async () => { ); assertEquals(res.status, 302); - assertEquals(res.headers.get("location"), "https://swamp.club/cli/success"); + assertEquals( + res.headers.get("location"), + "https://swamp-club.com/cli/success", + ); await res.body?.cancel(); const resolved = await server.token; @@ -43,7 +46,7 @@ Deno.test("callback server - resolves token on valid callback", async () => { Deno.test("callback server - rejects mismatched state", async () => { const state = crypto.randomUUID(); - const server = startCallbackServer(state, "https://swamp.club"); + const server = startCallbackServer(state, "https://swamp-club.com"); const res = await fetch( `http://localhost:${server.port}/callback?token=tok&state=wrong-state`, @@ -56,7 +59,7 @@ Deno.test("callback server - rejects mismatched state", async () => { Deno.test("callback server - rejects missing token", async () => { const state = crypto.randomUUID(); - const server = startCallbackServer(state, "https://swamp.club"); + const server = startCallbackServer(state, "https://swamp-club.com"); const res = await fetch( `http://localhost:${server.port}/callback?state=${state}`, @@ -69,7 +72,7 @@ Deno.test("callback server - rejects missing token", async () => { Deno.test("callback server - returns 404 for non-callback paths", async () => { const state = crypto.randomUUID(); - const server = startCallbackServer(state, "https://swamp.club"); + const server = startCallbackServer(state, "https://swamp-club.com"); const res = await fetch(`http://localhost:${server.port}/other`); @@ -79,7 +82,7 @@ Deno.test("callback server - returns 404 for non-callback paths", async () => { }); Deno.test("callback server - allocates a random port", async () => { - const server = startCallbackServer("test-state", "https://swamp.club"); + const server = startCallbackServer("test-state", "https://swamp-club.com"); assertEquals(server.port > 0, true); await server.shutdown(); }); diff --git a/src/infrastructure/persistence/auth_repository.ts b/src/infrastructure/persistence/auth_repository.ts index 83772bfd..bf24f918 100644 --- a/src/infrastructure/persistence/auth_repository.ts +++ b/src/infrastructure/persistence/auth_repository.ts @@ -23,6 +23,7 @@ import { getSwampConfigDir } from "./paths.ts"; import { type AuthCredentials, DEFAULT_SWAMP_CLUB_URL, + LEGACY_SWAMP_CLUB_URL, } from "../../domain/auth/auth_credentials.ts"; const AUTH_FILE = "auth.json"; @@ -62,7 +63,16 @@ export class AuthRepository { try { const content = await Deno.readTextFile(this.getAuthPath()); - return JSON.parse(content) as AuthCredentials; + const parsed = JSON.parse(content) as AuthCredentials; + // Domain migration: rewrite the legacy swamp.club URL to the new + // domain in-place so subsequent loads see the new value. Scoped to + // the exact legacy literal — custom servers and the new default + // are left alone. + if (parsed.serverUrl === LEGACY_SWAMP_CLUB_URL) { + parsed.serverUrl = DEFAULT_SWAMP_CLUB_URL; + await this.save(parsed); + } + return parsed; } catch (error) { if (error instanceof Deno.errors.NotFound) { return null; diff --git a/src/infrastructure/persistence/auth_repository_test.ts b/src/infrastructure/persistence/auth_repository_test.ts index a7e70571..0ea7d64a 100644 --- a/src/infrastructure/persistence/auth_repository_test.ts +++ b/src/infrastructure/persistence/auth_repository_test.ts @@ -18,11 +18,12 @@ // along with Swamp. If not, see . import { assertEquals, assertExists } from "@std/assert"; +import { join } from "@std/path"; import { AuthRepository } from "./auth_repository.ts"; import type { AuthCredentials } from "../../domain/auth/auth_credentials.ts"; const TEST_CREDENTIALS: AuthCredentials = { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", apiKey: "swamp_testkey123", apiKeyId: "key-id-456", username: "testuser", @@ -193,7 +194,7 @@ Deno.test("AuthRepository - load returns env var credentials when SWAMP_API_KEY assertExists(loaded); assertEquals(loaded.apiKey, "swamp_env_key_123"); - assertEquals(loaded.serverUrl, "https://swamp.club"); + assertEquals(loaded.serverUrl, "https://swamp-club.com"); assertEquals(loaded.apiKeyId, ""); assertEquals(loaded.username, ""); } finally { @@ -256,3 +257,56 @@ Deno.test("AuthRepository - empty SWAMP_API_KEY is treated as unset", async () = await Deno.remove(tmpDir, { recursive: true }); } }); + +// ── Domain migration: legacy swamp.club → swamp-club.com ───────────── + +Deno.test("AuthRepository - load rewrites legacy https://swamp.club to new domain and persists", async () => { + const tmpDir = await Deno.makeTempDir(); + const env = saveEnv("XDG_CONFIG_HOME", "SWAMP_API_KEY"); + try { + Deno.env.set("XDG_CONFIG_HOME", tmpDir); + Deno.env.delete("SWAMP_API_KEY"); + const repo = new AuthRepository(); + + // Seed an auth.json carrying the legacy domain. + await repo.save({ + ...TEST_CREDENTIALS, + serverUrl: "https://swamp.club", + }); + + const loaded = await repo.load(); + assertExists(loaded); + assertEquals(loaded.serverUrl, "https://swamp-club.com"); + + // Migration must persist so subsequent loads see the new value + // without re-running the rewrite. + const raw = await Deno.readTextFile(join(tmpDir, "swamp", "auth.json")); + const onDisk = JSON.parse(raw) as AuthCredentials; + assertEquals(onDisk.serverUrl, "https://swamp-club.com"); + } finally { + env.restore(); + await Deno.remove(tmpDir, { recursive: true }); + } +}); + +Deno.test("AuthRepository - load leaves custom server URLs untouched", async () => { + const tmpDir = await Deno.makeTempDir(); + const env = saveEnv("XDG_CONFIG_HOME", "SWAMP_API_KEY"); + try { + Deno.env.set("XDG_CONFIG_HOME", tmpDir); + Deno.env.delete("SWAMP_API_KEY"); + const repo = new AuthRepository(); + + await repo.save({ + ...TEST_CREDENTIALS, + serverUrl: "https://staging.swamp.club", + }); + + const loaded = await repo.load(); + assertExists(loaded); + assertEquals(loaded.serverUrl, "https://staging.swamp.club"); + } finally { + env.restore(); + await Deno.remove(tmpDir, { recursive: true }); + } +}); diff --git a/src/libswamp/auth/login_test.ts b/src/libswamp/auth/login_test.ts index 8627676a..5ab42e29 100644 --- a/src/libswamp/auth/login_test.ts +++ b/src/libswamp/auth/login_test.ts @@ -60,7 +60,7 @@ function makeDeps(overrides: Partial = {}): AuthLoginDeps { function makeInput(overrides: Partial = {}): AuthLoginInput { return { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", useBrowserFlow: false, ...overrides, }; @@ -101,7 +101,7 @@ Deno.test("authLogin: successful browser flow emits correct event sequence", asy >; assertEquals(completed.data.username, "testuser"); assertEquals(completed.data.email, "test@example.com"); - assertEquals(completed.data.serverUrl, "https://swamp.club"); + assertEquals(completed.data.serverUrl, "https://swamp-club.com"); assertEquals(completed.data.apiKey, "swamp_testapikey123456"); assertEquals(savedCreds.username, "testuser"); @@ -161,7 +161,7 @@ Deno.test("authLogin: successful stdin flow with provided credentials", async () { kind: "completed" } >; assertEquals(completed.data.username, "testuser"); - assertEquals(completed.data.serverUrl, "https://swamp.club"); + assertEquals(completed.data.serverUrl, "https://swamp-club.com"); }); Deno.test("authLogin: stdin flow reads credentials when not provided", async () => { diff --git a/src/libswamp/auth/whoami_test.ts b/src/libswamp/auth/whoami_test.ts index 0b0254c6..f521ecee 100644 --- a/src/libswamp/auth/whoami_test.ts +++ b/src/libswamp/auth/whoami_test.ts @@ -50,7 +50,7 @@ function makeDeps(overrides: { } const testCredentials: AuthCredentials = { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", apiKey: "swamp_test_key", apiKeyId: "key-1", username: "adam", @@ -78,11 +78,11 @@ Deno.test("whoami yields loading_credentials -> contacting_server -> completed o assertEquals(events, [ { kind: "loading_credentials" }, - { kind: "contacting_server", serverUrl: "https://swamp.club" }, + { kind: "contacting_server", serverUrl: "https://swamp-club.com" }, { kind: "completed", identity: { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", id: "user-1", username: "adam", email: "adam@example.com", @@ -119,7 +119,7 @@ Deno.test("whoami yields invalid_api_key error when server says not authenticate assertEquals(events[0], { kind: "loading_credentials" }); assertEquals(events[1], { kind: "contacting_server", - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", }); const last = events[2] as Extract; assertEquals(last.kind, "error"); @@ -199,7 +199,7 @@ Deno.test("whoami does not persist credentials when saveCredentials is a no-op", const ctx = createLibSwampContext(); let saveCalled = false; const envCredentials: AuthCredentials = { - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", apiKey: "swamp_env_key", apiKeyId: "", username: "", diff --git a/src/libswamp/extensions/pull.ts b/src/libswamp/extensions/pull.ts index e9a88446..cbc04dd8 100644 --- a/src/libswamp/extensions/pull.ts +++ b/src/libswamp/extensions/pull.ts @@ -36,8 +36,8 @@ import type { Logger } from "@logtape/logtape"; import type { LibSwampContext } from "../context.ts"; import type { SwampError } from "../errors.ts"; import { withGeneratorSpan } from "../../infrastructure/tracing/mod.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; -export const DEFAULT_SERVER_URL = "https://swamp.club"; const SCOPED_NAME_PATTERN = /^@[a-z0-9_-]+\/[a-z0-9_-]+(\/[a-z0-9_-]+)*$/; const MAX_DEPENDENCY_DEPTH = 10; const LOCK_RETRY_COUNT = 10; @@ -201,10 +201,10 @@ export function validateExtensionName(name: string): void { /** * Resolves the registry server URL. - * Priority: SWAMP_CLUB_URL env var > default "https://swamp.club" + * Priority: SWAMP_CLUB_URL env var > DEFAULT_SWAMP_CLUB_URL */ export function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } /** Returns true if the filename is a macOS resource fork (AppleDouble) file. */ diff --git a/src/libswamp/extensions/push.ts b/src/libswamp/extensions/push.ts index 635f75df..1bf3c035 100644 --- a/src/libswamp/extensions/push.ts +++ b/src/libswamp/extensions/push.ts @@ -294,11 +294,10 @@ import { checkExtensionQuality } from "../../domain/extensions/extension_quality import { bundleExtension } from "../../domain/models/bundle.ts"; import { extractContentMetadata } from "../../domain/extensions/extension_content_extractor.ts"; import { EmbeddedDenoRuntime } from "../../infrastructure/runtime/embedded_deno_runtime.ts"; - -const DEFAULT_SERVER_URL = "https://swamp.club"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } /** Wires real infrastructure into ExtensionPushPrepareDeps. */ diff --git a/src/libswamp/extensions/push_pull_roundtrip_test.ts b/src/libswamp/extensions/push_pull_roundtrip_test.ts index 69fe3a25..d38ff63f 100644 --- a/src/libswamp/extensions/push_pull_roundtrip_test.ts +++ b/src/libswamp/extensions/push_pull_roundtrip_test.ts @@ -66,7 +66,7 @@ function makePrepareDeps( return { loadCredentials: () => Promise.resolve({ - serverUrl: "https://test.swamp.club", + serverUrl: "https://test.swamp-club.com", apiKey: "swamp_test", username: "testuser", }), diff --git a/src/libswamp/extensions/push_test.ts b/src/libswamp/extensions/push_test.ts index fb4bfba3..174cd98a 100644 --- a/src/libswamp/extensions/push_test.ts +++ b/src/libswamp/extensions/push_test.ts @@ -95,7 +95,7 @@ function makePrepareDeps( return { loadCredentials: () => Promise.resolve({ - serverUrl: "https://test.swamp.club", + serverUrl: "https://test.swamp-club.com", apiKey: "swamp_test", username: "testuser", }), @@ -125,7 +125,7 @@ function makeExecuteDeps( return { loadCredentials: () => Promise.resolve({ - serverUrl: "https://test.swamp.club", + serverUrl: "https://test.swamp-club.com", apiKey: "swamp_test", }), initiatePush: () => diff --git a/src/libswamp/extensions/unyank.ts b/src/libswamp/extensions/unyank.ts index f722273b..24aff466 100644 --- a/src/libswamp/extensions/unyank.ts +++ b/src/libswamp/extensions/unyank.ts @@ -24,8 +24,8 @@ import type { SwampError } from "../errors.ts"; import { notAuthenticated, validationFailed } from "../errors.ts"; import { withGeneratorSpan } from "../../infrastructure/tracing/mod.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; const SCOPED_NAME_PATTERN = /^@[a-z0-9_-]+\/[a-z0-9_-]+(\/[a-z0-9_-]+)*$/; -const DEFAULT_SERVER_URL = "https://swamp.club"; /** * Data structure for the extension unyank output. @@ -98,7 +98,7 @@ export function createExtensionUnyankDeps(): ExtensionUnyankDeps { } function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } /** Gathers preview info for the extension unyank operation, validating inputs. */ diff --git a/src/libswamp/extensions/update.ts b/src/libswamp/extensions/update.ts index 79b2df50..c51c8a6a 100644 --- a/src/libswamp/extensions/update.ts +++ b/src/libswamp/extensions/update.ts @@ -30,10 +30,10 @@ import type { SwampError } from "../errors.ts"; import { validationFailed } from "../errors.ts"; import { withGeneratorSpan } from "../../infrastructure/tracing/mod.ts"; -const DEFAULT_SERVER_URL = "https://swamp.club"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } export type ExtensionUpdateEvent = diff --git a/src/libswamp/extensions/yank.ts b/src/libswamp/extensions/yank.ts index b43ac49e..a1789238 100644 --- a/src/libswamp/extensions/yank.ts +++ b/src/libswamp/extensions/yank.ts @@ -24,8 +24,8 @@ import type { SwampError } from "../errors.ts"; import { notAuthenticated, validationFailed } from "../errors.ts"; import { withGeneratorSpan } from "../../infrastructure/tracing/mod.ts"; +import { DEFAULT_SWAMP_CLUB_URL } from "../../domain/auth/auth_credentials.ts"; const SCOPED_NAME_PATTERN = /^@[a-z0-9_-]+\/[a-z0-9_-]+(\/[a-z0-9_-]+)*$/; -const DEFAULT_SERVER_URL = "https://swamp.club"; /** Data structure for the extension yank output. */ export interface ExtensionYankData { @@ -92,7 +92,7 @@ export function createExtensionYankDeps(): ExtensionYankDeps { } function resolveServerUrl(): string { - return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SERVER_URL; + return Deno.env.get("SWAMP_CLUB_URL") ?? DEFAULT_SWAMP_CLUB_URL; } /** Gathers preview info for the extension yank operation, validating inputs. */ diff --git a/src/libswamp/issues/create_test.ts b/src/libswamp/issues/create_test.ts index 44df5a3b..e61e2b86 100644 --- a/src/libswamp/issues/create_test.ts +++ b/src/libswamp/issues/create_test.ts @@ -30,7 +30,7 @@ import type { ReporterContext } from "../../domain/extensions/reporter_context.t function makeDeps(overrides: Partial = {}): IssueCreateDeps { return { submitToLab: () => - Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }), + Promise.resolve({ number: 1, serverUrl: "https://swamp-club.com" }), ...overrides, }; } @@ -70,7 +70,7 @@ Deno.test("issueCreate: submits bug to Lab and yields completed", async () => { Deno.test("issueCreate: submits feature to Lab", async () => { const deps = makeDeps({ submitToLab: () => - Promise.resolve({ number: 7, serverUrl: "https://swamp.club" }), + Promise.resolve({ number: 7, serverUrl: "https://swamp-club.com" }), }); const events = await collect( @@ -96,7 +96,10 @@ Deno.test("issueCreate: passes title, body, and type to submitToLab", async () = const deps = makeDeps({ submitToLab: (input) => { captured = input; - return Promise.resolve({ number: 42, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 42, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -144,7 +147,10 @@ Deno.test("issueCreate: plain lab request body is byte-identical to caller body const deps = makeDeps({ submitToLab: (input) => { captured = { body: input.body }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -164,7 +170,7 @@ Deno.test("issueCreate: plain lab request body is byte-identical to caller body Deno.test("issueCreate: extension-lab variant set when extensionName present", async () => { const deps = makeDeps({ submitToLab: () => - Promise.resolve({ number: 9, serverUrl: "https://swamp.club" }), + Promise.resolve({ number: 9, serverUrl: "https://swamp-club.com" }), }); const events = await collect( @@ -192,7 +198,10 @@ Deno.test("issueCreate: extension body contains Extension: line", async () => { const deps = makeDeps({ submitToLab: (input) => { captured = { body: input.body }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -216,7 +225,10 @@ Deno.test("issueCreate: extension body contains Upstream repository line when se const deps = makeDeps({ submitToLab: (input) => { captured = { body: input.body }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -241,7 +253,10 @@ Deno.test("issueCreate: extension body omits Upstream repository when unset", as const deps = makeDeps({ submitToLab: (input) => { captured = { body: input.body }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -263,7 +278,10 @@ Deno.test("issueCreate: extension body contains reporter-context Environment sec const deps = makeDeps({ submitToLab: (input) => { captured = { body: input.body }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); @@ -285,7 +303,10 @@ Deno.test("issueCreate: extension path does not modify title", async () => { const deps = makeDeps({ submitToLab: (input) => { captured = { title: input.title }; - return Promise.resolve({ number: 1, serverUrl: "https://swamp.club" }); + return Promise.resolve({ + number: 1, + serverUrl: "https://swamp-club.com", + }); }, }); diff --git a/src/presentation/renderers/issue_create.ts b/src/presentation/renderers/issue_create.ts index 8e8efc53..4d3d3213 100644 --- a/src/presentation/renderers/issue_create.ts +++ b/src/presentation/renderers/issue_create.ts @@ -43,7 +43,7 @@ class LogIssueCreateRenderer implements Renderer { }); if (data.type === "security") { logger.info( - "This security report is visible only to you and the admin team at swamp.club.", + "This security report is visible only to you and the admin team at swamp-club.com.", ); } } else if (data.method === "extension-lab") { @@ -60,7 +60,7 @@ class LogIssueCreateRenderer implements Renderer { }); if (data.type === "security") { logger.info( - "This security report is visible only to you and the admin team at swamp.club.", + "This security report is visible only to you and the admin team at swamp-club.com.", ); } } else { diff --git a/src/presentation/renderers/issue_create_test.ts b/src/presentation/renderers/issue_create_test.ts index 00e1e7b8..2a4b77c2 100644 --- a/src/presentation/renderers/issue_create_test.ts +++ b/src/presentation/renderers/issue_create_test.ts @@ -60,7 +60,7 @@ Deno.test("issue_create renderer (log): extension-lab variant runs without error number: 42, type: "bug", title: "t", - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", extensionName: "@swamp/aws", }, }, @@ -81,7 +81,7 @@ Deno.test("issue_create renderer (json): extension-lab variant serialises method number: 42, type: "bug", title: "t", - serverUrl: "https://swamp.club", + serverUrl: "https://swamp-club.com", extensionName: "@swamp/aws", }, },