Compare commits

...

1 Commits

Author SHA1 Message Date
J
9134f866a3 fix(slack): build the binding link from the web app URL, matching Lark (MUL-3666)
The Slack "link your account" prompt built its redeem link from
MULTICA_PUBLIC_URL, but /slack/bind is a web-app page — the link must use the
web app URL, not the backend/API URL. MULTICA_PUBLIC_URL is intentionally the
backend/API public URL (webhooks, daemon server_url, attachments); the Lark
replier already uses appURLFromEnv() (MULTICA_APP_URL ?? FRONTEND_ORIGIN).
Slack was never migrated, so on deployments that set FRONTEND_ORIGIN but not
MULTICA_PUBLIC_URL (e.g. dev) the binding prompt silently failed
("public url not configured") and @-mentions got no response.

Rename slack.OutboundReplierConfig.PublicURL -> AppURL and feed it
appURLFromEnv() in router.go, mirroring Lark. Backend/API-URL uses of
MULTICA_PUBLIC_URL (webhooks, attachments, daemon server_url) are unchanged.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-29 17:05:56 +08:00
3 changed files with 26 additions and 16 deletions

View File

@@ -469,10 +469,13 @@ func NewRouterWithOptions(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus
slackBindingSvc := slack.NewBindingTokenService(queries, pool)
h.SlackBindingTokens = slackBindingSvc
slackReplier := slack.NewOutboundReplier(slack.OutboundReplierConfig{
Binding: slackBindingSvc,
Decrypt: box.Open,
PublicURL: signupConfig.PublicURL,
Logger: slog.Default(),
Binding: slackBindingSvc,
Decrypt: box.Open,
// The bind link (/slack/bind) is a web-app page, so it must use the
// app URL (MULTICA_APP_URL ?? FRONTEND_ORIGIN), NOT MULTICA_PUBLIC_URL
// (the backend/API URL). Mirrors the Lark replier (appURLFromEnv).
AppURL: appURLFromEnv(),
Logger: slog.Default(),
})
channelRouter.Register(slack.TypeSlack, slack.NewSlackResolverSet(queries, pool, slackReplier))
slack.NewOutbound(queries, box.Open, slog.Default()).Register(bus)

View File

@@ -46,18 +46,25 @@ type OutboundReplier struct {
binding bindingMinter
decrypt Decrypter
newSender func(creds credentials) replySender
publicURL string
appURL string
bindingPath string
logger *slog.Logger
}
// OutboundReplierConfig configures the replier. Binding + PublicURL are required
// OutboundReplierConfig configures the replier. Binding + AppURL are required
// for the NeedsBinding prompt to work; without them the prompt is skipped (the
// offline/archived/issue notices still fire).
type OutboundReplierConfig struct {
Binding bindingMinter
Decrypt Decrypter
PublicURL string
Binding bindingMinter
Decrypt Decrypter
// AppURL is the Multica web app host the user clicks into to redeem the
// binding token (e.g. https://multica.example). It comes from MULTICA_APP_URL
// (falling back to FRONTEND_ORIGIN) and is intentionally separate from
// MULTICA_PUBLIC_URL, which is the backend/API public URL used for webhook and
// daemon-facing endpoints — the bind page (/slack/bind) is served by the web
// app, so the link must point at the app host, not the API host. Mirrors the
// Lark replier's AppURL.
AppURL string
BindingPath string // default "/slack/bind"
Logger *slog.Logger
}
@@ -81,7 +88,7 @@ func NewOutboundReplier(cfg OutboundReplierConfig) *OutboundReplier {
r := &OutboundReplier{
binding: cfg.Binding,
decrypt: cfg.Decrypt,
publicURL: strings.TrimRight(cfg.PublicURL, "/"),
appURL: strings.TrimRight(cfg.AppURL, "/"),
bindingPath: bindingPath,
logger: logger,
}
@@ -133,14 +140,14 @@ func (r *OutboundReplier) sendBindingPrompt(ctx context.Context, inst engine.Res
if r.binding == nil {
return errors.New("binding service not configured")
}
if r.publicURL == "" {
return errors.New("public url not configured")
if r.appURL == "" {
return errors.New("app url not configured")
}
token, err := r.binding.Mint(ctx, inst.WorkspaceID, inst.ID, sender)
if err != nil {
return fmt.Errorf("mint binding token: %w", err)
}
bindURL := r.publicURL + r.bindingPath + "?token=" + url.QueryEscape(token.Raw)
bindURL := r.appURL + r.bindingPath + "?token=" + url.QueryEscape(token.Raw)
// Wrap the URL as an explicit Slack link <url|label>: formatMrkdwn protects
// these from its markdown passes, so the base64url token's `_`/`-` chars are
// not mangled into italics.

View File

@@ -41,9 +41,9 @@ func (f *fakeBindingMinter) Mint(_ context.Context, ws, inst pgtype.UUID, user s
func newTestReplier(binding bindingMinter, sender replySender) *OutboundReplier {
r := NewOutboundReplier(OutboundReplierConfig{
Binding: binding,
Decrypt: nil, // identity: stored bot token is base64 plaintext
PublicURL: "https://multica.example",
Binding: binding,
Decrypt: nil, // identity: stored bot token is base64 plaintext
AppURL: "https://multica.example",
})
r.newSender = func(credentials) replySender { return sender }
return r