integrations

package
v1.801.150 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package integrations is the generic, provider-agnostic OAuth connector plane for the unified Hanzo Cloud binary — the /v1/integrations surface that lets an org connect a third-party account (Slack today; GitHub scaffolded; Google / Salesforce plug into the SAME registry later) and hands the resulting per-org tokens to KMS custody.

ONE framework, N providers. A provider self-registers (its file's init calls register) into a package registry declaring how to build its authorize URL, exchange the code, and revoke. The five HTTP handlers here are provider-blind: they resolve the provider by :provider, apply the SAME org gate, CSRF/state, KMS custody and console redirect for every one. Adding a provider is a new file, never a new route.

Surface (subsystem name "integrations", prefix /v1/integrations):

GET    /v1/integrations                      list providers + this org's status  -> {providers:[...]}
GET    /v1/integrations/:provider            one provider (404 unknown id)        -> Provider
POST   /v1/integrations/:provider/connect    begin OAuth (org-authed)             -> {authorizeUrl} | 503 | 403
GET    /v1/integrations/:provider/callback   PUBLIC, state-authed                  -> 302 to console
POST   /v1/integrations/:provider/disconnect revoke + forget (org-authed)         -> {disconnected:true}

TENANT ISOLATION. connect/list/get/disconnect derive the org from principal.Org (a VALIDATED principal — a client-forged X-Org-Id with no bearer is refused 403). The callback is Slack/GitHub-initiated and therefore UNAUTHENTICATED, so its org is taken ONLY from the HMAC-signed, single-use state — never a header (see state.go). Every org that reaches KMS or the store is additionally validOrg-checked so it can never smuggle path structure into a secret key.

SECRET CUSTODY. Per-org customer tokens live ONLY in KMS (sealed, AES-256-GCM envelope), keyed /orgs/{org}/integrations/{provider}. The store holds only non-secret connection metadata (external id, account label, granted scopes). Provider APP creds (client id/secret) come from ENV, injected by the operator from KMS via KMSSecret — never plaintext in the store or a manifest. If KMS is not Ready the connect/callback flows fail closed (503 / failure redirect); a token is NEVER written in plaintext and NEVER put in SQLite.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InstallationToken added in v1.801.23

func InstallationToken(ctx context.Context, org string) (string, error)

InstallationToken mints a fresh short-lived GitHub App installation token for org's connected installation. Fails closed (error, never a value) when GitHub is not connected for org or the App creds are absent. Called by the git object plane (outbound mirror) and the sync handlers below.

func LinkedSubject added in v1.801.115

func LinkedSubject(org, provider, extUser string) (string, bool, error)

LinkedSubject returns the Hanzo account subject bound to (org, provider, extUser) by the account-link flow (bridge_link.go / *_link.go). Returns ("", false, nil) when the user has not linked; an error (fail closed) on an unmounted subsystem, invalid org, or KMS-down.

func Mount

func Mount(app *zip.App, deps cloud.Deps) error

Mount wires /v1/integrations/* onto app. Complex flavour: it publishes the package global `mounted` (the in-process token-custody seam) and pairs with a Shutdown, so it constructs the cloud.Service value directly (cloud.NewBase + &cloud.Service[state]{…}) rather than via cloud.Mount.

func NotifySlack added in v1.800.1

func NotifySlack(ctx context.Context, org, channel, text string, blocks []any) error

NotifySlack posts a Block Kit message to channel on behalf of org's connected Slack workspace. It resolves the org's KMS-sealed bot token (TokenFor — fail-closed: unmounted / org not connected / KMS-down all error, never post) and delivers through PostSlackBlocks. The caller never handles the raw token. This is the door the git-lifecycle notifier (clients/git) uses so token custody stays entirely inside the integrations plane.

func OrgForExternalID

func OrgForExternalID(provider, externalID string) (string, bool)

OrgForExternalID resolves a provider account id (Slack team_id / GitHub installation_id) back to the org that connected it. Used by inbound provider events (which carry the external id, not an org) to find the tenant.

func PostSlackBlocks added in v1.800.1

func PostSlackBlocks(ctx context.Context, botToken, channel, text string, blocks []any) error

PostSlackBlocks posts a Block Kit message (with a text fallback shown in notifications) to channel using botToken, via the shared chat.postMessage path. blocks is Block Kit JSON (a []any of section/context/… maps); nil posts text only. Exported so a subsystem holding a resolved bot token (an automations connector) posts through the SAME code path as the OAuth bridge — no third chat.postMessage implementation.

func PostSlackBlocksThread added in v1.801.23

func PostSlackBlocksThread(ctx context.Context, botToken, channel, threadTS, text string, blocks []any) error

PostSlackBlocksThread posts a Block Kit message threaded under threadTS (when non-empty) via the shared chat.postMessage path — the same door PostSlackBlocks uses, plus in-thread delivery so a coding result lands under the triggering @hanzo message.

func RegisterIngress added in v1.801.115

func RegisterIngress(fn func(context.Context, IngressEvent))

RegisterIngress installs the ingress consumer. Called once at channels.Mount.

func SendDiscord added in v1.801.115

func SendDiscord(ctx context.Context, channelID, replyTo, text string) (string, error)

SendDiscord posts text to a Discord channel via POST /channels/{id}/messages, referencing replyTo when non-empty. Content is capped at discordMaxContent; the bot token rides only the Authorization header, which is never logged. Returns the created message id.

func SendSlack added in v1.801.115

func SendSlack(ctx context.Context, org, channel, threadTS, text string) error

SendSlack posts text to channel, threaded under threadTS when non-empty (slackPostThread, slack_events.go — posts top-level chat.postMessage when threadTS == ""). The token is the org's OWN custodied bot token: TokenFor fails closed for unmounted/unknown/not-connected/KMS-down, so an org that never connected Slack cannot post — the per-org token IS the tenancy gate.

func SendTeams added in v1.801.115

func SendTeams(ctx context.Context, serviceURL, conversationID, text string) error

SendTeams posts a message activity to conversationID at the Bot Connector serviceURL (teamsSendActivity, teams_events.go).

func SendTelegram added in v1.801.115

func SendTelegram(ctx context.Context, chatID, replyTo int64, text string) error

SendTelegram posts text to chatID via the Bot API sendMessage, threaded under replyTo when non-zero (telegramSend, telegram_events.go).

func SetCodingDispatcher added in v1.801.23

func SetCodingDispatcher(d coding.Dispatcher)

SetCodingDispatcher injects the coding orchestrator. Called once at wiring time by the composition root; production never reassigns it.

func Shutdown

func Shutdown(_ context.Context) error

Shutdown closes the store. Idempotent — safe when nothing is mounted.

func TokenFor

func TokenFor(ctx context.Context, org, provider, name string) ([]byte, error)

TokenFor returns a custodied secret for a CONNECTED (org,provider). It fails closed: unmounted, invalid org, unknown provider, not-connected, or KMS-down each return an error and NEVER a value.

Types

type Connection

type Connection struct {
	Org          string
	Provider     string
	ExternalID   string
	AccountLabel string
	BotUserID    string
	Scopes       []string
	ConnectedAt  int64
	UpdatedAt    int64
}

Connection is an org's non-secret link to a provider account. The token itself is NEVER here — it lives in KMS, keyed by (org,provider); this row holds only the metadata needed to render the card and route inbound events.

func ConnectionFor

func ConnectionFor(org, provider string) (Connection, bool)

ConnectionFor returns an org's non-secret connection metadata for a provider.

NOTE (single contract deviation): the contract names this seam `Connection`, but Go forbids a func and a type sharing an identifier and `Connection` is the domain-noun TYPE (used by SyncHook/WritebackHook, the store, and this return value). The accessor is therefore `ConnectionFor` — the idiomatic Go name for "the Connection for (org,provider)". The bridge calls integrations.ConnectionFor.

type ExchangeResult

type ExchangeResult struct {
	Tokens       map[string]string // secret name -> value (sealed into KMS)
	ExternalID   string            // provider account id (Slack team.id / GitHub installation_id)
	AccountLabel string            // human label (Slack team.name / GitHub org login)
	BotUserID    string            // Slack bot_user_id (non-secret)
	Scopes       []string          // granted scopes
}

ExchangeResult is what a provider's Exchange returns after trading the OAuth code for tokens. Tokens is a map of KMS secret-name -> secret-value; each entry is sealed into the org's KMS namespace. ExternalID/AccountLabel/BotUserID/Scopes are NON-secret and land in the connection row.

type Inbound added in v1.801.30

type Inbound struct {
	Provider   string // registry slug: "slack","teams","discord","telegram"
	ExternalID string // workspace/tenant/guild/chat id → OrgForExternalID (isolation root)
	User       string // platform-verified user id (billing/attribution subject via the link)
	Channel    string // reply target (channel/conversation/chat id)
	ThreadID   string // thread/message id to reply under (optional)
	Text       string // the user's prompt, mention stripped
	DedupeKey  string // event/update/interaction id ("" ⇒ non-dedupable)
}

Inbound is the normalized inbound chat event — ONE shape for every platform. An adapter produces it AFTER it has authenticated the request and parsed the payload. The core never sees a raw platform payload.

type IngressEvent added in v1.801.115

type IngressEvent struct {
	Org       string
	In        Inbound
	ReplyRoot string
}

IngressEvent is one authenticated inbound chat event crossing the seam. Org is resolved via OrgForExternalID on a signature-verified payload; In is the adapter-normalized Inbound (bridge.go); ReplyRoot is a transport-verified reply root (Teams: the JWT-verified serviceURL; "" elsewhere).

type OAuthConfig

type OAuthConfig struct {
	ClientID     string
	ClientSecret string
	Extra        map[string]string
}

OAuthConfig is a provider's resolved APP credentials, read from ENV at request time by the provider's Creds func. ClientID/ClientSecret cover OAuth2; Extra carries provider-specific non-secret config (e.g. the GitHub App slug). It is NEVER persisted — it lives only for the duration of one authorize/exchange.

type Provider

type Provider struct {
	ID           string   // stable slug, the :provider path segment ("slack","github")
	Name         string   // display name
	Description  string   // one-line card copy
	Category     string   // grouping ("Communication","Developer",...)
	Scopes       []string // requested scopes (display + authorize URL)
	RedirectPath string   // OAuth redirect path; MUST equal /v1/integrations/{id}/callback
	Secrets      []string // KMS secret names this provider custodies (deleted on disconnect)

	// Kind selects credential acquisition. Empty/"oauth" (default) uses the
	// 3-legged Authorize/Exchange flow. "apikey" (apiKeyKind) takes a
	// customer-held credential submitted to /connect (from `hanzo connector add`,
	// read on STDIN — never argv/URL), VERIFIES it live, and seals it to KMS; such
	// providers use Verify, not Authorize/Exchange, and have no OAuth callback.
	Kind string
	// AdminOnly gates /connect and /disconnect on the caller being an admin of its
	// OWN org (principal.IsOrgAdmin — NOT SuperAdmin), parity with the platform
	// deploy-provider adminProcedure. OAuth social/chat providers leave it false.
	AdminOnly bool
	// Verify validates an apikey credential against the provider and returns the
	// token(s) to seal + non-secret account metadata. It MUST fail closed (a
	// bad/inactive credential returns an error and NOTHING is stored) and its error
	// MUST NOT contain the credential value (it is logged). nil for oauth providers.
	Verify func(ctx context.Context, in VerifyInput) (*ExchangeResult, error)

	// Configured reports whether the provider's APP creds are present in ENV.
	// When false: available=false in the card, and connect/callback fail closed
	// with an honest 503 / failure redirect (never a dead-end, never a fake OK).
	Configured func() bool
	// Creds resolves the APP creds from ENV. Called only when Configured is true.
	Creds func() OAuthConfig
	// Authorize builds the provider's consent URL for (creds, redirectURI, state).
	Authorize func(creds OAuthConfig, redirectURI, state string) (string, error)
	// Exchange trades the OAuth code for tokens + account metadata.
	Exchange func(ctx context.Context, creds OAuthConfig, redirectURI, code string) (*ExchangeResult, error)
	// Revoke best-effort invalidates a token at the provider on disconnect. nil
	// when the provider has no revoke endpoint.
	Revoke func(ctx context.Context, creds OAuthConfig, token string) error

	// #51 seams — declared, nil today, not wired to a route (see SyncHook/WritebackHook).
	Sync      SyncHook
	Writeback WritebackHook
}

Provider is one connectable third-party. Everything provider-specific is a field here so the handlers stay provider-blind. The func fields read ENV at call time (not at init), so an operator can inject creds without a rebuild.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the integrations database. ONE SQLite file ({DataDir}/integrations.db) holds every org's connections + in-flight OAuth nonces; tenancy is the org column (PK includes it).

func (*Store) ClaimNonce added in v1.801.30

func (s *Store) ClaimNonce(ctx context.Context, nonce, provider string) (string, bool, error)

ClaimNonce atomically resolves the org bound to a nonce for `provider` and consumes it (single-use), returning the org. Unlike ConsumeNonce it does NOT know the org up front — it is the redemption side of a deep-link connect code (Telegram): the code arrives in a webhook that has not yet resolved a tenant, so the code ITSELF carries the org. found=false (nil error) when the code is unknown/already-claimed. The Store opens with MaxOpenConns(1) (store.go), so this read-then-delete pair runs with no concurrent writer — the DELETE's RowsAffected is the single-use proof.

func (*Store) Close

func (s *Store) Close() error

func (*Store) ConsumeNonce

func (s *Store) ConsumeNonce(ctx context.Context, nonce, org, provider string) (bool, error)

ConsumeNonce atomically deletes the nonce bound to (org,provider) and reports whether exactly one row went. A second consume (replay) or a mismatched org/provider deletes zero rows → consumed=false. This single-DELETE-with-rows- affected IS the single-use proof (no read-then-delete race).

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, org, provider string) (bool, error)

Delete removes a connection. Reports whether a row went (idempotent caller).

func (*Store) GCEvents added in v1.801.30

func (s *Store) GCEvents(ctx context.Context, before int64) (int64, error)

GCEvents reaps dedupe rows created before `before` (unix seconds), bounding the table's growth. Returns how many rows were removed. Called opportunistically from the webhook path so the table cannot accrete without bound.

func (*Store) GCNonces

func (s *Store) GCNonces(ctx context.Context, before int64) (int64, error)

GCNonces deletes nonces created before `before` (unix seconds). Returns how many were reaped. Called opportunistically on connect so abandoned flows don't accrete.

func (*Store) Get

func (s *Store) Get(ctx context.Context, org, provider string) (Connection, bool, error)

Get returns the connection for (org,provider). found=false (nil error) when there is no row; a real DB error is returned as err.

func (*Store) List

func (s *Store) List(ctx context.Context, org string) ([]Connection, error)

List returns every connection for org, newest-connected first.

func (*Store) MarkEvent added in v1.801.30

func (s *Store) MarkEvent(ctx context.Context, provider, key string) (bool, error)

MarkEvent is the atomic durable dedupe test-and-set: it inserts (provider,key) and returns fresh=true only on the FIRST sighting. A duplicate (a platform retry of the same event/update/interaction id) hits the PRIMARY KEY, the insert affects zero rows, and fresh=false. An empty key is non-dedupable (fresh) — callers only dedupe non-empty keys onto the billed path. The single INSERT ... ON CONFLICT DO NOTHING + RowsAffected IS the single-use proof (no read-then-write race). A genuine DB error is surfaced so the caller fails CLOSED (skips the run) rather than risk a double charge.

func (*Store) PutNonce

func (s *Store) PutNonce(ctx context.Context, nonce, org, provider string) error

PutNonce records a single-use OAuth nonce bound to (org,provider). A duplicate nonce (astronomically unlikely from 128 bits) is a conflict, surfaced so connect fails rather than silently overwriting an in-flight one.

func (*Store) ResolveOrgByExternalID

func (s *Store) ResolveOrgByExternalID(ctx context.Context, provider, externalID string) (string, bool, error)

ResolveOrgByExternalID maps a provider account id back to the connecting org. An empty externalID never matches (so unset/scaffold connections don't collide on ""). Ambiguity (two orgs, same external id — should not happen) resolves to the earliest-connected deterministically.

func (*Store) Upsert

func (s *Store) Upsert(ctx context.Context, c Connection) error

Upsert stores (or refreshes) a connection. On a re-connect the original connected_at is PRESERVED ("connected since"), only updated_at advances.

type SyncHook

type SyncHook func(ctx context.Context, conn Connection) error

SyncHook pulls provider-side state INTO Hanzo (e.g. a GitHub App installation's repo list). It is a #51 seam: DECLARED on Provider, nil for every provider today, and NOT wired to any route. When GitHub creds land, github.go sets this to the installation-token-minting + repo-sync implementation.

type VerifyInput added in v1.801.79

type VerifyInput struct {
	Token     string
	AccountID string
}

VerifyInput is what an apikey provider's Verify receives: the customer's credential (from the /connect body, originally read on STDIN by `hanzo connector add`) plus an OPTIONAL non-secret account hint the caller may supply when the provider's own verify response cannot disclose it (e.g. a Cloudflare least-privilege token that can list neither its own name nor its account).

type WritebackHook

type WritebackHook func(ctx context.Context, conn Connection, payload []byte) error

WritebackHook pushes Hanzo state TO the provider. It is a #51 seam: declared, nil today, not wired.

Jump to

Keyboard shortcuts

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