bot_api

package
v1.8.1 Latest Latest
Warning

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

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

Documentation

Overview

Module-side composer that wires bot_api.groupService.GetMembers + bot_api.robotService.ExistRobot into the pkg/mentionrewrite.ExpandAisToBotUIDs callback shape.

See modules/message/mention_expand.go for the design rationale — the leaf helper stays free of any modules/* dependency to preserve the import-cycle-free invariant called out in pkg/mentionrewrite/rewrite.go. Each ingress chokepoint owns its own thin composer.

Package bot_api · YUJ-1166 / Mininglamp-OSS/octo-server#81 — Persona Clone OBO REST endpoints.

These endpoints are mounted under /v1/obo behind the standard user-auth middleware (ba.ctx.AuthMiddleware) — they take a USER token, not a bot token. The acting user must be the grantor of the row they CRUD; we do NOT support cross-user persona management in v0 (RFC §2 / out-of-scope).

Status code map (kept narrow on purpose):

200 — success (single object or list)
400 — bad request body / missing required fields
401 — no user token (handled by upstream middleware)
403 — (reserved — production currently uses 404 for cross-user attempts
       as a user-enumeration defense; see requireOwnedGrant comment.)
404 — grant_id / scope_id not found; cross-user grant/scope access
       (existence-leak defense)
409 — duplicate (grantor+grantee already exists / scope already exists,
       with no soft-deleted row to reactivate in place)
500 — DB error

Package bot_api · YUJ-1166 / Mininglamp-OSS/octo-server#81 — Persona Clone authorization check used by sendMessage / stream endpoints.

checkOBO is the single boolean question on the dispatch hot path: "is bot B allowed to act as grantor G in (channel_id, channel_type)?". It is intentionally a thin wrapper over oboStore so:

  • the HTTP handler stays tiny (build req → check → dispatch);
  • unit tests can swap a fake oboStore without standing up MySQL;
  • future cache-aware variants (e.g. negative cache) can land here without touching the handler.

Package bot_api · YUJ-1166 / Mininglamp-OSS/octo-server#81 — Persona Clone (On-Behalf-Of) v0 data layer.

Backing tables: obo_grants, obo_scopes (see SQL migration 20260519000001_obo_v0.sql). Public surface is the oboStore interface so HTTP handlers, checkOBO, and the fan-out listener can all be unit-tested against an in-memory fake without sqlmock plumbing.

Cache strategy (RFC §11 risk row): the two hot-path questions are answered by short-TTL Redis keys, populated on read-through, invalidated on write:

  • obo:grantor:{uid} "1" any active grant exists for grantor; "0" no active grant. Read by findActiveGrantByGrantorBot — negative answer short-circuits the (grantor, bot) MySQL probe that checkOBO would otherwise issue per send.
  • obo:chan:{ctype}:{cid} "1" channel has at least one (active grant × enabled scope) match; "0" no match. Read by findActiveGrantsForChannel — negative answer short-circuits the JOIN that the fan-out listener would otherwise issue per inbound message system-wide.

Both keys are negative-cache friendly: a "0" answer returned within the 30-second TTL eliminates the MySQL round-trip entirely. Writes that can flip either answer (insertGrant / updateGrant / revokeGrant / insertScope / deleteScope) invalidate the affected keys inline. Stale "1" answers are safe — callers still consult MySQL when the cache says "1", so the cache cannot grant authorization it shouldn't. Stale "0" answers cap at 30s and are acceptable per RFC §11 (risk explicitly accepted for v0). Redis is best-effort throughout: a Redis outage silently degrades to the pre-cache path (full MySQL load), never to a permissions regression.

Package bot_api · YUJ-1166 / Mininglamp-OSS/octo-server#81 — Persona Clone fan-out hook.

Hook design (RFC §5.3): we register a MessagesListener on the shared context — same pattern the robot, botfather, thread, and message modules already use — so the fan-out happens AFTER WuKongIM has persisted the inbound message but BEFORE we deliver the copy. This matches "candidate 1" in the RFC and keeps the listener side-effect free with respect to the original message.

The listener pulls grants by (channel_id, channel_type) — a single index hit per inbound message — then applies the three loop-protection gates from RFC §5.3:

Gate 1: bot self-sent → never replay to that same bot
Gate 2: grantor's own outbound → don't fan it to the grantor's bot
        (covers the "I typed on my phone" case — bot should not echo)
Gate 3: already-OBO-processed → message_extra has __obo_processed__=true
        (the bot's outbound, marked by sendMessage, must not bounce)

PR#82 review #2 P1-2: gate 3's marker key is `__obo_processed__` (double- underscore reserved prefix), NOT the v0-shipped `obo_processed`. The v0 key was a plain JSON field that any bot could set on its own /v1/bot/sendMessage payload — letting a bot suppress its own fan-out by crafting `{"content":"…", "obo_processed":true}`. The new key sits in a reserved namespace (`__obo_*`) that sendMessage strips off inbound payloads (see send.go) before processing, so the marker is now server-only state. Compatibility note: messages persisted under the legacy key during the v0 testing window are NOT honored — gate 3 is strict on the new name. Any in-flight v0 messages would only suppress their own fan-out (a bounded edge case) and the test suite is the only caller that ever wrote the legacy key in this branch.

For each surviving (message, grant) pair we build a MsgSendReq addressed to the grantee bot's own PERSONAL mailbox (ChannelID=grantee_bot_uid, ChannelType=Person, Subscribers OMITTED). The original delivery to real users is untouched.

PR#82 review #5 P0 — WuKongIM /message/send contract: `channel_id` and `subscribers` are MUTUALLY EXCLUSIVE on a single MsgSendReq. The v0 implementation set BOTH (ChannelID = origin conversation, Subscribers = [granteeBot]) and WuKongIM rejected every dispatch with:

【message】channelId和subscribers不能同时存在!

The "OBO fan-out dispatch failed" line in im-test prod showed every inbound message tripping this. Fix: address the fan-out copy at the bot's personal mailbox and drop Subscribers. The original conversation context is preserved in the payload's `obo_origin_*` fields so the bot (and any downstream consumer) can still reason about where the message originated. We go through octo-lib's `NewPersonalMsgSendReq` builder so the PERSONAL DM authoritative-payload contract (Mininglamp-OSS#37) is preserved and the `tools/lint-personal-msgsendreq` invariant holds.

What we do NOT do here:

  • We do NOT call SendMessageWithResult (which would create a new persisted message everyone sees). The Person-channel route + NoPersist=1 gives the bot a one-shot copy via its existing subscriber pipeline (the bot is the sole subscriber of its own mailbox channel).
  • We do NOT recompute permissions; checkOBO already ran when the bot authored the message that's now bouncing, and inbound messages from real users are by definition allowed in the channel they arrived in.

PR#82 R6 P0 — The fan-out copy's FromUID is the GRANTOR uid (not the original sender). The v0 implementation used `FromUID=m.FromUID` (= the peer who sent the inbound, e.g. u_bob), so for DMs WuKongIM observed a (FromUID=u_bob, ChannelID=granteeBotUID) PERSONAL message and synced the conversation pair `u_bob ↔ granteeBot` to **u_bob's** client — leaking the persona-clone bot into bob's conversation list even though bob only ever spoke to admin. The whole point of "managed persona" is that bob sees ONLY admin as the counterparty; the bot is strictly behind admin's identity.

The fix routes the fan-out copy as "admin (grantor) forwarding to the bot's own mailbox". WuKongIM then syncs the pair `admin ↔ granteeBot` only — which is semantically correct because admin owns the bot (admin is the grantor in the OBO grant row) and the bot is admin's own managed persona. Bob is no longer in either UID of the fan-out copy and therefore cannot see the bot at all. The bot still learns who actually spoke via `obo_origin_from_uid` in the payload.

Package bot_api · YUJ-1166 — PR#82 R6 P0 friend-gate OBO bypass.

Background: the bot send / typing / readReceipt / messages-sync paths require the bot to be a friend of the target user (BotKindUser DM branch). That rule is correct for "bot user is talking directly to you" — the user must opt in.

It is **wrong** for the managed-persona / OBO path. When admin grants the persona-clone bot james OBO authority over the DM pair admin↔bob, bob has chosen to DM admin (not james); admin's clone is expected to reply as admin. Requiring a `bot↔bob` friend row would either:

  1. force the server to silently fabricate the friendship behind the real user's back — leaking the bot's existence to bob (R6 P0 Bug A in reverse: bob sees bot as a contact), OR
  2. block every managed-persona reply with `bot is not a friend of this user` — which is what im-test 2026-05-19 surfaced for james.

The fix is a server-side conditional bypass: when the request carries a validated OBO context (i.e. the bot is asking to dispatch as a real grantor via the `on_behalf_of` field on sendMessage), and the friend gate would otherwise reject the DM, we check whether an active OBO grant authorises the bot to operate as some grantor in that channel and the grantor still has a valid relation with the target. If so, the friendship requirement is satisfied transitively by the grantor's relation — the bot is acting as the grantor, the grantor is friends with the target, so the send is legitimate.

PR#82 R7 — the bypass is **gated on the caller passing hasOBOContext=true**. The bot adapter sets this when the inbound request carries `on_behalf_of`; the actual `checkOBO` validation (grant + scope + grantor-still-has-access) runs immediately after, so a bot that lies about the header gets short-circuited there. Critically, requests that omit `on_behalf_of` (sendMessage as the bot itself, typing, readReceipt, messages/sync) MUST NOT consult the OBO bypass — otherwise a bot with any unrelated grant covering a target user could dispatch directly bot→target and expose itself as a contact, defeating the user opt-in friend gate. See Jerry-Xin's R7 review on head a07b372 for the regression details.

Hot-path cost (only paid when hasOBOContext=true): one cached `findActiveGrantsForChannel` lookup (the same one the fan-out listener consults — already negative-cached via `obo:chan:*`) PLUS a per-matching-grant `grantorCanReadChannel` re-check. For the common case (no OBO grant covers the target channel) the negative cache makes this a single Redis GET. Requests without OBO context skip the bypass entirely and go straight back to `isFriend`.

Package bot_api · YUJ-644 / Mininglamp-OSS#33 / YUJ-660 / YUJ-688

PERSONAL DM 派发前为 payload 注入 Bot 的权威 SpaceID。WuKongIM 在 DM 上仅按 裸 uid 路由(无 Space 概念),收端客户端 SpaceFilter 唯一可信信号源是 payload.space_id;任何客户端上送的值都不可信,必须服务端覆盖。

解析顺序(自上而下,最快路径优先):

  1. App Bot scope=space —— 直接读 gin-context 里 authAppBot 写入的 CtxKeyAppBotSpaceID(O(1),无 DB 调用)。
  2. 其它情况(User Bot、App Bot scope=platform)—— 用 querySpaceIDByRobotID 查 space_member ⨝ space。结果为空表示 Bot 当前没有归属 Space(孤儿 Bot 或非 Space 部署)。

失败模式:

  • 真实 DB 错误 → warn + 不阻断发送(注入是优化,缺失走 fail-closed strip)。
  • dbr.ErrNotFound(零结果)→ 视为"Bot 没有归属 Space",不写 false-positive DB 错误日志,fall through 到 strip-or-warn 分支。

**YUJ-660 R3 Finding A — fail-closed strip语义(HIGH 修复)**:当 resolver 返 回 ""(任何原因:孤儿 Bot / DB 错误 / ErrNotFound),enrichBotPayloadWithSpaceID **必须删除** payload["space_id"],并 emit `client_space_id_stripped=true` 监控 warn(如果 client 上送过非空值);payload 本就没有 space_id 时 emit `enrich_payload_space_id_empty=true`。

之前版本在 resolver 返回 "" 时保留 client payload —— 攻击者可以构造 DB 错误条 件(或孤儿 Bot 触发条件)伪造 payload.space_id="victim_space" 通过派发,realtime + offline push 都会信任这个值。strip 是唯一 fail-closed 行为;message 层 R2 High-3 strip 只在 sendMsg 路径生效,bot_api / robot 路径需要本层独立 strip。

**YUJ-688 / PR#43 R1 fix-up — platform App Bot validator gap (Critical from Jerry-Xin + lml2468)**: the X-Space-ID validator previously checked only `space_member`. Platform App Bots are inserted in `app_bot` with `scope='platform'` and never get a `space_member` row, yet they are legitimately visible in every active Space (`pkg/space/query.go:99`). Result: every valid platform App Bot dispatch with a valid X-Space-ID header was rejected by the validator and the caller's strip path downgraded the payload, sending the message as a personal DM with no SpaceID. The fix renames `isBotSpaceMember` to `isBotSpaceAuthorized` and broadens the SQL to honor the production `app_bot` rows (platform OR scope=space-with-match). Trim whitespace from the header value first to avoid noisy reject logs from " space_X " / trailing CR.

Index

Constants

View Source
const (
	BotKindUser = "user" // User Bot (bf_ token, robot table)
	BotKindApp  = "app"  // App Bot (app_ token, app_bot table)
)

BotKind identifies the type of authenticated bot.

View Source
const (
	CtxKeyRobotID       = "robot_id"
	CtxKeyBotKind       = "bot_kind"
	CtxKeyRobot         = "robot"         // *robotModel for User Bot
	CtxKeyAppBotScope   = "app_bot_scope" // "platform" | "space"
	CtxKeyAppBotSpaceID = "app_bot_space_id"
)

Context keys for bot identity.

Variables

View Source
var (
	// ErrOBONotAuthorized — no active+globally-enabled grant exists OR the
	// scope row for the channel is missing/disabled. Returned for both
	// "grant never existed" and "grant revoked" so callers can't probe.
	ErrOBONotAuthorized = errors.New("obo not authorized")
)

Sentinel errors returned by checkOBO. Handlers map them to user-visible strings (and HTTP status); production logs include the underlying detail.

Functions

func SetAppBotRegistry

func SetAppBotRegistry(r AppBotRegistryInterface)

SetAppBotRegistry sets the global App Bot registry (called by app_bot module). A nil r is allowed (clears the slot back to "no registry").

Types

type AppBotRegistryAdapter

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

AppBotRegistryAdapter adapts an external registry to AppBotRegistryInterface. The app_bot module sets this on startup.

func NewAppBotRegistryAdapter

func NewAppBotRegistryAdapter() *AppBotRegistryAdapter

NewAppBotRegistryAdapter creates a new adapter.

func (*AppBotRegistryAdapter) Add

func (a *AppBotRegistryAdapter) Add(token string, spec *AppBotRegistrySpec)

Add adds a spec by token.

func (*AppBotRegistryAdapter) FindByToken

func (a *AppBotRegistryAdapter) FindByToken(token string) *AppBotRegistrySpec

FindByToken looks up spec by token.

func (*AppBotRegistryAdapter) Remove

func (a *AppBotRegistryAdapter) Remove(token string)

Remove removes a spec by token.

func (*AppBotRegistryAdapter) Update

func (a *AppBotRegistryAdapter) Update(oldToken, newToken string, spec *AppBotRegistrySpec)

Update atomically replaces a spec by old and new token.

func (*AppBotRegistryAdapter) Warm added in v1.7.0

func (a *AppBotRegistryAdapter) Warm(token string, spec *AppBotRegistrySpec)

Warm mirrors Add for the in-memory adapter: it is single-process, so the tombstone / SETNX semantics the shared Redis registry needs to close the cross-replica resurrection race don't apply here.

type AppBotRegistryInterface

type AppBotRegistryInterface interface {
	FindByToken(token string) *AppBotRegistrySpec
	// Add is an AUTHORITATIVE write (publish / rotate-new): it must establish the
	// spec, overwriting any prior value (e.g. a revocation tombstone on re-publish).
	Add(token string, spec *AppBotRegistrySpec)
	// Warm is a BEST-EFFORT, NON-BLOCKING/BOUNDED warm-up (auth-path repopulate +
	// startup load). It must never overwrite a concurrent revocation, and callers
	// may invoke it from a detached goroutine on the hot path — implementations
	// must keep it bounded (never an unbounded blocking write per call).
	Warm(token string, spec *AppBotRegistrySpec)
	Remove(token string)
	Update(oldToken, newToken string, spec *AppBotRegistrySpec)
}

AppBotRegistryInterface is the App Bot auth registry: a token -> spec lookup plus the mutators the app_bot admin handlers call on publish/rotate/unpublish/ delete. Two implementations satisfy it: AppBotRegistryAdapter (in-memory, single-process — used by unit tests) and RedisAppBotRegistry (shared write- through cache — used in production so revocations propagate across replicas; see issue #309).

FindByToken returns nil on a miss AND on any backend error, so the caller (authAppBot) falls through to the authoritative DB lookup — auth must fail safe, never serve a stale spec when the backend is degraded.

func GetAppBotRegistry

func GetAppBotRegistry() AppBotRegistryInterface

GetAppBotRegistry returns the global App Bot registry, or nil if unset/cleared.

type AppBotRegistrySpec

type AppBotRegistrySpec struct {
	UID     string
	Scope   string
	SpaceID string
}

AppBotRegistrySpec is the minimal spec needed by bot_api auth.

type BotAPI

type BotAPI struct {
	log.Log
	// contains filtered or unexported fields
}

BotAPI is the public Bot API gateway module. It handles all bot-facing endpoints (/v1/bot/*) with unified auth.

func NewBotAPI

func NewBotAPI(ctx *config.Context) *BotAPI

NewBotAPI creates the Bot API gateway module.

func (*BotAPI) Route

func (ba *BotAPI) Route(r *wkhttp.WKHttp)

Route registers all Bot API routes.

type BotEventsReq

type BotEventsReq struct {
	EventID int64 `json:"event_id"`
	Limit   int64 `json:"limit"`
}

BotEventsReq is the request for getEvents.

type BotReadReceiptReq

type BotReadReceiptReq struct {
	ChannelID   string   `json:"channel_id"`
	ChannelType uint8    `json:"channel_type"`
	MessageIDs  []string `json:"message_ids"`
}

BotReadReceiptReq is the request for readReceipt.

type BotRegisterReq

type BotRegisterReq struct {
	AgentPlatform string `json:"agent_platform"`
	AgentVersion  string `json:"agent_version"`
	PluginVersion string `json:"plugin_version"`
}

BotRegisterReq is the optional request body for register.

type BotRegisterResp

type BotRegisterResp struct {
	RobotID        string `json:"robot_id"`
	Name           string `json:"name"`
	IMToken        string `json:"im_token"`
	WSURL          string `json:"ws_url"`
	APIURL         string `json:"api_url"`
	OwnerUID       string `json:"owner_uid"`
	OwnerChannelID string `json:"owner_channel_id"`
}

BotRegisterResp is the response for bot registration.

type BotSendMessageReq

type BotSendMessageReq struct {
	ChannelID   string `json:"channel_id"`
	ChannelType uint8  `json:"channel_type"`
	StreamNo    string `json:"stream_no"`
	// OnBehalfOf — YUJ-1166 / Mininglamp-OSS/octo-server#81 (Persona Clone v0).
	// When non-empty the bot is asking to dispatch as the real user
	// `OnBehalfOf`. Server validates an active OBO grant
	// (grantor=OnBehalfOf, grantee=robotID) AND a per-channel scope row
	// (channel_id, channel_type) before substituting FromUID. Empty / absent
	// preserves legacy behavior (FromUID = robotID). See RFC §5.1 / §5.2.
	OnBehalfOf string                 `json:"on_behalf_of,omitempty"`
	Payload    map[string]interface{} `json:"payload"`
}

BotSendMessageReq is the request for sendMessage.

type BotSyncMessagesReq

type BotSyncMessagesReq struct {
	ChannelID       string `json:"channel_id"`
	ChannelType     uint8  `json:"channel_type"`
	StartMessageSeq uint32 `json:"start_message_seq"`
	EndMessageSeq   uint32 `json:"end_message_seq"`
	Limit           int    `json:"limit"`
	PullMode        int    `json:"pull_mode"`
}

BotSyncMessagesReq is the request for syncMessages.

type BotTypingReq

type BotTypingReq struct {
	ChannelID   string `json:"channel_id"`
	ChannelType uint8  `json:"channel_type"`
	OnBehalfOf  string `json:"on_behalf_of,omitempty"`
}

BotTypingReq is the request for typing.

OnBehalfOf — YUJ-1465 / Mininglamp-OSS/octo-server#108 (OBO v2). When non-empty the bot is signalling "the OBO grantor (this uid) is typing in this channel", not "the bot is typing". Server validates an active OBO grant (grantor=OnBehalfOf, grantee=robotID) AND a per-channel scope row (channel_id, channel_type) — same auth contract as /v1/bot/sendMessage — before signing the CMDTyping payload with `from_uid=OnBehalfOf` instead of the bot's uid. Empty / absent preserves the v0/v1 behaviour where typing is always attributed to the bot. See modules/bot_api/send.go / checkOBO for the grant + scope auth contract; we reuse it verbatim so a bot cannot signal typing as a grantor it cannot legitimately send as.

type FlexBoolInt added in v1.4.0

type FlexBoolInt int

FlexBoolInt — YUJ-1738 / PR#131 R2 B1.

Wire-shape adapter for fields whose on-disk representation is an integer (0/1) but whose historical clients send either a JSON boolean (true/false) OR a JSON integer (0/1). encoding/json's default *int decoder rejects boolean tokens with `json: cannot unmarshal bool into Go struct field`, which silently 400s the entire PUT — the symptom Jerry-Xin flagged on R2 where the octo-web persona toggle (sends raw {"active": false}) could never reach the handler.

Round-trip semantics:

  • JSON `true` → 1
  • JSON `false` → 0
  • JSON integer N → N (passed through; the handler still treats "non-zero ⇒ activate, zero ⇒ pause" so any non-zero integer is equivalent to `true`).
  • JSON `null` → leaves the value zero (the OUTER pointer is what carries the "field absent" semantic, see usage in oboUpdateGrantReq.Active).
  • any other shape (string, array, object, float) → typed error so the caller still gets a clean 400 instead of a silent skip.

Marshalling emits an integer so downstream consumers that already parse the response as a number keep working. The underlying type is `int` (not a struct) so callers in package-internal tests can keep using the pointer-pattern `v := FlexBoolInt(0); &v` they already use for `*int` fields like GlobalEnabled.

func (FlexBoolInt) MarshalJSON added in v1.4.0

func (f FlexBoolInt) MarshalJSON() ([]byte, error)

MarshalJSON emits the value as a JSON integer (NOT a boolean), preserving wire compatibility with the original `*int` field.

func (*FlexBoolInt) UnmarshalJSON added in v1.4.0

func (f *FlexBoolInt) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the dual boolean-or-integer decode.

type RedisAppBotRegistry added in v1.7.0

type RedisAppBotRegistry struct {
	log.Log
	// contains filtered or unexported fields
}

RedisAppBotRegistry is a SHARED, write-through Redis cache for App Bot auth (issue #309). Replacing the per-process in-memory map with one shared store makes token revocation (rotate / unpublish / delete) take effect on every replica the instant the admin request commits, instead of lingering on peer replicas until they restart.

Authority model: the app_bot table (queryAppBotByToken + status==1 gate) is the source of truth. This cache is a fast path in front of it:

  • FindByToken miss, tombstone, OR any Redis error -> nil -> authAppBot's DB fallback runs (fail safe; a Redis outage degrades to a correct, slower DB lookup, never to serving a stale/revoked spec).

Write model (the asymmetry is load-bearing):

  • Add (authoritative publish / rotate-new): SET, overwrites any tombstone.
  • Warm (best-effort warm-up: auth-path repopulate + startup load): SETNX, never overwrites a tombstone or a fresher spec; circuit-gated so a Redis outage can't pile up blocking writes on the auth hot path.
  • Remove (revoke: unpublish / delete / rotate-old): writes a short-lived TOMBSTONE (not a DEL), with bounded retry + loud-on-failure.

Revocation-resurrection is closed by the tombstone + SETNX pairing: a delayed auth-path Warm can no longer re-create a just-revoked key, because Remove left a tombstone there and SETNX won't overwrite it (and FindByToken denies on a tombstone regardless). The only residual is a failed revocation WRITE on a transient Redis error after retries — bounded by the key TTL, and moot when Redis is fully down (FindByToken then errors -> DB fallback -> rejected). The safety-net TTL (ttl(), clamped) also self-heals any orphaned key.

func NewRedisAppBotRegistry added in v1.7.0

func NewRedisAppBotRegistry(ctx *config.Context, ttl func() time.Duration) *RedisAppBotRegistry

NewRedisAppBotRegistry builds the shared registry. The Redis client is built the same way modules/opanalytics does (octoredis.MustBuildOptions over the process config). ttl supplies the safety-net key expiry; a non-positive value is coerced to a sane floor in set().

func (*RedisAppBotRegistry) Add added in v1.7.0

func (r *RedisAppBotRegistry) Add(token string, spec *AppBotRegistrySpec)

Add authoritatively write-throughs a spec (publish / rotate-new): an unconditional SET that overwrites any tombstone so a re-publish re-enables the token cluster-wide at once. Not circuit-gated — it is a low-frequency admin write that must establish authoritative state even on a slow backend.

func (*RedisAppBotRegistry) FindByToken added in v1.7.0

func (r *RedisAppBotRegistry) FindByToken(token string) *AppBotRegistrySpec

FindByToken reads the shared cache. Miss (redis.Nil) and any other Redis error both return nil so the caller falls through to the authoritative DB lookup — auth must never fail open on a degraded backend.

func (*RedisAppBotRegistry) Remove added in v1.7.0

func (r *RedisAppBotRegistry) Remove(token string)

Remove revokes a token by writing a short-lived TOMBSTONE (not a DEL), so every replica denies it immediately AND a racing Warm (SETNX) can't re-create the key. Bounded retry + loud-on-failure: a transient blip must not silently leave the token authenticating until TTL. Not circuit-gated — a revocation must always be attempted, even when the backend is degraded.

func (*RedisAppBotRegistry) Update added in v1.7.0

func (r *RedisAppBotRegistry) Update(oldToken, newToken string, spec *AppBotRegistrySpec)

Update revokes the old token (tombstone) and authoritatively write-throughs the new one. The two writes are not atomic, but each is on the shared store so peers converge immediately; the new token is brand-new (no tombstone) so the SET wins.

func (*RedisAppBotRegistry) Warm added in v1.7.0

func (r *RedisAppBotRegistry) Warm(token string, spec *AppBotRegistrySpec)

Warm is a best-effort cache warm-up (DB-fallback auth-path repopulate + startup load). It uses SETNX so it only ever fills an ABSENT key — it never overwrites a concurrent revocation's tombstone or a fresher authoritative spec, so a delayed warm-up cannot resurrect a just-revoked token. Circuit-gated so a Redis outage can't pile up blocking writes on the auth hot path.

Jump to

Keyboard shortcuts

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