/** * Hetzner Cloud credentials -- hand-written. * * The `Credentials` service resolves `{ token, apiBaseUrl }` per request; the * protocol layer formats the `Authorization: Bearer ` header from it. * The token is a Hetzner Cloud API token, created per PROJECT in the Cloud * Console -- there is no account-wide token, so a token IS the project scope * and no project/tenant field rides on the requests. * * A token is issued read-write or read-only; a read-only token answers any * non-GET request with `403 token_readonly` (surfaced as `Forbidden`). */ import * as EffectConfig from "@distilled.cloud/core/config"; import { ConfigError } from "@distilled.cloud/core/errors"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Redacted from "effect/Redacted"; /** * Hetzner Cloud's API root. The version is a PATH segment rather than a * header, so it belongs to the base URL: the generated operations carry * version-less routes (`/servers`, `/volumes/{id}`). */ export const DEFAULT_API_BASE_URL = "https://api.hetzner.cloud/v1"; export interface Config { readonly token: Redacted.Redacted; readonly apiBaseUrl: string; } export class Credentials extends /*@__PURE__*/ Context.Service>()( "HetznerCredentials", ) {} const envConfig = /*@__PURE__*/ EffectConfig.all({ // `HCLOUD_TOKEN` / `HCLOUD_ENDPOINT` are what the `hcloud` CLI, the // Terraform provider and the official Go library read. token: /*@__PURE__*/ EffectConfig.String("HCLOUD_TOKEN"), apiBaseUrl: /*@__PURE__*/ EffectConfig.String("HCLOUD_ENDPOINT").pipe( /*@__PURE__*/ EffectConfig.withDefault(DEFAULT_API_BASE_URL), ), }); export const CredentialsFromEnv = /*@__PURE__*/ Layer.succeed( Credentials, /*@__PURE__*/ envConfig.pipe( /*@__PURE__*/ Effect.mapError( () => new ConfigError({ message: "HCLOUD_TOKEN environment variable is required", }), ), /*@__PURE__*/ Effect.map(({ token, apiBaseUrl }) => ({ token: Redacted.make(token), apiBaseUrl, })), Effect.orDie, ), ); /** Convenience layer from a redacted token + optional base URL. */ export const credentials = (config: { readonly token: Redacted.Redacted; readonly apiBaseUrl?: string; }): Layer.Layer => Layer.succeed( Credentials, Effect.succeed({ token: config.token, apiBaseUrl: config.apiBaseUrl ?? DEFAULT_API_BASE_URL, }), );