feat(slack): autoFlagPreExistingSlackAccess helper

Best-effort lookup of an activating member's email in the Slack
workspace. On a hit, flips slackInvited:true and stamps slackInvitedAt
without sending a fresh invite. Races against a 3s timeout and swallows
all errors so activation never blocks on Slack.

- Promotes SlackService.findUserByEmail from private to public so the
  helper can call it without a wrapper.
- New activity-log action: slack_access_auto_detected (actor = subject).
- Idempotent: short-circuits when slackInvited is already true.

Callers wired in next commit.
This commit is contained in:
Jennie Robinson Faber 2026-04-29 12:13:59 +01:00
parent 2f6a92ac61
commit b1d8cb1966
4 changed files with 263 additions and 1 deletions

View file

@ -0,0 +1,61 @@
// Spec: docs/specs/wave-based-slack-onboarding.md
//
// Auto-detect existing Slack workspace membership for a Ghost Guild member
// and flag them as `slackInvited` without sending a duplicate invite.
//
// Contract:
// - Caller awaits this helper, but it never throws — all errors swallowed.
// - Internal lookup races against a 3s timeout (resolves null on hang).
// - No-op if Slack service isn't configured, lookup misses, or member is
// already flagged.
import Member from '../models/member.js'
import { getSlackService } from './slack.ts'
const LOOKUP_TIMEOUT_MS = 3000
export async function autoFlagPreExistingSlackAccess(member) {
try {
if (!member || !member._id || !member.email) return
if (member.slackInvited === true) return
const slackService = getSlackService()
if (!slackService) return
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(null), LOOKUP_TIMEOUT_MS)
})
let userId = null
try {
userId = await Promise.race([
slackService.findUserByEmail(member.email),
timeoutPromise
])
} catch (err) {
console.error('[slackAccess] findUserByEmail failed:', err)
return
}
if (!userId) return
await Member.findByIdAndUpdate(
member._id,
{
slackInvited: true,
slackInvitedAt: new Date(),
slackUserId: userId
},
{ runValidators: false }
)
await logActivity(
member._id,
'slack_access_auto_detected',
{ slackUserId: userId },
{ visibility: 'member' }
)
} catch (err) {
console.error('[slackAccess] autoFlagPreExistingSlackAccess error:', err)
}
}