integrations

package
v1.801.360 Latest Latest
Warning

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

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

Documentation

Overview

Package integrations is how your org connects third-party accounts like Slack, and revokes them.

It is the generic, provider-agnostic OAuth connector plane for the unified Hanzo Cloud binary — the /v1/integrations surface (Slack today; GitHub scaffolded; Google / Salesforce plug into the SAME registry later) — and it 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 Connected

func Connected(ctx context.Context, org, provider string) bool

Connected reports whether org has CONNECTED provider — a BOOLEAN presence check for the observe/growth plane. It reads ONLY the existence of the org's connection row (store.Get found), scoped to the org; it NEVER touches KMS and NEVER returns the token. Nil-safe and fail-closed: an unmounted subsystem, an invalid org, an unknown provider, or a store error all yield false — never a spurious true and never a secret.

func InstallationToken

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

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

The owner is required because a GitHub App is installed PER ACCOUNT: an org that owns hanzoai, hanzo-apps and hanzo-docs holds three installations, and a token minted for one grants nothing on the others. An empty owner selects the org's single connection when it has exactly one, which is what a caller with no account in hand can correctly mean; with several it is ambiguous and fails rather than guessing.

func LinkedSubject

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 cloud.Router, 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

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)

func PostSlackBlocks

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

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

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

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

func SendDiscord

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

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

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

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 SetAutomationTrigger

func SetAutomationTrigger(f TriggerFunc)

SetAutomationTrigger wires the automations Deliver seam. Called once at the composition root, before serving.

func SetCodingDispatcher

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 Bundle

type Bundle struct{ Access, Refresh, Account string }

Bundle is an externally obtained OAuth token set (CLI local PKCE) submitted for adoption. Access/Refresh are secret; Account is a non-secret hint.

type Connection

type Connection struct {
	Org      string
	Provider string
	// Owner is the provider-side account this connection is FOR — a GitHub org
	// login, empty for a provider with one account per org. Part of the key, so
	// one Hanzo org can hold several accounts of the same provider.
	Owner        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, owner 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.

func Connections added in v1.801.350

func Connections(org, provider string) []Connection

Connections returns every account an org has connected for a provider, one per owner. A caller that must reach ALL of an org's accounts iterates this rather than naming one; a caller that already knows which account it means uses ConnectionFor.

type Connector

type Connector struct {
	Org, User, Provider, Label string
	ExternalID, AccountLabel   string
	Scopes                     []string
	ExpiresAt                  int64 // access-token expiry, unix seconds; 0 = non-expiring
	ConnectedAt, UpdatedAt     int64
}

Connector is a user's non-secret link to a provider account — the per-user sibling of Connection (the /v1/connectors plane). The credential itself lives ONLY in KMS at userPath(org,user,provider,label); this row holds metadata.

type Device

type Device struct {
	Start func(ctx context.Context) (*DeviceStart, error)
	Poll  func(ctx context.Context, g Grant) (*DevicePoll, error)
}

Device is a provider's RFC-8628-style device authorization capability.

type DevicePoll

type DevicePoll struct {
	Status   string
	Interval int64           // seconds; pollSlow and throttled pending
	Result   *ExchangeResult // pollDone only
}

DevicePoll is one poll outcome. Interval is set for pollSlow (the new poll cadence) and on server-throttled pending answers (current cadence, no upstream call). Result is set for pollDone and MUST be live-proven by the provider (a real token exchange or verify call) — saveUser trusts it. Errors returned by Device funcs never carry token or device-code material.

type DeviceStart

type DeviceStart struct {
	Code, UserCode, VerifyURL string
	Interval                  int64 // seconds between polls, raw from the provider
	ExpiresAt                 int64 // unix seconds
}

DeviceStart is the non-secret-facing half of a started device authorization. Code is the provider device handle (secret-adjacent; persisted only in the cek-encrypted grants table, never a response). Interval is the raw wire value; begin() is the sole normalizer.

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
	ExpiresAt    int64             // access-token expiry, unix seconds; 0 = non-expiring/unknown. Set by user-plane device/refresh providers; the org plane ignores it.
}

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 Grant

type Grant struct {
	ID, Org, User, Provider, Label string
	Code, UserCode                 string
	Interval                       int64 // seconds between polls
	LastPollAt                     int64 // unix seconds of the last upstream poll; 0 = never
	CreatedAt, ExpiresAt           int64 // unix seconds
}

Grant is one in-flight device authorization. Code/UserCode come from the provider; Interval (seconds) is raised by slow_down; LastPollAt gates the server-side poll throttle; ExpiresAt is enforced at read time, not only GC.

type Inbound

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

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.
	// MultiAccount marks a provider that can be connected once per PROVIDER-SIDE
	// account. A GitHub App is installed per account, so one org holding hanzoai,
	// hanzo-apps and hanzo-docs holds three connections; the account name is then
	// part of the key and each row carries its own installation. A provider with
	// one account per org leaves this false and keeps an empty owner, which is what
	// its callers look up.
	MultiAccount bool

	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.
	// On the user plane it doubles as the token/apikey intake method.
	Verify func(ctx context.Context, in VerifyInput) (*ExchangeResult, error)

	// Scope selects the custody plane: "" = org-scoped /v1/integrations (default);
	// userScope = per-user /v1/connectors (rows keyed (org,user,provider,label),
	// KMS under /orgs/{org}/users/{user}/connectors/...). The planes are disjoint:
	// a user-scoped provider 404s on the org surface and vice versa; Mount asserts
	// scope coherence at boot.
	Scope string
	// Device: device-code sign-in (user scope). nil = unsupported.
	Device *Device
	// Adopt verifies an externally obtained OAuth bundle (CLI local PKCE) before
	// custody. Implementations MUST live-verify (e.g. one refresh) and return the
	// rotated material — custody owns the canonical refresh token afterwards.
	// nil = unsupported.
	Adopt func(ctx context.Context, b Bundle) (*ExchangeResult, error)
	// Refresh trades a refresh token for rotated material. The result MUST carry
	// Secrets[0] and refreshSecret entries and an ExpiresAt. nil = static credential.
	Refresh func(ctx context.Context, refresh string) (*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).
	// Org plane only — nil for Scope == userScope (nothing on the user plane
	// calls it; list() skips user providers before providerViewFor).
	Configured func() bool
	// Creds resolves the APP creds from ENV. Called only when Configured is true.
	// Org plane only — nil for Scope == userScope.
	Creds func() OAuthConfig
	// Authorize builds the provider's consent URL for (creds, redirectURI, state).
	Authorize func(creds OAuthConfig, redirectURI, state string) (string, error)
	// AuthorizeReady reports whether the Authorize leg has the credentials it
	// needs. OPTIONAL: when nil the leg is standard OAuth2 and readiness is
	// ClientID being set. A provider whose authorize leg is NOT OAuth sets this
	// so the gate asks the provider instead of assuming — a GitHub App install
	// URL is built from the app slug and has no client id at all, so without this
	// its connect flow is refused for a credential it never uses.
	AuthorizeReady func() bool
	// 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

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

func (s *Store) CountConnectors(ctx context.Context, org, user, provider string) (int, error)

CountConnectors counts (org,user,provider) rows — the maxConnectors intake cap.

func (*Store) Delete

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

Delete disconnects a provider for an org, removing EVERY owner's connection. Disconnecting is a statement about the provider, not about one of its accounts. Reports whether any row went (idempotent caller).

func (*Store) DeleteConnector

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

DeleteConnector removes a connector. Reports whether a row went (idempotent caller).

func (*Store) DeleteGrant

func (s *Store) DeleteGrant(ctx context.Context, id string) error

DeleteGrant forgets a grant (terminal poll outcomes; idempotent).

func (*Store) GCEvents

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

func (s *Store) GCGrants(ctx context.Context, now int64) (int64, error)

GCGrants deletes expired grants. Returns how many were reaped. Called opportunistically on device start so abandoned flows don't accrete (GCNonces parity).

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, owner string) (Connection, bool, error)

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

The owner is part of the key, not a filter: a provider installed per account has one connection per account, and asking without naming one cannot have a single right answer once an org holds more than one.

func (*Store) GetConnector

func (s *Store) GetConnector(ctx context.Context, org, user, provider, label string) (Connector, bool, error)

GetConnector returns the connector for (org,user,provider,label). found=false (nil error) when there is no row.

func (*Store) GetGrant

func (s *Store) GetGrant(ctx context.Context, id, org, user string) (Grant, bool, error)

GetGrant is (id,org,user)-scoped — another tenant's poll of a known id is found=false — and enforces the TTL in SQL (expires_at > now), so an expired grant is indistinguishable from an unknown one at read time.

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

func (s *Store) ListConnectors(ctx context.Context, org, user string) ([]Connector, error)

ListConnectors returns every connector for (org,user), newest-connected first, with a deterministic (provider,label) tiebreak.

func (*Store) ListFor added in v1.801.350

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

ListFor returns every connection an org holds for one provider — one per owner. This is what a caller iterates when it must reach all of an org's accounts, such as listing the repositories of every connected GitHub org.

func (*Store) MarkEvent

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

func (s *Store) PutGrant(ctx context.Context, g Grant) error

PutGrant records a started device authorization. A duplicate 128-bit id (astronomically unlikely) is a conflict, surfaced so the flow fails rather than silently overwriting an in-flight one (PutNonce parity).

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

func (s *Store) SetGrantInterval(ctx context.Context, id string, sec int64) error

SetGrantInterval records a provider slow_down bump so every later poll honors the raised cadence.

func (*Store) TouchGrant

func (s *Store) TouchGrant(ctx context.Context, id string, now int64) error

TouchGrant sets last_poll_at — the server-side poll-throttle clock. Tests pass 0 to rewind the throttle.

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.

func (*Store) UpsertConnector

func (s *Store) UpsertConnector(ctx context.Context, c Connector) error

UpsertConnector stores (or refreshes) a connector. On a re-connect or token refresh the original connected_at is PRESERVED ("connected since"), only the metadata and updated_at advance — Upsert (connections) parity.

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 TriggerFunc

type TriggerFunc func(ctx context.Context, org, source, name, dedupeKey string, depth int, payload map[string]any) (int, error)

TriggerFunc delivers a signature-VERIFIED inbound event to the automations engine. org is the resolved tenant (OrgForExternalID / a verified principal) — never a client-supplied field. depth is the causation depth (0 for an external-origin webhook). It returns how many flows the event started.

type VerifyInput

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