WiseHosting
Reference

Integrations

Git providers, outbound webhooks, framework presets.

Files: internal/gitproviders/*, internal/webhooks/*, internal/frameworks/frameworks.go.

internal/gitproviders/provider.go

Shared abstractions for the git-provider subsystem.

  • NewRegistry(ps...) — Indexes by ID and by hostname.
  • (*Registry) Get(id) (Provider, bool).
  • (*Registry) ForRepoURL(repoURL) (Provider, bool) — Hostname lookup.
  • (*Registry) Configured() []Provider — Only providers whose Configured() returns true.
  • (*Registry) All() []Provider.

Types

  • ProfileLogin, Email, Name, AvatarURL.
  • RepoName, FullName, HTMLURL, Private, Fork, Archived, DefaultBranch, Language, Provider, OwnerLogin, OwnerAvatar.
  • Provider (interface) — ID, Label, Configured, Hosts, AuthURL, SignInScopes, LinkScopes, ExchangeCode, FetchProfile, ListRepos, RegisterWebhook, DeleteWebhook, VerifyWebhook, AuthenticatedCloneURL.

internal/gitproviders/github.go

GitHub { ClientID, ClientSecret }. Hosts github.com. OAuth + REST API v3 + HMAC-SHA256 (X-Hub-Signature-256).

  • ID() returns "github".
  • Label() returns "GitHub".
  • Configured() — both creds set.
  • Hosts() returns [github.com].
  • SignInScopes() returns read:user user:email.
  • LinkScopes() returns repo read:user user:email.
  • AuthURL(redirect, state, scopes)allow_signup=true.
  • ExchangeCode(ctx, code, redirect) — POST /login/oauth/access_token.
  • FetchProfile(ctx, token) — GET /user + /user/emails; primary verified email.
  • ListRepos(ctx, token, query, page) — GET /user/repos (50/page); filters archived; client-side substring search.
  • RegisterWebhook(...) — POST /repos/{o}/{r}/hooks (push, HMAC).
  • DeleteWebhook(...) — DELETE /repos/{o}/{r}/hooks/{id}.
  • VerifyWebhook(headers, secret, body)X-Hub-Signature-256 HMAC; returns X-GitHub-Event.
  • AuthenticatedCloneURL(repoURL, token)x-access-token user.

Org-scoped fetch + revoke

  • ListReposScoped(ctx, token, query, org, page) ([]Repo, error) — When org is empty hits GET /user/repos?per_page=100; otherwise switches to GET /orgs/{org}/repos?per_page=100. Used by the deploy dialog's org picker so private org repos surface only when the user explicitly scopes to that org. ListRepos now delegates here with org="".
  • ListOrgs(ctx, token) ([]Org, error)GET /user/orgs?per_page=100; returns {Login, AvatarURL, Description}. Orgs that have OAuth-app access restrictions enabled without approving WiseHosting are missing from this list, by design — the SPA shows a "request access" link in that case.
  • RevokeGrant(ctx, token) errorDELETE https://api.github.com/applications/{client_id}/grant with HTTP Basic auth (client_id:client_secret). Called from the disconnect path so the next OAuth flow re-runs the consent screen — without this, GitHub silently re-authorizes the same scopes and the user can never grant new permissions through the dashboard's Re-auth button. Local token deletion proceeds even if the revoke call fails.

internal/gitproviders/gitlab.go

GitLab { ClientID, ClientSecret }. Hosts gitlab.com. Webhooks use shared-token header (X-Gitlab-Token) rather than HMAC.

  • Identity / scopes / auth as above (sign-in: read_user openid email profile; link: api read_repository write_repository).
  • AuthURLgitlab.com/oauth/authorize?response_type=code.
  • ExchangeCode — POST /oauth/token grant_type=authorization_code.
  • FetchProfile — GET /api/v4/user; errors no_verified_primary_email if blank.
  • ListRepos — GET /api/v4/projects membership=true ordered last_activity_at; server-side search; archived filter.
  • RegisterWebhook — POST /api/v4/projects/{id}/hooks (push_events=true, SSL-verified token).
  • DeleteWebhook — DELETE /api/v4/projects/{id}/hooks/{hookID}.
  • VerifyWebhook — Token equality; normalises "Push Hook" to "push".
  • AuthenticatedCloneURLoauth2 user.

internal/gitproviders/bitbucket.go

Bitbucket { ClientID, ClientSecret }. Hosts bitbucket.org. Basic-auth token exchange. UUID-typed hook IDs are converted to int64 via HMAC hash.

  • Sign-in: account email. Link: account email repository repository:admin webhook.
  • ExchangeCode — POST /site/oauth2/access_token (Basic auth).
  • FetchProfile — GET /2.0/user + /user/emails; primary confirmed.
  • ListRepos — GET /2.0/repositories?role=member sorted -updated_on; server-side name~"q" filter.
  • RegisterWebhook — POST /2.0/repositories/{o}/{r}/hooks (repo:push); HMAC-derived int64 hook ID.
  • DeleteWebhook — currently a no-op (returns nil).
  • VerifyWebhookX-Hub-Signature sha256=... HMAC; normalises "repo:push" to "push".
  • AuthenticatedCloneURLx-token-auth user.

internal/gitproviders/codeberg.go

Codeberg { ClientID, ClientSecret }. Hosts codeberg.org. Forgejo/Gitea-compatible API. Auth header is Authorization: token ... (not Bearer).

  • Sign-in: read:user. Link: read:user write:repository.
  • ExchangeCode — POST JSON to /login/oauth/access_token.
  • FetchProfile — GET /api/v1/user; no_verified_primary_email when blank.
  • ListRepos — GET /api/v1/user/repos; client-side substring filter; archived filter.
  • RegisterWebhook — POST JSON /api/v1/repos/{o}/{r}/hooks (type:gitea, push).
  • DeleteWebhook — DELETE /api/v1/repos/{o}/{r}/hooks/{id}.
  • VerifyWebhookX-Gitea-Signature HMAC-SHA256; returns X-Gitea-Event.
  • AuthenticatedCloneURLoauth2 user.

internal/webhooks/dispatcher.go

Async outbound webhook dispatcher: 256-event buffered queue, configurable worker pool, retry back-off [0s, 1s, 5s, 30s], wildcard subscriptions (*, deployment.*). Each subscriber's URL determines the delivery format:

  • https://... — HMAC-SHA256-signed JSON body, full event payload.
  • Shoutrrr scheme (discord://, slack://, ntfy://, …) — Compact {emoji} {title} — {app} text via formatShoutrrrMessage.

The HTTPS path uses httpx.NewWebhookClient (see internal/httpx/tls.go), which resolves DNS up-front and rejects private/loopback/reserved IPs before connecting — basic SSRF guard so user-configured webhook URLs can't be aimed at metadata endpoints or internal services.

  • New(db) *Dispatcher — 256 buffer, 10 s HTTP client (httpx.NewWebhookClient).
  • (*Dispatcher) Start(workers) — Spawns at least 2 goroutines.
  • (*Dispatcher) Stop() — Cancels ctx.
  • (*Dispatcher) Publish(e Event) — Validates against AllEvents(); non-blocking enqueue (drops on full).
  • (*Dispatcher) TestDeliver(h Webhook) WebhookDelivery — Single-shot synchronous send used by the Send test button. Branches HTTPS vs Shoutrrr the same way as the queue path, no retries, returns the resulting delivery row so the caller can persist it and update the webhook's last_* counters.
  • (*Dispatcher) sendHTTPS(h, ev) / (*Dispatcher) sendShoutrrr(h, ev) — Internal helpers; both delivery paths split out so TestDeliver can reuse them without re-implementing signing or formatting.
  • isShoutrrrURL(url) bool — Branches the delivery path.

internal/httpx/tls.go

Hardened HTTP client constructors shared by every outbound caller in the control plane.

  • NewSecureClient(timeout) *http.Client — TLS 1.3 minimum, sane handshake/idle timeouts. Used by git-provider OAuth + REST calls.
  • NewWebhookClient(timeout) *http.Client — Same TLS posture plus a DialContext that resolves the target to IPs and refuses to dial any address inside the loopback / link-local / private (RFC 1918) / unique-local / multicast ranges. Returns the opaque error "webhook URL resolves to a private or reserved IP address" so attackers can't probe internal topology by URL.
  • NewSecureTLSConfig() *tls.Config — TLS 1.3 minimum config used standalone (e.g. by the WSS hub).
  • isPrivateIP(ip) bool — Internal helper backing the SSRF guard.

Types

  • PublisherPublish(e Event) interface.
  • EventName, UserID (JSON-omitted), AppID*, AppUUID, AppName, Subdomain, Data map, Timestamp.
  • Dispatcher — DB, secure HTTP client, queue, ctx.

internal/webhooks/events.go

Single source of truth for event names.

  • AllEvents() []string.
  • IsValidEvent(e) bool.
ConstantValue
EventDeploymentSucceededdeployment.succeeded
EventDeploymentFaileddeployment.failed
EventAppCreatedapp.created
EventAppRestartedapp.restarted
EventAppStoppedapp.stopped
EventAppDeletedapp.deleted
EventAppViolationapp.violation
EventAppOfflineapp.offline
EventAppCPUapp.cpu
EventAppMemoryapp.memory
EventAppNetworkapp.network
EventAppDiskapp.disk
EventAppCrashloopapp.crashloop
EventAccountLoginNewaccount.login_new_device
EventTesttest

internal/frameworks/frameworks.go

Static registry of deployment framework presets + Dockerfile generation.

  • All() []Preset.
  • Get(key) (Preset, bool).
  • Default() Presetdockerfile.
  • MergeWithOverrides(p, install, build, start) — Non-blank overrides only.
  • GenerateDockerfile(p) string — Empty for the dockerfile preset (user-supplied).

Type

  • Preset { Key, Label, Description, Runtime, Icon, BaseImage, InstallCommand, BuildCommand, StartCommand, UsesDockerfile }.
KeyBase imageNotes
dockerfile(user-supplied)Default; no auto-detection
nodejsnode:20-alpinenpm start, no build step
nextjsnode:20-alpineInstall + npm run build
vitenode:20-alpine to nginxMulti-stage; output from dist/
gogolang:1.23-alpine to distrolessStatic binary (CGO_ENABLED=0)
pythonpython:3.12-slimpip install -r requirements.txt
staticnginx (inline)Copies public/

On this page