multica-ai/multica · error · ErrBindingNotWorkspaceMember
redeemer is not a workspace member
Error message
redeemer is not a workspace member
What it means
Lark binding sentinel error: the user redeeming the binding token is not (or no longer) a member of the target workspace. Since MUL-3515 removed the member foreign key, membership is enforced with an explicit IsWorkspaceMember check inside RedeemAndBind and BindInstallerTx, and the HTTP layer translates this error to 403.
Solutions
- Confirm the redeemer's membership in the token's target workspace and re-add them if they were removed, then request a new binding link (the old token may have expired in the meantime).
- Check that the redeem request carries the correct workspace context (the token encodes the workspace) and that the signed-in user matches the intended member.
- If membership exists but the error persists, verify the IsWorkspaceMember query's inputs (workspace_id, user_id) against the actual membership rows for drift after MUL-3515.
Example fix
// before: assuming any signed-in user can redeem
err := svc.RedeemAndBind(ctx, rawToken, sessionUser)
// -> "redeemer is not a workspace member" (403)
// after: pre-check membership and fail with actionable copy
if !members.IsWorkspaceMember(ctx, tx, tokenWorkspaceID, sessionUser.ID) {
http.Error(w, "join the workspace before linking", http.StatusForbidden)
return
}
err := svc.RedeemAndBind(ctx, rawToken, sessionUser) Defensive patterns
Strategy: validation
Validate before calling
// pre-check membership before showing/redeeming the link
if !members.IsWorkspaceMember(ctx, tx, token.WorkspaceID, user.ID) {
return respondForbidden(w, "join this workspace before linking Lark")
}
_ = larkSvc.RedeemAndBind(ctx, token, user) Try / catch
if err := larkSvc.RedeemAndBind(ctx, rawToken, user); err != nil {
if errors.Is(err, lark.ErrBindingNotWorkspaceMember) {
return respondStatus(w, http.StatusForbidden)
}
return err
} Prevention
- Show binding links only to current workspace members; re-check at redeem time since removal can happen inside the TTL.
- Keep membership rows accurate — since MUL-3515 removed the FK, drift is caught only by this explicit check.
- Include the workspace name in the 403 copy so multi-workspace users realize they clicked the wrong context.
When it happens
Trigger: Calling RedeemAndBind or BindInstallerTx after the redeemer was removed from the workspace between token mint and redemption, or when the token was minted in workspace A but redeemed by a user whose membership is in workspace B. The membership check runs inside the redemption transaction, so any non-member redeemer hits it.
Common situations: User was kicked off the workspace during the 15-minute token TTL and then clicks the stale link; user belongs to multiple workspaces and the binding URL carried the wrong workspace context; membership rows out of sync after an org change (the FK that used to catch this no longer exists).
Related errors
- slack: redeemer is not a workspace member
- wecom: redeemer is not a workspace member
- lark open_id is already bound to a different user
- ErrInstallationNotFound
- get Private Plugin status
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/924cff57c560cb73.
Report an issue: GitHub.
Appendix: source
Thrown at server/internal/integrations/lark/binding_token.go:287
// has expired. The caller must NOT distinguish those sub-cases —
// that distinction enables timing oracles for token replay races and
// adds no product value (the user sees the same "link invalid or
// expired, please request a new one" copy either way).
var ErrBindingTokenInvalid = errors.New("binding token invalid or expired")
// ErrBindingAlreadyAssigned is returned by RedeemAndBind when a
// lark_user_binding row already exists for the (installation,
// open_id) pair and points at a different Multica user. Account
// transfer must go through an explicit unbind flow; a binding token
// cannot be used to grab an already-bound open_id from another user.
var ErrBindingAlreadyAssigned = errors.New("lark open_id is already bound to a different user")
// ErrBindingNotWorkspaceMember is returned by RedeemAndBind and
// BindInstallerTx when the user is not (or no longer) a member of the
// target workspace, detected by an explicit IsWorkspaceMember check
// (MUL-3515 §4 removed the member FK that used to enforce this).
// Translated to 403 at the HTTP boundary.
var ErrBindingNotWorkspaceMember = errors.New("redeemer is not a workspace member")
func randomToken(n int) (string, error) {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return "", err
}
// URL-safe so the token embeds cleanly in the binding URL
// without escaping. RawURLEncoding drops `=` padding which is
// optional for decoders and would otherwise look ugly in
// user-visible URLs.
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func hashToken(raw string) string {
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
View on GitHub (pinned to 2c0912b6ec)