client

package
v0.32.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package client is the public, importable Go SDK for the Jentic platform.

It exposes typed HTTP clients, authentication, and config/context resolution so third-party Go programs can talk to the Jentic control and broker planes WITHOUT shelling out to the jentic binary. The jentic CLI and the jenticctl installer are themselves thin consumers of this same surface.

Import as github.com/jentic/jentic-one/cli/client/... — the same module as the CLI, no separate go.mod (impl/7.0 §2).

Getting started (external consumers)

Add the module and import the SDK; you do NOT need the jentic binary:

go get github.com/jentic/jentic-one/cli@cli/vX.Y.Z   // pin to a released cli/ tag

import (
	client "github.com/jentic/jentic-one/cli/client"
	"github.com/jentic/jentic-one/cli/client/paginate"
)

c, err := client.NewControl(client.Config{
	ControlBaseURL:      "https://api.jentic.example",
	InjectedBearerToken: os.Getenv("JENTIC_BEARER_TOKEN"), // bring-your-own token
})

Pin to a cli/vX.Y.Z release tag (that tag, not a bare vX.Y.Z, is what Go module resolution uses for a module rooted under cli/ — see impl/8.0 §1). Although the SDK ships in the same module as the CLI, importing only client/... resolves a build graph with NONE of the CLI's cobra/charmbracelet/bubbletea dependencies — Go compiles only what client/... transitively imports. The out-of-tree consumer contract test (cli/tests/sdkconsumer) enforces this on every CI run.

Public surface

  • client — top-level constructors + Config: NewControl, NewControlRaw, RawControlRequest, ProbeServerVersion, BrokerTransport.
  • client/generated/{control,broker} — typed OpenAPI clients + spec companions (read-only, generated by `make generate-api`): ClientWithResponses, request/response models, RequiredFields(), SensitiveFields.
  • client/auth — keys, token state (XDG state dir), RFC 7523 OAuth, request editor: Credentials, RequestEditor, GetOrGenerateKey, TokenSet, ReadTokens/SaveTokens, API-key storage.
  • client/config — XDG paths, config.yaml load/mutate, env-var (file-less) resolution: Config, ResolvedState, LoadState, Load, MutateConfig, ConfigDir, CacheDir, StateDir.
  • client/paginate — generic cursor-walk over the list-endpoint convention: Page, PageFn, All, ForEach.

See the runnable examples in this package (example_test.go) for the two authentication modes (injected bearer vs. disk-backed identity), reusing the CLI's own state resolution, and draining a paginated endpoint.

The clean boundary

The whole SDK is driven by one UX-free struct, client.Config. There is deliberately NO Mode or Theme here: formatting, prompting, redaction, and mode-fencing are CLI concerns and are intentionally absent from the SDK. The dependency arrow is one-directional — internal/ -> client/, never the reverse — and the public command-tree composition layer (pkg/core, pkg/clitree) depends on client/, never the reverse. Arch test Test1A_SDKBoundary (cli/tests/arch/boundary_test.go) enforces that client/... imports nothing from internal/..., pkg/..., cobra, or the ux/theme UX layers.

What is intentionally NOT here

The following stay CLI-private under internal/ and are never part of the SDK contract (impl/7.0 §2): the Cobra command implementations, the ux audience/mode layer and theme, the local-agent isolation layer (account lifecycle, confinement, ACL grants — host-side OS tooling, not API surface), and the jenticctl installer/lifecycle packages. Downstream binaries that want to embed and extend the Cobra command tree use the public pkg/ composition packages instead.

Stability & versioning

Because the SDK is public, anything exported is an API contract:

  • Generated clients track the specs. client/generated/* is regenerated from the upstream OpenAPI specs; a breaking spec change is a breaking change for importers. Treat spec bumps as deliberate, semver-significant events — never regenerate silently in a patch release.
  • Semantic import versioning. The module is v0 during the product beta (the cli/vX.Y.Z tag mirrors the 0.x app release), so Go makes no stability promise yet — breaking Go API changes are permitted in minors but MUST be flagged in the release notes. Once the SDK is declared stable (v1), breaking changes to Config, the constructors, client/auth, or client/config require a major bump (and, per Go rules, a /v2+ module path suffix).
  • ResolvedState.Persisted* fields are SDK-internal-by-convention: they exist so the CLI can interpret mode/theme. SDK consumers should not depend on them for behavior; they may change without a major bump.
  • Generated code is read-only. Never hand-edit client/generated/*; customize via a RequestEditor or a custom *http.Client instead.

Tradeoffs

Same-module distribution means the CLI's dependencies appear in the repo's go.mod. In practice this costs external SDK consumers nothing at build time: because client/... imports neither Cobra nor charmbracelet (enforced by Test1A_SDKBoundary and the out-of-tree cli/tests/sdkconsumer contract), Go's build graph prunes them — an importer of client/... pulls only the SDK's own lean deps (go-jose, oapi-codegen runtime, uuid, flock, yaml, x/sys). The one real friction is the /cli/ path segment in imports; splitting client/ into its own module (to drop it and give the SDK independent SemVer) stays a mechanical move later if desired. The SDK gives typed clients and auth only — it makes no UX guarantees.

Index

Examples

Constants

View Source
const MaxBodyBytes int64 = 64 << 20

MaxBodyBytes is the ceiling every whole-body read in the CLI/SDK is clamped to. 64 MiB comfortably covers any legitimate API request/response, OpenAPI spec, or release-metadata file we buffer into RAM, while denying a hostile or buggy peer the ability to exhaust memory by streaming an unbounded body (review round-3 P0 / theme 2: unbounded io.ReadAll is systemic). Streaming-to-disk paths (the release archive download) keep their own, larger io.Copy limit — this cap is specifically for "read the whole thing into a []byte" call sites.

Variables

View Source
var ErrBodyTooLarge = errors.New("response body exceeds maximum allowed size")

ErrBodyTooLarge is returned by ReadAllBounded when the source produces more than the allowed number of bytes. It is a sentinel so callers can distinguish a size refusal from an ordinary transport error with errors.Is.

Functions

func BrokerTransport

func BrokerTransport(c Config) *http.Client

BrokerTransport returns the *http.Client the broker plane uses — the caller's transport decorated with the SDK response policy (429 Retry-After and bounded 5xx/transport backoff for idempotent calls — 13 §5). It is the seam `jentic execute` re-plumbs onto (plan.md Phase 5 item 1): execute composes the broker catch-all URL itself ({scheme}://{host}/{upstreamURL}) to preserve its exact METHOD:url|operation_id|METHOD:/path contract and agent_directive/exit-2 denial handling, but sends through THIS transport rather than a bare http.Client, so it inherits the same retry/backoff every generated broker call gets.

The 401 re-exchange arm is deliberately DISABLED here (reExchange=false): execute forwards its OWN agent bearer and treats a broker 401 as a recoverable denial whose agent_directive body must reach the caller intact — a re-exchange attempt would both be meaningless (no disk-backed identity to refresh) and drain that body. 429/5xx idempotent backoff still applies.

func NewControl

func NewControl(c Config) (*control.ClientWithResponses, error)

NewControl builds the strictly-typed control-plane client, authenticated for the configured identity/environment.

Example (Identity)

ExampleNewControl_identity shows the disk-backed path: given a registered identity + environment, the SDK uses the env-scoped Ed25519 key (under ~/.config/jentic/keys) plus cached access tokens (XDG state dir), performing the RFC 7523 OAuth exchange on demand. A custom *http.Client injects timeouts or a custom CA pool; the SDK wraps its transport with the retry/backoff policy.

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/jentic/jentic-one/cli/client"
)

func main() {
	_, err := client.NewControl(client.Config{
		ControlBaseURL:  "https://control.jentic.example",
		IdentityName:    "ci-bot",
		EnvironmentName: "prod",
		HTTPClient:      &http.Client{Timeout: 30 * time.Second},
	})
	if err != nil {
		log.Fatal(err)
	}
}
Example (InjectedToken)

ExampleNewControl_injectedToken shows the file-less "bring-your-own-token" path: the caller already holds a bearer (e.g. an agent launched with JENTIC_BASE_URL + JENTIC_BEARER_TOKEN injected). No disk access, no key material — best for short-lived/ephemeral jobs.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/jentic/jentic-one/cli/client"
	"github.com/jentic/jentic-one/cli/client/generated/control"
)

func main() {
	c, err := client.NewControl(client.Config{
		ControlBaseURL:      "https://control.jentic.example",
		InjectedBearerToken: "eyJhbGciOi...",
		SessionID:           "my-batch-job-42", // optional telemetry grouping
	})
	if err != nil {
		log.Fatal(err)
	}

	resp, err := c.ListExecutionsWithResponse(context.Background(), &control.ListExecutionsParams{})
	if err != nil {
		log.Fatal(err)
	}
	if resp.JSON200 == nil {
		log.Fatalf("unexpected status %d", resp.StatusCode())
	}
	fmt.Printf("got %d executions\n", len(resp.JSON200.Data))
}

func NewControlRaw

func NewControlRaw(c Config) (*control.Client, error)

NewControlRaw builds a raw control-plane client (server + Doer + editor chain) for the `jentic api` passthrough, which issues arbitrary METHOD/PATH requests the generated per-operation methods do not cover. It shares the SAME transport (retry/backoff), auth editor (incl. requireSecureHost), and session editor as the typed client — the passthrough is a thin wrapper over this, never a second hand-rolled transport (impl/5.0 §6a).

func ProbeServerVersion

func ProbeServerVersion(ctx context.Context, raw *control.Client) (string, error)

ProbeServerVersion asks the control plane for its running app version (GET /system/version). It backs the backend version-negotiation path (impl/5.0 §6a, plan.md Phase 5 item 9): when the CLI's embedded spec advertises a route an OLDER self-hosted server doesn't serve yet, a bare 404 is indistinguishable from a typo, so the passthrough probes the version once to enrich the error. Returns the running version string; a probe failure returns "" and an error (the caller degrades to a plain 404 rather than fabricating a verdict).

func RawControlRequest

func RawControlRequest(ctx context.Context, raw *control.Client, method, path string, body io.Reader, extraHeaders ...string) (*http.Response, error)

RawControlRequest issues an arbitrary request through raw's transport and editor chain and returns the response. path is a spec-relative path (e.g. "/credentials"), joined to the client's Server. extraHeaders (key=value) are applied after the editor chain so a caller can override defaults. It applies every configured RequestEditor (auth, session) exactly as the typed methods do, so the passthrough inherits auth/redaction/session for free. The caller owns the response body (close it).

func ReadAllBounded

func ReadAllBounded(r io.Reader, limit int64) ([]byte, error)

ReadAllBounded reads from r into memory like io.ReadAll but refuses to buffer more than limit bytes, returning ErrBodyTooLarge instead of growing without bound. It is the single funnel every "read the whole body into a []byte" site (SDK request buffering for retries, execute response bodies, raw upstream bodies, release-metadata downloads) goes through so no single peer can OOM the process.

The implementation reads limit+1 bytes: if the source had EXACTLY limit bytes we return them; if it had more, the extra byte trips the ceiling and we fail closed. A non-positive limit is treated as MaxBodyBytes so a caller can never accidentally disable the cap by passing 0.

func SanitizeSessionID

func SanitizeSessionID(id string) string

SanitizeSessionID normalizes a caller-chosen session id for use as the X-Jentic-Session-Id header value (SEC-5, log hygiene). The id is untrusted input (usually $JENTIC_SESSION_ID): Go's transport already rejects CR/LF smuggling at request time, but a hostile or fat-fingered value would then fail the WHOLE request, and exotic-but-legal header bytes would land verbatim in server logs. Instead of failing, keep only [A-Za-z0-9._:-] (covers UUIDs, ULIDs, and dotted/namespaced names), drop everything else, and truncate to 128 bytes. Returns "" when nothing survives — correlation is best-effort and an unusable id must never block a call.

Types

type Config

type Config struct {
	// ControlBaseURL is the management/control-plane URL (registry, auth,
	// executions). Required.
	ControlBaseURL string
	// BrokerBaseURL is the execution/broker-plane URL. Optional and, today,
	// informational only: the SDK exposes the broker as a transport
	// (BrokerTransport) rather than a typed constructor, and `jentic execute`
	// composes the broker catch-all URL itself ({scheme}://{host}/{upstreamURL})
	// from its own flags/env — it does NOT read this field. It is carried on
	// Config so a future typed broker seam (or a consumer that wants the resolved
	// URL) has it without re-resolving; leaving it empty is harmless.
	BrokerBaseURL string

	IdentityName    string
	EnvironmentName string

	// SessionID, when set, is attached as X-Jentic-Session-Id on every request so
	// the backend can group a run's executions under a client-chosen session (the
	// server-side correlation pivot is trace_id; this is an additional grouping —
	// impl/5.0 §1). Populated for the CLI from JENTIC_SESSION_ID via ResolvedState.
	SessionID string

	// InjectedBearerToken, when set, bypasses the on-disk key/token exchange and is
	// attached verbatim (file-less / bring-your-own-token mode).
	InjectedBearerToken string

	// HTTPClient overrides the transport used by BOTH planes. Optional; a nil value
	// uses the generated clients' default. Supply one to inject timeouts, a custom
	// CA pool (env ca_cert), or test doubles.
	HTTPClient *http.Client

	// Editors are extra request editors appended AFTER the auth editor (tracing,
	// idempotency keys, etc.). The auth editor always runs first so a caller editor
	// can observe/override the Authorization header if it must.
	Editors []RequestEditor
}

Config is the resolved connection + identity a client needs. It is produced by mapping a config.ResolvedState (disk or file-less) into plain fields, so the SDK never re-reads config.yaml or env vars itself — the caller owns resolution.

type RequestEditor

type RequestEditor = func(ctx context.Context, req *http.Request) error

RequestEditor mutates an outbound request before it is sent. It is the generated clients' editor signature, re-exported so SDK consumers needn't import a generated package to add headers/tracing.

Directories

Path Synopsis
Package auth is the SDK-owned authentication layer: environment-scoped Ed25519 identity keys, the RFC 7523 JWT-bearer token exchange, cached token state, and the request-editor middleware that attaches bearers to generated clients.
Package auth is the SDK-owned authentication layer: environment-scoped Ed25519 identity keys, the RFC 7523 JWT-bearer token exchange, cached token state, and the request-editor middleware that attaches bearers to generated clients.
Package config is the SDK-owned configuration layer: it resolves WHERE to talk (Control/Broker URLs) and AS WHOM (identity/environment), from either injected environment variables (the file-less path) or ~/.config/jentic/config.yaml.
Package config is the SDK-owned configuration layer: it resolves WHERE to talk (Control/Broker URLs) and AS WHOM (identity/environment), from either injected environment variables (the file-less path) or ~/.config/jentic/config.yaml.
generated
broker
Package broker provides primitives to interact with the openapi HTTP API.
Package broker provides primitives to interact with the openapi HTTP API.
control
Package control provides primitives to interact with the openapi HTTP API.
Package control provides primitives to interact with the openapi HTTP API.
Package paginate is a small, UX-free helper for walking cursor-paginated control-plane list endpoints.
Package paginate is a small, UX-free helper for walking cursor-paginated control-plane list endpoints.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL