integrations

package
v1.786.131 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 28 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.Tenant (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 Mount

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

Mount wires /v1/integrations/* onto app.

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 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 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)

	// 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) 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) 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) GCSlackEvents added in v1.786.82

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

GCSlackEvents 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) 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) MarkSlackEvent added in v1.786.82

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

MarkSlackEvent is the atomic durable dedupe test-and-set: it inserts event_key and returns fresh=true only on the FIRST sighting. A duplicate (a Slack retry of the same event_id / slash trigger_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), mirroring the store's ConsumeNonce. 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 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