{"id":50661137,"url":"https://github.com/iamwavecut/seasond","last_synced_at":"2026-06-08T02:33:34.753Z","repository":{"id":362693228,"uuid":"1260371156","full_name":"iamwavecut/seasond","owner":"iamwavecut","description":"A GOPROXY-compatible age gate for public Go module versions.","archived":false,"fork":false,"pushed_at":"2026-06-07T15:40:50.000Z","size":41,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T02:33:30.595Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iamwavecut.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T12:26:26.000Z","updated_at":"2026-06-07T15:39:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/iamwavecut/seasond","commit_stats":null,"previous_names":["iamwavecut/seasond"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/iamwavecut/seasond","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iamwavecut%2Fseasond","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iamwavecut%2Fseasond/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iamwavecut%2Fseasond/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iamwavecut%2Fseasond/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iamwavecut","download_url":"https://codeload.github.com/iamwavecut/seasond/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iamwavecut%2Fseasond/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34046003,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-08T02:33:34.236Z","updated_at":"2026-06-08T02:33:34.748Z","avatar_url":"https://github.com/iamwavecut.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# seasond\n\n[![CI](https://github.com/iamwavecut/seasond/actions/workflows/ci.yml/badge.svg)](https://github.com/iamwavecut/seasond/actions/workflows/ci.yml)\n[![Release](https://github.com/iamwavecut/seasond/actions/workflows/release.yml/badge.svg)](https://github.com/iamwavecut/seasond/actions/workflows/release.yml)\n[![Latest Release](https://img.shields.io/github/v/release/iamwavecut/seasond?sort=semver)](https://github.com/iamwavecut/seasond/releases/latest)\n[![Go Reference](https://pkg.go.dev/badge/github.com/iamwavecut/seasond.svg)](https://pkg.go.dev/github.com/iamwavecut/seasond)\n[![Go Report Card](https://goreportcard.com/badge/github.com/iamwavecut/seasond)](https://goreportcard.com/report/github.com/iamwavecut/seasond)\n[![GHCR](https://img.shields.io/badge/ghcr.io-iamwavecut%2Fseasond-blue)](https://github.com/iamwavecut/seasond/pkgs/container/seasond)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\n`seasond` is a small GOPROXY-compatible HTTP service that blocks Go module\nversions until they have been visible in the public Go module ecosystem for a\nconfigured amount of time.\n\nThe default policy is a 14-day quarantine. Fresh or unknown versions fail\nclosed with `403 Forbidden`, while allowed `.mod` and `.zip` artifacts are\nserved by redirecting the Go tool to `proxy.golang.org`.\n\n## Why\n\nMost dependency updates are boring. The dangerous ones are often very new:\ncompromised maintainer accounts, poisoned tags, fast rollback incidents, or\nfresh supply-chain attacks that need time to be noticed.\n\n`seasond` adds a deliberately simple age gate in front of the public Go module\nproxy:\n\n- use `index.golang.org` as the first-seen timestamp source;\n- keep module/version metadata in local SQLite;\n- filter `@latest` and `@v/list` consistently;\n- block direct requests for fresh or unknown versions;\n- avoid proxying large module bodies through the service.\n\nIt is a policy layer, not a mirror.\n\n## Status\n\nThis is an MVP for public Go modules. It is useful for experiments, local\npolicy enforcement, and small deployments, but it intentionally does not handle\nprivate modules, custom VCS fallback, checksum database replacement, allowlists,\ndenylists, or a UI.\n\n## Install\n\nInstall the latest release on Linux or macOS:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/iamwavecut/seasond/main/scripts/install.sh | bash\n```\n\nInstall the latest release on Windows PowerShell:\n\n```powershell\nirm https://raw.githubusercontent.com/iamwavecut/seasond/main/scripts/install.ps1 | iex\n```\n\nInstall a specific version:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/iamwavecut/seasond/main/scripts/install.sh | VERSION=v0.1.0 bash\n```\n\n```powershell\n$env:VERSION = \"v0.1.0\"; irm https://raw.githubusercontent.com/iamwavecut/seasond/main/scripts/install.ps1 | iex\n```\n\nRequires Go 1.26 or newer.\n\n```bash\ngo install github.com/iamwavecut/seasond/cmd/seasond@latest\n```\n\nOr build from a checkout:\n\n```bash\ngo build -o seasond ./cmd/seasond\n```\n\n### Manual Downloads\n\nRelease archives contain a single `seasond` binary:\n\n```text\nseasond_v0.1.0_linux_x64.tar.gz\nseasond_v0.1.0_linux_x86.tar.gz\nseasond_v0.1.0_macos_x64.tar.gz\nseasond_v0.1.0_macos_arm64.tar.gz\nseasond_v0.1.0_windows_x64.zip\nseasond_v0.1.0_windows_x86.zip\n```\n\nDownload from [the latest release](https://github.com/iamwavecut/seasond/releases/latest),\nextract the archive, and place `seasond` in a directory on `PATH`.\n\n### Docker\n\nRun a published GitHub Container Registry image:\n\n```bash\ndocker run --rm \\\n  -p 8080:8080 \\\n  -v \"$PWD/seasond-data:/data\" \\\n  -e DB_PATH=/data/seasond.db \\\n  -e BOOTSTRAP_SINCE=2026-01-01T00:00:00Z \\\n  ghcr.io/iamwavecut/seasond:v0.1.0\n```\n\nThe release workflow also publishes `ghcr.io/iamwavecut/seasond:latest`, but\npinning a version is recommended for deployments.\n\n## Quick Start\n\nStart the service with a persistent SQLite database:\n\n```bash\nDB_PATH=./seasond.db \\\nBOOTSTRAP_SINCE=2026-01-01T00:00:00Z \\\nLISTEN_ADDR=:8080 \\\nseasond\n```\n\nUse it as a Go proxy:\n\n```bash\nGOPROXY=http://localhost:8080/age/14d \\\ngo list -m -versions github.com/pkg/errors\n```\n\nDownload through the default 14-day policy:\n\n```bash\nGOPROXY=http://localhost:8080/age/336h \\\ngo mod download github.com/pkg/errors@v0.9.1\n```\n\nFor strict enforcement, do not append `,direct` or another proxy to `GOPROXY`.\nBlocked versions return `403`, which the Go command does not treat as a normal\nfallback signal; keeping `GOPROXY` single-hop makes the policy boundary easier\nto reason about.\n\n## How It Works\n\n`seasond` has three main pieces:\n\n1. **HTTP facade**: accepts a subset of the GOPROXY protocol used by the Go\n   command.\n2. **SQLite metadata store**: records `(module_path, version, first_seen_at)`\n   plus index sync cursors.\n3. **Index ingester**: polls `index.golang.org/index?since=...` and upserts\n   first-seen timestamps.\n\nA version is allowed when:\n\n```text\nnow - first_seen_at \u003e= min_age\n```\n\nIf a version is missing from the local metadata database, it is blocked. This is\nintentional: unknown means \"not yet safe to serve\".\n\n## Supported Proxy Paths\n\nThe service supports both a root default policy and path-based age policies.\n\n```text\nhttp://localhost:8080/github.com/pkg/errors/@v/list\nhttp://localhost:8080/age/14d/github.com/pkg/errors/@v/list\nhttp://localhost:8080/age/336h/github.com/pkg/errors/@latest\nhttp://localhost:8080/age/14d/github.com/pkg/errors/@v/v0.9.1.info\nhttp://localhost:8080/age/14d/github.com/pkg/errors/@v/v0.9.1.mod\nhttp://localhost:8080/age/14d/github.com/pkg/errors/@v/v0.9.1.zip\n```\n\nPath age values accept `d` for days and Go duration strings such as `336h`.\n\n## Request Behavior\n\n| Path | Allowed version | Fresh or unknown version | Upstream missing |\n| --- | --- | --- | --- |\n| `@v/list` | `200 OK` with filtered versions | omitted from list | `404` or `410` |\n| `@latest` | `200 OK` with selected `.info` JSON | `403 Forbidden` if nothing is allowed | `404` or `410` |\n| `.info` | `200 OK` by default | `403 Forbidden` | `404` or `410` |\n| `.mod` | `302 Found` to upstream | `403 Forbidden` | not checked before redirect |\n| `.zip` | `302 Found` to upstream | `403 Forbidden` | not checked before redirect |\n\nSet `ALLOW_REDIRECTS_FOR_INFO=true` if you also want allowed `.info` requests\nto redirect instead of being fetched and returned by the service.\n\n## Configuration\n\nAll configuration is environment-based.\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `LISTEN_ADDR` | `:8080` | HTTP listen address. |\n| `UPSTREAM_PROXY_BASE` | `https://proxy.golang.org` | Public module proxy used for lists, info, and artifact redirects. |\n| `INDEX_BASE` | `https://index.golang.org` | Go module index base URL. |\n| `DEFAULT_MIN_AGE` | `336h` | Root policy age as a Go duration string. |\n| `DB_PATH` | `seasond.db` | SQLite database path. |\n| `POLL_INTERVAL` | `60s` | Delay between index sync attempts. |\n| `BOOTSTRAP_SINCE` | empty | Initial index cursor when no cursor exists in SQLite. |\n| `HTTP_TIMEOUT` | `15s` | Timeout for upstream/index HTTP calls and graceful shutdown. |\n| `ALLOW_REDIRECTS_FOR_INFO` | `false` | Redirect allowed `.info` requests instead of returning JSON. |\n| `USE_CACHED_ONLY_UPSTREAM` | `false` | Append `/cached-only` to the upstream proxy base. |\n| `LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, or `error`. |\n\n### Bootstrap Notes\n\nThe service only knows about versions that have been ingested into SQLite.\nChoose `BOOTSTRAP_SINCE` far enough back for your use case and let the ingester\ncatch up. The cursor is persisted as `index_since`, so restarts continue from\nthe last successful sync instead of starting over.\n\n## Health and Observability\n\n```bash\ncurl http://localhost:8080/healthz\ncurl http://localhost:8080/readyz\ncurl http://localhost:8080/status\ncurl http://localhost:8080/metrics\n```\n\nEndpoints:\n\n- `/healthz`: process liveness.\n- `/readyz`: SQLite connectivity.\n- `/status`: JSON summary with known version count and sync cursors.\n- `/metrics`: Prometheus-style text metrics.\n\nMetric names currently emitted:\n\n- `seasond_requests_total`\n- `seasond_request_duration_seconds_sum`\n- `seasond_blocked_requests_total`\n- `seasond_known_versions_total`\n\nRequest logs are structured with `method`, `path`, `endpoint`, `decision`,\n`duration_ms`, and `status_code`.\n\n## Development\n\nRun the standard checks:\n\n```bash\ngo test ./...\ngo vet ./...\ngo build -o /tmp/seasond ./cmd/seasond\n```\n\nRun the networked end-to-end smoke test with the real Go command:\n\n```bash\ngo test -tags=e2e ./internal/e2e\n```\n\nThe e2e test starts a local proxy handler, seeds SQLite metadata, and verifies\nthat `go mod download` succeeds for an old version and fails closed for a fresh\nversion.\n\nRun the Dockerized functional test with a real Go Alpine container:\n\n```bash\ngo test -tags=docker_e2e ./internal/e2e -run TestDockerizedGoToolUsesSeasonDIndexAgeGate -count=1 -v\n```\n\nThis test queries the live Go module index for Google module versions, starts\n`seasond` inside `golang:1.26-alpine`, creates a dummy module, and verifies that\nthe Go tool downloads the allowed version while the too-fresh version is blocked.\n\n## Releases\n\nReleases are automated from Conventional Commits merged into `main`.\n\n| Commit signal | Version bump |\n| --- | --- |\n| `fix:`, `perf:`, `refactor:`, `build:`, `ci:`, `chore:`, `docs:`, `style:`, `test:`, `revert:` | patch |\n| `feat:` | minor |\n| `type!:` or `BREAKING CHANGE:` | major |\n\nOn a release-producing push to `main`, GitHub Actions:\n\n1. calculates the next SemVer tag from commits since the latest `vX.Y.Z` tag;\n2. runs tests and vet;\n3. builds Linux, macOS, and Windows archives;\n4. creates the GitHub tag and release;\n5. publishes `ghcr.io/iamwavecut/seasond:\u003cversion\u003e`,\n   `ghcr.io/iamwavecut/seasond:\u003cversion-without-v\u003e`, and\n   `ghcr.io/iamwavecut/seasond:latest`.\n\nIf a push contains no recognized Conventional Commit signal, no release is\ncreated.\n\n## Security Model\n\n`seasond` helps reduce exposure to very fresh public module releases. It does\nnot replace Go checksum verification and does not prove that an older version is\nsafe. It only enforces a first-seen age policy based on public Go module index\ntimestamps.\n\nRecommended strict mode:\n\n```bash\nGOPROXY=https://your-seasond.example.com/age/14d\n```\n\nAvoid:\n\n```bash\nGOPROXY=https://your-seasond.example.com/age/14d,direct\n```\n\nThe second form allows fallback paths outside the policy boundary.\n\n## Project Layout\n\n```text\ncmd/seasond          binary entrypoint\ninternal/config       environment configuration\ninternal/httpapi      GOPROXY-compatible HTTP facade\ninternal/indexsync    index.golang.org ingestion\ninternal/metrics      minimal Prometheus-style metrics\ninternal/policy       age-gate policy parsing and decisions\ninternal/semverutil   Go module semver sorting helpers\ninternal/storage      SQLite metadata store\ninternal/upstream     proxy.golang.org client\ninternal/e2e          opt-in Go tool smoke tests\nscripts/install.*     release installers\nscripts/release       release helper scripts\n```\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiamwavecut%2Fseasond","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiamwavecut%2Fseasond","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiamwavecut%2Fseasond/lists"}