webhooks

package
v1.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package webhooks is iterion's inbound-webhook spine: long-lived, per-org webhook tokens that authenticate an external caller (a forge, CI, a script) and authorize it to launch a configured set of bots.

It is the first long-lived-token concept in iterion (operator auth is short-lived JWT + refresh). Tokens are shown once and stored only as a salted hash + last4 + fingerprint, mirroring the invitation/session token pattern in pkg/auth.

This package is provider-agnostic; the GitLab merge-request handler that consumes it lives in pkg/webhooks/gitlab + the server route.

Index

Constants

View Source
const (
	StatusAccepted      = "accepted"
	StatusDuplicate     = "duplicate"
	StatusRateLimited   = "rate_limited"
	StatusQuotaExceeded = "quota_exceeded"
	StatusInvalid       = "invalid"
	StatusFiltered      = "filtered"
	StatusLaunched      = "launched"
	StatusLaunchError   = "launch_error"
)

Delivery status values.

View Source
const DeliveryTTLDays = 90

DeliveryTTLDays caps how long delivery audit rows are retained.

View Source
const TokenPrefix = "iwh_"

TokenPrefix marks an iterion webhook token so it's recognisable in configs/logs (the secret material follows the prefix).

Variables

View Source
var (
	ErrNotFound  = errors.New("webhooks: not found")
	ErrDuplicate = errors.New("webhooks: duplicate idempotency key")
)

Sentinel errors. Callers compare with errors.Is.

Functions

func EnsureSchema

func EnsureSchema(ctx context.Context, db *mongo.Database) error

EnsureSchema creates every webhook index idempotently.

func FirstMatchingLabel

func FirstMatchingLabel(allowlist, added []string) string

FirstMatchingLabel returns the first freshly-applied label that passes the allowlist — the trigger — or "" when none qualifies. With an empty allowlist any added label qualifies (the first is returned). Used by the GitLab issues path, where one event can add several labels at once (changes.labels diff).

func MatchAuthor

func MatchAuthor(allowlist []string, login string) bool

MatchAuthor is the canonical PR/MR author-login allowlist matcher used by every provider call site (github/gitlab/forgejo). An empty allowlist allows any author. Matching is case-insensitive and trims surrounding space, so a webhook scoped to ["dependabot[bot]", "renovate[bot]"] reacts to a dependency bot's PRs while ignoring human PRs on the same repo. A "*" entry matches all (explicit allow-all). An empty login never matches a non-empty allowlist (an author we couldn't identify is not on the list).

func MatchEvent

func MatchEvent(allowlist []string, kind string, defaults ...string) bool

MatchEvent is the canonical event-kind allowlist matcher used by every provider call site (gitlab/github/forgejo). When allowlist is non-empty it accepts kind iff the list contains kind or "*". When empty the provider's defaults take over — variadic so each call site stays explicit about the zero-config contract:

  • gitlab: MatchEvent(list, kind, "merge_request", "note") — both the auto-review (MR open/reopen) and the on-demand /revi note trigger reach a zero-config webhook.
  • github / forgejo: MatchEvent(list, kind, "pull_request") — the only event V1 handles.

Operators who want to gate one off list the other explicitly (e.g. ["merge_request"] disables /revi while keeping auto-review).

func MatchLabel

func MatchLabel(allowlist []string, label string) bool

MatchLabel reports whether a freshly-applied issue label triggers a launch under this webhook's LabelAllowlist. An empty allowlist means "any label triggers" (the operator gates by which events the forge hook subscribes to instead). Matching is case-insensitive and trims space, so ["implement"] reacts to a "Implement" / "implement" label. A "*" entry is an explicit allow-all; an empty applied label never matches a non-empty allowlist (an unlabeled/edited event carries no label to match).

func MatchProject

func MatchProject(allowlist []string, projectPath string) bool

MatchProject is the canonical project-path allowlist matcher shared by every provider call site (gitlab/github/forgejo) and the generic JSON webhook in pkg/server. An empty allowlist allows every project in the tenant. Each entry supports:

  • a bare "*" (match all),
  • a trailing "/*" prefix wildcard ("group/*" matches "group/anything" and "group/sub/repo"),
  • otherwise an exact match.

func MintToken

func MintToken() (plaintext, hash, last4, fingerprint string, err error)

MintToken returns a fresh webhook token plaintext (shown to the operator exactly once) plus the at-rest fields persisted on a Config: a salted hash, the last 4 chars, and a fingerprint. Reuses the same random-token + hash primitives as operator session/invitation tokens.

func ParseSlashCommand

func ParseSlashCommand(body string) (cmd, args string)

ParseSlashCommand extracts a leading slash-command from a comment / note body, e.g. "/featurly add an export endpoint" → ("featurly", "add an export endpoint"). Returns ("", "") when the body does not start with a command.

The match is case-insensitive (the command id is lowercased) and tolerant of the noise forge UIs prepend: blank lines and quote-reply lines (">", a GitLab/GitHub quote of an earlier comment) are skipped, so a quote-reply that leads with the quoted text still finds the operator's command on the first real line. The first non-blank, non-quote line decides: if it doesn't start with "/" there is no command.

This is the single command grammar shared by every comment surface — gitlab.ParsedNote.Command, prforge.ParsedNote.Command, and the native board comment handler all delegate here so a "/command" parses identically everywhere.

func SealHMACSecret

func SealHMACSecret(sealer secrets.Sealer, webhookID, plaintext string) ([]byte, error)

SealHMACSecret seals the iwh_ plaintext for an hmac-mode webhook, so the same value can later be used to recompute the body HMAC without keeping cleartext at rest. The plaintext is the same minted token the operator pastes into the forge's "secret" field.

func VerifyHMACSignature

func VerifyHMACSignature(sealer secrets.Sealer, webhookID string, sealed, body []byte, signatureHex string) bool

VerifyHMACSignature recomputes HMAC-SHA256(body, plaintext) and constant-time compares the hex digest against the presented value. The presented value MAY carry a `sha256=` prefix (GitHub convention); we strip it before decoding. Malformed or empty inputs (no sealed secret, no signature, non-hex digest, length mismatch) return false so the caller can use this as a single boolean gate; it never panics.

func VerifyToken

func VerifyToken(presented, storedHash string) bool

VerifyToken constant-time compares a presented token against a stored hash. Constant-time avoids leaking a near-miss via timing.

Types

type CommandDiscovery

type CommandDiscovery interface {
	// LookupCommand returns the route an enabled bot declares for cmd
	// (lowercase, no leading slash), or ok=false when none does.
	LookupCommand(cmd string) (CommandRoute, bool)
}

CommandDiscovery is the live fallback resolver used when a webhook carries no provisioned CommandMap entry for a slash-command. It lets a hand-created WILDCARD webhook still route `/featurly`-style commands by asking the bot registry which enabled bot claims the command. Implemented in pkg/server by a botregistry-backed adapter; nil disables the fallback entirely.

type CommandRoute

type CommandRoute struct {
	BotID          string            `bson:"bot_id" json:"bot_id"`
	Mode           string            `bson:"mode,omitempty" json:"mode,omitempty"` // "direct" | "board" (empty = direct)
	ArgsVar        string            `bson:"args_var,omitempty" json:"args_var,omitempty"`
	ContextVars    map[string]string `bson:"context_vars,omitempty" json:"context_vars,omitempty"`
	Scope          string            `bson:"scope,omitempty" json:"scope,omitempty"` // "pr" | "issue" | "any" (empty = pr)
	MinReplierRole string            `bson:"min_replier_role,omitempty" json:"min_replier_role,omitempty"`
	Disambiguator  string            `bson:"disambiguator,omitempty" json:"disambiguator,omitempty"`

	// OpensMR mirrors the bot manifest command's opens_mr flag: when set, a
	// board-mode dispatch of this command stamps open_mr="true" +
	// source_issue_ref=<subject URL/ref> into the materialised card's bot_args
	// so the routed bot opens an MR and back-links the issue the human
	// commented on. Off for read-only commands (e.g. /revi).
	OpensMR bool `bson:"opens_mr,omitempty" json:"opens_mr,omitempty"`
}

CommandRoute records how a webhook routes one /slash-command to a bot and its execution mode. Mirrors the bot's manifest command invocation (bundle.InvocationCommand + the invocation's mode/args_var/context_vars), flattened by the orchestrator so a comment handler dispatches without touching the bot bundle.

func ResolveCommandRoute

func ResolveCommandRoute(cfg Config, cmd, args string, discovery CommandDiscovery) (CommandRoute, bool)

ResolveCommandRoute resolves a /slash-command to a route, the shared entry point every provider's comment handler calls after parsing a note.

Resolution order:

  1. The per-webhook CommandMap (the provisioned, scoped index). This is authoritative: for a NON-wildcard webhook an unknown command must NOT silently resolve to some other bot, so we stop here.
  2. Only when the webhook is wildcard, a live discovery fallback — and the resolved bot must still pass AllowsBot (defence in depth; a wildcard webhook allows any bot, but this keeps the contract explicit).

ok=false means nothing matched; the caller filters the delivery (200, never 4xx, so the forge doesn't auto-disable the webhook).

func (CommandRoute) AllowsScope

func (r CommandRoute) AllowsScope(surface string) bool

AllowsScope reports whether this route may fire for a comment on the given surface ("pr" or "issue"). An empty route scope defaults to "pr" (matching today's /revi-on-MR behaviour); "any" matches both.

type Config

type Config struct {
	ID          string        `bson:"_id" json:"id"`
	TenantID    string        `bson:"tenant_id" json:"tenant_id"`
	Name        string        `bson:"name" json:"name"`
	Provider    Provider      `bson:"provider" json:"provider"`
	SignMode    SignatureMode `bson:"sign_mode,omitempty" json:"sign_mode,omitempty"`
	Enabled     bool          `bson:"enabled" json:"enabled"`
	TokenHash   string        `bson:"token_hash" json:"-"`
	TokenLast4  string        `bson:"token_last4" json:"token_last4"`
	Fingerprint string        `bson:"fingerprint,omitempty" json:"fingerprint,omitempty"`

	// HMACSecretSealed holds the sealed plaintext used to recompute the
	// body HMAC for hmac-mode providers (GitHub, Forgejo). Same plaintext
	// as the minted iwh_ token — the operator pastes it once into the
	// forge's "secret" field. Empty for token-mode webhooks. Sealed via
	// secrets.Sealer with AAD bound to the webhook ID so a sealed blob
	// cannot be silently transplanted across configs.
	HMACSecretSealed []byte `bson:"hmac_secret_sealed,omitempty" json:"-"`

	// Bot scoping. BotIDs lists the allowed bot names; WildcardBots
	// (BotIDs == ["*"]) permits any bot and must be set explicitly so
	// the UI + audit can flag it.
	BotIDs       []string `bson:"bot_ids" json:"bot_ids"`
	WildcardBots bool     `bson:"wildcard_bots,omitempty" json:"wildcard_bots,omitempty"`
	DefaultBotID string   `bson:"default_bot_id,omitempty" json:"default_bot_id,omitempty"`

	// CommandMap routes a /slash-command (lowercase key, no leading slash) to
	// the bot(s) that claim it. Computed by the forge orchestrator from the
	// co-enabled bots' manifest invocations (kind=command), so a comment
	// handler resolves a command in O(1) without loading bundles on the hot
	// path. Aliases are flattened into the map (each alias is its own key).
	// The value is a slice because two bots may share a command via
	// args-based disambiguation (the review-pr vs revi-converse pattern);
	// ResolveCommand picks by whether args are present. Empty for
	// hand-created webhooks — those fall back to a live registry resolve
	// (ResolveCommandRoute) only when WildcardBots is set.
	CommandMap map[string][]CommandRoute `bson:"command_map,omitempty" json:"command_map,omitempty"`

	// Source allowlists (empty = allow-all within the tenant).
	ProjectAllowlist []string `bson:"project_allowlist,omitempty" json:"project_allowlist,omitempty"`
	EventAllowlist   []string `bson:"event_allowlist,omitempty" json:"event_allowlist,omitempty"`
	// AuthorAllowlist restricts which PR/MR author logins trigger a launch
	// (empty = any author). Case-insensitive; entries may be bot logins like
	// "dependabot[bot]" / "renovate[bot]". Lets a webhook react ONLY to a
	// dependency-bot's PRs while ignoring human PRs on the same repo.
	AuthorAllowlist []string `bson:"author_allowlist,omitempty" json:"author_allowlist,omitempty"`
	// LabelAllowlist restricts which freshly-applied issue label triggers a
	// launch on the GitHub/Forgejo `issues` (labeled) path (e.g.
	// ["implement"] so only that label dispatches the bot). Empty = any
	// label triggers. Case-insensitive; see MatchLabel. Has no effect on the
	// pull_request / issue_comment paths.
	LabelAllowlist []string `bson:"label_allowlist,omitempty" json:"label_allowlist,omitempty"`

	// BranchImproveAsPR changes how the branch-improvement bot (Billy) lands
	// its hardening on a PR it reviews. Default (false): it commits + pushes
	// directly onto the PR's own source branch (in-place — the author merges
	// its PR and gets the improvements with it). True: Billy instead opens a
	// SEPARATE PR targeting that source branch, so the author reviews the bot's
	// changes as an isolated diff before integrating them — the right posture
	// for a third-party contributor's work (they stay in control of their
	// branch). Routes Billy through open_mr=true + mr_base=<source branch>
	// instead of the direct push-back.
	BranchImproveAsPR bool `bson:"branch_improve_as_pr,omitempty" json:"branch_improve_as_pr,omitempty"`

	// AutoImplementOnOpen, when true, dispatches the implementer bot on a
	// freshly-OPENED issue (not only a labeled one) — the zero-touch lane where
	// iterion turns every new issue into a PR without a manual label. OFF by
	// default: labeling an issue (LabelAllowlist) stays the deliberate opt-in,
	// so enabling this is a per-webhook decision to auto-act on ALL new issues.
	// The labeled path keeps working alongside it. The opened lane is
	// author-gated (see MinAuthorRole): an untrusted author's issue is
	// filtered here and parks on the board for approval instead.
	AutoImplementOnOpen bool `bson:"auto_implement_on_open,omitempty" json:"auto_implement_on_open,omitempty"`

	// MinAuthorRole is the minimum repo role (gitlab vocabulary: guest|
	// reporter|developer|maintainer|owner; "" → developer ≡ write) the ISSUE
	// AUTHOR must hold for the AutoImplementOnOpen zero-touch lane to launch
	// — the budget boundary against drive-by issues. Trust resolves as:
	// AuthorAllowlist ∪ GitHub author_association fast path ∪ live
	// CollaboratorPermission (needs a forge_token binding). The labeled lane
	// is NOT author-gated: applying the trigger label already requires
	// triage+ rights on the forge, which IS the approval gesture.
	MinAuthorRole string `bson:"min_author_role,omitempty" json:"min_author_role,omitempty"`

	// BlockForkPRs, when true, filters (never auto-launches ANY bot on) a PR
	// whose head branch lives in a DIFFERENT repo than its base — a fork PR.
	// The anti budget-exhaustion boundary: a fork PR is untrusted (an adversary
	// can open many to trigger costly bot runs), so an operator must validate it
	// before a bot runs. Off by default (fork PRs still auto-review via Revi;
	// the mutating branch-improve bot never runs on a fork regardless — see
	// selectForgePRBot). Recommended ON for a public repo.
	BlockForkPRs bool `bson:"block_fork_prs,omitempty" json:"block_fork_prs,omitempty"`

	// ForgeBaseURL, when set, pins the forge instance this webhook's bot
	// token may call back to (e.g. "https://gitlab.example.com"). The
	// inbound payload's MR-URL host must match it or the delivery is
	// refused, so a hostile (but secret-authenticated) payload can't
	// redirect the bot's forge_token to an arbitrary host. Empty = derive
	// the host from the payload, still gated by the optional global
	// ITERION_WEBHOOK_FORGE_HOSTS allowlist. GitLab note/MR flows only.
	ForgeBaseURL string `bson:"forge_base_url,omitempty" json:"forge_base_url,omitempty"`

	// Limits.
	RateLimit        Rate `bson:"rate_limit" json:"rate_limit"`
	MonthlyCallLimit int  `bson:"monthly_call_limit,omitempty" json:"monthly_call_limit,omitempty"` // 0 = inherit org

	// LaunchVars are stamped onto every run launched through this webhook
	// (e.g. severity_threshold), overriding the handler-derived vars.
	LaunchVars map[string]string `bson:"launch_vars,omitempty" json:"launch_vars,omitempty"`

	// KeyOverrides pins a BYOK key per LLM provider for runs launched
	// through this webhook (provider name → api_key id), overriding the
	// org/user default in secrets.Resolve. Lets several webhooks for the
	// same bot bill against different keys. See docs/byok.md.
	KeyOverrides map[string]string `bson:"key_overrides,omitempty" json:"key_overrides,omitempty"`

	// SecretOverrides pins a specific stored secret per workflow-secret name
	// (name -> secret id) for runs launched through this webhook, overriding
	// the org bot-secret binding in secrets.ResolveGenericWithBindings. Lets
	// several webhooks for the same bot post under different forge tokens /
	// bot identities. See docs/byok.md.
	SecretOverrides map[string]string `bson:"secret_overrides,omitempty" json:"secret_overrides,omitempty"`

	// AuthorizedRepliers + MinReplierRole gate who may "talk back" to the bot
	// via a note (a /revi command or a reply): a note author is authorized
	// when they are in AuthorizedRepliers (usernames with/without @, or numeric
	// ids) OR a project member at >= MinReplierRole (guest|reporter|developer|
	// maintainer|owner; empty → developer). See docs/forge-conversations.md.
	AuthorizedRepliers []string `bson:"authorized_repliers,omitempty" json:"authorized_repliers,omitempty"`
	MinReplierRole     string   `bson:"min_replier_role,omitempty" json:"min_replier_role,omitempty"`

	// ProvisionedBy marks a config the forge Integrations orchestrator
	// created + owns (value "forge:<connection_id>"), as opposed to one an
	// operator hand-created. Non-empty configs are managed: the CRUD layer
	// blocks direct delete (the operator disables the integration instead)
	// and the studio Webhooks tab renders them read-only with a "Managed via
	// Integrations" pill. Empty = a normal operator-created webhook (the
	// default; every pre-existing row decodes to "" and behaves as before).
	ProvisionedBy string `bson:"provisioned_by,omitempty" json:"provisioned_by,omitempty"`

	CreatedBy  string     `bson:"created_by" json:"created_by"`
	CreatedAt  time.Time  `bson:"created_at" json:"created_at"`
	UpdatedAt  time.Time  `bson:"updated_at" json:"updated_at"`
	LastUsedAt *time.Time `bson:"last_used_at,omitempty" json:"last_used_at,omitempty"`
	RotatedAt  *time.Time `bson:"rotated_at,omitempty" json:"rotated_at,omitempty"`
}

Config is a per-org inbound webhook. The token plaintext is returned exactly once at create/rotate; only TokenHash/TokenLast4/Fingerprint persist.

func (*Config) AllowsBot

func (c *Config) AllowsBot(botID string) bool

AllowsBot reports whether this webhook may launch botID.

func (*Config) ResolveCommand

func (c *Config) ResolveCommand(cmd, args string) (CommandRoute, bool)

ResolveCommand resolves a /slash-command on this webhook to a single route, picking by args-presence when two bots share the command via disambiguation (when_args_present claims "/cmd <args>", when_args_empty claims a bare "/cmd"). ok=false means no route is configured for cmd (the caller may fall back to a live registry resolve for a wildcard webhook).

func (*Config) SelectBot

func (c *Config) SelectBot() string

SelectBot resolves which bot a delivery should launch: the explicit default, else the sole allowed bot. Returns "" when ambiguous (the caller decides per-provider, e.g. GitLab V1 pins Revi).

type ConfigStore

type ConfigStore interface {
	Create(ctx context.Context, c Config) error
	Get(ctx context.Context, id string) (Config, error)
	Update(ctx context.Context, c Config) error
	Delete(ctx context.Context, id string) error
	ListByTenant(ctx context.Context, tenantID string) ([]Config, error)
	MarkUsed(ctx context.Context, id string, t time.Time) error
}

ConfigStore persists webhook configs. Get is intentionally NOT tenant-scoped (the inbound auth path resolves the tenant FROM the webhook, so it has no tenant context yet); the HTTP CRUD layer enforces tenant ownership before mutating. All other reads are by explicit tenant.

type Counter

type Counter interface {
	Allow(ctx context.Context, tenantID, webhookID string, when time.Time, limits Limits) (bool, error)
	OrgCount(ctx context.Context, tenantID string, when time.Time) (int, error)
}

Counter enforces per-org (and optional per-webhook) monthly call quotas. Allow atomically increments the current month's counters and reports whether the call is within every applicable cap; a denied call does NOT consume quota.

type Delivery

type Delivery struct {
	ID             string   `bson:"_id" json:"id"`
	TenantID       string   `bson:"tenant_id" json:"tenant_id"`
	WebhookID      string   `bson:"webhook_id" json:"webhook_id"`
	Provider       Provider `bson:"provider" json:"provider"`
	IdempotencyKey string   `bson:"idempotency_key" json:"idempotency_key"`

	EventKind   string `bson:"event_kind,omitempty" json:"event_kind,omitempty"`
	EventAction string `bson:"event_action,omitempty" json:"event_action,omitempty"`
	ProjectPath string `bson:"project_path,omitempty" json:"project_path,omitempty"`
	SubjectID   string `bson:"subject_id,omitempty" json:"subject_id,omitempty"`
	SubjectSHA  string `bson:"subject_sha,omitempty" json:"subject_sha,omitempty"`
	PayloadHash string `bson:"payload_hash,omitempty" json:"payload_hash,omitempty"`

	Status     string     `bson:"status" json:"status"`
	BotID      string     `bson:"bot_id,omitempty" json:"bot_id,omitempty"`
	RunID      string     `bson:"run_id,omitempty" json:"run_id,omitempty"`
	Error      string     `bson:"error,omitempty" json:"error,omitempty"`
	SourceIP   string     `bson:"source_ip,omitempty" json:"source_ip,omitempty"`
	ReceivedAt time.Time  `bson:"received_at" json:"received_at"`
	LaunchedAt *time.Time `bson:"launched_at,omitempty" json:"launched_at,omitempty"`
}

Delivery records an inbound webhook delivery for audit + idempotency. It NEVER stores the raw payload — only a hash and the selected fields.

type DeliveryStore

type DeliveryStore interface {
	Insert(ctx context.Context, d Delivery) error
	GetByIdempotencyKey(ctx context.Context, key string) (Delivery, error)
	Update(ctx context.Context, d Delivery) error
	ListByWebhook(ctx context.Context, tenantID, webhookID string, limit int) ([]Delivery, error)
}

DeliveryStore records deliveries for audit + idempotent replay suppression. Insert returns ErrDuplicate when IdempotencyKey already exists — that unique constraint is the durable dedupe.

type Limits

type Limits struct {
	PerWebhookMonthly int
	PerOrgMonthly     int
}

Limits are the monthly call caps applied to a delivery. Zero means "no cap at that level".

type MemoryConfigStore

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

MemoryConfigStore is an in-process ConfigStore for tests and local mode. Keep its semantics in lock-step with MongoConfigStore.

func NewMemoryConfigStore

func NewMemoryConfigStore() *MemoryConfigStore

func (*MemoryConfigStore) Create

func (s *MemoryConfigStore) Create(_ context.Context, c Config) error

func (*MemoryConfigStore) Delete

func (s *MemoryConfigStore) Delete(_ context.Context, id string) error

func (*MemoryConfigStore) Get

func (*MemoryConfigStore) ListByTenant

func (s *MemoryConfigStore) ListByTenant(_ context.Context, tenantID string) ([]Config, error)

func (*MemoryConfigStore) MarkUsed

func (s *MemoryConfigStore) MarkUsed(_ context.Context, id string, t time.Time) error

func (*MemoryConfigStore) Update

func (s *MemoryConfigStore) Update(_ context.Context, c Config) error

type MemoryCounter

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

MemoryCounter is an in-process monthly Counter. Production uses the Mongo CAS variant; this one is mutex-serialised.

func NewMemoryCounter

func NewMemoryCounter() *MemoryCounter

func (*MemoryCounter) Allow

func (c *MemoryCounter) Allow(_ context.Context, tenantID, webhookID string, when time.Time, lim Limits) (bool, error)

func (*MemoryCounter) OrgCount

func (c *MemoryCounter) OrgCount(_ context.Context, tenantID string, when time.Time) (int, error)

type MemoryDeliveryStore

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

MemoryDeliveryStore is an in-process DeliveryStore.

func NewMemoryDeliveryStore

func NewMemoryDeliveryStore() *MemoryDeliveryStore

func (*MemoryDeliveryStore) GetByIdempotencyKey

func (s *MemoryDeliveryStore) GetByIdempotencyKey(_ context.Context, key string) (Delivery, error)

func (*MemoryDeliveryStore) Insert

func (*MemoryDeliveryStore) ListByWebhook

func (s *MemoryDeliveryStore) ListByWebhook(_ context.Context, tenantID, webhookID string, limit int) ([]Delivery, error)

func (*MemoryDeliveryStore) Update

type MongoConfigStore

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

func (*MongoConfigStore) Create

func (s *MongoConfigStore) Create(ctx context.Context, c Config) error

func (*MongoConfigStore) Delete

func (s *MongoConfigStore) Delete(ctx context.Context, id string) error

func (*MongoConfigStore) Get

func (s *MongoConfigStore) Get(ctx context.Context, id string) (Config, error)

func (*MongoConfigStore) ListByTenant

func (s *MongoConfigStore) ListByTenant(ctx context.Context, tenantID string) ([]Config, error)

func (*MongoConfigStore) MarkUsed

func (s *MongoConfigStore) MarkUsed(ctx context.Context, id string, t time.Time) error

func (*MongoConfigStore) Update

func (s *MongoConfigStore) Update(ctx context.Context, c Config) error

type MongoCounter

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

func (*MongoCounter) Allow

func (s *MongoCounter) Allow(ctx context.Context, tenantID, webhookID string, when time.Time, lim Limits) (bool, error)

Allow increments the org (and optional per-webhook) monthly counters and rolls back + denies when a cap is breached. Counters are eventually consistent under heavy concurrency (a denied call rolls back its increment); the allow/deny decision is atomic per findOneAndUpdate, which is the property a monthly call cap needs.

func (*MongoCounter) OrgCount

func (s *MongoCounter) OrgCount(ctx context.Context, tenantID string, when time.Time) (int, error)

type MongoDeliveryStore

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

func (*MongoDeliveryStore) GetByIdempotencyKey

func (s *MongoDeliveryStore) GetByIdempotencyKey(ctx context.Context, key string) (Delivery, error)

func (*MongoDeliveryStore) Insert

func (s *MongoDeliveryStore) Insert(ctx context.Context, d Delivery) error

func (*MongoDeliveryStore) ListByWebhook

func (s *MongoDeliveryStore) ListByWebhook(ctx context.Context, tenantID, webhookID string, limit int) ([]Delivery, error)

func (*MongoDeliveryStore) Update

func (s *MongoDeliveryStore) Update(ctx context.Context, d Delivery) error

type MongoStores

type MongoStores struct {
	Configs    *MongoConfigStore
	Deliveries *MongoDeliveryStore
	Counter    *MongoCounter
}

MongoStores bundles the three Mongo-backed stores over one database (reuse via the cloud store's DB() accessor). Each sub-store satisfies one interface; they are split because ConfigStore + DeliveryStore both declare an Update method.

func NewMongoStores

func NewMongoStores(db *mongo.Database) *MongoStores

type Provider

type Provider string

Provider identifies the external event source.

const (
	ProviderGitLab  Provider = "gitlab"
	ProviderGitHub  Provider = "github"
	ProviderForgejo Provider = "forgejo" // same wire shape as Gitea
	ProviderGeneric Provider = "generic"
)

type Rate

type Rate struct {
	Rate  float64 `bson:"rate" json:"rate"`   // sustained tokens/second
	Burst float64 `bson:"burst" json:"burst"` // bucket capacity
}

Rate is a token-bucket rate limit for a webhook.

type SignatureMode

type SignatureMode string

SignatureMode selects how an inbound delivery proves authenticity.

"token" (the default — empty string) means the forge presents the minted iwh_ plaintext in a header; the middleware does a constant-time hash compare. GitLab's "secret token" model + iterion's own X-Iterion-Webhook-Token fall under this mode.

"hmac" means the forge sends a hex HMAC-SHA256 of the raw request body computed with the SAME minted iwh_ plaintext as the key. The provider handler verifies the signature itself BEFORE acting on the body. The middleware MUST NOT touch the body (so we keep the bytes for signature recomputation) and MUST skip the header-token check (GitHub/Forgejo don't echo the token in any header). The plaintext is sealed at-rest on cfg.HMACSecretSealed so we can recompute the signature without storing it in cleartext.

const (
	SignModeToken SignatureMode = ""     // header-presented bearer
	SignModeHMAC  SignatureMode = "hmac" // X-*-Signature over body
)

Directories

Path Synopsis
Package generic decodes the bot-agnostic JSON shape iterion accepts on /api/webhooks/generic/{id}.
Package generic decodes the bot-agnostic JSON shape iterion accepts on /api/webhooks/generic/{id}.
Package gitlab decodes GitLab webhook payloads into the narrow, normalized shape iterion's inbound handler consumes.
Package gitlab decodes GitLab webhook payloads into the narrow, normalized shape iterion's inbound handler consumes.
Package prforge decodes pull_request webhook payloads from PR-over-forge providers — GitHub and Forgejo/Gitea — which share the same wire shape for the pull_request event.
Package prforge decodes pull_request webhook payloads from PR-over-forge providers — GitHub and Forgejo/Gitea — which share the same wire shape for the pull_request event.

Jump to

Keyboard shortcuts

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