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
- Variables
- func EnsureSchema(ctx context.Context, db *mongo.Database) error
- func FirstMatchingLabel(allowlist, added []string) string
- func MatchAuthor(allowlist []string, login string) bool
- func MatchEvent(allowlist []string, kind string, defaults ...string) bool
- func MatchLabel(allowlist []string, label string) bool
- func MatchProject(allowlist []string, projectPath string) bool
- func MintToken() (plaintext, hash, last4, fingerprint string, err error)
- func ParseSlashCommand(body string) (cmd, args string)
- func SealHMACSecret(sealer secrets.Sealer, webhookID, plaintext string) ([]byte, error)
- func VerifyHMACSignature(sealer secrets.Sealer, webhookID string, sealed, body []byte, ...) bool
- func VerifyToken(presented, storedHash string) bool
- type CommandDiscovery
- type CommandRoute
- type Config
- type ConfigStore
- type Counter
- type Delivery
- type DeliveryStore
- type Limits
- type MemoryConfigStore
- func (s *MemoryConfigStore) Create(_ context.Context, c Config) error
- func (s *MemoryConfigStore) Delete(_ context.Context, id string) error
- func (s *MemoryConfigStore) Get(_ context.Context, id string) (Config, error)
- func (s *MemoryConfigStore) ListByTenant(_ context.Context, tenantID string) ([]Config, error)
- func (s *MemoryConfigStore) MarkUsed(_ context.Context, id string, t time.Time) error
- func (s *MemoryConfigStore) Update(_ context.Context, c Config) error
- type MemoryCounter
- type MemoryDeliveryStore
- func (s *MemoryDeliveryStore) GetByIdempotencyKey(_ context.Context, key string) (Delivery, error)
- func (s *MemoryDeliveryStore) Insert(_ context.Context, d Delivery) error
- func (s *MemoryDeliveryStore) ListByWebhook(_ context.Context, tenantID, webhookID string, limit int) ([]Delivery, error)
- func (s *MemoryDeliveryStore) Update(_ context.Context, d Delivery) error
- type MongoConfigStore
- func (s *MongoConfigStore) Create(ctx context.Context, c Config) error
- func (s *MongoConfigStore) Delete(ctx context.Context, id string) error
- func (s *MongoConfigStore) Get(ctx context.Context, id string) (Config, error)
- func (s *MongoConfigStore) ListByTenant(ctx context.Context, tenantID string) ([]Config, error)
- func (s *MongoConfigStore) MarkUsed(ctx context.Context, id string, t time.Time) error
- func (s *MongoConfigStore) Update(ctx context.Context, c Config) error
- type MongoCounter
- type MongoDeliveryStore
- func (s *MongoDeliveryStore) GetByIdempotencyKey(ctx context.Context, key string) (Delivery, error)
- func (s *MongoDeliveryStore) Insert(ctx context.Context, d Delivery) error
- func (s *MongoDeliveryStore) ListByWebhook(ctx context.Context, tenantID, webhookID string, limit int) ([]Delivery, error)
- func (s *MongoDeliveryStore) Update(ctx context.Context, d Delivery) error
- type MongoStores
- type Provider
- type Rate
- type SignatureMode
Constants ¶
const ( StatusAccepted = "accepted" StatusDuplicate = "duplicate" StatusRateLimited = "rate_limited" StatusQuotaExceeded = "quota_exceeded" StatusInvalid = "invalid" StatusFiltered = "filtered" StatusLaunched = "launched" StatusLaunchError = "launch_error" )
Delivery status values.
const DeliveryTTLDays = 90
DeliveryTTLDays caps how long delivery audit rows are retained.
const TokenPrefix = "iwh_"
TokenPrefix marks an iterion webhook token so it's recognisable in configs/logs (the secret material follows the prefix).
Variables ¶
var ( ErrNotFound = errors.New("webhooks: not found") ErrDuplicate = errors.New("webhooks: duplicate idempotency key") )
Sentinel errors. Callers compare with errors.Is.
Functions ¶
func EnsureSchema ¶
EnsureSchema creates every webhook index idempotently.
func FirstMatchingLabel ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
- 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.
- 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) 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).
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 ¶
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) ListByTenant ¶
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
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 (*MemoryDeliveryStore) Insert ¶
func (s *MemoryDeliveryStore) Insert(_ context.Context, d Delivery) error
func (*MemoryDeliveryStore) ListByWebhook ¶
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) ListByTenant ¶
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.
type MongoDeliveryStore ¶
type MongoDeliveryStore struct {
// contains filtered or unexported fields
}
func (*MongoDeliveryStore) GetByIdempotencyKey ¶
func (*MongoDeliveryStore) Insert ¶
func (s *MongoDeliveryStore) Insert(ctx context.Context, d Delivery) error
func (*MongoDeliveryStore) ListByWebhook ¶
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 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 )
Source Files
¶
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. |